从这个系列开始,尝试手写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学习记录查看。

Pasted image 20230928171742.png

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 中取值了。

  • 从测试结果可以看到的,我们的接口已经被代理类实现了,同时我们可以在代理类中进行自己的操作封装。那么在我们后续实现的数据库操作中,就可以对这部分内容进行扩展了。

说明:以上学习内容参考小傅哥的博客