Spring Boot 是由 Pivotal 團隊提供的全新架構,其設計目的是用來簡化新 Spring 應用的初始搭建以及開發過程。該架構通過約定由于配置的原則,來進行簡化配置。Spring Boot緻力于在蓬勃發展的快速應用開發領域成為上司者。Spring Boot 目前廣泛應用與各大網際網路公司,有以下特點:
- 建立獨立的 Spring 應用程式
- 嵌入的 Tomcat,無需部署 WAR 檔案
- 簡化 Maven 配置
- 自動配置 Spring
- 提供生産就緒型功能,如名額,健康檢查和外部配置
- 絕對沒有代碼生成,對 XML 沒有要求配置
并且 Spring Boot 可以與Spring Cloud、Docker完美內建,是以我們非常有必要學習 Spring Boot 。并且了解其内部實作原理。通過本次分享,您不僅可以學會如何使用 Spring Boot,還可以學習到其内部實作原理,并深入了解:
- Spring Boot 項目結構,starter 結構
- 常用注解分析
- Spring Boot 啟動過程梳理(含:Spring 事件監聽與廣播;自定義事件; SpringFactoriesLoader 工廠加載機制等)
- 自定義 starter
- 自定義 condition
1. 項目初始化過程
springboot啟動類
springboot啟動類非常簡單,就一句話:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
要想了解springboot的啟動過程,肯定要從這句代碼開始了。
跟進去可以看到有兩步,一個是初始化,一個是run方法的執行:
public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
return new SpringApplication(sources).run(args);
}
先看初始化方法:
private void initialize(Object[] sources) {
//這裡的sources其實就隻有一個,我們的啟動類DemoApplication.class。
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
//是否是web應用程式。通過判斷應用程式中是否可以加載(class.forname)【"javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext"】這兩個類。
this.webEnvironment = deduceWebEnvironment();
//設定初始化類:從配置檔案spring.factories中查找所有的key=org.springframework.context.ApplicationContextInitializer的類【加載,初始化,排序】
//SpringFactoriesLoader:工廠加載機制
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
//設定Listeners:從配置檔案spring.factories中查找所有的key=org.springframework.context.ApplicationListener的類.【加載,初始化,排序】
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
//從目前調用棧中,查找主類
this.mainApplicationClass = deduceMainApplicationClass();
}
SpringFactoriesLoader工廠加載機制
上面代碼中 ,Initializers和Listeners的加載過程都是使用到了
SpringFactoriesLoader
工廠加載機制。我們進入到
getSpringFactoriesInstances
這個方法中:
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//擷取META-INF/spring.factories檔案中key為type類型的所有的類全限定名。注意是所有jar包内的。
Set<String> names = new LinkedHashSet<String>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
//通過上面擷取到的類的全限定名,這裡将會使用Class.forName加載類,并調用構造方法執行個體化類
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
//根據類上的org.springframework.core.annotation.Order注解,排序
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
配置檔案中的内容:

了解過dubbo的同學會覺得這個非常熟悉,是的,沒錯,和dubbo中的擴充點有異曲同工之妙。
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
方法的執行内容入下,下圖中展示了我們自定義的一個配置類的加載過程(後面自定義starter的實作一節中會講):
ApplicationContextInitializer的類圖:
下面是listener的類圖(太多了,不全,隻列出了部分)
總結初始化initialize過程
- 判斷是否是web應用程式
- 從所有類中查找META-INF/spring.factories檔案,加載其中的初始化類和監聽類。
- 查找運作的主類 預設初始化Initializers都繼承自ApplicationContextInitializer。
預設Listeners有:
run方法:
public ConfigurableApplicationContext run(String... args) {
//stopWatch 用于簡單監聽run啟動過程
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
//擷取監聽器。springboot中隻有一個SpringApplicationRunListener監聽器【參考spring的事件與廣播】
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
//下面兩句是加載屬性配置,執行完成後,所有的environment的屬性都會加載進來,包括application.properties和外部的屬性配置。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
//建立spring容器【Class.forName加載ing執行個體化類:"org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext"或"org.springframework.context.annotation.AnnotationConfigApplicationContext。前者是web應用程式時使用】
context = createApplicationContext();
//錯誤分析器
analyzers = new FailureAnalyzers(context);
//主要是調用所有初始化類的initialize方法
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
//初始化spring容器【後面寫到】
refreshContext(context);
//主要是執行ApplicationRunner和CommandLineRunner的實作類【後面會寫到】
afterRefresh(context, applicationArguments);
//通知監聽器
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
private void prepareContext(ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);//設定環境變量
postProcessApplicationContext(context);
//SpringApplication的的初始化器開始工作。主要是循環周遊調用所有Initializers的initialize方法
applyInitializers(context);
//目前是空實作
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// 注冊單例:applicationArguments
context.getBeanFactory().registerSingleton("springApplicationArguments",
applicationArguments);
if (printedBanner != null) {
context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
}
// Load the sources: 隻有一個主類:DemoApplication
Set<Object> sources = getSources();
Assert.notEmpty(sources, "Sources must not be empty");
//加載bean,目前隻有一個主類:DemoApplication
load(context, sources.toArray(new Object[sources.size()]));
//廣播容器加載完成事件
listeners.contextLoaded(context);
}
spring事件
自定義spring事件
spring事件為Bean與bean之間的通訊提供了支援。我們可以發送某個事件,然後通過監聽器來處理該事件。
spring事件繼承自, 監聽器實作了接口
ApplicationEvent
ApplicationListener<E extends ApplicationEvent>
下面我們自定義一個事件和監聽器
import org.springframework.context.ApplicationEvent;
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Data
public class MyEvent extends ApplicationEvent{
private String msg;
/**
* Create a new ApplicationEvent.
*
* @param source the object on which the event initially occurred (never {@code null})
*/
public MyEvent(Object source, String msg) {
super(source);
this.msg = msg;
}
}
package com.example.demo.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Slf4j
@Component
public class MyListener implements ApplicationListener<MyEvent>{
@Override
public void onApplicationEvent(MyEvent event) {
log.info("MyListener收到了MyEvent的消息:{}", event.getMsg());
}
}
編寫一個controller方法:
@Autowired
ApplicationContext applicationContext;
@RequestMapping("testPublishMsg")
public void testPublishMsg(String msg) {
applicationContext.publishEvent(new MyEvent(this, msg));
}
啟動服務,通路
testPublishMsg
接口,就可以在控制台看到日志列印:
MyListener收到了MyEvent的消息:。。。。。
springboot啟動過程中的事件廣播
SpringApplicationRunListener類圖:
上述run過程廣泛應用了spring事件機制(主要是廣播)。上述代碼中首先擷取SpringApplicationRunListeners。這就是在
spring.factories
檔案中配置的所有監聽器。然後整個run 過程使用了listeners的5個方法,每個方法對應一個事件Event:
- starting() run方法執行的時候立馬執行;對應事件的類型是
ApplicationStartedEvent
- environmentPrepared()
建立之前并且環境資訊準備好的時候調用;對應事件的類型是ApplicationContext
ApplicationEnvironmentPreparedEvent
- contextPrepared()
建立好并且在source加載之前調用一次;沒有具體的對應事件ApplicationContext
- contextLoaded()
建立并加載之後并在refresh之前調用;對應事件的類型是ApplicationContext
ApplicationPreparedEvent
- finished() run方法結束之前調用;對應事件的類型是
或ApplicationReadyEvent
ApplicationFailedEven
SpringApplicationRunListeners
是
SpringApplicationRunListener
的集合,
SpringApplicationRunListener
隻有一個實作類:
EventPublishingRunListener
,在這個實作類中,有一個
SimpleApplicationEventMulticaster
類型的屬性
initialMulticaster
,所有的事件都是通過這個屬性的
multicastEvent
方法廣播出去的。以
environmentPrepared()
方法為例,展示一下環境變了的加載過程:
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(
this.application, this.args, environment));
}
上述廣播了一個
ApplicationEnvironmentPreparedEvent
事件。我們知道所有的事件都會被監聽器捕獲處理,spring的監聽器都是
ApplicationListener
的子類。根據事件的類型,找到處理這個時間的監聽器類
ConfigFileApplicationListener
:
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(),
event.getSpringApplication());
}
}
//省略部分代碼
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
configureIgnoreBeanInfo(environment);
bindToSpringApplication(environment, application);
}
//省略部分代碼
/**
* Add config file property sources to the specified environment.
* @param environment the environment to add source to
* @param resourceLoader the resource loader
* @see #addPostProcessors(ConfigurableApplicationContext)
*/
protected void addPropertySources(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
//加載屬性配置的最關鍵的一行代碼
new Loader(environment, resourceLoader).load();
}
ConfigFileApplicationListener
類中可以看到一些其他有用的資訊:[DEFAULTSEARCHLOCATIONS = “classpath:/,classpath:/config/,file:./,file:./config/”][ACTIVEPROFILESPROPERTY = “spring.profiles.active”]等等。
OK,spring的事件機制以及springboot啟動過程中的事件廣播機制講完了。
FailureAnalyzers錯誤分析器
查找并加載spring.factories中所有的org.springframework.boot.diagnostics.FailureAnalyzer。
FailureAnalyzers的類圖:
當啟動過程中發生錯誤,會廣播事件并且執行分析器。 目前主要有analyze和report兩個方法,用途是列印日志。
比如:當我們的啟動類
DemoApplication
不是放到最外層的時候,報錯,這就是
FailureAnalyzers
中列印出的結果:
Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2017-12-17 17:04:23.107 ERROR 9356 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Field environmentService in com.example.demo.environment.impl.DemoApplication required a bean of type 'com.example.demo.environment.EnvironmentService' that could not be found.
Action:
Consider defining a bean of type 'com.example.demo.environment.EnvironmentService' in your configuration.
Process finished with exit code 1
refresh過程
比較複雜,主要是spring的加載過程,本文的目标是springboot而不是spring,是以不再深入,我會專門寫篇檔案介紹。現在隻需要了解:
- spring啟動過程
- bean的加載初始化都是在該方法中完成
afterRefresh與CommandLineRunner、ApplicationRunner
在springboot啟動過程中的run方法的最後,有一句·afterRefresh(context, applicationArguments);·。主要是執行·ApplicationRunner·和·CommandLineRunner·兩個接口的實作類,這兩個類都是在springboot啟動完成後執行的一點代碼,類似于普通bean中的init方法,開機自啟動。 兩者唯一不同是擷取參數方式不同,後面的小例子會講。callRunners的源碼:
List<Object> runners = new ArrayList<Object>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<Object>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}
如果有類似的需求:springboot啟動完成後執行一點代碼邏輯,則可以通過實作上述兩個類來完成。下面寫個小例子: 定義4個類:
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Component
@Order(7)
@Slf4j
public class MyApplicationRunner1 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
StringBuffer stringBuffer = new StringBuffer();
args.getOptionNames().forEach(s -> stringBuffer.append(s).append("=").append(args.getOptionValues(s)).append(","));
log.info("MyApplicationRunner1 "+stringBuffer.toString());
}
}
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Component
@Order(9)
@Slf4j
public class MyApplicationRunner2 implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
StringBuffer stringBuffer = new StringBuffer();
args.getOptionNames().forEach(s -> stringBuffer.append(s).append("=").append(args.getOptionValues(s)).append(","));
log.info("MyApplicationRunner2 "+stringBuffer.toString());
}
}
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Component
@Order(4)
@Slf4j
public class MyCommandLineRunner1 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("MyCommandLineRunner1 " + Arrays.asList(args));
}
}
/**
* Created by hzliubenlong on 2017/12/17.
*/
@Component
@Order(2)//數字越小,優先級越高
@Slf4j
public class MyCommandLineRunner2 implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
log.info("MyCommandLineRunner2 " + Arrays.asList(args));
}
}
執行結果:
2017-12-17 15:40:13.387 INFO 6244 --- [ main] c.e.demo.runners.MyCommandLineRunner2 : MyCommandLineRunner2 [--a=1, --b=2]
2017-12-17 15:40:13.387 INFO 6244 --- [ main] c.e.demo.runners.MyCommandLineRunner1 : MyCommandLineRunner1 [--a=1, --b=2]
2017-12-17 15:40:13.388 INFO 6244 --- [ main] c.e.demo.runners.MyApplicationRunner1 : MyApplicationRunner1 a=[1],b=[2],
2017-12-17 15:40:13.388 INFO 6244 --- [ main] c.e.demo.runners.MyApplicationRunner2 : MyApplicationRunner2 a=[1],b=[2],
結果分析:
-
與CommandLineRunner
的實作類是在springboot初始化完成後執行的ApplicationRunner
- 可以設定執行的順序,數字越小,優先級越高
- 兩者都可以從外界擷取參數。唯一不同是:
的參數類型是字元串數組。而CommandLineRunner
的參數類型是ApplicationRunner
。它可以解析--a=1 --b=2類型的參數為key-value形式。ApplicationArguments
@order
上述過程中,我們看到了好幾個地方使用到了order,比如
CommandLineRunner
ApplicationRunner
,初始化器Initializer,監聽器Listener。
order比較簡單,主要是為了控制這些實作類的執行順序。規則是數值越小,優先級越高。
在通過spring工廠模式,從
spring.factories
中加載這些實作之後,會通過
AnnotationAwareOrderComparator.sort(instances);
方法來進行排序。
Collections.sort(list, INSTANCE);
//上述第二個參數是比較器,是AnnotationAwareOrderComparator的執行個體,從Order注解中擷取對象的排序數值。
protected Integer findOrder(Object obj) {
// Check for regular Ordered interface
Integer order = super.findOrder(obj);
if (order != null) {
return order;
}
// Check for @Order and @Priority on various kinds of elements
if (obj instanceof Class) {
return OrderUtils.getOrder((Class<?>) obj);
}
else if (obj instanceof Method) {
Order ann = AnnotationUtils.findAnnotation((Method) obj, Order.class);
if (ann != null) {
return ann.value();
}
}
else if (obj instanceof AnnotatedElement) {
Order ann = AnnotationUtils.getAnnotation((AnnotatedElement) obj, Order.class);
if (ann != null) {
return ann.value();
}
}
else if (obj != null) {
//真正擷取order的代碼
order = OrderUtils.getOrder(obj.getClass());
if (order == null && obj instanceof DecoratingProxy) {
order = OrderUtils.getOrder(((DecoratingProxy) obj).getDecoratedClass());
}
}
return order;
}
//跳轉到getOrder方法中,其實作是擷取注解Order的值
public static Integer getOrder(Class<?> type, Integer defaultOrder) {
Order order = AnnotationUtils.findAnnotation(type, Order.class);
if (order != null) {
return order.value();
}
Integer priorityOrder = getPriority(type);
if (priorityOrder != null) {
return priorityOrder;
}
return defaultOrder;
}
總結啟動run過程
- 注冊一個StopWatch,用于監控啟動過程
- 擷取監聽器SpringApplicationRunListener,用于springboot啟動過程中的事件廣播
- 設定環境變量environment
- 建立spring容器
- 建立FailureAnalyzers錯誤分析器,用于處理記錄啟動過程中的錯誤資訊
- 調用所有初始化類的initialize方法
- 初始化spring容器
- 執行ApplicationRunner和CommandLineRunner的實作類
- 啟動完成
springboot中重要的注解
前面講到了@order等注解,下面看一下springboot中最重要的、最常用的幾個注解。
@SpringBootApplication
他是一個組合注解:
@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 {
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude")
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName")
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
@Inherited
Inherited作用是,使用此注解聲明出來的自定義注解,在使用此自定義注解時,如果注解在類上面時,子類會自動繼承此注解,否則的話,子類不會繼承此注解。這裡一定要記住,使用Inherited聲明出來的注解,隻有在類上使用時才會有效,對方法,屬性等其他無效。
參考
關于java 注解中元注解Inherited的使用詳解@SpringBootConfiguration
源碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
@SpringBootConfiguration
繼承自
@Configuration
,二者功能也一緻,标注目前類是配置類,并會将目前類内聲明的一個或多個以@Bean注解标記的方法的執行個體納入到srping容器中,并且執行個體名就是方法名。
@EnableAutoConfiguration
@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@EnableAutoConfiguration
用于啟動自動的配置,是springboot的核心注解。上面import了
EnableAutoConfigurationImportSelector
,這個類繼承自
AutoConfigurationImportSelector.AutoConfigurationImportSelector
是關鍵類。
EnableAutoConfigurationImportSelector
類使用了Spring Core包的
SpringFactoriesLoade
r類的
loadFactoryNamesof()
方法。
SpringFactoriesLoader
會查詢
META-INF/spring.factories
檔案中包含的JAR檔案。
當找到
spring.factories
檔案後,
SpringFactoriesLoader
将查詢配置檔案命名的屬性
org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值。
然後在spring啟動過程中的refresh方法中進行真正的bean加載。
@ComponentScan
@ComponentScan
,掃描目前包及其子包下被
@Component
,
@Controller
@Service
@Repository
注解标記的類并納入到spring容器中進行管理。這也是為什麼我們的啟動類
DemoApplication
要放到項目的最外層的原因。
springboot自動化配置原理及自定義starter
前面的文章已經講了springboot的實作原理,無非就是通過spring的condition條件實作的,還是比較簡單的(感謝spring設計的開放性與擴充性)。
在實際工作過程中會遇到需要自定義starter的需求,那麼我們接下來就自己實作一個starter。
先看一下目錄結構:
- MyConfig是自定義的配置類
- HelloService是自定義的bean
- HelloServiceProperties是自定義的類型安全的屬性配置
- MEYA-INF/spring.factories檔案是springboot的工廠配置檔案
本項目就是自定義的starter。假設我們這裡需要一些配置項,使用者在使用該starter時,需要在
application.properties
檔案中配置相關屬性。這裡我們使用了
@ConfigurationProperties
來将屬性配置到一個POJO類中。這樣做的好處是:可以檢測資料的類型,并且可以對資料值進行校驗,詳情請參考我的另一篇部落格:
HelloServiceProperties
類内容如下:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Created by hzliubenlong on 2017/12/13.
*/
@Data
@Component
@ConfigurationProperties(prefix = "hello")
public class HelloServiceProperties {
private String a;
private String b;
}
這樣使用這個starter的時候,就可以配置
hello.a=**
來設定屬性了。
HelloService
是我們的bean,這裡實作比較簡單,擷取外部的屬性配置,列印一下日志。内容如下:
/**
* Created by hzliubenlong on 2017/12/12.
*/
@Slf4j
public class HelloService {
String initVal;
public void setInitVal(String initVal) {
this.initVal = initVal;
}
public String getInitVal() {
return initVal;
}
public String hello(String name) {
log.info("initVal={}, name={}", initVal, name);
return "hello " + name;
}
}
接下來是比較重要的配置類MyConfig,前面已經講過,springboot是通過條件注解來判斷是否要加載bean的,這些内容都是在我們自定義的配置類中來實作:
import com.example.myservice.HelloService;
import com.example.properties.HelloServiceProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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)
public class MyConfig {
@Autowired
private HelloServiceProperties helloServiceProperties;
@ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing = true)
@ConditionalOnClass(HelloService.class)
@Bean
public HelloService initHelloService() {
HelloService helloService = new HelloService();
helloService.setInitVal(helloServiceProperties.getA());
return helloService;
}
}
@Configuration
表明這是一個配置類
@EnableConfigurationProperties(HelloServiceProperties.class)
表示該類使用了屬性配置類
HelloServiceProperties
initHelloService()
方法就是實際加載初始化helloServicebean的方法了,它上面有三個注解:
-
時才加載這個bean,配置沒有的話,預設為true,也就是隻要引入了這個依賴,不做任何配置,這個bean預設會加載。@ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing = true): hello.enabled=true
-
:當HelloService這個類存在時才加載bean。@ConditionalOnClass(HelloService.class)
-
:表明這是一個産生bean的方法,改方法生成一個HelloService的bean,交給spring容器管理。@Bean
好了,到這裡,我們的代碼已經寫完。根據前面講的springboot的原理我們知道,springboot是通過掃描
MEYA-INF/spring.factories
這個工廠配置檔案來加載配置類的, 是以我們還需要建立這個檔案。其内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.MyConfig
上面的
\
是換行符,隻是為了便于代碼的閱讀。通過這個檔案,springboot就可以讀取到我們自定義的配置類MyConfig。 接下來我們隻需要打個jar包即可供另外一個項目使用了。下面貼一下pom.xml的内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>springboot-starter-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-starter-hello</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<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>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>2.4.2</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.scm</groupId>
<artifactId>maven-scm-provider-gitexe</artifactId>
<version>1.8.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
上面定義了一個starter,下面我們将寫一個新的工程,來引用我們自定義的starter。還是先看一下項目目錄:
要想引用我們自定義的starter,自然是先引入依賴了:
<dependency>
<groupId>com.example</groupId>
<artifactId>springboot-starter-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
然後再application.properties檔案中添加如下配置:
hello.enabled=true
hello.a=hahaha
好了,現在就可以在項目中注入HelloService使用了。是的,就是那麼簡單.DemoApplication主類如下:
import com.example.demo.environment.EnvironmentService;
import com.example.myservice.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@SpringBootApplication
public class DemoApplication {
@Autowired
HelloService helloService;
@RequestMapping("/")
public String word(String name) {
return helloService.hello(name);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
啟動項目,通路URLhttp://127.0.0.1:8080/?name=hhh:
背景列印日志為
initVal=hahaha, name=hhh
其中
hahaha
就是我們沒在配置檔案中配置的屬性值,這句日志是我們上面starter項目中
com.example.myservice.HelloService#hello
方法列印出來的。
自定義Conditional注解
前面已經講過condition的原理。其實自定義一個condition很簡單,隻需要實作
SpringBootCondition
類即可,并重寫
com.example.demo.condition.OnLblCondition#getMatchOutcome
方法。
下面我們簡單寫個執行個體:根據屬性配置檔案中的内容,來判斷是否加載bean。
首先定義一個注解,有兩個内容:一個是屬性檔案的KEY,一個是value。我們要實作的是,屬性檔案的value與指定的value一緻,才建立bean。
package com.example.demo.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnLblCondition.class)
public @interface ConditionalOnLbl {
String key();
String value();
}
自定義SpringBootCondition的實作類OnLblCondition:
package com.example.demo.condition;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;
public class OnLblCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionalOnLbl.class.getName());
Object key = annotationAttributes.get("key");//
Object value = annotationAttributes.get("value");
if(key == null || value == null){
return new ConditionOutcome(false, "error");
}
//擷取environment中的值
String key1 = context.getEnvironment().getProperty(key.toString());
if (value.equals(key1)) {//如果environment中的值與指定的value一緻,則傳回true
return new ConditionOutcome(true, "ok");
}
return new ConditionOutcome(false, "error");
}
}
然後随便定義一個類:
@Slf4j
public class MyConditionService {
public void say() {
log.info("MyConditionService init。");
}
}
好了,condition已經自定義完成。接下來就是如何使用了:
@Configuration
@Slf4j
public class MySpringBootConfig {
@ConditionalOnLbl(key = "com.lbl.mycondition", value = "lbl")
@ConditionalOnClass(MyConditionService.class)
@Bean
public MyConditionService initMyConditionService() {
log.info("MyConditionService已加載。");
return new MyConditionService();
}
要想加載
MyConditionService
的bean到spring容器中,需要滿足以下兩個條件:
- 屬性檔案中配置了
com.lbl.mycondition=lbl
- 可以加載
類MyConditionService
好了,讓我們在
application.properties
檔案中添加配置
com.lbl.mycondition=lbl
啟動項目,可以看到日志:
MyConditionService
已加載。 把
application.properties
檔案中的
com.lbl.mycondition
去掉,或者更改個值,則上述日志不會列印,也就是不會建立
MyConditionService
這個bean .
spring @Conditional注解
前面講了springboot的實作基礎是spring的@Conditional注解。介紹原理前我們來看看怎麼用。後面介紹其原理。
我們實作這麼一個小功能:**根據不同的環境,執行個體化不同的bean。 ** springboot通常都是通過-Dspring.profiles.active=dev來區分環境的,如果我們想實作線上的代碼邏輯與開發或者測試環境不同,那麼這是一個解決方案。
使用java的多态,先定義一個接口:
public interface EnvironmentService {
void printEnvironment();
}
然後定義兩個不同環境的實作類,内容比較簡單,隻是輸出一句log。
@Slf4j
public class ProdService implements EnvironmentService {
@Override
public void printEnvironment() {
log.info("我是生産環境。");
}
}
@Slf4j
public class DevService implements EnvironmentService{
@Override
public void printEnvironment() {
log.info("我是開發環境。");
}
}
好了,,準備工作已經做完,接下來編寫配置類,使用spring的@Conditional注解來根據不同的環境配置加載不同的類:
import com.example.demo.environment.impl.DevService;
import com.example.demo.environment.impl.ProdService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 根據不同的環境,初始化不同的bean
*/
@Configuration
@Slf4j
public class MySpringBootConfig {
@ConditionalOnExpression("#{'${spring.profiles.active}'.equals('dev')}")//使用了spring的SPEL表達式
@ConditionalOnClass(DevService.class)
@Bean
public DevService initDevService() {
log.info("DevService已加載。");
return new DevService();
}
@ConditionalOnExpression("#{'${spring.profiles.active}'.equals('prod')}")
@ConditionalOnClass(ProdService.class)
@Bean
public ProdService initProdService() {
log.info("ProdService已加載。");
return new ProdService();
}
}
官方的
condition有很多: 這裡我們使用的是@ConditionalOnExpression。它根據SPEL表達式傳回的結果作為條件判斷。 這裡判斷條件為:spring.profiles.active=dev時,建立DevService。為prod時,建立ProdService。
接下來看如何使用:
import com.example.demo.environment.EnvironmentService;
import com.example.myservice.HelloService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
@SpringBootApplication
public class DemoApplication {
@Autowired
HelloService helloService;
@Autowired
EnvironmentService environmentService;
@Value("#{'${spring.profiles.active}'.equals('dev')}")
String springProfilesActive;
@RequestMapping("/")
public String word(String name) {
environmentService.printEnvironment();
log.info("springProfilesActive={}", springProfilesActive);
return helloService.hello(name);
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
很簡單,隻需要注入
EnvironmentService
即可。注意IDEA可能會提示錯誤,因為這個接口有兩個實作類。不過不用去管他,因為我們通過condition隻執行個體化了一個bean。
運作結果:
我是開發環境。
springProfilesActive=true
initVal=hahaha, name=hhh
将配置修改為prod後:
我是生産環境。
springProfilesActive=true
initVal=hahaha, name=hhh
使用非常簡單,那麼
@Conditional
是怎麼實作的呢?
官方文檔的說明是“隻有當所有指定的條件都滿足是,元件才可以注冊”。主要的用處是在建立bean時增加一系列限制條件。 他的核心類是:
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
所有的condition都是Condition接口的實作類,條件判斷是通過matches方法傳回的布爾值來判斷的。
以
@ConditionalOnExpression
注解為例:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnExpressionCondition.class)
public @interface ConditionalOnExpression {
/**
* The SpEL expression to evaluate. Expression should return {@code true} if the
* condition passes or {@code false} if it fails.
* @return the SpEL expression
*/
String value() default "true";
}
ConditionalOnExpression
注解上添加了另一個注解Conditional,指明是哪個condition類處理改注解。 我們打開
OnExpressionCondition
這個類:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
//擷取到我們配置的EL表達式的值
String expression = (String) metadata
.getAnnotationAttributes(ConditionalOnExpression.class.getName())
.get("value");
expression = wrapIfNecessary(expression);
String rawExpression = expression;
expression = context.getEnvironment().resolvePlaceholders(expression);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
BeanExpressionResolver resolver = (beanFactory != null)
? beanFactory.getBeanExpressionResolver() : null;
BeanExpressionContext expressionContext = (beanFactory != null)
? new BeanExpressionContext(beanFactory, null) : null;
if (resolver == null) {
resolver = new StandardBeanExpressionResolver();
}
boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
return new ConditionOutcome(result, ConditionMessage
.forCondition(ConditionalOnExpression.class, "(" + rawExpression + ")")
.resultedIn(result));
}
有這麼一個方法,擷取到我們設定的EL表達式的值後,進行一些處理,比如管道
environment
中的值,然後計算整體的結果。 其中關鍵的是
boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
這麼一行代碼,通過springEL計算最終結果。
至此,spring的condition算是介紹完了,我們可以通過實作
org.springframework.context.annotation.Condition#matches
來自定義condition。
當然抛開@Condition注解,實作不同環境加載不同的類,既可以使用
ConditionalOnExpression
也可以使用
@Profile
注解
兩種都可以實作不同環境加載不同的類,寫法不同,但是他們的實作原理都是一樣的,都是擷取環境變量的值,與value進行比較。讀者可以去看一下
spring.profiles.active
源碼
ProfileCondition
@Bean
@Profile("dev")
public DevService initDevService1() {
log.info("DevService已加載---Profile");
return new DevService();
}
@Bean
@Profile("prod")
public ProdService initProdService1() {
log.info("ProdService已加載---Profile");
return new ProdService();
}
備注:springboot的核心注解
spring.factories檔案裡每一個xxxAutoConfiguration檔案一般都會有下面的條件注解:
@ConditionalOnBean:當容器裡有指定Bean的條件下
@ConditionalOnClass:當類路徑下有指定類的條件下
@ConditionalOnExpression:基于SpEL表達式作為判斷條件
@ConditionalOnJava:基于JV版本作為判斷條件
@ConditionalOnJndi:在JNDI存在的條件下差在指定的位置
@ConditionalOnMissingBean:當容器裡沒有指定Bean的情況下
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下
@ConditionalOnNotWebApplication:目前項目不是Web項目的條件下
@ConditionalOnProperty:指定的屬性是否有指定的值
@ConditionalOnResource:類路徑是否有指定的值
@ConditionalOnSingleCandidate:當指定Bean在容器中隻有一個,或者雖然有多個但是指定首選Bean
@ConditionalOnWebApplication:目前項目是Web項目的條件下。
上面@ConditionalOnXXX都是組合@Conditional元注解,使用了不同的條件Condition