仿Mybatis持久层框架 - mapper代理方式查询流程实现 jdk动态代理 statementId命名规范

前言

最近在学习mybatis源码相关的课程,有一部分是仿Mybatis手写一个持久层框架,一开始是通过传统方式进行测试 每次执行sql 都需要进行如下步骤:

  1. 根据配置文件的路径 加载成字节输入流 存到内存中
  2. 解析配置文件 封装Configuration对象 创建SqlSessionFactory工厂对象
  3. 生产sqlSession 创建执行器对象
  4. 调用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);
}

有两个问题:

  1. 加载、解析配置文件,获取sqlSession代码,存在重复的问题。
  2. 调用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的命名规范。

学习过程中,觉得这部分很巧妙,所以在此记录一下。如果有不对的地方,还请各路大佬指正,谢谢各位~

相关推荐
章豪Mrrey nical5 小时前
前后端分离工作详解Detailed Explanation of Frontend-Backend Separation Work
后端·前端框架·状态模式
派大鑫wink6 小时前
【JAVA学习日志】SpringBoot 参数配置:从基础到实战,解锁灵活配置新姿势
java·spring boot·后端
程序员爱钓鱼6 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
xUxIAOrUIII6 小时前
【Spring Boot】控制器Controller方法
java·spring boot·后端
Dolphin_Home6 小时前
从理论到实战:图结构在仓库关联业务中的落地(小白→中级,附完整代码)
java·spring boot·后端·spring cloud·database·广度优先·图搜索算法
zfj3216 小时前
go为什么设计成源码依赖,而不是二进制依赖
开发语言·后端·golang
weixin_462446237 小时前
使用 Go 实现 SSE 流式推送 + 打字机效果(模拟 Coze Chat)
开发语言·后端·golang
JIngJaneIL7 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小信啊啊7 小时前
Go语言切片slice
开发语言·后端·golang
Victor3569 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端