Conditional注解与SpringBoot组件扩展

今天,我们还是来补一下SpringBoot自动装配原理留下的坑:如何查看组件的源码并进行自定义扩展。

在聊这个之前,我们得先来学习一下@Conditional注解的使用,看过组件里一些自动配置类的小伙伴肯定会发现这样的现象:里面充斥了大量的@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

日志里面不再出现foo service init!!,说明FooService没有注入到容器中,Conditonal生效了

原理

这里说一下大致的过程:Spring在扫描到该Bean时,判断该Bean是否含有@Conditional注解,如果有,则使用反射实例化注解中的条件类,然后调用条件类的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!!");
    }
}

现在,在注解中设值为true就表示该Bean生效,false则跳过

自定义注解中的变量做成任意属性的,只要能和Conditional类进行配套使用就行

比如SpringBoot中的ConditionalOnClass注解,里面的变量是个class数组,Contional类中的逻辑则为取出变量中的class,判断calss是否存在,存在则match,否则跳过

SpringBoot中的所有Conditional注解

我们已经学会了@Conditional的使用方式,现在,就来看看SpringBoot中为我们提供了哪些Conditional注解吧

SpringBoot中内置的注解全在这个包下面

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'}"), 表示环境变量中存在a并且a=1时才会生效

ConditionalOnMissingBean

当容器中不包含指定的Bean时生效,与ConditionalOnBean逻辑相反

ConditionalOnMissingClass

当项目中不存在指定的Class时生效

ConditionalOnProperty

当指定的属性有指定的值时生效

如@ConditionalOnProperty(name = "a", havingValue = "1"),表示环境变量中存在a并且a=1时才会生效

但是这个注解还有个变量matchIfMissing,表示环境变量中没有这个属性也生效

如@ConditionalOnProperty(name = "a", havingValue = "1", matchIfMissing = true)

matchIfMissing默认为false

SpringBoot组件扩展

终于到SpringBoot组件扩展的事了,不容易呀

回到问题:为什么在讲SpringCloud Gateway时我能给出自定义异常处理的实现方式?

如果小伙伴没有看过这篇文章也没有关系,我这里主要是讲思路,可以应用到任何的案例上

我觉得其实组件扩展的难点不在于怎么扩展,难点是怎么找到这个切入点,也就是找到源码中那一块处理逻辑

这个其实和我们写项目一样,你想要在同事的代码上加一块功能,压根就不需要清楚这段代码的上下文,只要知道这块代码是干嘛的就行了。

寻找切入点

之前讲过,springboot中所有spring-boot-starter-x的组件配置都是放在spring-boot-autoconfigura的组件中,那我们就来找找有没有这样的异常处理的自动配置类呢?

一直往下翻,你会看到这样一个配置类

image

咦,SpringCloud Gateway不就是用WebFlux写的嘛,这个类名还叫ErrorWebFluxAutoConfiguration,那么很有可能就是它了

打开这个类看看

image

这里注意两个点,一个是这个Bean上加了ConditionalOnMissingBean注解,第二个就是它返回的是个DefaultErrorWebExceptionHandler

我们再来看看DefaultErrorWebExceptionHandler中的处理逻辑

image
image

renderErrorView中的逻辑我们不看,因为我们的重点是怎么返回前端一个JSON格式的数据,而不是返回一个页面

这个时候可以getRoutingFunction方法中打个断点,然后运行一下,看看异常是不是真的由这里处理的,我这里就不演示了

整理扩展思路

现在,我们已经知道了出现异常时进行处理的是这个方法

image

然后我们不想要返回前端的是个页面,只想要返回一个JSON格式的信息给前端

所以我们需要把renderErrorView的逻辑砍掉,只保留renderErrorResponse的逻辑

那么我们是不是可以继承DefaultErrorWebExceptionHandler然后重写这个方法呢?

如果只重写这个方法的话还有个问题,那就是renderErrorResponse这个方法返回的数据也是Spring提供的,如果我们要自定义JSON数据的话还需要重写renderErrorResponse方法

方法重写完之后,我们要做的最后件事就是把我们自定义的ExceptionHandler替换成DefaultErrorWebExceptionHandler,这个也十分简单,因为我们已经注意到在ErrorWebFluxAutoConfiguration配置类中,注入ErrorWebExceptionHandler时有个@ConditionalOnMissingBean注解,所以我们直接将自定义的ExceptionHandler放到容器中就可以了

总结一下需要做的事情

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即可

小结

今天又是个补坑之作,介绍了Conditional注解的使用,以及SpringBoot中内置的所有@Conditional注解的作用,最后,给小伙伴们提供了一份SpringBoot组件扩展的思路。

希望大家有所收获~

想要了解更多精彩内容,欢迎关注公众号:程序员阿鉴

个人博客空间:https://zijiancode.cn

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容