1、SpringBoot Starter介绍
随着Spring的日渐臃肿,为了简化配置、开箱即用、快速集成,Spring Boot 横空出世。 目前已经成为 Java 目前最火热的框架了。平常我们用Spring Boot开发web应用。Spring mvc 默认使用tomcat servlet容器, 因为Spring mvc组件集成了spring-boot-starter-tomcat 。但是现在undertow servlet容器的性能非常好。我们可以通过以下方式先排除tomcat:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
<!-- 排除Tomcat依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
然后直接替换为undertow:
<!--添加 Undertow依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
代码无需更改。这就是组件化后的好处:1.可插拔。2.可定制。3.按需集成。为什么能够做到快速适配?Spring Boot 有一个“约定大于配置”的规则,让程序组件之间来减少配置,降低复杂性。因此在开发中自定义一个Spring Boot Starter的时候,也最好考虑你的starter如何达到以上的便利性。
2、Spring Boot的一些约定
2.1、starter命名规范
Spring官方提供的starter命名规范:spring-boot-starter-xxx.jar,
第三方的starter命名规范:xxx-spring-boot-starter.jar,比如mybatis-spring-boot-starter
3、starter自动配置原理
starter会将具备某种功能的坐标打包在一起,可以简化导入依赖的过程,例如,当导入spring-boot-starter-web
这个Starter时,会将和Web开发的相关jar包一起导入到项目中。
自动配置设计了几个关键的步骤:
- 基于Java代码的Bean配置
- 自动配置条件依赖
- Bean参数获取
- Bean的发现
- Bean的装载
接下来,就mybatis-spring-boot-starter
这个starter的引入,基于上面5个步骤来讲解Starter的自动配置原理
3.1、基于Java代码的Bean配置
pom文件中添加mybatis-spring-boot-starter的依赖坐标后,将会导入相关的jar包。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
其中一个重要的的jar包为mybatis.spring.boot.autoconfigure,根据命名可知,该jar包涉及了mybatis的自动装配信息。
在该jar中,有一个非常关键的类 MybatisAutoConfiguration,关键代码如下:
@Configuration 与 @Bean 注解的使用,可以创建一个基于java的配置类,相当于原生的xml配置文件。根据MybatisAutoConfiguration,自动创建了一个SqlSessionFactory session工厂和一个操作Sql的SqlSessionTemplate的template工具类,交给Spring容器自动管理。
@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 {
//忽略业务代码
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
//忽略业务代码
}
}
3.2、自动配置条件依赖
根据MybatisAutoConfiguration 上标注的其他注解可知,完成Mybatis配置类的装配是有多个依赖条件的:
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class)
完成自动装配首先类路径上必须存在SqlSessionFactory和SqlSessionFactoryBean两个类,同时DataSource的单例Bean也必须先行加载到Spring容器中。
同时也可根据 生成SqlSessionFactory 和SqlSessionTemplate 的方法参数可知必须依赖SqlSessionFactory、DataSource。
注解 | 功能说明 |
@ConditionalOnBean | 仅在当前上下文中存在某个bean时,才会实例化这个Bean |
@ConditionalOnClass | 某个class位于类路径上,才会实例化这个Bean。 使用场景: 当需要确保某些配置仅在引入特定第三方库的情况下才被激活。 |
@ConditionalOnExpression | 当表达式为true的时候,才会实例化这个Bean |
@ConditionalOnMissingBean | 仅在当前上下文中不存在某个bean时,才会实例化这个Bean |
@ConditionalOnMissingClass | 某个class在类路径上不存在的时候,才会实例化这个Bean |
@ConditionalOnNotWebApplication | 不是web应用时才会实例化这个Bean |
@AutoConfigureAfter | 在某个bean完成自动配置后实例化这个bean |
@AutoConfigureBefore | 在某个bean完成自动配置前实例化这个bean |
3.3、Bean参数获取
要完成mybatis的自动配置,需要在配置文件中提供数据源相关的参数,例如数据库驱动
URL,数据库用户名及密码等。根据@AutoConfigureAfter({DataSourceAutoConfiguration.
class, MybatisLanguageDriverAutoConfiguration.class})可知,数据源信息是通过DataSou
rceAutoConfiguration配置类先行配置的,关键代码如下:
package org.springframework.boot.autoconfigure.jdbc
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = {"io.r2dbc.spi.ConnectionFactory"})
@AutoConfigureBefore({SqlInitializationAutoConfiguration.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, InitializationSpecificCredentialsDataSourceInitializationConfiguration.class, SharedCredentialsDataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
......
}
Bean参数的获取离不开@EnableConfigurationProperties({DataSourceProperties.class}) 注解的使用,即引入数据源相关的配置属性,能将application.yml/application.properties配置文件中值加载到该属性配置类DataSourceProperties这个POJO对象上。【属性名和yml中配置的key保持一致】
//application.yml/application.properties 中配置数据源的前缀
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
private ClassLoader classLoader;
private boolean generateUniqueName = true;
private String name;
private Class<? extends DataSource> type;
private String driverClassName;
private String url;
private String username;
private String password;
private String jndiName;
//忽略get set方法
}
实现Bean参数获取的功能离不开两个重要的注解:
- @ConfigurationProperties 能将application.yml/application.properties中配置的值封装映射到标注了该bean的相应属性上.
- @EnableConfigurationProperties 启用自动装配属性,使得@ConfigurationProperties注解生效,两者搭配使用,缺一不可。
3.4、Bean的发现
SpringBoot默认扫描:启动类所在包下的类以及子类的所有组件,但并没有包括依赖包中类,这里不得不思考下,依赖包中的bean是如何被发现和加载的,需要从SpringBoot项目的启动类上进行追踪,在启动类上我们会加上@SpringBootApplication注解。
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
}
@EnableAutoConfiguration标识着开启了自动装配的功能:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration
@Import(AutoConfigurationImportSelector.class)引入自动加载选择器,其中有一个方法装载候选Bean.
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;
}
能将META-INF下的spring.factories文件配置的类通过类加载器装载到Spring 容器中。关于mybatis自动装配的类如下 :
# Auto Configure
org.spingframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis,spring.boot,autoconfigure.MybatisLanguageDriverAutoconfiguration,\
org.mybatis.spring.boot,autoconfigure.MybatisAutoConfiguration
3.5、Bean的加载
在SpringBoot应用中,自动配置加载Bean是通过@Import注解这种方式,AutoConfigurationImportSelector类的selectinports方法返回一组从META-INF/spring.factories文件读取的全限定类名,通过反射并实例化这些Bean到Spring容器中。
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
4、自定义一个starter
先创建一个项目,在该项目中定义 Starter 的内容,然后通过 Maven 将其打成 jar 包,之后在另一个项目中使用该 Starter 。
4.1、创建一个maven 项目,并引入如下依赖:
创建一个 Maven 项目,在其 pom 文件中引入自动装配的依赖,并定义好 Starter 的名称。
spring-boot-autoconfigure
依赖是必须要引入的,spring-boot-configuration-processor
的引入是为了在配置文件中使用属性时有提示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
4.2、新建一个 Properties
配置类,
配置类用于保存外部化配置文件中定义的配置数据,其中配置文件包括 properties 或 yml 。
// 定义配置文件中的属性前缀
@ConfigurationProperties(prefix = "demo")
public class DemoProperties {
private String name;
private String date;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
}
4.3、新建一个功能类
功能类 主要用来返回 DemoProperties
中的 name 和 date 属性。
public class DemoService {
private DemoProperties demoProperties;
public DemoService(DemoProperties demoProperties) {
this.demoProperties = demoProperties;
}
public String getName() {
return demoProperties.getName();
}
public String getDate() {
return demoProperties.getDate();
}
}
4.4、创建自动配置类
在该配置类中完成 Starter 的功能。这里,通过构造器注入 DemoProperties
配置类对象,并初始化 DemoService
功能类。
@Configuration
@EnableConfigurationProperties(value = DemoProperties.class)
public class DemoAutoConfiguration {
private final DemoProperties demoProperties;
public DemoAutoConfiguration(DemoProperties demoProperties) {
this.demoProperties = demoProperties;
}
@Bean
// 当前项目是否包含 DemoService Class
@ConditionalOnMissingBean(DemoService.class)
public DemoService demoService() {
return new DemoService(demoProperties);
}
}
自动配置类是 SpringBoot
自动装配特性不可或缺的一环,关于 SpringBoot
自动装配底层实现,大家可以参考《SpringBoot自动装配(二)》这篇文章。
4.5、自定义初始化器和监听器
这是 SpringBoot
提供的扩展点,主要在 SpringBoot
的不同生命周期执行相应操作.
public class DemoApplicationContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
System.out.println(" DemoApplicationContextInitializer 初始化成功 ");
}
}
public class DemoApplicationListener implements ApplicationListener<SpringApplicationEvent {
@Override
public void onApplicationEvent(SpringApplicationEvent springApplicationEvent) {
if (springApplicationEvent instanceof ApplicationStartingEvent) {
System.out.println(" DemoApplicationListener 监听 ApplicationStartingEvent 事件");
}
}
}
关于初始化器和监听器大家可以参考《Spring Boot SpringApplication启动类(一)》的 2.2 和 2.3 小节 。
4.6、starter生效配置
在 src/main/resources 目录下创建 META-INF 文件夹,并在文件夹中创建 spring.factories
文件,定义如下内容:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
com.loong.demo.context.DemoApplicationContextInitializer
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.loong.demo.configuration.DemoAutoConfiguration
# Application Listeners
org.springframework.context.ApplicationListener=\
com.loong.demo.listener.DemoApplicationListener
说明:自动配置类放入自动配置文件中,不同版本对应的配置文件不同,按需选择。
-
springboot 2.7 之前自动配置文件为
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,\
-
springboot 2.7到springboot 3.0,自动配置文件可以使用
spring.factories
,也可以使用META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
,推荐使用后者 -
springboot 3.0之后自动配置文件只能使用
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
4.7、starter主动生效和被动生效
-
一种是主动生效,在starter组件集成入Spring Boot应用时需要你主动声明启用该starter才生效,即使你配置完全。这里会用到@Import注解,将该注解标记到你自定义的@Enable注解上, 我们将@EnableSMS 该注解标记入Spring Boot应用就可以使用短信功能了。
/**
* 启用demo信息配置
*/
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DemoAutoConfiguration .class)
public @interface EnableDemo {
}
- 另一种被动生效,在starter组件集成入Spring Boot应用时就已经被应用捕捉到。这里会用到类似java的SPI机制。在autoconfigure资源包下新建META-INF/spring.factories写入DemoAutoConfiguration全限定名。 多个配置类逗号隔开,换行使用反斜杠。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.loong.demo.configuration.DemoAutoConfiguration
5、测试自定义 Starter
5.1、在另一个项目中引入该 Starter 的 Maven 依赖:
<dependency>
<groupId>com.loong</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
5.2、在 properties 文件中定义配置数据:
demo.name = loong
demo.date = 2020.01.01
5.3、获取自动配置类:
在启动类中,获取 DemoService
Bean ,并调用它的 getDate 和 getName 方法获取配置文件中的数据:
@SpringBootApplication
public class DiveInSpringBootApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DiveInSpringBootApplication.class, args);
DemoService bean = run.getBean(DemoService.class);
System.out.println(bean.getDate() + " === " + bean.getName());
}
}
最后,查看控制台的输出:
/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA CE.app..."
DemoApplicationListener 监听 ApplicationStartingEvent 事件
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.3.RELEASE)
DemoApplicationContextInitializer 初始化成功
2024-01-01 13:14:02.023 INFO 55657 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.189 INFO 55657 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@6b19b79: startup date [Wed Jan 01 13:13:59 CST 2020]; root of context hierarchy
2024-01-01 13:14:02.257 INFO 55657 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/hello],methods=[GET]}" onto public java.lang.String com.loong.diveinspringboot.Chapter1.controller.HelloWorldController.helloWorld(java.lang.String)
2024-01-01 13:14:02.260 INFO 55657 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2024-01-01 13:14:02.261 INFO 55657 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2024-01-01 13:14:02.296 INFO 55657 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.296 INFO 55657 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-01-01 13:14:02.341 WARN 55657 --- [ main] ion$DefaultTemplateResolverConfiguration : Cannot find template location: classpath:/templates/ (please add some templates or check your Thymeleaf configuration)
2024-01-01 13:14:02.718 INFO 55657 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 2 endpoint(s) beneath base path '/actuator'
2024-01-01 13:14:02.726 INFO 55657 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/health],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2024-01-01 13:14:02.727 INFO 55657 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/info],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web.servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
2024-01-01 13:14:02.728 INFO 55657 --- [ main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator],methods=[GET],produces=[application/vnd.spring-boot.actuator.v2+json || application/json]}" onto protected java.util.Map<java.lang.String, java.util.Map<java.lang.String, org.springframework.boot.actuate.endpoint.web.Link>> org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping.links(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2024-01-01 13:14:02.766 INFO 55657 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2024-01-01 13:14:02.822 INFO 55657 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2024-01-01 13:14:02.826 INFO 55657 --- [ main] c.l.d.C.DiveInSpringBootApplication : Started DiveInSpringBootApplication in 3.607 seconds (JVM running for 3.984)
2020.01.01 === loong
可以看到,结果正确输出,且初始化器和监听器都已被加载。这里只是一个简单的演示,Starter 较为简单,大家可以根据实际情况实现一个更为复杂的。