Enable模块驱动

前言

很多同学在使用Springboot时都会看到一个注解“@EnableXXX”,该注解用于显示的装配指定的模块,如@EnableScheduling用来装配spring的定时任务模块、@EnableCaching用于激活缓存等等。但是,不知道大家有没有思考过其背后的原理?本篇文章会从代码层面来解释Enable模块驱动的原理,同时也会讲解如何编写自定义的Enable注解来实现Enable模块驱动。 在正式开始讲解之前先讲解“Enable的前世今生”,便于让更加清楚的了解其背后的设计理念。

自定义Enable模块驱动

后面的例子都是围绕CustomServer这个接口来展开,该接口的功能很简单,就是启动和停止“自定义服务器”这两个功能。下面展示的是CustomServer接口的定义,同时也展示了两个该接口的具体实现。

public interface CustomServer {

    void start();

    void stop();

}
public class ServletCustomServer implements CustomServer{

    @Override
    public void start() {
        System.out.println("servlet custom server start.");
    }

    @Override
    public void stop() {
        System.out.println("servlet custom server stop.");
    }
}
public class ReactCustomServer implements CustomServer{

    @Override
    public void start() {
        System.out.println("react custom server start.");
    }

    @Override
    public void stop() {
        System.out.println("react custom server stop.");
    }
}

至此,CustomServer模块的功能已经实现完毕,那么接下来的工作就是要通过Enable模块驱动来将CustomServer模块集成到Spring应用中了。

注解驱动

使用实现Enable模块驱动时肯定先要定义Enable注解,在下面展示的代码中定义的注解中只有一个value属性,可选值是枚举类ServerType。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CustomServerConfiguration.class)
public @interface EnableCustomServer {

    ServerType value() default ServerType.SERVLET;

    enum ServerType {
        SERVLET("servletCustomServer"),
        REACT("reactCustomServer");

        ServerType(String name){
            this.name = name;
        }

        private String name;

        String getName(){
            return name;
        }

    }

}

既然是通过注解驱动来实现Enable模块驱动,那么必不可少的便是配置类啦,在下面展示的配置类中注册了SERVLET、REACT这两个CustomServer。

@Configuration
public class CustomServerConfiguration {

    @Bean("servletCustomServer")
    public CustomServer servletCustomServer(){
        return new ServletCustomServer();
    }

    @Bean("reactCustomServer")
    public CustomServer reactCustomServer(){
        return new ReactCustomServer();
    }

}

至此,所有的准备工作都已做完了,剩下的就是需要到Spring应用中去注册@EnableCustomServer标注的类啦,在下面代码中为了省事直接将@EnableCustomServer标注在测试类上,实际情况中切记要将Enable模块驱动注解标注到配置类上

@EnableCustomServer
public class EnableConfigurationTest {

    private AnnotationConfigApplicationContext applicationContext;

    @BeforeEach
    public void init(){
        applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(EnableConfigurationTest.class);
        applicationContext.refresh();
    }

    @Test
    public void annotationConfigTest(){
        Map<String, CustomServer> beansOfType = applicationContext.getBeansOfType(CustomServer.class);
        System.out.println(beansOfType);
        applicationContext.close();
    }

}

运行上示测试类,结果如下。可以看到配置类中的两个bean都加载进Spring中了,那么至此基于注解驱动实现Enable模块驱动的功能实现了。

//… {servletCustomServer=com.example.demo.enable.ServletCustomServer@27406a17, reactCustomServer=com.example.demo.enable.ReactCustomServer@2af004b} //…

接口编程

从上面展示的示例中可以看到,基于注解驱动实现的Enable模块驱动非常简单。但是,它的缺点也非常明显。在通过@EnableCustomServer来装配CustomServer时,并不需要将两个Bean都装配。因为,定义的CustomerServer是表示一个自定义的服务器,而这个服务器的类型是排他的,即非ServletCustomerServer即ReactCustomerServer不可能两个Bean都同时存在。所以,应用最终肯定只能装载其中的一个CustomerServer。 同时,细心的同学肯定也发现了,上面定义的@EnableCustomerServer中有一个value属性,它代表了当前服务器装配的CustomerServer类型。但是在配置类中根本没有办法去获取其属性,因此就需要借助接口编程来实现Enable模块装配。

ImportSelector

public class CustomServerSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableCustomServer.class.getName(), false));
        EnableCustomServer.ServerType serverType = (EnableCustomServer.ServerType) attributes.get("value");
        switch (serverType){
            case SERVLET:
                return new String[]{ServletCustomServer.class.getName()};
            case REACT:
                return new String[]{ReactCustomServer.class.getName()};
        }
        return new String[0];
    }
}

从上面的展示中可以看出,在该回调方法中由于传入的AnnotationMetadata中包含@EnableCustomerServer及其相关属性,因此在Spring进行装配时确定服务器想要装配的CustomerServer类型。 当然,接下来就要修改@EnabelCustomerServer中的@Import。修改后的类如下所示。可以看到,只是修改了@Import的部分

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CustomServerSelector.class)
public @interface EnableCustomServer {

    ServerType value() default ServerType.SERVLET;

    enum ServerType {
        SERVLET("servletCustomServer"),
        REACT("reactCustomServer");

        ServerType(String name){
            this.name = name;
        }

        private String name;

        String getName(){
            return name;
        }

    }

}

接下来去编写测试类去测试下效果。这里借用了前面注解驱动时写的测试类,其他不变,就是多加了个测试方法。

@EnableCustomServer(value = EnableCustomServer.ServerType.REACT)
public class EnableConfigurationTest {

    private AnnotationConfigApplicationContext applicationContext;

    @BeforeEach
    public void init(){
        applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(EnableConfigurationTest.class);
        applicationContext.refresh();
    }

    @Test
    public void interfaceConfigTest(){
        CustomServer server = applicationContext.getBean(CustomServer.class);
        server.start();
        server.stop();
        applicationContext.close();
    }

}

运行结果如下所示。可以看到,按照@EnableCustomerServer中value的属性来装配bean成功了,Spring中也只有一种CustomerServer。

//...
react custom server start.
react custom server stop.
//...

ImportBeanDefinitionRegistar

该接口和ImportSelector接口的功能大体类似。但是,该接口更加的有“弹性”,它将Bean的注册过程开放给接口使用者。 自定义的实现类如下,可以看到此处复用了前面ImportSelector的实现类。该实现类通过ImporSelector确定了需要注册的类名集合,然后通过BeanDefinitionRegistry将这些Bean注册到了Spring上下文中。

public class CustomServerRegistar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        CustomServerSelector selector = new CustomServerSelector();
        String[] strings = selector.selectImports(importingClassMetadata);
        Arrays.stream(strings).map(BeanDefinitionBuilder::genericBeanDefinition)
                .map(BeanDefinitionBuilder::getBeanDefinition)
                .forEach(bean -> BeanDefinitionReaderUtils.registerWithGeneratedName(bean, registry));
    }

}

接着,修改配置类如下所示。也是一样,只改动了@Import的部分。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CustomServerRegistar.class)
public @interface EnableCustomServer {

    ServerType value() default ServerType.SERVLET;

    enum ServerType {
        SERVLET("servletCustomServer"),
        REACT("reactCustomServer");

        ServerType(String name){
            this.name = name;
        }

        private String name;

        String getName(){
            return name;
        }

    }

}

测试类就复用ImportSelector中写的即可,不需要新增,测试结果入下。

//...
react custom server start.
react custom server stop.
//...

总结

当需要装配的模块并不需要用到Enable注解中的属性同时也不需要查询Spring上下文时,使用注解驱动的方式仍然是最佳的实践。然而,大多数情况下应用装配一个模块时并不是如此简单,可以从Spring和Springboot已有的Enable注解中看到,使用注解驱动方式装配的例子相对较少。 当需要装配的模块需要用到Enable注解中的属性时,选择ImportSelector或ImportBeanDefinitionRegistry才是最佳的实践。这两者本质上来说并没有太多不同,唯一的区别就是ImportBeanDefinitionRegistry会赋予接口使用者更大的弹性,使其可以自定义的注册或删除beanDefinition(虽然,从上示的例子中来看,ImportSelector与ImportBeanDefinitionRegistry并没任何不同。但是,熟悉BeanDefinitionRegistry的同学会知道,在做自定义的模块驱动时,有时需要用到BeanFactory、Envoronment等的相关功能来与Ioc进行通讯,然后才能确定最终注册到Spring的Bean。当然,这里只是举个例子,具体选择那个接口使用还是要根据同学们的具体诉求)