关于springboot:Conditional注解与SpringBoot组件扩展

34次阅读

共计 6549 个字符,预计需要花费 17 分钟才能阅读完成。

明天,咱们还是来补一下 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 入门时天天写这个类

启动测试

将 Conditonal 类中返回 ture,改为返回 false,再次测试

日志外面不再呈现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 中内置的注解全在这个包上面

官网文档也对它们进行了具体的阐明:https://docs.spring.io/spring…

阿鉴这里列举几个罕用的(其实看名字也晓得它们的作用是什么啦)

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 的组件中,那咱们就来找找有没有这样的异样解决的主动配置类呢?

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

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

关上这个类看看

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

咱们再来看看 DefaultErrorWebExceptionHandler 中的解决逻辑

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

这个时候能够 getRoutingFunction 办法中打个断点,而后运行一下,看看异样是不是真的由这里解决的,我这里就不演示了

整顿扩大思路

当初,咱们曾经晓得了出现异常时进行解决的是这个办法

而后咱们不想要返回前端的是个页面,只想要返回一个 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

正文完
 0