乐趣区

源码分析-手写mybaitspring核心功能干货好文一次学会工厂bean类代理bean注册的使用

作者:小傅哥
博客:https://bugstack.cn – 汇总系列原创专题文章

沉淀、分享、成长,让自己和他人都能有所收获!?

一、前言介绍

一个知识点的学习过程基本分为;运行 helloworld、熟练使用 api、源码分析、核心专家。在分析 mybaits 以及 mybatis-spring 源码之前,我也只是简单的使用,因为它好用。但是他是怎么做的多半是凭自己的经验去分析,但始终觉得这样的感觉缺少点什么,在几次夙兴夜寐,靡有朝矣之后决定彻底的研究一下,之后在去仿照着写一版核心功能。依次来补全自己的技术栈的空缺。在现在技术知识像爆炸一样迸发,而我们多半又忙于工作业务开发。就像一个不会修车的老司机,只能一脚油门,一脚刹车的奔波。车速很快,但经不起坏,累觉不爱。好!为了解决这样问题,也为了钱程似锦(形容钱多的想家里的棉布一样),努力!

开动之前先庆祝下我的 iPhone4s 又活了,还是那么好用 ( 嗯!有点卡);

二、以往章节

关于 mybaits & spring 源码分析以及 demo 功能的章节汇总,可以通过下列内容进行系统的学习,同时以下章节会有部分内容涉及到 demo 版本的 mybaits;

  • 源码分析 | Mybatis 接口没有实现类为什么可以执行增删改查
  • 源码分析 | 像盗墓一样分析 Spring 是怎么初始化 xml 并注册 bean 的
  • 源码分析 | 基于 jdbc 实现一个 Demo 版的 Mybatis

三、一碟小菜类代理

往往从最简单的内容才有抓手。先看一个接口到实现类的使用,在将这部分内容转换为代理类。

1. 定义一个 IUserDao 接口并实现这个接口类

public interface IUserDao {String queryUserInfo();

}

public class UserDao implements IUserDao {

    @Override
    public String queryUserInfo() {return "实现类";}

}

2. new() 方式实例化

IUserDao userDao = new UserDao();
userDao.queryUserInfo();

这是最简单的也是最常用的使用方式,new 个对象。

3. proxy 方式实例化

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Class<?>[] classes = {IUserDao.class};
InvocationHandler handler = (proxy, method, args) -> "你被代理了" + method.getName();

IUserDao userDao = (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);

String res = userDao.queryUserInfo();
logger.info("测试结果:{}", res);
  • Proxy.newProxyInstance 代理类实例化方式,对应传入类的参数即可
  • ClassLoader,是这个类加载器,我们可以获取当前线程的类加载器
  • InvocationHandler 是代理后实际操作方法执行的内容,在这里可以添加自己业务场景需要的逻辑,在这里我们只返回方法名

测试结果:

23:20:18.841 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:你被代理了 queryUserInfo

Process finished with exit code 0

四、盛宴来自 Bean 工厂

在使用 Spring 的时候,我们会采用注册或配置文件的方式,将我们的类交给 Spring 管理。例如;

<bean id="userDao" class="org.itstack.demo.UserDao" scope="singleton"/>

UserDao 是接口 IUserDao 的实现类,通过上面配置,就可以实例化一个类供我们使用,但如果 IUserDao 没有实现类或者我们希望去动态改变他的实现类比如挂载到别的地方(像 mybaits 一样),并且是由 spring bean 工厂管理的,该怎么做呢?

1. FactoryBean 的使用

FactoryBean 在 spring 起到着二当家的地位,它将近有 70 多个小弟(实现它的接口定义),那么它有三个方法;

  • T getObject() throws Exception; 返回 bean 实例对象
  • Class<?> getObjectType(); 返回实例类类型
  • boolean isSingleton(); 判断是否单例,单例会放到 Spring 容器中单实例缓存池中

那么我们现在就将上面用到的 代理类 交给 spring 的 FactoryBean 进行管理,代码如下;

ProxyBeanFactory.java & bean 工厂实现类

public class ProxyBeanFactory implements FactoryBean<IUserDao> {

    @Override
    public IUserDao getObject() throws Exception {ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Class<?>[] classes = {IUserDao.class};
        InvocationHandler handler = (proxy, method, args) -> "你被代理了" + method.getName();

        return (IUserDao) Proxy.newProxyInstance(classLoader, classes, handler);
    }

    @Override
    public Class<?> getObjectType() {return IUserDao.class;}

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

}

spring-config.xml & 配置 bean 类信息

<bean id="userDao" class="org.itstack.demo.bean.ProxyBeanFactory"/>

ApiTest.test_IUserDao() & 单元测试

@Test
public void test_IUserDao() {BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("测试结果:{}", res);
}

测试结果:

一月 20, 2020 23:43:35 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [spring-config.xml]
23:43:35.440 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:你被代理了 queryUserInfo

Process finished with exit code 0

咋样,神奇不!你的接口都不需要实现类,就被安排的明明白白的。记住这个方法 FactoryBean 和动态代理。

2. BeanDefinitionRegistryPostProcessor 类注册

你是否有怀疑过你媳妇把你钱没收了之后都存放到哪去了,为啥你每次 get 都那么费劲,像垃圾回收了一样,不可达。

好嘞,媳妇那就别想了,研究下你的 bean 都被注册到哪了就可以了。在 spring 的 bean 管理中,所有的 bean 最终都会被注册到类 DefaultListableBeanFactory 中,接下来我们就主动注册一个被我们代理了的 bean。

RegisterBeanFactory.java & 注册 bean 的实现类

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(ProxyBeanFactory.class);

        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");
        registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// left intentionally blank}

}
  • 这里包含 4 块主要内容,分别是;

    • 实现 BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry 方法,获取 bean 注册对象
    • 定义 bean,GenericBeanDefinition,这里主要设置了我们的代理类工厂。我们已经测试过他获取一个代理类
    • 创建 bean 定义处理类,BeanDefinitionHolder,这里需要的主要参数;定义 bean、bean 名称
    • 最后将我们自己的 bean 注册到 spring 容器中去,registry.registerBeanDefinition()

spring-config.xml & 配置 bean 类信息

<bean id="userDao" class="org.itstack.demo.bean.RegisterBeanFactory"/>

ApiTest.test_IUserDao() & 单元测试

@Test
public void test_IUserDao() {BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
    IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
    String res = userDao.queryUserInfo();
    logger.info("测试结果:{}", res);
}

测试结果:

信息: Loading XML bean definitions from class path resource [spring-config.xml]
一月 20, 2020 23:42:29 上午 org.springframework.beans.factory.support.DefaultListableBeanFactory registerBeanDefinition
信息: Overriding bean definition for bean 'userDao' with a different definition: replacing [Generic bean: class [org.itstack.demo.bean.RegisterBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=1; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in class path resource [spring-config.xml]] with [Generic bean: class [org.itstack.demo.bean.ProxyBeanFactory]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null]
23:42:29.754 [main] INFO  org.itstack.demo.test.ApiTest - 测试结果:你被代理了 queryUserInfo

Process finished with exit code 0

纳尼?是不有一种满脑子都是骚操作的感觉,自己注册的 bean 自己知道在哪了,咋回事了。

五、老板郎上主食呀(mybaits-spring)

如果通过上面的知识点;代理类、bean 工厂、bean 注册,将我们一个没有实现类的接口安排的明明白白,让他执行啥就执行啥,那么你是否可以想到,这个没有实现类的接口,可以通过我们的折腾,去调用到我们的 mybaits 呢!

如下图,通过 mybatis 使用的配置,我们可以看到数据源 DataSource 交给 SqlSessionFactoryBean,SqlSessionFactoryBean 实例化出的 SqlSessionFactory,再交给 MapperScannerConfigurer。而我们要实现的就是 MapperScannerConfigurer 这部分;

1. 需要实现哪些核心类

为了更易理解也更易于对照,我们将实现 mybatis-spring 中的流程核心类,如下;

  • MapperFactoryBean {给每一个没有实现类的接口都代理一个这样的类,用于操作数据库执行 crud}
  • MapperScannerConfigurer {扫描包下接口类,免去配置。这样是上图中核心配置类}
  • SimpleMetadataReader {这个类完全和 mybaits-spring 中的类一样,为了解析 class 文件。如果你对类加载处理很好奇,可以阅读我的《用 java 实现 jvm 虚拟机》}
  • SqlSessionFactoryBean {这个类核心内容就一件事,将我们写的 demo 版的 mybaits 结合进来}

在分析之前先看下我们实现主食是怎么食用的,如下;

<bean id="sqlSessionFactory" class="org.itstack.demo.like.spring.SqlSessionFactoryBean">
    <property name="resource" value="spring/mybatis-config-datasource.xml"/>
</bean>

<bean class="org.itstack.demo.like.spring.MapperScannerConfigurer">
    <!-- 注入 sqlSessionFactory -->
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    <!-- 给出需要扫描 Dao 接口包 -->
    <property name="basePackage" value="org.itstack.demo.dao"/>
</bean>

2. (类介绍)SqlSessionFactoryBean

这类本身比较简单,主要实现了 FactoryBean<SqlSessionFactory>, InitializingBean 用于帮我们处理 mybaits 核心流程类的加载处理。(关于 demo 版的 mybaits 已经在上文中提供学习链接)

SqlSessionFactoryBean.java

public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean {

    private String resource;
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void afterPropertiesSet() throws Exception {try (Reader reader = Resources.getResourceAsReader(resource)) {this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
        } catch (Exception e) {e.printStackTrace();
        }
    }

    @Override
    public SqlSessionFactory getObject() throws Exception {return sqlSessionFactory;}

    @Override
    public Class<?> getObjectType() {return sqlSessionFactory.getClass();
    }

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

    public void setResource(String resource) {this.resource = resource;}

}
  • 实现 InitializingBean 主要用于加载 mybatis 相关内容;解析 xml、构造 SqlSession、链接数据库等
  • FactoryBean,这个类我们介绍过,主要三个方法;getObject()、getObjectType()、isSingleton()

3. (类介绍)MapperScannerConfigurer

这类的内容看上去可能有点多,但是核心事情也就是将我们的 dao 层接口扫描、注册

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor {

    private String basePackage;
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        try {
            // classpath*:org/itstack/demo/dao/**/*.class
            String packageSearchPath = "classpath*:" + basePackage.replace('.', '/') + "/**/*.class";

            ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
            Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);

            for (Resource resource : resources) {MetadataReader metadataReader = new SimpleMetadataReader(resource, ClassUtils.getDefaultClassLoader());

                ScannedGenericBeanDefinition beanDefinition = new ScannedGenericBeanDefinition(metadataReader);
                String beanName = Introspector.decapitalize(ClassUtils.getShortName(beanDefinition.getBeanClassName()));
                
                beanDefinition.setResource(resource);
                beanDefinition.setSource(resource);
                beanDefinition.setScope("singleton");
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
                beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(sqlSessionFactory);
                beanDefinition.setBeanClass(MapperFactoryBean.class);

                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, beanName);
                registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
            }
        } catch (IOException e) {e.printStackTrace();
        }

    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {// left intentionally blank}

    public void setBasePackage(String basePackage) {this.basePackage = basePackage;}

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {this.sqlSessionFactory = sqlSessionFactory;}
}
  • 类的扫描注册,classpath:org/itstack/demo/dao//.class,解析 calss 文件获取资源信息;Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
  • 遍历 Resource,这里就你的 class 信息,用于注册 bean。ScannedGenericBeanDefinition
  • 这里有一点,bean 的定义设置时候,是把 beanDefinition.setBeanClass(MapperFactoryBean.class); 设置进去的。同时在前面给他设置了构造参数。(细细品味)
  • 最后执行注册 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

4. (类介绍)MapperFactoryBean

这个类就非常有意思了,因为你所有的 dao 接口类,实际就是他。他这里帮你执行你对 sql 的所有操作的分发处理。为了更加简化清晰,目前这里只实现了查询部分,在 mybatis-spring 源码中分别对 select、update、insert、delete、其他等做了操作。

public class MapperFactoryBean<T> implements FactoryBean<T> {

    private Class<T> mapperInterface;
    private SqlSessionFactory sqlSessionFactory;

    public MapperFactoryBean(Class<T> mapperInterface, SqlSessionFactory sqlSessionFactory) {
        this.mapperInterface = mapperInterface;
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public T getObject() throws Exception {InvocationHandler handler = (proxy, method, args) -> {System.out.println("你被代理了,执行 SQL 操作!" + method.getName());
            try {SqlSession session = sqlSessionFactory.openSession();
                try {return session.selectOne(mapperInterface.getName() + "." + method.getName(), args[0]);
                } finally {session.close();
                }
            } catch (Exception e) {e.printStackTrace();
            }

            return method.getReturnType().newInstance();
        };
        return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{mapperInterface}, handler);
    }

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

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

}
  • T getObject(),中是一个 java 代理类的实现,这个代理类对象会被挂到你的注入中。真正调用方法内容时会执行到代理类的实现部分,也就是“你被代理了,执行 SQL 操作!”
  • InvocationHandler,代理类的实现部分非常简单,主要开启 SqlSession,并通过固定的 key;“org.itstack.demo.dao.IUserDao.queryUserInfoById”执行 SQL 操作;

    session.selectOne(mapperInterface.getName() + “.” + method.getName(), args[0]);

    <mapper namespace="org.itstack.demo.dao.IUserDao">
    
        <select id="queryUserInfoById" parameterType="java.lang.Long" resultType="org.itstack.demo.po.User">
            SELECT id, name, age, createTime, updateTime
            FROM user
            where id = #{id}
        </select>
        
    </mapper>
  • 最终返回了执行结果,关于查询到结果信息会反射操作成对象类,这部分内容可以遇到 demo 版本的 mybatis

六、 倒满走一个

好!到这一切开发内容就完成了,测试走一个。

mybatis-config-datasource.xml & 数据源配置

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://127.0.0.1:3306/itstack_demo_ddd?useUnicode=true"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="mapper/User_Mapper.xml"/>
        <mapper resource="mapper/School_Mapper.xml"/>
    </mappers>

</configuration>

test-config.xml & 配置 xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
       default-autowire="byName">
    <context:component-scan base-package="org.itstack"/>

    <aop:aspectj-autoproxy/>

    <bean id="sqlSessionFactory" class="org.itstack.demo.like.spring.SqlSessionFactoryBean">
        <property name="resource" value="spring/mybatis-config-datasource.xml"/>
    </bean>

    <bean class="org.itstack.demo.like.spring.MapperScannerConfigurer">
        <!-- 注入 sqlSessionFactory -->
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
        <!-- 给出需要扫描 Dao 接口包 -->
        <property name="basePackage" value="org.itstack.demo.dao"/>
    </bean>

</beans>

SpringTest.java & 单元测试

public class SpringTest {private Logger logger = LoggerFactory.getLogger(SpringTest.class);

    @Test
    public void test_ClassPathXmlApplicationContext() {BeanFactory beanFactory = new ClassPathXmlApplicationContext("test-config.xml");
        IUserDao userDao = beanFactory.getBean("IUserDao", IUserDao.class);
        User user = userDao.queryUserInfoById(1L);
        logger.info("测试结果:{}", JSON.toJSONString(user));
    }

}

测试结果;

一月 20, 2020 23:51:43 上午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@30b8a058: startup date [Mon Jan 20 23:51:43 CST 2020]; root of context hierarchy
一月 20, 2020 23:51:43 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [test-config.xml]
你被代理了,执行 SQL 操作!queryUserInfoById
2020-01-20 23:51:45.592 [main] INFO  org.itstack.demo.SpringTest[26] - 测试结果:{"age":18,"createTime":1576944000000,"id":1,"name":"水水","updateTime":1576944000000}

Process finished with exit code 0

酒干热火笑红尘,春秋几载年轮,不问。回首皆是 Spring!Gun!变心!你被代理了!

七、综上总结

  • 通过这些核心关键类的实现;SqlSessionFactoryBean、MapperScannerConfigurer、SqlSessionFactoryBean,我们将 spring 与 mybaits 集合起来使用,解决了没有实现类的接口怎么处理数据库 CRUD 操作
  • 那么这个知识点可以用到哪里,不要只想着面试!在我们业务开发中是不会有很多其他数据源操作,比如 ES、Hadoop、数据中心等等,包括自建。那么我们就可以做成一套统一数据源处理服务,以优化服务开发效率
  • 由于这次工程类是在 itstack-demo-code-mybatis 中继续开发,如果需要获取源码可以关注公众号:bugstack 虫洞栈,回复:源码分析

八、推荐阅读

  • 这么折腾学习毕业进大厂不是问题
  • 工作两年简历写的差教你优化
  • 讲一下我自己的学习路线,给你一些参考
  • 基于 Springboot 的中间件开发,了解一下
退出移动版