仿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的命名规范。

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

相关推荐
丘山子1 分钟前
Python 布尔运算的优雅实践
后端·python·面试
汪子熙14 分钟前
理解 SSH Agent 的工作原理与应用场景
后端
苏琢玉24 分钟前
如何优雅地处理多种电商优惠规则?我用 PHP 封装了一个 Promotion Engine
后端·php·composer
豌豆花下猫26 分钟前
Python 潮流周刊#113:用虚拟线程取代 async/await
后端·python·ai
武子康27 分钟前
大数据-58 Kafka 消息发送全流程详解:序列化、分区策略与自定义实现
大数据·后端·kafka
福大大架构师每日一题28 分钟前
2025-08-02:最多 K 个元素的子数组的最值之和。用go语言,给定一个整数数组 nums 和一个正整数 k,请找出所有长度最多为 k 的连续子数组,计算
后端
Debug笔记28 分钟前
你真的理解 Java 中的线程池吗?一次“查不出原因的接口变慢”的真实排查经历
后端
Cache技术分享30 分钟前
149. Java Lambda 表达式 - Lambda 表达式的序列化
前端·后端
_風箏34 分钟前
Shell【脚本 01】实现定时备份文件、压缩、删除超时文件操作(showDoc文件备份脚本举例)
后端
橘黄的猫41 分钟前
MacBook Pro 安装 Java 开发环境指南
后端