类路径扫描和管理的组件

本章中的大多数示例都使用 XML 来指定配置元数据,这些配置元数据在 Infra 容器内生成每个 BeanDefinition。 上一节 (基于注解的容器配置) 演示了如何通过源代码级别的注解提供大量的配置元数据。然而,即使在那些示例中, “基础” bean 定义也是在 XML 文件中明确定义的,而注解只驱动依赖注入。本节介绍了一种通过扫描类路径来隐式检测候选组件的选项。 候选组件是符合过滤器标准并且已在容器中注册了相应 bean 定义的类。这消除了使用 XML 执行 bean 注册的需求。 相反,您可以使用注解(例如 @Component)、AspectJ 类型表达式或您自己的自定义过滤器标准来选择哪些类已在容器中注册了 bean 定义。

您可以使用 Java 来定义 beans,而不是使用 XML 文件。请查看 @Configuration@Bean@Import@DependsOn 注解的示例,了解如何使用这些功能。

@Component 和其他 Stereotype 注解

@Repository 注解用于标记任何充当存储库(也称为数据访问对象或 DAO)角色或 stereotype 的类。这个标记的用途之一是自动转换异常, 如 异常转换 中所述。

Infra 提供了其他立体类型注解:@Component@Service@Controller@Component 是 Infra 管理的任何组件的通用立体类型。@Repository@Service@Controller@Component 的专化 ,用于更具体的用例(分别在持久化层、服务层和表示层)。因此,您可以用 @Component 注解您的组件类, 但是,如果用 @Repository@Service@Controller 来注解它们,则您的类更适合由工具处理或与切面关联。 例如,这些立体类型注解是切点的理想目标。在 TODAY 框架的未来版本中,@Repository@Service@Controller 可能会携带额外的语义。因此,如果您在服务层选择使用 @Component@Service,那么 @Service 显然是更好的选择。 同样,如前所述,@Repository 已经支持在持久化层中作为自动异常转换的标记。

Using Meta-annotations and Composed Annotations

许多 Infra 提供的注解可以在您自己的代码中用作元注解。元注解是可以应用于另一个注解的注解。 例如,早些时候提到的 @Service 注解在元注解中与 @Component 注解一起使用,如下例所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component (1)
public @interface Service {

  // ...
}
1 @Component 使得 @Service 被视为与 @Component 相同的方式处理。

您还可以组合元注解来创建 “组合注解”。例如,Web MVC 中的 @RestController 注解由 @Controller@ResponseBody 组成。

此外,组合注解可以选择重新声明元注解中的属性,以允许定制。当您希望仅暴露元注解属性的子集时,这可能特别有用。 例如,Infra 的 @SessionScope 注解将作用域名称硬编码为 session,但仍允许对 proxyMode 进行定制。以下清单显示了 SessionScope 注解的定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

  /**
   * Alias for {@link Scope#proxyMode}.
   * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
   */
  @AliasFor(annotation = Scope.class)
  ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

您可以像以下这样在不声明 proxyMode 的情况下使用 @SessionScope

@Service
@SessionScope
public class SessionScopedService {
  // ...
}

您还可以覆盖 proxyMode 的值,如以下示例所示:

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
  // ...
}

进一步了解, 请查看 Annotation Programming Model.

自动检测类并注册 Bean

Infra 可以自动检测有注释的类,并在 ApplicationContext 中注册相应的 BeanDefinition 实例。例如,以下两个类符合此类自动检测的条件:

@Service
public class SimpleMovieLister {

  private MovieFinder movieFinder;

  public SimpleMovieLister(MovieFinder movieFinder) {
    this.movieFinder = movieFinder;
  }
}
@Repository
public class JpaMovieFinder implements MovieFinder {
  // implementation elided for clarity
}

要自动检测这些类并注册相应的 bean,您需要在 @Configuration 类中添加 @ComponentScan, 其中 basePackages 属性是这两个类的共同父包。或者,您可以指定一个逗号、分号或空格分隔的列表,其中包含每个类的父包。

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
  // ...
}
简洁起见,前面的示例可以使用注释的 value 属性(即 @ComponentScan("org.example"))。

以下是使用 XML 的替代方法:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    https://www.springframework.org/schema/context/spring-context.xsd">

  <context:component-scan base-package="org.example"/>

</beans>
<context:component-scan> 的使用隐式地启用了 <context:annotation-config> 的功能。 通常,在使用 <context:component-scan> 时,无需包含 <context:annotation-config> 元素。 在 JDK 9 的模块路径(Jigsaw)上,Infra 类路径扫描通常按预期工作。但是,请确保在您的 module-info 描述符中导出了组件类。如果您希望 Infra 调用类的非公开成员,请确保它们是“开放的”(即, 它们在 module-info 描述符中使用了 opens 声明而不是 exports 声明)。

此外,当您使用component-scan元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都会被隐式包含。这意味着这两个组件会被自动检测并连接在一起, 而无需在XML中提供任何 bean 配置元数据。

您可以通过包含值为 false 的 annotation-config 属性来禁用 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的注册。

使用过滤器自定义扫描

默认情况下,使用 @Component@Repository@Service@Controller@Configuration 或本身带有 @Component 注解的自定义注解标注的类,是唯一会被检测到的候选组件。但是,您可以通过应用自定义过滤器来修改和扩展此行为。 将它们作为 @ComponentScan 注解的 includeFiltersexcludeFilters 属性添加 (或在XML配置中作为 <context:component-scan> 元素的子元素 <context:include-filter /><context:exclude-filter /> 添加)。 每个过滤器元素都需要 typeexpression 属性。下表描述了过滤选项:

Table 1. 过滤器类型
过滤器类型 样例表达式 描述

annotation (default)

org.example.SomeAnnotation

在目标组件的类型级别上 presentmeta-present 的注解。

assignable

org.example.SomeClass

目标组件可分配(扩展或实现)的类(或接口)。

aspectj

org.example..*Service+

目标组件要匹配的 AspectJ 类型表达式。

regex

org\.example\.Default.*

与目标组件类名匹配的 正则表达式。

custom

org.example.MyTypeFilter

cn.taketoday.core.type.TypeFilter 接口的自定义实现。

下面的示例显示了忽略所有 @Repository 注解而使用 “stub” 版本库的配置 注释,而使用 “stub” 版本库:

@Configuration
@ComponentScan(basePackages = "org.example",
    includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
    excludeFilters = @Filter(Repository.class))
public class AppConfig {
  // ...
}

下面列出了等效的 XML:

<beans>
  <context:component-scan base-package="org.example">
    <context:include-filter type="regex" expression=".*Stub.*Repository"/>
    <context:exclude-filter type="annotation" expression="cn.taketoday.stereotype.Repository"/>
  </context:component-scan>
</beans>
您也可以通过在注解中设置 useDefaultFilters=false 或将 use-default-filters="false" 作为属性来禁用默认筛选器。 注解,或将 use-default-filters="false" 作为 <component-scan/> 元素的属性。这将有效禁止自动检测类 或元注释的类、@Component, @Repository, @Service, @Controller, @RestController@Configuration

在组件内定义 Bean 元数据

基础设施组件也可以向容器贡献 bean 定义元数据。您可以使用与在 @Configuration 注解类中定义 bean 元数据相同的 @Bean 注解来实现这一点。以下示例显示了如何这样做:

@Component
public class FactoryMethodComponent {

  @Bean
  @Qualifier("public")
  public TestBean publicInstance() {
    return new TestBean("publicInstance");
  }

  public void doWork() {
    // Component method implementation omitted
  }
}

上述类是一个基础设施组件,在其 doWork() 方法中包含特定于应用程序的代码。 然而,它还贡献了一个 bean 定义,该定义具有引用 publicInstance() 方法的工厂方法。 @Bean 注解标识了工厂方法和其他 bean 定义属性,例如通过 @Qualifier 注解指定的限定符值。 还可以指定的其他方法级注解包括 @Scope@Lazy 和自定义限定符注解。

除了在组件初始化方面的作用之外,您还可以在标有 @Autowired 或 @Inject 的注入点上放置 @Lazy 注解。 在这种情况下,它会导致注入一个延迟解析的代理。然而,这种代理方法相当受限。对于复杂的延迟交互, 特别是与可选依赖项相结合时,我们建议使用 ObjectProvider<MyTargetBean>

@Autowired 字段和方法得到了支持,如前所述,还支持自动装配 @Bean 方法。以下示例显示了如何执行此操作:

@Component
public class FactoryMethodComponent {

  private static int i;

  @Bean
  @Qualifier("public")
  public TestBean publicInstance() {
    return new TestBean("publicInstance");
  }

  // use of a custom qualifier and autowiring of method parameters
  @Bean
  protected TestBean protectedInstance(
      @Qualifier("public") TestBean spouse,
      @Value("#{privateInstance.age}") String country) {
    TestBean tb = new TestBean("protectedInstance", 1);
    tb.setSpouse(spouse);
    tb.setCountry(country);
    return tb;
  }

  @Bean
  private TestBean privateInstance() {
    return new TestBean("privateInstance", i++);
  }

  @Bean
  @RequestScope
  public TestBean requestScopedInstance() {
    return new TestBean("requestScopedInstance", 3);
  }
}

该示例将 String 方法参数 country 的自动装配值设置为另一个名为 privateInstance 的 bean 的 age 属性的值。 通过 Infra 表达式语言元素,可以通过 #{ <expression> } 符号来定义属性的值。对于 @Value 注解 ,表达式解析器预配置为在解析表达式文本时查找 bean 名称。

您还可以声明一个类型为 InjectionPoint(或其更具体的子类:DependencyDescriptor)的工厂方法参数, 以访问触发当前 bean 创建的请求注入点。请注意,这仅适用于实际创建 bean 实例,而不适用于注入现有实例。 因此,此功能对原型作用域的 bean 最有意义。对于其他作用域,工厂方法仅看到触发在给定作用域中创建新 bean 实例的注入点 (例如,触发创建懒惰单例 bean 的依赖项)。您可以在这种情况下谨慎使用提供的注入点元数据。

以下示例显示了如何使用 InjectionPoint

@Component
public class FactoryMethodComponent {

  @Bean @Scope("prototype")
  public TestBean prototypeInstance(InjectionPoint injectionPoint) {
    return new TestBean("prototypeInstance for " + injectionPoint.getMember());
  }
}

在常规的 Infra 组件中,@Bean 方法与其在 Infra @Configuration 类中的对应方法处理方式不同。 不同之处在于 @Component 类不会通过 CGLIB 进行增强,以拦截方法和字段的调用。CGLIB 代理是在 Infra @Configuration 类中调用 @Bean 方法或字段时创建 bean 元数据引用到协作对象的手段。 这种方法不是以普通的 Java 语义调用的,而是通过容器来提供通常的生命周期管理和 bean 的代理, 即使通过对 @Bean 方法进行编程调用引用其他 bean 时也是如此。相反,在普通的 @Component 类中的 @Bean 方法中调用方法或字段具有标准的 Java 语义,没有特殊的 CGLIB 处理或其他约束适用。

您可以将 @Bean 方法声明为 static,从而可以在不创建包含配置类实例的情况下调用它们。 这在定义后处理器 bean(例如 BeanFactoryPostProcessorBeanPostProcessor 类型)时特别有意义, 因为这些 bean 在容器生命周期的早期就会初始化,并且应该在那时避免触发配置的其他部分。

对静态 @Bean 方法的调用从不被容器拦截,即使在 @Configuration 类中(如本节早期所述), 由于技术限制:CGLIB 子类化只能覆盖非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的 Java 语义,从工厂方法本身直接返回独立的实例。

在 Infra 容器中,@Bean 方法的 Java 语言可见性对结果 bean 定义没有直接影响。 您可以自由地在非 @Configuration 类中声明工厂方法,也可以在任何地方声明静态方法。 但是,在 @Configuration 类中的常规 @Bean 方法需要是可重写的,也就是说,它们不能声明为 privatefinal

@Bean 方法还会在给定组件或配置类的基类上发现,以及在由组件或配置类实现的接口中声明的 Java 8 默认方法上。 这为组合复杂的配置方案提供了很大的灵活性,甚至通过 Java 8 默认方法可以实现多重继承。

最后,单个类可以拥有多个针对同一 bean 的 @Bean 方法,作为在运行时根据可用依赖项选择多个工厂方法的排列。 这与在其他配置场景中选择“最贪婪”的构造函数或工厂方法的算法相同:在构建时选择具有最多可满足依赖项的变体, 类似于容器在多个 @Autowired 构造函数之间进行选择的方式。

给自动探测的组件命名

当组件在扫描过程中自动检测到时,其 bean 名称是由该扫描器已知的 BeanNameGenerator 策略生成的。

默认情况下,使用 AnnotationBeanNameGenerator。对于基础设施的 stereotype 注解, 如果通过注解的 value 属性提供了名称,那么该名称将用作相应 bean 定义中的名称。 当使用以下 JSR-250 和 JSR-330 注解代替基础设施原型注解时,也适用该约定: @jakarta.annotation.ManagedBean@javax.annotation.ManagedBean@jakarta.inject.Named@javax.inject.Named

用于指定 bean 名称的注解属性的名称不再必须为 value。自定义原型注解可以声明一个具有不同名称 (例如 name)的属性,并使用 @AliasFor(annotation = Component.class, attribute = "value") 注解该属性。查看 ControllerAdvice#name() 的源代码声明,以获取具体示例。

如果无法从此类注解或任何其他检测到的组件(例如由自定义过滤器发现的组件)中派生出明确的 bean 名称, 那么默认的 bean 名称生成器将返回非大写的非限定类名。例如,如果检测到以下组件类, 名称将是 myMovieListermovieFinderImpl

@Service("myMovieLister")
public class SimpleMovieLister {
  // ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
  // ...
}

如果不想依赖默认的 bean 命名策略,可以提供自定义的 bean 命名策略。 首先,实现 BeanNameGenerator 接口,并确保包含一个默认的无参构造函数。然后,在配置扫描器时提供完全限定的类名,如下面的示例注解和 bean 定义所示。

如果因多个自动检测到的组件具有相同的非限定类名(即,具有相同名称但位于不同包中的类) 而遇到命名冲突,您可能需要配置一个默认为生成的 bean 名称使用完全限定类名的 BeanNameGenerator。 位于 cn.taketoday.context.annotation 包中的 FullyQualifiedAnnotationBeanNameGenerator 可用于此类目的。
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
  // ...
}
<beans>
  <context:component-scan base-package="org.example" name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,在其他组件可能会明确引用时,考虑使用注释指定名称。另一方面,当容器负责装配时,自动生成的名称就足够了。

为自动检测的组件提供 Scope

与一般的基础设施管理组件一样,自动检测到的组件的默认和最常见范围是 singleton。 然而,有时您可能需要不同的范围,可以通过 @Scope 注解指定。 可以在注解中提供范围的名称,如以下示例所示:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
  // ...
}
@Scope 注解仅在具体的 bean 类(用于带注解的组件)或工厂方法(对于 @Bean 方法)上进行内省。 与 XML bean 定义相比,没有 bean 定义继承的概念,类级别的继承层次对于元数据目的来说是无关紧要的。

有关基础设施上下文中的 Web 特定范围(如 “request” 或 “session”)的详细信息, 请参阅 Request, Session, Application, and WebSocket Scopes。 与这些范围的预建注解一样,您还可以通过使用基础设施元注解方法来组合自己的范围注解: 例如,使用 @Scope("prototype") 进行元注解的自定义注解,可能还声明了自定义作用域代理模式。

如果要提供自定义的范围解析策略,而不是依赖注解方式,您可以实现 ScopeMetadataResolver 接口。 确保包含一个默认的无参数构造函数。然后,在配置扫描器时,可以提供完全限定的类名,如下面示例中的注解和 bean 定义所示:
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
  // ...
}
<beans>
  <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

在使用某些非单例范围时,可能需要为作用域对象生成代理。相关原因在 作用域 Bean 作为依赖项 中有描述。为此,组件扫描元素上提供了一个 scoped-proxy 属性。可能的三个值是:nointerfacestargetClass。 例如,以下配置会导致标准 JDK 动态代理:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
  // ...
}
<beans>
  <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

提供带有注释的 Qualifier 元数据

@Qualifier 注解在 使用 Qualifiers 微调基于注解的自动装配 中有讨论。该部分的示例演示了使用 @Qualifier 注解和自定义限定符注解来在解析自动装配候选项时提供细粒度控制。 因为这些示例是基于 XML bean 定义的,所以在 XML 的 bean 元素的 qualifiermeta 子元素中提供了候选 bean 定义的限定符元数据。当依赖于类路径扫描进行组件的自动检测时, 可以在候选类上使用类型级别的注解提供限定符元数据。以下三个示例演示了这种技术:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
  // ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
  // ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
  // ...
}
与大多数基于注解的替代方案一样,请记住,注解元数据绑定到类定义本身, 而使用 XML 允许同一类型的多个 bean 提供限定符元数据的变化,因为该元数据是按实例而不是按类提供的。