Springboot Auto Configuration
demo
我们会用一个小demo来开启springboot自动配置的讲解。
父工程的pom:
<dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
我们写一个service叫做hello-service
:
package hello;
public interface HelloService {
void sayHello(String name);
}
package hello;
public class ConsoleHelloService implements HelloService {
private final String prefix;
private final String suffix;
public ConsoleHelloService(String prefix, String suffix) {
this.prefix = prefix != null ? prefix : "Hello";
this.suffix = suffix != null ? suffix : "!";
}
public ConsoleHelloService(){
this(null,null);
}
@Override
public void sayHello(String name) {
String msg = String.format("%s %s%s", this.prefix, name, this.suffix);
System.out.println(msg);
}
}
它的功能就是打个招呼。
我们将这个模块引入hello-app
:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springboot.auto</groupId>
<artifactId>hello-service</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
并在hello-app
中提供boot启动就运行的类:
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class HelloCommandLineRunner implements CommandLineRunner {
private HelloService helloService;
public HelloCommandLineRunner(HelloService helloService) {
this.helloService = helloService;
}
public void run(String... args) throws Exception {
this.helloService.sayHello("world");
}
}
我们使用主启动类启动boot:
@SpringBootApplication
public class HelloApp {
public static void main(String[] args) {
SpringApplication.run(HelloApp.class,args);
}
@Bean
public HelloService helloService(){
return new ConsoleHelloService();
}
}
注意,对于hello-app
来说,HelloService
是手动注入进去的。
@SpringBootApplication注解
问题是:主启动类上的@SpringBootApplication
是做什么的?
它主要由三个注解组成:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
首先是@ComponentScan
。它扫描主启动类以及下级目录的所有类。所以springboot要求主启动类必须写在工程的最外面。
然后是@SpringBootConfiguration
,它其实就是一个configuration:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration
其实,我们只要这两个注解就能够让程序跑起来:
@ComponentScan
@Configuration
public class HelloApp {
public static void main(String[] args) {
SpringApplication.run(HelloApp.class,args);
}
@Bean
public HelloService helloService(){
return new ConsoleHelloService();
}
}
现在我们要了解自动配置。
我们知道,剩下的那个注解@EnableAutoConfiguration
完成了自动配置的工作。
但是在点进去之前,我们自己试着把项目做成自动配置的样式。
将demo改成自动配置
我们再创建一个模块hello-starter
。
pom为:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springboot.auto</groupId>
<artifactId>hello-service</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
主类:
@Configuration
public class HelloAutoConfiguration {
@Bean
public HelloService helloService(){
return new ConsoleHelloService();
}
}
我们希望这个类是个Configuration
,并且手动注入HelloService
。
在test中,我们使用spring上下文环境测试:
public class HelloAutoConfigurationTest {
@Rule
public OutputCapture outputCapture = new OutputCapture();
private final ApplicationContextRunner contextRunner
= new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(HelloAutoConfiguration.class));
@Test
public void defaultServiceIsAutoConfigured() {
this.contextRunner.run(context -> {
assertThat(context).hasSingleBean(HelloService.class);
context.getBean(HelloService.class).sayHello("world");
assertThat(this.outputCapture.toString().contains("Hello world!"));
});
}
}
withConfiguration(AutoConfigurations.of(HelloAutoConfiguration.class))
表示我们希望HelloAutoConfiguration
对于当前测试是自动配置的。
为了完成HelloAutoConfiguration
的自动配置,我们需要重要的一步:
在resources
目录下创建META-INF
,在META-INF
下创建spring.factories
。
在spring.factories
中写:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=hello.autoconfigure.HelloAutoConfiguration
其中
org.springframework.boot.autoconfigure.EnableAutoConfiguration
这个注解主管自动配置。
跑test,自测通过。
这时候在应用hello-app
中,我们就可以使用完成了自动配置的starter了:
<dependency>
<groupId>org.springboot.auto</groupId>
<artifactId>hello-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
现在的问题是,容器中有两个一样的bean,一个是自动配置提供的,一个是程序员自己注入的。
@SpringBootApplication
public class HelloApp {
public static void main(String[] args) {
SpringApplication.run(HelloApp.class,args);
}
@Bean
public HelloService helloService(){
return new ConsoleHelloService("Howdy","#");
}
}
为了让程序不报错,我们在hello-app
的application.yml
中配置:
spring:
main:
allow-bean-definition-overriding: true
运行结果是Hello world!
。
也就是说,springboot的自动配置覆盖了用户自定义的配置。
解决bean的冲突
回到hello-starter
模块。
在HelloAutoConfigurationTest
中添加用户自定义配置(模拟现实情况):
@Configuration
static class UserConfiguration{
@Bean
public HelloService myHelloService(){
return new ConsoleHelloService("Mine","**");
}
}
让spring容器带着这个用户配置运行:
@Test
public void myDefaultServiceIsAutoConfigured() {
this.contextRunner.withUserConfiguration(UserConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(HelloService.class);
context.getBean(HelloService.class).sayHello("works");
assertThat(this.outputCapture.toString().contains("Mine works**"));
});
}
声明单例,却发现两个。
这时候我们要引进新的注解:
@ConditionalOnClass(HelloService.class)
@Configuration
public class HelloAutoConfiguration {
@ConditionalOnMissingBean
@Bean
public HelloService helloService(){
return new ConsoleHelloService();
}
}
@ConditionalOnClass(HelloService.class)
说如果类路径中压根没有HelloService
这个类型,那么整个自动配置就啥也别干。
@ConditionalOnMissingBean
说如果容器里没有HelloService
,那就用我这个默认的。
如此一来test通过:
Mine works**
hello-app
也打印了用户自定义的信息Howdy world#
。
在日志中我们看到:
============================
CONDITIONS EVALUATION REPORT
============================
HelloAutoConfiguration#helloService:
Did not match:
- @ConditionalOnMissingBean (types: hello.HelloService; SearchStrategy: all) found beans of type 'hello.HelloService' helloService (OnBeanCondition)
使用yml配置属性
在hello-app
中,如果是让程序员手动注入bean:
@Bean
public HelloService helloService(){
return new ConsoleHelloService("Howdy","#");
}
实在是有些麻烦。
我们希望能在配置文件中解决。
回到自动配置模块hello-starter
。
我们写一个配置类:
@ConfigurationProperties("hello")
public class HelloProperties {
private String prefix = "Hello";
private String suffix = "!";
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
这里,我们定义了一个名称空间hello
以及可用属性prefix
和suffix
。
然后我们需要告诉HelloAutoConfiguration
:
@ConditionalOnClass(HelloService.class)
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {
@ConditionalOnMissingBean
@Bean
public HelloService helloService(HelloProperties helloProperties){
return new ConsoleHelloService(helloProperties.getPrefix(),helloProperties.getSuffix());
}
}
如果类路径中有HelloService
,那就给我创建一个HelloProperties
的bean。
(测试要跑一下的,否则无法生效:)
@Test
public void defaultServiceIsAutoConfigured() {
this.contextRunner.
withUserConfiguration(UserConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(HelloService.class);
context.getBean(HelloService.class).sayHello("world");
assertThat(this.outputCapture.toString().contains("Hello world!"));
});
}
这时候我们就可以在hello-app
中配置属性了:
spring:
main:
allow-bean-definition-overriding: true
hello:
prefix:
"Howdy"
启动类手动注入的bean也可以删除了。
还有一个问题,当我们打hello.prefix
的时候,yml没有给我们提示。
这样很不方便。
另一方面,我们看到在属性配置类有一个红字警告:
这是因为我们没有自动配置模块的元数据。
在hello-starter
的pom中加入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
同时你也可以在属性配置类加一些注释:
跑过测试后,出现了元数据文件spring-configuration-metadata.json
:
这时候hello-app
的yml中就会有提示了:
更多自定义的自动配置
也不是所有的情况我都给你自动配置。springboot提供了更多自主的选择。
比如上面的例子,如果用户没有提供prefix
,或者说,你的prefix
小写字母开头,这两种情况,我们就不给你自动装配HelloService
。
为了演示,我们删掉属性配置类的默认prefix
:
并且写两个test:
@Test
public void defaultServiceIsNotAutoConfiguredIfPrefixIsMissing() {
this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(HelloService.class));
}
@Test
public void defaultServiceIsNotAutoConfiguredWithWrongPrefix() {
this.contextRunner.withPropertyValues("hello.ocean").
run(context -> assertThat(context).doesNotHaveBean(HelloService.class));
}
一个测试你没有给prefix
,另一个测试你给的prefix
小写字母开头。
当然,这两个测试都通不过。
现在我们自己写一个SpringBootCondition
:
package hello.autoconfigure;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;
class OnValidHelloServicePrefixCondition extends SpringBootCondition {
private static final String PROPERTY_NAME = "hello.prefix";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder condition = ConditionMessage.forCondition("ValidHelloServicePrefix");
Environment environment = context.getEnvironment();
if(environment.containsProperty(PROPERTY_NAME)){
String value = environment.getProperty(PROPERTY_NAME);
if(Character.isUpperCase(value.charAt(0))){
return ConditionOutcome.
match(condition.available(String.format("valid prefix (%s)",value)));
}
return ConditionOutcome.noMatch(condition.because(String.format("rejected the prefix" +
"%s as it does not start with an upper-case character",value)));
}
return ConditionOutcome.noMatch("does not provide a prefix!");
}
}
里面进行了有没有前缀
和前缀是否大写字母开头
的逻辑判断。
我们把这个自定义的condition
封装成一个注解:
package hello.autoconfigure;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnValidHelloServicePrefixCondition.class)
public @interface ConditionalOnValidHelloServicePrefix {
}
然后将其加在bean上面:
@ConditionalOnMissingBean
@Bean
@ConditionalOnValidHelloServicePrefix
public HelloService helloService(HelloProperties helloProperties){
return new ConsoleHelloService(helloProperties.getPrefix(),helloProperties.getSuffix());
}
测试通过。
为了让我们的debug输出具体的报错信息,我们要给出具体的condition
:
class OnValidHelloServicePrefixCondition extends SpringBootCondition {
private static final String PROPERTY_NAME = "hello.prefix";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder condition = ConditionMessage.forCondition(ConditionalOnValidHelloServicePrefix.class);
……
}
只需要把我们注解的类型传给forCondition
方法就行了。
运行hello-app
。
如果你不给prefix
:
如果你的prefix
小写开头: