SpringBoot自动配置的原理及自定义

523 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

注:不同版本的SpringBoot可能有差异,这里是以SpringBoot-2.5.6版本为基础讲的,2.7版本的自动配置文件已迁移到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中。

注解原理定位

每一个SpringBoot应用的启动入口都会有个@SpringBootApplication的注解,而SpringBoot完成自动配置的原理就在该注解中。

@SpringBootApplication注解中进入可以看到@EnableAutoConfiguration 的注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

再从里面看,发现该注解引入了AutoConfigurationImportSelector 这个叫做自动配置选择器的类。

@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 {};

}

AutoConfigurationImportSelector类中有个读取META-INF/spring.factories文件的方法。

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	 // 加载META-INF/spring.factories中的配置
		List<String> configurations = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())); 
		ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
		Assert.notEmpty(configurations,
				"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
						+ "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}

在spring-boot-autoconfigure-x.jar包下META-INF中也有个spring.factories文件,而前面的选择器就是读取这个位置的。里面配置了多个springboot自动配置类。 XcXzsx.png

我们以 DispatcherServletAutoConfiguration这个前端控制器的配置为例。

XcjMTS.png

在源码中可以发现,DispatcherServlet需要在满足Conditional的条件下才会创建该Bean,同时DispatcherServlet依赖于WebMvcProperties这个属性对象。

WebMvcProperties类中可以看出它的属性是由前缀spring.mvc去配置的。也即我们可以在SpringBoot的配置文件 application.properties 中用这个前缀去配置SpringMvc的属性。

Xcj661.png

如 spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss 配置日期格式。

从前面可以看出,如果要实现SpringBoot的自动配置,那么需要以下几个条件:

  • 属性配置文件(如WebMvcProperties)
  • 服务;(如DispatcherServlet)
  • 服务的自动配置类;(如DispatcherServletAutoConfiguration)
  • 有着META-INF/spring.factories文件,同时在里面有着配置指向该服务的自动配置类;

自定义自动配置的服务

知道自动配置的原理后,可以尝试自定义自动配置的服务。这里的场景是定义输出字符串的服务

0. 准备工作

新建maven项目,引入SpringBoot自动配置的依赖

<groupId>org.example</groupId>
  <artifactId>spring-boot-starter-hello</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>SpringBootStartHello</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>

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

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

1. 配置属性文件

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

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

    private static final String DEFAULT_MSG = "world";

    private String msg = DEFAULT_MSG;

    public String getMsg() {
        return msg;
    }

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

2. 设计服务

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;
    }
}

3. 自动配置类

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;
    }
}

4. 新建META-INF/spring.factories

配置指向该服务的自动配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.example.HelloServiceAutoConfiguration

5. 打包到本地仓库

idea 执行 Maven的install生命周期。

XgSZ4S.png

6. 新建一个新的SpringBoot项目

  • pom中引入刚打包的依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 本地仓库下的包 -->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spring-boot-starter-hello</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
  • 配置启动类,引入服务
@RestController
@SpringBootApplication
public class SpringbootDemoApplication {

    @Autowired
    private HelloService helloService;

    @RequestMapping("/")
    public String index() {
        return helloService.sayHello();
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDemoApplication.class, args);
    }

}
  • 启动项目,访问路径 在spring.factories中加上 debug=true 可以看到哪些自动配置类被加载了,哪些没有。

XcxSUO.png

可以发现自定义的自动配置类被加载,同时HelloService也被自动注入。

访问路径,可以得到Helloworld,说明HelloService被自动注入了,不需要额外去声明注入。且因为没在配置文件中设置属性,这里显示的是默认msg。 XcxKPg.png

在spring.factories中修改属性,hello.msg=zhangsan。重新启动项目,发现请求结果变了,说明属性可以被正确修改读取。 Xcxma8.png

最后

可以发现使用自动配置后,我们不需要显示的去声明HelloService这个服务,只需要直接引入即可。如果是普通的Spring项目,那么当我们需要引入外部服务的时候,一般都要在xml文件中定义 或者 在@Configuration标注下的类中返回对象到@Bean注解的方法中。

例如SpringMvc中的视图解析器需要这样配置。

 <bean class="org.springframework.web.servlet.view.UrlBasedViewResolver">
        <property name="prefix" value="/WEB-INF/page/"/>
        <property name="suffix" value=".jsp"/>
        <property name="viewClass" value="org.springframework.web.servlet.view.InternalResourceView"/>
    </bean>

又或者使用Java配置

@EnableWebMvc
@Configuration
@ComponentScan(value = "com.example.controller")
public class WebMvcConfig implements WebMvcConfigurer {
    /**
     * 视图解析器的bean配置
     * @return
     */
    @Bean
    public ViewResolver viewResolver() {
        System.out.println("viewResolver init");
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        /**
         * 如果需要在jsp中使用jstl标签的话,需要加上这个视图,且要用这个引用,否则会报找不到方法的500错误
         * 项目中使用JSTL,SpringMVC会把视图由InternalView转换为JstlView。
         * 若使用Jstl的fmt标签,需要在SpringMVC的配置文件中配置国际化资源文件。
         * 需要引入 jstl.jar和standard.jar
         */
//        resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
        resolver.setPrefix("/WEB-INF/page/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }
}    

通过查看spring-boot-autoconfigure-2.5.6.jar包下META-INF/spring.factories文件,可以知道SpringBoot可以帮我们自动配置的服务,这样就省去较多的配置工作了。同时,当针对服务的属性有调整时,通过 application.properties 配置文件就可以实现对服务参数的调整。可以说方便太多