Spring整合Mybatis原理


Spring整合Mybatis原理

在介紹Spring整合Mybatis原理之前,我們得先來稍微介紹Mybatis的工作原理。

Mybatis的基本工作原理

在Mybatis中,我們可以使用一個接口去定義要執行sql,簡化代碼如下:

 

定義一個接口,@Select表示要執行查詢sql語句。

public interface UserMapper {
  @Select("select * from user where id = #{id}")
  User selectById(Integer id);
}

以下為執行sql代碼:

InputStream inputStream = Resources.getResourceAsStream("mybatis.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();

// 以下使我們需要關注的重點
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
Integer id = 1;
User user = mapper.selectById(id);
 

Mybatis的目的是:使得程序員能夠以調用方法的方式執行某個指定的sql,將執行sql的底層邏輯進行了封裝。

這里重點思考以下mapper這個對象,當調用SqlSession的getMapper方法時,會對傳入的接口生成一個代理對象,而程序要真正用到的就是這個代理對象,在調用代理對象的方法時,Mybatis會取出該方法所對應的sql語句,然后利用JDBC去執行sql語句,最終得到結果。

 

分析需要解決的問題

Spring和Mybatis時,我們重點要關注的就是這個代理對象。因為整合的目的就是:把某個Mapper的代理對象作為一個bean放入Spring容器中,使得能夠像使用一個普通bean一樣去使用這個代理對象,比如能被@Autowire自動注入。

 

比如當Spring和Mybatis整合之后,我們就可以使用如下的代碼來使用Mybatis中的代理對象了:

@Component
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public User getUserById(Integer id) {
        return userMapper.selectById(id);
    }
}

UserService中的userMapper屬性就會被自動注入為Mybatis中的代理對象。如果你基於一個已經完成整合的項目去調試即可發現,userMapper的類型為:org.apache.ibatis.binding.MapperProxy@41a0aa7d。證明確實是Mybatis中的代理對象。

 

好,那么現在我們要解決的問題的就是:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?

 

要解決這個,我們需要對Spring的bean生成過程有一個了解。

 

Spring中Bean的產生過程

Spring啟動過程中,大致會經過如下步驟去生成bean

  1. 掃描指定的包路徑下的class文件
  2. 根據class信息生成對應的BeanDefinition
  3. 在此處,程序員可以利用某些機制去修改BeanDefinition
  4. 根據BeanDefinition生成bean實例
  5. 把生成的bean實例放入Spring容器中

 

假設有一個A類,假設有如下代碼:

 

一個A類:

@Component
public class A {
}

一個B類,不存在@Component注解

public class B {
}

執行如下代碼:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

輸出結果為:com.luban.util.A@6acdbdf5

 

A類對應的bean對象類型仍然為A類。但是這個結論是不確定的,我們可以利用BeanFactory后置處理器來修改BeanDefinition,我們添加一個BeanFactory后置處理器:

@Component
public class LubanBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("a");
        beanDefinition.setBeanClassName(B.class.getName());
    }
}

 

這樣就會導致,原本的A類對應的BeanDefiniton被修改了,被修改成了B類,那么后續正常生成的bean對象的類型就是B類。此時,調用如下代碼會報錯:

context.getBean(A.class);

 

但是調用如下代碼不會報錯,盡管B類上沒有@Component注解:

context.getBean(B.class);

 

並且,下面代碼返回的結果是:com.luban.util.B@4b1c1ea0

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

 

之所以講這個問題,是想說明一個問題:在Spring中,bean對象跟class沒有直接關系,跟BeanDefinition才有直接關系。

 

那么回到我們要解決的問題:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?

 

在Spring中,如果你想生成一個bean,那么得先生成一個BeanDefinition,就像你想new一個對象實例,得先有一個class。

 

解決問題

繼續回到我們的問題,我們現在想自己生成一個bean,那么得先生成一個BeanDefinition,只要有了BeanDefinition,通過在BeanDefinition中設置bean對象的類型,然后把BeanDefinition添加給Spring,Spring就會根據BeanDefinition自動幫我們生成一個類型對應的bean對象。

 

所以,現在我們要解決兩個問題:

  1. Mybatis的代理對象的類型是什么?因為我們要設置給BeanDefinition
  2. 我們怎么把BeanDefinition添加給Spring容器?

 

注意:上文中我們使用的BeanFactory后置處理器,他只能修改BeanDefinition,並不能新增一個BeanDefinition。我們應該使用Import技術來添加一個BeanDefinition。后文再詳細介紹如果使用Import技術來添加一個BeanDefinition,可以先看一下偽代碼實現思路。

 

假設:我們有一個UserMapper接口,他的代理對象的類型為UserMapperProxy。

那么我們的思路就是這樣的,偽代碼如下:

BeanDefinitoin bd = new BeanDefinitoin();
bd.setBeanClassName(UserMapperProxy.class.getName());
SpringContainer.addBd(bd);

 

但是,這里有一個嚴重的問題,就是上文中的UserMapperProxy是我們假設的,他表示一個代理類的類型,然而Mybatis中的代理對象是利用的JDK的動態代理技術實現的,也就是代理對象的代理類是動態生成的,我們根本無法確定代理對象的代理類到底是什么。

 

所以回到我們的問題:Mybatis的代理對象的類型是什么?

 

本來可以有兩個答案:

  1. 代理對象對應的代理類
  2. 代理對象對應的接口

 

那么答案1就相當於沒有了,因為是代理類是動態生成的,那么我們來看答案2:代理對象對應的接口

 

如果我們采用答案2,那么我們的思路就是:

BeanDefinition bd = new BeanDefinitoin();
// 注意這里,設置的是UserMapper
bd.setBeanClassName(UserMapper.class.getName());
SpringContainer.addBd(bd);

 

但是,實際上給BeanDefinition對應的類型設置為一個接口是行不通的,因為Spring沒有辦法根據這個BeanDefinition去new出對應類型的實例,接口是沒法直接new出實例的。

 

那么現在問題來了,我要解決的問題:Mybatis的代理對象的類型是什么?

兩個答案都被我們否定了,所以這個問題是無解的,所以我們不能再沿着這個思路去思考了,只能回到最開始的問題:如何能夠把Mybatis的代理對象作為一個bean放入Spring容器中?

 

總結上面的推理:我們想通過設置BeanDefinition的class類型,然后由Spring自動的幫助我們去生成對應的bean,但是這條路是行不通的。

 

終極解決方案

那么我們還有沒有其他辦法,可以去生成bean呢?並且生成bean的邏輯不能由Spring來幫我們做了,得由我們自己來做。

 

FactoryBean

有,那就是Spring中的FactoryBean。我們可以利用FactoryBean去自定義我們要生成的bean對象,比如:

@Component
public class LubanFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 執行代理邏輯
                    return null;
                }
            }
        });

        return proxyInstance;
    }

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

 

我們定義了一個LubanFactoryBean,它實現了FactoryBean,getObject方法就是用來自定義生成bean對象邏輯的。

 

執行如下代碼:

public class Test {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println("lubanFactoryBean: " + context.getBean("lubanFactoryBean"));
        System.out.println("&lubanFactoryBean: " + context.getBean("&lubanFactoryBean"));
        System.out.println("lubanFactoryBean-class: " + context.getBean("lubanFactoryBean").getClass());
    }
}

 

將打印:

lubanFactoryBean: com.luban.util.LubanFactoryBean$1@4d41cee

&lubanFactoryBean: com.luban.util.LubanFactoryBean@3712b94

lubanFactoryBean-class: class com.sun.proxy.$Proxy20

 

從結果我們可以看到,從Spring容器中拿名字為"lubanFactoryBean"的bean對象,就是我們所自定義的jdk動態代理所生成的代理對象。

 

所以,我們可以通過FactoryBean來向Spring容器中添加一個自定義的bean對象。上文中所定義的LubanFactoryBean對應的就是UserMapper,表示我們定義了一個LubanFactoryBean,相當於把UserMapper對應的代理對象作為一個bean放入到了容器中。

 

但是作為程序員,我們不可能每定義了一個Mapper,還得去定義一個LubanFactoryBean,這是很麻煩的事情,我們改造一下LubanFactoryBean,讓他變得更通用,比如:

 

@Component
public class LubanFactoryBean implements FactoryBean {

    // 注意這里
    private Class mapperInterface;
    public LubanFactoryBean(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(LubanFactoryBean.class.getClassLoader(), new Class[]{mapperInterface}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                } else {
                    // 執行代理邏輯
                    return null;
                }
            }
        });

        return proxyInstance;
    }

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

 

改造LubanFactoryBean之后,LubanFactoryBean變得靈活了,可以在構造LubanFactoryBean時,通過構造傳入不同的Mapper接口。

 

實際上LubanFactoryBean也是一個Bean,我們也可以通過生成一個BeanDefinition來生成一個LubanFactoryBean,並給構造方法的參數設置不同的值,比如偽代碼如下:

BeanDefinition bd = new BeanDefinitoin();
// 注意一:設置的是LubanFactoryBean
bd.setBeanClassName(LubanFactoryBean.class.getName());
// 注意二:表示當前BeanDefinition在生成bean對象時,會通過調用LubanFactoryBean的構造方法來生成,並傳入UserMapper
bd.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class.getName())
SpringContainer.addBd(bd);

 

特別說一下注意二,表示表示當前BeanDefinition在生成bean對象時,會通過調用LubanFactoryBean的構造方法來生成,並傳入UserMapper的Class對象。那么在生成LubanFactoryBean時就會生成一個UserMapper接口對應的代理對象作為bean了。

 

到此為止,其實就完成了我們要解決的問題:把Mybatis中的代理對象作為一個bean放入Spring容器中。只是我們這里是用簡單的JDK代理對象模擬的Mybatis中的代理對象,如果有時間,我們完全可以調用Mybatis中提供的方法區生成一個代理對象。這里就不花時間去介紹了。

 

Import

到這里,我們還有一個事情沒有做,就是怎么真正的定義一個BeanDefinition,並把它添加到Spring中,上文說到我們要利用Import技術,比如可以這么實現:

 

定義如下類:

public class LubanImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
        AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
        beanDefinition.setBeanClass(LubanFactoryBean.class);
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(UserMapper.class);
        // 添加beanDefinition
        registry.registerBeanDefinition("luban"+UserMapper.class.getSimpleName(), beanDefinition);
    }
}

 

並且在AppConfig上添加@Import注解:

@Import(LubanImportBeanDefinitionRegistrar.class)
public class AppConfig {

 

這樣在啟動Spring時就會新增一個BeanDefinition,該BeanDefinition會生成一個LubanFactoryBean對象,並且在生成LubanFactoryBean對象時會傳入UserMapper.class對象,通過LubanFactoryBean內部的邏輯,相當於會自動生產一個UserMapper接口的代理對象作為一個bean。

 

總結

總結一下,通過我們的分析,我們要整合Spring和Mybatis,需要我們做的事情如下:

  1. 定義一個LubanFactoryBean
  2. 定義一個LubanImportBeanDefinitionRegistrar
  3. 在AppConfig上添加一個注解@Import(LubanImportBeanDefinitionRegistrar.class)

 

優化

這樣就可以基本完成整合的需求了,當然還有兩個點是可以優化的

 

第一,單獨再定義一個@LubanScan的注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Import(LubanImportBeanDefinitionRegistrar.class)
public @interface LubanScan {
}

 

這樣在AppConfig上直接使用@LubanScan即可

 

第二,在LubanImportBeanDefinitionRegistrar中,我們可以去掃描Mapper,在LubanImportBeanDefinitionRegistrar我們可以通過AnnotationMetadata獲取到對應的@LubanScan注解,所以我們可以在@LubanScan上設置一個value,用來指定待掃描的包路徑。然后在LubanImportBeanDefinitionRegistrar中獲取所設置的包路徑,然后掃描該路徑下的所有Mapper,生成BeanDefinition,放入Spring容器中。

 

所以,到此為止,Spring整合Mybatis的核心原理就結束了,再次總結一下:

  1. 定義一個LubanFactoryBean,用來將Mybatis的代理對象生成一個bean對象
  2. 定義一個LubanImportBeanDefinitionRegistrar,用來生成不同Mapper對象的LubanFactoryBean
  3. 定義一個@LubanScan,用來在啟動Spring時執行LubanImportBeanDefinitionRegistrar的邏輯,並指定包路徑

 

以上這個三個要素分別對象org.mybatis.spring中的:

  1. MapperFactoryBean
  2. MapperScannerRegistrar
  3. @MapperScan​


免責聲明!

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



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