前言
SpringBoot 凭借 ” 约定大于配置 ” 的理念,已经成为最流行的 web 开发框架,所以有必须对其进行深入的了解;本文通过整合 Mybatis 类来分析 SpringBoot 提供的自动配置 (AutoConfigure) 功能,在此之前首先看一个整合 Mybatis 的实例。
SpringBoot 整合 Mybatis
提供 SpringBoot 整合 Mybatis 的实例,通过 Mybatis 实现简单的增删改查功能;
1. 表数据
CREATE TABLE `role` (`note` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
`role_name` varchar(255) DEFAULT NULL,
`id` bigint(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
提供创建 role 表相关的 sql,对表进行增删改查操作;
2. 整合 Mybatis 的依赖
主要是 mybatis-spring-boot-starter 和使用的 mysql 驱动:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.29</version>
</dependency>
3. 配置 application.properties
提供连接 mysql 相关的信息:url,驱动,用户名,密码;
spring.datasource.url=jdbc:mysql://localhost/mybatis
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
4. 提供 bean 和 Dao
分别提供表对应的 bean 类和操作数据库的 dao 类;
public class Role {
private long id;
private String roleName;
private String note;
// 省略 get/set 方法
}
@Mapper
public interface RoleDao {@Select("SELECT id,role_name as roleName,note FROM role WHERE id = #{id}")
Role findRoleById(@Param("id") long id);
}
5. 提供 Service 和 Controller
public interface RoleService {public Role findRoleById(long roleId);
}
@Service
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleDao roleDao;
@Override
public Role findRoleById(long roleId) {return roleDao.findRoleById(roleId);
}
}
@RestController
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/role")
public String getRole(long id) {return roleService.findRoleById(id).toString();}
}
启动服务,进行简单的测试:http://localhost:8888/role?id=111
结果如下:
Role [id=111, roleName=zhaohui, note=hello]
6. 提出问题
如上实例中,我们使用了很少的配置,就通过 mybatis 实现了操作数据库;正常使用 mybatis 需要的 SqlSessionFactory 和 SqlSession 没有看到被实例化,同时 mybatis 依赖的数据源也没有看到被引用,那 SpringBoot 是如何帮我们自动配置的,下面重点分析一下;
SpringBoot 自动配置
1. 自动配置注解
要想使用自动配置功能,SpringBoot 提供了注解 @EnableAutoConfiguration,当然不需要我们配置因为在 @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 注解本身也有注解 @EnableAutoConfiguration:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {//... 省略...}
在注解 @EnableAutoConfiguration 中重点看一下 @Import 注解中使用的 AutoConfigurationImportSelector 类,此类是自动注解的核心类,会有条件的加载我们默认指定的配置类;这里有两个概念一个是有条件,一个是配置类,分别简单介绍一下:配置类可以简单理解就是相关组件对接 SpringBoot 的对接类,此类可以做一些初始化的工作;有条件表示并不是有配置类就能被对接上,是有条件的,SpringBoot 默认提供了大量配置类,但并不是所有配置类都能被加载初始化的,是有条件的,比如 mybatis 在没有数据源的情况下,没有 mybatis 基础包的情况下是不能被对接的;下面首先看一下 SpringBoot 提供的哪些条件类;
2. 条件类
SpringBoot 提供了很多条件类,可以在配置中上配置注解条件类,相关条件类可以在 spring-boot-autoconfigure 包下的 org.springframework.boot.autoconfigure.condition 下找到,主要包含如下:
- ConditionalOnBean:当前容器有指定 Bean 的条件下;
- ConditionalOnClass:当前类路径下有指定类的条件下;
- ConditionalOnCloudPlatform:当指定了云平台的时候;
- ConditionalOnExpression:SpEL 表达式作为判断条件;
- ConditionalOnJava:JVM 版本作为判断条件;
- ConditionalOnJndi:在 JNDI 存在的条件下查找指定的位置;
- ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下;
- ConditionalOnMissingClass:当类路径下没有指定的类的条件下;
- ConditionalOnNotWebApplication:当前项目不是 WEB 项目的条件下;
- ConditionalOnProperty:当前应用是否配置了指定属性指定的值;
- ConditionalOnResource:只有当指定的资源位于类路径下;
- ConditionalOnSingleCandidate:bean 工厂中只有一个或者有多个情况下是主要的候选 bean;
- ConditionalOnWebApplication:当前项目是 WEB 项目的条件下。
以上是注解类,注解本身没有功能,只是提供标记的功能,具体功能在 @Conditional 中指定的,比如 ConditionalOnBean 注解如下所示:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {//... 省略...}
相关功能的实现就在 OnBeanCondition 类中,同样其他注解类的实现类也在包 org.springframework.boot.autoconfigure.condition 下找到;
3. 自动配置过程
Springboot 应用启动过程中使用 ConfigurationClassParser 分析配置类,此类中有一个 processImports 方法,此方法用来处理 @Import 注解,在 @EnableAutoConfiguration 注解存在 @Import 注解,这时候会实例化注解中的 AutoConfigurationImportSelector,在其内部有一个 AutoConfigurationGroup 内部类,内部类有两个核心方法分别是:process 和 selectImports;
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
此方法主要获取经过条件过滤之后可用的自动配置类,主要调用 AutoConfigurationImportSelector 中的 getAutoConfigurationEntry 完成的:
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
首先获取了所有备选的自动配置类,然后删除了重复和被排除的类,最后通过条件进行筛选出可用的配置类,下面分别看一下,首先看一下如何获取所有备选的配置类:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
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;
}
通过 SpringFactoriesLoader 获取类路径下 META-INF/spring.factories 文件中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的配置类,可以看一下 spring-boot-autoconfigure.jar 中的 spring.factories 内容:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
//... 以下省略...
当然这里只是截取了其中一个类路径 jar 下的部分配置,获取所有配置类之后进行去重,去被排除的类,然后进行条件过滤,下面重点看一下:
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {long startTime = System.nanoTime();
String[] candidates = StringUtils.toStringArray(configurations);
boolean[] skip = new boolean[candidates.length];
boolean skipped = false;
for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {invokeAwareMethods(filter);
boolean[] match = filter.match(candidates, autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {if (!match[i]) {skip[i] = true;
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {return configurations;}
List<String> result = new ArrayList<>(candidates.length);
for (int i = 0; i < candidates.length; i++) {if (!skip[i]) {result.add(candidates[i]);
}
}
if (logger.isTraceEnabled()) {int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered" + numberFiltered + "auto configuration class in"
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + "ms");
}
return new ArrayList<>(result);
}
此方法大致就是首先获取配置的 AutoConfigurationImportFilter,然后对之前获取的所有配置类进行过滤,最后返回过滤之后的配置类;AutoConfigurationImportFilter 同样也是通过 SpringFactoriesLoader 类进行加载类路径下 META-INF/spring.factories,只不过当前的 key 是:org.springframework.boot.autoconfigure.AutoConfigurationImportFilter,可以看一下 SpringBoot 默认配置的 filter:
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
可以看到 Filter 其实就是上文介绍的条件类,这里默认了 OnBeanCondition,OnClassCondition 以及 OnWebApplicationCondition,已这里使用的 Mybatis 为例看一下 MybatisAutoConfiguration 的注解:
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class MybatisAutoConfiguration implements InitializingBean {//... 以下省略...}
可以看到其中有用到 @ConditionalOnClass,表示必须提供 SqlSessionFactory 和 SqlSessionFactoryBean 类的情况下才加载此配置类,而整两个是正式 Mybatis 基础包中提供的;有了基础包还不行,还需要 DataSource,而且 DataSource 必须在 MybatisAutoConfiguration 实例化之前初始化好,SpringBoot 是如何实现,继续看另外一个核心方法 selectImports():
@Override
public Iterable<Entry> selectImports() {if (this.autoConfigurationEntries.isEmpty()) {return Collections.emptyList();
}
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
processedConfigurations.removeAll(allExclusions);
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
.collect(Collectors.toList());
}
private List<String> sortAutoConfigurations(Set<String> configurations,
AutoConfigurationMetadata autoConfigurationMetadata) {return new AutoConfigurationSorter(getMetadataReaderFactory(), autoConfigurationMetadata)
.getInPriorityOrder(configurations);
}
首先是对被排除类的一个过滤,然后接下来重点看一下对配置类进行排序的一个方法,具体操作在类 AutoConfigurationSorter 中进行的,具体方法为 getInPriorityOrder():
public List<String> getInPriorityOrder(Collection<String> classNames) {
AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory,
this.autoConfigurationMetadata, classNames);
List<String> orderedClassNames = new ArrayList<>(classNames);
// Initially sort alphabetically
Collections.sort(orderedClassNames);
// Then sort by order
orderedClassNames.sort((o1, o2) -> {int i1 = classes.get(o1).getOrder();
int i2 = classes.get(o2).getOrder();
return Integer.compare(i1, i2);
});
// Then respect @AutoConfigureBefore @AutoConfigureAfter
orderedClassNames = sortByAnnotation(classes, orderedClassNames);
return orderedClassNames;
}
首先使用 order 进行排序,然后使用 @AutoConfigureBefore 和 @AutoConfigureAfter 就行排序;order 其实就是通过注解 @AutoConfigureOrder 进行排序的,值是一个整数,结构类似如下:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureBefore 和 @AutoConfigureAfter 字面意思也很好理解,指定在其他配置类之前和之后,所以可以看到在 MybatisAutoConfiguration 中有如下配置:
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
表示在 DataSourceAutoConfiguration 配置类加载之后才会加载 Mybatis 配置类,这样就解决了依赖关系;还有上文提到的 Mybatis 操作数据库依赖的 SqlSessionFactory 和 SqlSession,都在 MybatisAutoConfiguration 进行了初始化操作;SpringBoot 本身其实以及提供了大量常用组件的自动配置类,我们只需要提供满足的特定条件,SpringBoot 自动会帮我加载初始化等操作,但是肯定也有自定义配置类的需求,下面用一个简单的实例来看看如何自定义一个自动配置类;
自定义配置类
接下来我们用很简单的实例来看一下自定义的流程,一个格式化大写消息的实例;
1.pom 文件引入依赖
<groupId>com.format</groupId>
<artifactId>format-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>format-spring-boot-starter</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Spring 官方 Starter 通常命名为 spring-boot-starter-{name}如 spring-boot-starter-web,Spring 官方建议非官方 Starter 命名应遵循{name}-spring-boot-starter 的格式;
2. 服务类和属性配置类
@ConfigurationProperties("format.service")
public class FormatServiceProperties {
private String type;
//...get/set 省略...
}
public class FormatService {
private String type;
public FormatService(String type) {this.type = type;}
public String wrap(String word) {if(type.equalsIgnoreCase("Upper")){// 大写
return word.toUpperCase();}else if(type.equalsIgnoreCase("Lower")){// 小写
return word.toLowerCase();}
return word;
}
}
属性类提供了 type 参数可以在 application.properties 中配置,可配置值包括:upper,lower;
3. 自动配置类和创建 spring.factories 文件
@Configuration
@ConditionalOnClass(FormatService.class)
@EnableConfigurationProperties(FormatServiceProperties.class)
public class FormatAutoConfigure {
@Autowired
private FormatServiceProperties properties;
@Bean
@ConditionalOnMissingBean
FormatService formatService() {return new FormatService(properties.getType());
}
}
这个就是自定义的自动配置类,SpringBoot 启动的时候会根据条件自动初始化;最后在 resources/META-INF/ 下创建 spring.factories 文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.format.FormatAutoConfigure
4. 测试
在其他 SpringBoot 中可以引入上面创建的项目,引入方式也很简单:
<dependency>
<groupId>com.format</groupId>
<artifactId>format-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
同时在 application.properties 配置格式化类型:
format.service.type=upper
启动应用,浏览器访问 http://localhost:8888/format?word=hello,结果为:HELLO
总结
本文从使用 SpringBoot 整合 Mybatis 开始,然后提出使用中产生的疑问,进而通过分析源码的方式来理解 SpringBoot 的自动配置机制,最后自定义了一个自动配置类来看看具体如何使用;SpringBoot 通过自动配置的方式帮助开发者减少了很大的工作量,达到开箱即用的效果;但是另一方面如果出现问题需要调试可能不是那么好定位。
示例代码地址
https://github.com/ksfzhaohui…
- [springboot]
- [format-spring-boot-starter]