1、springboot进阶
1.1、配置文件
1.1.1、默认配置
我们通常会使用yaml文件来配置配置文件,当然,使用$
符号能够满足一些我们的特殊需求。同时也贴出官方文档。
1.配置随机值
RandomValuePropertySource
对于随机值注入非常有用(比如在保密场景或者测试用例中)。它可以产生 integer、long、uuid 和 string。如下示例:
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number.less.than.ten=${random.int(10)}
my.number.in.range=${random.int[1024,65536]}
random.int*
语法为 OPEN value (,max) CLOSE
,OPEN,CLOSE
可为任意字符,value,max
为整数。如果使用了 max
,value
则为最小值,max
为最大值。
2.引用环境配置
server:
port: ${server.port:8080}
如上示例表示,我们会去读取启动命令参数,如环境命令,jvm启动命令中拿这个值。并使用:
分隔开,给一个默认8080的值。这在实际生产中是非常常见的配置。
如上图所示,idea中两个地方都可以配置环境,在jvm配置中需要使用-
分隔开:-server.port=8090
,在idea环境配置中直接配置即可:server.port=8090
。
1.1.2、配置注入
1.1.2.1、配置映射
如下所示,我们通常会使用@Value
来注入一些yaml配置中的配置信息,样例如下:
@Value("server.port")
private Integer port;
但如果我们需要注入很多配置,显然都使用@Value
不合适
我们可以使用@ConfigurationProperties
来对配置类实体进行映射。样例如下:
yaml配置
hy:
log:
enable: true
实体映射
@Setter
@Getter
@ConfigurationProperties("hy.log")
@RefreshScope //实时刷新配置文件
public class LogProperties {
/**
* 是否开启业务日志,默认为true
*/
private Boolean enable = Boolean.TRUE;
}
自此完成了通过实体类映射配置信息
值得注意的是
@ConfigurationProperties
中的value和prefix都用来表示prefix,也就是前缀- get和set方法是必须的,底层通过他们来实现注入
1.1.2.2、配置注入
配置映射之后,我们的配置实体类中就有值的,通常我们还会将其注入为bean。当然为了隔离和普通bean的区别,我们还是建议使用@EnableConfigurationProperties
来注入,样例如下:
@EnableConfigurationProperties({
LogProperties.class,Log4j2Properties.class
})
我们传入配置实体类的class类即可,@EnableConfigurationProperties
内部为class类型的数组,我们需要将所有的配置实体类的class类传入
1.1.2.3、使用配置
前面我们都注入bean了,使用常规的@Authwired
或者@Resource
注入即可
1.1.3、加载其他配置
当然,我们可以在其他位置加载配置实体。
比如我想在resources\production
目录下添加一个alipay.properties
文件,我们可以使用@PropertySource("classpath:/production/alipay.properties")
来加载配置类
我们也可以使用@ConfigurationProperties(prefix = "alipay")
来指定一下配置文件的前缀
1.2、springboot加载
1.2.1、加载优先级
我们可以使用@Order来控制bean加载顺序。浅看一下源码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
/**
* 默认是最低优先级,值越小优先级越高
*/
int value() default Ordered.LOWEST_PRECEDENCE;
}
如上所示,value为一个int类型的值,且越小,加载的优先级越高。我们也可以使用Ordered这个枚举类型进行控制。
使用场景:
- 安全框架如security等web过滤器
测试代码示例如下:
@Component
@Order(1)
public class BlackPersion implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("----BlackPersion----");
}
}
@Component
@Order(0)
public class YellowPersion implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("----YellowPersion----");
}
}
1.2.2、参数接收校验
@Validated
和@Valid
是我们常用来校验接收参数的两个注解,他们的区别如下:
- @Validated是spring提供的一个注解,而@Valid是jdk提供的,也就是java原生支持
- @Validated提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,而@Valid没有
- @Valid能用于成员属性,也就是能支持嵌套查询。而@Validated不能
下面注解可以在实体类字段上面定义,他们共同作用于@Validated和@Valid:
限制 | 说明 |
---|---|
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
1.2.2.1、样例
- 依赖
前面我们提到了@Validated
是spring官方提供的,我们可能需要引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- 通常我们会封装一个对其异常的拦截
package com.example.apps.advice;
import com.example.apps.result.ServiceResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
@RestControllerAdvice
public class GlobalControllerAdvice {
//对构造参数异常的拦截
@ExceptionHandler(value = ConstraintViolationException.class)
public ServiceResult errorHandler(ConstraintViolationException ex) {
ServiceResult serviceResult = new ServiceResult(400);
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
if (!CollectionUtils.isEmpty(constraintViolations)) {
StringBuilder stringBuilder = new StringBuilder();
for (ConstraintViolation constraintViolation : constraintViolations) {
stringBuilder.append(constraintViolation.getMessage()).append(",");
}
String errorMessage = stringBuilder.toString();
if (errorMessage.length() > 1) {
errorMessage = StringUtils.removeEnd(errorMessage, ",");
serviceResult.setMessage(errorMessage);
return serviceResult;
}
}
serviceResult.setMessage(ex.getMessage());
return serviceResult;
}
//对方法接收参数异常拦截
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ServiceResult errorHandler(MethodArgumentNotValidException ex) {
ServiceResult serviceResult = new ServiceResult(400);
List<ObjectError> objectErrors = ex.getBindingResult().getAllErrors();
if(!CollectionUtils.isEmpty(objectErrors)) {
StringBuilder builder = new StringBuilder();
for (ObjectError objectError : objectErrors) {
builder.append(objectError.getDefaultMessage()).append(",");
}
String errorMessage = builder.toString();
if (errorMessage.length() > 1) {
errorMessage = StringUtils.removeEnd(errorMessage,",");
}
serviceResult.setMessage(errorMessage);
return serviceResult;
}
serviceResult.setMessage(ex.getMessage());
return serviceResult;
}
}
- 实体
@Data
public class Clazz {
@NotNull(message = "班级主键ID不能为空")
private Integer id;
@NotBlank(message = "班级名称不能为空")
@Size(min = 1,max = 50,message = "班级名称长度必须在1到50之间")
private String className;
}
不难发现,我们可以传一个message表示错误信息。
- 测试controller
@RestController
@RequestMapping("/user")
public class ValidatorController {
@PostMapping("/addUser")
public User addUser(@Valid @RequestBody User user){
System.out.println(user);
return user;
}
@PostMapping("/addClazz")
public Clazz addClazz(@Validated @RequestBody Clazz clazz){
System.out.println(clazz);
return clazz;
}
}
1.2.2.2、分组
分组是spring官方注解@Validated
提供的,在我们没有分组校验场景的情况下,建议不适用它而使用jdk官方的。
- 定义分组,即定义接口
这里我们定义了两个分组,也就是两个接口
/**
* 校验分组1,校验规则1
*/
public interface Group1 {
}
/**
* 校验分组2,校验规则2
*/
public interface Group2 {
}
- 实体授权组
如下所示,底层采用class类的形式进行分组。也就是说,我们也可以使用任意类型的class类
@Data
public class User2Dto {
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空!", groups = {Group1.class})
private String username;
/**
* 性别
*/
@NotBlank(message = "性别不能为空!")
private String gender;
/**
* 年龄
*/
@Min(value = 1, message = "年龄有误!", groups = {Group1.class})
@Max(value = 120, message = "年龄有误!", groups = {Group2.class})
private int age;
/**
* 地址
*/
@NotBlank(message = "地址不能为空!")
private String address;
/**
* 邮箱
*/
@Email(message = "邮箱有误!", groups = {Group2.class})
private String email;
/**
* 手机号码
*/
@Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$", message = "手机号码有误!", groups = {Group2.class})
private String mobile;
}
- controller指定分组
@Controller
public class UserController {
@RequestMapping("/saveAdd")
public String saveAddUser(@Validated({Group1.class}) User user, BindingResult result) {
if(result.hasErrors()) {
return "error";
}
return "success";
}
1.2.3、创建自动配置
所谓自动配置(自动装配),即引入依赖就装配相关的bean,让我们做到开箱即用。
简单的说,就是引入依赖,自动注入一些bean,步骤如下:
- 引入
spring-boot-starter
依赖 - 创建配置实体类,并动态读取(选用)
- 创建配置类
- 创建
META-INF/spring.factories
文件用于注入自动配置文件
那么我们就来实现一个日志starter,hylog-spring-boot-starter
,引入了该依赖后,我们就可以实现注解打印日志。
这里我们也贴出springboot创建自己的自动配置中文文档
1.2.3.1、实现日志starter
1.2.3.1.1、引入依赖
springboot加载自然要引入boot相关的
值得注意的是spring-boot-configuration-processor
这个依赖包,这个包能够在我们编写配置文件的时候给予提示,但值得注意的是,有时候必须运行一遍springboot程序,才会出现提示。
<dependencies>
<!--spring-boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--AOP相关依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!--产生配置元数据,在配置文件配置时,能够提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope> <!--编译文件不生成-->
</dependency>
<!--主要是为了引入@RefreshScope 动态感知配置文件依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
1.2.3.1.2、配置实体类
我们创建一个配置实体类,用于yaml文件来配置我们的日志依赖。
/**
* @author: 王富贵
* @description: 日志配置文件映射实体
* @createTime: 2022年05月18日 20:12:56
*/
@Data //lombot生成getter和setter
@ConfigurationProperties("hy.log") //前缀
@RefreshScope //动态感知刷新
public class LogProperties {
/**
* 是否开启业务日志,默认为true
*/
private Boolean enable = Boolean.TRUE;
}
默认配置如下,如果在yaml文件中配置过,则会映射进这个实体
hy:
log:
enable: true
1.2.3.1.3、创建配置类
我们创建两个自动配置类,来模拟多自动配置类情况。
必须使用@EnableConfigurationProperties({配置实体.class})
来注入我们的配置实体类,请注意,这个很重要,在后续使用的时候,使用常规的@Authwired
或者@Resource
注入即可使用。
/**
* @author: 王富贵
* @description: 日志自动配置类
* @createTime: 2022年05月18日 20:10:16
*/
@EnableConfigurationProperties({LogProperties.class})
//@Import({WebLogAspect.class})
public class LogAutoConfiguration {
@Bean
@Order(Ordered.LOWEST_PRECEDENCE) //配置加载优先级
@ConditionalOnMissingBean(WebLogAspect.class)//在存在web日志自动装配类的时候才进行装配
public WebLogAspect webLogAspect(){
return new WebLogAspect();
}
}
@EnableConfigurationProperties({LogProperties.class})
public class LogWebAutoConfiguration {
}
1.2.3.1.3.1、配置类的作用
配置有有两个作用
- 只有在这里使用@EnableConfigurationProperties才能将配置实体类装配bean
- 可以装配一下我们需要使用的bean
提出问题:为什么要在这里装配?
因为我们的模块是用来引入的,我们需要装配的bean位置通常不属于ComponentScan的自动扫描装配范围,因此我们需要手动装配。
装配bean有两种方法
-
@Import({})
,在里面传入配置类实体class类数组即可 -
使用@Bean显式的声明创建,这是我们推荐的方法,这么做我们可以使用@Order来配置注入优先级,也可以使用条件注解
1.2.3.1.3.2、条件注解
我们可以使用条件注解,来表示在什么情况下这个bean创建,什么时候不创建
条件注解如下
@ConditionalOnBean
:当容器里有指定 Bean 的条件下@ConditionalOnMissingBean
:当容器里没有指定 Bean 的情况下@ConditionalOnSingleCandidate
:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean@ConditionalOnClass
:当类路径下有指定类的条件下@ConditionalOnMissingClass
:当类路径下没有指定类的条件下@ConditionalOnProperty
:指定的属性是否有指定的值@ConditionalOnResource
:类路径是否有指定的值@ConditionalOnExpression
:基于 SpEL 表达式作为判断条件@ConditionalOnJava
:基于 Java 版本作为判断条件@ConditionalOnJndi
:在 JNDI 存在的条件下差在指定的位置@ConditionalOnNotWebApplication
:当前项目不是 Web 项目的条件下@ConditionalOnWebApplication
:当前项目是 Web 项 目的条件下
通过使用条件注解,我们可以灵活的创建默认的bean,约定大约配置嘛。
通过配置文件开关装配bean示例:
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceAopConfiguration {
@Bean
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX + ".aop", name = "enabled", havingValue = "true", matchIfMissing = true)
public Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {
DynamicDatasourceAopProperties aopProperties = properties.getAop();
DynamicDataSourceAnnotationInterceptor interceptor = new DynamicDataSourceAnnotationInterceptor(aopProperties.getAllowedPublicOnly(), dsProcessor);
DynamicDataSourceAnnotationAdvisor advisor = new DynamicDataSourceAnnotationAdvisor(interceptor, DS.class);
advisor.setOrder(aopProperties.getOrder());
return advisor;
}
}
1.2.3.1.4、创建spring.factories文件
在resources
目录下创建META-INF/spring.factories
文件,用于读取配置类文件,将其装配入bean工厂中
通常我们的bean需要在springboot主启动类同级或以下目录下才会被扫描。但一旦你的包不在此目录下,则不会被装配入bean中,因此我们必须使用spring.factories将我们的配置类(也就是1.2.3.1.2中的配置类)的全类名加载进去。
示例如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hy.log.config.LogAutoConfiguration,\
com.hy.log.config.LogWebAutoConfiguration
请注意:
- 必须装配全类名,也就是带上路径
- 必须使用该文件装配所有的配置类
- 只能装配配置类
- 使用
,\
分隔不同的配置类
1.2.3.1.5、aop构建日志处理
- 自定义日志注解
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 保留字段,暂时无用
* @return
*/
String value() default "";
/**
* 日志类型
* @return
*/
LogEnum type() default LogEnum.BUSINESS;
}
- 日志枚举类型
public enum LogEnum {
LOGIN, //登录
LOGOUT, //登出
BUSINESS // 业务操作
}
- 以日志注解作为切点环绕
@Aspect
@Slf4j(topic = "request-log")
@Order(1)
public class WebLogAspect {
@Value("${spring.profiles.active}")
private String activeEnv;
@Autowired
private LogProperties logProperties;
/**
* ..表示包及子包 该方法代表controller层的所有方法
* Pointcut定义时,还可以使用&&、||、! 这三个运算
* 这里我们用注解进行环绕
*/
@Pointcut("@annotation(com.hy.common.annotation.Log)")
public void controllerMethod() {
}
@Around("controllerMethod()")
public Object around(ProceedingJoinPoint joinPoint) {
Object proceed = null;
//如果日志标记为false
if (logProperties.getEnable().equals(Boolean.FALSE)) {
log.info("不打印日志咯");
try {
proceed = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return proceed;
}
log.info("环境是:{},是否开启了日志:{}", activeEnv, logProperties.getEnable());
log.info("方法执行前日志");
try {
proceed = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
log.error("方法出现异常日志");
}
log.info("方法执行后日志");
return proceed;
}
}
1.2.3.1.6、总结
以上步骤,我们定义了一个日志starter方法,并做了起步依赖,当我们注入日志starter的时候,系统会监测配置文件中是否开启了日志,并且会在不存在切点bean:WebLogAspect的时候自动注入一个,作为起步依赖。
1.2.4、创建自动配置(spring2.7.0)
上面我们已经提到了,创建一个自动配置也就是starter需要做的事情。其核心意义在于因为我们包是在外部被引用的,而我们的Configuration配置类不在主启动类当前目录或子目录下无法被引用。因此采用开闭原则的思想采用spi机制进行引入。在springboot2.7.0中,为了满足标准spi机制的接口,对引入文件做出了改变。步骤如下:
- 引入
spring-boot-starter
依赖 - 创建配置实体类,并动态读取(选用)
- 创建配置类
- 创建
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件用于注入自动配置文件
此外,该文件内容也发生了改变,不再需要第一排的内容,同时结尾也不需要加上 ,\
作为换行符
com.hy.log.config.LogAutoConfiguration
com.hy.log.config.LogWebAutoConfiguration
1.2.4.1、自动spi
在工作中我们大量使用到了之前 spring.factory
自动配置,如今spi的修订非常麻烦。我们可以使用 mica-auto工具 ,通过读取配置自动写入spi文件。也就是通过读取注解自动生成 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件。
mica-auto
采用Annotation Processor
技术,自动生成java spi
和Spring boot starter
的配置。
mica-auto
功能
- 生成
spring.factories
。 - 生成
spring-devtools.properties
- 生成
FeignClient
到spring.factories
中,供mica-cloud
中完成Feign
自动化配置。 - 生成
java spi
配置,需要添加@AutoService
注解。
mica-auto
使用场景
主要是用来避免 Spring boot 主项目包同 子项目
或者子模块
包不一致,避免包扫描不到的问题。
spring boot starter
利器,自动生成spring.factories
配置。- 多模块项目中的
子项目
(不建议主项目添加mica-auto
)。
其使用方法也非常简单
1.引入maven依赖, provided
作用是编译或打包时使用他,但不随着文件打包。注意一定要放在 lombok
包依赖后面
<dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-auto</artifactId>
<version>${version}</version>
<scope>provided</scope>
</dependency>
2.使用对应配置注解即可。
组合有 @Component
的注解,例如:@Configuration
,其他的类似 ApplicationContextInitializer
需要自行添加注解,详见 mica-auto
注解说明。
/**
* 自动配置
*
* @author l.cm
*/
@Configuration(proxyBeanMethods = false)
public class MicaAutoConfiguration {
@Bean
public SpringContextUtil springUtils() {
return new SpringContextUtil();
}
}
3.在打包或者编译时,就会自动生成 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
和 META-INF/spring.factory
文件
1.3、自定义扫描路径装配bean
1.3.1、ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar接口用于运行时注册bean
应用场景:如果想要实现动态的Bean代理,尤其是想装载动态对象的时候,比如mybatis启动器mapper接口的代理对象bean生成,比如rocketMQConsumer(消费者)扫描 RocketMQMessageListener
注解注册消费者。等运行时才能够确定的bean对象。
实现
ImportBeanDefinitionRegistrar
接口的同样为bean,需要在Configuration
bean内使用@Import
注入才能生效。换句话说,ImportBeanDefinitionRegistrar
需要搭配@Import
使用。
使用演示,出自: rocketmq-spring-boot-starter-2.2.3
@Configuration
@AutoConfigureAfter(RocketMQAutoConfiguration.class)
public class RocketMQListenerConfiguration implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 使用@Import导入本类的那个类的元数据
* @param registry 注册器,可用于注册BeanDefinition,生成bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(RocketMQMessageListenerBeanPostProcessor.class.getName())) {
registry.registerBeanDefinition(RocketMQMessageListenerBeanPostProcessor.class.getName(),
new RootBeanDefinition(RocketMQMessageListenerBeanPostProcessor.class));
}
}
}
我们可以使用 new RootBeanDefinition()
来便捷的创建BeanDefinition,他通常会注册为 GenericBeanDefinition
1.3.2、ClassPathBeanDefinitionScanner
ClassPathBeanDefinitionScanner工具用于扫描指定路径并将路径下的类注入 BeanDefinitionRegistry
,后续注册成bean。结合其过滤器能够实现装配包含指定注解的类。一般搭配ImportBeanDefinitionRegistrar一起使用。
spring扫描示例代码:org.springframework.context.annotation.ComponentScanAnnotationParser#parse
使用演示,模拟扫描spring注解 @Component
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata 使用@Import导入本类的那个类的元数据
* @param registry 注册器,可用于注册BeanDefinition,生成bean
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 1.创建scanner对象
// 注意第二个参数表示是否使用默认的过滤规则,默认过滤规则是检查类是否有spring的注解,如@Service,@Component等,有才会被扫描
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, false);
// 包含过滤器,满足条件才注入
// 方法1 使用spring官方注解过滤器
scanner.addIncludeFilter(new AnnotationTypeFilter(Component.class));
// 方法2 自定义过滤器
scanner.addExcludeFilter((metadataReader, metadataReaderFactory) -> metadataReader.getAnnotationMetadata().hasAnnotation("org.springframework.stereotype.Component"));
// scanner.addExcludeFilter(); // 排除过滤器,满足条件排除注入
// 2.进行扫描,会将包下满足规律规则的类注册进入registry中
scanner.scan("com.wfg.core.producer");
}
}
tips:
ClassPathBeanDefinitionScanner
构造函数第二项表示是否使用默认过滤器,默认过滤器会过滤掉非@Component
注解的对象
实践示例,为feign装配client:org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
1.3.3、FactoryBean
说完FactoryBean的亿点点细节,Spring对你来说再也牛不起来了
FactoryBean一般用于创建过程比较辅助的bean的创建,例如需要动态代理生成的bean。FactoryBean借鉴工厂模式,将复杂对象的创建封装到工厂中,便于bean的创建。
tips: FactoryBean的意义在于需求是反射创建接口的实例时,BeanDefinition无法便捷定义的情况,即增强(自定义)Bean创建过程。特别是在类似于对接口实例化的场景中。例如mybatis的Mapper接口,OpenFeign的Client接口等无法使用new来创建对象的场景。
示例代码为示例项目包:com.wfg.spring.FactoryBean
使用时需要将FactoryBean注入容器中,FactoryBean注入的bean为 getObject()
返回的对象。使用FactoryBean的BeanName从容器中获取的是 getObject()
返回的对象,需要拿到FactoryBean可以使用 &
符号加上bean名称,例: context.getBean("&bean名称")
public class SelectMapperFactoryBean implements FactoryBean {
private String className = "";
public SelectMapperFactoryBean(String className) {
this.className = className;
}
/**
* 使用className创建FactoryBean,反射创建接口实现
*/
@Override
public Object getObject() throws ClassNotFoundException {
Class<?> aClass = Class.forName(className);
return Proxy.newProxyInstance(aClass.getClassLoader(), new Class[]{SelectMapper.class},
(proxy, method, args) -> {
if ("selectOne".equals(method.getName())) {
System.out.println("代理对象被执行了");
}
return proxy;
});
}
@Override
public Class<?> getObjectType() {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
1.3.4、终极用法
在上面我们提到
ImportBeanDefinitionRegistrar
接口能够拿到注册器BeanDefinitionRegistry
,并手动注册一些bean。他需要搭配注解@Import
使用ClassPathBeanDefinitionScanner
扫描器能够扫描指定路径下满足我们条件的类或接口,并注册成一个BeanDefinition
的set集合。且可以注入容器中。FactoryBean
采用工厂模式,辅助bean的创建,最后将getObject
方法的返回的对象注入容器。
那么,我们是否能模仿OpenFeign实现一下他的一些功能,使用 ClassPathBeanDefinitionScanner
扫描携带了我们自定义注解的接口,使用 ImportBeanDefinitionRegistrar
注入我们扫描到的接口。 FactoryBean
将扫描到的接口反射增强为一个实际可用的对象并响应回去注入。
需求:我们期望实现OpenFeign的效果,定义接口,接口上指定注解,然后生成对应的动态代理对象装载到容器中。
具体步骤如下:
-
自定义注解
-
定义一个接口,添加自定义注解
-
创建Registrar并创建扫描器子类
- 重写isCandidateComponent方法(原因是这个方法默认只扫描类,而我们需要扫描接口)
- 使用扫描器进行扫描(调用
findCandidateComponents
方法)获取BeanDefinition
的set集合 - 遍历集合进行转换,对接口进行增强,并使用注册器注入(当然你也可以重写
findCandidateComponents
方法返回一个增强过后的集合)
-
使用
@Import
注解注入Registrar
Registrar如下:
/**
* @author: 王富贵
* @description: 自定义注解注册器
* @createTime: 2023/11/19 13:54
*/
public class WfgMapperRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
HashSet<String> annotationNames = new HashSet<>();
annotationNames.add(WfgMapper.class.getName());
WfgClassPathBeanDefinitionScanner wfgClassPathBeanDefinitionScanner = new WfgClassPathBeanDefinitionScanner(registry, annotationNames);
// 方法1,重写ClassPathBeanDefinitionScanner的findCandidateComponents,替换扫描获得的Set<BeanDefinition>
wfgClassPathBeanDefinitionScanner.scan("com.wfg.spring.mapper");
// 方法2. 调用findCandidateComponents方法获取Set<BeanDefinition>,手动通过registry注入
Set<BeanDefinition> candidateComponents = wfgClassPathBeanDefinitionScanner.findCandidateComponents("com.wfg.spring.mapper");
for (BeanDefinition beanDefinition : candidateComponents) {
// 被代理的类使用FactoryBean
AbstractBeanDefinition proxyBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SelectMapperFactoryBean.class)
// SelectMapperFactoryBean构造函数调用参数传入
.addConstructorArgValue(beanDefinition.getBeanClassName())
.getBeanDefinition();
// 手动注册被代理过后的BeanDefinition
// registry.registerBeanDefinition(beanDefinition.getBeanClassName(), proxyBeanDefinition);
}
}
}
自定义扫描器如下:
/**
* @author: 王富贵
* @description: 自定义ClassPathBeanDefinition扫描器
* @createTime: 2023/11/17 20:32
*/
public class WfgClassPathBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
/**
* 默认构造器,扫描指定注解
* @param registry
*/
public WfgClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry,HashSet<String> annotationNames) {
super(registry, false);
for (String annotationClassName : annotationNames) {
this.addIncludeFilter((metadataReader, metadataReaderFactory) ->
metadataReader.getAnnotationMetadata().hasAnnotation(annotationClassName));
}
}
/**
* 重写是否是候选组件扫描规则,当是接口时扫描
*
* @param beanDefinition the bean definition to check
* @return
*/
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface();
}
@Override
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidateComponents = super.findCandidateComponents(basePackage);
Set<BeanDefinition> newBeanDefinition = new HashSet<>();
for (BeanDefinition beanDefinition : candidateComponents) {
AbstractBeanDefinition proxyBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SelectMapperFactoryBean.class)
.addConstructorArgValue(beanDefinition.getBeanClassName())
.getBeanDefinition();
newBeanDefinition.add(proxyBeanDefinition);
}
return newBeanDefinition;
}
}
评论区