Spring 5 中文解析核心篇-IoC容器之类路径扫描和组件管理

1.10 类路径扫描和组件管理

本章中的大多数示例都使用XML来指定在Spring容器中生成每个BeanDefinition的配置元数据。前面的部(基于注解的容器配置)分展示怎样去提供一些配置元素数据通过源码级别的注解。然而,即使在这些示例中,基本bean定义也是在XML文件中显式定义的,而注解只驱动依赖项注入。本节介绍通过扫描类路径来隐式检测候选组件的选项。候选组件是符合过滤条件的类,并在容器中注册了相应的Bean定义。这消除了使用XML进行bean注册的需要。相反,你可以使用注释(例如@Component)、AspectJ类型表达式或你自己的自定义筛选条件来选择哪些类具有在容器中注册的bean定义。

Spring3.0开始,通过Spring JavaConfig项目提供的许多Spring核心特性部分。这允许你使用Java定义bean而不是使用传统的XML文件。查看@Configuration@Bean@Import@DependsOn注解例子怎样去使用这些新特性。

1.10.1 @Component和其他构造型注解

@Repository注解是任何满足存储库角色或构造型(也称为数据访问对象或DAO)的类的标记。该标记的用途包括自动转译异常,如“异常转换”中所述。

Spring提供提供更进一步的构造型注解:@Component@Service@Controller@Component是一个通用的构造型注解对于任何Spring管理的组件。@Repository@Service@Controller是特殊化的@Component为更多特定的使用场景(在持久化、服务、表现层中)。因此,你可以使用@Component注解组件类,但是通过使用@ Repository@Service@Controller注解组件类,你的类更适合通过工具进行处理或与aspects相关联。例如,这些构造型注释成为切入点的理想目标。@Repository@Service@Controller在Spring框架未来的发布中可能增加额外的语义。因此,如果你在使用@Component@Service之间选择为你的服务层,@Service是更简洁的选择。类似地,如前所述,@Repository已经被支持作为持久化层中自动异常转换的标记。

1.10.2 使用元注解和组合组件

通过Spring提供的许多注解在你自己代码中能够被作为元注解使用。元注解也是注解它能够被应用到其他注解上。例如,前面提到的@Service注解使用@Component进行元注解,类似下面例子:

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

    // ...
}
  1. @Component导致@Service的处理方式与@Component相同。

你可以联合元注解去创建组合注解。例如,Spring MVC的RestController注解由@Controller@ResponseBody组成。

此外,组合注解可以选择从元注解中重新声明属性,以允许自定义。当你想去仅仅暴露一个元数据的属性子集的时候特别地有用。例如,Spring的@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;

}

你可以使用@SessionScope无需声明proxyMode类似下面:

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

你可以覆盖proxyMode值,类似下面例子:

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

更多详情,查看Spring注解编程模型wiki页。

1.10.3 自动地检查类和注册BeanDefinition

Spring可以自动检测构造型类并向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,你需要增加@ComponentScan到你的@Configuration类,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>元素。

类路径包扫描需要在类路径中对应的目录实体存在。使用Ant构建JAR时,请确保未激活JAR任务的文件专用开关。此外,在某些环境中,基于安全策略可能不会公开类路径目录-例如,在JDK 1.7.0_45及更高版本上的独立应用程序(这需要在清单中设置“信任的库” —请参见https://*.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources) 。

在JDK9的模块路径,Spring的类路径扫描通常按预期工作。然而,确保你的组件类在你的module-info描述被暴露。如果你期望Spring调用你的类非公共的成员,确保他们被打开(也就是,他们在module-info描述符中使用了一个opens声明而不是exports声明)。

此外,当你使用component-scan元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor两者隐式地包含。也就是说两个组件被自动检查和连接在一起-所有这些都没有XML提供的任何bean配置元数据。

你可以通过设置annotation-config属性值为false禁止AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor注册。

参考代码:com.liyong.ioccontainer.starter.AutoCheckAndRegisterBeanDefinitionIocContainer

1.10.4 使用Filer去自定义扫描

默认情况下,仅使用@Component@Repository@Service@Controller@Configuration进行注解的类或使用@Component进行注解的自定义注解是唯一被检测到的候选组件。然而,你可以修改和拓展这个行为通过使用自定义过滤器。增加@ComponentScan注解(在XML配置中子元素 <context:include-filter /><context:exclude-filter />)的includeFiltersexcludeFilters属性。每个过滤器元素需要typeexpression属性。下面表格描述过滤器可选项:

Filter Type Example Expression Description
annotation (default) org.example.SomeAnnotation 在目标组件的类型级别上呈现或元呈现的注释(备注:元组件描述形式)。
assignable org.example.SomeClass 目标组件可分配给(扩展或实现)的类(或接口)。
aspectj org.example..*Service+ 目标组件要匹配的AspectJ类型表达式。
regex org.example.Default.* 要与目标组件的类名匹配的正则表达式。
custom org.example.MyTypeFilter org.springframework.core.type.TypeFilter接口的自定义实现.

下面的例子展示配置文件忽略所有@Repository注解且使用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="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

你还可以通过在注解上设置useDefaultFilters = false或通过将use-default-filters =“ false”作为元素的属性来禁用默认过滤器。这有效地禁用了自动检测使用@Component@Repository@Service@Controller@RestController@Configuration注解或元注解的类的功能。

代码示例:com.liyong.ioccontainer.starter.CustomizeScanByFilterIocContainer

1.10.5 在组件中定义bean元数据

Spring组件有助于bean定义元数据到容器。在类上,你可以使用@Configuration定义同时在方法上使用@Bean即可定义元数据。下面例子展示怎样使用:

@Component
public class FactoryMethodComponent {

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

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

前面的类是一个Spring组件,它有一个在它的doWork()方法特定于应用的代码。但是,它也提供了一个具有工厂方法的bean定义,该工厂方法引用了方法publicInstance()@Bean注解标识工厂方法和其他bean定义的属性,例如,通过@Qualifier注解指定限定符。其他方法级别注解能够被指定为@Scope@Lazy和自定义限定符注解。

除了它的组件初始化角色外,你可以在注入点放置@Lazy注解标记@Autowired@Inject。在这种情况下,它导致注入了惰性解析代理。

如前所述,自动装配字段和方法是被支持的同时附加的支持@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属性的值。Spring表达式语言元素定义属性值通过符号\#{ <expression> }。对应@Value注解,表达式解析器已经预先配置当解析表达式文本的时候查找bean的名称。

自Spring4.3以后,你可以声明一个类型InjectionPoint(或其更具体的子类:DependencyDescriptor)工厂方法参数来访问触发当前bean创建的请求注入点。请注意,这仅适用于实际创建bean实例,不会注入已经存在的实例。这个特性大多数用在单例bean作用域。对于其他作用域,factory方法只能看到在给定作用域中触发创建新bean实例的注入点(例如,依赖触发懒加载单例bean的创建)。在这种情况下,可以将提供的注入点元数据与语义一起使用。下面的例子展示怎样使用InjectionPoint

@Component
public class FactoryMethodComponent {

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

常规Spring组件中的@Bean方法的处理方式与Spring 的@Configuration类中的@Bean方法不同。不同点是@Component这些类不会被CGLIB增强去拦截方法和字段的调用。CGLIB代理是调用@Configuration类中的@Bean方法中的方法或字段来创建对协作对象的bean元数据引用的方法。这些方法不会通过常规的Java语法调用,而是通过容器,去提供常规的生命周期管理和Spring bean的代理,即使通过对@Bean方法的编程调用来引用其他bean。相反,在普通@Component类中的@Bean方法中调用方法或字段具有标准Java语义,没有其他特定的CGLIB处理或约束。说明:@Component中调用@Bean注解的方法和常规的方法调用一样,而在@Configuration中的@Bean调用则是通过容器去查找Bean和生成bean的元数据。

你可以声明@Bean方法为static的,这个配置类实例在容器中没有被创建时也允许调用这个方法(备注:脱离了Configuration实例)。当定义后置处理器bean(例如,BeanFactoryPostProcessorBeanPostProcessor类型)时这是特殊的场景,因为在容器生命周期中更早的获取bean实例并且应该在那时避免触发配置类中的其他部分。说明:把@Bean方法标记为static后脱落了bean实例管理所以在需要提前触发场景可以使用,这样避免未被实例化的bean其他@Bean方法被触发。

静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类中也是如此(在这个章节前面被描述),由于技术限制:CGLIB子类仅仅能够覆盖非静态类。因此,直接调用另一个@Bean方法具有标准的Java语义,从而导致直接从工厂方法本身直接返回一个独立的实例。

@Bean方法的Java语言可见性不会对Spring容器中的最终bean定义产生直接影响。你可以在非@Configuration类中*声明自己的工厂方法,也可以在任何地方声明静态方法。常规的在@Configuration类中的@Bean方法需要可覆盖的。也就是说,他们不能被声明为privatefinal

@Bean方法在给定的组件或配置类的基类中也是可以被发现的,以及在Java 8默认方法中由组件或配置类实现的接口中声明的方法。这为组合复杂的配置安排提供了很大的灵活性,从Spring 4.2开始,通过Java 8默认方法甚至可以进行多重继承。

最后,一个类可以包含同一个bean的多个@Bean方法,根据运行时可用的依赖关系,可以安排使用多个工厂方法。这与在其他配置方案中选择“最贪婪”的构造函数或工厂方法的算法相同。在构造时会选择具有最大可满足依赖性的变量,这类似于容器在多个@Autowired构造函数之间进行选择的方式。

参考代码:com.liyong.ioccontainer.starter.XmlBeanDefinationWithinComponetIocContainer

1.10.6 命名自动检查组件

在扫描过程中自动检测到组件时,其Bean名称由该扫描程序已知的BeanNameGenerator策略生成。默认情况,任何Spring的构造型注解(@Component@Repository@Service@Controller)包含一个名字value,从而提供名称和对应bean的定义。

如果注解包含没有名字的value或者其他的检查组件(例如通过自定义过滤器),默认bean名称生成器返回没有大写字母的非限定类名称。例如,如果下面的组件类被检查到,他们的名字将会是myMovieListermovieFinderImpl

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

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

如果由于多个自动检测到的组件具有相同的非限定类名而导致命名冲突(具有相同名称但位于不同包中的类),你可能需要配置一个BeanNameGenerator,该名称默认为生成的Bean名称的完全限名的类名称(备注:类路径全限定名称)。从Spring Framework 5.2.3开始,位于org.springframework.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>

作为一般规则,当其他组件可能显式引用该名称时,请考虑使用注释指定该名称。另一方面,只要容器负责连接(连接其他组件),自动生成的名称就足够了。

代码示例:com.liyong.ioccontainer.starter.BeanNameGeneratorIoCContainer

1.10.7 为自动检查组件提供作用域

一般情况下Spring管理的组件,对于自动检查组件默认情况下是singleton作用域。然而,有时候你需要不同的作用域时可以通过@Scope注解指定。你可以提供在注解中的作用域名称,类似下面的例子:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

@Scope注解仅在具体的bean类(用于带注解的组件)或工厂方法(对于@Bean方法)上进行内省。与XML的bean定义对比,这里没有bean定义继承的概念,并且类级别的继承层次结构与元数据目的无关。

更多详情在Spring上下文中在特定的web作用域,例如requestsession,查看Request、 Session、 Application和 WebSocket 作用域。与这些作用域的预构建注解一样,你可以通过使用Spring的元注解方法构建你自己的作用域注解:例如,自定义元注解@Scope("prototype"),尽可能地声明一个自定义scoped-proxy模式。

你可以实现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 Beans as Dependencies中描述。为了这个目的,在component-scan元素上可以使用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>
1.10.8 为注解提供限定元数据

@Qualifie注解在基于注解细粒度调整限定符中已经讨论。本节中的示例演示了在解析autowire候选对象时使用@Qualifier注解和自定义qualifier注解来提供细粒度控制。因为这些例子是基于在XML定义的基础上的,因此限定符元数据是在XML中通过使用qualifier或bean子元素meta在候选者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在其限定符元数据中提供变体,因为元数据是针对每个实例而不是针对每个类提供的。

代码示例:com.liyong.ioccontainer.starter.XmlGenericsQualifierIocContainer

1.10.9 生成候选者组件索引

虽然类扫描非常快,在编译的时候通过创建一个静态的候选者清单去改善大型项目的启动性能。在这种模式下,作为组件扫描目标的所有模块都必须使用此机制。

现有的@ComponentScan<context:component-scan>指令必须保持不变,以便请求上下文扫描某些包中的候选者。当ApplicationContext检测索引的时候,它自动地使用索引而不是去扫描类路径。

去生成这个索引,增加一个附加的依赖到每个模块,它包含组件是指令扫描的目标。下面的例子显示怎样使用Maven:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.6.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

在Gradle 4.5已经之前的版本,依赖需要在compileOnly配置中声明,类似下面的例子:

dependencies {
    compileOnly "org.springframework:spring-context-indexer:5.2.6.RELEASE"
}

在Gradle 4.5已经之前的版本,在annotationProcessor配置中被声明,类似下面的例子:

dependencies {
 annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}

处理过程在jar文件中生成一个META-INF/spring.components文件。

在IDE中使用这种模式时,必须将spring-context-indexer注册为注解处理器,以确保在更新候选组件时索引是最新的。

当在类路径中找到META-INF/spring.components时索引被自动地激活。如果某些库(或用例)的索引部分可用,但无法为整个应用程序构建索引,你可以通过将spring.index.ignore设置为true来回退到常规的类路径扫描(就像根本没有索引一样),要么在系统属性或者类路径的根目录下spring.properties文件中配置。

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号:
Spring 5 中文解析核心篇-IoC容器之类路径扫描和组件管理

技术交流群:
Spring 5 中文解析核心篇-IoC容器之类路径扫描和组件管理

上一篇:Spring 5 中文解析核心篇-IoC容器之BeanDefinition继承与容器拓展点


下一篇:Spring 5 中文解析核心篇-IoC容器之基于注解的容器配置