1. 程式人生 > >SpringBoot整合MyBatis底層原理及簡易實現

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. 完成這些工作後,我們就可以呼叫new 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都有了更深入的認識。(累死我了,歇會兒(;´д`)ゞ)