springboot 运行原理:
springboot 的神奇是基于 spring4.x 条件配置 来实现的。我们可以借助这一特性来理解 springboot 运行自动配置的原理,并实现自己的自动配置。
springboot 关于自动配置的源码在 spring-boot-autoconfigure-.*.jar 内,主要包含了如下:
可通过如下三种方式查看当前项目中已启用和未启用的自动配置报告:
①:运行 jar 时增加--debug 参数:
java -jar xx.jar --debug
②:在 application.properties 中设置属性:
debug=true
③:在 STS 中设置:
此时启动可在控制台输出已启用的自动配置:
已启用:
未启用:
运作原理:
关于 springboot 的运作原理,我们还是回归到 @SpringBootApplication 注解上来,这个注解是一个组合注解,它的核心功能是由@EnableAutoConfiguration 注解提供的。
下面我们来看 @EnableAutoConfiguration 注解的源码:
这里的关键功能是 @Import 注解导入的功能配置,EnableAutoConfigurationImportSelector 使用 SpringFactoriesLoaderFactoryNames 方法来扫描 META-INF/spring.factories 文件的 jar 包,而我们的 spring-boot-autofigure-*.jar 里就有一个 spring.factories 文件,此文件就声明了有哪些自动配置:
核心注解:
打开上面任意一个 AutoConfiguration 文件,一般都有下面的条件注解,在 spring-boot-autoconfigure-*.jar 的 org.springframework.boot.autoconfigure.conditon 包下,条件注解如下:
@ConditionalOnBean: 当容器里有指定的 Beab 的条件下。
@ConditionalOnClass: 当类路径下有指定的条件下。
@ConditionalOnExpression: 基于 SpEL 表达式作为判断条件。
@ConditionalOnjava: 基于 JVM 版本作为判断条件。
@ConditionalOnJndi: 在 JNDI 存在的条件下查找指定的位置。
@ConditionalOnMissingBean: 当容器中没有指定的 bean 的情况下。
@ConditonalOnMissingClass: 当类路径中没有指定的类的条件下。
@ConditionalOnNotWebApplication: 当项目下不是 web 项目的条件下。
@ConditionalOnProperty: 指定的属性是否有指定的值。
@ConditionalOnResource: 类路径是否有指定额值。
@ConditionalOnSingleCandidate: 当指定 bean 在容器中只有一个,或者虽然有多个但是指定是首选的 bean。
@ConditionalOnWebApplication: 当前项目是 web 项目的条件下。
这些注解都是组合了 @Conditonal 元注解,只是使用了不同的条件(Condition)。
@ConditionalOnWebApplication 注解:
从源码可以看出使用的条件是 OnWebApplicationCondition,下面我们看看这个条件是如何构造的:
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.condition;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.web.reactive.context.ConfigurableReactiveWebEnvironment;
import org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext;
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.ConfigurableWebEnvironment;
import org.springframework.web.context.WebApplicationContext;
/**
* {@link Condition} that checks for the presence or absence of
* {@link WebApplicationContext}.
*
* @author Dave Syer
* @see ConditionalOnWebApplication
* @see ConditionalOnNotWebApplication
*/
@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 required = metadata
.isAnnotated(ConditionalOnWebApplication.class.getName());
ConditionOutcome outcome = isWebApplication(context, metadata, required);
if (required && !outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
if (!required && outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
return ConditionOutcome.match(outcome.getConditionMessage());
}
private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadata, boolean required) {
ConditionMessage.Builder message = ConditionMessage.forCondition(
ConditionalOnWebApplication.class, required ? "(required)" : "");
Type type = deduceType(metadata);
if (Type.SERVLET == type) {
return isServletWebApplication(context);
}
else if (Type.REACTIVE == type) {
return isReactiveWebApplication(context);
}
else {
ConditionOutcome servletOutcome = isServletWebApplication(context);
if (servletOutcome.isMatch() && required) {
return new ConditionOutcome(servletOutcome.isMatch(),
message.because(servletOutcome.getMessage()));
}
ConditionOutcome reactiveOutcome = isReactiveWebApplication(context);
if (reactiveOutcome.isMatch() && required) {
return new ConditionOutcome(reactiveOutcome.isMatch(),
message.because(reactiveOutcome.getMessage()));
}
boolean finalOutcome = (required
? servletOutcome.isMatch() && reactiveOutcome.isMatch()
: servletOutcome.isMatch() || reactiveOutcome.isMatch());
return new ConditionOutcome(finalOutcome,
message.because(servletOutcome.getMessage()).append("and")
.append(reactiveOutcome.getMessage()));
}
}
private ConditionOutcome isServletWebApplication(ConditionContext context) {
ConditionMessage.Builder message = ConditionMessage.forCondition("");
if (!ClassUtils.isPresent(WEB_CONTEXT_CLASS, context.getClassLoader())) {
return ConditionOutcome
.noMatch(message.didNotFind("web application classes").atAll());
}
if (context.getBeanFactory() != null) {
String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, "session")) {
return ConditionOutcome.match(message.foundExactly("'session' scope"));
}
}
if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
return ConditionOutcome
.match(message.foundExactly("ConfigurableWebEnvironment"));
}
if (context.getResourceLoader() instanceof WebApplicationContext) {
return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
}
return ConditionOutcome.noMatch(message.because("not a servlet web application"));
}
private ConditionOutcome isReactiveWebApplication(ConditionContext context) {
ConditionMessage.Builder message = ConditionMessage.forCondition("");
if (context.getEnvironment() instanceof ConfigurableReactiveWebEnvironment) {
return ConditionOutcome
.match(message.foundExactly("ConfigurableReactiveWebEnvironment"));
}
if (context.getResourceLoader() instanceof ReactiveWebApplicationContext) {
return ConditionOutcome
.match(message.foundExactly("ReactiveWebApplicationContext"));
}
return ConditionOutcome
.noMatch(message.because("not a reactive web application"));
}
private Type deduceType(AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata
.getAnnotationAttributes(ConditionalOnWebApplication.class.getName());
if (attributes != null) {
return (Type) attributes.get("type");
}
return Type.ANY;
}
}
从 isServletWebApplication 方法可以看出,判断条件是:
①:GenericWebApplicationContext 是否在类路径中。
②:容器是否有名为 session 的 scope。
③:当前容器的 Enviroment 是否为 ConfigurableWebEnvironment
④:当前 ResourceLoader 是否为 WebApplicationContext(ResourceLoader 是 ApplicationContext 的顶级接口之一);
======================================================================================================
实例分析:
在了解 springboot 的运作原理和主要的条件注解后,现在来分析一个简单的 springboot 内置的自动配置功能:HTTP 的编码配置。
我们在常规的配置 HTTP 编码的时候是在 web.xml 中配置一个 filter。
<filter>....</filter>
HTTP 编码配置的自动配置需要两个条件:
①:能配置 CharacterEncodingFilter 这个 Bean
②:能配置 encoding 和 forceEncoding 这两个参数。
配置参数:
springboot 是基于类型安全的配置,这里的配置类可以在 application.properties 中直接设置。
/*
* Copyright 2012-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.http;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for http encoding.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.http.encoding") //1
public class HttpEncodingProperties {
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;//2
/**
* Charset of HTTP requests and responses. Added to the "Content-Type" header if not
* set explicitly.
*/
private Charset charset = DEFAULT_CHARSET;//2
/**
* Whether to force the encoding to the configured charset on HTTP requests and
* responses.
*/
private Boolean force;//3
/**
* Whether to force the encoding to the configured charset on HTTP requests. Defaults
* to true when "force" has not been specified.
*/
private Boolean forceRequest;
/**
* Whether to force the encoding to the configured charset on HTTP responses.
*/
private Boolean forceResponse;
/**
* Locale in which to encode mapping.
*/
private Map<Locale, Charset> mapping;
public Charset getCharset() {
return this.charset;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
public boolean isForce() {
return Boolean.TRUE.equals(this.force);
}
public void setForce(boolean force) {
this.force = force;
}
public boolean isForceRequest() {
return Boolean.TRUE.equals(this.forceRequest);
}
public void setForceRequest(boolean forceRequest) {
this.forceRequest = forceRequest;
}
public boolean isForceResponse() {
return Boolean.TRUE.equals(this.forceResponse);
}
public void setForceResponse(boolean forceResponse) {
this.forceResponse = forceResponse;
}
public Map<Locale, Charset> getMapping() {
return this.mapping;
}
public void setMapping(Map<Locale, Charset> mapping) {
this.mapping = mapping;
}
public boolean shouldForce(Type type) {
Boolean force = (type == Type.REQUEST ? this.forceRequest : this.forceResponse);
if (force == null) {
force = this.force;
}
if (force == null) {
force = (type == Type.REQUEST);
}
return force;
}
public enum Type {
REQUEST, RESPONSE
}
}
①:在 application.properties 配置的时候前缀是 spring.http.encoding;
②:默认编码方式是 UTF-8,若修改可使用 spring.http.charset= 编码;
③:设置 forceEncoding,默认为 true,若修改可使用 spring.http.encoding.force = false;
配置 bean
通过上述配置,并根据条件配置 CharacterEcodingFilter 的 bean ,我们来看看源码:
/*
* Copyright 2012-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.servlet;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
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.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.http.HttpEncodingProperties;
import org.springframework.boot.autoconfigure.http.HttpEncodingProperties.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;
/**
* {@link EnableAutoConfiguration Auto-configuration} for configuring the encoding to use
* in web applications.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @since 1.2.0
*/
@Configuration
@EnableConfigurationProperties(HttpEncodingProperties.class) //1
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)//2
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)//3
public class HttpEncodingAutoConfiguration {
private final HttpEncodingProperties properties;//3
public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
this.properties = properties;
}
@Bean//4
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
@Bean
public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
return new LocaleCharsetMappingsCustomizer(this.properties);
}
private static class LocaleCharsetMappingsCustomizer implements
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
private final HttpEncodingProperties properties;
LocaleCharsetMappingsCustomizer(HttpEncodingProperties properties) {
this.properties = properties;
}
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
if (this.properties.getMapping() != null) {
factory.setLocaleCharsetMappings(this.properties.getMapping());
}
}
@Override
public int getOrder() {
return 0;
}
}
}
①:开启属性注入,通过 @EnableConfigurationProperties 声明,使用 @AutoWired 注入;
②:当 CharacterEncodingFilter 在类路径的条件下;
③:当设置 spring.http.encoding=enabled 的情况下,如果没有设置则默认为 true,即条件符合;
④:像使用 Java 配置的方式配置 CharacterEncodingFilter 这个 Bean;
⑤:当容器中没有这个 Bean 的时候新建 Bean;
实战!!!!
package com.pangu.springboot_autoconfig;
/**
* 本例使用这个类的存在与否来创建这个类的 bean,这个类可以是第三方类库
* @author etfox
*
*/
public class HelloService {
private String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
package com.pangu.springboot_autoconfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 在 application.properties 中使用 hello.msg=xxx 来设置值,不设置默认是 world
* @author etfox
*
*/
@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;
}
}
package com.pangu.springboot_autoconfig;
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;
/**
* 根据 HelloServiceProperties 提供的参数,并通过 @ConditionalOnClass 判断
* HelloService 这个类在类路径中是否存在,且当容器中没有这个 bean 的情况下自动配置这个 Bean.
* @author etfox
*
*/
@Configuration
@EnableConfigurationProperties(HelloServiceProperties.class)
@ConditionalOnClass(HelloService.class)
@ConditionalOnProperty(prefix = "hello", value = "enable", 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;
}
}
我们知道,若想要配置生效,需要注册自动配置类,在 src\main\resources 下新建 META-INF/spring.factories, 此处”\“是为了换行后仍然能读到属性。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pangu.springboot_autoconfig.HelloServiceAutoConfiguration
结构如下:
在项目中引用自定义的 starter pom:
<dependency>
<groupId>com.pangu</groupId>
<artifactId>springboot-autoconfig</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
application 设置值,默认 world.
run,访问....