源碼分析 | 手寫mybait-spring核心功能(乾貨好文一次學會工廠bean、類代理、bean註冊的使用)

做者:小傅哥
博客:https://bugstack.cn - 彙總系列原創專題文章html

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言介紹

一個知識點的學習過程基本分爲;運行helloworld、熟練使用api、源碼分析、核心專家。在分析mybaits以及mybatis-spring源碼以前,我也只是簡單的使用,由於它好用。可是他是怎麼作的多半是憑本身的經驗去分析,但始終以爲這樣的感受缺乏點什麼,在幾回夙興夜寐,靡有朝矣以後決定完全的研究一下,以後在去仿照着寫一版核心功能。依次來補全本身的技術棧的空缺。在如今技術知識像爆炸同樣迸發,而咱們多半又忙於工做業務開發。就像一個不會修車的老司機,只能一腳油門,一腳剎車的奔波。車速很快,但經不起壞,累覺不愛。好!爲了解決這樣問題,也爲了錢程似錦(形容錢多的想家裏的棉布同樣),努力!java

開動以前先慶祝下個人iPhone4s又活了,仍是那麼好用(嗯!有點卡);mysql

2、以往章節

關於mybaits & spring 源碼分析以及demo功能的章節彙總,能夠經過下列內容進行系統的學習,同時如下章節會有部份內容涉及到demo版本的mybaits;面試

3、一碟小菜類代理

每每從最簡單的內容纔有抓手。先看一個接口到實現類的使用,在將這部份內容轉換爲代理類。spring

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 個對象。sql

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

4、盛宴來自Bean工廠

在使用Spring的時候,咱們會採用註冊或配置文件的方式,將咱們的類交給Spring管理。例如;api

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

UserDao是接口IUserDao的實現類,經過上面配置,就能夠實例化一個類供咱們使用,但若是IUserDao沒有實現類或者咱們但願去動態改變他的實現類好比掛載到別的地方(像mybaits同樣),而且是由spring bean工廠管理的,該怎麼作呢?緩存

1. FactoryBean的使用

FactoryBean 在spring起到着二當家的地位,它將近有70多個小弟(實現它的接口定義),那麼它有三個方法;session

  • 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本身知道在哪了,咋回事了。

5、老闆郎上主食呀(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

6、倒滿走一個

好!到這一切開發內容就完成了,測試走一個。

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!變心!你被代理了!

7、綜上總結

  • 經過這些核心關鍵類的實現;SqlSessionFactoryBean、MapperScannerConfigurer、SqlSessionFactoryBean,咱們將spring與mybaits集合起來使用,解決了沒有實現類的接口怎麼處理數據庫CRUD操做
  • 那麼這個知識點能夠用到哪裏,不要只想着面試!在咱們業務開發中是不會有不少其餘數據源操做,好比ES、Hadoop、數據中心等等,包括自建。那麼咱們就能夠作成一套統一數據源處理服務,以優化服務開發效率
  • 因爲此次工程類是在itstack-demo-code-mybatis中繼續開發,若是須要獲取源碼能夠關注公衆號:bugstack蟲洞棧,回覆:源碼分析

8、推薦閱讀

相關文章
相關標籤/搜索