SpringBoot集成MyBatis底層原理及簡易實現


MyBatis是可以說是目前最主流的Spring持久層框架了,本文主要探討SpringBoot集成MyBatis的底層原理。完整代碼可移步Github

如何使用MyBatis

一般情況下,我們在SpringBoot項目中應該如何集成MyBatis呢?

  1. 引入MyBatis依賴
<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.2</version>
</dependency>
  1. 配置數據源
  2. 在啟動類上添加@MapperScan注解,傳入需要掃描的dao層的包路徑
  3. 在dao層中創建接口,在方法上傳入對應的SQL語句,也可以使用Mapper.xml文件進行配置。例如:
public interface UserDao {
    @Select("insert into user xxx")
    void insert();
}
  1. 完成這些工作后,我們就可以自動注入一個userDao,然后調用userDao.insert()方法來實現對數據庫的操作。

那么問題來了,MyBatis是如何通過如此簡單的配置完成完成與Spring的“無縫連接”和數據的持久化工作的呢?

Spring的BeanDefinition

眾所周知,Spring的一大特性是IoC,既控制反轉。當我們將一個對象交給Spring管理之后,我們就不需要手動地通過new關鍵字去創建對象,只需要通過@Autowired或者@Resource自動注入即可。那么這個過程是如何實現的呢?

簡單來說,Spring會通過一個被聲明為bean的類信息生成一個beanDefinition(后面簡稱BD)對象,然后通過這個BD對象創建一個單例(不聲明為prototype的情況下),存入單例池,需要時進行調用。

在創建beadDefinition時,Spring會調用一系列的后置處理器(postProcessor)對BD加以處理,我們也可以自定義后置處理器,對BD的屬性進行修改,只需要實現BeanFactoryPostProcessor接口即可,例如:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        GenericBeanDefinition userDaoBD = (GenericBeanDefinition)beanFactory.getBeanDefinition("userDao");
        userDaoBD.setBeanClassName("userDaoChanged");
        userDaoBD.setAbstract(false);
        // more...
    }
}

在這個postProcessor中,我們獲取了UserDao的BD對象,並且將它的名字修改為"userDaoChanged",這樣我們就可以通過調用ApplicationContext的getBean("userDaochanged")方法獲取到原來的userDao的bean。

關於MyBatis

現在我們知道了,當我們將一個類交給Spring管理時,Spring通過beanDefinition構建bean單例。現在,我們又有了兩個新的問題:

  1. MyBatis如何將dao交給Spring管理的?
  2. 我們編寫的dao是一個接口,接口是如何實例化的?

MyBatis如何將dao交給Spring管理的?

在Spring中,將一個對象交給Spring管理主要有三種方式:

  1. @Bean
  2. beanFactory.registerSingleton(String beanName, Object singletonObject)
  3. FactoryBean

其中MyBatis使用的是FactoryBean的方式,實現FactoryBean接口就可以將我們的userDao注入到Spring當中。

beanFactory管理着Spring所有的bean,是一個大工廠。FactoryBean也是一個bean,但它卻有着Factory的功能,當我們調用Spring上下文的getBean()方法,並傳入自定義的FactoryBean時,返回的bean並不是這個FactoryBean本身,而是重寫的getObject()方法中所返回的對象。

如此看來,FactoryBean就是一個“小工廠”。

@Component
public class UserFactoryBean implements FactoryBean {
    
    UserDao userDao;

    @Override
    public Object getObject() throws Exception {
        return userDao;
    }

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

只是這樣寫當然是不能滿足我們的要求的,我們這時候調用getBean()方法會報錯,我們無法傳入一個userDao參數,因為UserDao不能被實例化。但是在MyBatis中,我們卻可以通過sqlSession.getMapper(UserDao.class)方法獲取到一個UserDao的實例化對象,MyBatis是如何做到這一點的?

如何實例化一個接口?

為什么接口可以被實例化呢?查看MyBatis的源碼我們可以得知,MyBatis通過動態代理(Proxy)的技術在接口的基礎上包裝出了一個對象,然后將這個對象交給了Spring。沿着getMapper方法追根溯源我們可以發現這樣一個方法:

protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }

MyBatis可以,那我們也可以,我們改寫一下UserFatoryBean:

@Component
public class UserFactoryBean implements FactoryBean {

    @Override
    public Object getObject() throws Exception {
        Class[] classes = new Class[]{UserDao.class};
        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
    }

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

Proxy.newProxyInstance()方法接收三個參數,分別為:

  1. ClassLoader loader:決定用哪個類加載器來加載生成的代理對象
  2. Class<?>[] interfaces:決定這個代理對象要實現哪些接口,擁有哪些功能
  3. InvocationHandler h:決定調用這個代理對象的某個方法時執行的具體邏輯

然后編寫一個InvokationHandler類用於執行代理對象的具體方法邏輯:

public class MyInvokationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("假裝查詢數據庫:" + method.getAnnotation(Select.class).value()[0]);
        return null;
    }
}

在這個handler中,我們獲取到@Select注解中的信息,然后將它打印出來。

OK,現在我們運行一下:

@Test
void contextLoads() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
    UserDao userDao = (UserDao) ac.getBean("userFactoryBean");
    userDao.insert();
}

控制台輸出:

假裝查詢數據庫:insert into user xxx

至此,我們就完成了MyBatis的簡易實現的一小部分。但是還有一個重要的問題:我們這個FactoryBean是寫死的,只能返回UserDao的代理對象,實際情境下,我們如何根據用戶傳入的參數返回不同的代理對象呢?

動態生成代理對象

想要動態生成代理對象,首先我們需要修改UserFactoryBean的代碼,讓它能適配各種類型的dao,不妨直接改個名字叫MyFactoryBean:

public class MyFactoryBean implements FactoryBean {

    Class mapperInterface;

    // 為了支持XML配置,必須提供一個默認構造方法
    public MyFactoryBean(){}

    // 通過MapperScan方式
    public MyFactoryBean(Class mapperInterface){
        this.mapperInterface = mapperInterface;
    }


    @Override
    public Object getObject() throws Exception {
        Class[] classes = new Class[]{mapperInterface};
        return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
    }

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

我們將UserDao.class改成了動態的mapperInterface,那么我們如何向MyFactoryBean的構造方法傳入這個參數呢?這就回到了我們一開始說到的beanDefinition,在Spring中,可以在BD期間修改bean的各種屬性,這其中就包括構造方法的參數。我們修改我們一開始寫的MyBeanFactoryPostProcessor:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

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

        List<Class> daos = new ArrayList<>();
        // 獲取所有dao
        daos.add(UserDao.class);
        daos.add(AnchorDao.class);

        for(Class dao:daos){
            // 獲取一個空的beanDefinition
            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();

            // 為構造方法添加參數
            beanDefinition.setBeanClass(MyFactoryBean.class);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(dao);
        }
    }
}

這樣的話我們就構造出了我們想要的beanDefinition,現在要做的是把它加入到Spring容器中去。注意:是把beanDefinition加入到Spring容器中,而不是把bean加到Spring容器中。我們前面說的@Bean之內的方法是不適用的。

這里需要用到另兩個知識點:@ImportImportBeanDefinitionRegistrar

在應用中,有時沒有把某個類注入到IOC容器中,但在運用的時候需要獲取該類對應的bean,此時就需要用到@Import注解。

@Import最強大的地方在於,它提供了一個擴展點給用戶。當我們用@Import導入的類實現了ImportBeanDefinitionRegistrar接口時,Spring不會直接將這個類包裝成一個bean,而是執行其內部的registerBeanDefinitions方法。這有點像FactoryBean,可以在類中執行自己的邏輯。

我們編寫這樣一個registerBeanDefinitions:

public class ImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){

        // 獲得包名,遍歷獲得所有類名
        String packagePath = Appconfig.class.getAnnotation(MyScan.class).path();
        List<String> classNames = SelectClassName.selectByPackage(packagePath);

        for(String className:classNames){

            BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
            GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
            registry.registerBeanDefinition(SelectClassName.getShortName(className),genericBeanDefinition);
            // 添加構造方法參數
            genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(className);
        }
    }
}

並且編寫一個MyScan注解類用戶獲取需要掃描的包名:

@Retention(RetentionPolicy.RUNTIME)
public @interface MyScan {
    String path();
}

然后在AppConfig類上加入@MyScan注解,傳入包名,最后編寫一個工具類用來獲取包下的所有類名。MyBeanFactoryPostProcessor類也可以刪除了,ImportBeanDefinitionRegister替代了它的工作。

(完整代碼可以訪問我的Github

@Test
void contextLoads() {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
    ac.getBean(AnchorDao.class).query();
    ac.getBean(UserDao.class).insert();
}

控制台輸出:

假裝查詢數據庫:select * from anchor limit 5
假裝查詢數據庫:insert into user xxx

大功告成!回頭再看一下我們是如何使用MyBatis的:@MapperScan、編寫dao接口、@Select——和我們現在的功能幾乎完全一樣。只需要在MyInvokationHandler中封裝一下JDBC,實現具體的訪問數據庫邏輯,你就可以在項目中使用自己編寫的“MyBatis”了。

總結

Spring提供了很好的環境用於第三方框架的開發,這也是Spring能發展出如今這樣龐大且完善的生態的原因之一。知識都是觸類旁通的,例如Spring的另一大特性:AOP,它就與本文談到的后置處理器beanPostProcessor和動態代理有關,對SpringBoot集成MyBatis底層原理的學習和研究,讓我對Spring和MyBatis都有了更深入的認識。(累死我了,歇會兒(;´д`)ゞ)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM