Spring源碼深度解析之spring整合mybatis原理分析(上)

spring整合mybatis原理分析,分為兩篇,上篇從零開始寫一個spring整合mybatis的代碼,帶大家分析spring整合mybatis的思路。下篇分析spring整合mybatis的源碼。

1 原生mybatis中執(zhí)行SQL

我們先從原生mybatis執(zhí)行SQL的例子。代碼目錄如下。

│  pom.xml
└─src
    ├─main
    │  ├─java
    │  │  └─com
    │  │      │  Main.java
    │  │      │
    │  │      ├─entity
    │  │      │      User.java
    │  │      │
    │  │      └─mapper
    │  │              BaseDao.java
    │  │              UserDao.java
    │  │
    │  └─resources
    │      │  configuration.xml
    │      │
    │      └─mapper
    │              UserMapper.xml
    │
    └─test
        └─java


實(shí)體類

public class User {
    private Integer id;
    private String userName;
    private int age;
    private String address;
.........
}

DAO類

public interface BaseDao<T>{
     T findById(Integer id);
}
public interface UserDao extends BaseDao<User>{
}

mapper文件UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC
        "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 指明select * from user where id=#{id}這條sql對應(yīng)test.dao下的UserDao.java下的findById方法 -->
<!-- 同時findById的返回是一個User類型的類 -->
<mapper namespace="com.mapper.UserDao">
    <select id="findById" parameterType="HashMap" resultType="com.entity.User">
        select id, user_name userName, age, address from user where id=#{id}
    </select>
</mapper>

mybatis配置

<?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>
    <!-- 寫明關(guān)于mysql的連接信息 -->
    <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://localhost:3306/mybatis" />
                <property name="username" value="root" />
                <property name="password" value="6128109" />
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="./mapper/UserMapper.xml" />
    </mappers>
</configuration>

表結(jié)構(gòu)

CREATE TABLE `mybatis`.`Untitled`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

maven配置

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.9</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.19.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>
    </dependencies>

最后是執(zhí)行方法。

public class Main {
    public static void main(String[] args) throws IOException {
 SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
                .getResourceAsReader("configuration.xml"));
        SqlSession sqlSession = sessionFactory.openSession();
        UserDao userMapper = sqlSession.getMapper(UserDao.class);
        User user = userMapper.findById(1);
        System.out.println(user.toString());
    }
}
User{id=1, userName='zzl', age=18, address='法守法'}

從上面一個簡單的mybatis的例子,大家對spring整合mybatis是否有靈感,我們以UserDao 來分析一下spirng如何整合mybatis:

  1. 如果是UserDao 能通過@Autowired注入實(shí)例,那么首先需要把UserDao放入spring容器中,但是UserDao是一個接口,所以放入spring容器的是一個代理對象,這里我們可以使用FactoryBean
  2. 從最后的執(zhí)行方法可以看到,只要我們通過sqlSession.getMapper(UserDao.class)拿到mybatis中UserDao的代理對象那么我們就可以通過這個代理對象執(zhí)行方法。
  3. 通過上面分析可以知道UserDao 其實(shí)有兩個代理對象,第一次是在mybatis中生成的代理對象,第二次放入spring容器的生產(chǎn)代理對象,也就是我們通過FactoryBean生成的。

spring整合mybatis

按上面分析的思路,我們新建一個FactoryBean類。


@Component
public class UserDaoFactoryBean implements FactoryBean {

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

                SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
                        .getResourceAsReader("configuration.xml"));
                SqlSession sqlSession = sessionFactory.openSession();
                 Object result = method.invoke(userMapper, args);
                return result;
            }
        });
        return proxyInstance;
    }

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

新建一個UserService并入住UserDao

@Service
public class UserService {

    @Autowired
    UserDao userDao;

    public User getUser(int id){
      return userDao.findById(id);
    }
}

新建spring配置類

@ComponentScan("com")
public class AppConfig {
}
public class Main {
    public static void main(String[] args) throws IOException {
//        String resource = "configuration.xml";
//
//        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
//                .getResourceAsReader(resource));
//        SqlSession sqlSession = sessionFactory.openSession();
//        UserDao userMapper = sqlSession.getMapper(UserDao.class);
//        User user = userMapper.findById(1);
//        System.out.println(user.toString());

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        System.out.println(userService.getUser(1));

    }
}

執(zhí)行結(jié)果

User{id=1, userName='zzl', age=18, address='法守法'}

從上結(jié)果可以看到我們已經(jīng)把userDao整合到spring中。但是獲取SqlSessionFactory 是公共代碼我們把它抽取出來。重構(gòu)后的代碼如下。

@ComponentScan("com")
public class AppConfig {

    @Bean
    SqlSessionFactory getSessionFactory() throws IOException {
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
                .getResourceAsReader("configuration.xml"));
        return sessionFactory;
    }

}
@Component
public class UserDaoFactoryBean implements FactoryBean {

   private SqlSession sqlSession;

    public UserDaoFactoryBean(SqlSessionFactory sessionFactory) {
        this.sqlSession = sessionFactory.openSession();
    }

    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{UserDao.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                UserDao userMapper = sqlSession.getMapper(UserDao.class);
                Object result = method.invoke(userMapper, args);
                return result;
            }
        });
        return proxyInstance;
    }

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

重構(gòu)后大家對整合mybatis是不是感覺有點(diǎn)熟悉了。

通過spirng 的bean工廠后置處理器BeanFactoryPostProcessor注冊bean

UserDaoFactoryBean 中的接口類是寫死的,我們不可能為每一個接口都建一個FactoryBean,所以我們FactoryBean需要變的更通用。重構(gòu)UserDaoFactoryBean

構(gòu)造函數(shù)改為傳入接口類,新增一個setSqlSessionFactory用于注入SqlSessionFactory



public class MybatisFactoryBean implements FactoryBean {

   private SqlSession sqlSession;
   private Class mapperInterface;
  

    public MybatisFactoryBean(Class mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    
    @Override
    public Object getObject() throws Exception {
        Object proxyInstance = Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, (proxy, method, args) -> {
            UserDao userMapper = sqlSession.getMapper(UserDao.class);
            Object result = method.invoke(userMapper, args);
            return result;
        });
        return proxyInstance;
    }

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

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        this.sqlSession = sqlSessionFactory.openSession();
    }
}

為了方便理解和對比,我們在新增一個OrderDao

public interface OrderDao extends BaseDao<Order>{
}

在bean工廠后置處理器中注冊FactoryBeanBeanDefinition。(如果對bean工廠后置處理器不了解,只需知道在spring啟動的時候會自動調(diào)用postProcessBeanDefinitionRegistry方法即可)


@Component
public class MybatisBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    private Class<? extends MybatisFactoryBean> mapperFactoryBeanClass = MybatisFactoryBean.class;
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        AbstractBeanDefinition beanDefinition1 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
         beanDefinition1.setBeanClass(mapperFactoryBeanClass);
        ConstructorArgumentValues constructorArgumentValues = beanDefinition1.getConstructorArgumentValues();
        constructorArgumentValues.addGenericArgumentValue(UserDao.class);
        //自動注入BY TYPE
        beanDefinition1.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        registry.registerBeanDefinition("userDao",beanDefinition1);

        AbstractBeanDefinition beanDefinition2 = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
        beanDefinition2.setBeanClass(mapperFactoryBeanClass);
        beanDefinition2.getConstructorArgumentValues().addGenericArgumentValue(OrderDao.class);
        //自動注入BY TYPE
        beanDefinition2.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        registry.registerBeanDefinition("orderDao",beanDefinition2);
    }

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

    }
}

通過這種方式我們不用再為每一個接口都建一個FactoryBean,bean的注入模式為by type用于SqlSessionFactory的注入。

掃描

通過上面的兩個可以看出,其實(shí)除了構(gòu)造函數(shù)傳入的接口類型不一樣,其他都是一致的,那么我們是否可以約定一個目錄,我們掃描這個目錄把需要代理的接口讀進(jìn)來即可。

新增一個掃描注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MybatisImportBeanDefinitionRegistrar.class)
public @interface MybatisMapperScan {
    String value();
}

MybatisMapperScan 注解上通過@Import導(dǎo)入MybatisImportBeanDefinitionRegistrar用于注冊一個MybatisBeanDefinitionRegistryPostProcessor

public class MybatisImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

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

        Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(MybatisMapperScan.class.getName());
        String basePackage =(String) annotationAttributes.get("value");
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MybatisBeanDefinitionRegistryPostProcessor.class);
        //傳入要掃描的包路徑
        builder.addPropertyValue("basePackage", basePackage);
        builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(generateBaseBeanName(importingClassMetadata,0), builder.getBeanDefinition());
    }

    private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
        return importingClassMetadata.getClassName() + "#" + MybatisImportBeanDefinitionRegistrar.class.getSimpleName() + "#" + index;
    }
}

MybatisBeanDefinitionRegistryPostProcessor 邏輯中關(guān)鍵是MybatisClassPathBeanDefinitionScanner類,
它是ClassPathBeanDefinitionScanner的子類。(ClassPathBeanDefinitionScanner是用來掃描@Component,@Service等注解的,所以我們需要繼承這個類,并重寫相關(guān)方法,只掃描接口),另外它還實(shí)現(xiàn)BeanNameAware接口用于注入bean名字。


public class MybatisBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, BeanNameAware {

    private String basePackage;
    private String beanName;

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {

        BeanDefinition mapperScannerBean = registry.getBeanDefinition(beanName);
        PropertyValues propertyValues = mapperScannerBean.getPropertyValues();
        basePackage =  propertyValues.getPropertyValue("basePackage").getValue().toString();
        // mybatis接口掃描器
        MybatisClassPathBeanDefinitionScanner scanner = new MybatisClassPathBeanDefinitionScanner(registry);
        //這個過濾器在spring中是用來判斷是否有@Component等注解的,覆蓋默認(rèn)值,所有情況都返回true,
        scanner.addIncludeFilter((metadataReader, metadataReaderFactory) -> true);
        scanner.scan(basePackage);
        
    }

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

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

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }
}

MybatisClassPathBeanDefinitionScanner 實(shí)現(xiàn)如下


public class MybatisClassPathBeanDefinitionScanner  extends ClassPathBeanDefinitionScanner {
    private Class<? extends MybatisFactoryBean> mapperFactoryBeanClass = MybatisFactoryBean.class;

    public MybatisClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }

    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            AbstractBeanDefinition beanDefinition =(AbstractBeanDefinition) beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            //類型注入
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinition.setBeanClass(this.mapperFactoryBeanClass);
        }
        return beanDefinitionHolders;
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        //只掃描接口
        return beanDefinition.getMetadata().isInterface();
    }
}

在AppConfig中使用@MybatisMapperScan注解

@ComponentScan("com")
@MybatisMapperScan("com.mapper")
public class AppConfig {

    @Bean
    SqlSessionFactory getSessionFactory() throws IOException {
        SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(Resources
                .getResourceAsReader("configuration.xml"));
        return sessionFactory;
    }

}

執(zhí)行

   public static void main(String[] args) throws IOException {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        System.out.println(userService.getUser(1));

    }

測試結(jié)果如下


image.png
image.png

spring集成mybatis第一篇到這里就分析結(jié)束了,大家可能覺得有點(diǎn)奇怪,在MybatisImportBeanDefinitionRegistrar類中可以直接掃描,為什么還要注冊一個MybatisBeanDefinitionRegistryPostProcessor,在mybatis中整合到spring的項(xiàng)目中有兩個版本,第一個版本是在ImportBeanDefinitionRegistrar子類中實(shí)現(xiàn)掃描,第二個版本才改為通過實(shí)現(xiàn)BeanDefinitionRegistryPostProcessor在進(jìn)行掃描,至于原因,我們在第二篇源碼分析的時候在討論。

附源碼spring整合mybatis源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容