Conditional注解与SpringBoot组件扩展
今天,我们还是来补一下SpringBoot自动装配原理留下的坑:如何查看组件的源码并进行自定义扩展。
在聊这个之前,我们得先来学习一下@Conditional注解的使用,看过组件里一些自动配置类的小伙伴肯定会发现这样的现象:里面充斥了大量的@ConditionalOnXxxxx的注解,那么这些注解的用处是什么呢?
Conditional注解
Conditional注解是个条件注解,将该注解加在Bean上,当满足注解中所需要的条件时,这个Bean才会被启用。
例子
建立一个Spring项目,引入依赖
org.springframework.boot
spring-boot-starter
编写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生效了
原理【Conditional注解与SpringBoot组件扩展】这里说一下大致的过程: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 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则跳过SpringBoot中的所有Conditional注解 我们已经学会了@Conditional的使用方式,现在,就来看看SpringBoot中为我们提供了哪些Conditional注解吧
自定义注解中的变量做成任意属性的,只要能和Conditional类进行配套使用就行
比如SpringBoot中的ConditionalOnClass注解,里面的变量是个class数组,Contional类中的逻辑则为取出变量中的class,判断calss是否存在,存在则match,否则跳过
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 = "https://www.it610.com/article/1"),表示环境变量中存在a并且a=1时才会生效
但是这个注解还有个变量matchIfMissing,表示环境变量中没有这个属性也生效
如@ConditionalOnProperty(name = "a", havingValue = "https://www.it610.com/article/1", matchIfMissing = true)
matchIfMissing默认为falseSpringBoot组件扩展 终于到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.重写
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 getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}@NonNull
@Override
protected Mono 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 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;
}
}
这部分就是把源码里的那部分复制出来,然后把小结 今天又是个补坑之作,介绍了Conditional注解的使用,以及SpringBoot中内置的所有@Conditional注解的作用,最后,给小伙伴们提供了一份SpringBoot组件扩展的思路。DefaultErrorWebExceptionHandler
换成JsonExceptionHandler
即可
希望大家有所收获~
想要了解更多精彩内容,欢迎关注公众号:程序员阿鉴
个人博客空间:https://zijiancode.cn
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- Docker应用:容器间通信与Mariadb数据库主从复制
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量
- 第326天
- Shell-Bash变量与运算符
- 逻辑回归的理解与python示例
- Guava|Guava RateLimiter与限流算法
- SpringBoot调用公共模块的自定义注解失效的解决
- 我和你之前距离
- CGI,FastCGI,PHP-CGI与PHP-FPM