SpringBoot框架---自动配置原理的解密

版权声明: https://blog.csdn.net/UtopiaOfArtoria/article/details/82149606

SpringBoot框架—自动配置原理的解密

什么是SpringBoot的自动配置?

SpringBoot官网对Spring Boot的定义如下:

Spring Boot makes it easy to create stand-alone, production-grade Spring-based Applications that you can run. We take an opinionated view of the Spring platform and third-party libraries, so that you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

用一句话来概括这段话的核心思想,就是“约定优于配置”。也就是说,我们在开发基于Spring的应用时,SpringBoot框架会帮我们做很多默认的配置,这样大大地减少了我们的工作量。以extensible项目(SpringBoot版本:2.0.4.RELEASE)为例,现在我们来看看SpringBoot框架是如何进行自动配置的。

SpringBoot自动配置相关源码解读

所有SpringBoot项目主函数入口的类上都有一个组合注解@SpringBootApplication,而这个注解类上有一个注解@EnableAutoConfiguration。望文知意,这个注解就是用来实现自动配置的,其源码如下:

package org.springframework.boot.autoconfigure;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

这个类真正起作用的是 @Import({AutoConfigurationImportSelector.class}) AutoConfigurationImportSelector这个类会导入所有需要自动配置的类,其关键代码如下:

package org.springframework.boot.autoconfigure;

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

    // other codes...

    /**
      * 对SpringBoot默认的自动配置类列表进行处理,筛选出需要SpringBoot进行自动配置类的列表
      */
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
        }

    /**
      * 判断是否需要进行自动配置
      */
    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass() == AutoConfigurationImportSelector.class ? (Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true) : true;
    }

    /**
      * 加载SpringBoot默认的自动配置类列表
      */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
    }

    // other codes...
}

selectImports()方法的作用:筛选出需要SpringBoot自动配置的配置类列表,并进行相应的配置。

isEnalbed()方法的作用:加载porperties配置文件,判断该项目是否开启了自动配置功能,默认就是开启状态。
getCandidateConfigurations()方法的作用: 加载META-INF/spring.factories中的信息,获取SpringBoot默认的自动配置类列表。

自动配置项列表

web项目发送http请求可能会出现乱码,以前为了解决这个问题,我们是在web.xml添加一个filter,统一强制设置http的编码为utf-8。如下:

<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

接下来看SpringBoot是如何帮我们自动配置Http编码的。刚刚的spring.factories里面有内容如下:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
# other codes...
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
# other codes...

HttpEncodingAutoConfiguration配置类的关键代码如下:

package org.springframework.boot.autoconfigure.web.servlet;

@Configuration
@EnableConfigurationProperties({HttpEncodingProperties.class})
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final HttpEncodingProperties properties;

    public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
        this.properties = properties;
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpEncodingProperties.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpEncodingProperties.Type.RESPONSE));
        return filter;
    }

    // other codes...
}

这个类的作用是:如果在properties配置文件中没有设置对应的属性(spring.http.encoding)的值时,SpringBoot会创建一个过滤器CharacterEncodingFilter,并会强制设置其编码—filter.setEncoding(this.properties.getCharset().name());
而HttpEncodingProperties的关键源码如下:

package org.springframework.boot.autoconfigure.http;

@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties {
    public static final Charset DEFAULT_CHARSET;
    private Charset charset;

    public HttpEncodingProperties() {
        this.charset = DEFAULT_CHARSET;
    }

    static {
        DEFAULT_CHARSET = StandardCharsets.UTF_8;
    }

    // other codes...
}

这里是@EnableConfigurationProperties/@ConditionalOn***/@ConfigurationProperties,这三个注解协同作用,实现自动配置—给属性赋默认值。

其他注解的功能很好理解,就是@ConditionOn***注解不好理解,而且种类繁多。

以@ConditionalOnClass为例,进行源码解读:

package org.springframework.boot.autoconfigure.condition;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({OnClassCondition.class})
public @interface ConditionalOnClass {
    Class<?>[] value() default {};
    String[] name() default {};
}

@Conditional相关的代码如下:

package org.springframework.context.annotation;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}
package org.springframework.context.annotation;

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

说明:

1. @ConditionalOnClass注解的类上面有注解@Conditional({OnClassCondition.class})
2. OnClassCondition.class必定是Condition.class的子类,而且应该有matches()方法的实现—这里有个地方需要额外注意,matches()方法的实现并不是发生在OnClassCondition里面,而是发生在抽象类SpringBootCondition里面。
3. 类似的,其他的On***Condition.class都是Condition.class的子类,都继承了SpringBootCondition。

SpringBootCondition的关键代码如下:

package org.springframework.boot.autoconfigure.condition;

public abstract class SpringBootCondition implements Condition {
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String classOrMethodName = getClassOrMethodName(metadata);

        try {
            ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
            this.logOutcome(classOrMethodName, outcome);
            this.recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();
        } catch (NoClassDefFoundError var5) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
        } catch (RuntimeException var6) {
            throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
        }
    }

    public abstract ConditionOutcome getMatchOutcome(ConditionContext var1, AnnotatedTypeMetadata var2);

    // other codes...
}

SpringBootCondition.matches()方法是用final修饰的,所以不可被子类重写。
其他的代码都是记录作用,与实际功能关系不大。

这里最关键的一句代码是:ConditionOutcome outcome = this.getMatchOutcome(context, metadata); 根据JAVA语言多态的特性,需要知道On***Condition.class具体作用的话,就去查看其getMatchOutcome()方法是如何实现的!!!Spring提供的辅助自动配置用的注解有如下:
辅助自动配置注解
汇总这些注解(对应的类)的作用如下:

注解 功能
@ConditionalOnBean 当SpringIoc容器内存在指定Bean的条件,才会实例化Bean
@ConditionalOnClass 当SpringIoc容器内存在指定Class的条件,才会实例化Bean
@ConditionalOnCloudPlatform 云平台与给定参数一致时,才会实例化Bean
@ConditionalOnExpression 当表达式为true的时,才会实例化Bean
@ConditionalOnJava 当前java版本在给定参数的范围内时,才会实例化Bean
@ConditionalOnJndi 在JNDI存在时,才会实例化Bean
@ConditionalOnMissingBean 在当前上下文中不存在某个对象时,才会实例化Bean
@ConditionalOnMissingClass 某个class类路径上不存在的时候,才会实例化Bean
@ConditionalOnNotWebApplication 当前项目不是Web项目,才会实例化Bean
@ConditionalOnProperty 指定的属性有指定的值,才会实例化Bean
@ConditionalOnResource 类路径有指定的值,才会实例化Bean
@ConditionalOnSingleCandidate 当指定Bean在SpringIoc容器内只有一个,或者虽然有多个但是是指定首选的Bean,才会实例化Bean
@ConditionalOnWebApplication 当前项目是Web项目,才会实例化Bean

实战—实现一个能够具备自动配置功能的模块

新建一个maven项目robot如下:

项目的maven坐标命名如下

在该项目中导入自动配置必要的依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>

导入成功后的效果图如下:

导入成功结果

定义配置类RobotProperties

package com.netopstec.robot;
/**
 * robot项目的配置
 * @author zhenye 2018/8/27
 */
@ConfigurationProperties(prefix = "robot")
public class RobotProperties {

    private static final String DEFAULT_NAME = "Big White";
    private static final String DEFAULT_AGE = "7";

    /**
     * 如果不配置(robot.name/age)的话,就是默认值。
     */
    private String name = DEFAULT_NAME;
    private String age = DEFAULT_AGE;

    // ...getters and setters
}

定义业务类RobotService

package com.netopstec.robot;
/**
 * 提供给外部系统的业务类
 * @author zhenye 2018/8/27
 */
public class RobotService {

    private String name;
    private String age;

    /**
     * 提供给外部系统的方法
     * @return
     */
    public String selfIntroduction(){
        return "If you don't set something in properties, then robot's name is " + name + " and robot's age is " + age + ".";
    }

    // ...getters and setters
}

定义自动配置的实现类RobotConfiguration

/**
 * 注解@EnableConfigurationProperties指定自动配置的类
 * 注解@ConditionalOnProperty开启自动配置的实现
 * @author zhenye 2018/8/27
 */
@Configuration
@EnableConfigurationProperties(RobotProperties.class)
@ConditionalOnClass(RobotService.class)
@ConditionalOnProperty(prefix = "robot",value = "enabled",matchIfMissing = true)
public class RobotConfiguration {

    @Autowired
    private RobotProperties robotProperties;

    @Bean
    @ConditionalOnMissingBean(RobotService.class)
    public RobotService robotService(){
        RobotService robotService = new RobotService();
        robotService.setName(robotProperties.getName());
        robotService.setAge(robotProperties.getAge());
        return robotService;
    }
}

在resources下面新建META-INF文件夹和spring.factories(加载自动配置类),并编辑内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.netopstec.robot.RobotConfiguration

具体的robot项目结构图如下:

robot项目结构图

在命令行工具中运行命令”mvn install”,将robot项目打包发布到maven的本地仓库中。其打包发布的日志如下(BUILD SUCCESS说明打包发布成功,pom文件中红色框标注的是该项目的maven坐标):

maven打包

要想使用这个robot项目的自动配置功能,只需要在pom文件中加入robot项目的maven坐标,然后通过注解@Autowired导入RobotService就行。

robot项目的maven坐标:

robot项目的maven坐标

测试类的代码如下:

package com.netopstec.extensible.controller;

/**
 * @author zhenye 2018/8/27
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    private RobotService robotService;

    @RequestMapping("/robot")
    public String test(){
        return robotService.selfIntroduction();
    }
}

启动项目,在Postman中测试,发现导入了自动配置的内容。

postman测试效果1

自定义robot属性值如下:

自定义robot属性值

重新启动项目,再次在Postman中测试,效果图如下:

postman测试效果2

猜你喜欢

转载自blog.csdn.net/UtopiaOfArtoria/article/details/82149606
今日推荐