Spring Boot 一个重要的特点就是自动配置,约定大于配置,几乎所有组件使用其本身约定好的默认配置就可以使用,大大减轻配置的麻烦。其实现自动配置一个方式就是使用@Enable*注解,见其名知其意也,即“使什么可用或开启什么的支持”。

Spring Boot 常用@Enable*

首先来简单介绍一下Spring Boot 常用的@Enable*注解及其作用吧。

  1. @EnableAutoConfiguration 开启自动扫描装配Bean,组合成@SpringBootApplication注解之一
  2. @EnableScheduling 开启计划任务的支持
  3. @EnableTransactionManagement 开启注解式事务的支持。
  4. @EnableCaching 开启注解式的缓存支持。
  5. @EnableAspectJAutoProxy 开启对AspectJ自动代理的支持。
  6. @EnableEurekaServer 开启Euraka Service 的支持,开启spring cloud的服务注册与发现
  7. @EnableDiscoveryClient 开启服务提供者或消费者,客户端的支持,用来注册服务或连接到如Eureka之类的注册中心
  8. @EnableFeignClients 开启Feign功能

还有一些不常用的比如:

  1. @EnableAsync 开启异步方法的支持
  2. @EnableWebMvc 开启Web MVC的配置支持。
  3. @EnableConfigurationProperties 开启对@ConfigurationProperties注解配置Bean的支持。
  4. @EnableJpaRepositories 开启对Spring Data JPA Repository的支持。

参考:http://tangxiaolin.com/learn/show?id=402881d2648c88cc01648c89d8730001

@Enable*的源码解析

查看它们的源码

@EnableAutoConfiguration

@EnableCaching 开启注解式的缓存支持。

@EnableDiscoveryClient(@EnableEurekaServer 也是使用了这个组合注解) 开启服务提供者或消费者,客户端的支持,用来注册服务或连接到如Eureka之类的注册中心

@EnableAspectJAutoProxy 开启对AspectJ自动代理的支持。

@EnableFeignClients 开启Feign功能

@EnableScheduling(这个比较特殊,为自己直接新建相关类,不继承Selector和Registrar) 开启计划任务的支持

源码规律及解析

可以发现它们都使用了@Import注解(其中@Target:注解的作用目标,@Retention:注解的保留位置,@Inherited:说明子类可以继承父类中的该注解,@Document:说明该注解将被包含在javadoc中)

该元注解是被用来整合所有在@Configuration注解中定义的bean配置,即相当于我们将多个XML配置文件导入到单个文件的情形。

而它们所引入的配置类,主要分为Selector和Registrar,其分别实现了ImportSelectorImportBeanDefinitionRegistrar接口,

两个的大概意思都是说,会根据AnnotationMetadata元数据注册bean类,即返回的Bean 会自动的被注入,被Spring所管理。

既然他们功能都相同,都是用来返回类,为什么 Spring 有这两种不同的接口类的呢?

其实刚开始的时候我也以为它们功能应该都是一样的,后面我在组内分享的时候,我的导师就问了我这个问题,然后当时我没有留意这个点所以答不出来。后面回去细看了一下和搜索了相关资料,发现它们的功能有些细微差别。首先我们从上面截图可以清楚地看到ImportBeanDefinitionRegistrar接口类中 registerBeanDefinitions方法多了一个参数 BeanDefinitionRegistry(点击这个参数进入看这个参数的Javadoc,可以知道,它是用于保存bean定义的注册表的接口),所以如果是实现了这个接口类的首先可以应用比较复杂的注册类的判断条件,例如:可以判断之前的类是否有注册到 Spring 中了。另外就是实现了这个接口类能修改或新增 Spring 类定义BeanDefinition的一些属性(查看其中一个实现了这个接口例子如:AspectJAutoProxyRegistrar,追查 BeanDefinitionRegistry参数可以查看到)。

具体实现例子@EnableDiscoveryClient

可以看一下具体的一个例子在@EnableDiscoveryClient引入了EnableDiscoveryClientImportSelector,通过查看其继承实现图

可以看到其最终实现了ImportSelector接口,查看其具体实现源码

知道其先得到父类注册的bean类,然后如果在查看AnnotationMetadata中是否存在autoRegister,是否需要注册该类,如果存在,则继续将org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration该类返回,加入到spring容器中。

源码小结

通过查看@Enable*源码,我们可以清楚知道其实现自动配置的方式的底层就是通过@Import注解引入相关配置类,然后再在配置类将所需的bean注册到spring容器中和实现组件相关处理逻辑去。

自定义@Enable*注解(EnableSelfBean)

  在这里我们利用@Import和ImportSelector动手自定义一个自己的EnableSelfBean。该Enable注解可以将某些包下的所有类自动注册到spring容器中,对于一些实体类的项目很多的情况下,可以考虑一下通过这种方式将某包下所有类自动加入到spring容器,不再需要每个类再加上@Component等注解。

  1. 先创建一个spring boot项目。
  2. 创建包entity,并新建类Role,将其放入到entity包中。
/**
* 测试自己的自动注解的实体类
* @author zhangcanlong
* @since 2019/2/14 10:41
**/
public class Role {
public String test(){
return "hello";
}
}
  1. 创建自定义配置类SelfEnableAutoConfig并实现ImportSelector接口。其中使用到ClassUtils类是用来获取自己某个包下的所有类的名称的。
/**
* 自己的定义的自动注解配置类
* @author zhangcanlong
* @since 2019/2/14 10:45
**/
public class SelfEnableAutoConfig implements ImportSelector {
Logger logger = LoggerFactory.getLogger(SelfEnableAutoConfig.class);
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//获取EnableEcho注解的所有属性的value
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(EnableSelfBean.class.getName());
if(attributes==null){return new String[0]; }
//获取package属性的value
String[] packages = (String[]) attributes.get("packages");
if(packages==null || packages.length<=0 || StringUtils.isEmpty(packages[0])){
return new String[0];
}
logger.info("加载该包所有类到spring容器中的包名为:"+ Arrays.toString(packages));
Set<String> classNames = new HashSet<>(); for(String packageName:packages){
classNames.addAll(ClassUtils.getClassName(packageName,true));
}
//将类打印到日志中
for(String className:classNames){
logger.info(className+"加载到spring容器中");
}
String[] returnClassNames = new String[classNames.size()];
returnClassNames= classNames.toArray(returnClassNames);
return returnClassNames;
}
}

ClassUtil类

/**
* 获取所有包下的类名的工具类。参考:https://my.oschina.net/cnlw/blog/299265
* @author zhangcanlong
* @since 2019/2/14
**/
@Component
public class ClassUtils {
private static final String FILE_STR= "file";
private static final String JAR_STR = "jar"; /**
* 获取某包下所有类
* @param packageName 包名
* @param isRecursion 是否遍历子包
* @return 类的完整名称
*/
public static Set<String> getClassName(String packageName, boolean isRecursion) {
Set<String> classNames = null;
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String packagePath = packageName.replace(".", "/");
URL url = loader.getResource(packagePath);
if (url != null) {
String protocol = url.getProtocol();
if (FILE_STR.equals(protocol)) {
classNames = getClassNameFromDir(url.getPath(), packageName, isRecursion);
} else if (JAR_STR.equals(protocol)) {
JarFile jarFile = null;
try{
jarFile = ((JarURLConnection) url.openConnection()).getJarFile();
} catch(Exception e){
e.printStackTrace();
}
if(jarFile != null){
getClassNameFromJar(jarFile.entries(), packageName, isRecursion);
}
}
} else {
/*从所有的jar包中查找包名*/
classNames = getClassNameFromJars(((URLClassLoader)loader).getURLs(), packageName, isRecursion);
}
return classNames;
} /**
* 从项目文件获取某包下所有类
* @param filePath 文件路径
* @param isRecursion 是否遍历子包
* @return 类的完整名称
*/
private static Set<String> getClassNameFromDir(String filePath, String packageName, boolean isRecursion) {
Set<String> className = new HashSet<>();
File file = new File(filePath);
File[] files = file.listFiles();
if(files==null){return className;}
for (File childFile : files) {
if (childFile.isDirectory()) {
if (isRecursion) {
className.addAll(getClassNameFromDir(childFile.getPath(), packageName+"."+childFile.getName(), isRecursion));
}
} else {
String fileName = childFile.getName();
if (fileName.endsWith(".class") && !fileName.contains("$")) {
className.add(packageName+ "." + fileName.replace(".class", ""));
}
}
}
return className;
} private static Set<String> getClassNameFromJar(Enumeration<JarEntry> jarEntries, String packageName, boolean isRecursion){
Set<String> classNames = new HashSet<>(); while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
if(!jarEntry.isDirectory()){
String entryName = jarEntry.getName().replace("/", ".");
if (entryName.endsWith(".class") && !entryName.contains("$") && entryName.startsWith(packageName)) {
entryName = entryName.replace(".class", "");
if(isRecursion){
classNames.add(entryName);
} else if(!entryName.replace(packageName+".", "").contains(".")){
classNames.add(entryName);
}
}
}
}
return classNames;
} /**
* 从所有jar中搜索该包,并获取该包下所有类
* @param urls URL集合
* @param packageName 包路径
* @param isRecursion 是否遍历子包
* @return 类的完整名称
*/
private static Set<String> getClassNameFromJars(URL[] urls, String packageName, boolean isRecursion) {
Set<String> classNames = new HashSet<>();
for (URL url : urls) {
String classPath = url.getPath();
//不必搜索classes文件夹
if (classPath.endsWith("classes/")) {
continue;
}
JarFile jarFile = null;
try {
jarFile = new JarFile(classPath.substring(classPath.indexOf("/")));
} catch (IOException e) {
e.printStackTrace();
}
if (jarFile != null) {
classNames.addAll(getClassNameFromJar(jarFile.entries(), packageName, isRecursion));
}
}
return classNames;
}
}
  1. 创建自定义注解类EnableSelfBean
/**
* 自定义注解类,将某个包下的所有类自动加载到spring 容器中,不管有没有注解,并打印出
* @author zhangcanlong
* @since 2019/2/14 10:42
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(SelfEnableAutoConfig.class)
public @interface EnableSelfBean {
//传入包名
String[] packages() default "";
}
  1. 创建启动类SpringBootEnableApplication
@SpringBootApplication
@EnableSelfBean(packages = "com.kanlon.entity")
public class SpringBootEnableApplication {
@Autowired
Role abc; public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringBootEnableApplication.class, args);
//打印出所有spring中注册的bean
String[] allBeans = context.getBeanDefinitionNames();
for(String bean:allBeans){
System.out.println(bean);
}
System.out.println("已注册Role:"+context.getBean(Role.class));
SpringBootEnableApplication application = context.getBean(SpringBootEnableApplication.class);
System.out.println("Role的测试方法:"+application.abc.test());
}
}

启动类测试的一些感悟:重新复习了回了spring的一些基础东西,如:

  1. @Autowired是默认通过by type(即类对象)得到注册的类,如果有多个实现才使用by name来确定。
  2. 所有注册的类的信息存储在ApplicationContext中,可以通过ApplicationContext得到注册类,这个是很基础的,但是真的很久没看,没想到竟然又忘记了。
  3. Spring boot中如果@ComponentScan没有,则默认是指扫描当前启动类所在的包里的对象。

自定义Enable注解源码地址:https://github.com/KANLON/practice/tree/master/spring-boot-enable

参考:

  1. http://tangxiaolin.com/learn/show?id=402881d2648c88cc01648c89d8730001
  2. SpringBoot @Enable* 注解 https://segmentfault.com/a/1190000015188776
  3. 获取指定包名下的所有类的类名(全名) https://my.oschina.net/cnlw/blog/299265)
  4. Spring-Boot之@Enable*注解的工作原理 https://www.jianshu.com/p/3da069bd865c
  5. Spring源码解析------@Import注解解析与ImportSelector,ImportBeanDefinitionRegistrar以及DeferredImportSelector区别 https://www.xiaoquan.work/articles/2020/01/03/1578016154544.html
  6. @import和@Bean的区别,以及ImportSelector和ImportBeanDefinitionRegistrar两个接口的简单实用 https://blog.csdn.net/qq_22701869/article/details/102561494

Spring Boot @Enable*注解源码解析及自定义@Enable*的更多相关文章

  1. Spring Boot系列(三):Spring Boot整合Mybatis源码解析

    一.Mybatis回顾 1.MyBatis介绍 Mybatis是一个半ORM框架,它使用简单的 XML 或注解用于配置和原始映射,将接口和Java的POJOs(普通的Java 对象)映射成数据库中的记 ...

  2. Spring Boot自动配置源码解析(基于Spring Boot 2.0.2.RELEASE)

    在Spring Boot官方介绍中,首一段话是这样的(如下图).我们可以大概了解到其所表达的含义:我们可以利用Spring Boot写很少的配置来创建一个非常方便的基于Spring整合第三方类库的单体 ...

  3. Spring Boot入门,源码解析

    目录 1.Spring Boot简介 2.微服务 3.Spring Boot HelloWorld 3.1 创建一个Maven工程 3.2 导入依赖Spring Boot相关的依赖 3.3 编写一个主 ...

  4. SpringBoot的条件注解源码解析

    SpringBoot的条件注解源码解析 @ConditionalOnBean.@ConditionalOnMissingBean 启动项目 会在ConfigurationClassBeanDefini ...

  5. Spring Boot 自动配置 源码分析

    Spring Boot 最大的特点(亮点)就是自动配置 AutoConfiguration 下面,先说一下 @EnableAutoConfiguration ,然后再看源代码,到底自动配置是怎么配置的 ...

  6. spring boot 2.0 源码分析(一)

    在学习spring boot 2.0源码之前,我们先利用spring initializr快速地创建一个基本的简单的示例: 1.先从创建示例中的main函数开始读起: package com.exam ...

  7. spring boot 2.0 源码分析(二)

    在上一章学习了spring boot 2.0启动的大概流程以后,今天我们来深挖一下SpringApplication实例变量的run函数. 先把这段run函数的代码贴出来: /** * Run the ...

  8. spring boot 2.0 源码分析(四)

    在上一章的源码分析里,我们知道了spring boot 2.0中的环境是如何区分普通环境和web环境的,以及如何准备运行时环境和应用上下文的,今天我们继续分析一下run函数接下来又做了那些事情.先把r ...

  9. spring boot 2.0 源码分析(三)

    通过上一章的源码分析,我们知道了spring boot里面的listeners到底是什么(META-INF/spring.factories定义的资源的实例),以及它是创建和启动的,今天我们继续深入分 ...

随机推荐

  1. 【Linux】rsync中sending incremental file list时间优化

    每次使用rsync的时候,前面出现sending incremental file list 这句之后要等待很长时间 查了很多帖子和官方文档后,发现是-c这个选项的问题, -v, --verbose ...

  2. 攻防世界—pwn—guess_num

    题目分析 checksec检查文件保护机制 这个结果看的我满是问号??? \n ida分析程序 是一个猜数字的游戏,需要全部输入正确才能得到flag 根据大佬的wp得出一下内容 先使用srand()进 ...

  3. top有用的开关控制命令

    [原创]本文为原创博文,转发请注明出处:https://www.cnblogs.com/dingbj/p/top_command.html 今天偶然用到top命令,在动态刷新的界面上输入h顺便看了下帮 ...

  4. RocketMQ在linx安装及其有关问题解决

    Linx安装和使用: rocketmq官网:http://rocketmq.apache.org/ 首先安装JDK(推荐使用JDK1.8),并配置环境变量 下载rocketmq压碎包并解压到指定目录 ...

  5. centos7安装docker、docker-compose、es7.3.0、kibana7.3.0

    一.安装docker 1.更新yum包 sudo yum update 2.卸载旧版本(如果安装过旧版本的话) sudo yum remove docker docker-common docker- ...

  6. VMware中安装Ubuntu后,安装VMwareTools提示“Not enough free space to extract VMwareTools-10.3.10-13959562.tar.gz”的解决办法

    将加载后的Vmware Tools中的*.tar.gz文件复制到桌面后提取,否则会报错:

  7. idea maven package报错"不再支持源选项 5 请使用 6 或更高版本。不支持发行版本 5"

    解决办法: 1.确保java compile以及project和module的java字节码版本是所用的java版本:

  8. 三路握手 三报文握手 TIME_WAIT three way handshake three message handshake

    UNIX网络编程卷1:套接字联网API(第3版) 第2章 传输层:TCP.UDP和SCTP 2.4 TCP TCP不能被描述成100%可靠的协议 数次重传失败,则放弃 数据的可靠递送和故障的可靠通知 ...

  9. 从源码解析Nginx对 Native aio支持_运维_youbingchen的博客-CSDN博客 https://blog.csdn.net/youbingchen/article/details/51767587

    从源码解析Nginx对 Native aio支持_运维_youbingchen的博客-CSDN博客 https://blog.csdn.net/youbingchen/article/details/ ...

  10. 圣诞快乐!OIer挂分小技巧

    OIer常犯错误 自己的错误 循环里套return 线段树求和 int 定义,下传 int 定义 cmp<,>号分不清 主观行为举动错误 踢电源线,注意安全(_Destiny) TLE 大 ...