Spring Boot @Enable*注解源码解析及自定义@Enable*(转)

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

Spring Boot 常用@Enable*

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

@EnableAutoConfiguration 开启自动扫描装配Bean,组合成@SpringBootApplication注解之一

@EnableTransactionManagement 开启注解式事务的支持。

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

@Enable*的源码解析

@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 {};
}

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

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

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

public interface ImportSelector {
    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

public interface ImportBeanDefinitionRegistrar {
    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        this.registerBeanDefinitions(importingClassMetadata, registry);
    }

    default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    }
}

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

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

首先我们从上面截图可以看到ImportBeanDefinitionRegistrar接口类中 registerBeanDefinitions方法多了一个参数 BeanDefinitionRegistry(点击这个参数进入看这个参数的Javadoc,可以知道,它是用于保存bean定义的注册表的接口),所以如果是实现了这个接口类的首先可以应用比较复杂的注册类的判断条件,例如:可以判断之前的类是否有注册到 Spring 中了。另外就是实现了这个接口类能修改或新增 Spring 类定义BeanDefinition的一些属性(查看其中一个实现了这个接口例子如:AspectJAutoProxyRegistrar,追查 BeanDefinitionRegistry参数可以查看到)。

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

自定义@Enable*注解(EnableSelfBean)

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

先创建一个spring boot项目。
创建包entity,并新建类Role,将其放入到entity包中。

public class Role {
    public String test(){
        return "hello";
    }

创建自定义配置类SelfEnableAutoConfig并实现ImportSelector接口。其中使用到ClassUtils类是用来获取自己某个包下的所有类的名称的。

/**
 * 自己的定义的自动注解配置类
 **/
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
 **/
   @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;   
      }   

创建自定义注解类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 "";
   }

创建启动类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的一些基础东西,如:

@Autowired是默认通过by type(即类对象)得到注册的类,如果有多个实现才使用by name来确定。
所有注册的类的信息存储在ApplicationContext中,可以通过ApplicationContext得到注册类,
Spring boot中如果@ComponentScan没有,则默认是指扫描当前启动类所在的包里的对象。

Leave a Reply

Your email address will not be published. Required fields are marked *

lWoHvYe 无悔,专一