前言
很多同学在使用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。当然,这里只是举个例子,具体选择那个接口使用还是要根据同学们的具体诉求)