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

spring整合mybatis原理分析,分为两篇,上篇从零开始写一个spring整合mybatis的代码,带大家分析spring整合mybatis的思路。下篇分析spring整合mybatis的源码。

1 原生mybatis中执行SQL

我们先从原生mybatis执行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


实体类

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对应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>
    <!-- 写明关于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>

表结构

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>

最后是执行方法。

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注入实例,那么首先需要把UserDao放入spring容器中,但是UserDao是一个接口,所以放入spring容器的是一个代理对象,这里我们可以使用FactoryBean
  2. 从最后的执行方法可以看到,只要我们通过sqlSession.getMapper(UserDao.class)拿到mybatis中UserDao的代理对象那么我们就可以通过这个代理对象执行方法。
  3. 通过上面分析可以知道UserDao 其实有两个代理对象,第一次是在mybatis中生成的代理对象,第二次放入spring容器的生产代理对象,也就是我们通过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));

    }
}

执行结果

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

从上结果可以看到我们已经把userDao整合到spring中。但是获取SqlSessionFactory 是公共代码我们把它抽取出来。重构后的代码如下。

@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;
    }
}

重构后大家对整合mybatis是不是感觉有点熟悉了。

通过spirng 的bean工厂后置处理器BeanFactoryPostProcessor注册bean

UserDaoFactoryBean 中的接口类是写死的,我们不可能为每一个接口都建一个FactoryBean,所以我们FactoryBean需要变的更通用。重构UserDaoFactoryBean

构造函数改为传入接口类,新增一个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启动的时候会自动调用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的注入。

扫描

通过上面的两个可以看出,其实除了构造函数传入的接口类型不一样,其他都是一致的,那么我们是否可以约定一个目录,我们扫描这个目录把需要代理的接口读进来即可。

新增一个扫描注解

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

MybatisMapperScan 注解上通过@Import导入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 逻辑中关键是MybatisClassPathBeanDefinitionScanner类,
它是ClassPathBeanDefinitionScanner的子类。(ClassPathBeanDefinitionScanner是用来扫描@Component,@Service等注解的,所以我们需要继承这个类,并重写相关方法,只扫描接口),另外它还实现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等注解的,覆盖默认值,所有情况都返回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 实现如下


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;
    }

}

执行

   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));

    }

测试结果如下


image.png
image.png

spring集成mybatis第一篇到这里就分析结束了,大家可能觉得有点奇怪,在MybatisImportBeanDefinitionRegistrar类中可以直接扫描,为什么还要注册一个MybatisBeanDefinitionRegistryPostProcessor,在mybatis中整合到spring的项目中有两个版本,第一个版本是在ImportBeanDefinitionRegistrar子类中实现扫描,第二个版本才改为通过实现BeanDefinitionRegistryPostProcessor在进行扫描,至于原因,我们在第二篇源码分析的时候在讨论。

附源码spring整合mybatis源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容