Spring 与自定义注解、外部配置化的结合使用

Spring 与自定义注解、外部配置化的结合使用

一、Java注解的简单介绍

注解,也叫Annotation、标注,是 Java 5 带来的新特性。

  1. 可使用范围

    类、字段、方法、参数、构造函数、包等,具体可参阅枚举类 java.lang.annotation.ElementType

  2. 生命周期(摘自 刘大飞的博客

    • RetentionPolicy.SOURCE 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
    • RetentionPolicy.CLASS 注解被保留到class文件,但 jvm 加载class文件时候被遗弃,这是默认的生命周期
    • RetentionPolicy.RUNTIME 注解不仅被保存到class文件中,jvm 加载class文件之后,仍然存在
  3. 使用方式

    可以使用反射获取注解的内容,具体如何使用请自己百度,可参考这篇Java注解完全解析,这里不是重点,不多做介绍

二、Spring的 @Import注解

@Import 注解是Spring用来注入 Spring Bean 的一种方式,可以用来修饰别的注解,也可以直接在Springboot配置类上使用。

它只有一个value属性需要设置,来看一下源码

public @interface Import {
    Class[] value();
}

这里的 value属性只接受三种类型的Class:

  • @Configuration 修饰的配置类
  • 接口org.springframework.context.annotation.ImportBeanDefinitionRegistrar的实现类
  • 接口org.springframework.context.annotation.ImportSelector的实现类

下面针对三种类型的Class分别做简单介绍,中间穿插自定义注解与外部配置的结合使用方式。

三、被@Configuration 修饰的配置类

像 Springboot 中的配置类一样正常使用,需要注意的是,如果该类的包路径已在Springboot启动类上配置的扫描路径下,则不需要再重新使用@Import导入了,因为@Import的目的是注入bean,但是Springboot启动类自动扫描已经可以注入你想通过@Import导入的bean了。

这种Class可以进行如下拓展

  • 继承各种Aware接口, 获取对应的信息(如果不清楚Aware接口在Spring当中的作用,请自行百度),如,继承EnviromentAware,可以拿到Spring的环境配置信息,进而从中拿到 @Value所需要的值,如 environment.getProperty("user.username")
  • 使用@Autowire@Resource@Value注入各种所需Spring 资源
  • 像普通Spring Bean 一样使用该类

更多使用方式,请自行百度。

四、接口org.springframework.context.annotation.ImportBeanDefinitionRegistrar的实现类

@Import修饰自定义注解时候,通常会导入这种类。

来看一下接口定义

public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
            BeanNameGenerator importBeanNameGenerator) {
        registerBeanDefinitions(importingClassMetadata, registry);
    }

    /**
    * importingClassMetadata 被@Import修饰的自定义注解的元信息,可以获得属性集合
    * registry Spring bean注册中心
    **/
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }

通过这种方式,我们可以根据自定义注解配置的属性值来注入Spring Bean 信息。

来看如下案例,我们通过一个注解,启动RocketMq的消息发送器:

@SpringBootApplication
@EnableMqProducer(group="xxx")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

这是一个服务项目的启动类,这个服务开启了RocketMq的一个发送器,并且分到xxx组里。

来下一下@EnableMqProducer注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({XXXRegistrar.class,XXXConfig.class})
public @interface EnableMqProducer {

    String group() default "DEFAULT_PRODUCER_GROUP";

    String instanceName() default "defaultProducer";

    boolean retryAnotherBrokerWhenNotStoreOK() default true;
}

这里使用@Import导入了两个配置类,第一个是接口org.springframework.context.annotation.ImportBeanDefinitionRegistrar的实现类,第二个是被@Configuration 修饰的配置类

我们看第一个类,这个类注入了一个DefaultMQProducer到Spring 容器中,使业务方可以直接通过@Autowired注入使用

public class XXXRegistrar implements ImportBeanDefinitionRegistrar {  
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableMqProducer.class.getName()));
        registerBeanDefinitions(attributes, registry);
    }

    private void registerBeanDefinitions(AnnotationAttributes attributes, BeanDefinitionRegistry registry) {
        //获取配置
        String group = attributes.getString("group");
        //省略部分代码...
        //添加要注入的类的字段值
        Map values = new HashMap<>();
        //这里有的同学可能不清楚为什么key是这个
        //这里的key就是DefaultMQProducer的字段名
        values.put("producerGroup", group);
        //省略部分代码

        //注册到Spring中
        BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, DefaultMQProducer.class.getName(), DefaultMQProducer.class, values);
    }

到这里,我们已经注入了一个DefaultMQProducer的实例到Spring容器中,但是这个实例,还不完整,比如,还没有启动,nameServer地址还没有配置,可外部配置的属性还没有覆盖实例已有的值(nameServer地址建议外部配置)。好消息是,我们已经可以通过注入来使用这个实例了。

上面遗留的问题,就是第二个类接下来要做的事。

来看第二个配置类

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@EnableConfigurationProperties(XxxProperties.class)  //Spring提供的配置自动映射功能,配置后可直接注入 
public class XXXConfig {

    @Resource //直接注入
    private XxxProperties XxxProperties;

    @Autowired //注入上一步生成的实例
    private DefaultMQProducer producer;

    @PostConstruct
    public void init() {
        //省略部分代码
        //获取外部配置的值
        String nameServer = XxxProperties.getNameServer();
        //修改实例
        producer.setNamesrvAddr(nameServer);
        //启动实例
        try {
            this.producer.start();
        } catch (MQClientException e) {
            throw new RocketMqException("mq消息发送实例启动失败", e);
        }
    }

    @PreDestroy
    public void destroy() {
        producer.shutdown();
    }

到这里,通过自定义注解和外部配置的结合,一个完整的消息发送器就可以使用了,但方式有取巧之嫌,因为在消息发送器启动之前,不知道还有没有别的类使用了这个实例,这是不安全的。

五、接口org.springframework.context.annotation.ImportSelector的实现类

首先看一下接口

public interface ImportSelector {

    /**
     * importingClassMetadata 注解元信息,可获取自定义注解的属性集合
     * 根据自定义注解的属性,或者没有属性,返回要注入Spring的Class全限定类名集合
     如:XXX.class.getName(),Spring会自动注入XXX的一个实例
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate getExclusionFilter() {
        return null;
    }

}

这个接口的实现类如果没有进行@Aware拓展,功能比较单一,因为我们无法参与Spring Bean 的构建过程,只是告诉Spring 要注入的Bean的名字。不再详述。

六、总结

通过接口和配置类的灵活结合,可以实现自定义注解的配置化设计,归根到底是Spring Bean的灵活构建,如果你有更好更优雅的方式,欢迎留言指教。

你可能感兴趣的:(springboot,spring,java)