Spring Boot 核心(二)

ETFOX發表於2018-05-13

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,訪問....




相關文章