likes
comments
collection
share

Spring Framework的模块装配

作者站长头像
站长
· 阅读数 2

声明:文章内容主要来自书籍《Spring Boot 源码解读与原理分析》,本文目的只是想通过博客文章记录个人学习历程

什么是模块装配?

在罗列概念之前,我们先看两个我们熟悉的注解

  • @EnableScheduling: 开启定时任务
@Target({ElementType.TYPE}) //标识这个注解只可以作用在类上
@Retention(RetentionPolicy.RUNTIME) //标识这个注解是在运行时生效
@Import({SchedulingConfiguration.class}) //标识这个注解导入的类
@Documented
public @interface EnableScheduling {
}
  • @EnableAspectJAutoProxy:开启注解AOP编程
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass() default false;

    boolean exposeProxy() default false;
}
  • 这两个注解大家应该都不陌生,他们有个共同的特点就是注解的名字都是@Enablexxx,还有就是都被@Import注解标识,表示需要导入哪些类。这两个注解就是我们模块装配的表现形式,是不是很简单呢?

现在我们开始正式讲解一下什么是模块装配:

  • 模块:可以理解为成一个一个可以分解,组合更换的独立单元,模块之间可能存在一定的依赖,模块内部通常是高内聚的,每个模块各司其职
  • 模块装配:是自动装配的核心,它可以把一个模块所需的核心功能组件都装配到IOC容器中,并保证装配的方式尽可能简单

快速体会模块装配

光看概念好像并不能有什么具体的感受,还是需要我们一起动手实操写代码才有具体的感受的。那我们应该如何自己实现一个模块装配呢?还是看到上面两个注解,其实最主要的就两点:自定义注解+@Import导入组件

场景设想

在这里我就沿用书本上的一个场景:我们使用代码构建一个酒馆,当然酒馆里面必不可少的元素就是:吧台,老板,调酒师,服务员,那么这个时候我们可以将酒馆看作一个ApplicationContext, 那么酒馆中比不可以少的四个元素我们就可以看作是一个一个组件,也可以说是一个一个Bean对象,我们最终的目的是创建一个@EnableBar注解,将这些元素可以一并填充到酒馆中。

代码实践

声明自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target(ElementType.TYPE) //只能标注在类上
public @interface EnableBar {
}

当然,仅仅是这样还是不行的,还需要一个最关键的注解,那就是@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();

}
  • 从属性我们可以看出,这个注解是可以传递一个类的字节码数组,从注释我们可以看出,这个注解是支持导出配置类,ImportSelector的实现类,ImportBeanDefinitionRegistrar的实现类,普通类

导入普通类

@Import支持导入的类还是挺丰富的,我们就先来尝试一个最简单的,导入一个简单的Boss

下面简要定义一个Boss类,因为我们主要是体验模块装配,而不是酒馆的细节实现

public class Boss {
}

其实也可以通过给Boss类标注@Component注解,之后通过@ComponentScan扫描机制将其注入到IOC容器中,但是我们这里主要是体验@Import注解导入,所以就不展开了。

所以,这个时候我们只需要使用@Import注解将其导入即可,代码如下

@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target(ElementType.TYPE) //只能标注在类上
@Import({Boss.class})
public @interface EnableBar {
}

之后我们简单编写一个测试启动类

public class TestApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BarConfiguration.class);
        Boss boss = context.getBean(Boss.class);
        System.out.println("boss = " + boss);
    }
}

这里需要注意了,是要我们自己编写一个测试启动类,不是直接使用spring给我们生成好的启动类,当然对于当前这个是没有影响的,但是对后面的检验组件装配效果是有影响的,如果你使用spring为我们提供的启动类你会发现后面的组件即使没有使用@Import注解导入也被注册到了容器中,是因为spring为我们提供的启动类整合了@ComponentScan开启了组件扫描。

让我们启动程序,测试一把,结果如下:

boss = com.zgq.springdemo.spring.demo.entity.Boss@1d082e88

因此,我们成功将Boss这个普通类导入了,完成了最简单的模块装配。

导入配置类

配置类大家应该都不陌生,就是使用一个@Configuration注解标注一个类,声明配置类,之后这个类的主要代码就是使用@Bean注解标注一个一个方法,将这些方法的返回值当作一个一个组件注入到我们的容器中。下面我们就动手实践

我们需要先将我们的调酒师实体类声明出来,代码如下:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Bartender {

    private String name;
}

当然,一个酒馆不可能只有一个调酒师,所以我们使用配置类,一次性注册多名调酒师

@Configuration
public class BartenderConfiguration {

    @Bean
    public Bartender gouDan() {
        return new Bartender("狗蛋");
    }

    @Bean
    public Bartender maZi() {
        return new Bartender("麻子");
    }
}

此处需要再次提醒,如果你的配置类是在spring为我们提供的启动类的所在包或子包下,那么他是不会有黄色波浪线提醒的,因为它处于扫描机制下,所以此时你不将这个配置类使用@Import注解导入,直接使用spring为我们提供的启动类去启动,也是可以从容器中拿到我们的调酒师组件的。、

那我们此时再回到我们自定义的@EnableBar注解中将其导入

@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target(ElementType.TYPE) //只能标注在类上
@Import({Boss.class, BartenderConfiguration.class})
public @interface EnableBar {
}
  • 此时我们直接导入配置类就行,这样我们可以直接将配置类连带类中定义的那两个被@Bean注解标识的组件一同注册到容器中

那我们这个时候就测试一把,看看调酒师是否能被注入进来,测试代码如下:

public class TestApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BarConfiguration.class);
        Map<String, Bartender> beans = context.getBeansOfType(Bartender.class);
        beans.forEach((s, bartender) -> System.out.println(s + ":" + bartender));
        Map<String, BartenderConfiguration> beansOfType = context.getBeansOfType(BartenderConfiguration.class);
        beansOfType.forEach((s, bartenderConfiguration) -> System.out.println(s + ":" + bartenderConfiguration));
    }
}

测试结果如下:

gouDan:Bartender(name=狗蛋)
maZi:Bartender(name=麻子)
com.zgq.springdemo.spring.demo.config.BartenderConfiguration:com.zgq.springdemo.spring.demo.config.BartenderConfiguration$$EnhancerBySpringCGLIB$$e6085808@3d680b5a
  • 可以非常清晰的看到,不仅我们需要的调酒师组件注册了进来,配置类也给我们一同注册了进来

导入ImportSelector

对于ImportSelector这个接口我想应该应该有不少小伙伴还是挺陌生的,所以我们先简单了解一下

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.
	 * @return the class names, or an empty array if none
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

	/**
	 * Return a predicate for excluding classes from the import candidates, to be
	 * transitively applied to all classes found through this selector's imports.
	 * <p>If this predicate returns {@code true} for a given fully-qualified
	 * class name, said class will not be considered as an imported configuration
	 * class, bypassing class file loading as well as metadata introspection.
	 * @return the filter predicate for fully-qualified candidate class names
	 * of transitively imported configuration classes, or {@code null} if none
	 * @since 5.2.4
	 */
	@Nullable
	default Predicate<String> getExclusionFilter() {
		return null;
	}

}

ImportSelector 是一个接口,用于选择并返回应该基于导入的 @Configuration 类的 AnnotationMetadata 来导入的类的名称。

该接口定义了一个方法 selectImports(),它接收一个 AnnotationMetadata 参数,表示导入的 @Configuration 类的注解元数据。该方法应该根据导入的 @Configuration 类的注解信息选择并返回要导入的类的名称。

具体来说,selectImports() 方法应该返回一个字符串数组,包含要导入的类的名称。如果没有需要导入的类,则返回一个空数组。

使用 ImportSelector 接口,你可以通过自定义逻辑来选择要在 @Configuration 类中导入的其他类。这样可以将额外的类注入到 Spring 容器中,以扩展配置和功能。

请注意,ImportSelector 接口需要你创建一个实现类,并在实现类中实现 selectImports() 方法,根据你的需求选择要导入的类并返回类的名称数组。

这是我问gpt的答复,其实看得还是云里雾里,对于这个接口其实我们主要是关注第一个方法,这个方法强调的是返回一组类名(一定要是全限定类名喔,不然无法确定一个类),它会根据返回的类名去找到对应的类将其导入。

那接下来我们就动手实现一下,那我们就先把我们的吧台实体类定义一下,代码如下

public class BarTable { 
}

当然为了测试,我们还是少不了一个配置类

@Configuration
public class BarTableConfiguration {
    @Bean
    public BarTable BarTable() {
        return new BarTable();
    }

然后我们再来实现刚刚讲到的ImportSelector接口,代码如下:

public class BarTableImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{BarTable.class.getName(), BarTableConfiguration.class.getName()};
    }
}
  • 虽然它的文档注释说,是返回配置的全限定名,但是其实返回普通类的全限定名也是的

回到我们的@EnableBar注解中,将其导入

@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target(ElementType.TYPE) //只能标注在类上
@Import({Boss.class, BartenderConfiguration.class, BarTableImportSelector.class})
public @interface EnableBar {
}

接下来,我们就测试一把,代码如下:

public class TestApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BarConfiguration.class);
        Map<String, BarTable> beansOfType = context.getBeansOfType(BarTable.class);
        beansOfType.forEach((s, barTable) -> System.out.println(s+":"+barTable));
        BarTableConfiguration bean = context.getBean(BarTableConfiguration.class);
        System.out.println("bean = " + bean);
    }
}

测试结果如下:

com.zgq.springdemo.spring.demo.entity.BarTable:com.zgq.springdemo.spring.demo.entity.BarTable@48e4374
BarTable:com.zgq.springdemo.spring.demo.entity.BarTable@3d680b5a
bean = com.zgq.springdemo.spring.demo.config.BarTableConfiguration$$EnhancerBySpringCGLIB$$2cc3d9de@61230f6a
  • 通过测试可以看出,不仅可以导入配置类,还将普通类也导入了,因此会有两个吧台组件

这里穿插一下书中对于ImportSelector灵活性的讨论:

采用类的全限定名的声明式注册,在目前看来可能是不太灵活的,但是这样可以灵活指定Bean,此外其实在Spring Boot的自动装配中,底层就是使用了ImportSelector,但是和我们目前测试的的形式不一样,底层是将这些信息存储在spring.factories文件中供程序读取,这样就解决了硬编码问题

导入ImportBeanDefinitionRegistrar

相较前者的声明式注册bean对象,这个就更像解释型注册bean对象,它实际导入的是bean的定义信息。

在动手写代码之前,我们先简单了解一下ImportBeanDefinitionRegistrar这个接口

public interface ImportBeanDefinitionRegistrar {

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * <p>The default implementation delegates to
	 * {@link #registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)}.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 * @param importBeanNameGenerator the bean name generator strategy for imported beans:
	 * {@link ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR} by default, or a
	 * user-provided one if {@link ConfigurationClassPostProcessor#setBeanNameGenerator}
	 * has been set. In the latter case, the passed-in strategy will be the same used for
	 * component scanning in the containing application context (otherwise, the default
	 * component-scan naming strategy is {@link AnnotationBeanNameGenerator#INSTANCE}).
	 * @since 5.2
	 * @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR
	 * @see ConfigurationClassPostProcessor#setBeanNameGenerator
	 */
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {

		registerBeanDefinitions(importingClassMetadata, registry);
	}

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * <p>The default implementation is empty.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 */
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}

}

ImportBeanDefinitionRegistrar 是一个接口,用于根据导入的 @Configuration 类的注解元数据,在当前的Bean定义注册表(BeanDefinitionRegistry)中注册所需的Bean定义。

该接口定义了两个方法:

  1. registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator): 该方法在给定的导入的 @Configuration 类的注解元数据和当前的Bean定义注册表中注册Bean定义。它还接收一个 BeanNameGenerator 参数,用于生成导入的Bean的名称。默认实现会委托给 registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) 方法。

  2. registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry): 该方法根据给定的导入的 @Configuration 类的注解元数据,在当前的Bean定义注册表中注册Bean定义。默认实现是空的,需要在实现类中根据需要进行重写。

通过实现 ImportBeanDefinitionRegistrar 接口并实现其中的方法,你可以在 @Configuration 类中根据特定条件动态注册Bean定义。这样可以在Spring容器中添加自定义的Bean定义,扩展应用程序的功能。

需要注意的是,ImportBeanDefinitionRegistrar 接口用于注册Bean定义,而不是实例化Bean。如果你需要实例化Bean,可以考虑使用 @Bean 注解或其他适用的方式。

当然,这也是我问gpt得到的答复,也是看得云里雾里,我们主要使用第二个方法来测试

我们先定义我们的服务员实体类,代码如下

public class Waiter {
}

紧接着,我们实现刚刚讲到的那个接口

public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//        第一个参数是bean的名称,第二个参数是bean的字节码
        registry.registerBeanDefinition("waiter",new RootBeanDefinition(Waiter.class));
    }
}

此时,我们再回到我们的@EnableBar中,将其导入

@Documented
@Retention(RetentionPolicy.RUNTIME) //运行时生效
@Target(ElementType.TYPE) //只能标注在类上
@Import({Boss.class, BartenderConfiguration.class, BarTableImportSelector.class, WaiterRegistrar.class})
public @interface EnableBar {
}

那我们就趁热打铁,测试一把,测试代码如下:

public class TestApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BarConfiguration.class);
        Waiter bean = context.getBean(Waiter.class);
        System.out.println("bean = " + bean);
    }
}

测试结果如下:

bean = com.zgq.springdemo.spring.demo.entity.Waiter@4a22f9e2
  • 因此我们也成功实现了将我们的服务员组件注入进入我们的酒馆中了

总结

模块装配是指将一个模块所需的核心功能组件自动装配到IOC容器中的过程。在介绍模块装配之前,我们先回顾了两个熟悉的注解:@EnableScheduling和@EnableAspectJAutoProxy,它们都是模块装配的表现形式,通过@Import注解来导入相应的类。

模块被定义为可以分解、组合和更换的独立单元,模块之间可能存在依赖关系,而模块内部通常是高内聚的,每个模块各司其职。模块装配是自动装配的核心,它将一个模块所需的核心功能组件装配到IOC容器中,并且保证装配的方式尽可能简单。

为了更好地理解模块装配的概念,我们提供了一个实践例子:构建一个酒馆。酒馆中包含吧台、老板、调酒师和服务员等元素,可以将酒馆看作一个ApplicationContext,而这些元素可以看作是组件或Bean对象。为了实现模块装配,我们引入了自定义注解@EnableBar,并使用@Import注解将各个组件导入到酒馆中。