明天,咱们还是来补一下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@Configurationpublic 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办法

@Overrideprotected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {  return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);}@NonNull@Overrideprotected 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容器

@Configurationpublic 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