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 的底层逻辑进行了封装。
当调用 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:
- 扫描指定的包路径下的 class 文件
- 根据 class 信息生成对应的 BeanDefinition
- 在此处,程序员可以利用某些机制去修改 BeanDefinition
- 根据 BeanDefinition 生成 bean 实例
- 把生成的 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 代理对象对应的接口。
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 对象,比如:
我们定义了一个 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 ,让他变得更通用,比如:
改造 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 技术,比如可以这么实现
定义如下类:
并且在 AppConfig 上添加 @Import 注解:
@Import(LubanImportBeanDefinitionRegistrar.class)
public class AppConfig {
这样在启动 Spring 时就会新增一个 BeanDefinition,该 BeanDefinition 会生成一个 LubanFactoryBean 对象,并且在生成 LubanFactoryBean 对象时会传入 UserMapper.class 对象,通过 LubanFactoryBean 内部的逻辑,相当于会自动生产一个 UserMapper 接口的代理对象作为一个 bean。
总结
总结一下,通过我们的分析,我们要整合Spring和Mybatis,需要我们做的事情如下:
- 定义一个 LubanFactoryBean
- 定义一个 LubanImportBeanDefinitionRegistrar
- 在 AppConfig 上添加一个注解 @Import(LubanImportBeanDefinitionRegistrar.class)
优化
这样就可以基本完成整合的需求了,当然还有两个点是可以优化的
第一,单独再定义一个 @LubanScan 的注解,如下:
@Retention(RetentionPolicy.RUNTIME)
@Import(LubanImportBeanDefinitionRegistrar.class)
public @
这样在 AppConfig 上直接使用 @LubanScan 即可
第二,在 LubanImportBeanDefinitionRegistrar 中,我们可以去扫描 Mapper ,在LubanImportBeanDefinitionRegistrar 我们可以通过 AnnotationMetadata 获取到对应的 @LubanScan 注解,所以我们可以在 @LubanScan 上设置一个 value ,用来指定待扫描的包路径。然后在 LubanImportBeanDefinitionRegistrar 中获取所设置的包路径,然后扫描该路径下的所有 Mapper,生成 BeanDefinition,放入 Spring 容器中。
所以,到此为止,Spring 整合 Mybatis 的核心原理就结束了,再次总结一下:
- 定义一个 LubanFactoryBean,用来将 Mybatis 的代理对象生成一个 bean 对象
- 定义一个 LubanImportBeanDefinitionRegistrar,用来生成不同 Mapper 对象的 LubanFactoryBean
- 定义一个 @LubanScan,用来在启动 Spring 时执行 LubanImportBeanDefinitionRegistrar 的逻辑,并指定包路径
以上这个三个要素分别对象 org.mybatis.spring 中的:
MapperFactoryBean
MapperScannerRegistrar
@MapperScan