在Spring 和 SpringBoot 中有很多这样的注解,例如常见的:@EnableAsync
、 @EnableCaching
、@EnableConfigurationProperties
。
每引用一个starer 几乎都有一个 @Enable*相关的注解。
这一注解的作用:就是用来启用某一个功能的配置。启用某一功能,仅需要加上一个注解即可生效,可以使组建之间的相互依赖降低了耦合性。
例如:@EnableAsync
注解启用异步功能,在SpringBoot中如果没有启用这个注解,直接在使用 @Async
是没法起到异步执行的作用的。所有使用这个功能,就先启用 @EnableAsync
,不然他的相关配置不生效,也就是配置了@Async 注解,也没有人来管它。
一个简单的注解,能带动一个某一个功能模块甚至带动一整个框架的,比如:@SpringBootApplication。来分析一下这个 @Enable*注解的原理。
其实 @Enable注解很简单,随便找一个注解,点进去一看就能恍然大悟,它的所有核心 都在 *@Import 注解当中。 所有真正核心的 是 @Import**注解,由它去加载它自己对应的配置类,然后启动他的功能。
所以 @Enbale 注解就是没有啥用的,就是营造一种 *高大上的、又神奇装牛X的 感觉。所以它有啥用?其实它没有用。你看既然 使用 @Import注解就可以了,还要它干嘛,我直接使用 @Import注解去加载就好了**,这不是多此一举吗?
真的没有用吗? 有用的!! 存在即合理
举个例子:@EnableAutoConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
1、除了桥梁的作用,它还可以携带上一些参数:在解析处理这个 注解的时候,可以从这里拿到一些 自定义配置的参数,去做相关的操作。
2、除了 @Import 注解还有其它的注解 @AutoConfigurationPackage ,它可以组合多个注解放到一起。
3、高大上又神秘高级,又方面你开发者去使用。假如:启用这个功能可能需要更多的 类加载,还有要其它注解去配和,如果不将其包装到 @Enable*中,那对开发者来说,配置起来又相对麻烦了许多,将其包装到一起,只需要记住使用这一功能记住这个注解即可。极大的方面!!。
4、将功能做组建抽离开来,降低耦合性。
所以此重点就不再是 @Enable* 注解了,而是 @Import
注解了,挂羊头卖狗肉!
1、@Import 注解的作用
我们看一下这个注解的源码解释
/** @author Chris Beams
* @author Juergen Hoeller
* @since 3.0
* @see Configuration
* @see ImportSelector
* @see ImportResource
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*
* 这个value值 有三种类型:
* - 其它的常规类型,就相当于Configuration配置累解析
* - ImportSelector
* - ImportBeanDefinitionRegistrar
*
*
*/
Class<?>[] value();
}
@Import 注解导入的类 有三种,分别是:ImportSelector、 ImportBeanDefinitionRegistrar、一种是普通各类,会当作为配置类去处理。
@Import 注解的作用可以用来将 JavaBean 注入到IOC容器中。
1、比如:
@Import({UserService.class})
注入一个JavaBean到 IOC容器中。
2、又比如:
@Import({XXConfig.class})
导入一个配置类,然后继续扩展解析这个配置类。
3、又比如: @Import({XXImportSelector.class})
/**
* <br>
*
* @author 永健
* @since 2020-09-27 13:50
*/
public class MyImportSelector implements ImportSelector
{
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata)
{
Class<?> zClass = null;
Set<String> annotationTypes = importingClassMetadata.getAnnotationTypes();
Iterator<String> zClasses = annotationTypes.iterator();
while (zClasses.hasNext())
{
String type = zClasses.next();
if (type.equals(EnableConfig.class.getName()))
{
Map<String, Object> annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableConfig.class.getName());
zClass = (Class<?>) annotationAttributes.get("zClass");
}
}
return new String[]{zClass.getName()};
}
}
导入实现ImportSelector的类,这个实现可以拿到许多相关注解,然后可以取对应注解上的值,进行你所想要的操作。
4、还比如:
@Import({XXBeanDefinitionRegistry.class})
导入 BeanDefinitionRegistry 的实现类。该接口是Spring中定义的bean的注册类。所以的Bean注册都会走这个类。所以导入一个BeanDefinitionRegistry的实现类,可以自定义实现动态注册 bean的操作。
2、源码了解
如下是处理 @Import注解的的方法
// 处理 @Import 注解,加载某个类,将其假如IOC容器中 擦数: 当前的配置类,当前的源码类,导入的类
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
if (importCandidates.isEmpty()) {
return;
}
if (checkForCircularImports && isChainedImportOnStack(configClass)) {
this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
}
else {
this.importStack.push(configClass);
try {
// @Import 注解 可以配置多个类没循环遍历
// 该注解上面说了 有三种类型,遍历做分支处理
for (SourceClass candidate : importCandidates) {
// 三种类行,做分支判断
if (candidate.isAssignable(ImportSelector.class)) {
// Candidate class is an ImportSelector -> delegate to it to determine imports
Class<?> candidateClass = candidate.loadClass();
ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
ParserStrategyUtils.invokeAwareMethods(
selector, this.environment, this.resourceLoader, this.registry);
//延迟导入处理
if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
this.deferredImportSelectors.add(
new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
}else {
// 执行 ImportSelector 接口的方法,实现了该ImportSelector的方法,拿到要注入的类全名
String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
// 返回来的转为一个配置类去加载
Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
// 继续递归检查
processImports(configClass, currentSourceClass, importSourceClasses, false);
}
}
else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
// 处理 ImportBeanDefinitionRegistrar 的实现类
// Candidate class is an ImportBeanDefinitionRegistrar ->
// delegate to it to register additional bean definitions
Class<?> candidateClass = candidate.loadClass();
ImportBeanDefinitionRegistrar registrar =
BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
ParserStrategyUtils.invokeAwareMethods(
registrar, this.environment, this.resourceLoader, this.registry);
configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
}
else {
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
// 当作配置类解析
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
processConfigurationClass(candidate.asConfigClass(configClass));
}
}
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
finally {
this.importStack.pop();
}
}
}
这个源码了解就到这里。
3、利用 @Enable* 自己也装一次高大上。
我们来写一个启用切面打印日志功能。
我们新建一个项目,作为一个starter , 一个jar包。 然后其他项目引入这个 jar启用日志打印的操作。
项目如下:
![v2-a228903e207405c3b19863254a231f42_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-a228903e207405c3b19863254a231f42_b.jpg)
1、EnablePrintRequestLog.java
开启日切面功能的注解。
/**
* <p>
*
* </p>
*
* @author 永健
* @since 2020-11-10 19:15
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({LogPrintImportSelector.class, MyImportBeanDefinitionRegistrar.class, TestBean.class})
public @interface EnablePrintRequestLog
{
/**
* 拦截哪个包
*
* @return
*/
String targetPackage() default "";
}
2、SysAspect.java
切面类, 此处只拦截 自定义的包下的加了 @RestController 和 @Controller 注解的类。
/**
* <p>
*
* </p>
*
* @author 永健
* @since 2020-11-10 10:15
*/
@Aspect
@Component
public class SysAspect extends Rules
{
@Autowired
List<Interceptor> interceptor;
/**
* 方法规则拦截
* 此处只拦截 加了 @RestController 和 @Controller 注解的类
*
*/
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)||@within(org.springframework.stereotype.Controller)")
public void controllerAspect()
{
}
@Before("controllerAspect()")
public void before(JoinPoint joinPoint)
{
interceptor.forEach(j -> {
if (getPackages().equals(getTargetClassPackage(joinPoint)))
{
try
{
j.before(joinPoint);
} catch (Exception e)
{
e.printStackTrace();
}
}
});
}
private String getTargetClassPackage(JoinPoint point)
{
//拦截的实体类
Object target = point.getTarget();
//拦截的方法名称
String methodName = point.getSignature().getName();
String simpleName = target.getClass().getSimpleName();
return simpleName.substring(simpleName.lastIndexOf(".") - 1);
}
@After(value = "controllerAspect()")
public void after(JoinPoint joinPoint)
{
interceptor.forEach(j -> {
if (getPackages().equals(getTargetClassPackage(joinPoint)))
{
try
{
j.after(joinPoint);
} catch (Exception e)
{
e.printStackTrace();
}
}
});
}
@AfterReturning(value = "controllerAspect()")
public void afterReturning(JoinPoint joinPoint)
{
interceptor.forEach(j -> {
if (getPackages().equals(getTargetClassPackage(joinPoint)))
{
try
{
j.afterReturning(joinPoint);
} catch (Exception e)
{
e.printStackTrace();
}
}
});
}
/**
* 异常通知
*/
@AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e)
{
interceptor.forEach(j -> {
if (getPackages().equals(getTargetClassPackage(joinPoint)))
{
try
{
j.doAfterThrowing(joinPoint, e);
} catch (Exception ex)
{
ex.printStackTrace();
}
}
});
}
@Around(value = "controllerAspect()")
public void around(JoinPoint joinPoint)
{
interceptor.forEach(j -> {
if (getPackages().equals(getTargetClassPackage(joinPoint)))
{
try
{
j.around(joinPoint);
} catch (Exception e)
{
e.printStackTrace();
}
}
});
}
}
3、Interceptor.java
为了扩展性,我留了切面的 5种方法的接口:Interceptor.jarva
/**
* <br>
*
* @author 永健
* @since 2020-11-10 19:18
*/
public interface Interceptor
{
void before(JoinPoint joinPoint);
void after(JoinPoint joinPoint);
void doAfterThrowing(JoinPoint joinPoint,Throwable e);
void afterReturning(JoinPoint joinPoint);
void around(ProceedingJoinPoint joinPoint);
}
默认一个实现打印日志:需要扩展继续实现该接口,比如我需要存储记录日志的信息。请求的时候会打印如下信息。
/**
* <br>
*
* @author 永健
* @since 2020-11-10 19:20
*/
public class DefaultInterceptor implements Interceptor{
private static final Logger LOGGER = LoggerFactory.getLogger(SysAspect.class);
@Override
public void before(JoinPoint joinPoint) {
LOGGER.info("######## {} 请求开始 " + LocalDateTime.now() + " ###############", Thread.currentThread().getName());
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
LOGGER.info("######## {} 请求的方法--> " + className + "/" + methodName + "()" + "/", Thread.currentThread().getName());
}
@Override
public void after(JoinPoint joinPoint) {
LOGGER.info("######## 结束了");
}
@Override
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
LOGGER.error("异常了");
}
@Override
public void afterReturning(JoinPoint joinPoint) {
LOGGER.info("######## afterReturning");
}
@Override
public void around(ProceedingJoinPoint joinPoint){
System.out.println();
LOGGER.info("######## around");
}
}
4、LogPrintImportSelector.java
实现 ImportSelector,主要用来拿到注解 @EnablePrintRequestLog 的参数 tagetPackage 的参数。并且加载其全局配置类:GlobConfig.java
5、GlobConfig.java
该类的可以做所有的配置,将所需要的Bean 放到这里注入到IOC容器中,此处为了 测试 @Import注解的使用,i u 没有放在这里。
/**
* <br>
*
* @author 永健
* @since 2020-11-10 18:58
*/
@Configuration
// 扫描包,让其自动注入 好了
@ComponentScan("com.enable.demo")
public class GlobConfig
{
}
6、MyImportBeanDefinitionRegistrar.java
测试动态注册Bean,注册提供的默认切面打印日志类的实现类。
/**
* <br>
*
* @author 永健
* @since 2020-11-10 19:43
*/
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar
{
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
{
BeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClassName(DefaultInterceptor.class.getName());
registry.registerBeanDefinition("defaultInterceptor", beanDefinition);
}
}
6、Rules.java
主要用来存放 切面的拦截包路径。
/**
* <br>
*
* @author 永健
* @since 2020-11-10 19:29
*/
public abstract class Rules
{
private static String packages;
protected String getPackages(){
return packages;
}
protected Rules setPackages(String packages){
this.packages = packages;
return this;
}
}
7、TestBean.java
一个测试 @Import 导入普通Bean的类
/**
* <br>
*
* @author 永健
* @since 2020-11-10 19:49
*/
public class TestBean{
public void test(){
System.out.println("I'm test Bean");
}
}
8、TestController.java
测试对比 该类在 com.enable.demo 的包下。
/**
* <br>
*
* @author 永健
* @since 2020-11-10 20:22
*/
@RestController
public class TesController{
@GetMapping("/t")
public String tes() {
return "ok";
}
}
3、使用
将上面的项目打成一个 jar包,然后在新的项目中引入:如下:
![v2-db2ef067df0466ed0498aa837544827c_b.jpg](http://img-03.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-db2ef067df0466ed0498aa837544827c_b.jpg)
在新项目中的启动类上加上:该注解:@EnablePrintRequestLog(targetPackage = "generator.test.demo") 并且指定拦截的包名称。
![v2-ffa6c661d5043b78dfceacdfcdb7bb34_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-ffa6c661d5043b78dfceacdfcdb7bb34_b.jpg)
我们启动测试。
如上:我们定义了两个接口:
1、http://127.0.0.1:9999/test. 该接口在包引用者的包下:generator.test.demo
2、http://127.0.0.1:9999/t 该接口在本身的 jar 包内 :com.enable.demo
所以这时候应该只会有一个请求打印日志。
项目启动完毕。
![v2-ece99befd5fd0a02d8dbe789b12a855e_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-ece99befd5fd0a02d8dbe789b12a855e_b.jpg)
1、请求:http://127.0.0.1:9999/test接口,应该打印请求日志:结果如下:
![v2-70f2ce3fcd66616c489c6c63e25755b8_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-70f2ce3fcd66616c489c6c63e25755b8_b.jpg)
请求结果正确。
2、请求另外一个接口 http://127.0.0.1:9999/t :该接口不应该打印请求日志。
结果:正确
![v2-8c48ef685d46536bab6841f748de02c0_b.jpg](http://img-02.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-8c48ef685d46536bab6841f748de02c0_b.jpg)
3、重写预留的切面增强的方法
/**
* <br>
*
* @author 永健
* @since 2020-11-10 22:21
*/
@Component
public class YY extends DefaultInterceptor{
@Override
public void before(JoinPoint joinPoint){
System.out.println("重写切面前置增强方法");
}
}
看打印结果:
![v2-1f9156f9586c4bd6caecdc82af769024_b.jpg](http://img-02.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-1f9156f9586c4bd6caecdc82af769024_b.jpg)
3、去除 @EnablePrintRequestLog注解测试。
此时没有打印日志:正确
![v2-9a4d47fbf943f70f8e608c2fa66bb8e0_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic1.zhimg.com/v2-9a4d47fbf943f70f8e608c2fa66bb8e0_b.jpg)
总而言之,言而总之。利用@Enbale***的原理,你也可以很轻松的打造一个低耦合的组件功能,给别人很简单明了的使用,即使你的功能错中复杂,但是对于使用者来说,越少配置越好,拿来即用。
比如:你可以包装一个 权限登陆组件。以后哪个项目中使用到了,注解引入 jar 包 @Enable注解打开就可以使用,多么简单容易,不用重复去写代码.......
你学会了吗??????
https://zhuanlan.zhihu.com/p/156099918zhuanlan.zhihu.com
![v2-654eb72da6e950dad8adf3b9403d0a0f_b.jpg](http://img-01.proxy.5ce.com/view/image?&type=2&guid=37d96e63-e32f-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-654eb72da6e950dad8adf3b9403d0a0f_b.jpg)