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. missing tables and indexes的处理办法

    最近做了SAP系统的异构迁移,顺便把oracle DB也升级了,从10g升级到11g,但是升级后,在DB02或者是ora_space中的diagnostics->Missing Tables a ...

  2. 深度学习DeepLearning技术实战(12月18日---21日)

    12月线上课程报名中 深度学习DeepLearning(Python)实战培训班 时间地点: 2020 年 12 月 18 日-2020 年 12 月 21日 (第一天报到 授课三天:提前环境部署 电 ...

  3. 牛逼!MySQL 8.0 中的索引可以隐藏了…

    MySQL 8.0 虽然发布很久了,但可能大家都停留在 5.7.x,甚至更老,其实 MySQL 8.0 新增了许多重磅新特性,比如栈长今天要介绍的 "隐藏索引" 或者 " ...

  4. 转 Jmeter测试实践:文件下载接口

    Jmeter测试实践:文件下载接口   一 Jmeter步骤 1.打开jmeter4.0,新建测试计划,添加线程组.根据实际情况配置线程属性. 2.添加HTTP请求.根据接口文档进行配置. Basic ...

  5. JavaScript中创建数组的方式!

    JavaScript中创建数组的方式! 利用数组字面量 // 1 直接量 console.log(Array.prototype); var arr = [1, 2, 4, 87432]; // 注意 ...

  6. ESXI6.7主机降级至ESXI6.5

    上一条博客vcenter添加主机失败:https://www.cnblogs.com/Crazy-Liu/p/11211760.html 原因esxi主机和vcenter版本不一致,因为vcenter ...

  7. 关掉IE提示“当前安全设置会使计算机有风险”

    我们先来看一下IE浏览器出现的提示窗口,该窗口位于最顶端,不点击设置的话,无法进行下一步的操作. 这时我们点击开始按钮 ,在弹出菜单中选择"运行"菜单项. 在打开的Windows运 ...

  8. IDEA_2019.1版本中Protobuf的使用

    一.Protobuf是什么 Protobuf 是 Google 发布的开源项目,全称 Google Protocol(/'prəʊtəkɒl/,协议,草案) Buffers,是一种轻便高效的结构化数据 ...

  9. 关于redis搭建环境

    首先,window键+r 输入cmd进入dos命名窗口,我的redis是装在了d盘,so我得输入cd:或者d:进入d盘,cd\redis文件夹路径,这样的话,直接输入  redis-server -- ...

  10. PostgreSQL创建只读权限的用户

    1.创建只读角色 CREATE ROLE readaccess; 2.授予对现有表的访问权限 GRANT USAGE ON SCHEMA public TO readaccess; GRANT SEL ...