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 的类
原文地址:https://www.cnblogs.com/kendoziyu/p/how-mybatis-spring-work.html