手写 Mybatis-plus 基础架构(工厂模式+ Jdk 动态代理统一生成代理 Mapper)

这里写目录标题

    • 前言
    • 温馨提示
    • [手把手带你解析 @MapperScan 源码](#手把手带你解析 @MapperScan 源码)
    • [手把手带你解析 @MapperScan 源码细节剖析](#手把手带你解析 @MapperScan 源码细节剖析)
    • [工厂模式+Jdk 代理手撕脚手架,复刻 BeanDefinitionRegistryPostProcessor](#工厂模式+Jdk 代理手撕脚手架,复刻 BeanDefinitionRegistryPostProcessor)
    • [手撕 FactoryBean](#手撕 FactoryBean)
    • [代理 Mapper 在 Spring 源码中的生成流程](#代理 Mapper 在 Spring 源码中的生成流程)
    • [手撕 MapperProxyFactory](#手撕 MapperProxyFactory)
    • [手撕增强逻辑 InvocationHandler](#手撕增强逻辑 InvocationHandler)
    • [源码级别解读 Mapper 要被设计成接口的原因](#源码级别解读 Mapper 要被设计成接口的原因)
    • [自定义 Executor 实现,框架 Dao 层](#自定义 Executor 实现,框架 Dao 层)
    • 手写基础架构效果演示
    • 总结

前言

最近在码云搜 Es 的开源项目学学技术,无意间搜到 Easy-Es 这么一个项目,里面的用法和 Mybatis-Plus 一模一样,当时心想我擦,这个人是直接悟透了 Mybatis-Plus 吗,虽然老早前看过源码。之前大概看了一下,就是对 Mapper 对象进行代理,植入了一些自定义逻辑而已,没仔细看过实现细节,现在网上居然有人直接又造了一个轮子,直呼 666,于是乎深入看了 Mybatis-Plus 是如何生成 Mapper 代理对象的全部源码,并且一比一复刻出来了。

温馨提示

阅读以下文章了解前置知识对理解本文更有帮助

  1. 深入jdk动态代理源码解析
  2. 模拟jdk动态代理(完整版)
  3. Factorybean与BeanFactory的区别
  4. 手把手debug自动装配源码、顺带弄懂了@Import等相关的源码(全文3w字、超详细)
  5. spring 源码解析(配图文讲解)顺带搞懂了循环依赖、aop底层实现

手把手带你解析 @MapperScan 源码

废话不多说直接步入正题,我们在使用 Mybatis 的时候要要设置 @MapperScan 扫描对应的 Mapper 接口,一步步点进去

发现其实就是注册了 MapperScannerConfigurer 这个 Bean ,都是些常用套路。然后发现 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor、InitializingBean。

  1. BeanDefinitionRegistryPostProcessor:Spring 为我们提供的扩展点,让程序员可以自己干预 Bean 的生成

  2. InitializingBean:在 Bean 填充属性(populateBean)完成后会调用

直接看重写了 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法就行,看下图可以看到就是利用 ClassPathMapperScanner (路径扫描器)去扫描指定包下面的类,然后生成对应的 BeanDefinition 注册到 BeanDefinitionMap 中,然后 Spring 会将 BeanDefinitionMap 中的所有 BeanDefinition 生成 Bean 放到 Spring 单例池里面提供给程序员使用。

然后来到 scan 源码,cmd+art+b 查看 doScan 实现类,点进第二个,第一个是 Spring 实现的,而我们看的是 Mybatis 的源码这里大家要注意一下!

然后你会发现扫描完 basePackages 下的类生成对应的 BeanDefinition ,后还会去处理一下这些 BeanDefinition,click 进去。

发现得到的所有 Mapper 的 BeanDefinition 的 BeanClass 都被替换成了mapperFactoryBeanClass (工厂 bean)


到这里我大概就明白了,所有的 Mapper BeanDefinition 统一设置为 MapperFactoryBean 类型,最终生成的 Bean 本质 Class 是 MapperFactoryBean 但是名字依然是原来的名字,然后通过代理工厂统一生成代理对象(这也是很多开源框架的常用套路)。接下来验证一下我的猜想。看一下 MapperFactoryBean 构造实现了 FactoryBean 。

当我们的项目中使用了如下代码时,拿到的 Bean 其实是在紧挨上图一中的 getObject 方法中创建的。

java 复制代码
@Autowired
UserMapper userMapper;

然后进入 getMapper 方法里面。看到确实是通过 MapperProxyFactory (代理工厂)生成的代理对象 Mapper。

看到这你是不是觉得源码也不过如此,对于整个简单的流程虽然走完了,但是作为一个要进行开发整个轮子的开发者来说,还远远不够。还需要了解更多细节

  1. 如何将指定包路径下的所有类生成 BeanDefinition ?。
  2. MapperProxyFactory 如何初始化,并且 MapperProxyFactory 如何根据感知生产什么类型的代理对象等

手把手带你解析 @MapperScan 源码细节剖析

这部分的文章读者可选择自行跳过,

knownMappers 中的数据什么时候初始化的?

回到 MapperFactoryBean 类中可以看到 checkDaoConfig 方法左侧有一个这个小图标,说明就是抽象接口的实现类,一般为了简化操作很多框架包括我也喜欢利用抽象接口封装逻辑

点击来到了上层的实现类,发现还被包裹了一层逻辑接着点向上的那个图标

来到最顶层的 checkDaoConfig 发现原来 MapperFactoryBean 居然实现了 InitializingBean 接口,当 MapperFactoryBean 属性填充完成以后,进行调用 afterPropertiesSet 方法,触发我们的 checkDaoConfig 方法调用。

最终会发现在进行 addMapper 的时候会以 key:mapperInterface ,value:MapperProxyFactory 的键值对放到 knownMappers 里面,而 mapperInterface 其实就UserMapper 的 Class。

Mapper 是使用什么代理创建的?

答:看一下 MapperProxyFactory 源码得知是用的 Jdk 代理,直接代理接口

如何动态的批量创建、修改 Bean ?

答:通过实现 Spring 提供的扩展接口 BeanDefinitionRegistryPostProcessor 动态注册、修改 BeanDefinition 即可。

如何实现动态的将一个普通 Bean 改成工厂 Bean ?

答:通过设置 BeanDefinition 的 BeanClass、ConstructorArgumentValues 替换成工厂 Bean 的 Class 即可。关键代码如下

javascript 复制代码
genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinition).getBeanClass());   
genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);

源码中的 mapperInterface 是什么东西?

答:Mapper 对象的 Class。举个例子当项目中用到了

javascript 复制代码
@Autowired
UserMapper userMapper;

此时的 mapperInterface 就是 UserMapper.Class

为什么 Mapper 的代理对象能转换成目标对象?

了解 Jdk 动态代理的都知道,代理对象不能转换成目标对象,只能装换成目标对象的接口实现类或者 Proxy 对象,原因就是如下,可以看到代理对象和目标对象半毛钱关系都没有。

java 复制代码
代理对象 extends proxy implments 目标对象实现接口

那为什么 UserMapper 的代理对象但是还能用 UserMapper 接收呢?项目中应该这样使用才对啊!!

javascript 复制代码
@Autowired
Proxy userMapper;

工厂模式+Jdk 代理手撕脚手架,复刻 BeanDefinitionRegistryPostProcessor

里面的逻辑主要就是扫描指定包下面的类,生成对应的 BeanDefinition,然后自定义一个我们自己的后置处理器,将所有 BeanDefinition 替换成工厂 Bean。读者可自行封装对应的后置处理器,方便其他使用者进行扩展。整个流程对标 ClassPathMapperScanner 源码中的 doScan 逻辑。

javascript 复制代码
/**
 * 扫描哪些包是 mapper,并统一设置类型为 BaseFactoryBean
 */
@Slf4j
@Component
public class RegistryPostProcessorConfig implements BeanDefinitionRegistryPostProcessor {
    private Class<? extends BaseFactoryBean> mapperFactoryBeanClass = BaseFactoryBean.class;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        //扫描指定路径下的 BeanDefinition
        Set<BeanDefinitionHolder> beanDefinitions = scan();
        if (beanDefinitions.size() >= 1) {
            //后置处理器:全部替换成工厂 Bean
            factoryBeanDefinitionPostProcess(beanDefinitions);
        }
        //注册 BeanDefinition
        register(beanDefinitions,registry);
        log.info("自定义 Mapper 扫描注册完成");
    }

    /**
     * 扫描指定包下面的类,包装成一个个的 BeanDefinitionHolder,我这里就简单写写直接指定了
     */
    public Set<BeanDefinitionHolder> scan() {
        HashSet<BeanDefinitionHolder> beanDefinitions = new HashSet<>();
        GenericBeanDefinition scanBeanDefinition = new GenericBeanDefinition();
        scanBeanDefinition.setBeanClassName("userMapper");
        scanBeanDefinition.setBeanClass(UserMapper.class);
        GenericBeanDefinition scanBeanDefinition2 = new GenericBeanDefinition();
        scanBeanDefinition2.setBeanClassName("studentMapper");
        scanBeanDefinition2.setBeanClass(StudentMapper.class);
        beanDefinitions.add(new BeanDefinitionHolder("userMapper",scanBeanDefinition));
        beanDefinitions.add(new BeanDefinitionHolder("studentMapper",scanBeanDefinition2));
        return beanDefinitions;
    }

    public void factoryBeanDefinitionPostProcess(Set<BeanDefinitionHolder> beanDefinitions) {
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
            GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
            genericBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            genericBeanDefinition.setLazyInit(false);
            /**
             * 设置 bean 创建的构造 class,必须设置不然 bean 无法被创建
             */
            genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition()).getBeanClass());
            genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);
        }
    }

    public void register(Set<BeanDefinitionHolder> beanDefinitions, BeanDefinitionRegistry registry) {
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitions) {
            /**
             * BeanDefinition 重置了 BeanClass 为 BaseFactoryBean 后,对应的 BeanClassName 会自动变成 com.zzh.service2.structure.factory.bean.BaseFactoryBean
             * 造成所有的 Mapper 接口的 BeanDefinition 的 BeanClassName 都是 com.zzh.service2.structure.factory.bean.BaseFactoryBean 导致注册报错!!!
             * 因此自定义包装 BeanDefinitionHolder 对象,设置原始 BeanName
             * 例如:BeanDefinitionHolder(key->userMapper,value->BeanDefinition)
             * BeanDefinitionHolder(key->studentMapper,value->BeanDefinition)
             */
            registry.registerBeanDefinition(beanDefinitionHolder.getBeanName(), beanDefinitionHolder.getBeanDefinition());
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

    }

}

手撕 FactoryBean

实现 FactoryBean 接口,同时设置成泛型,让任何类型的 Mapper 接口都是转换成此 FactoryBean,当 Spring 进行属性填充完成之后,进行初始化 Bean 的时候会调用 InitializingBean 接口里面的方法,此时我们将 UserMapper.Class 放到一个临时容器中,等 BaseFactoryBean.getObject 方法被调用的时候,再去容器里面拿到 UserMapper.Class 进行 Jdk 代理创建代理对象。

javascript 复制代码
@Data
public class BaseFactoryBean<T> implements FactoryBean<T>, InitializingBean {
    /**
     * > 如何实现动态的将一个普通 Bean 改成工厂 Bean ?
     * 通过设置 BeanDefinition 的 BeanClass、ConstructorArgumentValues 替换成工厂 Bean 的 Class 即可。关键代码如下
     * ```javascript
     * genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(((GenericBeanDefinition) beanDefinition).getBeanClass());
     * genericBeanDefinition.setBeanClass(mapperFactoryBeanClass);
     * ```
     */
    public BaseFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    private Class<T> mapperInterface;

    /**
     * 通过 MapperProxyFactory 工厂统一生产代理对象
     */
    @Override
    public T getObject() throws Exception {
        return (T) BaseMapperRegistry
                .getMapper(mapperInterface)
                .newInstance(DaoTemplateFactory.getInstance().getDaoTemplate());
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }


    @Override
    public void afterPropertiesSet() {
        BaseMapperRegistry.addMapper(this.mapperInterface);
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

补充一嘴 Spring 中的源码逻辑,BeanDefinitionMap 中所有的 BeanDefinition 都会走

CreateBean 的流程,先是调用 createBeanInstance 方法创建一个实例对象,然后调用 populateBean 方法为实例对象填充属性,接着才是调用 InitializingBean 里面的方法。可以看到此时的 mapperInterface 是 UserMapper.Class

代理 Mapper 在 Spring 源码中的生成流程

当创建好 UserMapper 这个 Bean 的时候,会调用 getObjectForBeanInstance 方法获取其实例,发现 UserMapper 是个工厂 Bean,于是乎调用 getObject 方法,走我们的 Jdk 创建代理对象的逻辑,最终放到 Ioc 容器里面的是我们自己创建的代理对象!

然后顺着栈帧来到 getObject,到此整个流程结束!

手撕 MapperProxyFactory

根据目标对象的 Class 生成代理对象,同时 InvocationHandler 里面织入我们手写的 DaoTemplate,用来与数据库进行交互。
亮点代码只有一行:由于目标对象自身是一个 Mapper 接口,参数二实现类的接口用的是自己本身 new Class[]{mapperInterface} 这样生成的代理对象就可以转换成目标对象了。

javascript 复制代码
T proxyInstance = (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, baseMapperProxy);
javascript 复制代码
@Slf4j
public class MapperProxyFactory<T> {
    /**
     * 被代理对象 Class
     */
    @Getter
    private final Class<T> mapperInterface;
    private ConcurrentHashMap methodCaches = new ConcurrentHashMap<Object, Object>();

    public MapperProxyFactory(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    public T newInstance(MapperProxyInvoke<T> baseMapperProxy) {
        T proxyInstance = (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, baseMapperProxy);
        log.info("proxyInstance instanceof BaseMapper:{}", proxyInstance instanceof BaseMapper);
        log.info("proxyInstance instanceof BaseMapper:{}", proxyInstance instanceof UserMapper);
        return proxyInstance;
    }

    /**
     * Mybatis-Plus 封装了 SqlSession 对象操作 db,我这里也简单封装一个 DaoTemplate 做做样子
     * @param daoTemplate
     * @return
     */
    public T newInstance(DaoTemplate daoTemplate) {
        MapperProxyInvoke<T> baseMapperProxy = new MapperProxyInvoke<T>(daoTemplate, mapperInterface, methodCaches);
        return newInstance(baseMapperProxy);
    }

    /**
     * 为啥jdk生成的代理对象居然不支持类型转换为目标对象?
     * https://blog.csdn.net/qq_42875345/article/details/115413716
     */
    public static void main(String[] args) {
        test1();
        test2();
    }

    /**
     * 强制代理对象实现 UserMapper 接口,从而实现 jdk生成的代理对象支持转换为目标对象!!!!!!!
     * 关键代码:new Class[]{UserMapper.class}
     * 这也是为什么 Mapper 要设计成接口的原因!!!!!!!
     * 代理对象结构:代理对象 extends proxy implments UserMapper
     */
    static void test1() {
        Mapper proxyInstance = (Mapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.err.println("代理前置输出");
                return null;
            }
        });
        System.err.println(proxyInstance instanceof UserMapper); //true
        System.err.println(proxyInstance instanceof BaseMapper);
        System.err.println(proxyInstance instanceof Mapper);
    }

    /**
     * 普通 Jdk 代理对象只实现目标对象的实现接口
     * 代理对象结构:代理对象 extends proxy implments 目标对象实现接口
     */
    static void test2() {
        Mapper proxyInstance = (Mapper) Proxy.newProxyInstance(UserMapper.class.getClassLoader(), UserMapper.class.getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.err.println("代理前置输出");
                return null;
            }
        });
        System.err.println(proxyInstance instanceof UserMapper); //false
        System.err.println(proxyInstance instanceof BaseMapper);
        System.err.println(proxyInstance instanceof Mapper);
    }

}

手撕增强逻辑 InvocationHandler

实现了 InvocationHandler 接口,每当代理 Mapper 中的方法被调用的时候,都会执行 invoke 中的逻辑。里面分默认方法(被 default 修饰的方法)与 db 查询的方法

javascript 复制代码
/**
 * 代理 Mapper 增强逻辑
 */
@Slf4j
public class MapperProxyInvoke<T> implements InvocationHandler, Serializable {

    private static final long serialVersionUID = -6424540398559729838L;
    private final DaoTemplate daoTemplate;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;

    public MapperProxyInvoke(DaoTemplate daoTemplate, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
        this.daoTemplate = daoTemplate;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }

    /**
     *
     * @param proxy 生成的代理对象
     * @param method 被调用的目标对象方法
     * @param args 被调用的目标对象方法中的参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("代理对象前置输出!!!!");
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (method.isDefault()) {
                /**
                 * Mapper 自带的默认方法走这调用(userMapper.say())
                 */
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        /**
         * (userMapper.seleteById(1))走这调用
         * 此处需要 Method 与 daoTemplate 中的方法名称、参数做匹配然后调用 daoTemplate 中的方法
         * 源码中也是这么干的,我懒这里直接硬编码匹配 seleteById 了
         */
        return daoTemplate.seleteById(1);
//        mybatis 源码中还做了方法缓存加快处理速度
//        final MapperMethod mapperMethod = cachedMapperMethod(method);
//        return mapperMethod.execute(sqlSession, args);
    }

    private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
            throws Throwable {
        final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
                .getDeclaredConstructor(Class.class, int.class);
        if (!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }
        final Class<?> declaringClass = method.getDeclaringClass();
        return constructor
                .newInstance(declaringClass,
                        MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                                | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
                .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
    }
}

源码级别解读 Mapper 要被设计成接口的原因

这里也贴一下 UserMapper 的代码吧,也解释一下 Mapper 为什么要采用接口的形式

javascript 复制代码
/**
 * UserMapper 只能是接口,如果 UserMapper 为类,生成的代理对象不能转换为 UserMapper
 * 只能转换为 proxy、或者 BaseMapper ,原因:代理对象 extends proxy implments BaseMapper
 * 但是我们需要 @Autowire UserMapper 这样使用。需要代理对象为 UserMapper 类型,因此 UserMapper 只能是接口
 * 让生成的代理对象 extends proxy implments UserMapper
 */
public interface UserMapper extends BaseMapper<User> {
    default String say() {
        return "UserMapper say";
    }
}

自定义 Executor 实现,框架 Dao 层

代理对象会根据被调用的方法匹配 DaoTemplate 中的方法进行执行,在这里面可以自行封装类似于 Mybatis 二级缓存,多级 Executor ,动态数据源切换的逻辑,工程量巨大,我这里只提供思路,简单查个库给大家演示一下设计原理。到此所有组件全部开发完成。

javascript 复制代码
/**
 * 封装原始的 jdbc 逻辑,可扩展组件:多级缓存查询、多级 Executor 查询、数据库连接池切换等等
 */
public class DaoTemplate {
    String driver = "com.mysql.cj.jdbc.Driver";
    String url = "url";
    String username = "root";
    String password = "pwd";

    public Connection getConnection() throws SQLException, ClassNotFoundException {
        Class.forName(driver);
        return DriverManager.getConnection(url, username, password);
    }

    //随便写写了,直接拼接
    public User seleteById(Integer id) throws SQLException, ClassNotFoundException {
        String sql = "select * from user where id = " + id;
        ResultSet resultSet = getConnection().createStatement().executeQuery(sql);
        resultSet.next();
        return new User()
                .setId(resultSet.getInt("id"))
                .setName(resultSet.getString("name"));

    }

}

手写基础架构效果演示

基础依赖包如下

可以看到使用我们手撕的 UserMapper 可以成功的查到 db 中的数据

总结

本文基于源码分析了 Mybatis 中代理 Mapper 创建的详细流程,基于理解一比一手撕复刻了出来,期间遇到的问题都总结在注释里面了。有人说会这个有啥用,会这个你可以将所有和数据库打交道的技术,都封装成类似于 Mybatis-Plus 的框架造福全宇宙!!!让技术不在复杂让小学生都会写代码,你就是明日之星!!!!

相关推荐
煤泥做不到的!22 分钟前
挑战一个月基本掌握C++(第十一天)进阶文件,异常处理,动态内存
开发语言·c++
F-2H24 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱056727 分钟前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i1 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx1 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
bryant_meng2 小时前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩
武子康2 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
若亦_Royi2 小时前
C++ 的大括号的用法合集
开发语言·c++
资源补给站3 小时前
大恒相机开发(2)—Python软触发调用采集图像
开发语言·python·数码相机
豪宇刘3 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat