SpringBoot集成MyBatis底层原理及简易实现
MyBatis是可以说是目前最主流的Spring持久层框架了,本文主要探讨SpringBoot集成MyBatis的底层原理。完整代码可移步Github。
如何使用MyBatis
一般情况下,我们在SpringBoot项目中应该如何集成MyBatis呢?
- 引入MyBatis依赖
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
- 配置数据源
- 在启动类上添加
@MapperScan注解,传入需要扫描的dao层的包路径 - 在dao层中创建接口,在方法上传入对应的SQL语句,也可以使用Mapper.xml文件进行配置。例如:
public interface UserDao {
@Select("insert into user xxx")
void insert();
}
- 完成这些工作后,我们就可以自动注入一个userDao,然后调用userDao.insert()方法来实现对数据库的操作。
那么问题来了,MyBatis是如何通过如此简单的配置完成完成与Spring的“无缝连接”和数据的持久化工作的呢?
Spring的BeanDefinition
众所周知,Spring的一大特性是IoC,既控制反转。当我们将一个对象交给Spring管理之后,我们就不需要手动地通过new关键字去创建对象,只需要通过@Autowired或者@Resource自动注入即可。那么这个过程是如何实现的呢?
简单来说,Spring会通过一个被声明为bean的类信息生成一个beanDefinition(后面简称BD)对象,然后通过这个BD对象创建一个单例(不声明为prototype的情况下),存入单例池,需要时进行调用。
在创建beadDefinition时,Spring会调用一系列的后置处理器(postProcessor)对BD加以处理,我们也可以自定义后置处理器,对BD的属性进行修改,只需要实现BeanFactoryPostProcessor接口即可,例如:
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
GenericBeanDefinition userDaoBD = (GenericBeanDefinition)beanFactory.getBeanDefinition("userDao");
userDaoBD.setBeanClassName("userDaoChanged");
userDaoBD.setAbstract(false);
// more...
}
}
在这个postProcessor中,我们获取了UserDao的BD对象,并且将它的名字修改为"userDaoChanged",这样我们就可以通过调用ApplicationContext的getBean("userDaochanged")方法获取到原来的userDao的bean。
关于MyBatis
现在我们知道了,当我们将一个类交给Spring管理时,Spring通过beanDefinition构建bean单例。现在,我们又有了两个新的问题:
- MyBatis如何将dao交给Spring管理的?
- 我们编写的dao是一个接口,接口是如何实例化的?
MyBatis如何将dao交给Spring管理的?
在Spring中,将一个对象交给Spring管理主要有三种方式:
- @Bean
- beanFactory.registerSingleton(String beanName, Object singletonObject)
- FactoryBean
其中MyBatis使用的是FactoryBean的方式,实现FactoryBean接口就可以将我们的userDao注入到Spring当中。
beanFactory管理着Spring所有的bean,是一个大工厂。FactoryBean也是一个bean,但它却有着Factory的功能,当我们调用Spring上下文的getBean()方法,并传入自定义的FactoryBean时,返回的bean并不是这个FactoryBean本身,而是重写的getObject()方法中所返回的对象。
如此看来,FactoryBean就是一个“小工厂”。
@Component
public class UserFactoryBean implements FactoryBean {
UserDao userDao;
@Override
public Object getObject() throws Exception {
return userDao;
}
@Override
public Class<?> getObjectType() {
return UserDao.class;
}
}
只是这样写当然是不能满足我们的要求的,我们这时候调用getBean()方法会报错,我们无法传入一个userDao参数,因为UserDao不能被实例化。但是在MyBatis中,我们却可以通过sqlSession.getMapper(UserDao.class)方法获取到一个UserDao的实例化对象,MyBatis是如何做到这一点的?
如何实例化一个接口?
为什么接口可以被实例化呢?查看MyBatis的源码我们可以得知,MyBatis通过动态代理(Proxy)的技术在接口的基础上包装出了一个对象,然后将这个对象交给了Spring。沿着getMapper方法追根溯源我们可以发现这样一个方法:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
MyBatis可以,那我们也可以,我们改写一下UserFatoryBean:
@Component
public class UserFactoryBean implements FactoryBean {
@Override
public Object getObject() throws Exception {
Class[] classes = new Class[]{UserDao.class};
return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
}
@Override
public Class<?> getObjectType() {
return UserDao.class;
}
}
Proxy.newProxyInstance()方法接收三个参数,分别为:
- ClassLoader loader:决定用哪个类加载器来加载生成的代理对象
- Class<?>[] interfaces:决定这个代理对象要实现哪些接口,拥有哪些功能
- InvocationHandler h:决定调用这个代理对象的某个方法时执行的具体逻辑
然后编写一个InvokationHandler类用于执行代理对象的具体方法逻辑:
public class MyInvokationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("假装查询数据库:" + method.getAnnotation(Select.class).value()[0]);
return null;
}
}
在这个handler中,我们获取到@Select注解中的信息,然后将它打印出来。
OK,现在我们运行一下:
@Test
void contextLoads() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
UserDao userDao = (UserDao) ac.getBean("userFactoryBean");
userDao.insert();
}
控制台输出:
假装查询数据库:insert into user xxx
至此,我们就完成了MyBatis的简易实现的一小部分。但是还有一个重要的问题:我们这个FactoryBean是写死的,只能返回UserDao的代理对象,实际情境下,我们如何根据用户传入的参数返回不同的代理对象呢?
动态生成代理对象
想要动态生成代理对象,首先我们需要修改UserFactoryBean的代码,让它能适配各种类型的dao,不妨直接改个名字叫MyFactoryBean:
public class MyFactoryBean implements FactoryBean {
Class mapperInterface;
// 为了支持XML配置,必须提供一个默认构造方法
public MyFactoryBean(){}
// 通过MapperScan方式
public MyFactoryBean(Class mapperInterface){
this.mapperInterface = mapperInterface;
}
@Override
public Object getObject() throws Exception {
Class[] classes = new Class[]{mapperInterface};
return Proxy.newProxyInstance(MyFactoryBean.class.getClassLoader(),classes, new MyInvokationHandler());
}
@Override
public Class<?> getObjectType() {
return mapperInterface;
}
}
我们将UserDao.class改成了动态的mapperInterface,那么我们如何向MyFactoryBean的构造方法传入这个参数呢?这就回到了我们一开始说到的beanDefinition,在Spring中,可以在BD期间修改bean的各种属性,这其中就包括构造方法的参数。我们修改我们一开始写的MyBeanFactoryPostProcessor:
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
List<Class> daos = new ArrayList<>();
// 获取所有dao
daos.add(UserDao.class);
daos.add(AnchorDao.class);
for(Class dao:daos){
// 获取一个空的beanDefinition
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
// 为构造方法添加参数
beanDefinition.setBeanClass(MyFactoryBean.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(dao);
}
}
}
这样的话我们就构造出了我们想要的beanDefinition,现在要做的是把它加入到Spring容器中去。注意:是把beanDefinition加入到Spring容器中,而不是把bean加到Spring容器中。我们前面说的@Bean之内的方法是不适用的。
这里需要用到另两个知识点:@Import和ImportBeanDefinitionRegistrar
在应用中,有时没有把某个类注入到IOC容器中,但在运用的时候需要获取该类对应的bean,此时就需要用到@Import注解。
@Import最强大的地方在于,它提供了一个扩展点给用户。当我们用@Import导入的类实现了ImportBeanDefinitionRegistrar接口时,Spring不会直接将这个类包装成一个bean,而是执行其内部的registerBeanDefinitions方法。这有点像FactoryBean,可以在类中执行自己的逻辑。
我们编写这样一个registerBeanDefinitions:
public class ImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry){
// 获得包名,遍历获得所有类名
String packagePath = Appconfig.class.getAnnotation(MyScan.class).path();
List<String> classNames = SelectClassName.selectByPackage(packagePath);
for(String className:classNames){
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
GenericBeanDefinition genericBeanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
registry.registerBeanDefinition(SelectClassName.getShortName(className),genericBeanDefinition);
// 添加构造方法参数
genericBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(className);
}
}
}
并且编写一个MyScan注解类用户获取需要扫描的包名:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyScan {
String path();
}
然后在AppConfig类上加入@MyScan注解,传入包名,最后编写一个工具类用来获取包下的所有类名。MyBeanFactoryPostProcessor类也可以删除了,ImportBeanDefinitionRegister替代了它的工作。
(完整代码可以访问我的Github)
@Test
void contextLoads() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
ac.getBean(AnchorDao.class).query();
ac.getBean(UserDao.class).insert();
}
控制台输出:
假装查询数据库:select * from anchor limit 5
假装查询数据库:insert into user xxx
大功告成!回头再看一下我们是如何使用MyBatis的:@MapperScan、编写dao接口、@Select——和我们现在的功能几乎完全一样。只需要在MyInvokationHandler中封装一下JDBC,实现具体的访问数据库逻辑,你就可以在项目中使用自己编写的“MyBatis”了。
总结
Spring提供了很好的环境用于第三方框架的开发,这也是Spring能发展出如今这样庞大且完善的生态的原因之一。知识都是触类旁通的,例如Spring的另一大特性:AOP,它就与本文谈到的后置处理器beanPostProcessor和动态代理有关,对SpringBoot集成MyBatis底层原理的学习和研究,让我对Spring和MyBatis都有了更深入的认识。(累死我了,歇会儿(;´д`)ゞ)
SpringBoot集成MyBatis底层原理及简易实现的更多相关文章
- springboot集成mybatis(二)
上篇文章<springboot集成mybatis(一)>介绍了SpringBoot集成MyBatis注解版.本文还是使用上篇中的案例,咱们换个姿势来一遍^_^ 二.MyBatis配置版(X ...
- springboot集成mybatis(一)
MyBatis简介 MyBatis本是apache的一个开源项目iBatis, 2010年这个项目由apache software foundation迁移到了google code,并且改名为MyB ...
- SpringBoot 集成Mybatis 连接Mysql数据库
记录SpringBoot 集成Mybatis 连接数据库 防止后面忘记 1.添加Mybatis和Mysql依赖 <dependency> <groupId>org.mybati ...
- SpringBoot Mybatis整合(注解版),SpringBoot集成Mybatis(注解版)
SpringBoot Mybatis整合(注解版),SpringBoot集成Mybatis(注解版) ================================ ©Copyright 蕃薯耀 2 ...
- SpringBoot集成Mybatis并具有分页功能PageHelper
SpringBoot集成Mybatis并具有分页功能PageHelper 环境:IDEA编译工具 第一步:生成测试的数据库表和数据 SET FOREIGN_KEY_CHECKS=0; ...
- Springboot集成mybatis(mysql),mail,mongodb,cassandra,scheduler,redis,kafka,shiro,websocket
https://blog.csdn.net/a123demi/article/details/78234023 : Springboot集成mybatis(mysql),mail,mongodb,c ...
- BindingException: Invalid bound statement (not found)问题排查:SpringBoot集成Mybatis重点分析
重构代码,方法抛出异常:BindingException: Invalid bound statement (not found) 提示信息很明显:mybatis没有提供某方法 先不解释问题原因和排查 ...
- SpringBoot集成Mybatis配置动态数据源
很多人在项目里边都会用到多个数据源,下面记录一次SpringBoot集成Mybatis配置多数据源的过程. pom.xml <?xml version="1.0" encod ...
- SpringBoot集成Mybatis实现多表查询的两种方式(基于xml)
下面将在用户和账户进行一对一查询的基础上进行介绍SpringBoot集成Mybatis实现多表查询的基于xml的两种方式. 首先我们先创建两个数据库表,分别是user用户表和account账户表 ...
随机推荐
- Springboot学习笔记【持续更新】
1.Springboot四大核心: 自动配置 与Spring应用程序和常见的应用功能,Springboot能自动提供相关配置 起步依赖 告诉Springboot需要什么功能,它就能引入需要的依赖库 A ...
- 详解Redis持久化(RDB和AOF)
详解Redis持久化(RDB和AOF) 什么是Redis持久化? Redis读写速度快.性能优越是因为它将所有数据存在了内存中,然而,当Redis进程退出或重启后,所有数据就会丢失.所以我们希望Red ...
- CSS3动画的使用以及优化
CSS3 动画 目录 1. 定义动画2. animation 属性3. animation 属性的兼容4. animation与transition 属性的取别5. animate.css 动画库6. ...
- windows10远程桌面,出现“出现身份验证错误 要求的函数不受支持...”等错误解决方法
windows家庭普通版,更新补丁后无法远程连接windows server2012,出现以下报错: 解决方法: 1.win + R打开运行,输入 regedit,回车进入注册表 2.找到以下路径 \ ...
- 面试官:ThreadLocal的应用场景和注意事项有哪些?
前言 ThreadLocal主要有如下2个作用 保证线程安全 在线程级别传递变量 保证线程安全 最近一个小伙伴把项目中封装的日期工具类用在多线程环境下居然出了问题,来看看怎么回事吧 日期转换的一个工具 ...
- 深入理解Java AIO(一)—— Java AIO的简单使用
深入理解Java AIO(一)—— Java AIO的简单使用 深入理解AIO系列分为三个部分 第一部分也就是本节的Java AIO的简单使用 第二部分是AIO源码解析(只解析关键部分)(待更新) 第 ...
- 瀑布流vue-waterfall的高度设置
最近用vue做项目,用到了瀑布流vue-waterfall,其中遇到高度的设置问题,大概介绍下,希望可以帮到一些人 1.安装 npm install --save vue-waterfall 2.引入 ...
- [codevs2597]团伙<并查集>
题目描述 Description 1920年的芝加哥,出现了一群强盗.如果两个强盗遇上了,那么他们要么是朋友,要么是敌人.而且有一点是肯定的,就是: 我朋友的朋友是我的朋友: 我敌人的敌人也是我的朋友 ...
- 关于 IDEA 启动 springboot 项目异常 - Disconnected from the target VM, address: '127.0.0.1:59770', transport: 'socket'
关于 IDEA 启动 springboot 项目异常 - Disconnected from the target VM, address: '127.0.0.1:59770', transport: ...
- Sublime Text 2 Install Package Debug
本文转载自CSDN空间freshlover的博客<Sublime Text 无法使用Package Control或插件安装失败的解决方法>,转载请注明出处,谢谢! Sublime Tex ...