今天,我們還是來補一下SpringBoot自動裝配原理留下的坑:如何查看組件的源碼并進行自定義擴展。
在聊這個之前,我們得先來學習一下@Conditional注解的使用,看過組件里一些自動配置類的小伙伴肯定會發(fā)現(xiàn)這樣的現(xiàn)象:里面充斥了大量的@ConditionalOnXxxxx的注解,那么這些注解的用處是什么呢?
Conditional注解
Conditional注解是個條件注解,將該注解加在Bean上,當滿足注解中所需要的條件時,這個Bean才會被啟用。
例子
建立一個Spring項目,引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
編寫Conditional類
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* @author Zijian Liao
* @since 1.0.0
*/
public class FooConditional implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return true;
}
}
這里先不做任何操作,默認返回true
編寫一個Service用于測試
@Component
@Conditional(FooConditional.class)
public class FooService {
public FooService(){
System.out.println("foo service init!!");
}
}
在上面加上@Conditional注解,并指定使用我們自己的Conditional
編寫啟動類
@ComponentScan
@Configuration
public class ConditionalApplication {
public static void main(String[] args) {
new AnnotationConfigApplicationContext(ConditionalApplication.class);
}
}
這里采取了最原始的啟動方式,不知道還有沒有小伙伴記得學習Spring入門時天天寫這個類
啟動測試

將Conditonal類中返回ture,改為返回false,再次測試

日志里面不再出現(xiàn)
foo service init!!,說明FooService沒有注入到容器中,Conditonal生效了
原理
這里說一下大致的過程:Spring在掃描到該Bean時,判斷該Bean是否含有@Conditional注解,如果有,則使用反射實例化注解中的條件類,然后調(diào)用條件類的matchs方法,如果返回false,則跳過該Bean
感興趣的小伙伴可以看下這塊源碼:ConditionEvaluator#shouldSkip,或者與我交流也是可以的哈
進階
看完例子,有沒有有種好雞肋的感覺?因為單純的使用@Conditional注解里面只能傳入一個class,可操作性太小了,所以我們可以將它改造一下,改造方式如下:
編寫Conditional類
public class OnFooConditional implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 取出自定義注解ConditionalOnFoo中的所有變量
final Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnFoo.class.getName());
if (attributes == null) {
return false;
}
// 返回value的值
return (boolean) attributes.get("value");
}
}
編寫自定義條件注解
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnFooConditional.class)
public @interface ConditionalOnFoo {
boolean value();
}
這里將@Conditional注解加在自定義注解上,這樣我們的注解就成了一個有變量的條件注解
使用
@ConditionalOnFoo(true)
public class FooService {
public FooService(){
System.out.println("foo service init!!");
}
}
現(xiàn)在,在注解中設(shè)值為true就表示該Bean生效,false則跳過
自定義注解中的變量做成任意屬性的,只要能和Conditional類進行配套使用就行
比如SpringBoot中的ConditionalOnClass注解,里面的變量是個class數(shù)組,Contional類中的邏輯則為取出變量中的class,判斷calss是否存在,存在則match,否則跳過
SpringBoot中的所有Conditional注解
我們已經(jīng)學會了@Conditional的使用方式,現(xiàn)在,就來看看SpringBoot中為我們提供了哪些Conditional注解吧
SpringBoot中內(nèi)置的注解全在這個包下面

官方文檔也對它們進行了詳細的說明:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.condition-annotations
阿鑒這里列舉幾個常用的(其實看名字也知道它們的作用是什么啦)
ConditionalOnBean
當容器中包含指定的Bean時生效
如@ConditionalOnBean(RedisConnectionFactory.class), 當容器中存在RedisConnectionFactory的Bean時,使用了該注解的Bean才會生效
ConditionalOnClass
當項目中存在指定的Class時生效
ConditionalOnExpression
當其中的SpEL表達式返回ture時生效
如@ConditionalOnExpression("#{environment.getProperty('a') =='1'}"), 表示環(huán)境變量中存在a并且a=1時才會生效
ConditionalOnMissingBean
當容器中不包含指定的Bean時生效,與ConditionalOnBean邏輯相反
ConditionalOnMissingClass
當項目中不存在指定的Class時生效
ConditionalOnProperty
當指定的屬性有指定的值時生效
如@ConditionalOnProperty(name = "a", havingValue = "1"),表示環(huán)境變量中存在a并且a=1時才會生效
但是這個注解還有個變量matchIfMissing,表示環(huán)境變量中沒有這個屬性也生效
如@ConditionalOnProperty(name = "a", havingValue = "1", matchIfMissing = true)
matchIfMissing默認為false
SpringBoot組件擴展
終于到SpringBoot組件擴展的事了,不容易呀
回到問題:為什么在講SpringCloud Gateway時我能給出自定義異常處理的實現(xiàn)方式?
如果小伙伴沒有看過這篇文章也沒有關(guān)系,我這里主要是講思路,可以應(yīng)用到任何的案例上
我覺得其實組件擴展的難點不在于怎么擴展,難點是怎么找到這個切入點,也就是找到源碼中那一塊處理邏輯
這個其實和我們寫項目一樣,你想要在同事的代碼上加一塊功能,壓根就不需要清楚這段代碼的上下文,只要知道這塊代碼是干嘛的就行了。
尋找切入點
之前講過,springboot中所有spring-boot-starter-x的組件配置都是放在spring-boot-autoconfigura的組件中,那我們就來找找有沒有這樣的異常處理的自動配置類呢?
一直往下翻,你會看到這樣一個配置類

咦,SpringCloud Gateway不就是用WebFlux寫的嘛,這個類名還叫ErrorWebFluxAutoConfiguration,那么很有可能就是它了
打開這個類看看

這里注意兩個點,一個是這個Bean上加了ConditionalOnMissingBean注解,第二個就是它返回的是個DefaultErrorWebExceptionHandler
我們再來看看DefaultErrorWebExceptionHandler中的處理邏輯


renderErrorView中的邏輯我們不看,因為我們的重點是怎么返回前端一個JSON格式的數(shù)據(jù),而不是返回一個頁面
這個時候可以getRoutingFunction方法中打個斷點,然后運行一下,看看異常是不是真的由這里處理的,我這里就不演示了
整理擴展思路
現(xiàn)在,我們已經(jīng)知道了出現(xiàn)異常時進行處理的是這個方法

然后我們不想要返回前端的是個頁面,只想要返回一個JSON格式的信息給前端
所以我們需要把renderErrorView的邏輯砍掉,只保留renderErrorResponse的邏輯
那么我們是不是可以繼承DefaultErrorWebExceptionHandler然后重寫這個方法呢?
如果只重寫這個方法的話還有個問題,那就是renderErrorResponse這個方法返回的數(shù)據(jù)也是Spring提供的,如果我們要自定義JSON數(shù)據(jù)的話還需要重寫renderErrorResponse方法
方法重寫完之后,我們要做的最后件事就是把我們自定義的ExceptionHandler替換成DefaultErrorWebExceptionHandler,這個也十分簡單,因為我們已經(jīng)注意到在ErrorWebFluxAutoConfiguration配置類中,注入ErrorWebExceptionHandler時有個@ConditionalOnMissingBean注解,所以我們直接將自定義的ExceptionHandler放到容器中就可以了
總結(jié)一下需要做的事情
1.自定義ExceptionHandler繼承DefaultErrorWebExceptionHandler
2.重寫getRoutingFunction和renderErrorResponse方法
3.將自定義ExceptionHandler注入到Spring容器
編寫代碼
1.自定義ExceptionHandler繼承DefaultErrorWebExceptionHandler
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
2.重寫getRoutingFunction和renderErrorResponse方法
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
@NonNull
@Override
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Throwable throwable = getError(request);
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(BaseResult.failure(throwable.getMessage())));
}
BaseResult.failure(throwable.getMessage()) 就是我自己定義的result對象
3.將自定義ExceptionHandler注入到Spring容器
@Configuration
public class ExceptionConfiguration {
@Primary
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes, ServerProperties serverProperties, ResourceProperties resourceProperties,
ObjectProvider<ViewResolver> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
resourceProperties, serverProperties.getError(), applicationContext);
exceptionHandler.setViewResolvers(viewResolversProvider.orderedStream().collect(Collectors.toList()));
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
這部分就是把源碼里的那部分復制出來,然后把
DefaultErrorWebExceptionHandler換成JsonExceptionHandler即可
小結(jié)
今天又是個補坑之作,介紹了Conditional注解的使用,以及SpringBoot中內(nèi)置的所有@Conditional注解的作用,最后,給小伙伴們提供了一份SpringBoot組件擴展的思路。
希望大家有所收獲~
想要了解更多精彩內(nèi)容,歡迎關(guān)注公眾號:程序員阿鑒
個人博客空間:https://zijiancode.cn