侧边栏壁纸
  • 累计撰写 14 篇文章
  • 累计创建 8 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

SpringBoot进阶

王富贵
2024-05-25 / 0 评论 / 1 点赞 / 41 阅读 / 0 字

1、springboot进阶

1.1、配置文件

1.1.1、默认配置

yaml通用配置文件

我们通常会使用yaml文件来配置配置文件,当然,使用$符号能够满足一些我们的特殊需求。同时也贴出官方文档。

springboot 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) CLOSEOPEN,CLOSE 可为任意字符,value,max 为整数。如果使用了 maxvalue 则为最小值,max 为最大值。

2.引用环境配置

server:
  port: ${server.port:8080}

如上示例表示,我们会去读取启动命令参数,如环境命令,jvm启动命令中拿这个值。并使用:分隔开,给一个默认8080的值。这在实际生产中是非常常见的配置。

1

如上图所示,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来对配置类实体进行映射。样例如下:

@ConfigurationProperties中文文档

yaml配置

hy:
  log:
    enable: true

实体映射

@Setter
@Getter
@ConfigurationProperties("hy.log")
@RefreshScope //实时刷新配置文件
public class LogProperties {
    /**
     * 是否开启业务日志,默认为true
     */
    private Boolean enable = Boolean.TRUE;
}

自此完成了通过实体类映射配置信息

值得注意的是

  1. @ConfigurationProperties中的value和prefix都用来表示prefix,也就是前缀
  2. 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这个枚举类型进行控制。

使用场景:

  1. 安全框架如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是我们常用来校验接收参数的两个注解,他们的区别如下:

  1. @Validated是spring提供的一个注解,而@Valid是jdk提供的,也就是java原生支持
  2. @Validated提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,而@Valid没有
  3. @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验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式

1.2.2.1、样例

  1. 依赖

前面我们提到了@Validated是spring官方提供的,我们可能需要引入依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. 通常我们会封装一个对其异常的拦截
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;
    }
}
  1. 实体
@Data
public class Clazz {
    @NotNull(message = "班级主键ID不能为空")
    private Integer id;
    @NotBlank(message = "班级名称不能为空")
    @Size(min = 1,max = 50,message = "班级名称长度必须在1到50之间")
    private String className;
}

不难发现,我们可以传一个message表示错误信息。

  1. 测试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,校验规则1
 */
public interface Group1 {
}
/**
 * 校验分组2,校验规则2
 */
public interface Group2 {
}
  1. 实体授权组

如下所示,底层采用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;
}
  1. 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,步骤如下:

  1. 引入spring-boot-starter依赖
  2. 创建配置实体类,并动态读取(选用)
  3. 创建配置类
  4. 创建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、配置类的作用

配置有有两个作用

  1. 只有在这里使用@EnableConfigurationProperties才能将配置实体类装配bean
  2. 可以装配一下我们需要使用的bean

提出问题:为什么要在这里装配?

因为我们的模块是用来引入的,我们需要装配的bean位置通常不属于ComponentScan的自动扫描装配范围,因此我们需要手动装配。

装配bean有两种方法

  1. @Import({}),在里面传入配置类实体class类数组即可

  2. 使用@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. 只能装配配置类
  4. 使用,\分隔不同的配置类
1.2.3.1.5、aop构建日志处理
  1. 自定义日志注解
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 保留字段,暂时无用
     * @return
     */
    String value() default "";

    /**
     * 日志类型
     * @return
     */
    LogEnum type() default LogEnum.BUSINESS;
}
  1. 日志枚举类型
public enum LogEnum {
    LOGIN,      //登录
    LOGOUT,     //登出
    BUSINESS    // 业务操作
}
  1. 以日志注解作为切点环绕
@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机制的接口,对引入文件做出了改变。步骤如下:

  1. 引入spring-boot-starter依赖
  2. 创建配置实体类,并动态读取(选用)
  3. 创建配置类
  4. 创建 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 spiSpring boot starter 的配置。

mica-auto 功能

  1. 生成 spring.factories
  2. 生成 spring-devtools.properties
  3. 生成 FeignClientspring.factories 中,供 mica-cloud 中完成 Feign 自动化配置。
  4. 生成 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.importsMETA-INF/spring.factory 文件

1.3、自定义扫描路径装配bean

样例demo: spring-registrar

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、终极用法

在上面我们提到

  1. ImportBeanDefinitionRegistrar 接口能够拿到注册器 BeanDefinitionRegistry ,并手动注册一些bean。他需要搭配注解 @Import 使用
  2. ClassPathBeanDefinitionScanner 扫描器能够扫描指定路径下满足我们条件的类或接口,并注册成一个 BeanDefinition 的set集合。且可以注入容器中。
  3. FactoryBean 采用工厂模式,辅助bean的创建,最后将 getObject 方法的返回的对象注入容器。

那么,我们是否能模仿OpenFeign实现一下他的一些功能,使用 ClassPathBeanDefinitionScanner 扫描携带了我们自定义注解的接口,使用 ImportBeanDefinitionRegistrar 注入我们扫描到的接口。 FactoryBean 将扫描到的接口反射增强为一个实际可用的对象并响应回去注入。

需求:我们期望实现OpenFeign的效果,定义接口,接口上指定注解,然后生成对应的动态代理对象装载到容器中。

具体步骤如下:

  1. 自定义注解

  2. 定义一个接口,添加自定义注解

  3. 创建Registrar并创建扫描器子类

    1. 重写isCandidateComponent方法(原因是这个方法默认只扫描类,而我们需要扫描接口)
    2. 使用扫描器进行扫描(调用 findCandidateComponents 方法)获取 BeanDefinition 的set集合
    3. 遍历集合进行转换,对接口进行增强,并使用注册器注入(当然你也可以重写 findCandidateComponents 方法返回一个增强过后的集合)
  4. 使用 @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;
    }
}
1

评论区