springboot自动配置魔法

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-appapplication.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以及可用属性prefixsuffix

然后我们需要告诉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小写开头:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值