• Spring源码之一步步拓展实现spring-mybatis


    讲在前面

    上一章 Spring源码之BeanFactoryPostProcessor的执行顺序,我们掌握了 BeanFactoryPostProcessor 的执行顺序。
    这一章,我们就来看一下程序员要如何使用 BeanFactoryPostProcessor 对 Spring 进行拓展? 本文以 mybatis 为例,看看 mybatis-spring 是如何将 Spring 和 Mybatis 做整合的?
    首先,我们当然需要通过官方网站来了解 mybatis 和 mybatis-spring :

    网站 网址
    mybatis英文 https://mybatis.org/mybatis-3/
    mybatis中文 https://mybatis.org/mybatis-3/zh/
    mybatis-spring英文 https://mybatis.org/spring/
    mybatis-spring中文 https://mybatis.org/spring/zh/

    这次实验需要用到的,写在子模块 build.gradle 中的依赖

    dependencies {
        compile(project(":spring-context"))
        compile('org.mybatis:mybatis:3.5.0')
        compile('org.mybatis:mybatis-spring:2.0.5')
        // JDBC驱动
        compile('mysql:mysql-connector-java:8.0.20')
        // 轻量连接池
        compile(project(":spring-jdbc"))
        testCompile group: 'junit', name: 'junit', version: '4.12'
    }
    

    MyBatis-Spring Quick Start

    配置类

    @MapperScan("coderead.springframework.dao")
    @ComponentScan("coderead.springframework")
    public class AppConfig {
    
    	@Bean
    	public SqlSessionFactory sqlSessionFactory() throws Exception {
    		SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
    		factoryBean.setDataSource(dataSource());
    		return factoryBean.getObject();
    	}
    
    	@Bean
    	public DataSource dataSource() {
    		DriverManagerDataSource dataSource = new DriverManagerDataSource();
    		dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    		dataSource.setUrl("jdbc:mysql://localhost:3306/test?serverTimezone=UTC");
    		dataSource.setUsername("root");
    		dataSource.setPassword("123456");
    		return dataSource;
    	}
    }
    
    • SqlSessionFactoryBean 正是 FactoryBean 的子类,也是依赖项 mybatis-spring 的类
    • DriverManagerDataSource 是依赖项目 spring-jdbc 中的连接池的类,这个类在开发演示中比较轻量和简单。复杂的商用连接池有 druidj3p0
    • MapperScan 告诉 Spring 要去哪里扫描 DAO

    常见服务类:

    @Component
    public class UserService {
    
    	@Autowired
    	private UserDao dao;
    
    	public List<Map<String, Object>> query() {
    		return dao.query();
    	}
    
    }
    
    • 这是我们项目中常写的服务类的样式,使用 @Component 注解 UserService 类,并且使用 @Autowired 注解自动注入 UserDao 的实例

    DAO接口类:

    public interface UserDao {
    
    	@Select("select * from user")
    	public List<Map<String,Object>> query();
    }
    

    应用启动类:

    public class MyApplication {
    
    	public static void main(String[] args) throws IOException {
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    		ctx.register(AppConfig.class);
    		ctx.refresh();
    
    		UserService service = ctx.getBean(UserService.class);
    		System.out.println(service.query());
    	}
    }
    

    如果你比较倾向于看官方文档,那么在此给你推荐:

    Mybatis Quick Start

    我们来看一下如何使用 Java 的方式获取 SqlSessionFactory 对象:

    依赖结构如下:

    当我们得到了 SqlSessionFactory 对象,我们就可以获取 SqlSession 对象,进而得到 Mapper 对象:

    SqlSession session = sqlSessionFactory.openSession();
    UserDao mapper = session.getMapper(UserDao.class);
    System.out.println(mapper.query());
    

    SqlSession.getMapper可以帮我们得到一个对象,且这个对象必然是实现了 UserDao 接口的。
    要满足这两点,常用的核心技术就是JDK 动态代理

    /**
     * 根据接口类(可以多个),生成一个动态代理对象
     * @param loader 类加载器
     * @param interfaces 需要代理对象实现的一组接口
     * @param h 分发方法触发的处理器
     */
    Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h);
    

    InvocationHandler 接口

    /**
     * 处理代理对象的触发方法,并且返回结果对象
     * @param proxy 代理对象
     * @param method 反射方法
     * @param args 方法参数
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    

    SqlSession#getMapper方法的调用时序图如下图所示:

    • DefaultSqlSession 依赖成员变量 Configuration,所以可以通过该成员变量直接调用 configuration#getMapper(Class cls, SqlSession session)
    • Configuration、MapperRegistry 的 getMapper方法参数除了 Class 类对象参数,还有 SqlSession 对象参数(即上一步的 DefaultSqlSession 对象)

    如果你比较倾向自己去官方网站查证,那么在此推荐链接:

    过程简化

    我们知道了原理是JDK动态代理,那么我们可以用 MockSqlSession 简化模拟一个 Mybatis 的 SqlSession 来执行 getMapper 方法。当触发代理接口对象的方法时的逻辑是先获取数据库连接,然后执行 sql 语句,我们都用打印日志的方式来简化示意。

    public class MockSqlSession {
    
    	public static Object getMapper(Class mapper) {
    		Class[] classes = new Class[]{mapper};
    		return Proxy.newProxyInstance(MockSqlSession.class.getClassLoader(), classes, new MyInvocationHandler());
    	}
    
    	static class MyInvocationHandler implements InvocationHandler {
    		@Override
    		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    			System.out.println("------- getConnection -------");
    			Select select = method.getAnnotation(Select.class);
    			String[] sqls = select.value();
    			System.out.println("------- execute : " + sqls[0] + " -------");
    			return null;
    		}
    	}
    }
    

    接着我们不用 mybatis-spring 中的 MapperScan, 也不用 SqlSessionFactoryBean, 因此我们来对 AppConfig 类做一些删减:

    @ComponentScan("coderead.springframework")
    public class AppConfig {
          
    }
    

    然后我们再次运行 MyApplication,运行之后出现如下异常

    Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userService': Unsatisfied dependency expressed through field 'dao'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'coderead.springframework.dao.UserDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

    分析: 无法创建 userService Bean对象,因为无法注入 UserDao。找不到 UserDao 的 BeanDefinition,Spring 没法帮我们创建一个 userDao Bean对象。MyBatis 是用 JDK 动态代理来创建 UserDao 对象的,MyBatis 需要自己掌控 UserDao 的创建过程,因此不能把类交给 Spring 管理,而是要把创建好的对象交给 Spring 管理!

    把对象交给 Spring 管理

    如何把 MyBatis 的对象交给 Spring 管理?可选的方法有:

    1. FactoryBean
    2. AnnotationConfigApplicationContext#getBeanFactory().registerSingleton()
    3. @Bean

    registerSingleton

    我们向 Spring 中注入了一个 Singleton 对象

    public class MyApplication {
    
    	public static void main(String[] args) throws IOException {
    		UserDao dao = (UserDao) MockSqlSession.getMapper(UserDao.class);
    
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    		ctx.getBeanFactory().registerSingleton("userDao", dao);
    		ctx.register(AppConfig.class);
    		ctx.refresh();
    
    		UserService service = ctx.getBean(UserService.class);
    		System.out.println(service.query());
    	}
    }
    

    这种写法意味着,每多一个 XXXDao,都需要调用获取 XXXDao 对象,并通过 registerSingleton 方法注册到 Spring 中去。

    @Bean

    还原 MyApplication 类,修改 AppConfig 类改用注解方法注入 UserDao

    public class MyApplication {
    
    	public static void main(String[] args) throws IOException {
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    		ctx.register(AppConfig.class);
    		ctx.refresh();
    
    		UserService service = ctx.getBean(UserService.class);
    		System.out.println(service.query());
    	}
    }
    

    AppConfig类

    @ComponentScan("coderead.springframework")
    public class AppConfig {
    
    	@Bean(name = "userDao")
    	public UserDao userDao() {
    		return (UserDao) MockSqlSession.getMapper(UserDao.class);
    	}
    }
    

    这种写法意味着,每多一个 XXXDao,都需要写一段相似度极高的@Bean方法,注册 XXXDao Bean对象到 Spring 中去。假如有 100 个 Dao,那么 AppConfig 中就有 100 段 @Bean 的代码

    FactoryBean

    FactoryBean 是一个特殊的Bean,它必须实现一个接口,这个 FactoryBean 还能产生一个 Bean。
    我们去掉 AppConfig 中的 @Bean 的代码

    @Component("userDao")
    public class MockFactoryBean implements FactoryBean {
    	@Override
    	public Object getObject() throws Exception {
    		return MockSqlSession.getMapper(UserDao.class);
    	}
    
    	@Override
    	public Class<?> getObjectType() {
    		return UserDao.class;
    	}
    }
    

    这里需要注意的是虽然 "userDao" 在单例池 singletonObjects 中对应的是 MockFactoryBean 对象:

    但是,使用"userDao"这个名称获取到的 Bean 是 MockFactoryBean 所产生的 Bean 对象,即实现了 UserDao 的动态代理对象。

    public class MyApplication {
    
    	public static void main(String[] args) throws IOException {
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    		ctx.register(AppConfig.class);
    		ctx.refresh();
    
    		System.out.println(ctx.getBean("userDao") instanceof UserDao);
    	}
    }
    

    实验结果如下:

    如果想要获取 MockFactoryBean 对象,应该 ctx.getBean("&userDao"), beanName 多加上一个 & 符号。
    MockFactoryBean.getObject() 获取的对象会存放在 FactoryBeanRegistrySupport 成员变量 factoryBeanObjectCache 中:

    MyBatis 使用的就是 FactoryBean 的方式来把对象交给 Spring 管理的

    MapperFactoryBean

    MyBatis 是以第三方 jar 的形式被我们所使用的,最好不要用@Component注解,因为那样需要用户配置扫描 MyBatis 的包。如果本来是内部项目,然后捐给了 Apache,强制要修改包名了,配置扫描那就不太好。

    这里使用了 mapperInterface 变量支持动态创建不同 DAO 接口的 MapperFactoryBean 对象。解决的问题:

    原来需要每新增一个 XXXDAO 类,就要对应新增一个 XXXFactoryBean。现在方便了,用户创建再多的 XXXDAO 类,也只需要一个 MapperFactoryBean 类!。这就是设计成员变量 mapperInterface 的作用!
    我们再来改写我们的 MockFactoryBean,删除 @Component 注解,新增成员变量 mapperInterface:

    public class MockFactoryBean implements FactoryBean {
    	private Class mapperInterface;
    
            public MockFactoryBean() {
    	}
    
    	public MockFactoryBean(Class mapperInterface) {
    		this.mapperInterface = mapperInterface;
    	}
    
    	public void setMapperInterface(Class mapperInterface) {
    		this.mapperInterface = mapperInterface;
    	}
    
    	@Override
    	public Object getObject() throws Exception {
    		return MockSqlSession.getMapper(mapperInterface);
    	}
    
    	@Override
    	public Class<?> getObjectType() {
    		return mapperInterface;
    	}
    }
    

    Injecting Mappers

    既然我们不使用 @Component,现在就需要我们来考虑注入的逻辑了。首先是注入一个 Mapper 类的方法,官网上提供了指南

    注册单个 mapper

    首先修改 AppConfig 类,添加一个 @Bean 注解注册一个 MockFactoryBean,参数是 UserDao.class。

    @ComponentScan("coderead.springframework")
    public class AppConfig {
    
    	@Bean
    	public MockFactoryBean userFactoryBean() {
    		MockFactoryBean bean = new MockFactoryBean(UserDao.class);
    		return bean;
    	}
    }
    

    然后测试 MyApplication :

    public class MyApplication {
    
    	public static void main(String[] args) throws IOException {
    		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    		ctx.register(AppConfig.class);
    		ctx.refresh();
    
    		UserDao dao = (UserDao) ctx.getBean(UserDao.class);
    		System.out.println(dao.query());
    	}
    }
    

    这种注入单个 Mapper 的方式有一个非常大的缺陷,每次新增一个 XXXDao,AppConfig 类中就需要新增加一段“类似”的 @Bean 的代码,只是把构造函数的入参改为 XXXDao.class 这一点变化。
    为了解决这个缺陷,MyBatis 提供的方案是扫描所有的 Mapper。

    模拟扫描

    如果希望自定义的 FactoryBean 能够提供我们所期望的 Bean 对象,首先要保证 FactoryBean 在 Spring 容器中。那么,如何使得 FactoryBean 在 Spring 容器中呢?

    1. 加 @Component 注解,但是这个方法不能传递 mapperInterface 参数,该方法 pass!
    2. spring.xml 中加入 <bean> 标签(或者在 AppConfig 类中加入 @Bean) ———— 该方式可以传递 mapperInterface 参数,但是不能实现扫描功能,该方法 pass!
    3. 拓展 Spring ,把自定义 FactoryBean 对应类的 BeanDefinition 放入 beanDefinitionMap 中。

    首先想到使用 BeanPostProcessor ,但是不可行。因为 postProcessBeanFactory(ConfigurableListableBeanFactory factory) 方法只能修改已有的 BeanDefinition,不能新增 BeanDefinition!

    新增 BeanDefinition

    新增 BeanDefinition 需要借助 ImportBeanDefinitionRegistrar:

    public class MockImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
    	@Override
    	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    		BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MockFactoryBean.class);
    		builder.addPropertyValue("mapperInterface", "coderead.springframework.dao.UserDao");
    		BeanDefinition bd = builder.getBeanDefinition();
    		registry.registerBeanDefinition("mockFactoryBean", bd);
    	}
    }
    

    这段代码需要注意的是 builder.addPropertyValue("mapperInterface", "coderead.springframework.dao.UserDao"),实际上调用的代码是 this.beanDefinition.getPropertyValues().add("mapperInterface", "coderead.springframework.dao.UserDao"),这里"coderead.springframework.dao.UserDao"表示的是类的路径。

    使 ImportBeanDefinitionRegistrar 生效

    使用 @Import 注解来使得自定义的 MockImportBeanDefinitionRegistrar 生效

    @ComponentScan("coderead.springframework")
    @Import(MockImportBeanDefinitionRegistrar.class)
    public class AppConfig {
    }
    

    批量添加 BeanDefinition

    public class MockImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
    	@Override
    	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    		List<Class> list = new ArrayList<>();
    		list.add(UserDao.class);
    		// ...这里还可以有更多 Dao 
    
    		for (Class aClass : list) {
    			BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MockFactoryBean.class);
    			builder.addPropertyValue("mapperInterface", aClass.getName());
    			BeanDefinition bd = builder.getBeanDefinition();
    			registry.registerBeanDefinition(aClass.getSimpleName(), bd);
    		}
    	}
    }
    

    总结

    由于篇幅原因,本文就不再继续介绍实现扫描的方法,下一篇文章,会分析 mybatis-spring 的源码,来看看 mybatis 到底是如何实现扫描的。
    通过本文,我们可以学到的是

    1. 把对象交给 Spring 管理的几种方法:① FactoryBean ② @Bean ③registerSingleton
    2. 新增 BeanDefinition 的方法:加上 @Import 注解实现 ImportBeanDefinitionRegistrar 的类
  • 相关阅读:
    HDU4529 郑厂长系列故事——N骑士问题 —— 状压DP
    POJ1185 炮兵阵地 —— 状压DP
    BZOJ1415 聪聪和可可 —— 期望 记忆化搜索
    TopCoder SRM420 Div1 RedIsGood —— 期望
    LightOJ
    LightOJ
    后缀数组小结
    URAL
    POJ3581 Sequence —— 后缀数组
    hdu 5269 ZYB loves Xor I
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/how-mybatis-spring-work.html
Copyright © 2020-2023  润新知