Autoconfiguration詳解——自動註入配置參數 一、 理解自動裝配bean 1. 常用註解 @AutoConfiguration(每個配置類都要加上) Class<?>[] after() default {}; Class<?>[] before() default {}; 以上兩個配 ...
目錄
Autoconfiguration詳解——自動註入配置參數
一、 理解自動裝配bean
1. 常用註解
@AutoConfiguration
(每個配置類都要加上)Class<?>[] after() default {};
Class<?>[] before() default {};
- 以上兩個配置可以控制載入順序;
- 不需要再增加
@Configuration
註解;
@AutoConfigureBefore
and@AutoConfigureAfter
@Configuration
@Conditional
(後面會詳細講到)@ConditianalOnClass
@ConditionalOnMissingClass
@ConditionalOnWebApplication
:只在web應用中載入;
@EnableConfigurationProperties
:配置文件參數內容,參照類RedisProperties
;@ConfigurationProperties(prefix = "spring.redis")
,該註解展示了配置文件首碼;
@DependsOn
:列舉一些前置的註入bean,以備用,用在類上需要有@Component
自動掃描的時候才能生效;- 實際上控制了bean載入的順序,優先載入指定的bean,然後載入當前bean;
- 銷毀的時候,註解的bean優先與於依賴的bean銷毀;
2. 定位自動裝配的候選類
springboot
框架會自動掃描 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
進行引入,所以需要自動註入的Configuration
文件都寫在這個文件中。每個class
一行。
這裡本質上是一個自動版的@Import。
示例:
# comments
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
如果需要引入特定的
Component
,使用@Import註解。
3. 條件註解
在所有自動裝配類出現的地方,我們都因該時加上@Conditional
註解,允許使用的開發人員覆蓋自動裝配的bean,當然他們也可以選擇什麼也不做,使用預設的配置。
Spring Boot 提供了一些條件註解,可以註解在@Configuration
類或者@Bean
方法上。
3.1 有關類的判斷
對於@Configuration
類來說,@ConditionalOnClass
和@ConditionalOnMissingClass
代表了在指定類存在或者不存在的時候進行載入。因為實際上註解的元數據使用ASM技術進行解析,所以可以使用value
參數來指定特定的類的class對象(可以是多個),或者使用name
參數來指定特定的類名(可以是多個),兩種方式所指向的類即使不存在也不影響正常執行。
當@Bean
方法返回值是條件註解的的目標之時,可能會因為JVM載入順序的問題導致載入失敗,上文提到的兩個註解可以用在@Bean
方法上。
3.2 有關bean的判斷
@ConditionalOnBean
和@ConditionalOnMissingBean
,代表在指定bean存在或者不存在時載入。value
參數可以指定bean的class(多個),name
可以指定bean的名稱(多個)。search
參數允許你限制ApplicationContext
即應用上下文的搜索範圍,可選當前上下文,繼承上層,或者是全部(預設)。
在@Bean
方法上使用時,預設參數為當前方法返回類型。
在使用@Bean註解時,建議使用具體類型而不是父類型進行指代。
3.3 配置條件
@ConditionalOnProperty
,指定配置項文件(例如dev,pro),prefix
屬性規定了配置首碼,name
屬性指定了應該被檢查的參數。預設,所有存在且不等於false
的參數都會被匹配到,你也可以使用havingValue
和matchIfMissing
屬性闖將更多的校驗。
例子:@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
;
屬性名 | 類型 | 解析 |
---|---|---|
name | String[] name() default {}; | 配置項全稱,如果有prefix,可以省略prefix中的首碼部分 |
prefix | String prefix() default ""; | 統一的配置項首碼 |
havingValue | String havingValue() default ""; | 配置項需要匹配的內容,如果沒有指定,那麼配置的值等於false 時結果為false,否則結果都為true |
matchIfMissing | boolean matchIfMissing() default false; | 配置項不存在時的配置,預設為false |
3.4 源文件條件
@ConditionalOnResource
,指定源文件存在時引入。
例如:@ConditionalOnResource(resources = {"classpath:test.log"})
;
3.5 web 應用條件
@ConditionalOnWebApplication
和 @ConditionalOnNotWebApplication
,web應用或者不是web應用時啟用,以下部分只要滿足一個條件即為web 應用。
servlet-based
web 應用特點:
- 使用 Spring
WebApplicationContext
; - 定義了一個
session
作用域的bean; - 有一個
WebApplicationContext
;
reactive
web 應用特點:
-
使用了
ReactiveWebApplicationContext
; -
有一個
ConfigurableReactiveWebEnvironment
;
ConditionalOnWarDeployment
,僅限於使用war進行部署的場景,在嵌入式tomcat的場景里不會啟用;
3.6 Spel表單式條件
ConditionalOnWarDeployment
,使用Spel表達式返回結果進行判斷。
註意:在表達式中引用一個bean會導致這個bean非常早的被載入,此時還沒有進行預載入(例如配置項的綁定),可能會導致不完成的載入。
二、自動註入配置基礎
@EnableConfigurationProperties(CommonRedisProperties.class)
註解configuration
類;@ConfigurationProperties(prefix = "myserver")
註解配置文件類,prefix標明配置文件的首碼;public RedisTemplate<String, Object> getRedisTemplate(CommonRedisProperties properties, RedisConnectionFactory redisConnectionFactory)
,加到需要使用的參數中即可;META-INF
目錄下添加additional-spring-configuration-metadata.json
文件,格式如下
{
"groups": [
{
"name": "server",
"type": "com.huawei.workbenchcommon.redis.CommonRedisProperties",
"sourceType": "com.huawei.workbenchcommon.redis.CommonRedisProperties"
}
],
"properties": [
{
"name": "myserver.database",
"type": "java.lang.String",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
}
]
}
三、註釋切麵 @Metrics
1. 註解@Metrics
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Metrics {
/**
* 在方法成功執行後打點,記錄方法的執行時間發送到指標系統,預設開啟
*/
boolean recordSuccessMetrics() default true;
/**
* 在方法成功失敗後打點,記錄方法的執行時間發送到指標系統,預設開啟
*/
boolean recordFailMetrics() default true;
/**
* 通過日誌記錄請求參數,預設開啟
*/
boolean logParameters() default true;
/**
* 通過日誌記錄方法返回值,預設開啟
*/
boolean logReturn() default true;
/**
* 出現異常後通過日誌記錄異常信息,預設開啟
*/
boolean logException() default true;
/**
* 出現異常後忽略異常返回預設值,預設關閉
*/
boolean ignoreException() default false;
2. 切麵MetricsAspect
@Aspect
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MetricsAspect {
/**
* 讓Spring幫我們註入ObjectMapper,以方便通過JSON序列化來記錄方法入參和出參
*/
@Resource
private ObjectMapper objectMapper;
/**
* 實現一個返回Java基本類型預設值的工具。其實,你也可以逐一寫很多if-else判斷類型,然後手動設置其預設值。
* 這裡為了減少代碼量用了一個小技巧,即通過初始化一個具有1個元素的數組,然後通過獲取這個數組的值來獲取基本類型預設值
*/
private static final Map<Class<?>, Object> DEFAULT_VALUES = Stream
.of(boolean.class, byte.class, char.class, double.class, float.class, int.class, long.class, short.class)
.collect(toMap(clazz -> clazz, clazz -> Array.get(Array.newInstance(clazz, 1), 0)));
public static <T> T getDefaultValue(Class<T> clazz) {
//noinspection unchecked
return (T) DEFAULT_VALUES.get(clazz);
}
/**
* 標記了Metrics註解的方法進行匹配
*/
@Pointcut("@annotation(com.common.config.metrics.annotation.Metrics)")
public void withMetricsAnnotationMethod() {
}
/**
* within指示器實現了匹配那些類型上標記了@RestController註解的方法
* 註意這裡使用了@,標識了對註解標註的目標進行切入
*/
@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
public void controllerBean() {
}
@Pointcut("@within(com.common.config.metrics.annotation.Metrics)")
public void withMetricsAnnotationClass() {
}
@Around("controllerBean() || withMetricsAnnotationMethod() || withMetricsAnnotationClass()")
public Object metrics(ProceedingJoinPoint pjp) throws Throwable {
// 通過連接點獲取方法簽名和方法上Metrics註解,並根據方法簽名生成日誌中要輸出的方法定義描述
MethodSignature signature = (MethodSignature) pjp.getSignature();
Metrics metrics = signature.getMethod().getAnnotation(Metrics.class);
String name = String.format("【%s】【%s】", signature.getDeclaringType().toString(), signature.toLongString());
if (metrics == null) {
@Metrics
final class InnerClass {
}
metrics = InnerClass.class.getAnnotation(Metrics.class);
}
// 嘗試從請求上下文獲得請求URL
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
name += String.format("【%s】", request.getRequestURL().toString());
}
// 入參的日誌輸出
if (metrics.logParameters()) {
log.info(String.format("【入參日誌】調用 %s 的參數是:【%s】", name, objectMapper.writeValueAsString(pjp.getArgs())));
}
// 連接點方法的執行,以及成功失敗的打點,出現異常的時候記錄日誌
Object returnValue;
Instant start = Instant.now();
try {
returnValue = pjp.proceed();
if (metrics.recordSuccessMetrics()) {
// 在生產級代碼中,應考慮使用類似Micrometer的指標框架,把打點信息記錄到時間序列資料庫中,實現通過圖表來查看方法的調用次數和執行時間,
log.info(String.format("【成功打點】調用 %s 成功,耗時:%d ms", name, Duration.between(start, Instant.now()).toMillis()));
}
} catch (Exception ex) {
if (metrics.recordFailMetrics()) {
log.info(String.format("【失敗打點】調用 %s 失敗,耗時:%d ms", name, Duration.between(start, Instant.now()).toMillis()));
}
if (metrics.logException()) {
log.error(String.format("【異常日誌】調用 %s 出現異常!", name), ex);
}
if (metrics.ignoreException()) {
returnValue = getDefaultValue(signature.getReturnType());
} else {
throw ex;
}
}
// 返回值輸出
if (metrics.logReturn()) {
log.info(String.format("【出參日誌】調用 %s 的返回是:【%s】", name, returnValue));
}
return returnValue;
}
3. 自動註入AutoConfiguration
@AutoConfiguration
@Slf4j
@EnableConfigurationProperties(MetricsProperties.class)
@ConditionalOnProperty(prefix = "common.metrics", name = {"keep-alive"}, havingValue = "true", matchIfMissing = true)
public class AspectAutoConfiguration {
public AspectAutoConfiguration() {
log.info("AspectAutoConfiguration initialize.");
}
@Bean
public MetricsAspect metricsAspect() {
return new MetricsAspect();
}
}
4. 配置文件MetricsProperties
@ConfigurationProperties(prefix = "common.metrics")
public class MetricsProperties {
public Boolean getKeepAlive() {
return keepAlive;
}
public void setKeepAlive(Boolean keepAlive) {
this.keepAlive = keepAlive;
}
private Boolean keepAlive = true;
}
5. 其它配置
配置自動註入
配置resource.META-INF.spring.org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,增加AspectAutoConfiguration
類路徑。
配置文件提示
{
"groups": [],
"properties": [
{
"name": "common.metrics.keepAlive",
"type": "java.lang.Boolean",
"sourceType": "com.common.config.metrics.properties.MetricsProperties"
}
]
}
四、自定義spring的profile限定註解
1. 註解@RunOnProfiles
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface RunOnProfiles {
/**
* Profile name array,eg,dev,pro.
*/
String[] value() default {};
/**
* Skip the code of the method of the class or method itself.
*/
boolean skip() default true;
}
2. 切麵RunOnProfilesAspect
@Aspect
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class RunOnProfilesAspect {
@Autowired
private ApplicationContext applicationContext;
@Pointcut("@annotation(com.common.config.profiles.annotation.RunOnProfiles)")
public void withAnnotationMethod() {
}
@Pointcut("@within(com.common.config.profiles.annotation.RunOnProfiles)")
public void withAnnotationClass() {
}
@Around("withAnnotationMethod() || withAnnotationClass()")
public Object runsOnAspect(ProceedingJoinPoint pjp) throws Throwable {
var activeArray = applicationContext.getEnvironment().getActiveProfiles();
MethodSignature signature = (MethodSignature) pjp.getSignature();
RunOnProfiles runOnProfiles = signature.getMethod().getAnnotation(RunOnProfiles.class);
if (runOnProfiles == null) {
return null;
}
var profilesArray = runOnProfiles.value();
if (profilesArray == null || profilesArray.length == 0) {
return pjp.proceed();
}
for (var profile : profilesArray) {
for (var p : activeArray) {
if (p.equals(profile)) {
return pjp.proceed();
}
}
}
return null;
}
}
3. 自動註入AutoConfiguration
@AutoConfiguration
@Slf4j
public class RunsOnProfilesAutoConfiguration {
public RunsOnProfilesAutoConfiguration() {
log.info("RunsOnProfilesAutoConfiguration initialize.");
}
@Bean
public RunOnProfilesAspect runsOnProfilesAspect() {
return new RunOnProfilesAspect();
}
}
4. 其它配置
配置自動註入
配置resource.META-INF.spring.org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,增加RunsOnProfilesAutoConfiguration
類路徑。
參考
[1] springboot doc configuration metadata