Spring Bean 装配 之 @Import 注解介绍

Spring Bean 装配 之 @Import 注解介绍

 

 

1、简介

    随着 Spring 2.0引入注解,以及Spring 3.0全面支持注解驱动开发,这个过程变得更加自动化。例如,通过使用 @Component + @ComponentScanSpring可以自动地找到并创建bean,通过@AutowiredSpring可以自动地注入依赖。这种方式被称为 “自动装配”。在Spring框架中,有多种方式可以实现Bean的装配,包括:

  1. 基于Java的配置: 通过使用@Configuration 和 @Bean注解在Java代码中定义的Bean。这是一种声明式的方式,我们可以明确地控制Bean的创建过程,也可以使用@Value@PropertySource等注解来处理配置属性。
  2. 基于XML的配置: Spring也支持通过XML配置文件定义Bean,这种方式在早期的Spring版本中更常见,但现在基于Java的配置方式更为主流。
  3. 基于注解的组件扫描: 通过使用 @Component@Service@Repository@Controller等注解以及@ComponentScan来自动检测和注册Bean。这是一种隐式的方式,Spring会自动扫描指定的包来查找带有这些注解的类,并将这些类注册为Bean
  4. 使用@Import: 这是一种显式的方式,可以通过它直接注册类到IOC容器中,无需这些类带有@Component或其他特殊注解。我们可以使用它来注册普通的类,或者注册实现了ImportSelectorImportBeanDefinitionRegistrar接口的类,以提供更高级的装配能力。

每种方式都有其应用场景,根据具体的需求,我们可以选择合适的方式来实现模块装配。比如在Spring Boot中,我们日常开发可能会更多地使用基于Java的配置和基于注解的组件扫描来实现模块装配。@Import 注解提供了类似 @Bean 注解的功能,向Spring容器中注入bean,也对应实现了与Spring XML中的<import/>元素相同的功能,注解定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
​
  /**
   * {@link Configuration @Configuration}, {@link ImportSelector},
   * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
   */
  Class<?>[] value();
​
}

从定义上来看,@Import注解非常简单,只有一个属性value(),类型为类对象数组,如value={A.class, B.class},这样就可以把类A和B交给Spring容器管理。但是这个类对象需要细分为三种对象,也对应着@Import的三种用法如下:

  1. 普通类。
  2. 实现了ImportSelector接口的类(这是重点~Spring Boot的自动配置原理就用到这种方式)。如果想动态地导入一些BeanSpringIOC容器中,那么可以实现ImportSelector接口,然后在@Import注解中引入ImportSelector实现类,这样Spring就会将ImportSelector实现类返回的类导入到SpringIOC容器中。
  3. 实现了ImportBeanDefinitionRegistrar接口的类。如果想在运行时动态地注册一些BeanSpringIOC容器中,那么可以实现ImportBeanDefinitionRegistrar接口,然后在@Import注解中引入ImportBeanDefinitionRegistrar实现类,这样Spring就会将ImportBeanDefinitionRegistrar实现类注册的Bean导入到SpringIOC容器中。

导入实现了 ImportSelector 或 ImportBeanDefinitionRegistrar 接口的类。这两个接口提供了更多的灵活性和控制力,使得我们可以在运行时动态地注册 Bean,这是通过 @Configuration + @Bean 注解组合无法做到的。下面我们就针对@Import的三种不同用法。

 

2、@Import 注解用法

2.1 注入普通类

当我们使用 @Import 注解来导入一个普通的类(即一个没有使用 @Component 或者 @Service 之类的注解标记的类),Spring 会为该类创建一个 Bean,并且这个 Bean 的名字默认就是这个类的全限定类名。这种方式很简单,首先先随便定义一个普通类:Student、Person类:

@Data
public class Student {
    private Long id;
    private String name;
    private Integer age;
}

@Data
public class Person{
    private Long id;
    private String name;
    private Integer age;
}

 

接下来就是声明一个配置类,然后使用@Import导入注入即可:

@Configuration
@Import({Student.class})
public class MyConfig {
​
    @Bean
    public Person person() {
        Person person = Person.builder().id(1).name("黄晓明").age(18).build();
        return person;
    }
​
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        // 遍历Spring容器中的beanName
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
​
}

这里我用@Bean注入了一个bean,想证实一下@Import实现的功能与其类似,执行上面的main()结果如下:

myConfig
com.dw.study.config.Student
person

可以看到,这里注入了我们在MyConfig中注入的bean,@Import({Student.class})Student类注入到了Spring容器中,beanName默认为全限定类名com.dw.study.config.Student,而@Bean注入的默认为方法名,这也是两者的区别。

 

2.2、使用ImportSelector进行选择性装配

如果我们想动态地选择要导入的类,我们可以使用一个ImportSelector实现。

我们先来看看ImportSelector这个接口的定义:

public interface ImportSelector {
​
  String[] selectImports(AnnotationMetadata importingClassMetadata);
@Nullable
default Predicate<String> getExclusionFilter() { return null; } }
  • selectImports( )返回一个包含了类全限定名的数组,这些类会注入到Spring容器当中。注意如果为null,要返回空数组,不然后续处理会报错空指针
  • getExclusionFilter()该方法制定了一个对类全限定名的排除规则来过滤一些候选的导入类,默认不排除过滤。该接口可以不实现

接下来我们编写一个类来实现ImportSelector接口:

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.dw.study.config.Student", "com.dw.study.config.Person"};
    }
}

最后在配置类中使用@Import导入:

@Configuration
@Import({MyImportSelector.class})
public class MyConfig {
​
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        // 遍历Spring容器中的beanName
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
}

执行结果如下:

myConfig
com.dw.study.config.Student
com.dw.study.config.Person

         可以看出,Spring没有把MyImportSelector当初一个普通类进行处理,而是根据selectImports( )返回的全限定类名数组批量注入到Spring容器中。当然你也可以实现重写getExclusionFilter()方法排除某些类,比如你不想注入Person类,你就可以通过这种方式操作一下即可,这里就不再展示代码案例,可以自行尝试。

        在 Spring Boot 中,ImportSelector 被大量使用,尤其在自动配置(auto-configuration)机制中起着关键作用。例如,AutoConfigurationImportSelector 类就是间接实现了 ImportSelector,用于自动导入所有 Spring Boot 的自动配置类。我们通常会在Spring Boot启动类上使用 @SpringBootApplication 注解,实际上,@SpringBootApplication 注解中也包含了 @EnableAutoConfiguration@EnableAutoConfiguration 是一个复合注解,它的实现中导入了普通类 @Import(AutoConfigurationImportSelector.class)AutoConfigurationImportSelector 类间接实现了 ImportSelector接口,用于自动导入所有 Spring Boot 的自动配置类。

 

2.3 、实现了ImportBeanDefinitionRegistrar接口的类

  ImportBeanDefinitionRegistrar接口的主要功能是在运行时动态的往Spring容器中注册Bean,实现该接口的类需要重写registerBeanDefinitions方法,这个方法可以通过参数中的BeanDefinitionRegistry接口向Spring容器注册新的类,给应用提供了更大的灵活性。

ImportBeanDefinitionRegistrar 接口是也是spring的扩展点之一,它可以支持我们自己写的代码封装成BeanDefinition对象,注册到Spring容器中,功能类似于注解@Service @Component。
很多三方框架集成Spring的时候,都会通过该接口,实现扫描指定的类,然后注册到Spring容器中,比如Mybatis中的Mapper接口,SpringCloud中的FeignClient接口,都是通过该接口实现的自定义注册逻辑。
先看看ImportBeanDefinitionRegistrar的定义:

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

可以看到一共有两个同名重载方法,都是用于将类的BeanDefinition注入。唯一的区别就是,2个参数的方法,只能手动的输入beanName,而3个参数的方法,可以利用BeanNameGenerator根据beanDefinition自动生成beanName。

自定义一个类实现 ImportBeanDefinitionRegistrar:

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Student.class);
        // 通过反射技术调用setter方法给name赋值,也可以在构造器赋值name,需要调用beanDefinitionBuilder.addConstructorArgValue("");
        beanDefinitionBuilder.addPropertyValue("name", "刘亦菲");
        BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
        beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        registry.registerBeanDefinition("student", beanDefinition);
    }
}

注意,这里只能注入一个bean,所以只能实现一个方法进行注入,如果两个都是实现,前面的一个方法生效。

  • AnnotationMetadata importingClassMetadata: 这个参数表示当前被@Import注解导入的类的所有注解信息,它包含了该类上所有注解的详细信息,比如注解的名称,注解的参数等等。
  • BeanDefinitionRegistry registry: 这个参数是SpringBean定义注册类,我们可以通过它往Spring容器中注册Bean。在这里,我们使用它来注册我们的MyUser。

将上面2.2小节的配置类变成@Import({MyImportBeanDefinitionRegistrar.class})即可,执行结果和上面一样的,这里不再展示了。

ImportBeanDefinitionRegistrar接口提供了非常大的灵活性,我们可以根据自己的需求编写任何需要的注册逻辑。这对于构建复杂的、高度定制的Spring应用是非常有用的。

Spring Boot就广泛地使用了ImportBeanDefinitionRegistrar。例如,它的@EnableConfigurationProperties注解就是通过使用一个ImportBeanDefinitionRegistrar来将配置属性绑定到Beans上的,这就是ImportBeanDefinitionRegistrar在实践中的一个实际应用的例子。

 如果只是把业务类注册到Spring容器中,我们通过其他注解就可以做到了,比如@Service、@Component,那么ImportBeanDefinitionRegistrar有没有更高级的玩法?

 

2.3.2、ImportBeanDefinitionRegistrar 案例 2

定义一个业务接口,通过 FactoryBean + InvocationHandler 来生成该接口的代理类,无需手动写业务接口的实现类,很多底层框架(Mybatis的mapper 接口、FeignClient)就是这样实现的。

  • InvocationHandler:主要是通过Invoke方法来拦截业务接口的方法。
  • FactoryBean:主要是用来生成的代理类。
  • ImportBeanDefinitionRegistrar:在这里的作用就是帮忙我们把自定义的FactoryBean注册到Spring中。

1、自定义业务接口:

public interface UserServiceInterface {
    List<String> list();
}

// 方便调试, 我们这里实现该接口。
public class UserServiceInterfaceImpl implements UserServiceInterface {
    @Override
    public List<String> list() {
        ArrayList<String> userNameList = new ArrayList<>(2);
        userNameList.add("張三");
        userNameList.add("李四");
        return userNameList;
    }
}

 

2、自定义FactoryBean这样我们控制Bean的创建的过程:

public class MyFactoryBean implements FactoryBean<Object> {

    /**
     * 被代理的目标接口
     */
    private final Class<?> targetInterface;

    /**
     * 代理处理类
     */
    private final InvocationHandler invocationHandler;

    public MyFactoryBean(Class<?> cls, InvocationHandler invocationHandler) {
        this.targetInterface = cls;
        this.invocationHandler = invocationHandler;
    }

    /**
     * 返回bean的对象。spring会自动把它add到容器里面去。
     *
     * @return 创建的Bean
     * @throws Exception
     */
    @Override
    public Object getObject() throws Exception {
        // 通过proxy来得到代理对象。
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{targetInterface}, invocationHandler);
    }

    /**
     * 返回要添加到容器里bean的类型
     *
     * @return Class
     */
    @Override
    public Class<?> getObjectType() {
        return this.targetInterface;
    }
}

 

3、实现 InvocationHandler 用来拦截业务接口的方法:

public class MyInvocationHandler implements InvocationHandler {

    /**
     * 被代理的实例对象
     */
    private final Object delegate;

    public MyInvocationHandler(Object delegate) {
        this.delegate = delegate;
    }

    /**
     * 拦截 cls 的所有方法
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("拦截调用目标方法");
        return method.invoke(delegate, args);
    }
}

 

4、自定义 ImportBeanDefinitionRegistrar 实现类,把我们自定义的FactoryBean注册到Spring中。

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 通过工具类生成一个bd,只是这个Db对象比较纯洁没有绑定任何类
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MyFactoryBean.class);
        // 通过反射使用构造函数初始化属性值,如果是无参构造也可以通过addPropertyValue("", "");方式实例化属性值
        beanDefinitionBuilder.addConstructorArgValue(UserServiceInterface.class.getName());
        // 注意这里我直接new了一个已知的对象, 正常使用时,应该扫描某一个包下的符合条件的类,然后通过反射创建对应的实例
        beanDefinitionBuilder.addConstructorArgValue(new MyInvocationHandler(new UserServiceInterfaceImpl()));
        // 注册bd
        registry.registerBeanDefinition("userService", beanDefinitionBuilder.getBeanDefinition());
    }
}

 

5、导入配置:

@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)
public class MyConfig {
}

 

6、测试

 public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
        //通过name获取Bean
        Object userServiceInterface = applicationContext.getBean("userService");
        //针对这种场景Bean的类型是,通过FactoryBean的getObjectType方法返回的。
        UserServiceInterface u = (UserServiceInterface) userServiceInterface;
        System.out.println(u.list());
    }

 

ImportBeanDefinitionRegistrar是被谁处理了?

     因为@Import是修饰在配置类里面的,所以在解析配置类的时候,也就是 ConfigurationClassPostProcessor 处理的时候,会解析@Import注解,然后会判断@Import导入类是否是ImportBeanDefinitionRegistrar的子类,执行重写的registerBeanDefinitions方法。这样我们的类就被加到BD集合里面去了。

 

 

 

 

 

 

posted @ 2023-11-25 19:40  邓维-java  阅读(336)  评论(0)    收藏  举报