前言
最近在学习mybatis源码相关的课程,有一部分是仿Mybatis手写一个持久层框架,一开始是通过传统方式进行测试 每次执行sql 都需要进行如下步骤:
- 根据配置文件的路径 加载成字节输入流 存到内存中
- 解析配置文件 封装Configuration对象 创建SqlSessionFactory工厂对象
- 生产sqlSession 创建执行器对象
- 调用sqlSession中的方法
ini
public void test() throws DocumentException {
//1.根据配置文件的路径 加载成字节输入流 存到内存中。此时配置文件还并未解析
InputStream stream = Resources.getResourceAsStream("sqlMapConfig.xml");
//2.解析了配置文件 封装了Configuration 创建了SqlSessionFactory工厂对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(stream);
//3.生产sqlSession 创建了执行器对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//4.调用sqlSession中的方法
User user = new User();
user.setId(1);
user.setUsername("tom");
User user1 = sqlSession.selectOne("user.selectOne", user);
System.out.println(user1);
}
有两个问题:
- 加载、解析配置文件,获取sqlSession代码,存在重复的问题。
- 调用sqlSession中方法的时候,statementId存在硬编码问题。
下面通过JDK动态代理,使用代理对象来进行数据操作,规避上述两个问题。
代理方式实现
创建dao层接口
创建dao层的接口,同时在接口中声明查询方法。就等同于在项目开发中的使用方式
csharp
public interface IUserDao {
/**
* 查询所有数据
*/
List<User> findAll();
/**
* 根据多条件查询数据
*/
User findByCondition(User user);
}
SqlSession添加getMapper方法
SqlSession接口中 添加getMapper方法 用来获取dao层对象的代理对象
arduino
public interface SqlSession {
//查询多个结果
<E> List<E> selectList(String statementId, Object param);
//查询单个结果
<T> T selectOne(String statementId, Object param);
//释放资源
void close();
//生成基于接口的代理对象
<T> T getMapper(Class<?> mapperClass);
}
typescript
public class DefaultSqlSession implements SqlSession {
private Configuration configuration;
private Executor executor;
public DefaultSqlSession(Configuration configuration, Executor executor) {
this.configuration = configuration;
this.executor = executor;
}
@Override
public <E> List<E> selectList(String statementId, Object param) {
//省略具体查询逻辑...
}
@Override
public <T> T selectOne(String statementId, Object param) {
//省略具体查询逻辑...
}
@Override
public void close() {
executor.close();//资源的释放 也委派给底层的执行器
}
@Override
public <T> T getMapper(Class<?> mapperClass) {
//基于JDK动态代理 生成基于接口的代理对象
Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
return ((T) proxy);
}
}
getMapper分析
java
public class DefaultSqlSession implements SqlSession {
//其它代码省略...
/*
InvocationHandler参数:
Object proxy:代理对象的引用,很少用
Method method:被调用方法的字节码对象
Object[] args:调用的方法的参数
*/
@Override
public <T> T getMapper(Class<?> mapperClass) {
//基于JDK动态代理 生成基于接口的代理对象
/*
InvocationHandler参数:
Object proxy:代理对象的引用,很少用
Method method:被调用方法的字节码对象
Object[] args:调用的方法的参数
*/
Object proxy = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//具体逻辑:执行底层JDBC代码 应当通过调用sqlsession中的方法来完成sql执行
//1、参数的准备:1.statementId(sql语句的唯一标识) 2.param(sql的参数)
/*
sql配置文件中的namespace以及sql语句的id 这里无法获取
为了成功拼接statementId,定下一个规范:
1.namespace的值 要与接口的全路径一致
2.sql语句id的值 要与接口中的方法名一致
满足上面的规范 就可以用 接口的全路径 + 方法名 拼接出statementId,找到对应的sql语句
*/
String namespace = method.getDeclaringClass().getName();
String id = method.getName();
String statementId = namespace + "." + id;
//2、接下来要调用SqlSession中的哪个方法
/*
MappedStatement中 添加属性 sqlCommandType 来标识本条sql的操作类型
*/
MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
//select update delete insert
String sqlCommandType = mappedStatement.getSqlCommandType();
switch (sqlCommandType) {
case "select":
//3.判断进行查询时 要进行selectList还是selectOne 通过method的返回值类型来判断
Type genericReturnType = method.getGenericReturnType();
//判断返回值 是否实现了 泛型类型参数化
if (genericReturnType instanceof ParameterizedType) {
if (args != null) {
return selectList(statementId, args[0]);
}
return selectList(statementId, null);
}
return selectOne(statementId, args[0]);
case "update":
break;
case "delete":
break;
case "insert":
break;
}
return null;
}
});
return ((T) proxy);
}
}
总结
通过jdk动态代理方式、在执行数据库操作时,就可以调用getMapper方法,传入一个class,获取到接口的代理对象,通过代理对象来执行数据库操作,同时 为了避免这种方式拿不到自定义的statementId,这里还引入了namespace和sql ID的命名规范。
学习过程中,觉得这部分很巧妙,所以在此记录一下。如果有不对的地方,还请各路大佬指正,谢谢各位~