Spring Boot项目中集成mybatis来开发项目,我相信每个用Spring boot 的小伙伴都使用过,感觉就是特别爽,在yml文件中配置一下,就能对数据库进行访问了,其实现原理是什么呢?带着疑问,我们走进代码。
- 在pom.xml文件中新增配置
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
- 在yml中添加数据库配置和mybatis配置
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/lz_test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: ldd_biz password: Hello1234 initial-size: 10 max-active: 100 min-idle: 10 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT 1 FROM DUAL test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true mybatis: #实体扫描,多个package用逗号或者分号分隔 type-aliases-package: com.example.springbootstudy.entity mapper-locations: classpath:mapper/*Mapper.xml
- 数据库脚本
CREATE TABLE `lz_test_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `is_delete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `type` int(11) DEFAULT '0' COMMENT '0', `branch_id` int(11) DEFAULT NULL COMMENT '版本号', `real_name` varchar(256) DEFAULT NULL COMMENT '真实名称', `mobile` varchar(256) DEFAULT NULL COMMENT '手机号码', `username` varchar(256) DEFAULT NULL COMMENT '用户名', `task_id` int(11) DEFAULT NULL COMMENT '任务 id', `staff_id` int(11) DEFAULT '0' COMMENT '员工 id', `encrypt_flag` int(11) DEFAULT '0' COMMENT '是否加密,0 未加密,1 己加密,2其他加密算法', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4 COMMENT='项目用户';
- 创建实体,Mapper,Mapper.xml 测试方法
@Data @Mapper public class TestUser implements java.io.Serializable { // 主键id private Long id; //是否删除 private Integer isDelete; //生成时间 private Date gmtCreate; //修改时间 private Date gmtModified; //0 private Integer type; //版本号 private Long branchId; //真实名称 private String realName; //手机号码 private String mobile; //用户名 private String username; //任务id private Long taskId; //员工 id private Long staffId; // 是否加密,0 未加密,1 己经加密 ,2 ,3 其他加密算法,在密钥泄漏时需要 private int encryptFlag; } @Mapper public interface TestUserMapper { // 所有的查询条件,默认是 AND 和 = 关系,如果想在其他的关系,可以写相关的注解@OR ,或@Like TestUser selectTestUserById(Long id); } <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.springbootstudy.mapper.TestUserMapper"> <select id="selectTestUserById" resultType="com.example.springbootstudy.entity.TestUser"> select * from lz_test_user where id = #{id} </select> </mapper> @RequestMapping("select") public String select() { TestUser testUser = testUserMapper.selectTestUserById(14l); System.out.println(JSON.toJSONString(testUser)); return "SUCESS"; }
【测试结果】
因为Mybatis这一块也牵涉到太多的内容,而本文着重讲Spring Boot如何整合Mybatis的,如果有兴趣去研究MyBatis源码这一块,可以去看我的这一篇博客,https://blog.csdn.net/quyixiao/article/details/110295148,如果对本文中的一些注解的功能及源码不太理解的话,可以去看https://blog.csdn.net/quyixiao/article/details/117777543这一篇博客,因为在研究Spring Boot 整合Mybatis时,发现研究不下去,才去写Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析这一篇博客的,Spring Boot 整个体系,在了Spring源码深度解析(郝佳)-学习-Spring Boot体系原理,这一篇博客做了分析,目前万事具备了,只欠如何分析Spring Boot 整合MyBatis源码了。话不多说,直接进入主题吧。
这基本上是一个最简单的Spring 整个mybatis的例子了,虽然例子小,但是对于小项目来说,也是可以用于生产了,那Spring Boot是如何让开发变得如此简单的呢?带着疑问,我们来看看源码如何实现。
先从MybatisAutoConfiguration这个类看起。顾名思义,这个类就是mybatis自动配置类。
@org.springframework.context.annotation.Configuration @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnBean(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) public class MybatisAutoConfiguration { private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class); private final MybatisProperties properties; private final Interceptor[] interceptors; private final ResourceLoader resourceLoader; private final DatabaseIdProvider databaseIdProvider; private final List<ConfigurationCustomizer> configurationCustomizers; public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) { this.properties = properties; this.interceptors = interceptorsProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = databaseIdProvider.getIfAvailable(); this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); } @PostConstruct public void checkConfigFileExists() { if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) { Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation()); Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)"); } } @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())); } Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { for (ConfigurationCustomizer customizer : this.configurationCustomizers) { customizer.customize(configuration); } } factory.setConfiguration(configuration); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } return factory.getObject(); } @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } } public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware { private BeanFactory beanFactory; private ResourceLoader resourceLoader; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { logger.debug("Searching for mappers annotated with @Mapper"); ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); try { if (this.resourceLoader != null) { scanner.setResourceLoader(this.resourceLoader); } List<String> packages = AutoConfigurationPackages.get(this.beanFactory); if (logger.isDebugEnabled()) { for (String pkg : packages) { logger.debug("Using auto-configuration base package '{}'", pkg); } } scanner.setAnnotationClass(Mapper.class); scanner.registerFilters(); scanner.doScan(StringUtils.toStringArray(packages)); } catch (IllegalStateException ex) { logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex); } } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } } @org.springframework.context.annotation.Configuration @Import({ AutoConfiguredMapperScannerRegistrar.class }) @ConditionalOnMissingBean(MapperFactoryBean.class) public static class MapperScannerRegistrarNotFoundConfiguration { @PostConstruct public void afterPropertiesSet() { logger.debug("No {} found.", MapperFactoryBean.class.getName()); } } }
MybatisAutoConfiguration这个类实在是太重要了,今天,我们就围绕着这个类来分析,首先,我们看这个类配置了@org.springframework.context.annotation.Configuration注解,显然会被SpringBootApplication注解扫描到。来看一下SpringBootApplication注解代码
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ... }
默认SpringBootApplication的ComponentScan注解扫描当前项目下所有的**.*.class的,但是其加了excludeFilters属性,配置了TypeExcludeFilter和AutoConfigurationExcludeFilter过滤器。而在Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析这篇博客也对SpringBootApplication的AutoConfigurationExcludeFilter属性做了详细分析,在AutoConfigurationExcludeFilter中会排除掉所有META-INF/spring.factories下org.springframework.boot.autoconfigure.EnableAutoConfiguration属性配置的configuration类,遗憾的是MybatisAutoConfiguration类刚好配置在mybatis-spring-boot-autoconfigure类下的META-INF/spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration属性中,如下图所示。
那么MybatisAutoConfiguration的BeanDefinition是何时加入到容器中的呢?在Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析这篇博客中也分析了,在processDeferredImportSelectors方法中,获取META-INF/spring.factories下的所有org.springframework.boot.autoconfigure.EnableAutoConfiguration属性值bean,并调用processImports方法,最终将bean的BeanDefinition注入到容器中。
接下来,我们来看MybatisAutoConfiguration的ConditionalOnClass注解的第一个属性SqlSessionFactory,显然,我们知道SqlSessionFactory是sqlSessionFactory方法配置了Bean注解,在扫描MybatisAutoConfiguration的Bean方法时,创建的BeanDefinition。那我们来看一下这和段代码的实现。
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass); // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { if (this.environment instanceof ConfigurableEnvironment) { processPropertySource(propertySource); } else { logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() + "]. Reason: Environment must implement ConfigurableEnvironment"); } } // Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { if (ConfigurationClassUtils.checkConfigurationClassCandidate( holder.getBeanDefinition(), this.metadataReaderFactory)) { parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName()); } } } } // Process any @Import annotations processImports(configClass, sourceClass, getImports(sourceClass), true); // Process any @ImportResource annotations if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) { AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // Process default methods on interfaces processInterfaces(configClass, sourceClass); // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (!superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } // No superclass -> processing is complete return null; }
上述方法中,其他的方法也在https://blog.csdn.net/quyixiao/article/details/117777543这篇博客做了详细分析,但是 retrieveBeanMethodMetadata方法,是扫描@Configuration注解类中的有@Bean注解的方法,并获取其方法元数据,并返回。接下来,我们来看retrieveBeanMethodMetadata方法的具体实现。
private Set<MethodMetadata> retrieveBeanMethodMetadata(SourceClass sourceClass) { AnnotationMetadata original = sourceClass.getMetadata(); Set<MethodMetadata> beanMethods = original.getAnnotatedMethods(Bean.class.getName()); if (beanMethods.size() > 1 && original instanceof StandardAnnotationMetadata) { try { // 尝试通过 ASM 读取类文件以获得确定性声明顺序... // 不幸的是,JVM 的标准反射以任意顺序 // 返回方法,即使在同一 JVM 上同一应用程序的不同运行之间也是如此。 AnnotationMetadata asm = this.metadataReaderFactory.getMetadataReader(original.getClassName()).getAnnotationMetadata(); Set<MethodMetadata> asmMethods = asm.getAnnotatedMethods(Bean.class.getName()); if (asmMethods.size() >= beanMethods.size()) { Set<MethodMetadata> selectedMethods = new LinkedHashSet<MethodMetadata>(asmMethods.size()); for (MethodMetadata asmMethod : asmMethods) { for (MethodMetadata beanMethod : beanMethods) { if (beanMethod.getMethodName().equals(asmMethod.getMethodName())) { selectedMethods.add(beanMethod); break; } } } if (selectedMethods.size() == beanMethods.size()) { // All reflection-detected methods found in ASM method set -> proceed beanMethods = selectedMethods; } } } catch (IOException ex) { logger.debug("Failed to read class file via ASM for determining @Bean method order", ex); // No worries, let's continue with the reflection metadata we started with... } } return beanMethods; }
关于StandardAnnotationMetadata的使用,也是非常简单
public static void main(String[] args) throws IOException { AnnotationMetadata reflectReader = new StandardAnnotationMetadata(BeanConfig.class); System.out.println(reflectReader.getAnnotationTypes()); }
网上有关StandardAnnotationMetadata的解释是
SimpleAnnotationMetadataReadingVisitor与StandardAnnotationMetadata的主要区别在于,SimpleAnnotationMetadataReadingVisitor是基于asm的实现,StandardAnnotationMetadata是基于反射的实现,那我们在使用时,应该要怎么选呢?
由于基于反射是要先加类加载到jvm中的,因此我的判断是,如果当前类没有加载到jvm中,就使用SimpleAnnotationMetadataReadingVisitor,如果类已经加载到jvm中了,两者皆可使用。
事实上,在spring包扫描阶段,读取类上的注解时,使用的都是SimpleAnnotationMetadataReadingVisitor,因为此时类并没有加载到jvm,如果使用StandardAnnotationMetadata读取,就会导致类提前加载。类提前加载有什么问题呢?java类是按需加载的,有的类可能在整个jvm生命周期内都没用到,如果全都加载了,就白白浪费内存了。
【总结】
本文介绍了 AnnotationMetadata两种实现方案,yyyyyyyyyyyyy一种基于 Java 反射,另一种基于ASM 框架。
因此上述方法主要是通过ASM技术,访问class文件,将配置了Bean注解的方法元数据返回。
接下来,我们来看看addBeanMethod方法。
public void addBeanMethod(BeanMethod method) { this.beanMethods.add(method); }
这个方法没有什么特别我地方,只将封装好的BeanMethod对象,加入到beanMethods集合中,而beanMethods又何时使用呢?
通过idea很快发现,只有一个地方使用了。
public Set<BeanMethod> getBeanMethods() { return this.beanMethods; } private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { if (trackedConditionEvaluator.shouldSkip(configClass)) { String beanName = configClass.getBeanName(); if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { this.registry.removeBeanDefinition(beanName); } this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); return; } if (configClass.isImported()) { registerBeanDefinitionForImportedConfigurationClass(configClass); } //遍历当前configuration类的所有的配置了@Bean注解的方法 for (BeanMethod beanMethod : configClass.getBeanMethods()) { loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); }
接下来,我们继续看是如何通过BeanMethod创建bean的BeanDefinition的呢?
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { ConfigurationClass configClass = beanMethod.getConfigurationClass(); //获取Bean注解方法的元数据 MethodMetadata metadata = beanMethod.getMetadata(); //获得方法名 String methodName = metadata.getMethodName(); //判断方法是是否配置了Conditional注解, //如ConditionalOnBean,ConditionalOnClass,ConditionalOnMissingBean,ConditionalOnMissingClass //注解等,如果配置了,根据不同注解的条件,看当前BeanMethod是否跳过创建Bean if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { configClass.skippedBeanMethods.add(methodName); return; } if (configClass.skippedBeanMethods.contains(methodName)) { return; } AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class); //@Bean注解上是否配置名称 List<String> names = new ArrayList<String>(Arrays.asList(bean.getStringArray("name"))); //如果@Bean注解中配置了名称,则使用@Bean注解上的第一个名称作为bean在容器中的名称, //否则以方法名作为Bean在容器中的名称 String beanName = (!names.isEmpty() ? names.remove(0) : methodName); //为Bean注册别名 for (String alias : names) { this.registry.registerAlias(beanName, alias); } //当前容器中是否存在相同beanName的beanDefinition //如果存在,则抛出异常 if (isOverriddenByExistingDefinition(beanMethod, beanName)) { if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) { throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(), beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() + "' clashes with bean name for containing configuration class; please make those names unique!"); } return; } ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata); beanDef.setResource(configClass.getResource()); beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource())); if (metadata.isStatic()) { //如果是static方法,则设置BeanClassName为configClass的类名 beanDef.setBeanClassName(configClass.getMetadata().getClassName()); beanDef.setFactoryMethodName(methodName); } else { //如果不是静态方法,则设置当前configuration bean名称 beanDef.setFactoryBeanName(configClass.getBeanName()); beanDef.setUniqueFactoryMethodName(methodName); } beanDef.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); // 设置RequiredAnnotationBeanPostProcessor的skipRequiredCheck为true beanDef.setAttribute("org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.skipRequiredCheck", Boolean.TRUE); //处理普通注解Lazy,Primary,DependsOn,Role,Description等注解 AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata); // @Bean(autowire = Autowire.BY_NAME) Autowire autowire = bean.getEnum("autowire"); if (autowire.isAutowire()) { beanDef.setAutowireMode(autowire.value()); } /* initMethod和destroyMethod如下用法 //@Bean(initMethod="initMethod",destroyMethod = "destroyMethod") public ImportByBCC importByBB(){ return new ImportByBCC(); } class ImportByBCC{ private void initMethod() { } private void destroyMethod(){ } }*/ String initMethodName = bean.getString("initMethod"); if (StringUtils.hasText(initMethodName)) { beanDef.setInitMethodName(initMethodName); } String destroyMethodName = bean.getString("destroyMethod"); if (destroyMethodName != null) { beanDef.setDestroyMethodName(destroyMethodName); } //@Scope注解的使用 ScopedProxyMode proxyMode = ScopedProxyMode.NO; AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, Scope.class); if (attributes != null) { beanDef.setScope(attributes.getString("value")); proxyMode = attributes.getEnum("proxyMode"); if (proxyMode == ScopedProxyMode.DEFAULT) { proxyMode = ScopedProxyMode.NO; } } //如果Scope的proxyMode模式配置了ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES BeanDefinition beanDefToRegister = beanDef; if (proxyMode != ScopedProxyMode.NO) { BeanDefinitionHolder proxyDef = ScopedProxyCreator.createScopedProxy( new BeanDefinitionHolder(beanDef, beanName), this.registry, proxyMode == ScopedProxyMode.TARGET_CLASS); beanDefToRegister = new ConfigurationClassBeanDefinition( (RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata); } if (logger.isDebugEnabled()) { logger.debug(String.format("Registering bean definition for @Bean method %s.%s()", configClass.getMetadata().getClassName(), beanName)); } this.registry.registerBeanDefinition(beanName, beanDefToRegister); }
其实这个方法的重点是讲BeanDefinition设置setFactoryBeanName为当前ConfigureClass 和FactoryMethodName,Bean注解修饰的方法为FactoryMethod方法,方法所在的Class为FactoryBean。而关于Scope注解的使用,在另外一篇博客Spring @Bean @Scope注解 proxyMode的组合使用及源码解析。
下面,我们来看看sqlSessionFactory Bean的实例化。
@org.springframework.context.annotation.Configuration @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnBean(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) public class MybatisAutoConfiguration { private final MybatisProperties properties; public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) { this.properties = properties; this.interceptors = interceptorsProvider.getIfAvailable(); this.resourceLoader = resourceLoader; this.databaseIdProvider = databaseIdProvider.getIfAvailable(); this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable(); } @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())); } Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { for (ConfigurationCustomizer customizer : this.configurationCustomizers) { customizer.customize(configuration); } } factory.setConfiguration(configuration); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } //设置插件拦截器 if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } //数据库id,如MYSQL,oracle等 if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } //设置xml中使用到的别名类,所在包 if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } //设置类型处理器所在的包位置 if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } //设置Mapper所在位置 if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } return factory.getObject(); }
从sqlSessionFactory方法中可以看到,整个方法都是围绕着属性文件properties来赋值的,在Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析在篇博客中,我们就己经详细的解析了EnableConfigurationProperties注解源码,只要配置了EnableConfigurationProperties注解,Spring会自动通过getProperty方法从环境中获取参数,并设置到MybatisProperties属性中,而yml中刚好设置了MybatisProperties对象的属性值。
mybatis: type-aliases-package: com.example.springbootstudy.entity mapper-locations: classpath:mapper/*Mapper.xml
而命名规则,则去掉-,以驼峰命名映射,如type-aliases-package对应的是MybatisProperties的typeAliasesPackage属性,而mapper-locations对应的是mapperLocations属性。
而细心的读者有没有发现,假如先实例化sqlSessionFactory的bean,后面实例化MybatisAutoConfiguration对象,properties属性值不就为空了嘛。这种情况会不会发生呢?我们在MybatisAutoConfiguration构造函数中打一个断点。
追踪方法调用栈,发现在bean实例化时调用了instantiateUsingFactoryMethod方法
而在实例化bean时,从BeanDefinition中,发现有FactoryBean,则先实例化FactoryBean,再调用factoryBean的factoryMethod来实例化当前bean。显然,无论如何MybatisAutoConfiguration总比SqlSessionFactory先实例化。因此properties属性不可能为空。而loadBeanDefinitionsForBeanMethod方法里面有一个重要的设置,就是为当前BeanMethod设置FactoryBeanName和FactoryMethodName。
接来下,我们继续看SqlSessionFactoryBean的getObject方法的内部实现。
public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { afterPropertiesSet(); } return this.sqlSessionFactory; } public void afterPropertiesSet() throws Exception { notNull(dataSource, "Property 'dataSource' is required"); notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required"); state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null), "Property 'configuration' and 'configLocation' can not specified with together"); this.sqlSessionFactory = buildSqlSessionFactory(); } protected SqlSessionFactory buildSqlSessionFactory() throws IOException { Configuration configuration; XMLConfigBuilder xmlConfigBuilder = null; if (this.configuration != null) { configuration = this.configuration; if (configuration.getVariables() == null) { configuration.setVariables(this.configurationProperties); } else if (this.configurationProperties != null) { configuration.getVariables().putAll(this.configurationProperties); } } else if (this.configLocation != null) { xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties); configuration = xmlConfigBuilder.getConfiguration(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'configuration' or 'configLocation' not specified, using default MyBatis Configuration"); } configuration = new Configuration(); if (this.configurationProperties != null) { configuration.setVariables(this.configurationProperties); } } if (this.objectFactory != null) { configuration.setObjectFactory(this.objectFactory); } if (this.objectWrapperFactory != null) { configuration.setObjectWrapperFactory(this.objectWrapperFactory); } if (this.vfs != null) { configuration.setVfsImpl(this.vfs); } if (hasLength(this.typeAliasesPackage)) { String[] typeAliasPackageArray = tokenizeToStringArray(this.typeAliasesPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeAliasPackageArray) { configuration.getTypeAliasRegistry().registerAliases(packageToScan, typeAliasesSuperType == null ? Object.class : typeAliasesSuperType); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for aliases"); } } } if (!isEmpty(this.typeAliases)) { for (Class<?> typeAlias : this.typeAliases) { configuration.getTypeAliasRegistry().registerAlias(typeAlias); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type alias: '" + typeAlias + "'"); } } } if (!isEmpty(this.plugins)) { for (Interceptor plugin : this.plugins) { configuration.addInterceptor(plugin); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered plugin: '" + plugin + "'"); } } } if (hasLength(this.typeHandlersPackage)) { String[] typeHandlersPackageArray = tokenizeToStringArray(this.typeHandlersPackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); for (String packageToScan : typeHandlersPackageArray) { configuration.getTypeHandlerRegistry().register(packageToScan); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Scanned package: '" + packageToScan + "' for type handlers"); } } } if (!isEmpty(this.typeHandlers)) { for (TypeHandler<?> typeHandler : this.typeHandlers) { configuration.getTypeHandlerRegistry().register(typeHandler); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registered type handler: '" + typeHandler + "'"); } } } if (this.databaseIdProvider != null) {//fix #64 set databaseId before parse mapper xmls try { configuration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource)); } catch (SQLException e) { throw new NestedIOException("Failed getting a databaseId", e); } } if (this.cache != null) { configuration.addCache(this.cache); } if (xmlConfigBuilder != null) { try { xmlConfigBuilder.parse(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed configuration file: '" + this.configLocation + "'"); } } catch (Exception ex) { throw new NestedIOException("Failed to parse config resource: " + this.configLocation, ex); } finally { ErrorContext.instance().reset(); } } if (this.transactionFactory == null) { this.transactionFactory = new SpringManagedTransactionFactory(); } configuration.setEnvironment(new Environment(this.environment, this.transactionFactory, this.dataSource)); if (!isEmpty(this.mapperLocations)) { for (Resource mapperLocation : this.mapperLocations) { if (mapperLocation == null) { continue; } try { XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), configuration, mapperLocation.toString(), configuration.getSqlFragments()); xmlMapperBuilder.parse(); } catch (Exception e) { throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e); } finally { ErrorContext.instance().reset(); } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parsed mapper file: '" + mapperLocation + "'"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Property 'mapperLocations' was not specified or no matching resources found"); } } return this.sqlSessionFactoryBuilder.build(configuration); } public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
这个方法,实际是属性mybatis源码这一块了,前面主要是将Spring boot配置文件中的配置信息设置到configuration中,前部分根据settings,typeAliases,typeHandlers,plugins,objectFactory,objectWrapperFactory等配置信息设置到configuration中,后部分主要是对Mapper.xml进行parse操作,关于parse操作,在我的MyBatis源码解析系列也做了详细的解析,这里就不再赘述,而Mapper.xml中的一个个元素最终被解析成mapperStatement存储到configuration的属性中。而configuration对于mybatis而言,相当于一个配置的内存数据库。所有与mybatis相关的配置都存储到了Configuration中,而最终容器sqlSessionFactory是DefaultSqlSessionFactory对象,configuration就在创建对象时设置到DefaultSqlSessionFactory的configuration属性中。
而sqlSessionFactory实例化,主要是将yml或properties配置文件的内容设置到SqlSessionFactoryBean中,可能大家对这一块有点陌生了,我们来看看用mybatis-config.xml是如何来配置这些参数的。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="spring_101_200/config_121_130/spring125_mybatis_properties/db.properties"></properties> <settings> <setting name="cacheEnabled" value="false"/> <setting name="useGeneratedKeys" value="true"/> <setting name="defaultExecutorType" value="REUSE"/> <setting name="mapUnderscoreToCamelCase" value="true" /> <setting name="autoMappingBehavior" value="FULL"/> <setting name="localCacheScope" value="STATEMENT" /> <!--打开延迟加载的开关,全局性设置懒加载。如果设为‘false’,则所有相关联的都会被初始化加载。默认值 false --> <setting name="lazyLoadingEnabled" value="true"/> <!--将积极加载改为消极加载及按需加载,当设置为‘true’的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载,默认值 true --> <setting name="aggressiveLazyLoading" value="false"/> <!--当逻辑触发lazyLoadTriggerMethods 对应的方法(equals,clone,hashCode,toString)则执行延迟加载 --> <setting name="lazyLoadTriggerMethods" value="hashCode"/> <setting name="safeResultHandlerEnabled" value="true"/> </settings> <typeAliases> <typeAlias type="com.spring_101_200.test_121_130.test_125_mybatis_properties.User" alias="User"></typeAlias> <package name="com.spring_101_200.test_131_140.test_134_mybatis_usecolumnlabel"/> <package name="com.spring_101_200.test_131_140.test_135_mybatis_executor"/> </typeAliases> <typeHandlers> <package name="com.spring_101_200.test_131_140.test_132_mybatis_typehandlers"/> </typeHandlers> <plugins> <plugin interceptor="com.spring_101_200.test_121_130.test_127_mybatis_plugins.DataScopeInterceptor"> <property name="someProperty" value="100"/> </plugin> <plugin interceptor="com.spring_101_200.test_121_130.test_127_mybatis_plugins.QueryScopeInterceptor"> <property name="someProperty" value="100"/> </plugin> </plugins> <objectFactory type="com.spring_101_200.test_121_130.test_128_mybatis_objectfactory.UserObjectFactory"> <property name="email" value="哈哈"/> </objectFactory> <objectWrapperFactory type="com.spring_101_200.test_121_130.test_129_mybatis_objectwrapper.MyMapWrapperFactory"></objectWrapperFactory> <databaseIdProvider type="DB_VENDOR"> <property name="MySQL" value="mysql" /> <property name="Oracle" value="oracle" /> </databaseIdProvider> <environments default="online"> <environment id="development"> <transactionManager type="jdbc"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"></property> <property name="username" value="${db.username}"></property> <property name="password" value="${db.pwd}"></property> </dataSource> </environment> <environment id="online"> <transactionManager type="jdbc"></transactionManager> <dataSource type="POOLED"> <property name="driver" value="${db.driver}"/> <property name="url" value="${db.url}"></property> <property name="username" value="${db.username}"></property> <property name="password" value="${db.pwd}"></property> </dataSource> </environment> </environments> <mappers> <mapper class="com.spring_101_200.test_121_130.test_125_mybatis_properties.UserMapper"></mapper> </mappers> </configuration>
我们看了mybatis原生配置文件后,你会觉得,无非是将mybatis-config.xml配置文件中的内容移植到了Spring boot的yml或properties文件中而已。
接下来,我们继续来看sqlSessionFactory方法上的ConditionalOnMissingBean注解,这个注解的意思 ,就是说当前容器中存在SqlSessionFactory的bean,容器将不会调用sqlSessionFactory()方法实例化SqlSessionFactory,那什么时候会出现SqlSessionFactory被实例化了呢?我们先来看一个例子。
先注释掉yml配置文件中的mybatis配置信息
#mybatis:
# 实体扫描,多个package用逗号或者分号分隔
# type-aliases-package: com.example.springbootstudy.entity
# mapper-locations: classpath:mapper/*Mapper.xml
自定义SqlSessionFactoryBean。
@Configuration public class SqlSessionFactoryBeanConfig { @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){ SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setVfs(SpringBootVFS.class); sqlSessionFactoryBean.setTypeAliasesPackage("com.example.springbootstudy.entity"); sqlSessionFactoryBean.setMapperLocations(resolveMapperLocations(new String [] {"classpath:mapper/*Mapper.xml"})); return sqlSessionFactoryBean; } public Resource[] resolveMapperLocations(String [] mapperLocations) { ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(); List<Resource> resources = new ArrayList<Resource>(); if (mapperLocations != null) { for (String mapperLocation : mapperLocations) { try { Resource[] mappers = resourceResolver.getResources(mapperLocation); resources.addAll(Arrays.asList(mappers)); } catch (IOException e) { // ignore } } } return resources.toArray(new Resource[resources.size()]); } }
而此时MybatisAutoConfiguration的sqlSessionFactory方法将不再执行,为什么呢?聪明的读者肯定会想到是我们自己创建了SqlSessionFactoryBean导致的,从名字上可以看出。SqlSessionFactoryBean不就是SqlSessionFactory的Bean工厂嘛。先看一下SqlSessionFactoryBean的getObject方法,getObject方法返回的是SqlSessionFactory,确定无疑了。但是sqlSessionFactory什么时候初始化呢?从getObject方法得知,如果sqlSessionFactory为空,则调用afterPropertiesSet方法初始化sqlSessionFactory。
@Override public SqlSessionFactory getObject() throws Exception { if (this.sqlSessionFactory == null) { afterPropertiesSet(); } return this.sqlSessionFactory; }
确定是调用getObject方法时实例化的吗?先来看一下SqlSessionFactoryBean类结构 。
其中实现了InitializingBean接口非常重要,而这个接口中的afterPropertiesSet方法,是bean生命周期中必走的方法。下面来看一下Bean的生命周期流程图。
也就是说,在SqlSessionFactoryBean实例化过程中,肯定会实例化sqlSessionFactory。因此。getObject方法中调用afterPropertiesSet,只是为了保险起见而己,一般在调用getObject方法时,sqlSessionFactory不可能为空。
因为SqlSessionFactoryBean是sqlSessionFactory的工厂bean,而在实例化SqlSessionFactoryBean过程中,会将sqlSessionFactory注册到容器中,而由于ConditionalOnMissingBean的作用,因此MybatisAutoConfiguration的sqlSessionFactory()方法不会再执行。
接下来,我们来看MybatisAutoConfiguration中的另外一个方法。
@org.springframework.context.annotation.Configuration @Import({ AutoConfiguredMapperScannerRegistrar.class }) @ConditionalOnMissingBean(MapperFactoryBean.class) public static class MapperScannerRegistrarNotFoundConfiguration { @PostConstruct public void afterPropertiesSet() { logger.debug("No {} found.", MapperFactoryBean.class.getName()); } }
这个方法非常重要,又非常有意思,为什么非常重要呢?因为其中的Import注解,只要配置了Import注解,AutoConfiguredMapperScannerRegistrar类就会被注入到容器中,即使AutoConfiguredMapperScannerRegistrar是一个普通类,没有任何注解,而为什么会有意思呢?如果容器中有MapperFactoryBean,则MapperScannerRegistrarNotFoundConfiguration不会被实例化,当容器中没有MapperFactoryBean 的bean时,会实例化MapperScannerRegistrarNotFoundConfiguration,并且在实例化地过程中,只打印一条日志说并没有发现MapperFactoryBean 的bean。
接下来,我们来看非常重要的部分,AutoConfiguredMapperScannerRegistrar。
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware { private BeanFactory beanFactory; private ResourceLoader resourceLoader; @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { logger.debug("Searching for mappers annotated with @Mapper"); ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); try { if (this.resourceLoader != null) { scanner.setResourceLoader(this.resourceLoader); } //获取默认的Spring Boot 启动类所在的包位置 List<String> packages = AutoConfigurationPackages.get(this.beanFactory); if (logger.isDebugEnabled()) { for (String pkg : packages) { logger.debug("Using auto-configuration base package '{}'", pkg); } } //设置扫描的类中,需要配置Mapper注解 scanner.setAnnotationClass(Mapper.class); //设置过滤器 scanner.registerFilters(); //开始扫描包下的所有类 scanner.doScan(StringUtils.toStringArray(packages)); } catch (IllegalStateException ex) { logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", ex); } } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } } private static final String BEAN = AutoConfigurationPackages.class.getName(); public static List<String> get(BeanFactory beanFactory) { try { return beanFactory.getBean(BEAN, BasePackages.class).get(); } catch (NoSuchBeanDefinitionException ex) { throw new IllegalStateException( "Unable to retrieve @EnableAutoConfiguration base packages"); } }
大家可能比较困惑,为什么通过AutoConfigurationPackages.get(this.beanFactory)方法,就能获取到当前Spring Boot 启动类所在的包呢?我们再来看SpringBootStudyApplication启动类上的SpringBootApplication注解。这不就是一个普通的注解嘛,有什么玄机呢?请听我慢慢道来。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { ... } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(EnableAutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { ... } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class) public @interface AutoConfigurationPackage { ... }
我们在Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析这篇博客就己经分析过。在Spring启动时,会通过processImports方法,递归扫描Bean的注解中是否在Import注解。接来下,我们看其对Import注解中的内容如何处理。
如果Import的内容是ImportBeanDefinitionRegistrar或其实现类,则加入到configClass的importBeanDefinitionRegistrars的属性中,而此时ConfigurationClass就是我们的启动类SpringBootStudyApplication。接着继续来看。
在加载configClasses中有一个重要的方法loadBeanDefinitionsForConfigurationClass。如下
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { if (trackedConditionEvaluator.shouldSkip(configClass)) { String beanName = configClass.getBeanName(); if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { this.registry.removeBeanDefinition(beanName); } this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); return; } if (configClass.isImported()) { registerBeanDefinitionForImportedConfigurationClass(configClass); } for (BeanMethod beanMethod : configClass.getBeanMethods()) { loadBeanDefinitionsForBeanMethod(beanMethod); } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); }
功夫不负有心人,终于看到了importBeanDefinitionRegistrars被使用了。我们继续跟进代码。
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) { for (Map.Entry<ImportBeanDefinitionRegistrar, AnnotationMetadata> entry : registrars.entrySet()) { entry.getKey().registerBeanDefinitions(entry.getValue(), this.registry); } } @Order(Ordered.HIGHEST_PRECEDENCE) static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImport(metadata).getPackageName()); } @Override public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.<Object>singleton(new PackageImport(metadata)); } }
在addImportBeanDefinitionRegistrar方法调用得知,importBeanDefinitionRegistrars的key为registrar,而value为configClass的元数据信息。而最终调用register,使用的metadata其实是启动类SpringBootStudyApplication的类元数据 。而接来下,我们来看看new PackageImport(metadata).getPackageName()这行代码的整体实现。
private final static class PackageImport { private final String packageName; PackageImport(AnnotationMetadata metadata) { this.packageName = ClassUtils.getPackageName(metadata.getClassName()); } public String getPackageName() { return this.packageName; } }
上述代码实现很简单,就是获取元数据类所在的包名。
接下来,我们继续来看register方法的内部实现。
public static void register(BeanDefinitionRegistry registry, String... packageNames) { if (registry.containsBeanDefinition(BEAN)) { BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN); ConstructorArgumentValues constructorArguments = beanDefinition .getConstructorArgumentValues(); constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames)); } else { GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); beanDefinition.setBeanClass(BasePackages.class); beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames); beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); registry.registerBeanDefinition( BEAN, beanDefinition); } }
上述方法实现也非常简单,无非就是注册BasePackages的BeanDefinition到容器中,但是需要注意的一点就是,将启动类的包名作为BasePackages的构造函数参数注入,接下来,我们来看看BasePackages的内部实现。
static final class BasePackages { private final List<String> packages; private boolean loggedBasePackageInfo; BasePackages(String... names) { List<String> packages = new ArrayList<String>(); for (String name : names) { if (StringUtils.hasText(name)) { packages.add(name); } } this.packages = packages; } public List<String> get() { if (!this.loggedBasePackageInfo) { if (this.packages.isEmpty()) { if (logger.isWarnEnabled()) { logger.warn("@EnableAutoConfiguration was declared on a class " + "in the default package. Automatic @Repository and " + "@Entity scanning is not enabled."); } } else { if (logger.isDebugEnabled()) { String packageNames = StringUtils .collectionToCommaDelimitedString(this.packages); logger.debug("@EnableAutoConfiguration was declared on a class " + "in the package '" + packageNames + "'. Automatic @Repository and @Entity scanning is " + "enabled."); } } this.loggedBasePackageInfo = true; } return this.packages; } }
除去日志不看,上面的方法也非常简单,因为在创建Bean的时候,设置了构造函数参数值为启动类所在的包名,所以调用get方法,实际上返回就是启动类的包名。
按这么说,只要修改启动类所在位置,Mapper注解的类将不会被注入到容器中。来测试一把。
显然,换了启动类的所在包的位置,出现了各种问题。
可能项目访问都访问不了。
接下来,我们继续来看registerFilters方法。
public void registerFilters() { boolean acceptAllInterfaces = true; if (this.annotationClass != null) { //设置Mapper注解 addIncludeFilter(new AnnotationTypeFilter(this.annotationClass)); acceptAllInterfaces = false; } //如果指定了接口实现 if (this.markerInterface != null) { addIncludeFilter(new AssignableTypeFilter(this.markerInterface) { @Override protected boolean matchClassName(String className) { return false; } }); acceptAllInterfaces = false; } //既不指定注解,也不指定接口 if (acceptAllInterfaces) { addIncludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { return true; } }); } //排除容器下所有以package-info类名结尾的类 addExcludeFilter(new TypeFilter() { @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { String className = metadataReader.getClassMetadata().getClassName(); return className.endsWith("package-info"); } }); }
对于默认的Spring Boot 加载Mapper类而言,就是扫描启动类所在的包下配置了Mapper注解类,并为Mapper类创建BeanDefinition注册到容器中。
public Set<BeanDefinitionHolder> doScan(String... basePackages) { //扫描包下的所有符合条件的类,并获取BeanDefinition 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; } private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); 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) { if (logger.isDebugEnabled()) { logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'."); } //bean通过类型注入 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
如果当前ClassPathMapperScanner中有sqlSessionFactoryBeanName或sqlSessionTemplateBeanName以及addToConfig参数,则为当前Mapper的BeanDefinition设置这些参数,如果没有sqlSessionFactory和sqlSessionTemplate相关参数,则按类型注入。关于这些参数该如何配置呢?
我们在生产环境中,不可能每个Mapper都去配置@Mapper注解,而我们使用统一的@MapperScan注解来指定Mapper所在的包。会将MapperScan所指定包下的所有Bean的相关BeanDefinition都注册到容器中,接下来,我们来看看@MapperScan注解及使用。
注释掉TestUserMapper上的注解Mapper
在启动类SpringBootStudyApplication上添加@MapperScan(basePackages = { “com.example.springbootstudy” })注解。
在之前,我们先来看一下MapperScan注解源码。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) public @interface MapperScan { //指定包名 String[] value() default {}; //指定包名 String[] basePackages() default {}; //指定类数组 Class<?>[] basePackageClasses() default {}; Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; //指定注解类型 Class<? extends Annotation> annotationClass() default Annotation.class; //指定实现的接口 Class<?> markerInterface() default Class.class; //指定sqlSessionTeamplate String sqlSessionTemplateRef() default ""; //指定sqlSessionFactory String sqlSessionFactoryRef() default ""; //指定 Mapper工厂Bean Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class; }
我们一般情况下,都是使用指定包名称,扫描包下所有的Bean,而其他属性,我们用得少之又少,下面我们来看一个其他属性的使用。先来看看markerInterface属性使用。
public interface BaseMapper { } public interface TestUserMapper extends BaseMapper{ TestUser selectTestUserById(Long id); } @SpringBootApplication @MapperScan(value = "com.example.springbootstudy",markerInterface = BaseMapper.class) public class SpringBootStudyApplication { ... 省略 } @RequestMapping("select") public String select() { helloService.sayHello(); TestUser testUser = testUserMapper.selectTestUserById(14l); System.out.println(JSON.toJSONString(testUser)); return "SUCESS"; }
执行结果
可能有人会想,你不加markerInterface属性,执行结果也一样嘛。
我们去掉markerInterface属性看看。
显然执行结果异常,当将包配置成com.example.springbootstudy.mapper时,执行结果如下。
显然是因为包的扫描范围导致的异常,Spring区分不出HelloService和TestUserMapper接口,哪个是业务类,哪个是Mybatis Mapper类,因此,Spring 一股脑的将其的FactoryBean设置成了MapperFactoryBean,因此导致了以上异常。看下图。
因为设置了HelloService的FactoryBean为MapperFactoryBean,导致在实例化时,创建的Bean是MapperProxy的代理。
调用HelloService的sayHello()方法当然就会出现异常。
通过例子,我们终于知道了markerInterface使用场景,假如Mapper扫描的包无法精确配置时,而MyBatis的Mapper都实现了一个统一的接口BaseMapper,因此,此时就可以使用markerInterface参数,来排除掉和MyBatis无关的类。
接下来,我们来看看annotationClass属性使用
@Documented @Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) public @interface MyMapper { } @MyMapper public interface TestUserMapper { TestUser selectTestUserById(Long id); } @SpringBootApplication @MapperScan(value = "com.example.springbootstudy",annotationClass = MyMapper.class) public class SpringBootStudyApplication { ... }
使用场景和markerInterface一样,只是根据具体的业务需求来做具体实现,MapperScan注解使用这一块,就到这里了,我们接下来分析源码又是如何实现的呢?
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(MapperScannerRegistrar.class) public @interface MapperScan { ... 省略 }
MapperScan上配置了@Import(MapperScannerRegistrar.class),在Spring启动时,会调用Registrar的registerBeanDefinitions方法,对这一块的逻辑,我们之前不知道分析过多少遍了,这里就不再赘述了,直接进入registerBeanDefinitions方法。
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); if (resourceLoader != null) { scanner.setResourceLoader(resourceLoader); } Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass"); if (!Annotation.class.equals(annotationClass)) { scanner.setAnnotationClass(annotationClass); } Class<?> markerInterface = annoAttrs.getClass("markerInterface"); if (!Class.class.equals(markerInterface)) { scanner.setMarkerInterface(markerInterface); } Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator"); if (!BeanNameGenerator.class.equals(generatorClass)) { scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass)); } Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean"); if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) { scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass)); } scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); List<String> basePackages = new ArrayList<String>(); for (String pkg : annoAttrs.getStringArray("value")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (String pkg : annoAttrs.getStringArray("basePackages")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } //注册过滤器 scanner.registerFilters(); //扫描BeanDefinition scanner.doScan(StringUtils.toStringArray(basePackages)); }
registerBeanDefinitions方法的内部实现也非常简单,无非就是将MapperScan注解中配置的内容设置到ClassPathMapperScanner中,再调用其doScan方法而已。
在processBeanDefinitions方法中,有一行非常重要的代码,就是
definition.setBeanClass(this.mapperFactoryBean.getClass());
mapperFactoryBean默认就是MapperFactoryBean类,这就意味着每个Mapper的FactoryBean就是MapperFactoryBean。接下来,我们来看看MapperFactoryBean的内部实现。在看内部实现之前,先来看一个类关系。
接下来,我们来看源码。
public abstract class DaoSupport implements InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); @Override public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { checkDaoConfig(); try { initDao(); } catch (Exception ex) { throw new BeanInitializationException("Initialization of DAO failed", ex); } } protected abstract void checkDaoConfig() throws IllegalArgumentException; protected void initDao() throws Exception { } } public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSession sqlSession; private boolean externalSqlSession; public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (!this.externalSqlSession) { this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } } public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSession = sqlSessionTemplate; this.externalSqlSession = true; } public SqlSession getSqlSession() { return this.sqlSession; } @Override protected void checkDaoConfig() { notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required"); } } public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { private Class<T> mapperInterface; private boolean addToConfig = true; public MapperFactoryBean() { } public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property 'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } } @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } 省略... }
这三个类,我们先从属性入手,因为有setSqlSessionFactory和setSqlSessionTemplate方法,因此,在Bean的初始化时会填充,我们之前分析过创建好的DefaultSqlSessionFactory。再来看MapperFactoryBean有两个构造方法,会调用哪一个呢?再回头来看processBeanDefinitions方法,在这个方法中有一行:
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
将解析到的BeanClassName作为mapperFactoryBean的构造函数参数,因此在实例化时,肯定是调用带参数的构造方法,同时设置了mapperInterface为Mapper的类名称。因为MapperFactoryBean实现了InitializingBean接口,因此在Bean的生命周期中会调用afterPropertiesSet方法,再次回头来看Spring Bean的生命周期图。
而在afterPropertiesSet方法中,做一两件事情,第一件整改,对sqlSessionFactory较验,如果为空,抛出异常,较验通过后,将当前接口加入到configuration的Mapper中,容器中存储的Mapper是最终调用getObject方法返回值,接下来,我们来看看getObject方法内部又是如何实现的。
public <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this); } public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } 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); } } public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
从最终结果来看,返回的是一个MapperProxy的JDK代理。而最终所有的Mapper方法的调用逻辑都在MapperProxy的invoke方法中。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
当我们配置@MapperScan扫描路径不正确时,此时会调用HelloService的 mapperMethod 的cachedMapperMethod 方法,肯定会报错,接下来,我们来看看HelloService出错原因。
简单的来讲,也就是说HelloService没有对应的MappedStatement,更直白的来讲,就是说HelloService接口没有对应的Mapper.xml,可能有读者又会问了,那接口和Mapper.xml又是如何关联起来的呢?
细心的读者肯定会发现,Mapper.xml和Mapper接口,就是通过mapper标签的namespace关联起来的,在之前的Mybatis系列博客中对这一块讲得很透彻了,这里也就再赘述。关于Spring Boot整合MyBatis这一块,好像己经解析完了,好像还漏了什么东西,那就是DataSource这一块。Datasource又是如何注入的呢?
同样是看MybatisAutoConfiguration类。
@org.springframework.context.annotation.Configuration @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) @ConditionalOnBean(DataSource.class) @EnableConfigurationProperties(MybatisProperties.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) public class MybatisAutoConfiguration { ... 省略 }
因为MybatisAutoConfiguration配置了AutoConfigureAfter注解。证明DataSourceAutoConfiguration一定配置在classpath下的META-INF/spring.factories文件内,而DataSourceAutoConfiguration还在MybatisAutoConfiguration先实例化,之前我们也分析过,MybatisAutoConfiguration肯定比SqlSessionFactory先实例化,因此,sqlSessionFactory(DataSource dataSource)方法的dataSource参数,可能在DataSourceAutoConfiguration中,这只是我们的猜测,下面,来分析我们的猜测。
所以,Spring Boot在启动时,会默认扫描DataSourceAutoConfiguration下的Bean。
@Configuration @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Dbcp.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class }) @SuppressWarnings("deprecation") protected static class PooledDataSourceConfiguration { }
为了证实猜想,我们在DataSourceConfiguration.Tomcat的dataSource方法中打一个断点。
显然,程序进入断点,而关键代码是createDataSource方法,我们进入这个方法看看。
protected <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) { return (T) properties.initializeDataSourceBuilder().type(type).build(); } public DataSource build() { Class<? extends DataSource> type = getType(); DataSource result = BeanUtils.instantiate(type); //如果yml中没有配置没有driverClassName属性,从url中获取driverClassName属性 maybeGetDriverClassName(); bind(result); return result; } private void bind(DataSource result) { MutablePropertyValues properties = new MutablePropertyValues(this.properties); //为属性命别名 new RelaxedDataBinder(result).withAlias("url", "jdbcUrl") .withAlias("username", "user").bind(properties); } public void bind(PropertyValues pvs) { MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues) ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs); doBind(mpvs); } protected void doBind(MutablePropertyValues mpvs) { checkAllowedFields(mpvs); checkRequiredFields(mpvs); applyPropertyValues(mpvs); }
上述代码,始终围绕着如何将yml配置文件中的配置,设置到Datasource的属性中去。而比较复杂的代码就是applyPropertyValues方法了,但是最终都是将yml中配置的数据库相关的属性注入到DataSource的poolProperties属性中。当Datasource创建成功后,会调用getValidationQuery方法,获取验证sql(/* ping */ SELECT 1),并最终调用setTestOnBorrow()方法判断Datasource是否创建成功。
大家可能又会想,你这个解析不太有用,因为,我们一般不用org.apache.tomcat.jdbc.pool.DataSource,用是用阿里的com.alibaba.druid.pool.DruidDataSource。接下来,我们来分析一下阿里的DruidDataSource在Spring Boot时,是如何初始化的,导入druid-spring-boot-starter的pom.xml在原来的配置文件中添加type配置,如下。
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.13</version> </dependency> spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver druid: url: jdbc:mysql://172.16.157.238:3306/lz_test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8 username: ldd_biz password: Hello1234 initial-size: 10 max-active: 10 min-idle: 5 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 #Oracle需要打开注释 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: false wall: config: multi-statement-allow: true
其实理解了org.apache.tomcat.jdbc.pool.DataSource的创建过程,再来看DruidDataSource的创建,那就非常简单了。
先来看META-INF
我们先来看看DruidDataSourceAutoConfigure 类
@Configuration @ConditionalOnClass(DruidDataSource.class) @AutoConfigureBefore(DataSourceAutoConfiguration.class) @EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class}) @Import({DruidSpringAopConfiguration.class, DruidStatViewServletConfiguration.class, DruidWebStatFilterConfiguration.class, DruidFilterConfiguration.class}) public class DruidDataSourceAutoConfigure { private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class); @Bean(initMethod = "init") @ConditionalOnMissingBean public DataSource dataSource() { LOGGER.info("Init DruidDataSource"); return new DruidDataSourceWrapper(); } }
先来看看AutoConfigureBefore注解,DruidDataSourceAutoConfigure的AutoConfigureBefore注解中配置了DataSourceAutoConfiguration类,Spring Boot这么做的意图是什么呢?我们在之前的博客中知道。系统默认的Datasource是在DataSourceAutoConfiguration的PooledDataSourceConfiguration静态内部类的Import注解中DataSourceConfiguration.Tomcat bean的dataSource方法注册org.apache.tomcat.jdbc.pool.DataSource的。因此只要DruidDataSourceAutoConfigure比DataSourceAutoConfiguration先实例化。DataSourceConfiguration.Tomcat就不会被实例化。
要得出上面的结论,可能有小伙伴觉得有点牵强,其实我也觉得牵强。
但是上面有一点,无须置疑的一点是,DruidDataSourceAutoConfigure比DataSourceAutoConfiguration先实例化,那么在实例化DataSourceAutoConfiguration的内部类PooledDataSourceConfiguration时,肯定己经存在dataSource了,再来看PooledDataSourceConfiguration的配置。
@Configuration @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Dbcp.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class }) @SuppressWarnings("deprecation") protected static class PooledDataSourceConfiguration { }
我们知道,因为配置了ConditionalOnMissingBean注解,注解中有DataSource,ConditionalOnMissingBean注解的意思是,只要存在DataSource和XADataSource任意一个,PooledDataSourceConfiguration就不会被注入到容器中,但是疑问在于PooledDataSourceConfiguration不会被注入,那他Import中的Bean会被注入吗?如果会被注入,那我们之前说的DataSourceConfiguration.Tomcat不会被实例化容器中的结论就是错误的。
下面我们来看一个例子。
-
在META-INF/spring.factories中配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.example.springbootstudy.service.impl.ConfigurationTestA,
com.example.springbootstudy.service.impl.ConfigurationTestA.ConfigurationTestB -
创建config配置文件
public class ImportTestA { public ImportTestA() { System.out.println("ImportTestA实例化"); } } @Configuration public class ConfigurationTestA { public ConfigurationTestA() { System.out.println("ConfigurationTestA实例化"); } @Configuration @Import(ImportTestA.class) @ConditionalOnMissingBean({ DataSource.class }) class ConfigurationTestB { public ConfigurationTestB() { System.out.println("ConfigurationTestB 实例化"); } } }
启动项目
3. 去掉ConfigurationTestB上的@ConditionalOnMissingBean({ DataSource.class })注解
从测试结果来看,显然己经实例化ImportTestA,为什么呢?
其实我们之前也分析过,在扫描Spring Boot启动类所在的包下所有class文件时,会先通过当前Class上的ConditionalOnMissingBean注解上的Conditional注解配置的Class【OnBeanCondition】,作为当前Class是否注入容器的判断条件,调用OnBeanCondition上的matches方法,如果返回false,则忽略掉当前Class,如果matches方法返回true,才会调用processImports方法,将当前Class所有Import的类的BeanDefinition注册到容器中。因而当前Class都不能被容器注册,那更不会调用processImports方法去扫描当前类的Import注解,去注册Bean了。
这一点明白以后,我们再来看看另外一个疑惑点。
@Configuration public class ConfigurationTestA { public ConfigurationTestA() { System.out.println("ConfigurationTestA实例化"); } @Configuration class ConfigurationTestB { public ConfigurationTestB() { System.out.println("ConfigurationTestB 实例化"); } } } class ConfigurationTestC { public ConfigurationTestC() { System.out.println("ConfigurationTestB 实例化"); } } @Bean public ConfigurationTestC configurationTestC(){ return new ConfigurationTestC(); }
也就是说ConfigurationTestA一定比ConfigurationTestB先实例化吗?我们之前分析过Bean注解,如果ConfigurationTestC上配置了@Bean注解,那么ConfigurationTestA一定比ConfigurationTestC先实例化,因为在创建ConfigurationTestC的BeanDefinition时,将ConfigurationTestA的configurationTestC()方法作为ConfigurationTestC的uniqueFactoryMethod,ConfigurationTestA也作为ConfigurationTestC的FactoryBean,在实例化Bean时,发现有FactoryBean,则需要先实例化FactoryBean。
ConfigurationTestA和ConfigurationTestB都被@Configuration注解修饰,而且ConfigurationTestB作为ConfigurationTestA的内部类。那么ConfigurationTestA一定比ConfigurationTestB先实例化吗?带着疑问,我们还是来先看一个源码。
configurationClasses我们之前在Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析博客中分析过,在Spring启动时,越在configurationClasses集合前面的BeanDefinition,越先被实例化。而我们看到内部类ConfigurationTestA.ConfigurationTestB排在ConfigurationTestA的BeanDefinition前面,那么肯定ConfigurationTestA.ConfigurationTestB比ConfigurationTestA先实例化。一定是这样吗?带着疑问,我们继续追踪代码,我们在ConfigurationTestA的构造函数中打一个断点,来看看。
根据方法调用栈,我们发现在ConfigurationTestA.ConfigurationTestB的实例化过程中,在处理构造函数参数ConstructorResolver的createArgumentArray方法中,出现了ConfigurationTestA类。
也就是ConfigurationTestA.ConfigurationTestB实例化时,根据构造函数参数注入时,需要先创建ConfigurationTestA类。是否如此呢?我们先来看一个例子。
public static void main(String[] args) { Constructor<?>[] rawCandidates = ConfigurationTestA.ConfigurationTestB.class.getDeclaredConstructors(); for (Constructor constructor : rawCandidates) { Class<?>[] classes = constructor.getParameterTypes(); for (Class c : classes) { System.out.println(c.getName()); } } }
结果打印
从结果中得知,内部类的无参构造方法中会注入其所在类对象。因此,即使ConfigurationTestA.ConfigurationTestB和ConfigurationTestA都配置了@Configuration注解,即使内部类ConfigurationTestA.ConfigurationTestB的BeanDefinition在ConfigurationTestA前面,在实例化ConfigurationTestB时,会发现需要其所在类ConfigurationTestA的实例,因此会先调用ConfigurationTestA的getBean方法,获取ConfigurationTestA的实例,因此ConfigurationTestA比ConfigurationTestA.ConfigurationTestB先实例化。带着疑问,我们来看看Spring中获取构造函数参数的代码在哪里。
经过两个例子的分析,我们知道因为AutoConfigureBefore注解DruidDataSourceAutoConfigure肯定比DataSourceAutoConfiguration先实例化。而DruidDataSourceAutoConfigure的实例化过程中会创建DataSource,因此PooledDataSourceConfiguration就不会被实例化。PooledDataSourceConfiguration上的Import注解内容DataSourceConfiguration.Tomcat就更加不会被实例化了,也就org.apache.tomcat.jdbc.pool.DataSource也不会被实例化了,当我们导入druid-spring-boot-starter包时,DruidDataSourceWrapper就代替了org.apache.tomcat.jdbc.pool.DataSource。
接下来,我们继续来看DruidDataSourceWrapper的实例化过程,同样。DruidDataSourceAutoConfigure 的Configuration中配置了@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})注解,容器中会注入DruidStatProperties和DataSourceProperties的bean,并使用环境变量中的值填充DruidStatProperties和DataSourceProperties的属性值,一般环境变量属性值来自于Spring Boot的yml或properties文件。
@ConfigurationProperties("spring.datasource.druid") class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean { @Autowired private DataSourceProperties basicProperties; @Override public void afterPropertiesSet() throws Exception { //if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used. if (super.getUsername() == null) { super.setUsername(basicProperties.determineUsername()); } if (super.getPassword() == null) { super.setPassword(basicProperties.determinePassword()); } if (super.getUrl() == null) { super.setUrl(basicProperties.determineUrl()); } if(super.getDriverClassName() == null){ super.setDriverClassName(basicProperties.getDriverClassName()); } } }
DruidDataSourceWrapper的实例化过程中,肯定会执行afterPropertiesSet,设置username,password,url,driverClassName,而接下来就是Datasource的初始化这一块逻辑了,而DataSource初始化逻辑也不是一篇两篇博客能讲清楚的,将来有机会,再来写关于DataSource这一系列的博客,再来分析其内部实现了,这里就不再深入。
总结 :
关于Spring Boot 整合MyBatis这一块的博客就到里了,如果发现有问题或者有疑问,请在博客下方留言。在读这篇博客时,尽量先去看Spring源码深度解析(郝佳)-Spring 常用注解使用及源码解析 和Spring源码深度解析(郝佳)-学习-Spring Boot体系原理 这两篇博客,不然有些东西,可能还是不明白。如果你能从我的博客中学习到知识或者解读源码的方法,我也是比较高兴的,如果能发现问题,那我就更加高兴,如果对Spring Boot 整合MyBatis这一块有了深入理解的小伙伴,可以去研究一下Spring Boot 是如何整合Redis 和RabbitMQ的。我相信,只有自己学习以后再去实践,实践之后再来学习,这样的效果才会更好。
本文的github地址
https://github.com/quyixiao/spring-boot-study