1. 前言
本文只是理清脉络,没有深究各个具体位置的实现原理,先心中有个大概思路:
在看下面内容之前,最好先大致了解一下spring的启动流程,spring扫描器扫描流程,bean的生命周期等信息
mybatis大致通过以下几个步骤实现了与springboot的集成:
springboot开启自动注入(标注@EnableAutoConfiguration注解表示开启自动注入,但其实@SpringBootConfiguration注解已经帮助我们引入了@EnableAutoConfiguration注解,所以无需我们手动标注@EnableAutoConfiguration注解)。springboot启动根据MybatisAutoConfiguration自动配置类将SqlSessionTemplate注册到spring容器。
@MapperScan注解里有@Import注解,@import注解导入了MapperScannerRegistrar类,MapperScannerRegistrar由于实现了ImportBeanDefinitionRegistrar接口,spring扫描解析到@Import注解,会马上执行ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法(该接口方法可编程式的注册bean)。
ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法向spring注册了MapperScannerConfigurer。
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,spring扫描完指定路径下所有的类后会调用BeanDefinitionRegistryPostProcessor接口的postProcessBeanDefinitionRegistry方法,该方法可以向spring的beanDefinition容器中注册beanDefinition(beanDefinition是bean的模板对象,spring会先将扫描到的类以beanDefinition类型进行存储,等到要实例化和初始化的时候,再将beanDefinition变成真正的bean存储)。
MapperScannerConfigurer通过postProcessBeanDefinitionRegistry方法向spring中注册了mybatis的定制化扫描器ClassPathMapperScanner。ClassPathMapperScanner继承了ClassPathBeanDefinitionScanner,获取了父类的扫描功能。ClassPathMapperScanner通过重写isCandidateComponet方法实现定制化过滤,会保留所有扫描到的接口类。
ClassPathMapperScanner在重写的doScan()方法中除了完成上面的扫描,后面还会把上面扫描到的接口类通过动态代理生具体的实现类。具体生成方法后面会讲到。
在了解mybatis如何与springboot集成之前,先回顾一下mybatis最初的使用方法:
- 首先解析配置文件mybatis-config.xml,并根据mybatis-config.xml中配置的mapper.xml的路径信息找到mapper.xml一起解析。将所有解析到的信息放到一个全局的mybatis的configuration配置文件中,供后面使用。
- 生成一个sqlSession的工厂,该工厂实现了SqlSessionFactory接口,具体实现类是DefaultSqlSessionFactory,通过该工厂的openSession()方法能获取到具体的SqlSession实现类对象。
- sqlSession中有操作数据库的增删改查方法,我们操作数据库时都离不开这个sqlSession。
- 通过动态代理技术,生成PersonDao接口的代理类,代理类中的findAll()方法实际上就是调用sqlSession中的selectList()方法。
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
SqlSession sqlSession = sqlSessionFactory.openSession();
PersonDao personDao = sqlSession.getMapper(PersonDao.class);
List<Person> all = personDao.findAll();
2. SpringBoot自动注入原理
2.1 如何获取自动配置类MybatisAutoConfiguration
springboot的启动类通常情况下都会打上@SpringBootApplication注解。@SpringBootApplication注解引入了@EnableAutoConfiguration注解。@EnableAutoConfiguration注解引入了@Import注解。@Import注解导入了AutoConfigurationImportSelector类。
NOTES:
当spring扫描指定路径下所有类并解析类上的注解时遇到了@import,并且@Import注解引入了DeferredImportSelector接口的实现类,会立即执行DeferredImportSelector接口的selectImports方法。selectImports方法的返回值是一个string数组,通常传入一个类的全限定名,spring根据全限定名找到并注册这个类对象。
下方代码就是AutoConfigurationImportSelector对DeferredImportSelector接口的selectImports方法的具体实现:
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
在上方代码执行完成处打入断点,下图显示MybatisAutoConfiguration已经被找到。
springboot通过自己实现的spi完成扫描,spi这个概念并不是spring独有的。spring spi旨在不通过代码而是通过配置文件的方式注入Bean。springboot通过引入的autoconfiguration依赖,该依赖中提前定义好好了市面大多数框架的自动配置类。如果我们自己开发了一套框架,也想让用户配合springboot使用的时候能够自动注入,我们也可以写一个自动配置类。自动配置类若想要被自动发现,需要按照spring指定的方式存储。
进selectImports() --> getAutoConfigurationEntry() --> getCandidateConfigurations() – >SpringFactoriesLoader.loadFactoryNames方法,SpringFactoriesLoader就是spring spi的具体实现方法:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
loadFactoryNames()方法通过ClassLoader.getResources()方法读取了META-INF目录下的spring.factories文件。
值得一提的是,loadFactoryNames方法不仅加载当前资源目录META-INF下的spring.factories文件,还会导入项目中引入jar包的META-INF/spring.factories文件。
spring.factories文件中的存储格式为key-value,其中key为org.springframework.boot.autoconfigure.EnableAutoConfiguration,value是待引入的类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
所以如果我们想要开发一个我们自己框架的自动配置类,我们可以新建一个xxxStarter的项目,该项目除了引入我们自己的框架依赖外,还需要引入springboot的autoConfiguration的依赖,然后我们写一个AutoConfiguration自动配置类。然后再在当前项目的META-INF/spring.factories目录中配置好自动配置类。用户如果想使用我们的框架并配合springboot使用,只需要引入xxxStarter就可以了。
2.2 MybatisAutoConfiguration
再来看一下mybatis在不集成springboot时的使用方法:
InputStream xml = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(xml);
SqlSession sqlSession = sqlSessionFactory.openSession();
PersonDao personDao = sqlSession.getMapper(PersonDao.class);
List<Person> all = personDao.findAll();
mybatis在不集成springboot时,需要mybatis-config.xml配置文件,而且需要手动调用SqlSessionFactoryBuilder()的build()方法解析配置文件并生成SqlSessionFactory。这个工作其实不需要人手动完成。
至于SqlSession,由于mybatis一级缓存的存在(缓存定义在DefaultSqlSession内部执行器中),多线程环境下,如果按照单例模式存储到spring容器中有可能会造成数据不一致问题。为解决这个问题,mybatis定义了一个线程安全的SqlSession接口的实现类对象SqlSessionTemplate。SqlSessionTemplate通过动态代理和threadLocal技术,每次调用SqlSessionTemplate内部重写的SqlSession的增删改查方法时,实际上调用的都是SqlSessionTemplate内部存储的SqlSession接口的代理对象。这个代理对象每次调用前都会检查当前线程的threadLocal里是否已经存在创建好的SqlSession实现对象了,如果有直接从threadLocal取,如果没有再重新创建(这里创建的还是DefaultSqlSession,所以说真正干事的还是DefaultSqlSession)。所以我们在使用SqlSessionTemplate时,无需关心线程安全问题。
springboot会将SqlSessionTemplate注册到容器中,但是我们调用SqlSessionTemplate上的增删改查接口时,若当前线程中不存在已经创建好的DefaultSqlSession对象,SqlSessionTemplate就会创建一个DefaultSqlSession对象,实际上调用的是他的增删改查方法,并将新创建的DefaultSqlSession放到ThreadLocal中。这里只是顺带一提,后面会按照源码仔细讲一下。
下方代码演示了Springboot如何通过MybatisAutoConfiguration自动注册SqlSessionFactory和SqlSessionTemplate。
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
// ... 省略部分代码
// 通过SqlSessionFactoryBean 注入 SqlSessionFactory
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
// ...注入SqlSessionTemplate
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
Conditional相关的注解是实现springboot自动注入的精髓,能够在多种前提条件下完成bean的自动注入,避免冲突。仔细观察注入sqlSessionTemplate的代码,其实又把上面的sqlSessionFactory放到它的构造器里了,实际上sqlSessionTemplate就是对sqlSessionFactory的扩展。代码中通过@bean注解向spring注册了sqlSessionTemplate和sqlSessionFactory两个bean。
2.3 SqlSession的创建
mybatis查询数据库实际上是通过DefaultSqlSession重写的SqlSession接口中的增删改查方法完成的。前文也指出,DefaultSqlSession是非线程安全的,是通过动态代理和threadlocal避免了线程安全问题,如何实现的呢?下面我简略的概述一下:
SqlSessionTemplate实现了SqlSession接口,其中定义了操作数据库的方法。使用mapper对象对数据库增删改查时,调用的就是SqlSessionTemplate中定义的增删改查方法。
以SqlSessionTemplate中的selectOne()方法为例。下方源码可以看出,selectOne()方法中又调用了this.sqlSessionProxy的selectOne()方法。所以真正干事的其实是this.sqlSessionProxy中的selectOne()方法。
public <T> T selectOne(String statement) {
return this.sqlSessionProxy.selectOne(statement)
}
SqlSessionProxy是SqlSessionTemplate实例变量,会在SqlSessionTemplate实例化的时候被初始化(在SqlSessionTemplate的构造方法中被初始化,源码见下方)。
// SqlSessionTemplate的构造方法
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// SqlSession代理对象的初始化过程
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
SqlSessionProxy通过JDK动态代理,生成了SqlSession接口的代理对象。SqlSessionTemplate在执行它内部的增删改查方法时,实际上是调用它内部SqlSession代理对象中的增删改查方法。
由JDK动态代理的使用方法可知,在执行SqlSessionProxy中的增删改查方法时,实际调用的是它对应的invokeHandler中的invoke()方法。invoke()方法完成对接口方法的代理。SqlSessionProxy的InvokeHandler就是SqlSessionProxy初始化时调用的SqlSessionIncerceptor对象。
下方是SqlSessionInterceptor的invoke()方法的源码
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 通过getSqlSession()方法去获取DefaultSqlSession对象
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator
.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
getSession()方法是具体获取SqlSession的地方:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
// 从事务管理器中尝试获取一次,看看当前现成的ThreadLocal中是否存在已经创建好的SqlSession对象
SqlSession session = sessionHolder(executorType, holder);
// 如果获取到了这里就直接返回了
if (session != null) {
return session;
}
LOGGER.debug(() -> "Creating a new SqlSession");
// 没有获取到这里就新建一个
session = sessionFactory.openSession(executorType);
// 把新建好的SqlSession对象交给事务管理器管理,其中会保存到ThreadLocal对象中,供下次继续使用
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
文章最开始的时候演示了只使用mybatis时是如何使用的,其中我们通过调用SqlSession的getMapper(Dao.class)方法,会返回给我们Dao接口的实现类对象。
这部分代码,在使用springboot的时候,我们通常都是先定义好一个dao接口,该接口中有我们自定义的操作数据库的方法,该方法对应的sql映射到对应mapper.xml文件中。在service层我们想调用dao层接口的方法时,通过@Autowired注解引入dao层接口的实现类对象就可以直接操作。
下面会讲到mybatis-spring如何将一个dao接口自动转为了dao的实现对象并注册到spring容器中。
3. spring扫描器
在讲mybatis扩展spring扫描器前,先简单讲一下spring的扫描器,扫描器是何时注册到spring容器中,扫描器何时执行。
3.1 扫描器是什么?在哪里?
spring扫描器的功能简单来说就是扫描指定路径下的所有Class类,如果该Class类符合提交,就将该Class类转为BeanDefinition对象保存起来,并在合适的实际转为Bean对象。
首先来springboot的启动类上,进入到SpringApplication的run()方法中,并一直点击后面出现的run(),最终会进入到下面的方法中。
下方的run()方法非常重要,是springboot的流水线。每一个方法中包含许多复杂的逻辑,这里先只探讨与spring扫描相关步骤。
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
// 根据我们运行环境,选择合适的ApplicationContext,并向beanDefinition容器中加入了扫描器对象
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 初始化applicationContext,并向beanDefinition容器里放入了一些spring自己的对象
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 扫描器扫描,一些回调接口的执行,bean的实例化,依赖注入和初始化等操作都在这里执行,是最重要的生产线
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
首先先进入到createApplicationContext()方法中。见名知意,该方法会为我们创建一个ApplicationContext对象。ApplicationContext是spring的上下文对象,何为上下文对象,简单来说,就是springboot启动时生成的各种缓存都能在这个这个对象和这个对象继承的父类对象中找到。其中就有我们BeanDefinition容器和Bean容器,说是容器,其实就是个Map对象。
createApplicationContext()比较智能的,能够根据我们当前的环境推断出我们应该使用哪种ApplicationContext对象。一般我们都是web环境中,所以我们的依赖中肯定有servlet相关的类,springboot发现了servlet相关的类,就会认为我们目前处理web环境中,就会走case SERVLET的条件。
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
DEFAULT_SERVLET_WEB_CONTEXT_CLASS指向的是AnnotationConfigServletWebServerApplicationContext这个类,所以在springboot在web环境下自动会为我们创建这个ApplicationContext对象。
AnnotationConfigServletWebServerApplicationContext的无参构造方法new了两个对象。
其中AnnotatedBeanDefinitionReader称之为解析器,可以将我们的一个类转换成一个beanDefinition对象,并注册到beanDefinition容器中,所以spring可以直接通过这个对象将一个类进行注册。
ClassPathBeanDefinitionScanner称之为扫描器,这是一个比较单纯的扫描器,这个扫描器就是可以直接到指定的目录下扫描所有的Class对象,把符合条件的类转为BeanDefinition对象并注册到容器中。但是springboot启动时并不是根据这里new的扫描器进行扫描的。这里new了一个扫描器,是如果我们通过new AnnotationConfigServletWebServerApplicationContext对象启动容器的时候,根据调用AnnotationConfigServletWebServerApplicationContext中的scan方法某个路径下扫描类,这是额外的手动指定扫描位置,与自动扫描区分开。
public AnnotationConfigServletWebServerApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
spring的自动扫描,是spring在启动期间,spring自己手动向beanDefinition容器中注入了一个beanDefinition对象,该对象实现了BeanDefinitionRegistryPostProcessor接口,这个接口名字有一个Registry,说明这个接口能够向beanDefinition容器中注册beanDefinition。开始的时候,beanDefinition容器中一穷二白,只有一个beanDefinition对象并且继承了BeanDefinitionRegistryPostProcessor接口,所以,在初次回调的时机,spring完成扫描,注入了我们项目中所有的bean。 这个能够完成扫描的beanDefinition是何时被spring注入到容器中的呢?继续向后看。
springboot自动扫描的扫描器是在 AnnotatedBeanDefinitionReader(this)解析器初始化时被注入的。
藏的比较深,按照下面的方式找,我标记了1,2,3…,按照这个顺序逐级跳入:
// 1
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) {
// 2,这个this就是下面这个构造器
this(registry, getOrCreateEnvironment(registry));
}
// 2
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
Assert.notNull(environment, "Environment must not be null");
this.registry = registry;
this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
// 3
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
// 3
public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
// 4
registerAnnotationConfigProcessors(registry, null);
}
// 4
public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {
DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
if (beanFactory != null) {
if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
}
if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
}
}
Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);
// 这里的ConfigurationClassPostProcessor就是我们要找的对象,实现了自动扫描功能
if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// ... 后面我忽略了部分代码
}
可以看到上面代码中BeanDefinitionRegistry 贯穿始终,这个BeanDefinitionRegistry 可以用于向BeanDefinition容器中注入对象。扫描器也是通过这个组件向beanDeinition容器进行注册的。
上面代码中,ConfigurationClassPostProcessor对象被注入到了beanDefinitionMap容器中了。ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法中实现类极为复杂的自动扫描过程。在自动扫描过程中,其实也会调用ClassPathBeanDefinitionScanner扫描器扫描指定路径下的Class。
4. mybatis扩展spring扫描器,实现mapper接口类的扫描、代理、注入
刚接触mybatis时,我就有两个疑问:
- 为什么标记了@MapperScan注解后,spring就能够将mapper接口的实现类注入到容器中?
- 为什么我声明的mapper类只是一个接口,但是@Autowired注入的时候,得到的却是一个可以直接调用的对象呢?
4.1 spring如何扫描到mapper接口
对于一个普通的web应用来说,通常会在dao包下创建多个mapper接口。mapper接口的每个方法都对应了一个对数据库的操作。然后再在某个配置类中打上@MapperScan注解,注解中有一个属性值用来记录dao包的路径。spring根据我们在@MapperScan注解中记录的dao包路径,到指定路径下扫描出这些接口。
mybatis扫描的核心就在@MapperScan注解中:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan {
// ... 省略了注解属性值
}
@MapperScan注解中引入了@Import注解,@Import注解导入了MapperScannerRegistrar类:
public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {
}
@Import注解导入了ImportBeanDefinitionRegistrar接口的实现类,spring扫描解析到@Import注解导入了ImportBeanDefinitionRegistrar实现类后回调ImportBeanDefinitionRegistrar接口的registerBeanDefinitions()方法。
registerBeanDefinitions()方法可以向beanDefinitionMap容器中注入beanDefinition对象。MapperScannerRegistrar的registerBeanDefinitions代码如下:
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes mapperScanAttrs = AnnotationAttributes
.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
if (mapperScanAttrs != null) {
registerBeanDefinitions(importingClassMetadata, mapperScanAttrs, registry,generateBaseBeanName(importingClassMetadata, 0));
}
}
void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
BeanDefinitionRegistry registry, String beanName) {
// MapperScannerConfigurer实现了扫描Mapper类
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
builder.addPropertyValue("processPropertyPlaceHolders", true);
// ... 忽略了部分代码
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
上面代码中,MapperScannerRegistrar 向容器注入了MapperScannerConfigurer。MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口。
MapperScannerConfigurer就像上面介绍spring扫描器中出现的ConfigurationClassPostProcessor 。二者同样是实现了BeanDefinitionRegistryPostProcessor接口。只不过ConfigurationClassPostProcessor 是spring自己创建的BeanDefinitionRegistryPostProcessor接口的实现类。spring第一次回调BeanDefinitionRegistryPostProcessor接口方法的时候,beanDefinitionMap中还只有ConfigurationClassPostProcessor 一个,等到这个扫描出来其他BeanDefinitionRegistryPostProcessor接口实现类的时候,会再次回调。
可以看一下MapperScannerConfigurer的postProcessBeanDefinitionRegistry方法:
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
scanner.setAddToConfig(this.addToConfig);
scanner.setAnnotationClass(this.annotationClass);
scanner.setMarkerInterface(this.markerInterface);
scanner.setSqlSessionFactory(this.sqlSessionFactory);
scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
scanner.setResourceLoader(this.applicationContext);
scanner.setBeanNameGenerator(this.nameGenerator);
scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
if (StringUtils.hasText(lazyInitialization)) {
scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
}
if (StringUtils.hasText(defaultScope)) {
scanner.setDefaultScope(defaultScope);
}
scanner.registerFilters();
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
首先new一个ClassPathMapperScanner对象,向这个对象的构造器中注入了registry对象,registry对象能够向beanDefinitionMap容器中注入beanDefinition。
我们先简单看一下这个ClassPathMapperScanner扫描器的继承关系:
public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {}
ClassPathMapperScanner 继承了ClassPathBeanDefinitionScanner。前文讲到ClassPathBeanDefinitionScanner能够做到给一个路径,将这个路径下所有符合条件的Class转为beanDefinition注册到beanDefinitionMap容器中。所以ClassPathMapperScanner 也具体了这个功能。
但是作为mybatis的扫描器,我们想要把接口扫描出来,其他的东西我们并不想要,所以就需要重写扫描器的过滤方法,使扫描器只扫描接口类,先继续向后看。
代码中的scanner.scan()方法实际调用的还是父类ClassPathBeanDefinitionScanner的scan()方法,让我们进入到父类的scan()方法。
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
spring的作者有个习惯,真正执行某项功能的方法喜欢在方法前面加个do,所以这里的doScan()方法才是真正扫描的方法。doScan()方法ClassPathMapperScanner重写了,具体代码如下:
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
super.doScan(basePackages)说明mybatis扫描器的扫描功能还是采用的父类扫描方法,实际去指定包下扫描还是用spring扫描器完成的。但spring扫描器本身的过滤规则并不适用于mybatis。
spring扫描器在得到一个Class类转为beanDefinition对象后会调用isCandidateComponent()方法,判断这个对象是不是一个Componet,如果是的话会加入到集合中,不是则过滤。
mybaits的ClassPathMapperScanne便r重写了isCandidateComponent()方法,用了更为简单的方式进行替换,代码如下:
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
beanDefinition.getMetadata().isInterface()表示只有接口类才能够返回true。
上面大致就是扫描的过程,也就是ClassPathMapperScanner重写的spring扫描器的doScan方法的前半部分,通过调用定制化的spring扫描器将指定包下的接口都扫描出来了,后面就是如果对这些接口做特殊处理。
4.2 mybatis如何创建mapper接口对象
4.2.1 构造MapperFactoryBean
再次回到ClassPathMapperScanner类的doScan方法
@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
LOGGER.warn(() -> "No MyBatis mapper was found in '" + Arrays.toString(basePackages)
+ "' package. Please check your configuration.");
} else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
在得到BeanDefinitionHolder集合后,调用processBeanDefinitions方法,进行后续的特殊处理:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
boolean scopedProxy = false;
if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
definition = (AbstractBeanDefinition) Optional
.ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
.map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
"The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
scopedProxy = true;
}
// 记录Mapper类名
String beanClassName = definition.getBeanClassName();
LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
+ "' mapperInterface");
// the mapper interface is the original class of the bean
// but, the actual class of the bean is MapperFactoryBean
// 设置beanDefinition的构造器参数,将Mapper类名作为构造器参数值
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); // issue #59
try {
// for spring-native
// 注释上写的是for spring-native,所以暂时可以忽略掉这个行代码,mapperInterface还是通过上面的构造器进行赋值的
definition.getPropertyValues().add("mapperInterface", Resources.classForName(beanClassName));
} catch (ClassNotFoundException ignore) {
// ignore
}
// 这里很重要,可以看到,beanDefinition实际指向的Class对象由Mapper类改为了MapperFactoryBeanClass
// 而原先的MapperClass则通过构造器的方式注入到MapperFactoryBeanClass的mapperInterface属性中
definition.setBeanClass(this.mapperFactoryBeanClass);
definition.getPropertyValues().add("addToConfig", this.addToConfig);
// Attribute for MockitoPostProcessor
// https://github.com/mybatis/spring-boot-starter/issues/475
definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);
boolean explicitFactoryUsed = false;
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory",
new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate",
new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
if (explicitFactoryUsed) {
LOGGER.warn(
() -> "Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");
}
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
// 这里设置了AUTOWIRE_BY_TYPE,在实例化MapperFactoryBean时,父类的SqlSessionTemplate属性就能够根据类型到容器中查找并进行赋值
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
}
definition.setLazyInit(lazyInitialization);
if (scopedProxy) {
continue;
}
if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
definition.setScope(defaultScope);
}
if (!definition.isSingleton()) {
BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
registry.removeBeanDefinition(proxyHolder.getBeanName());
}
registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
}
}
}
结合上面的简单注释,大致讲一下这里都做了什么:
扫描过后已经获取到mapper接口对应的beanDefinition对象了。通过BeanDefinitionHolder获取AbstractBeanDefinition,简单来点就是获取beanDefinition,此时的beanDefinition还是mapper接口的类模板。
获取mapper接口的类名,然后设置到beanDefinition的构造器参数中。记录下这个类名,方便后面通过factoryBean获取bean的时候能知道要实例化哪个bean。
将beanDefiniton的指向类改为MapperFactoryBean。意思就是此刻的beanDefinition已经不再是mapper接口的类模板了,而是MapperFactoryBean的类模板了。而原来mapper接口类则通过MapperFactoryBean的构造器初始化了他内部的实例变量mapperInterface。spring实例化MapperFactoryBean时,会调用FactoryBean的getObject()方法,该方法的返回值会作为bean注册到spring容器中。
上方代码中,beanDefinition设置AUTOWIRE_BY_TYPE类型,MapperFactoryBean继承了SqlSessionDaoSupport。SqlSessionDaoSupport中存在一个实例变量sqlSessionTemplate,该变量在MybatisAutoConfuguration中被初始化并注入到bean容器中,所以这里会根据类型自动初始化该变量值。(前面讲到的SqlSessionTemplate就是在这里被用起来的)。
4.2.2 实例化MapperFactoryBean
spring实例化MapperFactoryBean时,调用getObject方法生成代理对象,也就是我们在service层通过@Autowired注解引入的dao层接口对象。下面主要讲一下getObject()方法,看一下这个接口对象是怎么生成的:
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
private boolean addToConfig = true;
public MapperFactoryBean() {
// intentionally empty
}
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
/**
* {@inheritDoc}
*/
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
}
getObject()方法中,调用了父类SqlSessionDaoSupport的getSqlSession()方法,getSqlSession()方法返回了SqlSessionDaoSupport中初始化号的SqlSessionTemplate对象。所以这里调用的是SqlSessionTemplate的getMapper()方法。
SlqSessionTemplate中的getMapper()代码如下:
@Override
public <T> T getMapper(Class<T> type) {
return getConfiguration().getMapper(type, this);
}
这里调用了getConfiguration()方法,通过debug简单看看Configuration里都有什么
图片只截取了部分。Configuration里包涵了我们配置的所有东西,比如可以看有resultMap,MappedStatements等等,这些都是我们在Mapper.xml文件里的配置,一般都是有具体的SQL,参数值和返回值等配置。
Configuration同样包涵了MapperRegister组件,在实例化Configuration时new了MapperRegister组件。根据他的名字我们可以猜一下,这个是用于注册mapper对象的组件。该组件中有getMapper()方法。根据这个方法中的泛型我们就可以猜出这个方法的功能。方法参数中传入了一个类型为T的Class对象,最后返回给我们了一个T对象,所以这里就是实现动态代理的地方,传入一个Class类,返回该Class的实例化后的对象。
进入MapperRegistry中的getMapper()方法:
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
Configuration初始化时将所有扫描出的mapper类存入到knownMappers中,Key为Mapper的Class对象,Value为MapperProxyFactory工厂。
MapperProxyFactory作为mapper类的代理工厂,是通过newInstance方法构造mapper代理对象:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
这里采用了JDK动态代理,所以我们需要找到invocationHandler接口,invocationHandler接口中的invoke()方法中会有具体的代理过程。
根据JDK动态代理的api参数,上面的MapperProxy类就是InvocationHandler接口的实现类,实际上调用的也就是MapperProxy中的invoke()方法。不过在new MapperProxy的同时,SqlSessionTemplate一起也通过构造器注入到了MapperProxy中,下面是invoke()代码:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else {
return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
if中Object.class.equals(method.getDeclaringClass()用来过滤掉Object中的方法,比如toString()等等,这些不做任何处理,所以除去这些方法一定会进入else:
进入到cachedInvoker()方法,注意这里调用cachedInvoker()后又调用了invoke()方法,看完cachedInvoker()的返回值后我会直接看它的invoke()方法。
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
try {
// A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
// It should be removed once the fix is backported to Java 8 or
// MyBatis drops Java 8 support. See gh-1929
MapperMethodInvoker invoker = methodCache.get(method);
if (invoker != null) {
return invoker;
}
return methodCache.computeIfAbsent(method, m -> {
if (m.isDefault()) {
try {
if (privateLookupInMethod == null) {
return new DefaultMethodInvoker(getMethodHandleJava8(method));
} else {
return new DefaultMethodInvoker(getMethodHandleJava9(method));
}
} catch (IllegalAccessException | InstantiationException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException(e);
}
} else {
return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
});
} catch (RuntimeException re) {
Throwable cause = re.getCause();
throw cause == null ? re : cause;
}
}
这里首先判断这个方法是不是接口中的default方法,大家知道,jdk8之后接口中是允许有具体实现方法的,不过它必须是default方法。mapper接口中定义的方法肯定是public的,所以进入else中:
else中首先new了一个PlainMethodInvoker对象,我们进入到这个对象的invoke()方法
@Override
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return mapperMethod.execute(sqlSession, args);
}
再进入到上面的execute()方法:
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
这里就已经开始执行增删改查的代码了。因为在new MapperMethod的时候传入了Configuration,也传入了method对象,所以能够根据我们在XML中配置的sql解析出操作类型,根据操作类型swtich进入到不同的case中。
其实到这里已经可以看到,我们mapper接口的动态代理对象的全貌了,当开始执行mapper代理对象的增删改查方法时,最后都会到这里。