一、前言
在平时使用spring boot的时候,很多时候都会用到@EnableXXX的注解,来装配一些功能模块,有代表性的,比如:
- @EnableWebMvc 开启Web MVC的配置支持;
- @EnableCaching 开启注解式的缓存支持。
如果,想自定义实现这些功能,我们应该怎么做呢?先看一下spring boot是怎么帮我们做的。
- @EnableWebMvc(采用基于注解驱动方式)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
三个元注解,没有什么好说的,重点是@Import注解,@Import注解支持导入普通的java类,并将其声明成一个bean。这也说明了,自动开启的实现,其实是导入了一些配置类。
@Configuration(
proxyBeanMethods = false
)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
从源码来说,DelegatingWebMvcConfiguration 确实是一个配置类。
- @EnableCaching(采用基于接口编程方式)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
int order() default Ordered.LOWEST_PRECEDENCE;
}
可以看到,@EnableCaching注解与@EnableWebMvc注解差不多,只是多了一些注解元素,那么这些注解元素是怎么解析的呢,看一下@Import注解导入的CachingConfigurationSelector的继承关系:
注意ImportSelector接口,当我们自定义一个基于接口驱动的@EnableXXX注解时,模块需要实现ImportSelector接口,
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
ImportSelector接口的实现类,主要通过selectImports方法,来解析我们自定义的注解元素。
二、自定义基于注解驱动的@EnableXXX
1.创建一个简单的spring boot工程,项目结构如下:
为了方便演示,我直接把自定义的@EnableSystem注解放在同一个spring boot工程上了,实际开发肯定不是这样的。
注意:@EnableSystem所在包,不是启动类SpringbootEnableAnnotationApplication所在包的子包,这样是为了spring boot在启动的时候不会扫描到@EnableSystem注解的相关类!
- 创建一个SystemConfiguration配置类:
@Configuration
public class SystemConfiguration {
@Bean
public String fileExtension() {
return ".bat";
}
}
- 创建自定义@EnableSystem注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({SystemConfiguration.class})
public @interface EnableSystem {
}
- 测试
- 不在启动类上加@EnableSystem 注解:
@SpringBootApplication
public class SpringbootEnableAnnotationApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableAnnotationApplication.class, args);
String fileExtension = context.getBean("fileExtension", String.class);
System.out.println("fileExtension=" + fileExtension);
}
}
运行结果,抛出异常:
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'fileExtension' available
- 在启动类上加@EnableSystem 注解:
@SpringBootApplication
@EnableSystem
public class SpringbootEnableAnnotationApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableAnnotationApplication.class, args);
String fileExtension = context.getBean("fileExtension", String.class);
System.out.println("fileExtension=" + fileExtension);
}
}
运行结果,正常:
fileExtension=.bat
这样就实现了一个自定义@EnableSystem模块装配功能了,如果,我们想自动装配这个模块,连注解都不想使用,可不可以呢,答案是可以的。
自动装配
实现自动装备步骤:
在resources目录下,新建META-INF文件夹,然后创建spring.factories文件:
spring.factories:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.annoenable.SystemConfiguration
在启动类中注释掉@EnableSystem注解,启动:
@SpringBootApplication
//@EnableSystem
public class SpringbootEnableAnnotationApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableAnnotationApplication.class, args);
String fileExtension = context.getBean("fileExtension", String.class);
System.out.println("fileExtension=" + fileExtension);
}
}
运行结果:
fileExtension=.bat
可以看到是正常运行了,可是.bat是Windows系统的可执行文件后缀名,如果是Linux系统,我不想加载这个Bean了,怎么办呢?
自定义注解@ConditionalOnXXX
想要解决上面的区分Windows和Linux系统问题,我们可以再自定义一个条件装配注解,步骤如下:
在@EnableSystem注解包下,再自定义一个@ConditionalOnSystem注解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({SystemCondition.class})
public @interface ConditionalOnSystem {
String value();
}
自定义注解主要是依赖@Conditional注解,它可以根据代码中设置的条件装载不同的bean。不过既然要使用@Conditional注解,就要自定义一个类实现Condition,编写自己的过滤条件:
public class SystemCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
// 获取@ConditionalOnSystem注解的所有注解元素
Map<String, Object> attributes = annotatedTypeMetadata.getAnnotationAttributes(ConditionalOnSystem.class.getName());
//获取注解元素value的属性值
String value = (String) attributes.get("value");
// 判断传入是不是windows
return "windows".equals(value);
}
}
重新修改一下SystemConfiguration中的代码:
@Configuration
public class SystemConfiguration {
@Bean
@ConditionalOnSystem("windows")
public String windowsFileExtension() {
return ".bat";
}
}
测试一下:
public class SpringbootEnableAnnotationApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableAnnotationApplication.class, args);
boolean b = context.containsBean("windowsFileExtension");
System.out.println("windowsFileExtension : " + b);
}
}
运行结果:
windowsFileExtension : true
如果 @ConditionalOnSystem(“windows”)的值不是Windows,就不会加载这个Bean了。
三、自定义基于接口驱动的@EnableXXX
以上,基于注解驱动的@EnableXXX装配模块感觉很方便了,但是总感觉不够灵活,那么接下来再实现一下,使用自定义基于接口驱动的@EnableXXX装配模块吧。
声明:以下工程和自定义基于注解驱动的@EnableXXX操作类型,重复地方,不再赘述。
- 创建一个spring boot项目,项目结构如下:
2. 创建WindowsSystemConfiguration配置类:
@Configuration
public class WindowsSystemConfiguration {
@Bean
public String fileExtension() {
return ".bat";
}
}
- 创建LinuxSystemConfiguration配置类:
@Configuration
public class LinuxSystemConfiguration {
@Bean
public String fileExtension() {
return ".sh";
}
}
- 编写自定义注解@EnableSystemSelector:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({SystemSelector.class})
public @interface EnableSystemSelector {
// 加载配置时,判断是Windows系统还是Linux系统
boolean isWindows() default true; //默认是Windows系统
}
可以看到,这里也是依赖@Import注解,上面已经分析过,自定义的SystemSelector类,是需要继承ImportSelector接口的。
- 实现自定义的SystemSelector类,处理@EnableSystemSelector注解元素数据:
public class SystemSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 获取@EnableSystemSelector注解的所有注解元素
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableSystemSelector.class.getName());
//获取注解元素isWindows的属性值
boolean isWindows = (boolean) annotationAttributes.get("isWindows");
//判断加载Windows还是linux相关配置类
return new String[]{isWindows ? WindowsSystemConfiguration.class.getName() : LinuxSystemConfiguration.class.getName()};
}
}
- 测试:
到此,自定义基于接口驱动的@EnableSystemSelector 就写好了,下面测试一下:
- 测试Windows配置
@SpringBootApplication
@EnableSystemSelector
public class SpringbootEnableInterfaceApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableInterfaceApplication.class, args);
String fileExtension = context.getBean("fileExtension", String.class);
System.out.println("fileExtension=" + fileExtension);
}
}
运行结果:
fileExtension=.bat
- 测试Linux配置
@SpringBootApplication
@EnableSystemSelector(isWindows = false) //把isWindows属性改成false
public class SpringbootEnableInterfaceApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringbootEnableInterfaceApplication.class, args);
String fileExtension = context.getBean("fileExtension", String.class);
System.out.println("fileExtension=" + fileExtension);
}
}
运行结果:
fileExtension=.sh
四、总结
到此,文章介绍了
- 自定义基于注解驱动的@EnableXXX模块装配功能;
- 自定义基于接口驱动的@EnableXXX模块装配功能。
两种实现方式,虽然演示的案例都十分简陋和不规范,但是核心实现代码还是可以参考一下的。