Spring Boot的Auto-configuration以其易用性和实用性,得到了开发者们的广泛认可;但与此同时,Spring Boot内置的Auto-configuration仅能满足基本需求,对于企业级的应用生态来说是不够的,所以自定义Auto-configuration变的尤为重要。

本文将从这几个方面阐述如何自定义一个Auto-configuration: 原理、结构、实战步骤。

1. 原理

Spring是如何发现自动配置的内容,并选择性的加载组件呢?

自动配置功能的实现,得益于spring-context的强扩展性,主要使用了它两个扩展点: @ImportImportSelector@ConditionalCondition

  1. @EnableAutoConfiguration内部使用@ImportImportSelector特性,使配置可以委托到外部来处理。
  2. 在接入点切入后,再使用各种自定义的@ConditionalCondition来筛选预先定义好的自动配置类和相关的Bean。

简单来讲,Spring Boot的Auto-configuration依靠spring-context中提供已有特性,做了大量的默认配置(通常也是最常见的重复性配置),使开发者的配置方式由“从头开始配置”转变为了“直接使用默认配置或覆盖小部分配置”,着实解放了双手。

另外,@ImportImportSelector是在入口处统一处理的,开发者只需要在META-INF/spring.factories中增加一个配置,即可让ImportSelector识别我们自定义的Auto-configuration; 所以,开发一个Auto-configuration,实质上就是基于@ConditionalCondition实现一个@Configuration类

更详细的原理阐述

在笔者的上一篇文章中,曾尝试详细阐述其原理,在此不再赘述,有兴趣的开发者可以点击《Spring Boot Auto-configuration 自动配置详解》查看。

2. 结构

想要自定义一个Auto-configuration,首先必须了解其内部结构,知晓哪些部分是开放给开发者实现的。MultipartAutoConfiguration具有自动配置MultipartResolver的功能, 现以MultipartAutoConfiguration为例,分析其类图如下:

uml

图中红框标识的四部分通常需要定制,以表格的形式展示如下:

序号 名称 功能
@Configuration类 若该配置被开启,指定具体配置的内容。
@Conditional*注解 决定是否开启该配置,将具体决定权委托出去了;Spring Boot已经提供了很多此类注解,如@ConditionalOnClass,如果不够用再考虑新增。
On*Condition类 接受@Conditional*注解的委托,指定如何决定的具体逻辑。
@ConfigurationProperties类 指定该自动配置中可配置的变量(即application.properties/application.yml中的变量)。

下面,我们将详细阐述四个类别的作用和原理。

注意

另外,除了上述的Java代码之外,还需要配在classpath目录下的META-INF/spring.factories文件中,存在如下配置, Spring Boot启动时将检查文件中是否存在如上配置,若不存在则不加载该自动配置。

 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
 org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

2.1 @Configuration类

  1. @Configuration类类中指定若启用该自动配置后,创建哪些Bean;同时添加@Conditional*和@EnableConfigurationProperties注解,用于限制条件和指定属性。
@Configuration
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class,
        MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {

    private final MultipartProperties multipartProperties;

    public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
        this.multipartProperties = multipartProperties;
    }

    @Bean
    @ConditionalOnMissingBean({ MultipartConfigElement.class,
            CommonsMultipartResolver.class })
    public MultipartConfigElement multipartConfigElement() {
        return this.multipartProperties.createMultipartConfig();
    }

    @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    @ConditionalOnMissingBean(MultipartResolver.class)
    public StandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
        return multipartResolver;
    }

}

2.2 @Conditional*注解

指定自动配置需要经过哪些条件的筛选,可以定义多个@Conditional*注解,必须同时满足这些条件,才允许开启自动配置。这类注解的作用,通常是收集参数,再讲决定权委托到@Conditional的value中,如OnClassConditionOnPropertyCondition等。

MultipartAutoConfiguration中使用的@ConditionalOnProperty如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {

    String[] value() default {};

    String prefix() default "";

    String[] name() default {};

    String havingValue() default "";

    boolean matchIfMissing() default false;
}

String Boot已经提供的@Conditional*注解注解如下:

@ConditionalOnClass : classpath中存在该类时起效
@ConditionalOnMissingClass : classpath中不存在该类时起效
@ConditionalOnBean : DI容器中存在该类型Bean时起效
@ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效
@ConditionalOnSingleCandidate : DI容器中该类型Bean只有一个或@Primary的只有一个时起效
@ConditionalOnExpression : SpEL表达式结果为true时
@ConditionalOnProperty : 参数设置或者值一致时起效
@ConditionalOnResource : 指定的文件存在时起效
@ConditionalOnJndi : 指定的JNDI存在时起效
@ConditionalOnJava : 指定的Java版本存在时起效
@ConditionalOnWebApplication : Web应用环境下起效
@ConditionalOnNotWebApplication : 非Web应用环境下起效

2.3 On*Condition类

@Conditional*注解是一对一的关系,接受@Conditional*注解的委托,同时获得传递过来的参数,Condition接口实现类根据参数返回一个boolean值,为true代表通过了该条件的校验。

OnPropertyCondition为例,其类图如下:
OnPropertyCondition

其中抽象类SpringBootCondition提供了日志上的优化,提供了合理的日志记录和一致的异常处理。

2.4 @ConfigurationProperties类

@ConfigurationProperties类是通过@EnableConfigurationProperties开启的,它声明了一些属性,这些属性都可以在application.properties/application.yml进行设置;同时,当前的自动配置也可以获得用户配置的属性值,根据不同的配置做出不同的动作。

MultipartProperties的代码片段如下:

@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties {

    private boolean enabled = true;

    private String location;

    private DataSize maxFileSize = DataSize.ofMegabytes(1);

  ...
}

3. 实战步骤

实现一个自定义自动配置,通常需要考虑这几个问题:

  1. 自动配置到底配置了什么,内容有哪些;
  2. 什么条件下开启自动配置;
  3. 设计支持定制的参数开放给用户;
  4. 确定多个自动配置之间的加载顺序;
  5. 如何使自定义的自动配置被Spring纳入管理范围;
  6. 用户如何确认系统是否开启了该自动配置。

下面我们将搭配示例代码,对每一个问题进行详细阐述;整个工程代码已经提交到码云,点击示例源码查看;

3.1 定义自动配置的内容--@Configuration类

定义自动配置的内容,即配置一个@Configuration类,在其内部声明想要创建的配置项;声明什么好呢,我们声明个girlfriend吧!

//省略包
@Configuration
public class GirlfriendAutoConfiguration {
    @Bean
    public GirlfriendAtHome girlfriendAtHome() {
        return new GirlfriendAtHome();
    }
}

如此,一个简单的配置类就创建完了,当然目前它还只是个配置,不是自动的。

3.2 设定开启自动配置的条件--@ConditionalOn*注解

@Configuration类增加@ConditionalOn*注解,设置条件。现在给GirlfriendAutoConfiguration增加条件@ConditionalOnClass@ConditionalOnMotherInLaw,这样创建GirlfriendAutoConfiguration就多出了两个条件。

  1. @ConditionalOnClass 是Spring Boot内置的注解,用来校验传入的类型在在classpath中是否存在,这里我们传入GirlfriendAtHome.class
  2. @ConditionalOnMotherInLaw 是我们自己创建的注解,用来校验岳母的信息,参数中传入心情为HAPPY;同时,为实现这个自定义条件,我们还创建了OnMotherInLawCondition类,该类继承自SpringBootCondition

此时代码是这样的:

//省略包
@Configuration
@ConditionalOnClass({GirlfriendAtHome.class})//必须在家
@ConditionalOnMotherInLaw(mood = MoodConst.HAPPY)//必须高兴才加载这个自动配置
public class GirlfriendAutoConfiguration {
    @Bean
    public GirlfriendAtHome girlfriendAtHome() {
        return new GirlfriendAtHome();
    }
}
//省略包
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnMotherInLawCondition.class})
public @interface ConditionalOnMotherInLaw {
    String mood();//岳母心情

}
//省略包
public class OnMotherInLawCondition extends SpringBootCondition {
    private static final String DEFAULT_MOOD = MoodConst.HAPPY;//假定一直很开心

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnMotherInLaw.class.getName());
        String mood = attributes.get("mood").toString();
        boolean match = DEFAULT_MOOD.equals(mood);
        String message = match ? "心情匹配" : "心情不匹配";
        return new ConditionOutcome(match, message);
    }

}

至此,这个普通的@Configuration类拥有了两个限制条件,只有在两个条件都成立时才会被创建。

3.3 设计支持定制的参数--@EnableConfigurationProperties

有时候,girlfriend的心情会发生变化,所以我们也要支持用户在application.properties/application.yaml中定制心情,@EnableConfigurationProperties注解专门用来完成配置项的创建。

GirlfriendAutoConfiguration增加注解@EnableConfigurationProperties,同时指定具体的配置实现类GirlfriendProperties,代码如下:

//省略包
@Configuration
@ConditionalOnClass({GirlfriendAtHome.class})//必须在家
@ConditionalOnMotherInLaw(mood = MoodConst.HAPPY)//必须高兴才加载这个自动配置
@EnableConfigurationProperties(GirlfriendProperties.class)
public class GirlfriendAutoConfiguration {
    private final GirlfriendProperties properties;
    @Autowired
    public GirlfriendAutoConfiguration(GirlfriendProperties properties) {
        this.properties = properties;
    }
    @Bean
    public GirlfriendAtHome girlfriendAtHome() {
        String mood = properties.getMood();
        mood = mood == null ? "未知" : mood;
        GirlfriendAtHome girlfriendAtHome = new GirlfriendAtHome();
        girlfriendAtHome.setMood(mood);
        girlfriendAtHome.setName("甄姬");
        girlfriendAtHome.setAge("18");
        return girlfriendAtHome;
    }
}
//省略包
@ConfigurationProperties("third.party.girlfriend")
public class GirlfriendProperties {
    private String mood;//心情
    public String getMood() {
        return mood;
    }
    public void setMood(String mood) {
        this.mood = mood;
    }
}

这样,我们就可以在application.properties/application.yaml中指定girlfriend的心情了:third.party.girlfriend.mood=happy

3.4 确定加载顺序

Spring Boot为开发者提供了几个注解,专门用于指定自动配置的先后顺序,这些注解使用时注解在@Configuration类上即可。

@AutoConfigureAfter:在指定的配置类初始化后再加载, 如@AutoConfigureAfter(JacksonAutoConfiguration.class)
@AutoConfigureBefore:在指定的配置类初始化前加载, 如@AutoConfigureBefore(JacksonAutoConfiguration.class)
@AutoConfigureOrder:数越小越先初始化, 如@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)

我们为GirlfriendAutoConfiguration增加@AutoConfigureOrder,使其优先级最高^_^。

//省略包
@Configuration
@ConditionalOnClass({GirlfriendAtHome.class})
@ConditionalOnMotherInLaw(mood = MoodConst.HAPPY)//必须高兴才加载这个自动配置
@EnableConfigurationProperties(GirlfriendProperties.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class GirlfriendAutoConfiguration {
  //省略
}

3.5 使自定义的自动配置被Spring纳入管理范围–META-INF/spring.factories

现在万事具备,只欠东风,Spring Boot在META-INF/spring.factories中设置有总开关,必须开启开行。增加如下配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.learn.girlfriend.spring.boot.autoconfigure.GirlfriendAutoConfiguration

3.6 用户如何确认系统是否开启了该自动配置–使用-Ddebug参数启动

启动服务时,增加VM参数-Ddebug,就能看到我们的girlfriend是否正常加载了:
auto-configuration-result-check

4. 小结

本文从这几个方面阐述如何自定义一个Auto-configuration: 原理、结构、实战步骤;在理解内部原理和结构之后,再实现了一个简单的自动配置。

原理:基于Spring Framework的扩展机制@ImportImportSelector@ConditionalCondition,实现自动配置功能。

结构:大体上由这几部分构成@Configuration类@Conditional*注解On*Condition类@ConfigurationProperties类

实战步骤中的各个操作,基本上就是创建上述结构中的各个部分内容,示例源码已提交码云,可下载查阅。

参考:
Spring Boot Reference Guide
demo project


文章作者: 沉迷思考的鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沉迷思考的鱼 !
评论
 上一篇
Java Logging Framework 现状 Java Logging Framework 现状
某日笔者心情不错,写代码时没有复制粘贴,打算手敲logger的相关代码,在IDE获得的提示是这样的: 尽管知道ch.qos.logback.classic.Logger在项目中是正确的选择,上图中Logger同名类的数量确实确实让笔者惊讶
下一篇 
Spring Boot Auto-configuration 自动配置详解 Spring Boot Auto-configuration 自动配置详解
毋庸置疑,Auto-configuration是Spring Boot的核心特性,其约定大于配置的思想,赋予了Spring Boot开箱即用的强大能力。本文从诞生背景、使用方式、实现原理这几个方面详细介绍这一特性。 1. 诞生背景一直以来
2018-12-05
  目录