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

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

相关推荐
白总Server18 分钟前
Golang实现分布式Masscan任务调度系统
java·运维·服务器·开发语言·分布式·后端·golang
lb29171 小时前
关于golang热加载安装,实时响应
开发语言·后端·golang·热加载
羊小猪~~1 小时前
数据库学习笔记(十五)--变量与定义条件与处理程序
数据库·人工智能·笔记·后端·sql·学习·mysql
墨家巨子@俏如来1 小时前
一.干货干货!!!SpringAI入门到实战-小试牛刀
后端·springai·ai人工智能
ahhhhaaaa-1 小时前
【AI图像生成网站&Golang】部署图像生成服务(阿里云ACK+GPU实例)
开发语言·数据仓库·人工智能·后端·阿里云·golang
jdyzzy2 小时前
从0到1做一个“任务管理系统”:Spring Boot + Vue 实战教程(含源码)
vue.js·spring boot·后端
愚农搬码2 小时前
LangChain 调用不同类型的多MCP服务
人工智能·后端
我会冲击波2 小时前
推荐一款让代码命名变得轻松高效的idea插件
后端
楽码2 小时前
安装和编写grpc协议文件
服务器·后端·grpc
码农之王2 小时前
(二)TypeScript前置编译配置
前端·后端·typescript