Spring Boot 自动配置理解 以及实现自定义Starter



本文节选自《 JavaEE开发的颠覆者——Spring Boot实战 》一书。本书从Spring 基础、Spring MVC 基础讲起,从而无难度地引入Spring Boot 的学习。涵盖使用Spring Boot 进行Java EE 开发的绝大数应用场景,包含:Web 开发、数据访问、安全控制、批处理、异步消息、系统集成、开发与部署、应用监控、分布式系统开发等。

本文先通过分析Spring Boot 的运行原理后,根据已掌握的知识自定义一个starter pom。

我们知道Spring 4.x 提供了基于条件来配置Bean 的能力,其实Spring Boot的神奇的实现也是基于这一原理的。

本文内容是理解Spring Boot 运作原理的关键。我们可以借助这一特性来理解Spring Boot 运行自动配置的原理,并实现自己的自动配置。

Spring Boot 关于自动配置的源码在spring-boot-autoconfigure-1.3.0.x.jar 内,主要包含了如图1 所示的配置。

图1 包含的配置

若想知道Spring Boot 为我们做了哪些自动配置,可以查看这里的源码。

可以通过下面三种方式查看当前项目中已启用和未启用的自动配置的报告。

(1)运行jar 时增加–debug 参数:

java -jar xx.jar –debug

(2)在application.properties 中设置属性:

debug=true

(3)在STS 中设置,如图2 所示。

图2 在STS 中设置

此时启动,可在控制台输出。已启用的自动配置为:

未启用的自动配置为:

运作原理

关于Spring Boot 的运作原理,我们还是回归到@SpringBootApplication 注解上来,这个注解是一个组合注解,它的核心功能是由@EnableAutoConfiguration 注解提供的。

下面我们来看下@EnableAutoConfiguration 注解的源码:

@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ EnableAutoConfigurationImportSelector.class,
AutoConfigurationPackages.Registrar.class })
public @interface EnableAutoConfiguration {
Class[] exclude() default {};
String[] excludeName() default {};
}

这里的关键功能是@Import 注解导入的配置功能,EnableAutoConfigurationImportSelector 使用SpringFactoriesLoader.loadFactoryNames 方法来扫描具有META-INF/spring.factories 文件的jar包,而我们的spring-boot-autoconfigure-1.3.0.x.jar 里就有一个spring.factories 文件,此文件中声明了有哪些自动配置,如图3 所示。

图3 自动配置

核心注解

打开上面任意一个AutoConfiguration 文件, 一般都有下面的条件注解, 在

spring-boot-autoconfigure-1.3.0.x.jar 的org.springframwork.boot.autoconfigure.condition 包下,条件注解如下。

@ConditionalOnBean:当容器里有指定的Bean 的条件下。

@ConditionalOnClass:当类路径下有指定的类的条件下。

@ConditionalOnExpression:基于SpEL 表达式作为判断条件。

@ConditionalOnJava:基于JVM 版本作为判断条件。

@ConditionalOnJndi:在JNDI 存在的条件下查找指定的位置。

@ConditionalOnMissingBean:当容器里没有指定Bean 的情况下。

@ConditionalOnMissingClass:当类路径下没有指定的类的条件下。

@ConditionalOnNotWebApplication:当前项目不是Web 项目的条件下。

@ConditionalOnProperty:指定的属性是否有指定的值。

@ConditionalOnResource:类路径是否有指定的值。

@ConditionalOnSingleCandidate:当指定Bean 在容器中只有一个,或者虽然有多个但是指定首选的Bean。

@ConditionalOnWebApplication:当前项目是Web 项目的条件下。

这些注解都是组合了@Conditional 元注解,只是使用了不同的条件(Condition)。

下面我们简单分析一下@ConditionalOnWebApplication 注解。

package org.springframework.boot.autoconfigure.condition;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Conditional;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {
}

从源码可以看出,此注解使用的条件是OnWebApplicationCondition,下面我们看看这个条件是如何构造的:

package org.springframework.boot.autoconfigure.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StandardServletEnvironment;

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnWebApplicationCondition extends SpringBootCondition {

private static final String WEB_CONTEXT_CLASS =
“org.springframework.web.context.” + “support.GenericWebApplicationContext”;

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
boolean webApplicationRequired = metadata.isAnnotated(ConditionalOnWebApplication.class.getName());
ConditionOutcome webApplication = isWebApplication(context, metadata);
if (webApplicationRequired && !webApplication.isMatch()) {
return ConditionOutcome.noMatch(webApplication.getMessage());
}

   if (!webApplicationRequired && webApplication.isMatch()) {
       return ConditionOutcome.noMatch(webApplication.getMessage());
    }

  return ConditionOutcome.match(webApplication.getMessage());

}

private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadata) {

   if (!ClassUtils.isPresent(WEB_CONTEXT_CLASS, context.getClassLoader()))
   {
         return ConditionOutcome.noMatch("web application classes not found");
   }

  if (context.getBeanFactory() != null) {
         String[] scopes =

context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, “session”)) {
return ConditionOutcome.match(“found web application ‘session’
scope”);
}
}

if (context.getEnvironment() instanceof StandardServletEnvironment) {
        return ConditionOutcome.match("found web application StandardServletEnvironment");
 }

 if (context.getResourceLoader() instanceof WebApplicationContext) {
       return ConditionOutcome.match("found web application WebApplicationContext");
  }

 return ConditionOutcome.noMatch("not a web application");

}
}

从isWebApplication 方法可以看出,判断条件是:

  1. GenericWebApplicationContext 是否在类路径中;
  2. 容器里是否有名为session 的scope;
  3. 当前容器的Enviroment 是否为StandardServletEnvironment;
  4. 当前的ResourceLoader 是否为WebApplicationContext ( ResourceLoader 是ApplicationContext 的顶级接口之一);
  5. 我们需要构造ConditionOutcome 类的对象来帮助我们, 最终通过ConditionOutcome.isMatch 方法返回布尔值来确定条件。
  6. 实例分析

    在了解了Spring Boot 的运作原理和主要的条件注解后,现在来分析一个简单的Spring Boot 内置的自动配置功能:http 的编码配置。

    我们在常规项目中配置http 编码的时候是在web.xml 里配置一个filter,如:

     
    encodingFilter
    org.springframework.web.filter.CharacterEncodingFilter


    encoding
    UTF-8


    forceEncoding
    true

    自动配置要满足两个条件:

    1. 能配置CharacterEncodingFilter 这个Bean;
    2. 能配置encoding 和forceEncoding 这两个参数。
    3. 1.配置参数

      Spring Boot 的自动配置是基于类型安全的配置实现的,这里的配置类可以在application.properties 中直接设置,源码如下:

      @ConfigurationProperties(prefix = “spring.http.encoding”)//1 
      public class HttpEncodingProperties {

      public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");//2
      
      private Charset charset = DEFAULT_CHARSET; //2
      
      private boolean force = true; //3
      
      public Charset getCharset() {
         return this.charset;
      }
      public void setCharset(Charset charset) {
         this.charset = charset;
      }
      
      public boolean isForce() {
         return this.force;
      }
      
      public void setForce(boolean force) {
         this.force = force;
      }
      

      }

      代码解释

      ① 在application.properties 配置的时候前缀是spring.http.encoding;

      ② 默认编码方式为UTF-8,若修改可使用spring.http.encoding.charset=编码;

      ③ 设置forceEncoding,默认为true,若修改可使用spring.http.encoding.force=false。

      2.配置Bean

      通过调用上述配置,并根据条件配置CharacterEncodingFilter 的Bean,我们来看看源码:

      @Configuration 
      @EnableConfigurationProperties(HttpEncodingProperties.class) //1
      @ConditionalOnClass(CharacterEncodingFilter.class) //2
      @ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”,
      matchIfMissing = true) //3
      public class HttpEncodingAutoConfiguration {

      @Autowired
      private HttpEncodingProperties httpEncodingProperties; //3

      @Bean//4
      @ConditionalOnMissingBean(CharacterEncodingFilter.class) //5
      public CharacterEncodingFilter characterEncodingFilter() {
      CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
      filter.setEncoding(this.httpEncodingProperties.getCharset().name());
      filter.setForceEncoding(this.httpEncodingProperties.isForce());
      return filter;
      }
      }

      代码解释

      ① 开启属性注入,通过@EnableConfigurationProperties 声明,使用@Autowired 注入;

      ② 当CharacterEncodingFilter 在类路径的条件下;

      ③ 当设置spring.http.encoding=enabled 的情况下,如果没有设置则默认为true,即条件符合;

      ④ 像使用Java 配置的方式配置CharacterEncodingFilter 这个Bean;

      ⑤ 当容器中没有这个Bean 的时候新建Bean。

      实战

      看完前面几节的讲述,是不是觉得Spring Boot 的自动配置其实很简单,是不是跃跃欲试地想让自己的项目也具备这样的功能。其实我们完全可以仿照上面http 编码配置的例子自己写一个自动配置,不过这里再做的彻底点,我们自己写一个starter pom,这意味着我们不仅有自动配置的功能,而且具有更通用的耦合度更低的配置。

      为了方便理解,在这里举一个简单的实战例子,包含当某个类存在的时候,自动配置这个类的Bean,并可将Bean 的属性在application.properties 中配置。

      (1)新建starter 的Maven 项目,如图4 所示。

      图4 新建starter 的Maven 项目

      在pom.xml 中修改代码如下:

      
      xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” 
      xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0
      http://maven.apache.org/xsd/maven-4.0.0.xsd“>
      4.0.0

      <groupId>com.wisely</groupId>
      <artifactId>spring-boot-starter-hello</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <packaging>jar</packaging>
      
      <name>spring-boot-starter-hello</name>
      <url>http://maven.apache.org</url>
      
      <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
      
      <dependencies>
        <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-autoconfigure</artifactId>
           <version>1.3.0.M1</version>
        </dependency>
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>3.8.1</version>
         <scope>test</scope>
       </dependency>
      </dependencies>
      




      spring-snapshots
      Spring Snapshots
      https://repo.spring.io/snapshot
      true



      spring-milestones
      Spring Milestones
      https://repo.spring.io/milestone
      false





      spring-snapshots
      Spring Snapshots
      https://repo.spring.io/snapshot
      true



      spring-milestones
      Spring Milestones
      https://repo.spring.io/milestone
      false



      代码解释

      在此处增加Spring Boot 自身的自动配置作为依赖。

      (2)属性配置,代码如下:

      package com.wisely.spring_boot_starter_hello;

      import org.springframework.boot.context.properties.ConfigurationProperties;

      @ConfigurationProperties(prefix=”hello”)
      public class HelloServiceProperties {

       private static final String MSG = "world";
      
       private String msg = MSG;
      
       public String getMsg() {
          return msg;
       }
      
       public void setMsg(String msg) {
          this.msg = msg;
       }
      

      }

      代码解释

      这里配置是类型安全的属性获取。在application.properties 中通过hello.msg= 来设置,若不设置,默认为hello.msg=world。

      (3)判断依据类,代码如下:

      package com.wisely.spring_boot_starter_hello;

      public class HelloService {

      private String msg;
      public String sayHello(){
      return “Hello” + msg;
      }

      public String getMsg() {
         return msg;
      }
      
      public void setMsg(String msg) {
         this.msg = msg;
      }
      

      }

      代码解释

      本例根据此类的存在与否来创建这个类的Bean,这个类可以是第三方类库的类。

      (4)自动配置类,代码如下:

      package com.wisely.spring_boot_starter_hello;

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
      import
      org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
      import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
      import
      org.springframework.boot.context.properties.EnableConfigurationProperties;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;

      @Configuration
      @EnableConfigurationProperties(HelloServiceProperties.class)
      @ConditionalOnClass(HelloService.class)
      @ConditionalOnProperty(prefix = “hello”, value = “enabled”, matchIfMissing = true)
      public class HelloServiceAutoConfiguration {

      @Autowired
      private HelloServiceProperties helloServiceProperties;
      
      @Bean
      @ConditionalOnMissingBean(HelloService.class)
      public HelloService helloService(){
          HelloService helloService = new HelloService();
          helloService.setMsg(helloServiceProperties.getMsg());
          return helloService;
      }
      

      }

      代码解释

      根据HelloServiceProperties 提供的参数,并通过@ConditionalOnClass 判断HelloService 这个类在类路径中是否存在,且当容器中没有这个Bean 的情况下自动配置这个Bean。

      (5)注册配置。若想自动配置生效,需要注册自动配置类。在src/main/resources 下新建META-INF/spring.factories,结构如图5所示。

      在spring.factories 中填写如下内容注册:

      org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 
      com.wisely.spring_boot_starter_hello.HelloServiceAutoConfiguration

      若有多个自动配置,则用“,”隔开,此处“\”是为了换行后仍然能读到属性。

      另外,若在此例新建的项目中无src/main/resources 文件夹,需执行如图6所示操作。

      图6 调出src/maln/resources 文件夹

      (5)使用starter。新建Spring Boot 项目,并将我们的starter 作为依赖,如图7 所示。

      图7 新建Spring Boot 项目

      在pom.xml 中添加spring-boot-starter-hello 的依赖,代码如下:

       
      com.wisely
      spring-boot-starter-hello
      0.0.1-SNAPSHOT

      我们可以在Maven 的依赖里查看spring-boot-starter-hello,如图8所示。

      图8 查看spring-Doot-starter-hello

      在开发阶段,我们引入的依赖是spring-boot-starter-hello 这个项目。在starter 稳定之后,

      我们可以将spring-boot-starter-hello 通过“mvn install”安装到本地库,或者将这个jar 包发布到Maven 私服上。

      简单的运行类代码如下:

      package com.wisely.ch6_5;

      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.SpringApplication;
      import org.springframework.boot.autoconfigure.SpringBootApplication;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RestController;

      import com.wisely.spring_boot_starter_hello.HelloService;
      @RestController
      @SpringBootApplication
      public class Ch65Application {

      @Autowired
      HelloService helloService;
      
      @RequestMapping("/")
      public String index(){
         return helloService.sayHello();
      }
      
      public static void main(String[] args) {
         SpringApplication.run(Ch65Application.class, args);
      }
      

      }

      在代码中可以直接注入HelloService 的Bean,但在项目中我们并没有配置这个Bean,这是通过自动配置完成的。

      访问 http://localhost:8080 ,效果如图9 所示。

      图9 访问http://local host:8080

      这时在application.properties 中配置msg 的内容:

      hello.msg= wangyunfei

      此时再次访问 http://localhost:8080 ,效果如图10 所示。

      图10 查看效果

      在application.properties 中添加debug 属性,查看自动配置报告:

      debug=true

      我们新增的自动配置显示在控制台的报告中,如图11所示。

      图11 控制台报告

      你可能感兴趣的:(spring,spring)