
手写Mybatis之01
从这个系列开始,尝试手写Mybatis,实现Mybatis的基本功能。
思考:我们使用 JDBC 的时候,需要手动建立数据库链接、编码 SQL 语句、执行数据库操作、自己封装返回结果等。但在使用 ORM 框架后,只需要通过简单配置即可对定义的 DAO 接口进行数据库的操作了。
那么从mapper接口,到执行sql语句到底干了什么?
本节重点:解决 ORM 框架第一个关联对象接口和映射类的问题,把 DAO 接口使用代理类,包装映射操作。
其实在看到思考的时候,本能第一印象就联想到JDK动态代理,因为这实在是太符合动态代理的思路,把一系列的复杂流程封装为一个接口对象,再实例化,调用实例化对象的方法实现功能(功能增强吗?还蛮像的是不是!)
1、简单JDK动态代理复习
/**
* 使用反射调用dao下面的模拟mapper文件
*/
@Test
public void test_proxy_class(){
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IUserDao.class},
(proxy, method, args) -> "你的类被代理了!");
String result = userDao.queryUserName("123");
System.out.println("测试结果:" + result);
}
Proxy.newProxyInstance的方法讲解见下图,也可以指路之前的文章:Spring5学习记录查看。
2、把 DAO 接口使用代理类,包装映射操作
2.1 mapper的代理增强类
通过实现 InvocationHandler#invoke 代理类接口,封装操作逻辑的方式,对外接口提供数据库操作对象。
目前我们这里只是简单的封装了一个 sqlSession 的 Map 对象,你可以想象成所有的数据库语句操作,都是通过
接口名称+方法名称作为key
,操作作为逻辑的方式进行使用的。那么在反射调用中则获取对应的操作直接执行并返回结果即可。当然这还只是最核心的简化流程,后续不断补充内容后,会看到对数据库的操作另外这里要注意如果是 Object 提供的 toString、hashCode 等方法是不需要代理执行的,所以添加
Object.class.equals(method.getDeclaringClass())
判断。
package cn.sunnyy.mybatis;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/24 18:30
* 此类是mapper的代理增强类,主要作用是代理完成mapper的方法去操作数据库
* 在JDK动态代理中,增强操作是 InvocationHandler 去完成的,所以这里我们需要实现 InvocationHandler
* 还需呀实现序列化 方便后续序列化操作
*/
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = 3596583683747825122L;
/**
模拟sql方法
*/
private final Map<String, String> sqlSession;
/**
模拟需要动态代理的mapper接口
*/
private final Class<T> mapperInterface;
public MapperProxy(Map<String, String> sqlSession, Class<T> mapperInterface) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())){
// 如果方法是Objects自有的,不做增强,直接调用
return method.invoke(args);
}
// 这里的 mapperInterface.getName() + "." + method.getName() 就和mybatis中的方法名是一样的,通过类的全路径+方法名定位
return "你的被代理了!" + sqlSession.get(mapperInterface.getName() + "." + method.getName());
}
}
2.2 代理工厂
工厂操作相当于把代理的创建给封装起来了,如果不做这层封装,那么每一个创建代理类的操作,都需要自己使用
Proxy.newProxyInstance
进行处理,那么这样的操作方式就显得比较麻烦了。代理工厂,方便直接的生成代理的实例对象
package cn.sunnyy.mybatis;
import java.lang.reflect.Proxy;
import java.util.Map;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/24 18:42
* 这里是代理工厂,方便直接的生成代理的实例对象
*/
public class MapperProxyFactory<T> {
/**
模拟需要动态代理的mapper接口
*/
private final Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance(Map<String, String> sqlSession){
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
new MapperProxy<>(sqlSession, mapperInterface));
}
}
3、测试
对上面的功能实现进行测试
package cn.sunnyy.mybatis.test;
import cn.sunnyy.mybatis.MapperProxyFactory;
import cn.sunnyy.mybatis.test.dao.IUserDao;
import com.sun.org.slf4j.internal.Logger;
import com.sun.org.slf4j.internal.LoggerFactory;
import org.junit.Test;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
/**
* @author sunzhen
* @version 1.0
* @date 2024/8/24 17:01
*/
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
/**
* 使用反射调用dao下面的模拟mapper文件
*/
@Test
public void test_proxy_class(){
IUserDao userDao = (IUserDao) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IUserDao.class},
(proxy, method, args) -> "你的类被代理了!");
String result = userDao.queryUserName("123");
System.out.println("测试结果:" + result);
}
/**
* 测试使用代理工厂创建的mapper接口示例对象
*/
@Test
public void test_factory_class(){
MapperProxyFactory<IUserDao> factory = new MapperProxyFactory<>(IUserDao.class);
Map<String,String> sqlSession = new HashMap<>();
sqlSession.put("cn.sunnyy.mybatis.test.dao.IUserDao.queryUserName",
"模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名");
IUserDao userDao = factory.newInstance(sqlSession);
String result = userDao.queryUserName("zhangsan");
System.out.println("测试结果:"+result);
}
}
测试结果
测试结果:你的类被代理了!
测试结果:你的被代理了!模拟执行 Mapper.xml 中 SQL 语句的操作:查询用户姓名
在单测中创建 MapperProxyFactory 工厂,并手动给 sqlSession Map 赋值,这里的赋值相当于模拟数据库中的操作。
接下来再把赋值信息传递给代理对象实例化操作,这样就可以在我们调用具体的 DAO 方法时从 sqlSession 中取值了。
从测试结果可以看到的,我们的接口已经被代理类实现了,同时我们可以在代理类中进行自己的操作封装。那么在我们后续实现的数据库操作中,就可以对这部分内容进行扩展了。
说明:以上学习内容参考小傅哥的博客。