一、SpringBoot自动装配
- 使用SpringBoot创建项目可以减少很多xml文件的配置工作,甚至不需要写一行xml,就可以直接将整个项目启动,这种“零配置”的做法减轻了开发人员很多的工作量,可以让开发人员专注于业务逻辑的实现。
- SpringBoot采用JavaConfig的配置风格,导入组件的方式也由原来的直接配置改为@EnableXXX,这种纯Java代码的配置和导入组件的方式,使代码看上去更加优雅。
- SpringBoot的自动装配基于其内部的两种设计策略:开箱即用和约定优先于配置(或者叫约定大于配置)。
二、开箱即用
- 概念
在开发过程中,通过maven项目的pom文件添加相关依赖包,然后通过相应的注解来代替繁琐的XML配置,来管理对象的生命周期。
例如常用注解@Bean、@Configuration和@Autowired等。
- 原理
(1)从pom.xml开始 SpringBoot的项目都会存在一个父依赖,按住Ctrl+鼠标左键,可以点进去。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
点进去之后发现里面除了一些插件和配置文件的格式外,还存在一个依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.3</version>
</parent>
这个文件中配置了大量的依赖和相应的版本号,局部信息如下所示。
<properties>
<activemq.version>5.16.2</activemq.version>
<antlr2.version>2.7.7</antlr2.version>
<appengine-sdk.version>1.9.90</appengine-sdk.version>
<artemis.version>2.17.0</artemis.version>
<aspectj.version>1.9.7</aspectj.version>
<assertj.version>3.19.0</assertj.version>
<atomikos.version>4.0.6</atomikos.version>
<awaitility.version>4.0.3</awaitility.version>
<build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
...
</properties>
结论1: spring-boot-dependencies作为父工程,存放了SpringBoot的核心依赖,我们在写或者引入一些依赖的时候,不需要指定版本,正是因为SpringBoot的父依赖已经帮我们维护了一套版本。
在父依赖中也帮我们写好了资源库,不用我们自己再去配置(代码中配置了可以读取的配置文件):
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/application*.properties</exclude>
</excludes>
</resource>
</resources>
(2)启动类 启动器是SpringBoot的启动场景,比如我们要使用web相关的,那么就直接引入spring-boot-starter-web,就会帮我们自动导入web环境下所有的必须的依赖。
spring-boot-starter启动类:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.3</version>
</dependency>
该启动器中包含的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>1.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.1.RELEASE</version>
<scope>compile</scope>
</dependency>
...
其中存放了自动配置相关的依赖、日志相关依赖、还有spring-core等依赖,这些依赖只需要导入一个spring-boot-starter就可以直接将其全部引入,而不需要像以前那样逐个导入。spring-boot-starter-web同样也包含多种依赖(web所需)。SpringBoot会将所有的功能场景都封装成一个一个的启动器,供开发人员使用。不同的启动器包含着不相同的依赖项。
(3)主程序(重要) SpringBoot项目中,有一个主程序,即程序启动入口,这个程序的最大特点就是其类上放了一个@SpringBootApplication注解,这也正是SpringBoot项目启动的核心。
@SpringBootApplication
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HellospringbootApplication.class, args);
}
}
点击@SpringBootApplication注解,可以知道它是一个组合注解,其中比较重要的是@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。
@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 {...}
- @SpringBootConfiguration:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
可以认为@SpringBootConfiguration其实就携带了一个@Configuration注解,代表自己是一个Spring的配置类;
- @EnableAutoConfiguration:
@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})比较重要,它帮我们导入了AutoConfigurationSelector,这个类中存在一个方法可以帮助我们获取所有的配置:
/*
所有的配置都放在configurations中,而这些配置都从getCandiateConfiguration中获取,
这个方法是用来获取候选的配置
*/
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
getCandiateConfigurations():
// 获取候选的配置
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;
}
Q:这个方法可以用来获取所有候选的配置,那么这些候选的配置又是从哪里得到的? A:实际上它返回了一个List,这个List是由loadFactoryNames()方法返回的,其中传入了一个getSpringFactoriesLoaderFactoryClass(),其主要返回了EnableAutoConfiguration:
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
也就是说,它实际返回的就是标注了这个类的所有包,标注了这个类的包就是@SpringBootApplication 另外,在断言中, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."可以确定所有的自动配置类都是定义在spring.factories文件中,从而我,们定位到spring-boot-autoconfigure包下的META-INF/spring.factories文件,其中,auto configure的部分内容如下:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
...
以WebMvcAutoConfiguration为例:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "";
public static final String DEFAULT_SUFFIX = "";
private static final String SERVLET_LOCATION = "/";
public WebMvcAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.hiddenmethod.filter",
name = {"enabled"}
)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean({FormContentFilter.class})
@ConditionalOnProperty(
prefix = "spring.mvc.formcontent.filter",
name = {"enabled"},
matchIfMissing = true
)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}
...
}
这里面放了所有关于WebMvc的配置,如视图解析器、国际化等。
这个注解的功能其实就是自动扫描并加载符合条件的组件或bean定义,最终将这些bean定义加载到容器中,详见:https://www.jianshu.com/p/a00d2b43e160
结论2: 当SpringBoot项目启动的时候,会先导入AutoConfigurationImportSelector,这个类会帮我们选择所有的候选配置,我们需要导入的配置都是SpringBoot帮我们写好的一个一个的配置类,这些配置类的位置,存在于META-INF/spring.factories文件中,通过这个文件,Spring可以找到这些配置类的位置,然后去架加载其中的配置。
项目每次启动时并不是把spring.factories文件中所有的配置类都加载,上面WebMvcAutoConfiguration中@ConditionalOnXXX注解,只有其中的所有条件都满足,该类才会生效,才会被加载。
因此,在pom.xml文件中加入starter启动器, SpringBoot自动进行配置,完成开箱即用。
在这里贴一个我认为的比较容易理解的过程:
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
- 以前我们需要自己配置的东西 , 自动配置类都帮我们解决了
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 它将所有需要导入的组件以全类名的方式返回 , 这些组件就会被添加到容器中 ;
- 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;
- 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
三、约定大于配置
- 概念
我们的配置文件(.yml)应该放在哪个目录下,配置文件的命名规范,项目启动时扫描的Bean,组件的默认配置是什么样的等等一系列的东西,都可以被称为约定。 - 举例
- maven目录结构的约定
file:./config/、file:./、classpath:/config/、classpath:/,约定spring的配置文件目录可以放在/config、/根目录/resource/config/、resource/。 - SpringBoot默认文件的约定
SpringBoot默认可以加载application.yml、application.yaml、application.properties三种配置文件,推荐使用前面两种。 - 项目启动时扫描包范围的约定
SpringBoot的注解扫描的默认规则是SpringBoot的入口类所在包及其子包。
四、SpringBoot自动配置类如何读取yml配置
- yml配置文件中可以配置哪些东西
SpringBoot总是将所有的配置都用JavaConfig的形式去呈现出来,这样能够使代码更加优雅。 那么yml中配置的东西,必然是要和这种配置模式去进行联系的,我们在application.yml中配置的东西,通常是一些存在与自动配置类中的属性,那么这些自动配置类,在启动的时候是怎么找到的呢? 如果你还记得上文的描述,那么你可以很明确地知道:spring.factories!没错,就是它,所以这个问题我们似乎得到了答案——只要存在与spring.factories中的,我们都可以在application.yml中进行配置。 当然,这并不意味着不存在其中的我们就不能配置,这些配置类我们是可以进行自定义的,只要我们写了配置类,我们就可以在yml中配置我们需要的属性值,然后在配置类中直接读取这个配置文件,将其映射到配置类的属性上。那么就牵扯出我们的问题了:配置类是如何去读取yml配置文件中的信息的呢?
核心注解@ConfigurationProperties: 可以将yml文件中写好的值注入到类的属性中。 使用样例: (1)application.yml文件
object:
name: Object
blogurl: blog.objectspace.cn
(2)配置类
@Component
@ConfigurationProperties(prefix = "object")
public class TestConfig {
private String name;
private String blogUrl;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBlogUrl() {
return blogUrl;
}
public void setBlogUrl(String blogUrl) {
this.blogUrl = blogUrl;
}
}
(3)测试类
@SpringBootTest
class SpringbootdemoApplicationTests {
@Autowired
TestConfig testConfig;
@Test
void contextLoads() {
System.out.println(testConfig.getName());
System.out.println(testConfig.getBlogUrl());
}
}
SpringBoot自动配置类—WebMvcAutoConfigurationAdapter:
@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final Resources resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
private final ObjectProvider<DispatcherServletPath> dispatcherServletPath;
private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations;
private final WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private ServletContext servletContext;
public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, ObjectProvider<DispatcherServletPath> dispatcherServletPath, ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = (Resources)(resourceProperties.hasBeenCustomized() ? resourceProperties : webProperties.getResources());
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
}
引入了WebMvcProperties.class, ResourceProperties.class, WebProperties.class三个Properties类(旧版本的是两个),并在构造函数中将这两个Properties类注入到类属性中,点击其中一个,查看其内容: WebMvcProperties:
@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {
private Locale locale;
...
}
其中,prefix="spring.mvc"就是application.yml文件中的(date-format是一个需要配置的属性):
spring:
mvc:
date-format
在yml中配置的属性,就可以通过@ConfigurationProperties映射到类中属性,然后再通过自动配置类,将这些属性配置到配置类中。
参考:SpringBoot:认认真真梳理以便自动装配原理、SpringBoot自动装配原理
|