Spring之@Import

Spring之@Import

前言

在平常开发中我们自己开发的组件通常我们可以通过Spring的XML配置文件注解(例如@Component)配置类(例如@Configuration)等方式将组件注入到容器中。但是通常情况下对于第三方开发的组件,我们很难通过上面的方式来完成。并且想动态的将组件注册到容器中,实现起也相对麻烦。例如下面的例子:

public class App {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(GlobalConfig.class);
        //获取com.buydeem.springimport.UserService实例
        System.out.println(context.getBean(UserService.class));
        //获取com.buydeem.package2.Person实例
        System.out.println(context.getBean(Person.class));

    }
}

@ComponentScan
@Configuration
class GlobalConfig{

}

@Component
public class Person {
}

@Component
public class UserService {
}
Spring之@Import
目录结果

在上面的代码示例中,容器中只能获取到UserService的实例,但是对于Person的实例并不能获取到。这是因为通常情况下,@ComponentScan注解扫描的包为单前配置类下的包,而Person实例与GlobalConfig并不在同一包下,而对于第三方组件就是这种情况。当然我们也可以通过设置@ComponentScan自定义扫描包来将Person实例注入到容器,不过对于推荐约定大于配置的今天来说,这种方式并不被推荐。

@Import

对于上面的示例中,因为Person类所在的包路径并不是包扫描的路径所以无法被注册到容器中,有没有什么简单的方式能将其注入到容器呢?最简单的方式就是通过@Import注解将Person导入到容器中完成注入。修改代码如下:

@ComponentScan
@Configuration
@Import(Person.class)
class GlobalConfig
{

}

通常情况下,@Import有三种使用方式:

  • 导入一个类作为Spring Bean注册到容器中
  • @Import注解和ImportSelector组合使用
  • @Import注解和ImportBeanDefinitionRegistrar组合使用

直接导入类注册到容器

前面示例中我们使用的就是这种方式,通过在注解@Import中设置导入类,将普通的一个类导入到容器中。不过需要注意的是,在Spring4.2之前是无法将一个普通的类导入到容器中,但是在Spring4.2之后这是允许的,关于这一点可以参考Spring官方文档中Using the @Import Annotation中的描述。

@ImportImportSelector组合使用

前面我们说过,@Import方式会更加灵活。但是目前为止,并没有何处体现出它的灵活之处,而使用ImportSelector我们可以根据相关环境来决定注入哪些类。该接口中只有一个方法selectImports,该返回一个字符串数组,数组中的值则是要注入的Spring Bean。需要注意的一点是,如果没有需要注入的组件,不能返回null,需要返回一个空的数组。例如现在有一个这样的需求,我们需要向容器中注入一个日志类的实现,这个日志需要根据相关设置动态的来注入。我们可以通过ImportSelector来完成。

  • 日志接口和实现
  public interface LogService {
  
      /**
       * 打印日志
       * @param log
       */

      void printLog(String log);
  }
  
  public class LogAServiceImpl implements LogService{
      @Override
      public void printLog(String log) {
          System.out.printf("日志A:[%s]",log);
      }
  }
  
  public class LogBServiceImpl implements LogService{
  
      @Override
      public void printLog(String log) {
          System.out.printf("日志B:[%s]",log);
      }
  }
  • LogImportSelectorEnableLog
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Import(value = {LogImportSelector.class})
  public @interface EnableLog 
{
  
      String value() default "a";
  }
  
  public class LogImportSelector implements ImportSelector {
  
      @Override
      public String[] selectImports(AnnotationMetadata importingClassMetadata) {
          Map<String, Object> map = importingClassMetadata.getAnnotationAttributes(EnableLog.class.getName(), true);
          String value = (String) map.get("value");
          if (Objects.equals(value,"a")){
              return new String[]{LogAServiceImpl.class.getName()};
          }
          return new String[]{LogBServiceImpl.class.getName()};
      }
  }
  • 测试实例类
  @EnableLog(value = "b")
  public class App {
      public static void main(String[] args) {
          ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
  
          LogService logService = context.getBean(LogService.class);
  
          logService.printLog("日志内容");
      }
  }

运行上面的代码,如果@EnableLog中的值为a时打印的结果为:

日志A:[日志内容]

如果@EnableLog中的值为b时打印的结果为:

日志B:[日志内容]

@EnableXXX的秘密

上面的示例代码中,LogImportSelector#selectImports()方法通过AnnotationMetadata获取到注解EnableLog中的值,根据这个值的配置来动态的确认注入哪个LogService的实现,而这种方式就是Spring中@EnableXXX的实现。例如@EnableAsync注解,一般在方法上面加上@Async注解,就可以让这个方法变成异步执行(简单的说就是使用线程中的一个线程来执行,而不是调用该方法的线程)。不过想要实现这样的效果,前提条件是需要使用@EnableAsync开启,下面我们来看看源码中是如何实现的。

Spring之@Import
AsyncConfigurationSelector

下面是AdviceModeImportSelector#selectImports()方法的实现

public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
    //获取到注解
   Class<?> annoType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
    //获取注解里面的值
   AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
   if (attributes == null) {
      throw new IllegalArgumentException(String.format(
         "@%s is not present on importing class '%s' as expected",
         annoType.getSimpleName(), importingClassMetadata.getClassName()));
   }
    //获取是基于JDK动态代理还是基于AspectJ做动态代理的值
   AdviceMode adviceMode = attributes.getEnum(this.getAdviceModeAttributeName());
    //该方法由子类实现
   String[] imports = selectImports(adviceMode);
   if (imports == null) {
      throw new IllegalArgumentException(String.format("Unknown AdviceMode: '%s'", adviceMode));
   }
   return imports;
}

上面的方法中还有一个抽象方法未实现protected abstract String[] selectImports(AdviceMode adviceMode),我们可以看AsyncConfigurationSelector中对该方法的实现。

 public String[] selectImports(AdviceMode adviceMode) {
  switch (adviceMode) {
   case PROXY:
    return new String[] { ProxyAsyncConfiguration.class.getName() };
   case ASPECTJ:
    return new String[] { ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME };
   default:
    return null;
  }
 }

上面代码的逻辑也比较简单,就是根据传入的AdviceMode枚举值来判断是使用哪种动态代理实现方式,从而注入哪个类到容器中。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync 
{
 Class<? extends Annotation> annotation() default Annotation.class;
 boolean proxyTargetClass() default false;
 AdviceMode mode() default AdviceMode.PROXY;
 int order() default Ordered.LOWEST_PRECEDENCE;
}

EnableAsync注解可以知道,在默认情况下AdviceMode的默认值为AdviceMode.PROXY也就是默认情况下是使用JDK动态代理。所以说默认情况下注入的Bean为ProxyAsyncConfiguration。简单的说就是,当你的方法使用了@Async之后,通过容器获得的Bean不是Bean本身,而是一个经过加强后的代理Bean。例如我们可以将LogService#printLog方法使用@Async标记,然后从容器中获取LogService并打印出它的类名,通过类名我们可以知道它是一个代理类。

@ImportImportBeanDefinitionRegistrar组合使用

通过前面的内容我们了解到,我们可以通过@Import注解导入一个配置类或者一个ImportSelector子类,同样还可以导入一个ImportBeanDefinitionRegistrar子类。如果导入的是一个普通类时,容器会创建一个Bean并注册到容器中。如果导入的是一个ImportSelector子类时,则会创建方法selectImports返回的类集合。而ImportBeanDefinitionRegistrar的功能同样如此,该接口只有一个方法registerBeanDefinitions,这个方法的特别之处在于该方法的入参提供了BeanDefinitionRegistry实例,而有了BeanDefinitionRegistry则意味着我们可以通过注册BeanDefinition的方式向容器中注入Bean。

public interface ImportBeanDefinitionRegistrar {
 public void registerBeanDefinitions(
   AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
;
}

简单应用

例如现在我们有一个Dog,我们通过ImportBeanDefinitionRegistrar导入的方式来完成注入。

//Dog类
public class Dog {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
//ImportBeanDefinitionRegistrar实现类
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AbstractBeanDefinition dogBd = BeanDefinitionBuilder.genericBeanDefinition(Dog.class)
                .addPropertyValue("name", "大黄")
                .getBeanDefinition()
;
        registry.registerBeanDefinition("dog",dogBd);
    }
}
//使用示例
@Import(value = MyImportBeanDefinitionRegistrar.class)
public class App 
{
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(App.class);
        Dog dog = context.getBean(Dog.class);
        System.out.println(dog.getName());
    }
}

运行代码结果打出我们预想中的结果大黄。之所以我们能从容器中获取到Dog实例,是因为MyImportBeanDefinitionRegistrar#registerBeanDefinitions方法向容器中注册了DogBeanDefinition

进阶应用

从上面的示例中来看,并没有体现出什么高级灵活的地方。假如现在我们有这样一个需求,我们需要把某个包下类都注入到容器中,同时使用一个注解来标记只有类上带有这个标记的类我们才注入,并不是所有的类都注入到容器中。面对这种自定义的注入需求,使用ImportBeanDefinitionRegistrar就能很轻松的完成我们的需求。我们将实现分为如下几步:

定义开启扫描包的注解和标记注解
//该注解用来指定扫描包
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {PackageImportBeanDefinitionRegistrar.class})
public @interface PackageScan 
{

    String[] basePackages() default {};
}
//该注解用来指定哪些类需要被注入到容器
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomInjection {

}
实现扫描并注入功能
public class PackageImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        //获取注解上的属性
        Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(PackageScan.class.getName());
        //扫描包
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        //添加过滤器扫描含有注解CustomInjection的类
        provider.addIncludeFilter(new AnnotationTypeFilter(CustomInjection.class));
        //扫描包下的BeanDefinition
        String[] basePackages = (String[]) attributes.get("basePackages");
        Set<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            Set<BeanDefinition> components = provider.findCandidateComponents(basePackage);
            candidateComponents.addAll(components);
        }
        //注册BeanDefinition
        for (BeanDefinition component : candidateComponents) {
            beanDefinitionRegistry.registerBeanDefinition(component.getBeanClassName(),component);
        }
    }
}

上面代码的整体实现逻辑比较简单,首先是获取到PackageScan中需要指定的包路名集合,接着就是使用ClassPathScanningCandidateComponentProvider获取到包包中的BeanDefinition,最后通过BeanDefinitionRegistry将找到的BeanDefinition注入到容器。可能难理解的就是获取包下BeanDefinition时使用的ClassPathScanningCandidateComponentProvider,该类由Spring提供的一个工具类,它的主要功能就是可以帮助我们从包路径中获取到所需的 BeanDefinition 集合。

@MapperScans的实现

在使用Mybatis+Spring集成时我们会用到一个工具包mybatis-spring,在该工具包中提供了一个@MapperScans注解,通过该注解我们可以指定扫描包下的Mapper注入到Spring容器中。该注解的实现其实也是借助了@ImportImportBeanDefinitionRegistrar,它的源码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(MapperScannerRegistrar.class)
@Repeatable(MapperScans.class)
public @interface MapperScan 
{
    //省略部分内容
}

通过注解上的@Import(MapperScannerRegistrar.class)可以知道,它的实现类为MapperScannerRegistrar

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrarResourceLoaderAware {
   //省略部分代码
  void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs,
      BeanDefinitionRegistry registry, String beanName)
 
{

    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
     //省略部分代码
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
  }

MapperScannerRegistrar内部要做的事简单来说就是将MapperScannerConfigurer注入到Spring容器中。

总结

通过上面的内容我们了解到,@Import主要有三种使用方式,不管哪种方式其主要目的就是为了动态而灵活的将组建注入到容器中。虽然我们平时很少使用,但是在很多源码中我们会看到,特别是在SpringBoot中使用的特别多,它的主要使用场景还是在Spring和第三方组建整合的场景。

本文的示例代码地址:https://gitee.com/zengchao_workspace/spring-import.git


原文始发于微信公众号(一只菜鸟程序员):Spring之@Import

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72929.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!