Conditional注解與SpringBoot組件擴展

今天,我們還是來補一下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入門時天天寫這個類

啟動測試

image

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

image

日志里面不再出現(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)置的注解全在這個包下面

image

官方文檔也對它們進行了詳細的說明: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的組件中,那我們就來找找有沒有這樣的異常處理的自動配置類呢?

一直往下翻,你會看到這樣一個配置類

image

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

打開這個類看看

image

這里注意兩個點,一個是這個Bean上加了ConditionalOnMissingBean注解,第二個就是它返回的是個DefaultErrorWebExceptionHandler

我們再來看看DefaultErrorWebExceptionHandler中的處理邏輯

image
image

renderErrorView中的邏輯我們不看,因為我們的重點是怎么返回前端一個JSON格式的數(shù)據(jù),而不是返回一個頁面

這個時候可以getRoutingFunction方法中打個斷點,然后運行一下,看看異常是不是真的由這里處理的,我這里就不演示了

整理擴展思路

現(xiàn)在,我們已經(jīng)知道了出現(xiàn)異常時進行處理的是這個方法

image

然后我們不想要返回前端的是個頁面,只想要返回一個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.重寫getRoutingFunctionrenderErrorResponse方法

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.重寫getRoutingFunctionrenderErrorResponse方法

@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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容