看actuator項目的包結構,如下:
本文中的介紹Endpoints。
Endpoints(端點)介紹
Endpoints 是 Actuator 的核心部分,它用來監視應用程序及交互,spring-boot-actuator中已經內置了非常多的Endpoints(health、info、beans、httptrace、shutdown等等),同時也允許我們擴展自己的端點。
Endpoints 分成兩類:原生端點和用戶自定義端點:
- 原生端點是在應用程序里提供的眾多 restful api 接口,通過它們可以監控應用程序運行時的內部狀況。原生端點又可以分成三類:
- 應用配置類:可以查看應用在運行期間的靜態信息:例如自動配置信息、加載的spring bean信息、yml文件配置信息、環境信息、請求映射信息;
- 度量指標類:主要是運行期間的動態信息,例如堆棧、請求連、一些健康指標、metrics信息等;
- 操作控制類:主要是指shutdown,用戶可以發送一個請求將應用的監控功能關閉。
- 自定義端點主要是指擴展性,用戶可以根據自己的實際應用,定義一些比較關心的指標,在運行期進行監控。
我們這里詳細說明org.springframework.boot.actuate.endpoint中原生端點的實現.通過如下幾個維度來進行分析:
- xxxEndpoint的作用
- xxxEndpoint的字段,構造器
- xxxEndpoint核心方法invoke 實現分析
- xxxEndpoint如何進行配置
- xxxEndpoint如何自動化裝配
在org.springframework.boot.actuate.endpoint 中還有2個子包-jmx(可通過jmx協議訪問),mvc(通過spring mvc 暴露,可通過接口進行訪問,在下篇文章進行分析).這里我們不關注這些,這看org.springframework.boot.actuate.endpoint 包下的類,類比較多,先看個類圖吧,如下:
Endpoint接口:org.springframework.boot.actuate.endpoint.Endpoint.java
Endpoint接口:一個端點可以用於暴露(系統信息、操作入口等)信息。通常暴露方式是通過spring mvc的,如繼承AbstractEndpoint的方式實現自己的endpoint。
public interface Endpoint<T> { // 端點的邏輯標識(字母、數字和下划線('_') 組成) String getId(); // 端點是否啟用 boolean isEnabled(); // 端點是否輸出敏感數據 boolean isSensitive(); // 調用端點,並返回調用結果 T invoke(); }
其中泛型參數T為暴露的數據類型.方法的作用已經注釋。
AbstractEndpoint抽象類
Endpoint的一個抽象子類:AbstractEndpoint(Endpoint接口實現的抽象基類),該類實現了EnvironmentAware,因此, AbstractEndpoint也就持有了Environment。
1、AbstractEndpoint 有如下屬性:
// 匹配包括下划線的任何單詞字符。類似但不等價於“[A-Za-z0-9_]” private static final Pattern ID_PATTERN = Pattern.compile("\\w+"); // 通過EnvironmentAware接口注入 private Environment environment; // 端點標識符 private String id; // 是否默認敏感 private final boolean sensitiveDefault; // 標識該端點是否暴露敏感信息 private Boolean sensitive; // 是否端點可用 private Boolean enabled;
2、AbstractEndpoint方法
AbstractEndpoint方法實現了Endpoint接口中的getId, isEnabled, isSensitive,其中, getId只需返回AbstractEndpoint中的id屬性即可,我們分別來看下其他方法的實現:
2.1、isEnabled,代碼如下:
@Override public boolean isEnabled() { return EndpointProperties.isEnabled(this.environment, this.enabled); } @ConfigurationProperties(prefix = "endpoints") public class EndpointProperties { private static final String ENDPOINTS_ENABLED_PROPERTY = "endpoints.enabled"; private static final String ENDPOINTS_SENSITIVE_PROPERTY = "endpoints.sensitive"; public static boolean isEnabled(Environment environment, Boolean enabled) { //1、如果AbstractEndpoint#enabled屬性有值,則使用AbstractEndpoint的配置 if (enabled != null) { return enabled; } //如果Environment 不等於null 並且Environment 配置有endpoints.enabled的屬性,則返回其配置的值 if (environment != null && environment.containsProperty(ENDPOINTS_ENABLED_PROPERTY)) { return environment.getProperty(ENDPOINTS_ENABLED_PROPERTY, Boolean.class); } //3、如果1和2沒有值,則返回默認值true return true; }
2.2、isSensitive和isEnabled實現差不多,如下:
@Override public boolean isSensitive() { return EndpointProperties.isSensitive(this.environment, this.sensitive, this.sensitiveDefault); } @ConfigurationProperties(prefix = "endpoints") public class EndpointProperties { private static final String ENDPOINTS_ENABLED_PROPERTY = "endpoints.enabled"; private static final String ENDPOINTS_SENSITIVE_PROPERTY = "endpoints.sensitive"; public static boolean isSensitive(Environment environment, Boolean sensitive, boolean sensitiveDefault) { //1、如果abstractEndpoint的sensitive有值,則使用這個配置 if (sensitive != null) { return sensitive; } //2、如果environment 不等於null 並且 environment中配置有endpoints.sensitive的屬性,則返回其配置值 if (environment != null && environment.containsProperty(ENDPOINTS_SENSITIVE_PROPERTY)) { return environment.getProperty(ENDPOINTS_SENSITIVE_PROPERTY, Boolean.class); } //3、返回指定的默認值(默認為false) return sensitiveDefault; }
EnvironmentEndpoint
AbstractEndpoint的實現類之EnvironmentEndpoint--敏感數據
1、構造函數
@ConfigurationProperties(prefix = "endpoints.env") public class EnvironmentEndpoint extends AbstractEndpoint<Map<String, Object>> { public EnvironmentEndpoint() { super("env"); } //調用AbstractEndpoint的構造函數 public AbstractEndpoint(String id) { this(id, true); }
最終,設置id為env,標識為敏感數據。
2、實現的invoke(),代碼如下:
public Map<String, Object> invoke() { // 1. 先定義空的map返回值 Map<String, Object> result = new LinkedHashMap<String, Object>(); // 2. 將spring boot 中激活的profile 放入result中,key --> profile result.put("profiles", getEnvironment().getActiveProfiles()); // 3. 獲得PlaceholderSanitizingPropertyResolver --> 處理占位符,處理敏感數據 PropertyResolver resolver = getResolver(); // 4. 遍歷environment 配置的PropertySource,依次處理之 for (Entry<String, PropertySource<?>> entry : getPropertySourcesAsMap() .entrySet()) { PropertySource<?> source = entry.getValue(); String sourceName = entry.getKey(); if (source instanceof EnumerablePropertySource) { // 4.1 只針對EnumerablePropertySource 類型的PropertySource 進行處理--> 依次將屬性添加到properties中, // 如果屬性值為string,則在添加前進行占位符,數據脫敏的處理 EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source; Map<String, Object> properties = new LinkedHashMap<String, Object>(); for (String name : enumerable.getPropertyNames()) { Object property = source.getProperty(name); Object resolved = property instanceof String ? resolver.resolvePlaceholders((String) property) : property; //調用Sanitizer類進行脫敏 properties.put(name, sanitize(name, resolved)); } // 4.2 后置處理,該方法的實現是直接返回原始值,可以通過覆寫的方式進行擴展 properties = postProcessSourceProperties(sourceName, properties); if (properties != null) { // 4.3 如果不為空,則添加到result中 result.put(sourceName, properties); } } } return result; }
處理占位符,處理敏感數據:PlaceholderSanitizingPropertyResolver.java是EnvironmentEndpoint的內部類
public PropertyResolver getResolver() { // 1. 實例化PlaceholderSanitizingPropertyResolver --> 處理占位符,處理敏感數據 PlaceholderSanitizingPropertyResolver resolver = new PlaceholderSanitizingPropertyResolver( getPropertySources(), this.sanitizer); // 2. 設置ignoreUnresolvableNestedPlaceholders 為true resolver.setIgnoreUnresolvableNestedPlaceholders(true); return resolver; }
PlaceholderSanitizingPropertyResolver繼承了PropertySourcesPropertyResolver,這樣就能對占位符進行處理了,又因為其內部持有Sanitizer(用於敏感數據脫敏),復寫了getPropertyAsRawString,這樣就能處理占位符,敏感數據了.代碼如下:
@Override protected String getPropertyAsRawString(String key) { String value = super.getPropertyAsRawString(key); return (String) this.sanitizer.sanitize(key, value); }
3、EnvironmentEndpoint的屬性配置,由於EnvironmentEndpoint被@ConfigurationProperties(prefix = “endpoints.env”)注解,因此可通過如下配置進行個性化配置:
endpoints.env.id=env endpoints.env.sensitive=true endpoints.env.enabled=true
同時,又因為其聲明了如下方法:
public void setKeysToSanitize(String... keysToSanitize) { this.sanitizer.setKeysToSanitize(keysToSanitize); }
因此可以通過endpoints.env.keys-to-sanitize=xx,xx 來配置對指定的數據進行脫敏。脫敏配置
4、EnvironmentEndpoint的自動化裝配
EnvironmentEndpoint的自動化裝配是在EndpointAutoConfiguration中,代碼如下:
@Configuration @AutoConfigureAfter({ FlywayAutoConfiguration.class, LiquibaseAutoConfiguration.class }) @EnableConfigurationProperties(EndpointProperties.class) public class EndpointAutoConfiguration { @Bean @ConditionalOnMissingBean public EnvironmentEndpoint environmentEndpoint() { return new EnvironmentEndpoint(); }
- @Bean注解:注冊1個id為environmentEndpoint,類型為EnvironmentEndpoint的bean
- @ConditionalOnMissingBean注解:當beanFactory中不存在EnvironmentEndpoint類型的bean時注冊
InfoEndpoint
AbstractEndpoint的實現類之InfoEndpoint,用於暴露應用信息。
其字段和構造器如下:
private final List<InfoContributor> infoContributors; public InfoEndpoint(List<InfoContributor> infoContributors) { super("info", false); Assert.notNull(infoContributors, "Info contributors must not be null"); this.infoContributors = infoContributors; }
其內部持有了BeanFactory中所有InfoContributor類型的bean,其通過構造器注入。
2、invoke 實現如下:
public Map<String, Object> invoke() { Info.Builder builder = new Info.Builder(); for (InfoContributor contributor : this.infoContributors) { contributor.contribute(builder); } Info build = builder.build(); return build.getDetails(); }
通過遍歷其內部的持有infoContributors,因此調用其contribute將info的數據添加到Info.Builder中,最后通過Info.Builder構建出Info,返回Info持有的details(建造者模式). Info中的details為Map.
InfoContributor接口用於向Info$Builder添加信息,關於這部分的內容,我們后續文章有分析.這里就不在贅述了.
3、InfoEndpoint的屬性配置
@ConfigurationProperties(prefix = "endpoints.info") public class InfoEndpoint extends AbstractEndpoint<Map<String, Object>> {
因此可通過如下進行配置:
endpoints.info.id=info endpoints.info.sensitive=true endpoints.info.enabled=true
4、InfoEndpoint的自動化裝配–>在EndpointAutoConfiguration中,代碼如下:
@Bean @ConditionalOnMissingBean public InfoEndpoint infoEndpoint() throws Exception { return new InfoEndpoint(this.infoContributors == null ? Collections.<InfoContributor>emptyList() : this.infoContributors); }
和EnvironmentEndpoint一樣。
RequestMappingEndpoint:
AbstractEndpoint的實現類之RequestMappingEndpoint,由於RequestMappingEndpoint同時也實現了ApplicationContextAware接口,因此,在初始化該類時會注入applicationContext。這個類的作用是打印Spring MVC 映射信息。
1、構造函數
public RequestMappingEndpoint() { super("mappings"); }
因此, RequestMappingEndpoint的id為 mappings,默認為敏感。
2、invoke 實現如下:
public Map<String, Object> invoke() { Map<String, Object> result = new LinkedHashMap<String, Object>(); // 1. 從handlerMappings中獲取HandlerMapping,默認情況下handlerMappings是不存在數據的 extractHandlerMappings(this.handlerMappings, result); // 2. 從applicationContext中獲取AbstractUrlHandlerMapping類型的bean,依次將其注冊的handler 添加進去. extractHandlerMappings(this.applicationContext, result); // 3. 從methodMappings中獲取HandlerMapping,默認情況下methodMappings是不存在數據的 extractMethodMappings(this.methodMappings, result); // 3. 從applicationContext中獲取AbstractUrlHandlerMapping類型的bean,依次獲得其持有的HandlerMethods,進行處理. extractMethodMappings(this.applicationContext, result); return result; }
從applicationContext中獲取
protected void extractHandlerMappings(ApplicationContext applicationContext, Map<String, Object> result) { if (applicationContext != null) { Map<String, AbstractUrlHandlerMapping> mappings = applicationContext .getBeansOfType(AbstractUrlHandlerMapping.class); for (Entry<String, AbstractUrlHandlerMapping> mapping : mappings.entrySet()) { Map<String, Object> handlers = getHandlerMap(mapping.getValue()); for (Entry<String, Object> handler : handlers.entrySet()) { result.put(handler.getKey(), Collections.singletonMap("bean", mapping.getKey())); } } } }
獲得AbstractUrlHandlerMapping類型的bean,此時有4個:
beanNameHandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
resourceHandlerMapping=org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
faviconHandlerMapping=org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
welcomePageHandlerMapping=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WelcomePageHandlerMapping
依次遍歷mappings:
獲得AbstractUrlHandlerMapping中注冊的handler,key–> path,value–>handler
依次遍歷handlerss,存入結果集中,存入的key–>AbstractUrlHandlerMapping的id,value={bean=AbstractUrlHandlerMapping中注冊的handler的路徑}
從methodMappings中獲取HandlerMapping,默認情況下methodMappings是不存在數據的
從applicationContext中獲取AbstractUrlHandlerMapping類型的bean,依次獲得其持有的HandlerMethods,進行處理.代碼如下:
protected void extractMethodMappings( Collection<AbstractHandlerMethodMapping<?>> methodMappings, Map<String, Object> result) { for (AbstractHandlerMethodMapping<?> mapping : methodMappings) { Map<?, HandlerMethod> methods = mapping.getHandlerMethods(); for (Map.Entry<?, HandlerMethod> entry : methods.entrySet()) { result.put(String.valueOf(entry.getKey()), Collections .singletonMap("method", String.valueOf(entry.getValue()))); } } }
獲得AbstractUrlHandlerMapping類型的bean
依次遍歷AbstractUrlHandlerMapping中注冊的handler,添加至結果集中,key–> Handler 映射路徑 ,value = {bean = AbstractHandlerMethodMapping的id,method=HandlerMethod}
3、RequestMappingEndpoint的配置
@ConfigurationProperties(prefix = "endpoints.mappings") public class RequestMappingEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
從類上的配置注解,可知:
endpoints.mappings.enabled= # Enable the endpoint. endpoints.mappings.id= # Endpoint identifier. endpoints.mappings.sensitive= # Mark if the endpoint exposes sensitive information.
4、RequestMappingEndpoint的自動裝配
@Configuration @ConditionalOnClass(AbstractHandlerMethodMapping.class) protected static class RequestMappingEndpointConfiguration { @Bean @ConditionalOnMissingBean public RequestMappingEndpoint requestMappingEndpoint() { RequestMappingEndpoint endpoint = new RequestMappingEndpoint(); return endpoint; } }
當滿足如下兩個條件時創建requestMappingEndpoint,即注冊1個id為requestMappingEndpoint,類型為RequestMappingEndpoint的bean:
@ConditionalOnClass(AbstractHandlerMethodMapping.class) –> 在beanFactory中存在AbstractHandlerMethodMapping類型的bean時生效
@ConditionalOnMissingBean–>在beanFactory中不存在RequestMappingEndpoint類型的bean時生效
DumpEndpoint
AbstractEndpoint的實現類之DumpEndpoint,這個類的作用是打印線程信息,為敏感。
1、構造函數
public DumpEndpoint() { super("dump"); }
2、invoke()方法實現
@Override public List<ThreadInfo> invoke() { return Arrays .asList(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true)); }
調用了ThreadMXBean的dumpAllThreads來返回所有活動線程的線程信息,並帶有堆棧跟蹤和同步信息。 當此方法返回時,返回數組中包含的一些線程可能已經終止。其中兩個參數指的意義如下:
第1個–>如果為 true,則轉儲所有鎖定的監視器。
第2個–>如果為 true,則轉儲所有鎖定的可擁有同步器。
ThreadMXBean怎么讀取Thread信息見《JMX學習一》
3、DumpEndpoint的配置
@ConfigurationProperties(prefix = "endpoints.dump") public class DumpEndpoint extends AbstractEndpoint<List<ThreadInfo>> {
可知:
endpoints.dump.enabled= # Enable the endpoint. endpoints.dump.id= # Endpoint identifier. endpoints.dump.sensitive= # Mark if the endpoint exposes sensitive information.
4、DumpEndpoint自動化裝配:
@Bean @ConditionalOnMissingBean public DumpEndpoint dumpEndpoint() { return new DumpEndpoint(); }
FlywayEndpoint
AbstractEndpoint的實現類之FlywayEndpoint,Flyway是一款開源的數據庫版本管理工具。
ShutdownEndpoint
AbstractEndpoint的實現類之ShutdownEndpoint,作用是關閉應用。這個類繼承了ApplicationAware,得到applicationContext,關閉spring容器調用applicationContext.close()方法。
1、構造函數
public ShutdownEndpoint() { super("shutdown", true, false); }
id為shutdown,為敏感,關閉應用的endpoin默認是關閉的,不啟用的。
2、invoke()方法實現
@Override public Map<String, Object> invoke() { //如果context為null,直接返回 if (this.context == null) { return NO_CONTEXT_MESSAGE; } //context不為null,先返回,再啟動一個線程,在該線程中,先sleep 5秒后,然后調用了ShutdownEndpoint中持有的context的close方法進行關閉. try { return SHUTDOWN_MESSAGE; } finally { Thread thread = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(500L); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } ShutdownEndpoint.this.context.close(); } }); thread.setContextClassLoader(getClass().getClassLoader()); thread.start(); } }
在返回后,還希望做點啥,用try--finally。
3、ShutdownEndpoint的配置
@ConfigurationProperties(prefix = "endpoints.shutdown") public class ShutdownEndpoint extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
可知配置有:
endpoints.shutdown.enabled= # Enable the endpoint. endpoints.shutdown.id= # Endpoint identifier. endpoints.shutdown.sensitive= # Mark if the endpoint exposes sensitive information.
4、ShutdownEndpoint自動化裝配:
@Bean @ConditionalOnMissingBean public ShutdownEndpoint shutdownEndpoint() { return new ShutdownEndpoint(); }
AutoConfigurationReportEndpoint
AbstractEndpoint的實現類之AutoConfigurationReportEndpoint,作用是暴露ConditionEvaluationReport
.。
1、構造函數
public AutoConfigurationReportEndpoint() { super("autoconfig"); }
id為autoconfig,為敏感。
2、invoke()方法實現
@Override public Report invoke() { return new Report(this.autoConfigurationReport); }
Report為其內部類
@JsonPropertyOrder({ "positiveMatches", "negativeMatches", "exclusions" }) @JsonInclude(Include.NON_EMPTY) public static class Report {
- @JsonPropertyOrder–>作用在類上,被用來指明當序列化時需要對屬性做排序,它有2個屬性:一個是alphabetic:布爾類型,表示是否采用字母拼音順序排序,默認是為false,即不排序
- @JsonInclude–> Report中的屬性值為空集合則不進行展示
Report 有如下字段:
// 匹配的 private final MultiValueMap<String, MessageAndCondition> positiveMatches; // 不匹配的 private final Map<String, MessageAndConditions> negativeMatches; // 去除的 private final List<String> exclusions; // 一般為null private final Report parent;
其中MessageAndCondition封裝了ConditionAndOutcome中的condition,message以進行更好的展示(json友好).其類上聲明了如下注解:
@JsonPropertyOrder({ "condition", "message" })
因此在進行輸出的時候,先輸出condition,再輸出message.
構造器如下:
public MessageAndCondition(ConditionAndOutcome conditionAndOutcome) { Condition condition = conditionAndOutcome.getCondition(); ConditionOutcome outcome = conditionAndOutcome.getOutcome(); this.condition = ClassUtils.getShortName(condition.getClass()); if (StringUtils.hasLength(outcome.getMessage())) { this.message = outcome.getMessage(); } else { this.message = (outcome.isMatch() ? "matched" : "did not match"); } }
賦值condition為ConditionAndOutcome中的Condition的短類名.
賦值message:如果ConditionAndOutcome中的Message有值則直接賦值,否則,如果對應的Condition匹配,則賦值為matched,否則賦值為did not match。
回到report,構造器如下:
public Report(ConditionEvaluationReport report) { this.positiveMatches = new LinkedMultiValueMap<String, MessageAndCondition>(); this.negativeMatches = new LinkedHashMap<String, MessageAndConditions>(); // 1. 通過report#getExclusions 獲得不進行加載的bean this.exclusions = report.getExclusions(); // 2. for (Map.Entry<String, ConditionAndOutcomes> entry : report .getConditionAndOutcomesBySource().entrySet()) { // 2.1 如果該配置生效條件都匹配,則加入到positiveMatches,否則,加入到negativeMatches if (entry.getValue().isFullMatch()) { add(this.positiveMatches, entry.getKey(), entry.getValue()); } else { add(this.negativeMatches, entry.getKey(), entry.getValue()); } } // 3. 如果report存在父report,則進行初始化Report 賦值為當前類的parent 屬性 boolean hasParent = report.getParent() != null; this.parent = (hasParent ? new Report(report.getParent()) : null); }
通過ConditionEvaluationReport#getExclusions 獲得不進行加載的bean,賦值為exclusions
調用ConditionEvaluationReport#getConditionAndOutcomesBySource 獲得ConditionEvaluationReport中持有匹配信息,返回的map中,key–> 匹配類名,ConditionAndOutcomes–> 匹配結果.
依次遍歷第2步的返回值–>如果該配置生效條件都匹配,則加入到positiveMatches,否則,加入到negativeMatches.其中add 代碼如下:
private void add(MultiValueMap<String, MessageAndCondition> map, String source, ConditionAndOutcomes conditionAndOutcomes) { String name = ClassUtils.getShortName(source); for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) { map.add(name, new MessageAndCondition(conditionAndOutcome)); } }
因此positiveMatches,negativeMatches 中的key為配置類的簡單類名.
如果report存在父report,則進行初始化Report 賦值為當前類的parent 屬性.一般來說,是不存在父report的
屬性配置(因為有@ConfigurationProperties(prefix = “endpoints.autoconfig”) 注解):
endpoints.autoconfig.enabled= # Enable the endpoint. endpoints.autoconfig.id= # Endpoint identifier. endpoints.autoconfig.sensitive= # Mark if the endpoint exposes sensitive information.
自動裝配:
同樣還是在EndpointAutoConfiguration中,代碼如下:
@Bean @ConditionalOnBean(ConditionEvaluationReport.class) @ConditionalOnMissingBean(search = SearchStrategy.CURRENT) public AutoConfigurationReportEndpoint autoConfigurationReportEndpoint() { return new AutoConfigurationReportEndpoint(); }
@Bean –> 注冊1個id為autoConfigurationReportEndpoint,類型為AutoConfigurationReportEndpoint的bean
@ConditionalOnBean(ConditionEvaluationReport.class)–>beanFactory中存在ConditionEvaluationReport類型的bean時生效
@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)–> 在當前上下文中不存在AutoConfigurationReportEndpoint類型的bean時生效
ConditionEvaluationReport
AutoConfigurationReportEndpoint 是通過ConditionEvaluationReport 來進行暴露信息.
ConditionEvaluationReport 字段如下:
private static final String BEAN_NAME = "autoConfigurationReport"; // 如果一個配置類中內部配置類不匹配,則在其外部類的所對應的ConditionAndOutcomes中添加1個AncestorsMatchedCondition private static final AncestorsMatchedCondition ANCESTOR_CONDITION = new AncestorsMatchedCondition(); // key-->配置類類名,ConditionAndOutcomes-->匹配條件結果的封裝 private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<String, ConditionAndOutcomes>(); // 是否添加AncestorsMatchedCondition,默認為false private boolean addedAncestorOutcomes; // 父ConditionEvaluationReport,一般為null private ConditionEvaluationReport parent; // 去除加載的配置 private List<String> exclusions = Collections.emptyList(); // 在ConditionEvaluationReportAutoConfigurationImportListener#onAutoConfigurationImportEvent 添加,用於保存還沒有 // 執行判斷的class private Set<String> unconditionalClasses = new HashSet<String>();
ConditionEvaluationReport 實例化過程如下:
在SpringApplication#run中會調用AbstractApplicationContext#refresh,在refresh會調用PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors,在該方法中最終會調用到AutoConfigurationImportSelector#selectImports方法.在該方法中會調用fireAutoConfigurationImportEvents,代碼如下:AutoConfigurationImportSelector.fireAutoConfigurationImportEvents()
private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) { List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners(); if (!listeners.isEmpty()) { AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions); for (AutoConfigurationImportListener listener : listeners) { invokeAwareMethods(listener); listener.onAutoConfigurationImportEvent(event); } } }
會加載/META-INF/spring.factories 中配置的org.springframework.boot.autoconfigure.AutoConfigurationImportListener,實例化后,依次調用其onAutoConfigurationImportEvent 方法. spring.factories 配置如下:
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
因此此處會調用ConditionEvaluationReportAutoConfigurationImportListener#onAutoConfigurationImportEvent.代碼如下:
@Override public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) { if (this.beanFactory != null) { ConditionEvaluationReport report = ConditionEvaluationReport .get(this.beanFactory); report.recordEvaluationCandidates(event.getCandidateConfigurations()); report.recordExclusions(event.getExclusions()); } }
A、實例化ConditionEvaluationReport,代碼如下:
public static ConditionEvaluationReport get( ConfigurableListableBeanFactory beanFactory) { synchronized (beanFactory) { ConditionEvaluationReport report; // 1. 如果當前beanFactory包含autoConfigurationReport定義的話,就從beanFactory中獲取, if (beanFactory.containsSingleton(BEAN_NAME)) { report = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class); } else { // 否則就實例化一個,然后進行注冊 report = new ConditionEvaluationReport(); beanFactory.registerSingleton(BEAN_NAME, report); } // 2. 如果存在父容器的話,就從父容器中獲取。 locateParent(beanFactory.getParentBeanFactory(), report); return report; } }
如果當前beanFactory包含autoConfigurationReport定義的話,就從beanFactory中獲取,否則就實例化一個,然后進行注冊
如果存在父容器的話,就從父容器中獲取,並將其賦值為當前context中獲得的ConditionEvaluationReport的父ConditionEvaluationReport.代碼如下:
private static void locateParent(BeanFactory beanFactory, ConditionEvaluationReport report) { if (beanFactory != null && report.parent == null && beanFactory.containsBean(BEAN_NAME)) { report.parent = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class); } }
一般都是null,不會執行的。
B、設置unconditionalClasses為event#getCandidateConfigurations的返回值。
C、設置exclusions為event#getExclusions的返回值。
在AutoConfigurationReportEndpoint中是通過Report來進行暴露信息的,而在其構造器中,調用了ConditionEvaluationReport#getConditionAndOutcomesBySource方法,代碼如下:
public Map<String, ConditionAndOutcomes> getConditionAndOutcomesBySource() { if (!this.addedAncestorOutcomes) { // 1. 如果addedAncestorOutcomes 設為false,則依次遍歷outcomes,如果一個配置類中內部配置類不匹配,則在其外部類的所對應的ConditionAndOutcomes中添加1個AncestorsMatchedCondition for (Map.Entry<String, ConditionAndOutcomes> entry : this.outcomes.entrySet()) { if (!entry.getValue().isFullMatch()) { addNoMatchOutcomeToAncestors(entry.getKey()); } } this.addedAncestorOutcomes = true; } return Collections.unmodifiableMap(this.outcomes); }
如果addedAncestorOutcomes 設為false,則依次遍歷outcomes,如果一個配置類中內部配置類不匹配,則在其外部類的所對應的ConditionAndOutcomes中添加1個AncestorsMatchedCondition
返回outcomes.
問題來了, outcomes 中的數據是如何添加的?
答案: 有2處.
還是在AutoConfigurationImportSelector#selectImports中,會調用其filter方法.代碼如下
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) { long startTime = System.nanoTime(); String[] candidates = configurations.toArray(new String[configurations.size()]); boolean[] skip = new boolean[candidates.length]; boolean skipped = false; // 1. 獲取META-INFspring.factories/中配置的org.springframework.boot.autoconfigure.AutoConfigurationImportFilter,.OnClassCondition.依次進行遍歷之 // 此時獲得的是org.springframework.boot.autoconfigure.condition for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) { // 1.1 進行屬性注入 invokeAwareMethods(filter); // 1.2 調用AutoConfigurationImportFilter#match 進行判斷,依次遍歷其返回值,如果返回的是false,則說明該配置為跳過,並將skipped設置為true // 獲得AutoConfigurationMetadata中配置的 ConditionalOnClass,如果不會空,則依次遍歷之,看是否在當前類路徑下存在 // 如果不匹配的話,則調用 ConditionEvaluationReport.#ecordConditionEvaluation 進行記錄 // 由於此時AutoConfigurationMetadata 什么都沒有配置,因此此步驟相當於空操作,最終會在第2步返回 boolean[] match = filter.match(candidates, autoConfigurationMetadata); for (int i = 0; i < match.length; i++) { if (!match[i]) { skip[i] = true; skipped = true; } } } // 2. 如果skipped 等於false,則直接返回configurations,說明沒有配置是需要跳過的 if (!skipped) { return configurations; } // 3. 依次遍歷candidates,如果該配置是不進行跳過的,則添加至result中進行返回 List<String> result = new ArrayList<String>(candidates.length); for (int i = 0; i < candidates.length; i++) { if (!skip[i]) { result.add(candidates[i]); } } return new ArrayList<String>(result); }
獲取META-INFspring.factories/中配置的org.springframework.boot.autoconfigure.AutoConfigurationImportFilter,.OnClassCondition.依次進行遍歷之.此時獲得的是org.springframework.boot.autoconfigure.condition
進行屬性注入
調用AutoConfigurationImportFilter#match 進行判斷,依次遍歷其返回值,如果返回的是false,則說明該配置為跳過,並將skipped設置為true.由於此時調用的是OnClassCondition,其判斷邏輯為獲得AutoConfigurationMetadata中配置的ConditionalOnClass,如果不會空,則依次遍歷之,看是否在當前類路徑下存在.如果不匹配的話,則調用ConditionEvaluationReport#ecordConditionEvaluation 進行記錄.由於此時AutoConfigurationMetadata 什么都沒有配置,因此此步驟相當於空操作,最終會在第2步返回
如果skipped 等於false,則直接返回configurations,說明沒有配置是需要跳過的
依次遍歷candidates,如果該配置是不進行跳過的,則添加至result中進行返回
在ConfigurationClassParser#processConfigurationClass進行解析加載配置類時,會調用ConditionEvaluator#shouldSkip,在該方法中,會因此遍歷配置類配置的@Conditional所對應的處理類.此時,如果處理類是SpringBootCondition的子類的話,就會調用ConditionEvaluationReport進行記錄匹配結果. 代碼如下:
private void recordEvaluation(ConditionContext context, String classOrMethodName, ConditionOutcome outcome) { if (context.getBeanFactory() != null) { ConditionEvaluationReport.get(context.getBeanFactory()) .recordConditionEvaluation(classOrMethodName, this, outcome); } }
LiquibaseEndpoint
默認不生效,這里就不進行分析了
BeansEndpoint
BeansEndpoint的作用 –> 暴露關於beans的json視圖.如果Environment 中設置了spring.liveBeansView.mbeanDomain,則所有spring 上下文的bean都會展示.否則只會展示當前的上下文中的bean.默認是沒有配置的
BeansEndpoint,構造器如下:
public BeansEndpoint() { super("beans"); }
id為beans,默認為敏感
BeansEndpoint的字段如下:
// 繼承自LiveBeansView,用於生成json格式的數據 private final HierarchyAwareLiveBeansView liveBeansView = new HierarchyAwareLiveBeansView(); // json 解析器對象 private final JsonParser parser = JsonParserFactory.getJsonParser();
由於BeansEndpoint實現了ApplicationContextAware接口,因此當前初始化時,會調用其setApplicationContext方法,代碼如下:
@Override public void setApplicationContext(ApplicationContext context) throws BeansException { if (context.getEnvironment() .getProperty(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME) == null) { this.liveBeansView.setLeafContext(context); } }
如果沒有設置spring.liveBeansView.mbeanDomain的屬性,則將HierarchyAwareLiveBeansView中的leafContext設置為傳入的ApplicationContext(通常是當前應用所對應的上下文)
invoke實現如下:
@Override public List<Object> invoke() { return this.parser.parseList(this.liveBeansView.getSnapshotAsJson()); }
調用HierarchyAwareLiveBeansView#getSnapshotAsJson 生成json串.代碼如下:
public String getSnapshotAsJson() { if (this.leafContext == null) { return super.getSnapshotAsJson(); } // 2. 將leafContext的整個繼承關系都添加到contexts中,即:如果給定的leafContext 存在父context,則一直遞歸的添加至contexts // 直至頂級容器,然后調用LiveBeansView#generateJson 來生成json串 return generateJson(getContextHierarchy()); }
如果leafContext 等於null,則調用LiveBeansView#getSnapshotAsJson來生成json串.一般都會執行第2步
將leafContext的整個繼承關系都添加到contexts中,即:如果給定的leafContext 存在父context,則一直遞歸的添加至contexts.代碼如下:
private Set<ConfigurableApplicationContext> getContextHierarchy() { Set<ConfigurableApplicationContext> contexts = new LinkedHashSet<ConfigurableApplicationContext>(); ApplicationContext context = this.leafContext; while (context != null) { contexts.add(asConfigurableContext(context)); context = context.getParent(); } return contexts; }
將leafContext的整個繼承關系都添加到contexts中,即:如果給定的leafContext 存在父context,則一直遞歸的添加至contexts直至頂級容器。
調用LiveBeansView#generateJson 來生成json串.在該方法中沒有使用第3方的json解析庫的目的是為了避免對其進行依賴.返回的格式為數組格式.代碼如下:
由於BeansEndpoint聲明了@ConfigurationProperties(prefix = “endpoints.beans”),因此可以通過如下屬性來配置:
endpoints.beans.id
endpoints.beans.sensitive
endpoints.beans.enabled
自動化配置:
beansEndpoint 是在EndpointAutoConfiguration 中進行配置的,代碼如下:
@Bean @ConditionalOnMissingBean public BeansEndpoint beansEndpoint() { return new BeansEndpoint(); }
@Bean –> 注冊1個id為 beansEndpoint,類型為BeansEndpoint的bean
@ConditionalOnMissingBean–> 當beanFactory中不存在BeansEndpoint類型的bean時生效
ConfigurationPropertiesReportEndpoint
作用:–>暴露被@ConfigurationProperties 注解的bean的屬性.為了保護數據,將敏感數據進行了脫敏
字段:
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter"; // 數據脫敏 private final Sanitizer sanitizer = new Sanitizer(); // 通過實現ApplicationContextAware 進行自動注入 private ApplicationContext context; public ConfigurationPropertiesReportEndpoint() { super("configprops"); }
invoke 實現:
public Map<String, Object> invoke() { return extract(this.context); }
調用:
protected Map<String, Object> extract(ApplicationContext context) { // Serialize beans into map structure and sanitize values ObjectMapper mapper = new ObjectMapper(); // 1. 配置ObjectMapper的屬性 configureObjectMapper(mapper); // 2. 抽取數據 return extract(context, mapper); }
對ObjectMapper 進行配置:
代碼如下:
protected void configureObjectMapper(ObjectMapper mapper) { mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false); applyConfigurationPropertiesFilter(mapper); applySerializationModifier(mapper); }
SerializationFeature.FAIL_ON_EMPTY_BEANS –> 設置當對於給定的類型無法訪問時如何處理,true–> 拋出異常,false–> 返回null
SerializationFeature.WRITE_NULL_MAP_VALUES –> 設置當map對於的value為null時如何處理,treu–>序列化,false–> 跳過
applyConfigurationPropertiesFilter 代碼如下:
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) { mapper.setAnnotationIntrospector( new ConfigurationPropertiesAnnotationIntrospector()); mapper.setFilterProvider(new SimpleFilterProvider() .setDefaultFilter(new ConfigurationPropertiesPropertyFilter())); }
設置AnnotationIntrospector為ConfigurationPropertiesAnnotationIntrospector.該類的作用是:獲得@JsonFilter注解的值,如果獲取不到,則返回configurationPropertiesFilter.代碼如下:
private static class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector { @Override public Object findFilterId(Annotated a) { // 1. 獲得@JsonFilter注解的值,如果獲取不到,則返回 configurationPropertiesFilter Object id = super.findFilterId(a); if (id == null) { id = CONFIGURATION_PROPERTIES_FILTER_ID; } return id; } }
設置默認的過濾器為ConfigurationPropertiesPropertyFilter,該類的作用是進行如下規則的過濾:
類名以$$開頭的被過濾掉
自我引用的字段被過濾掉
當在序列化時拋出異常時過濾掉
代碼如下:
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer) throws Exception { if (writer instanceof BeanPropertyWriter) { try { // 1. 自我引用的字段被過濾掉 if (pojo == ((BeanPropertyWriter) writer).get(pojo)) { if (logger.isDebugEnabled()) { logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName() + "' as it is self-referential"); } return; } } catch (Exception ex) { // 2. 當在序列化時拋出異常時過濾掉 if (logger.isDebugEnabled()) { logger.debug("Skipping '" + writer.getFullName() + "' on '" + pojo.getClass().getName() + "' as an exception " + "was thrown when retrieving its value", ex); } return; } } // 3. 序列化字段 super.serializeAsField(pojo, jgen, provider, writer); }
設置序列化工廠中的方法序列化器為GenericSerializerModifier.代碼如下:
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) { List<BeanPropertyWriter> result = new ArrayList<BeanPropertyWriter>(); for (BeanPropertyWriter writer : beanProperties) { boolean readable = isReadable(beanDesc, writer); if (readable) { result.add(writer); } } return result; }
依次遍歷beanProperties,如果可讀的話,則添加至result中.可讀的判斷邏輯如下:
根據bean 的類型和屬性的類型 找出對應的set方法
如果set方法 存在或者是同一包下的類或者是map或者集合的子類,則返回true
調用extract方法抽取數據.代碼如下:
private Map<String, Object> extract(ApplicationContext context, ObjectMapper mapper) { Map<String, Object> result = new HashMap<String, Object>(); // 1. 獲得beanFactory中ConfigurationBeanFactoryMetaData類型的bean ConfigurationBeanFactoryMetaData beanFactoryMetaData = getBeanFactoryMetaData( context); // 2. 獲得被@ConfigurationProperties注解的bean,key--> bean的id,value --> bean Map<String, Object> beans = getConfigurationPropertiesBeans(context, beanFactoryMetaData); // 3. 依次遍歷beans for (Map.Entry<String, Object> entry : beans.entrySet()) { String beanName = entry.getKey(); Object bean = entry.getValue(); Map<String, Object> root = new HashMap<String, Object>(); String prefix = extractPrefix(context, beanFactoryMetaData, beanName, bean); // 3.1 獲得@ConfigurationProperties注解的前綴,添加至root中,key-->prefix,value-->@ConfigurationProperties注解的前綴 root.put("prefix", prefix); // 3.2 root.put("properties", sanitize(prefix, safeSerialize(mapper, bean, prefix))); // 3.3 添加至result中,key--> bean id,value-->map{prefix=xx,properties=xx} result.put(beanName, root); } // 4. 如果ApplicationContext存在父容器的,則遞歸調用extract提取數據,key為parent. if (context.getParent() != null) { result.put("parent", extract(context.getParent(), mapper)); } return result; }
獲得beanFactory中ConfigurationBeanFactoryMetaData類型的bean
獲得被@ConfigurationProperties注解的bean,key–> bean的id,value –> bean
依次遍歷beans
獲得@ConfigurationProperties注解的前綴,添加至root中,key–>prefix,value–>@ConfigurationProperties注解的前綴
對數據進行脫敏.代碼如下:
private Map<String, Object> sanitize(String prefix, Map<String, Object> map) { for (Map.Entry<String, Object> entry : map.entrySet()) { String key = entry.getKey(); String qualifiedKey = (prefix.isEmpty() ? prefix : prefix + ".") + key; Object value = entry.getValue(); if (value instanceof Map) { // 1. 如果對應的屬性值為Map,List 則遞歸調用sanitize 進行數據脫敏 map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value)); } else if (value instanceof List) { map.put(key, sanitize(qualifiedKey, (List<Object>) value)); } else { // 2. 如果屬性名包含password", "secret", "key", "token", ".*credentials.*", "vcap_services,則將其替換為****** value = this.sanitizer.sanitize(key, value); value = this.sanitizer.sanitize(qualifiedKey, value); map.put(key, value); } } return map; }
如果對應的屬性值為Map,List 則遞歸調用sanitize 進行數據脫敏
否則如果屬性名包含password, secret, key, token, .*credentials.*, vcap_services,則將其替換為**
添加至result中,key–> bean id,value–>map{prefix=xx,properties=xx}
如果ApplicationContext存在父容器的,則遞歸調用extract提取數據,key為parent.
由於ConfigurationPropertiesReportEndpoint 被@ConfigurationProperties(prefix = “endpoints.configprops”)注解,因此可通過如下屬性配置:
endpoints.configprops.id=configprops endpoints.configprops.sensitive=true endpoints.configprops.enabled=true endpoints.configprops.keys-to-sanitize=password,secret
之所以可以通過 endpoints.configprops.keys-to-sanitize 進行配置,是因為ConfigurationPropertiesReportEndpoint聲明了如下方法:
public void setKeysToSanitize(String... keysToSanitize) { this.sanitizer.setKeysToSanitize(keysToSanitize); }
自動化配置(在EndpointAutoConfiguration中配置):
代碼如下:
@Bean @ConditionalOnMissingBean public ConfigurationPropertiesReportEndpoint configurationPropertiesReportEndpoint() { return new ConfigurationPropertiesReportEndpoint(); }
@Bean –> 注冊1個id為configurationPropertiesReportEndpoint,類型為ConfigurationPropertiesReportEndpoint的Bean
@ConditionalOnMissingBean –> beanFactory中不存在ConfigurationPropertiesReportEndpoint類型的bean時生效
TraceEndpoint
作用:–> 該端點用來返回基本的HTTP跟蹤信息。默認情況下,跟蹤信息的存儲采用org.springframework.boot.actuate.trace.InMemoryTraceRepository實現的內存方式,始終保留最近的100條請求記錄.其中,返回的Trace定義如下:
public final class Trace { // 時間戳 private final Date timestamp; // 保存用於分析上下文信息,例如HTTP頭 private final Map<String, Object> info; public Trace(Date timestamp, Map<String, Object> info) { super(); Assert.notNull(timestamp, "Timestamp must not be null"); Assert.notNull(info, "Info must not be null"); this.timestamp = timestamp; this.info = info; } public Date getTimestamp() { return this.timestamp; } public Map<String, Object> getInfo() { return this.info; } }
字段:
private final TraceRepository repository;
這里我們有必要說明一下TraceRepository:
TraceRepository 是用來保存Trace的. 接口定義如下:
public interface TraceRepository {
// 返回保存的Trace
List<Trace> findAll();
// 進行添加
void add(Map<String, Object> traceInfo);
}
TraceRepository 只要1個實現–>InMemoryTraceRepository.其字段如下:
// 容量為100 private int capacity = 100; // 是否倒序展示,默認為false private boolean reverse = true; // 容器,用於保存Trace private final List<Trace> traces = new LinkedList<Trace>();
findAll只需簡單的返回保存的traces即可.實現如下:
public List<Trace> findAll() {
synchronized (this.traces) {
return Collections.unmodifiableList(new ArrayList<Trace>(this.traces));
}
}
add,代碼如下:
public void add(Map<String, Object> map) {
// 1. 實例化Trace,時間戳為當前時間
Trace trace = new Trace(new Date(), map);
synchronized (this.traces) {
// 2. 如果traces中的容量大於等於了閾值,則進行刪除.如果reverse等於true 則刪除最后1個,否則,刪除第1個
while (this.traces.size() >= this.capacity) {
this.traces.remove(this.reverse ? this.capacity - 1 : 0);
}
// 3. 進行添加,如果reverse等於true 則添加至第1個,否則,添加至最后
if (this.reverse) {
this.traces.add(0, trace);
}
else {
this.traces.add(trace);
}
}
}
實例化Trace,時間戳為當前時間
如果traces中的容量大於等於了閾值,則進行刪除.如果reverse等於true 則刪除最后1個,否則,刪除第1個
進行添加,如果reverse等於true 則添加至第1個,否則,添加至最后
TraceRepository 是在何處配置的呢?
在TraceRepositoryAutoConfiguration中,代碼如下:
@ConditionalOnMissingBean(TraceRepository.class)
@Bean
public InMemoryTraceRepository traceRepository() {
return new InMemoryTraceRepository();
}
@Bean –> 注冊1個id為traceRepository,類型為InMemoryTraceRepository的bean
@ConditionalOnMissingBean(TraceRepository.class)–> 當beanFactory中不存在TraceRepository類型的bean時生效
invoke 實現:
@Override
public List<Trace> invoke() {
return this.repository.findAll();
}
只需調用TraceRepository# findAll,返回保存的Trace即可.
屬性配置,因此其被@ConfigurationProperties(prefix = “endpoints.trace”)注解,因此可以通過如下屬性進行配置:
endpoints.trace.id=trace
endpoints.trace.sensitive=true
endpoints.trace.enabled=true
自動裝配:
在EndpointAutoConfiguration中進行了裝配,代碼如下:
@Bean
@ConditionalOnMissingBean
public TraceEndpoint traceEndpoint() {
return new TraceEndpoint(this.traceRepository == null
? new InMemoryTraceRepository() : this.traceRepository);
}
@Bean–> 注冊1個id為traceEndpoint,類型為TraceEndpoint的bean
@ConditionalOnMissingBean–> 當beanFactory中不存在TraceEndpoint類型的bean時生效
這里提1個問題,TraceEndpoint 是通過TraceRepository獲取Trace,那么TraceRepository中的Trace是如何保存的呢?
答案:
通過WebRequestTraceFilter(Filter)來實現. WebRequestTraceFilter實現了Ordered接口,指定了其在過濾器鏈中的順序,代碼如下:
private int order = Ordered.LOWEST_PRECEDENCE - 10;
public int getOrder() {
return this.order;
}
WebRequestTraceFilter中的字段如下:
private static final Log logger = LogFactory.getLog(WebRequestTraceFilter.class);
// debug時使用.如果啟用的話,並且log的trace級別可用的話,則打印請求頭信息,默認為false
private boolean dumpRequests = false;
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final TraceRepository repository;
private ErrorAttributes errorAttributes;
private final TraceProperties properties;
其中TraceProperties的定義如下:
@ConfigurationProperties(prefix = "management.trace")
public class TraceProperties {
private static final Set<Include> DEFAULT_INCLUDES;
static {
Set<Include> defaultIncludes = new LinkedHashSet<Include>();
defaultIncludes.add(Include.REQUEST_HEADERS);
defaultIncludes.add(Include.RESPONSE_HEADERS);
defaultIncludes.add(Include.COOKIES);
defaultIncludes.add(Include.ERRORS);
defaultIncludes.add(Include.TIME_TAKEN);
DEFAULT_INCLUDES = Collections.unmodifiableSet(defaultIncludes);
}
private Set<Include> include = new HashSet<Include>(DEFAULT_INCLUDES);
public Set<Include> getInclude() {
return this.include;
}
public void setInclude(Set<Include> include) {
this.include = include;
}
}
默認配置的是Include.REQUEST_HEADERS, Include.RESPONSE_HEADERS, Include.COOKIES ,Include.ERRORS, Include.TIME_TAKEN. 可以通過如下進行配置
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS
1
可選值為org.springframework.boot.actuate.trace.TraceProperties.Include.這里就不在貼出了
WebRequestTraceFilter 繼承了OncePerRequestFilter,因此只需實現doFilterInternal即可,代碼如下:
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
long startTime = System.nanoTime();
// 1. 獲得trace
Map<String, Object> trace = getTrace(request);
// 2. 打印日志-->如果log的trace級別可用的話並且dumpRequests等於true,則打印請求頭信息,默認為false
logTrace(request, trace);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
try {
// 3. 繼續過濾器鏈的過濾,最后獲得響應狀態
filterChain.doFilter(request, response);
status = response.getStatus();
}
finally {
// 4. 添加Http請求耗時統計-->如果TraceProperties中配置了Include#TIME_TAKEN(默認配置了),則添加到trace中,key為timeTaken,value-->當前時間-開始時間的毫秒值
addTimeTaken(trace, startTime);
// 5. 添加響應頭信息
enhanceTrace(trace, status == response.getStatus() ? response
: new CustomStatusResponseWrapper(response, status));
// 6. 添加至TraceRepository 中
this.repository.add(trace);
}
}
記錄開始時間
獲得trace.代碼如下:
protected Map<String, Object> getTrace(HttpServletRequest request) {
// 1. 獲得HttpSession
HttpSession session = request.getSession(false);
// 2. 獲得javax.servlet.error.exception,所對應的異常--> 當spring mvc 出現異常時,會加入到javax.servlet.error.exception中
Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception");
// 3. 獲得Principal,如果返回null,說明沒有該請求沒有進行驗證
Principal userPrincipal = request.getUserPrincipal();
Map<String, Object> trace = new LinkedHashMap<String, Object>();
Map<String, Object> headers = new LinkedHashMap<String, Object>();
// 4. 添加請求方法,請求路徑,請求頭到trace中
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
trace.put("headers", headers);
if (isIncluded(Include.REQUEST_HEADERS)) {
headers.put("request", getRequestHeaders(request));
}
// 省略掉默認不執行的代碼....
// 5. 如果有異常並且errorAttributes不等於null,則記錄error
if (isIncluded(Include.ERRORS) && exception != null
&& this.errorAttributes != null) {
trace.put("error", this.errorAttributes
.getErrorAttributes(new ServletRequestAttributes(request), true));
}
return trace;
}
繼續過濾器鏈的過濾,最后獲得響應狀態
添加Http請求耗時統計–>如果TraceProperties中配置了Include#TIME_TAKEN(默認配置了),則添加到trace中,key為timeTaken,value–>當前時間-開始時間的毫秒值.代碼如下:
private void addTimeTaken(Map<String, Object> trace, long startTime) {
long timeTaken = System.nanoTime() - startTime;
add(trace, Include.TIME_TAKEN, "timeTaken",
"" + TimeUnit.NANOSECONDS.toMillis(timeTaken));
}
添加響應頭信息.代碼如下:
protected void enhanceTrace(Map<String, Object> trace, HttpServletResponse response) {
if (isIncluded(Include.RESPONSE_HEADERS)) {
Map<String, Object> headers = (Map<String, Object>) trace.get("headers");
headers.put("response", getResponseHeaders(response));
}
}
getResponseHeaders 代碼如下:
private Map<String, String> getResponseHeaders(HttpServletResponse response) {
Map<String, String> headers = new LinkedHashMap<String, String>();
// 1. 依次遍歷響應頭,添加至headers 中,key--> 響應頭名,value-->響應頭對應的值
for (String header : response.getHeaderNames()) {
String value = response.getHeader(header);
headers.put(header, value);
}
// 2. 如果TraceProperties中沒有配置Include#COOKIES,則在headers中刪除key為Set-Cookie的值.默認是配置了的,因此不會刪除
if (!isIncluded(Include.COOKIES)) {
headers.remove("Set-Cookie");
}
// 3. 向headers 中添加 key--> status,value-->響應狀態碼
headers.put("status", "" + response.getStatus());
return headers;
}
遍歷響應頭,添加至headers 中,key–> 響應頭名,value–>響應頭對應的值
如果TraceProperties中沒有配置Include#COOKIES,則在headers中刪除key為Set-Cookie的值.默認是配置了的,因此不會刪除
向headers 中添加 key–> status,value–>響應狀態碼
添加至TraceRepository中
WebRequestTraceFilter 是如何配置的呢?
答案: 在TraceWebFilterAutoConfiguration中.代碼如下:
@Bean
@ConditionalOnMissingBean
public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) {
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository,
this.traceProperties);
if (this.errorAttributes != null) {
filter.setErrorAttributes(this.errorAttributes);
}
return filter;
}
@Bean–> 注冊1個id為webRequestLoggingFilter,類型為WebRequestTraceFilter的bean
@ConditionalOnMissingBean–> 當beanFactory中不存在WebRequestTraceFilter類型的bean時生效
同時由於TraceWebFilterAutoConfiguration聲明了如下注解:
@Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class })
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class)
@ConditionalOnProperty(prefix = "endpoints.trace.filter", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(TraceProperties.class)
@Configuration–> 配置類
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, ServletRegistration.class })–> 在當前的類路徑下存在Servlet.class, DispatcherServlet.class, ServletRegistration.class時生效
@AutoConfigureAfter(TraceRepositoryAutoConfiguration.class)–> 在TraceRepositoryAutoConfiguration之后進行自動裝配,這樣就可以自動注入TraceRepository
@ConditionalOnProperty(prefix = “endpoints.trace.filter”, name = “enabled”, matchIfMissing = true)–> 當配置有endpoints.trace.filter.enabled 等於true時生效,如果沒有配置,默認生效
@EnableConfigurationProperties(TraceProperties.class)–> 可以通過management.trace.include 進行配置.
轉:https://blog.csdn.net/qq_26000415/article/details/79060258