当前位置:   article > 正文

Spring 整合 Mybatis 原理_spring mybatis整合原理

spring mybatis整合原理

Mybatis的基本工作原理

在 Mybatis 中,我们可以使用一个接口去定义要执行 sql,简化代码如下:定义一个接口,@Select 表示要执行查询 sql 语句。

public interface UserMapper {
	@Select("select * from user where id = #{id}")
 	User selectById(Integer id);
}
  • 1
  • 2
  • 3
  • 4

以下为执行 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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);
 	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

UserService 中的 userMapper 属性就会被自动注入为 Mybatis 中的代理对象。如果你基于一个已经完成整合的项目去调试即可发现,userMapper 的类型为:org.apache.ibatis.binding.MapperProxy@41a0aa7d。证明确实是 Mybatis 中的代理对象。

如何能够把 Mybatis 的代理对象作为一个 bean 放入 Spring 容器中?要解决这个,我们需要对 Spring 的 bean 生成过程有一个了解。

Spring中Bean的产生过程

Spring 启动过程中,大致会经过如下步骤去生成 bean:

  1. 扫描指定的包路径下的 class 文件
  2. 根据 class 信息生成对应的 BeanDefinition
  3. 在此处,程序员可以利用某些机制去修改 BeanDefinition
  4. 根据 BeanDefinition 生成 bean 实例
  5. 把生成的 bean 实例放入 Spring 容器中

假设有一个 A 类,假设有如下代码:

一个 A 类:

@Component
public class A {
}
  • 1
  • 2
  • 3

一个 B 类,不存在 @Component 注解

public class B {
}
  • 1
  • 2

执行如下代码:

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));

// 输出结果为
com.luban.util.A@6acdbdf5
  • 1
  • 2
  • 3
  • 4
  • 5

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());
 	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这样就会导致,原本的 A 类对应的 BeanDefiniton 被修改了,被修改成了 B 类,那么后续正常生成的 bean 对象的类型就是 B 类。此时,调用如下代码会报错:

context.getBean(A.class);
  • 1

但是调用如下代码不会报错,尽管 B 类上没有 @Component 注解:

context.getBean(B.class);
  • 1

并且,下面代码返回的结果是:com.luban.util.B@4b1c1ea0

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(context.getBean("a"));
  • 1
  • 2

之所以讲这个问题,是想说明一个问题:在 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);
  • 1
  • 2
  • 3

但是,这里有一个严重的问题,就是上文中的 UserMapperProxy 是我们假设的,他表示一个代理类的类型,然而 Mybatis 中的代理对象是利用的 JDK 的动态代理技术实现的,也就是代理对象的代理类是动态生成的,我们根本无法确定代理对象的代理类到底是什么。

所以回到我们的问题:Mybatis的代理对象的类型是什么?

本来可以有两个答案:

  1. 代理对象对应的代理类
  2. 代理对象对应的接口

那么答案 1 就相当于没有了,因为是代理类是动态生成的,那么我们来看答案 2 代理对象对应的接口。

BeanDefinition bd = new BeanDefinitoin();
// 注意这里,设置的是UserMapper
bd.setBeanClassName(UserMapper.class.getName());
SpringContainer.addBd(bd);
  • 1
  • 2
  • 3
  • 4

但是,实际上给 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

从结果我们可以看到,从 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

特别说一下注意二,表示当前 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 {
  • 1
  • 2

这样在启动 Spring 时就会新增一个 BeanDefinition,该 BeanDefinition 会生成一个 LubanFactoryBean 对象,并且在生成 LubanFactoryBean 对象时会传入 UserMapper.class 对象,通过 LubanFactoryBean 内部的逻辑,相当于会自动生产一个 UserMapper 接口的代理对象作为一个 bean。

总结

总结一下,通过我们的分析,我们要整合Spring和Mybatis,需要我们做的事情如下:

  1. 定义一个 LubanFactoryBean
  2. 定义一个 LubanImportBeanDefinitionRegistrar
  3. 在 AppConfig 上添加一个注解 @Import(LubanImportBeanDefinitionRegistrar.class)

优化

这样就可以基本完成整合的需求了,当然还有两个点是可以优化的
第一,单独再定义一个 @LubanScan 的注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Import(LubanImportBeanDefinitionRegistrar.class)
public @
  • 1
  • 2
  • 3

这样在 AppConfig 上直接使用 @LubanScan 即可

第二,在 LubanImportBeanDefinitionRegistrar 中,我们可以去扫描 Mapper ,在LubanImportBeanDefinitionRegistrar 我们可以通过 AnnotationMetadata 获取到对应的 @LubanScan 注解,所以我们可以在 @LubanScan 上设置一个 value ,用来指定待扫描的包路径。然后在 LubanImportBeanDefinitionRegistrar 中获取所设置的包路径,然后扫描该路径下的所有 Mapper,生成 BeanDefinition,放入 Spring 容器中。

所以,到此为止,Spring 整合 Mybatis 的核心原理就结束了,再次总结一下:

  1. 定义一个 LubanFactoryBean,用来将 Mybatis 的代理对象生成一个 bean 对象
  2. 定义一个 LubanImportBeanDefinitionRegistrar,用来生成不同 Mapper 对象的 LubanFactoryBean
  3. 定义一个 @LubanScan,用来在启动 Spring 时执行 LubanImportBeanDefinitionRegistrar 的逻辑,并指定包路径

以上这个三个要素分别对象 org.mybatis.spring 中的:
MapperFactoryBean
MapperScannerRegistrar
@MapperScan

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/892626
推荐阅读
相关标签
  

闽ICP备14008679号