# 核心技术 参考文档的这一部分涵盖了对 Spring 框架绝对必要的所有技术。 其中最重要的是 Spring 框架的控制反转容器。在对 Spring 框架的 IOC 容器进行了全面的介绍之后,还对 Spring 的面向方面编程( AOP)技术进行了全面的介绍。 Spring 框架有其自己的 AOP 框架,该框架在概念上易于理解,并且成功地解决了 爪哇 Enterprise 编程中 AOP 需求的 80% 的甜蜜点。 还提供了对 Spring 与 AspectJ 的集成的覆盖(就功能而言,目前是最丰富的——当然也是 爪哇 Enterprise 领域中最成熟的 AOP 实现)。 ## 1. IOC 容器 本章介绍 Spring 的控制反转容器。 ### 1.1. Spring IOC 容器和 bean 介绍 这一章涵盖了 Spring 倒置控制原则的框架实现。IoC 也被称为依赖注入。在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们所使用的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。这个过程从根本上讲是 Bean 本身的逆过程(因此称为控制的逆过程),通过使用类的直接构造或诸如服务定位器模式的机制来控制其依赖关系的实例化或位置。 `org.springframework.beans`和`org.springframework.context`包是 Spring Framework 的 IOC 容器的基础。[`BeanFactory`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/BeanFactory.html)接口提供了一种能够管理任何类型对象的高级配置机制。[` 应用上下文’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationContext.html)是`BeanFactory`的子接口。它补充道: * 与 Spring AOP 的特性更容易集成 * 消息资源处理(用于国际化) * 事件发布 * 应用程序层特定的上下文,例如在 Web 应用程序中使用的`WebApplicationContext`。 简而言之,`BeanFactory`提供了配置框架和基本功能,而`ApplicationContext`增加了更多特定于 Enterprise 的功能。`ApplicationContext`是`BeanFactory`的完整超集,在本章 Spring 的 IOC 容器的描述中专门使用。有关使用`BeanFactory`而不是`ApplicationContext,`的更多信息,请参见[The `BeanFactory`](#beans-beanfactory)。 在 Spring 中,构成应用程序主干并由 Spring IOC 容器管理的对象称为 bean。 Bean 是由 Spring IOC 容器实例化、组装和管理的对象。否则, Bean 只是应用程序中的许多对象之一。bean 和它们之间的依赖关系反映在容器使用的配置元数据中。 ### 1.2.集装箱概述 `org.springframework.context.ApplicationContext`接口表示 Spring IOC 容器,并负责实例化、配置和组装 bean。容器通过读取配置元数据获得有关要实例化、配置和组装哪些对象的指令。配置元数据以 XML、爪哇 注释或 爪哇 代码表示。它允许你表达组成应用程序的对象,以及这些对象之间丰富的相互依赖关系。 Spring 提供了`ApplicationContext`接口的几种实现方式。在独立应用程序中,通常创建[“ClassPathXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)或[“FilesyStemXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/FileSystemXmlApplicationContext.html)的实例。尽管 XML 一直是定义配置元数据的传统格式,但你可以通过提供少量的 XML 配置来指示容器使用 爪哇 注释或代码作为元数据格式,从而声明性地支持这些附加的元数据格式。 在大多数应用程序场景中,不需要显式的用户代码来实例化 Spring IOC 容器的一个或多个实例。例如,在一个 Web 应用程序场景中,应用程序的`web.xml`文件中的一个简单的 8 行样板 Web 描述符 XML 通常就足够了(参见[用于 Web 应用程序的方便的应用程序上下文实例化](#context-create))。如果使用`name`(Eclipse 驱动的开发环境),只需单击几下鼠标或击键,就可以轻松地创建这个样板配置。 下图显示了 Spring 如何工作的高级视图。你的应用程序类与配置元数据相结合,这样,在创建和初始化`ApplicationContext`之后,你就拥有了一个完全配置和可执行的系统或应用程序。 ![container magic](images/container-magic.png) 图 1。 Spring IOC 容器 #### 1.2.1.配置元数据 如上图所示, Spring IOC 容器使用一种形式的配置元数据。这个配置元数据表示你作为应用程序开发人员如何告诉 Spring 容器实例化、配置和组装应用程序中的对象。 传统上,配置元数据是以简单直观的 XML 格式提供的,本章的大部分内容都使用这种格式来传达 Spring IoC 容器的关键概念和特性。 | |基于 XML 的元数据并不是配置元数据的唯一允许的形式。
Spring IOC 容器本身与
配置元数据实际使用的格式完全解耦。如今,许多开发人员为他们的 Spring 应用程序选择[基于 爪哇 的配置](#beans-java)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 有关在 Spring 容器中使用其他形式的元数据的信息,请参见: * [基于注释的配置](#beans-annotation-config): Spring 2.5 引入了对基于注释的配置元数据的支持。 * [基于 爪哇 的配置](#beans-java):从 Spring 3.0 开始, Spring 爪哇Config 项目提供的许多特性成为了核心 Spring 框架的一部分。因此,你可以使用 爪哇 而不是 XML 文件来定义应用程序类外部的 bean。要使用这些新功能,请参见[@configuration](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)、[`@Bean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html)、[`@Import`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Import.html)和[`@DependsOn`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/DependsOn.html)注释。 Spring 配置包括容器必须管理的至少一个且通常不止一个定义。基于 XML 的配置元数据将这些 bean 配置为顶级``元素中的``元素。爪哇 配置通常在`@Configuration`类中使用`@Bean`-带注释的方法。 Bean 这些定义对应于构成你的应用程序的实际对象。通常,你定义服务层对象、数据访问对象、表示对象(如 Struts实例)、基础设施对象(如 Hibernate `SessionFactory’、JMS)等等。通常,人们不会在容器中配置细粒度的域对象,因为通常是 DAO 和业务逻辑负责创建和加载域对象。但是,你可以使用 Spring 与 AspectJ 的集成来配置在 IOC 容器控制范围之外创建的对象。见[Using AspectJ to dependency-inject domain objects with Spring](#aop-atconfigurable)。 下面的示例展示了基于 XML 的配置元数据的基本结构: ``` (1) (2) ``` |**1**|`id`属性是一个字符串,用于标识单独的 Bean 定义。| |-----|----------------------------------------------------------------------------------------------| |**2**|`class`属性定义 Bean 的类型,并使用完全限定的
类名。| `id`属性的值是指协作对象。本例中没有显示用于引用协作对象的 XML。有关更多信息,请参见[Dependencies](#beans-dependencies)。 #### 1.2.2.实例化容器 提供给`ApplicationContext`构造函数的位置路径是资源字符串,这些资源字符串允许容器从各种外部资源加载配置元数据,例如本地文件系统、爪哇`CLASSPATH`,等等。 爪哇 ``` ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); ``` Kotlin ``` val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") ``` | |在了解了 Spring 的 IoC 容器之后,你可能想更多地了解 Spring 的 `resource`abstraction(如[Resources](#resources)中所述),它提供了一种方便的`adminEmails`机制,用于从在 URI 语法中定义的位置读取 InputStream。特别是,“资源”路径用于构造应用程序上下文,如[应用程序上下文和资源路径](#resources-app-ctx)中所述。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例显示了服务层对象`(services.xml)`配置文件: ``` ``` 下面的示例显示了数据访问对象`daos.xml`文件: ``` ``` 在前面的示例中,服务层由`PetStoreServiceImpl`类和`JpaAccountDao`和`JpaItemDao`类型的两个数据访问对象组成(基于 JPA 对象关系映射标准)。`property name`元素指的是 爪哇Bean 属性的名称,而[@configuration](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)元素指的是另一个 Bean 定义的名称。`id`和`ref`元素之间的这种链接表达了协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参见[Dependencies](#beans-dependencies)。 ##### 编写基于 XML 的配置元数据 Bean 定义跨越多个 XML 文件是有用的。通常,每个单独的 XML 配置文件都表示体系结构中的逻辑层或模块。 你可以使用应用程序上下文构造函数从所有这些 XML 片段加载 Bean 定义。这个构造函数接受多个`Resource`位置,如[上一节](#beans-factory-instantiation)中所示。或者,使用``元素的一个或多个出现来从另一个或多个文件加载 Bean 定义。下面的示例展示了如何做到这一点: ``` ``` 在前面的示例中,外部 Bean 定义是从三个文件加载的:`services.xml`、`messageSource.xml`和`themeSource.xml`。所有的位置路径都与执行导入的定义文件相关,因此`services.xml`必须与执行导入的文件位于同一目录或 Classpath 位置,而 `messagesource.xml` 和`themeSource.xml`必须位于导入文件位置下方的`resources`位置。正如你所看到的,前导斜杠被忽略了。然而,鉴于这些路径是相对的,最好的形式是根本不使用斜杠。根据 Spring 模式,要导入的文件的内容,包括顶层``元素,必须是有效的 XML Bean 定义。 | |使用
relative“../”路径引用父目录中的文件是可能的,但不推荐。这样做会在当前
应用程序之外的文件上创建一个依赖项。特别是,对于`classpath:`URL(对于
示例,`classpath:../services.xml`),不建议使用该引用,在该示例中,运行时解析过程选择
“最近的”根目录,然后查看其父目录。 Classpath
配置更改可能会导致选择不同的、不正确的目录。

你总是可以使用完全限定的资源位置,而不是相对路径:对于`file:C:/config/services.xml`示例,`file:C:/config/services.xml`或`ApplicationContext`。但是,请注意,你正在将应用程序的配置耦合到特定的绝对位置
。对于这样的绝对[Bean Scopes](#beans-factory-scopes)位置,通常最好是保持间接的——例如,通过在运行时针对 JVM
系统属性解析的“${…}”占位符。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 名称空间本身提供了导入指令功能。除了普通的 Bean 定义之外,在 Spring 提供的一系列 XML 命名空间中还可以获得更多的配置特性——例如,`context`和`util`命名空间。 ##### Groovy Bean 定义 DSL 作为外部化配置元数据的另一个示例, Bean 定义也可以在 Spring 的 Groovy Bean 定义 DSL 中表示,正如 Grails 框架中所知的那样。通常,这样的配置存在于一个“.groovy”文件中,其结构如以下示例所示: ``` beans { dataSource(BasicDataSource) { driverClassName = "org.hsqldb.jdbcDriver" url = "jdbc:hsqldb:mem:grailsDB" username = "sa" password = "" settings = [mynew:"setting"] } sessionFactory(SessionFactory) { dataSource = dataSource } myService(MyService) { nestedBean = { AnotherBean bean -> dataSource = dataSource } } } ``` 这种配置风格在很大程度上等同于 XML Bean 定义,甚至支持 Spring 的 XML 配置名称空间。它还允许通过`importBeans`指令导入 XML Bean 定义文件。 #### 1.2.3.使用容器 `ApplicationContext`是高级工厂的接口,该工厂能够维护不同 bean 及其依赖项的注册表。通过使用方法 `t getBean(字符串名称,类RequiredType)`,你可以检索 bean 的实例。 `ApplicationContext`允许你读取 Bean 定义并访问它们,如下例所示: 爪哇 ``` // create and configure beans ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); // retrieve configured instance PetStoreService service = context.getBean("petStore", PetStoreService.class); // use configured instance List userList = service.getUsernameList(); ``` Kotlin ``` import org.springframework.beans.factory.getBean // create and configure beans val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") // retrieve configured instance val service = context.getBean("petStore") // use configured instance var userList = service.getUsernameList() ``` 在 Groovy 配置下,引导看起来非常相似。它有一个不同的上下文实现类,它是 Groovy 感知的(但也理解 XML Bean 定义)。下面的示例展示了 Groovy 配置: 爪哇 ``` ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy"); ``` Kotlin ``` val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy") ``` 最灵活的变体是结合阅读器委托的`GenericApplicationContext`,例如,对于 XML 文件,使用`XmlBeanDefinitionReader`,如下例所示: 爪哇 ``` GenericApplicationContext context = new GenericApplicationContext(); new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); context.refresh(); ``` Kotlin ``` val context = GenericApplicationContext() XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml") context.refresh() ``` 对于 Groovy 文件,也可以使用`GroovyBeanDefinitionReader`,如下例所示: 爪哇 ``` GenericApplicationContext context = new GenericApplicationContext(); new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy"); context.refresh(); ``` Kotlin ``` val context = GenericApplicationContext() GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy") context.refresh() ``` 你可以在相同的`ApplicationContext`上混合和匹配这样的读取器委托,读取来自不同配置源的 Bean 定义。 然后,你可以使用`getBean`来检索你的 bean 实例。`ApplicationContext`接口有一些其他方法来检索 bean,但是,理想情况下,应用程序代码不应该使用它们。实际上,你的应用程序代码应该完全没有对“getBean()”方法的调用,因此完全没有对 Spring API 的依赖。例如, Spring 与 Web Frameworks 的集成为各种 Web Framework 组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,允许你通过元数据(例如自动连接注释)声明对特定 Bean 的依赖。 ### 1.3. Bean 概述 Spring IOC 容器管理一个或多个 bean。这些 bean 是使用你提供给容器的配置元数据创建的(例如,以 XML`` 定义的形式)。 在容器本身内,这些 Bean 定义被表示为`BeanDefinition`对象,其中包含(除其他信息外)以下元数据: * 包限定类名称:通常是正在定义的 Bean 的实际实现类。 * Bean 行为配置元素,其中说明 Bean 在容器中应该如何表现(作用域、生命周期回调,等等)。 * 引用 Bean 完成其工作所需的其他 bean。这些引用也被称为协作者或依赖项。 * 在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的 Bean 中使用的连接数量。 该元数据转换为一组属性,这些属性构成了每个 Bean 定义。下表描述了这些属性: | Property |解释在…| |------------------------|---------------------------------------------------------------------| | Class |[实例化豆类](#beans-factory-class)| | Name |[Naming Beans](#beans-beanname)| | Scope |[Bean Scopes](#beans-factory-scopes)| | Constructor arguments |[依赖注入](#beans-factory-collaborators)| | Properties |[依赖注入](#beans-factory-collaborators)| | Autowiring mode |[自动布线合作者](#beans-factory-autowire)| |Lazy initialization mode|[惰性初始化的 bean](#beans-factory-lazy-init)| | Initialization method |[初始化回调](#beans-factory-lifecycle-initializingbean)| | Destruction method |[销毁回调](#beans-factory-lifecycle-disposablebean)| Bean 定义包含关于如何创建特定 Bean 的信息,此外,[初始化回调](#beans-factory-lifecycle-initializingbean)实现还允许注册在容器之外(由用户)创建的现有对象。这是通过通过`getBeanFactory()`方法访问应用程序上下文的 BeanFactory 来完成的,该方法返回 BeanFactory`DefaultListableBeanFactory`实现。`DefaultListableBeanFactory`通过`registerSingleton(..)`和“registerBeanDefinition”方法支持这种注册。然而,典型的应用程序仅使用通过常规 Bean 定义元数据定义的 bean。 | |Bean 元数据和手动提供的单例实例需要尽可能早地
进行注册,以便容器在自动布线
和其他内省步骤期间对它们进行适当的推理。虽然在一定程度上支持重写现有元数据和现有的
单例实例,但官方不支持在
运行时注册新的 bean(与工厂的实时访问同时进行),并且
可能导致并发访问异常、 Bean 容器中的不一致状态,或者两者兼而有之。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.3.1.命名 bean 每个 Bean 具有一个或多个标识符。在承载 Bean 的容器中,这些标识符必须是唯一的。 Bean 通常只有一个标识符。但是,如果它需要多个,那么额外的一个可以被视为别名。 在基于 XML 的配置元数据中,使用`id`属性、`name`属性或两者来指定 Bean 标识符。`id`属性允许你精确地指定一个 ID。传统上,这些名称是字母数字(“MyBean”、“Someservice”等),但它们也可以包含特殊字符。如果要为 Bean 引入其他别名,还可以在`name`属性中指定它们,并用逗号、分号或空格分隔。作为一个历史记录,在 Spring 3.1 之前的版本中,`id`属性被定义为`xsd:ID`类型,该类型限制了可能的字符。在 3.1 中,它被定义为`xsd:string`类型。注意, Bean `id`唯一性仍然由容器强制执行,尽管不再由 XML 解析器执行。 你不需要为 Bean 提供`name`或`id`。如果没有显式地提供 `name’或`id`,则容器将为该 Bean 生成唯一的名称。但是,如果你希望通过使用`ref`元素或服务定位器样式查找来按名称引用该 Bean,则必须提供一个名称。不提供名称的动机与使用[inner beans](#beans-inner-beans)和[inner beans](#beans-inner-beans)有关。 Bean 命名约定 约定是在命名 bean 时对实例字段名称使用标准 爪哇 约定。也就是说, Bean 名称以小写字母开头,并从那里以驼峰式开头。这类名称的例子包括`accountManager`、`accountService’、`userDao`、`loginController`,等等。 始终如一地命名 bean 会使你的配置更易于阅读和理解。此外,如果你使用 Spring AOP,那么在将建议应用到一组与名称相关的 bean 时,它会有很大帮助。 | |使用 Classpath 中的组件扫描, Spring 为未命名的
组件生成 Bean 名称,遵循前面描述的规则:本质上,采用简单的类名
并将其初始字符转换为小写字母。然而,在(不寻常的)特殊`id`情况下,当有多个字符并且第一个和第二个字符
都是大写时,原始的外壳得到保留。这些规则与
定义的`java.beans.Introspector.decapitalize`( Spring 在此使用)相同。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 在 Bean 定义之外别名 Bean 在 Bean 定义本身中,可以通过使用由`id`属性指定的最多一个名称和`name`属性中的任意数量的其他名称的组合,为 Bean 提供多个名称。这些名称可以是相同的 Bean 的等价别名,并且在某些情况下是有用的,例如通过使用特定于该组件本身的 Bean 名称,让应用程序中的每个组件引用公共依赖项。 然而,在 Bean 实际定义的地方指定所有别名并不总是足够的。有时需要为在别处定义的 Bean 引入别名。在大型系统中,配置通常在每个子系统之间进行分配,每个子系统都有自己的一组对象定义,这种情况很常见。在基于 XML 的配置元数据中,可以使用``元素来实现这一点。下面的示例展示了如何做到这一点: ``` ``` 在这种情况下, Bean(在相同的容器中)名为的还可以在使用这种别名定义之后,被称为。 例如,子系统 A 的配置元数据可以通过`subsystemA-dataSource`的名称引用数据源。子系统 B 的配置元数据可以引用名为`subsystemB-dataSource`的数据源。在组成使用这两个子系统的主应用程序时,主应用程序以`myApp-dataSource`的名称引用数据源。要让这三个名称都引用同一个对象,可以向配置元数据添加以下别名定义: ``` ``` 现在,每个组件和主应用程序都可以通过一个唯一的名称来引用数据源,并保证不会与任何其他定义冲突(有效地创建一个名称空间),但是它们引用的是相同的 Bean。 爪哇 配置 如果使用 爪哇Configuration,可以使用`@Bean`注释来提供别名。详见[Using the `@Bean` Annotation](#beans-java-bean-annotation)。 #### 1.3.2.实例化豆类 Bean 定义本质上是用于创建一个或多个对象的配方。容器在被请求时查看命名 Bean 的配方,并使用由该 Bean 定义封装的配置元数据来创建(或获取)实际对象。 如果使用基于 XML 的配置元数据,则可以在``元素的`class`属性中指定要实例化的对象类型(或类)。这个 `class` 属性(在内部是`Class`实例上的`Class`属性)通常是强制的。(有关异常,请参见[通过使用实例工厂方法实现实例化](#beans-factory-class-instance-factory-method)和[Bean Definition Inheritance](#beans-child-bean-definitions)。)你可以通过以下两种方式之一使用`Class`属性: * 通常,在容器本身通过反射地调用其构造函数直接创建 Bean 的情况下,指定要构造的 Bean 类,这在某种程度上相当于使用`new`操作符的 爪哇 代码。 * 要指定包含用于创建对象的`static`工厂方法的实际类,在不太常见的情况下,容器调用类上的 ` 静态’工厂方法来创建 Bean。调用`static`工厂方法返回的对象类型可以是同一个类,也可以完全是另一个类。 嵌套类名 如果你希望为嵌套类配置 Bean 定义,那么你可以使用嵌套类的二进制名称或源名。 例如,如果在`com.example`包中有一个名为`SomeThing`的类,而这个`SomeThing`类有一个名为`static`的嵌套类,它们可以用美元符号或点分隔。因此,在 Bean 定义中,`class`属性的值将是`com.example.SomeThing$OtherThing`或 `com.example.something.otherthing’。 ##### 使用构造函数实例化 当通过构造函数方法创建 Bean 时,所有普通类都可由 Spring 使用并与 Spring 兼容。也就是说,正在开发的类不需要实现任何特定的接口,也不需要以特定的方式进行编码。只需指定 Bean 类就足够了。然而,根据你为该特定 Bean 使用的 IOC 类型,你可能需要一个默认(空)构造函数。 Spring IOC 容器实际上可以管理你希望它管理的任何类。它不仅限于管理真正的 爪哇Beans。 Spring 大多数用户更喜欢实际的 爪哇Bean,它只有一个默认的(无参数的)构造函数,以及按照容器中的属性建模的适当的 setter 和 getter。你还可以在你的容器中有更多奇异的非 Bean 样式类。例如,如果你需要使用一个绝对不遵守 爪哇Bean 规范的遗留连接池, Spring 也可以对其进行管理。 使用基于 XML 的配置元数据,你可以按以下方式指定你的 Bean 类: ``` ``` 有关在构造对象之后向构造函数提供参数和设置对象实例属性的机制的详细信息,请参见[注入依赖项](#beans-factory-collaborators)。 ##### 使用静态工厂方法的实例化 在定义使用静态工厂方法创建的 Bean 时,使用`class`属性指定包含`static`工厂方法和一个名为`factory-method`的属性的类,以指定工厂方法本身的名称。你应该能够调用这个方法(使用可选参数,如后面所述)并返回一个活动对象,随后将其视为通过构造函数创建的对象。这种 Bean 定义的一种用途是在遗留代码中调用`static`工厂。 下面的 Bean 定义指定通过调用工厂方法来创建 Bean。该定义没有指定返回对象的类型(类),只指定了包含工厂方法的类。在本例中,`createInstance()`方法必须是静态方法。下面的示例展示了如何指定工厂方法: ``` ``` 下面的示例展示了一个将与前面的 Bean 定义一起工作的类: 爪哇 ``` public class ClientService { private static ClientService clientService = new ClientService(); private ClientService() {} public static ClientService createInstance() { return clientService; } } ``` Kotlin ``` class ClientService private constructor() { companion object { private val clientService = ClientService() fun createInstance() = clientService } } ``` 有关向工厂方法提供(可选的)参数并在从工厂返回对象后设置对象实例属性的机制的详细信息,请参见[详细介绍依赖关系和配置](#beans-factory-properties-detailed)。 ##### 通过使用实例工厂方法实现实例化 与通过[静态工厂法](#beans-factory-class-static-factory-method)的实例化类似,使用实例工厂方法的实例化从容器调用现有 Bean 的非静态方法来创建新的 Bean。要使用此机制,将`class`属性保留为空,并在`factory-bean`属性中,指定当前(或父容器或祖先容器)中的 Bean 的名称,该容器包含将被调用以创建对象的实例方法。使用`factory-method`属性设置工厂方法本身的名称。下面的示例显示了如何配置这样的 Bean: ``` ``` 下面的示例展示了相应的类: 爪哇 ``` public class DefaultServiceLocator { private static ClientService clientService = new ClientServiceImpl(); public ClientService createClientServiceInstance() { return clientService; } } ``` Kotlin ``` class DefaultServiceLocator { companion object { private val clientService = ClientServiceImpl() } fun createClientServiceInstance(): ClientService { return clientService } } ``` 一个工厂类也可以包含多个工厂方法,如下例所示: ``` ``` 下面的示例展示了相应的类: 爪哇 ``` public class DefaultServiceLocator { private static ClientService clientService = new ClientServiceImpl(); private static AccountService accountService = new AccountServiceImpl(); public ClientService createClientServiceInstance() { return clientService; } public AccountService createAccountServiceInstance() { return accountService; } } ``` Kotlin ``` class DefaultServiceLocator { companion object { private val clientService = ClientServiceImpl() private val accountService = AccountServiceImpl() } fun createClientServiceInstance(): ClientService { return clientService } fun createAccountServiceInstance(): AccountService { return accountService } } ``` 这种方法表明, Bean 工厂本身可以通过依赖注入进行管理和配置。见[详细介绍依赖关系和配置](#beans-factory-properties-detailed)。 | |在 Spring 文档中,“factory Bean”是指在
Spring 容器中配置并通过[instance](#beans-factory-class-instance-factory-method)或[static](#beans-factory-class-static-factory-method)工厂方法创建对象的 Bean。相比之下,“FactoryBean”(请注意大写)是指特定于 Spring 的[`FactoryBean`](#beans-factory-extension-factorybean)实现类。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 确定 Bean 的运行时类型 Bean 特定的运行时类型是不可平凡地确定的。 Bean 元数据定义中的指定类只是一个初始类引用,可能与声明的工厂方法结合在一起,或者是一个`FactoryBean`类,这可能导致 Bean 的不同运行时类型,或者在实例级工厂方法的情况下根本没有设置(而是通过指定的`factory-bean`名称进行解析)。此外, AOP 代理可以用基于接口的代理包装 Bean 实例,该代理有限地暴露目标 Bean 的实际类型(仅包括其实现的接口)。 查找特定 Bean 的实际运行时类型的推荐方法是对指定的 Bean 名称进行`BeanFactory.getType`调用。这将考虑上述所有情况,并返回`BeanFactory.getBean`调用将为相同的 Bean 名称返回的对象类型。 ### 1.4.依赖关系 典型的 Enterprise 应用程序不包括单个对象(或者在 Spring 的说法中是 Bean)。即使是最简单的应用程序也有几个对象一起工作,以呈现最终用户认为是一致的应用程序。下一节将解释如何从定义多个 Bean 单独的定义发展到一个完全实现的应用程序,在该应用程序中,对象协作以实现一个目标。 #### 1.4.1.依赖注入 依赖注入是一个过程,在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们所使用的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。这个过程从根本上讲是 Bean 本身的逆过程(因此称为控制的逆过程),通过使用类的直接构造或服务定位器模式来控制其依赖项的实例化或位置。 使用 DI 原则的代码更干净,当对象提供了它们的依赖关系时,解耦更有效。对象不会查找其依赖项,也不知道依赖项的位置或类。结果,类变得更容易测试,特别是当依赖于接口或抽象基类时,这允许在单元测试中使用存根或模拟实现。 DI 有两个主要的变体:[基于构造函数的依赖注入](#beans-constructor-injection)和[基于 setter 的依赖注入](#beans-setter-injection)。 ##### 基于构造函数的依赖注入 基于构造函数的 DI 是由容器调用具有多个参数的构造函数来完成的,每个参数表示一个依赖项。调用带有特定参数的`static`工厂方法来构造 Bean 几乎是等效的,并且此讨论将参数处理为构造函数和`static`工厂方法。下面的示例展示了一个只能通过构造函数注入进行依赖注入的类: 爪哇 ``` public class SimpleMovieLister { // the SimpleMovieLister has a dependency on a MovieFinder private final MovieFinder movieFinder; // a constructor so that the Spring container can inject a MovieFinder public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // business logic that actually uses the injected MovieFinder is omitted... } ``` Kotlin ``` // a constructor so that the Spring container can inject a MovieFinder class SimpleMovieLister(private val movieFinder: MovieFinder) { // business logic that actually uses the injected MovieFinder is omitted... } ``` 请注意,这门课没有什么特别之处。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。 ###### 构造函数参数解析 构造函数的参数解析匹配是通过使用参数的类型来实现的。如果在 Bean 定义的构造函数参数中不存在潜在的歧义,则在 Bean 定义中定义构造函数参数的顺序是在 Bean 被实例化时将这些参数提供给适当的构造函数的顺序。考虑以下类: 爪哇 ``` package x.y; public class ThingOne { public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { // ... } } ``` Kotlin ``` package x.y class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree) ``` 假设`ThingTwo`和`ThingThree`类之间没有继承关系,则不存在潜在的歧义。因此,下面的配置工作正常,并且你不需要在 `` 元素中显式地指定构造函数参数索引或类型。 ``` ``` 当引用另一个 Bean 时,类型是已知的,并且可以发生匹配(与前面的示例一样)。当使用简单的类型时,例如 `true`, Spring 无法确定该值的类型,因此在没有帮助的情况下无法按类型进行匹配。考虑以下类: 爪哇 ``` package examples; public class ExampleBean { // Number of years to calculate the Ultimate Answer private final int years; // The Answer to Life, the Universe, and Everything private final String ultimateAnswer; public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` Kotlin ``` package examples class ExampleBean( private val years: Int, // Number of years to calculate the Ultimate Answer private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything ) ``` []()构造函数参数类型匹配 在前面的场景中,如果你使用`type`属性显式地指定构造函数参数的类型,则容器可以使用与简单类型匹配的类型,如下例所示: ``` ``` []()构造函数参数索引 可以使用`index`属性显式指定构造函数参数的索引,如下例所示: ``` ``` 除了解决多个简单值的歧义之外,在构造函数具有两个相同类型的参数的情况下,指定索引还可以解决歧义。 | |该索引是以 0 为基础的。| |---|---------------------| []()构造函数参数 Name 还可以使用构造函数参数名称来消歧,如下例所示: ``` ``` 请记住,要使这项工作开箱即用,你的代码必须使用启用的调试标志进行编译,以便 Spring 可以从构造函数中查找参数名称。如果不能或不想使用 Debug 标志编译代码,可以使用[@constructorProperties](https://download.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html)JDK 注释显式地命名构造函数参数。然后,示例类必须如下所示: 爪哇 ``` package examples; public class ExampleBean { // Fields omitted @ConstructorProperties({"years", "ultimateAnswer"}) public ExampleBean(int years, String ultimateAnswer) { this.years = years; this.ultimateAnswer = ultimateAnswer; } } ``` Kotlin ``` package examples class ExampleBean @ConstructorProperties("years", "ultimateAnswer") constructor(val years: Int, val ultimateAnswer: String) ``` ##### 基于 setter 的依赖注入 基于 setter 的 DI 是在调用一个无参数构造函数或一个无参数`static`工厂方法实例化你的 Bean 之后,由容器调用 bean 上的 setter 方法来完成的。 下面的示例展示了一个只能通过使用纯 setter 注入进行依赖注入的类。这个类是传统的 爪哇。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。 Java ``` public class SimpleMovieLister { // the SimpleMovieLister has a dependency on the MovieFinder private MovieFinder movieFinder; // a setter method so that the Spring container can inject a MovieFinder public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // business logic that actually uses the injected MovieFinder is omitted... } ``` Kotlin ``` class SimpleMovieLister { // a late-initialized property so that the Spring container can inject a MovieFinder lateinit var movieFinder: MovieFinder // business logic that actually uses the injected MovieFinder is omitted... } ``` `ApplicationContext`为其管理的 bean 支持基于构造函数和基于 setter 的 DI。在通过构造函数方法注入了一些依赖项之后,它还支持基于 setter 的 DI。你以`BeanDefinition`的形式配置依赖项,该依赖项与`ApplicationContext`实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即编程),而是使用 XML`bean`定义、带注释的组件(即用`@Component`、`@controller` 等注释的类),或者基于 Java 的`ApplicationContext`类中的`ApplicationContext`方法。然后将这些源在内部转换为`BeanDefinition`的实例,并用于加载整个 Spring IoC 容器实例。 基于构造器还是基于设置器的 DI? 由于可以混合使用基于构造函数和基于 setter 的 DI,因此使用构造函数来实现强制依赖项,使用 setter 方法或配置方法来实现可选依赖项是一个很好的经验法则。请注意,在 setter 方法上使用`ApplicationContext`注释可以使属性成为一个必需的依赖项;但是,更好的方法是使用带参数的编程验证的构造函数注入。 Spring 团队通常提倡构造函数注入,因为它允许将应用程序组件实现为不可变对象,并确保所需的依赖关系不是`null`。此外,构造函数注入的组件总是以完全初始化的状态返回给客户机(调用)代码。作为附带说明,大量的构造函数参数是一种糟糕的代码气味,这意味着类可能有太多的责任,应该进行重构以更好地解决关注的适当分离。 Setter 注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,在代码使用依赖项的所有地方都必须执行 not-null 检查。setter 注入的一个好处是,setter 方法使该类的对象可以在以后进行重新配置或重新注入。因此,通过[JMX MBeans](integration.html#jmx)进行管理是 Setter 注入的一个引人注目的用例。 使用对特定类最有意义的 DI 样式。有时,在处理你没有源代码的第三方类时,你需要做出选择。例如,如果第三方类不公开任何 setter 方法,那么构造函数注入可能是唯一可用的 DI 形式。 ##### 依赖关系解决过程 容器执行 Bean 依赖项解析,如下所示: * 使用描述所有 bean 的配置元数据创建和初始化`ApplicationContext`。配置元数据可以通过 XML、Java 代码或注释来指定。 * 对于每个 Bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示(如果你使用它而不是普通的构造函数)。这些依赖关系被提供给 Bean,当 Bean 实际被创建时。 * Bean 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个值的引用。 * 作为值的每个属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下, Spring 可以将以字符串格式提供的值转换为所有内置类型,例如`int`、`long’、`String`、`boolean`,等等。 Spring 容器在创建容器时验证每个 Bean 的配置。然而,在实际创建 Bean 之前, Bean 属性本身不会被设置。当创建容器时,将创建单实例范围并设置为预实例化(默认)的 bean。作用域在[Bean Scopes](#beans-factory-scopes)中定义。否则, Bean 仅在被请求时才被创建。 Bean 的创建可能会导致创建 bean 的图形,因为 Bean 的依赖项及其依赖项的依赖项(等等)被创建和分配。请注意,这些依赖项之间的分辨率不匹配可能会出现在较晚的时候——也就是说,在第一次创建受影响的 Bean 时。 循环依赖 如果主要使用构造函数注入,则有可能创建一个不可解析的循环依赖场景。 例如:类 A 通过构造函数注入需要类 B 的实例,而类 B 通过构造函数注入需要类 A 的实例。如果将 bean 配置为类 A 和类 B 相互注入, Spring IOC 容器在运行时检测到这个循环引用,并抛出一个“BeanCurrentlyIncreationException”。 一种可能的解决方案是编辑一些类的源代码,由 setter 而不是构造函数来配置。或者,避免构造函数注入,而只使用 setter 注入。换句话说,尽管不推荐使用它,但你可以使用 setter 注入配置循环依赖项。 与典型的情况(没有循环依赖关系)不同, Bean a 和 Bean b 之间的循环依赖关系迫使其中一个 bean 在完全初始化自身之前被注入到另一个 bean 中(典型的先有鸡后有蛋的场景)。 你通常可以相信 Spring 会做正确的事。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖关系。 Spring 设置属性并尽可能晚地解决依赖关系,当 Bean 实际创建时。这意味着,当你请求一个对象时,如果在创建该对象或其依赖关系中存在问题时,已经正确加载的 Spring 容器可以在以后生成一个异常——例如, Bean 抛出一个异常是由于缺少或无效的属性造成的。这种对某些配置问题的潜在延迟可见性是`ApplicationContext`实现默认预实例化单例 bean 的原因。在实际需要这些 bean 之前创建这些 bean 需要一些前期时间和内存,但在创建`ApplicationContext`时(而不是以后),你会发现配置问题。你仍然可以重写此缺省行为,以便 Singleton Bean 可以惰性地初始化,而不是急切地预先实例化。 如果不存在循环依赖关系,则当一个或多个协作 bean 被注入到依赖的 Bean 中时,每个协作的 Bean 在被注入到依赖的 Bean 中之前被完全配置。这意味着,如果 Bean a 对 Bean b 具有依赖性,则 Spring IOC 容器在 Bean a 上调用 setter 方法之前完全配置 Bean b。换句话说, Bean 是实例化的(如果它不是预实例化的单例),它的依赖项被设置,并且相关的生命周期方法(例如[初始化 bean 回调方法](#beans-factory-lifecycle-initializingbean)或[初始化 bean 回调方法](#beans-factory-lifecycle-initializingbean))被调用。 ##### 依赖注入示例 下面的示例为基于 setter 的 DI 使用基于 XML 的配置元数据。 Spring XML 配置文件的一小部分指定了如下一些 Bean 定义: ``` ``` 下面的示例显示了相应的`ExampleBean`类: Java ``` public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public void setBeanOne(AnotherBean beanOne) { this.beanOne = beanOne; } public void setBeanTwo(YetAnotherBean beanTwo) { this.beanTwo = beanTwo; } public void setIntegerProperty(int i) { this.i = i; } } ``` Kotlin ``` class ExampleBean { lateinit var beanOne: AnotherBean lateinit var beanTwo: YetAnotherBean var i: Int = 0 } ``` 在前面的示例中,setter 被声明为与 XML 文件中指定的属性匹配。下面的示例使用基于构造函数的 DI: ``` ``` 下面的示例显示了相应的`ExampleBean`类: Java ``` public class ExampleBean { private AnotherBean beanOne; private YetAnotherBean beanTwo; private int i; public ExampleBean( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { this.beanOne = anotherBean; this.beanTwo = yetAnotherBean; this.i = i; } } ``` Kotlin ``` class ExampleBean( private val beanOne: AnotherBean, private val beanTwo: YetAnotherBean, private val i: Int) ``` Bean 定义中指定的构造函数参数被用作`ExampleBean`构造函数的参数。 现在考虑这个示例的一个变体,其中, Spring 被告知调用`static`Factory 方法来返回对象的实例,而不是使用构造函数: ``` ``` 下面的示例显示了相应的`ExampleBean`类: Java ``` public class ExampleBean { // a private constructor private ExampleBean(...) { ... } // a static factory method; the arguments to this method can be // considered the dependencies of the bean that is returned, // regardless of how those arguments are actually used. public static ExampleBean createInstance ( AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { ExampleBean eb = new ExampleBean (...); // some other operations... return eb; } } ``` Kotlin ``` class ExampleBean private constructor() { companion object { // a static factory method; the arguments to this method can be // considered the dependencies of the bean that is returned, // regardless of how those arguments are actually used. fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean { val eb = ExampleBean (...) // some other operations... return eb } } } ``` `static`工厂方法的参数由``元素提供,这与实际使用构造函数的情况完全相同。工厂方法返回的类的类型不必与包含`static`工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用`factory-bean`属性而不是`class`属性),因此我们在这里不讨论这些细节。 #### 1.4.2.详细介绍依赖关系和配置 正如[上一节](#beans-factory-collaborators)中提到的,你可以将 Bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用,或者作为内联定义的值。 Spring 的基于 XML 的配置元数据为此支持其``和`ExampleBean`元素中的子元素类型。 ##### 直值(原语、字符串等) ``元素的`value`属性将一个属性或构造函数参数指定为人类可读的字符串表示形式。 Spring 的[转换服务](#core-convert-ConversionService-API)用于将这些值从`String`转换为属性或参数的实际类型。下面的示例显示了正在设置的各种值: ``` ``` 下面的示例使用[p-namespace](#beans-p-namespace)实现更简洁的 XML 配置: ``` ``` 前面的 XML 更简洁。但是,除非你使用一个 IDE(例如[IntelliJ IDEA](https://www.jetbrains.com/idea/)或[Spring Tools for Eclipse](https://spring.io/tools))来支持在创建 Bean 定义时自动完成属性,否则会在运行时而不是在设计时发现打字错误。强烈推荐这种 IDE 协助。 你还可以配置`java.util.Properties`实例,如下所示: ``` jdbc.driver.className=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mydb ``` Spring 容器使用 JavaBeans``机制将``元素内的文本转换为 `java.util.properties’实例。这是一个很好的快捷方式,并且是 Spring 团队确实支持使用嵌套``元素而不是`value`属性样式的少数几个地方之一。 ###### `idref`元素 `idref`元素只是一种防错误的方式,可以将容器中另一个 Bean 的`id`(一个字符串值-不是引用)传递给``或``元素。下面的示例展示了如何使用它: ``` ``` Bean 前面的定义片段(在运行时)与下面的片段完全等效: ``` ``` 第一种形式比第二种形式更好,因为使用`idref`标记可以让容器在部署时验证所引用的名为 Bean 的容器实际存在。在第二个变化中,不对传递给`client` Bean 的`targetName`属性的值执行验证。只有当`client` Bean 被实际实例化时,才会发现错别字(最有可能导致致命的结果)。如果`client` Bean 是[prototype](#beans-factory-scopes) Bean,则只有在部署容器很长时间后才能发现该错别字和由此产生的异常。 | |在 4.0bean
XSD 中,`local`元素上的`local`属性不再受支持,因为它不再为常规的`bean`引用提供值。在升级到 4.0 模式时,将现有的
引用更改为`idref bean`。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ``元素带来价值的一个常见位置(至少在 Spring 2.0 之前的版本中)是在 `proxyFactoryBean` Bean 定义中的[AOP interceptors](#aop-pfb-1)的配置中。在指定拦截器名称时使用``元素可以防止错误拼写拦截器 ID。 ##### 对其他 bean 的引用(协作者) `ref`元素是``或``定义元素中的最后一个元素。在这里,你将 Bean 的指定属性的值设置为对由容器管理的另一个 Bean(协作者)的引用。引用的 Bean 是要设置其属性的 Bean 的依赖项,并且在设置该属性之前根据需要对其进行初始化。(如果协作者是单例 Bean,它可能已经被容器初始化了。)所有引用最终都是对另一个对象的引用。范围和验证取决于你是通过`bean`还是`parent`属性指定另一个对象的 ID 或名称。 通过``标记的`bean`属性指定目标 Bean 是最通用的形式,并且允许在相同的容器或父容器中创建对任何 Bean 的引用,无论它是否在相同的 XML 文件中。 Bean 属性的值可以与目标 Bean 的`id`属性相同,或者与目标 Bean 的`name`属性中的一个值相同。下面的示例展示了如何使用`ref`元素: ``` ``` 通过`parent`属性指定目标 Bean 将创建对当前容器的父容器中的 Bean 的引用。`parent`属性的值可以与目标 Bean 的`id`属性相同,也可以与目标 Bean 的`name`属性中的一个值相同。目标 Bean 必须位于当前容器的父容器中。当你有一个容器的层次结构,并且希望用一个与父 Bean 名称相同的代理来包装父容器中的现有 Bean 时,你应该主要使用这个 Bean 引用变体。下面的一对清单展示了如何使用`parent`属性: ``` ``` ``` class="org.springframework.aop.framework.ProxyFactoryBean"> ``` | |在 4.0bean
XSD 中,`local`元素上的`local`属性不再受支持,因为它不再为常规的`bean`引用提供值。在升级到 4.0 模式时,将你现有的
引用更改为`ref bean`。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 内豆 一个``元素内部的``或``元素定义了一个内部 Bean,如下例所示: ``` ``` 内部 Bean 定义不需要定义的 ID 或名称。如果指定,则容器不使用这样的值作为标识符。容器还在创建时忽略`scope`标志,因为内部 bean 总是匿名的,并且总是与外部 bean 一起创建 Bean。 Bean 不可能独立地访问内部 bean 或将它们注入协作 bean 中。 作为一个角的情况,可以从自定义范围接收销毁回调——例如,对于包含在单例 Bean 中的请求范围的内部 Bean。内部 Bean 实例的创建与其包含的 Bean 绑定在一起,但是销毁回调允许它参与请求作用域的生命周期。这种情况并不常见。内部 bean 通常只共享其包含 Bean 的范围。 ##### 收藏 ``、``、``和``元素分别设置 Java`Collection`类型`List`、`Set`、`Set`和`Properties`的属性和参数。下面的示例展示了如何使用它们: ``` [email protected] [email protected] [email protected] a list element followed by a reference just some string ``` 映射键或值或设定值的值也可以是以下任何元素: ``` bean | ref | idref | list | set | map | props | value | null ``` ###### 集合合并 Spring 容器还支持合并集合。应用程序开发人员可以定义父``、``、``或``元素,并具有子元素``、``、``或``元素来继承和重写来自父集合的值。也就是说,子集合的值是合并父集合和子集合的元素的结果,而子集合的元素重写了父集合中指定的值。 关于合并的这一节讨论了父-子机制 Bean。 Bean 不熟悉父和子定义的读者在继续之前可能希望阅读[相关部分](#beans-child-bean-definitions)。 下面的示例演示集合合并: ``` [email protected] [email protected] [email protected] [email protected] ``` 注意在`child` Bean 定义的 `adminemails’属性的``元素上使用`merge=true`属性。当容器解析并实例化`child` Bean 时,生成的实例具有一个`adminEmails``properties` 集合,该集合包含将子的 `adminemails’集合与父的`adminEmails`集合合并的结果。下面的列表显示了结果: ``` [email protected] [email protected] [email protected] ``` 子`Properties`集合的值集继承了父``中的所有属性元素,而子`support`值的值覆盖了父集合中的值。 这种合并行为类似于``、``和``集合类型。在``元素的特定情况下,维护与`List`集合类型(即`ordered`值集合的概念)相关联的语义。父级的值在所有子列表的值之前。在`Map`、`Set`和`Properties`集合类型的情况下,不存在排序。因此,对于容器内部使用的关联`Map`、`Set`和`Properties`实现类型的集合类型,没有任何排序语义。 ###### 集合合并的局限性 不能合并不同的集合类型(例如`Map`和`List`)。如果你尝试这样做,将抛出一个适当的`Exception`。`merge`属性必须在较低的、继承的子定义上指定。在父集合定义上指定`merge`属性是多余的,不会导致所需的合并。 ###### 强类型集合 通过在 Java5 中引入泛型类型,你可以使用强类型集合。也就是说,可以声明`Collection`类型,使得它只能包含(例如)`String`元素。如果使用 Spring 来将强类型的``注入到 Bean 中,则可以利用 Spring 的类型转换支持,使得强类型的`Collection`实例的元素在被添加到`Collection`之前被转换为适当的类型。下面的 Java 类和 Bean 定义展示了如何做到这一点: Java ``` public class SomeClass { private Map accounts; public void setAccounts(Map accounts) { this.accounts = accounts; } } ``` Kotlin ``` class SomeClass { lateinit var accounts: Map } ``` ``` ``` 当`accounts`属性的`something`被准备注入时,关于强类型的元素类型的泛型信息`Map`通过反射是可用的。因此, Spring 的类型转换基础结构将各种值元素识别为类型,并且将字符串值(`9.99’、和 `3.99’)转换为实际的类型。 ##### 空字符串符值和空字符串符值 Spring 将属性等的空参数视为空`Strings`。以下基于 XML 的配置元数据片段将`email`属性设置为空的 `string`value。 ``` ``` 前面的示例相当于下面的 Java 代码: Java ``` exampleBean.setEmail(""); ``` Kotlin ``` exampleBean.email = "" ``` ``元素处理`null`值。下面的清单展示了一个示例: ``` ``` 前面的配置相当于下面的 Java 代码: Java ``` exampleBean.setEmail(null); ``` Kotlin ``` exampleBean.email = null ``` ##### 带有 P-namespace 的 XML 快捷方式 P-Namespace 允许你使用`bean`元素的属性(而不是嵌套的 `` 元素)来描述你的属性值协作 bean,或两者兼而有之。 Spring 支持可扩展的配置格式[with namespaces](#xsd-schemas),其基于 XML 模式定义。本章讨论的`beans`配置格式是在 XML 模式文档中定义的。然而,P-命名空间不是在 XSD 文件中定义的,并且仅存在于 Spring 的核心中。 下面的示例展示了两个解析为相同结果的 XML 片段(第一个使用标准 XML 格式,第二个使用 P 名称空间): ``` ``` 该示例在 Bean 定义中显示了一个名为`email`的 P-namespace 中的属性。这告诉 Spring 要包括一个属性声明。如前所述,P-Namespace 没有模式定义,因此你可以将属性的名称设置为属性名称。 下一个示例包括另外两个 Bean 定义,这两个定义都具有对另一个 Bean 的引用: ``` ``` 这个示例不仅包括使用 P 名称空间的属性值,还使用一种特殊格式声明属性引用。鉴于第一个 Bean 定义使用``来创建从 Bean `John` 到 Bean `jane`的引用,而第二个 Bean 定义使用`p:spouse-ref="jane"`作为属性来做完全相同的事情。在这种情况下,`spouse`是属性名,而`-ref`部分表明这不是一个直值,而是对另一个值的引用 Bean。 | |P-namespace 不像标准 XML 格式那样灵活。例如,用于声明属性引用的
格式与以`Ref`结尾的属性相冲突,而
标准 XML 格式则不冲突。我们建议你谨慎地选择你的方法,并将
与你的团队成员进行沟通,以避免生成同时使用所有
三种方法的 XML 文档。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 带有 C-Namespace 的 XML 快捷方式 与[带有 P-namespace 的 XML 快捷方式](#beans-p-namespace)类似, Spring 3.1 中引入的 C-namespace 允许内联属性用于配置构造函数参数,而不是嵌套`constructor-arg`元素。 下面的示例使用`c:`名称空间来执行与 from[基于构造函数的依赖注入](#beans-constructor-injection)相同的操作: ``` ``` `c:`名称空间使用与`p:`1( Bean 引用的尾随`-ref`)相同的约定来根据构造函数参数的名称设置它们。类似地,它需要在 XML 文件中声明,即使它不是在 XSD 模式中定义的(它存在于 Spring 内核中)。 对于构造函数参数名称不可用的罕见情况(通常如果字节码是在没有调试信息的情况下编译的),可以使用回退到参数索引,如下所示: ``` ``` | |由于 XML 语法的原因,由于 XML 属性名称不能以数字开头(即使某些 IDE 允许),因此索引符号需要存在前导数`_`,

也可以使用相应的索引符号。对于``元素但
不常用,因为声明的普通顺序通常在那里就足够了。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在实践中,构造函数分辨率[mechanism](#beans-factory-ctor-arguments-resolution)在匹配参数方面非常有效,因此,除非你确实需要,否则我们建议在整个配置中使用名称符号。 ##### 复合属性名称 在设置 Bean 属性时,可以使用复合或嵌套的属性名称,只要路径的所有组件(除了最终的属性名称)不是`null`。考虑以下 Bean 定义: ``` ``` `something` Bean 有一个`fred`属性,它有一个`bob`属性,它有一个`sammy`属性,最后的`sammy`属性被设置为一个值`123`。为了使其工作,`fred`的`something`属性和`bob`的`bob`属性在构造 Bean 之后一定不能是`null`。否则,将抛出一个`NullPointerException`。 #### 1.4.3.使用`depends-on` 如果一个 Bean 是另一个 Bean 的依赖项,这通常意味着一个 Bean 被设置为另一个 Bean 的属性。通常,你使用基于 XML 的配置元数据中的[``element](#beans-ref-element)来实现这一点。然而,有时 bean 之间的依赖关系并不那么直接。一个例子是当需要触发类中的静态初始化器时,例如用于数据库驱动程序注册。`depends-on`属性可以显式地强制一个或多个 bean 在使用该元素初始化 Bean 之前被初始化。下面的示例使用`depends-on`属性来表示对单个 Bean 的依赖关系: ``` ``` 要表示对多个 bean 的依赖关系,请提供一个 Bean 名称列表,作为`depends-on`属性的值(逗号、空格和分号是有效的分隔符): ``` ``` | |`depends-on`属性既可以指定一个初始化-时间依赖项,也可以指定
在[singleton](#beans-factory-scopes-singleton)bean 的情况下,对应的
销毁-时间依赖项。定义与给定 Bean 的`depends-on`关系
的依赖 bean 首先被销毁,在给定 Bean 本身被销毁之前。
因此,`depends-on`也可以控制关闭顺序。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.4.4.惰性初始化的 bean 默认情况下,`ApplicationContext`实现急切地创建和配置所有[singleton](#beans-factory-scopes-singleton)bean,作为初始化过程的一部分。通常,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是在几个小时甚至几天之后。当这种行为不可取时,可以通过将 Bean 定义标记为惰性初始化来防止单例 Bean 的预实例化。惰性初始化的 Bean 告诉 IOC 容器在首次请求时而不是在启动时创建 Bean 实例。 在 XML 中,这种行为由``元素上的`lazy-init`属性控制,如下例所示: ``` ``` 当前面的配置被`ApplicationContext`消耗时,当`ApplicationContext`开始时,`lazy` Bean 不会急切地预先实例化,而`not.lazy` Bean 会急切地预先实例化。 然而,当惰性初始化的 Bean 是未惰性初始化的单例 Bean 的依赖项时,`ApplicationContext`在启动时创建惰性初始化的 Bean,因为它必须满足单例的依赖项。将惰性初始化的 Bean 注入到其他未惰性初始化的单例 Bean 中。 你还可以在容器级别使用``元素上的 `default-lazy-init’属性来控制 lazy-initialization,如下例所示: ``` ``` #### 1.4.5.自动布线合作者 Spring 容器可以自动连接协作 bean 之间的关系。你可以通过检查`ApplicationContext`的内容,让 Spring 为你的 Bean 自动解析协作者(其他 bean)。自动布线有以下优点: * 自动布线可以大大减少指定属性或构造函数参数的需要。(其他机制,如 Bean 模板[在本章的其他地方讨论过](#beans-child-bean-definitions)在这方面也很有价值。) * 随着对象的发展,自动布线可以更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。因此,在开发过程中,自动布线特别有用,而无需否定当代码库变得更稳定时切换到显式布线的选项。 当使用基于 XML 的配置元数据(参见[依赖注入](#beans-factory-collaborators))时,你可以使用 `` 元素的`autowire`属性为 Bean 定义指定 AutoWire 模式。自动接线功能有四种模式。你可以根据 Bean 指定自动接线,因此可以选择要自动接线的接线方式。下表描述了四种自动布线模式: | Mode |解释| |-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `no` |(默认)没有自动接线。 Bean 引用必须由`ref`元素定义。对于较大的部署,不建议更改
缺省设置,因为显式地指定
协作者可获得更大的控制和清晰度。在某种程度上,它
记录了一个系统的结构。| | `byName` |按属性名称自动接线。 Spring 寻找与
属性同名的需要自动连线的 Bean。例如,如果一个 Bean 定义通过名称设置为
AutoWire,并且它包含一个`master`属性(即,它有一个 `setmaster(..)’方法), Spring 查找一个名为`master`的 Bean 定义,并使用
它来设置该属性。| | `byType` |如果在
容器中正好存在一个属性类型 Bean,则让属性自动连线。如果存在多个异常,将抛出一个致命的异常,该异常表示
你可能不会为此使用`byType`自动布线 Bean。如果没有匹配的
bean,则不会发生任何事情(未设置该属性)。| |`constructor`|类似于`byType`,但适用于构造函数参数。如果容器中没有一个 Bean 构造函数参数类型的
,则会产生致命的错误。| 使用`byType`或`constructor`自动布线模式,可以连接数组和类型化集合。在这种情况下,将提供容器中匹配预期类型的所有 AutoWire 候选项,以满足依赖关系。如果期望的键类型是`String`,则可以自动连接强类型`Map`实例。AutoWired`Map`实例的值由所有与预期类型匹配的 Bean 实例组成,而 `map’实例的键包含相应的 Bean 名称。 ##### 自动布线的局限性和缺点 自动布线在项目中一致使用时效果最好。如果通常不使用自动布线,那么只使用一个或两个定义来连接可能会使开发人员感到困惑 Bean。 考虑一下自动布线的局限性和缺点: * `property`和`constructor-arg`设置中的显式依赖总是覆盖自动布线。你不能自动连接简单的属性,例如原语、`strings’和`Classes`(以及此类简单属性的数组)。这种限制是人为设计的。 * 自动布线不像显式布线那样精确。尽管,正如在前面的表中所指出的, Spring 小心地避免在可能产生意外结果的模棱两可的情况下进行猜测。你的 Spring-托管对象之间的关系不再被显式地记录。 * 接线信息可能不能用于可能从 Spring 容器生成文档的工具。 * Bean 容器内的多个定义可以匹配由 setter 方法或构造函数参数指定的类型以进行自动连线。对于数组、集合或“map”实例,这不一定是个问题。然而,对于期望单个值的依赖关系,这种歧义不能任意解决。如果没有唯一的 Bean 定义可用,则抛出一个异常。 在后一种情况下,你有几种选择: * 放弃自动布线,改用显式布线。 * 通过将其`autowire-candidate`属性设置为`false`,避免 Bean 定义的自动布线,如[next section](#beans-factory-autowire-candidate)中所述。 * 通过将其``元素的 `primary’属性设置为`true`,指定一个 Bean 定义作为主要候选者。 * 用基于注释的配置实现更细粒度的控件,如[基于注释的容器配置](#beans-annotation-config)中所述。 ##### 从自动接线中排除 A Bean 在每 Bean 个基础上,可以将 Bean 个从自动布线中排除。在 Spring 的 XML 格式中,将`autowire-candidate`元素的`autowire-candidate`属性设置为`false`。容器使得该特定的 Bean 定义对自动布线基础设施不可用(包括注释样式配置,例如[`@Autowired`](#beans-autowired-annotation))。 | |`autowire-candidate`属性被设计为仅影响基于类型的自动连接。
它不会影响通过名称的显式引用,即使指定的 Bean
未标记为自动连接候选项,也会得到解析。因此,如果名称匹配,则按名称自动布线
仍然会注入 Bean。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 还可以根据模式匹配对 Bean 名称限制自动连接候选项。顶层``元素在其 `default-autowire-candidates’属性中接受一个或多个模式。例如,要将 AutoWire 候选状态限制为名称以`Repository`结尾的任何 Bean,请提供一个值`*Repository`。要提供多个模式,请在逗号分隔的列表中定义它们。对于 Bean 定义的`autowire-candidate`属性,显式的值 `true’或`false`总是优先的。对于这样的 bean,模式匹配规则不适用。 对于那些你永远不希望通过自动布线被注入到其他 bean 中的 bean,这些技术非常有用。这并不意味着被排除的 Bean 本身不能通过使用自动布线来进行配置。相反, Bean 本身并不是自动连接其他 bean 的候选者。 #### 1.4.6.方法注入 在大多数应用程序场景中,容器中的大多数 bean 都是[singletons](#beans-factory-scopes-singleton)。当单例 Bean 需要与另一个单例 Bean 协作时,或者非单例 Bean 需要与另一个非单例 Bean 协作时,通常通过将一个 Bean 定义为另一个的属性来处理依赖关系。当 Bean 生命周期不同时会出现问题。假设单例 Bean a 需要使用非单例(原型) Bean b,也许是在 a 上的每个方法调用上。容器只创建单例 Bean a 一次,因此只获得一次设置属性的机会。容器不能在每次需要 Bean b 的新实例时向 Bean a 提供新实例。 一种解决办法是放弃某种程度的控制权倒置。通过实现`ApplicationContextAware`接口,并通过[making a `getBean("B")` call to the container](#beans-factory-client)在每次 Bean a 需要它时请求(通常是新的) Bean b 实例,可以[make bean A aware of the container](#beans-factory-aware)。下面的示例展示了这种方法: Java ``` // a class that uses a stateful Command-style class to perform some processing package fiona.apple; // Spring-API imports import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; public class CommandManager implements ApplicationContextAware { private ApplicationContext applicationContext; public Object process(Map commandState) { // grab a new instance of the appropriate Command Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } protected Command createCommand() { // notice the Spring API dependency! return this.applicationContext.getBean("command", Command.class); } public void setApplicationContext( ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } } ``` Kotlin ``` // a class that uses a stateful Command-style class to perform some processing package fiona.apple // Spring-API imports import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContextAware class CommandManager : ApplicationContextAware { private lateinit var applicationContext: ApplicationContext fun process(commandState: Map<*, *>): Any { // grab a new instance of the appropriate Command val command = createCommand() // set the state on the (hopefully brand new) Command instance command.state = commandState return command.execute() } // notice the Spring API dependency! protected fun createCommand() = applicationContext.getBean("command", Command::class.java) override fun setApplicationContext(applicationContext: ApplicationContext) { this.applicationContext = applicationContext } } ``` 上述方法是不可取的,因为业务代码知道并耦合到 Spring 框架。方法注入是 Spring IOC 容器的一个比较高级的特性,它允许你干净地处理这个用例。 你可以在[this blog entry](https://spring.io/blog/2004/08/06/method-injection/)中阅读有关方法注入的动机的更多信息。 ##### 查找方法注入 查找方法注入是容器重写容器管理的 bean 上的方法并返回容器中另一个名为 Bean 的查找结果的能力。查找通常涉及原型 Bean,如[上一节](#beans-factory-method-injection)中描述的场景中所述的那样。 Spring 框架通过使用来自 CGlib 库的字节码生成来动态地生成覆盖该方法的子类,从而实现了该方法注入。 | |* For this dynamic subclassing to work, the class that the Spring bean container
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.

*单元-测试具有`abstract`方法的类需要你自己对类
进行子类分类,并提供`abstract`方法的存根实现。

* 组件扫描也需要具体的方法,这需要具体的
类来拾取。

* 另一个关键限制是,查找方法不适用于工厂方法,而且
尤其不适用于配置类中的`@Bean`方法,因为,在这种情况下,
容器不负责创建实例,因此不能动态创建
运行时生成的子类。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在前面的代码片段中的`CommandManager`类的情况下, Spring 容器动态地覆盖`createCommand()`方法的实现。正如重做的示例所示,`CommandManager`类没有任何 Spring 依赖关系: Java ``` package fiona.apple; // no more Spring imports! public abstract class CommandManager { public Object process(Object commandState) { // grab a new instance of the appropriate Command interface Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } // okay... but where is the implementation of this method? protected abstract Command createCommand(); } ``` Kotlin ``` package fiona.apple // no more Spring imports! abstract class CommandManager { fun process(commandState: Any): Any { // grab a new instance of the appropriate Command interface val command = createCommand() // set the state on the (hopefully brand new) Command instance command.state = commandState return command.execute() } // okay... but where is the implementation of this method? protected abstract fun createCommand(): Command } ``` 在包含要注入的方法的客户机类中(在本例中为`CommandManager`),要注入的方法需要以下形式的签名: ``` [abstract] theMethodName(no-arguments); ``` 如果方法是`abstract`,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖在原始类中定义的具体方法。考虑以下示例: ``` ``` Bean 标识为`commandManager`的方法在需要`myCommand` Bean 的新实例时调用它自己的`createCommand()`方法。如果确实需要将`myCommand` Bean 作为原型部署,则必须小心。如果是[singleton](#beans-factory-scopes-singleton),则每次都返回相同的`myCommand` Bean 实例。 或者,在基于注释的组件模型中,可以通过`@Lookup`注释声明查找方法,如下例所示: Java ``` public abstract class CommandManager { public Object process(Object commandState) { Command command = createCommand(); command.setState(commandState); return command.execute(); } @Lookup("myCommand") protected abstract Command createCommand(); } ``` Kotlin ``` abstract class CommandManager { fun process(commandState: Any): Any { val command = createCommand() command.state = commandState return command.execute() } @Lookup("myCommand") protected abstract fun createCommand(): Command } ``` 或者,更常见的是,你可以依赖于目标 Bean 根据查找方法的声明的返回类型进行解析: Java ``` public abstract class CommandManager { public Object process(Object commandState) { Command command = createCommand(); command.setState(commandState); return command.execute(); } @Lookup protected abstract Command createCommand(); } ``` Kotlin ``` abstract class CommandManager { fun process(commandState: Any): Any { val command = createCommand() command.state = commandState return command.execute() } @Lookup protected abstract fun createCommand(): Command } ``` 请注意,你通常应该使用一个具体的存根实现来声明这种带注释的查找方法,以便它们与 Spring 的组件扫描规则兼容,在组件扫描规则中,抽象类在默认情况下会被忽略。此限制不适用于显式注册或显式导入的 Bean 类。 | |访问不同范围的目标 bean 的另一种方法是`ObjectFactory`/`provider’注入点。参见[作为依赖项的作用域 bean](#beans-factory-scopes-other-injection).

你也可能会发现`ServiceLocatorFactoryBean`(在 `org.springframework.beans.factory.config` 包中)是有用的。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 任意方法替换 Bean 与查找方法注入相比,方法注入的一种不那么有用的形式是能够用另一种方法实现来替换托管中的任意方法。你可以安全地跳过本节的其余部分,直到你真正需要此功能为止。 对于基于 XML 的配置元数据,你可以使用`replaced-method`元素来替换已部署的另一个方法实现。考虑下面的类,它有一个名为`computeValue`的方法,我们想要覆盖它: Java ``` public class MyValueCalculator { public String computeValue(String input) { // some real code... } // some other methods... } ``` Kotlin ``` class MyValueCalculator { fun computeValue(input: String): String { // some real code... } // some other methods... } ``` 实现`org.springframework.beans.factory.support.MethodReplacer`接口的类提供了新的方法定义,如下例所示: Java ``` /** * meant to be used to override the existing computeValue(String) * implementation in MyValueCalculator */ public class ReplacementComputeValue implements MethodReplacer { public Object reimplement(Object o, Method m, Object[] args) throws Throwable { // get the input value, work with it, and return a computed result String input = (String) args[0]; ... return ...; } } ``` Kotlin ``` /** * meant to be used to override the existing computeValue(String) * implementation in MyValueCalculator */ class ReplacementComputeValue : MethodReplacer { override fun reimplement(obj: Any, method: Method, args: Array): Any { // get the input value, work with it, and return a computed result val input = args[0] as String; ... return ...; } } ``` Bean 用于部署原始类并指定方法覆盖的定义将类似于以下示例: ``` String ``` 可以在``元素中使用一个或多个``元素来指示要重写的方法的方法签名。只有当方法重载并且类中存在多个变量时,参数的签名才是必需的。为了方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,下面的 all match`java.lang.string’: ``` java.lang.String String Str ``` 因为参数的数量通常足以区分每个可能的选择,所以通过允许你只键入与参数类型匹配的最短字符串,此快捷方式可以节省大量的输入。 ### 1.5. Bean 范围 当你创建 Bean 定义时,你创建了用于创建由该 Bean 定义定义的类的实际实例的配方。 Bean 定义是一个配方的想法很重要,因为它意味着,与类一样,你可以从一个配方创建许多对象实例。 你不仅可以控制要插入到从特定 Bean 定义创建的对象中的各种依赖关系和配置值,还可以控制从特定 Bean 定义创建的对象的范围。这种方法功能强大且灵活,因为你可以选择通过配置创建的对象的范围,而不必在 Java 类级别上烘烤对象的范围。可以将 bean 定义为部署在多个作用域中的一个。 Spring 框架支持六个作用域,其中四个作用域只有在使用 Web 感知`ApplicationContext`时才可用。你还可以创建[a custom scope.](#beans-factory-scopes-custom) 下表描述了受支持的作用域: | Scope |说明| |-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [singleton](#beans-factory-scopes-singleton) |(缺省)将单个 Bean 定义作用于每个 Spring IOC
容器的单个对象实例。| | [prototype](#beans-factory-scopes-prototype) |将单个 Bean 定义作用于任意数量的对象实例。| | [request](#beans-factory-scopes-request) |Bean 将单个定义作用于单个 HTTP 请求的生命周期。即,
每个 HTTP 请求都有自己的实例 Bean,该实例是在单个 Bean
定义的后面创建的。 Spring `ApplicationContext`仅在可感知 Web 的上下文中有效。| | [session](#beans-factory-scopes-session) |Bean 将单个定义作用于 HTTP`Session`的生命周期。 Spring
仅在网络感知的上下文中有效。| | [application](#beans-factory-scopes-application) |将一个 Bean 定义的范围应用于`ServletContext`的生命周期。 Spring `ApplicationContext`仅在网络感知的上下文`ApplicationContext`中有效。| |[websocket](web.html#websocket-stomp-websocket-scope)|将一个 Bean 定义作用于`WebSocket`的生命周期。 Spring `ApplicationContext`仅在网络感知的上下文中有效。| | |从 Spring 3.0 开始,线程作用域是可用的,但默认情况下不会注册。有关
的更多信息,请参见[“SimpleThreadscope”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/SimpleThreadScope.html)的文档。
有关如何注册此范围或任何其他自定义范围的说明,请参见[使用自定义作用域](#beans-factory-scopes-custom-using)。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.5.1.单例范围 只管理单例 Bean 的一个共享实例,对具有与 Bean 定义相匹配的一个或多个 ID 的 bean 的所有请求导致由 Spring 容器返回一个特定的 Bean 实例。 换句话说,当你定义 Bean 定义并将其作为单例作用域时, Spring IoC 容器将创建由该 Bean 定义定义的对象的一个实例。这个单个实例存储在这样的单例 bean 的缓存中,并且所有后续的请求和对命名为 Bean 的引用都返回缓存的对象。下图显示了单例范围的工作原理: ![singleton](images/singleton.png) Spring 的单例模式的概念 Bean 与四人帮模式书中定义的单例模式不同。GOF 单例硬编码对象的作用域,使得每个类装入器只创建一个特定类的实例。 Spring 单例的范围最好描述为每个容器和每个- Bean。这意味着,如果在单个 Spring 容器中为特定类定义一个 Bean,则 Spring 容器创建由该 Bean 定义的类的一个且只有一个实例。Singleton 作用域是 Spring 中的默认作用域。要将 Bean 定义为 XML 中的单例,可以定义 Bean,如以下示例所示: ``` ``` #### 1.5.2.原型范围 Bean 部署的非单例原型作用域在每次提出对该特定 Bean 的请求时都会创建一个新的 Bean 实例。即,将 Bean 注入到另一个 Bean 中,或者通过容器上的`getBean()`方法调用来请求它。通常,你应该为所有有状态 bean 使用原型作用域,为无状态 bean 使用单例作用域。 下图说明了 Spring 原型范围: ![prototype](images/prototype.png) (数据访问对象(DAO)通常不被配置为原型,因为典型的 DAO 不持有任何会话状态。对我们来说,重用单例图的核心更容易。 下面的示例将 Bean 定义为 XML 的原型: ``` ``` Spring 与其他范围相反,不管理原型的完整生命周期 Bean。容器实例化、配置和以其他方式组装一个原型对象,并将其交给客户机,而不需要进一步记录该原型实例。因此,尽管在所有对象上调用初始化生命周期回调方法,而不考虑作用域,但在原型的情况下,不调用已配置的销毁生命周期回调。客户机代码必须清理原型范围内的对象,并释放原型 bean 所拥有的昂贵资源。要获得 Spring 容器以释放由原型范围的 bean 持有的资源,请尝试使用自定义的[bean post-processor](#beans-factory-extension-bpp),它保存了对需要清理的 bean 的引用。 在某些方面, Spring 容器对于原型范围 Bean 的作用是 Java`new`操作符的替代。超过这一点的所有生命周期管理都必须由客户机处理。(有关 Spring 容器中 Bean 的生命周期的详细信息,请参见[生命周期回调](#beans-factory-lifecycle)。 #### 1.5.3.具有原型依赖关系的单例 bean- Bean 当你使用依赖于原型 bean 的单例作用域 bean 时,请注意,依赖关系在实例化时已得到解决。因此,如果你将依赖-注入原型范围的 Bean 到单例范围的 Bean 中,那么将实例化一个新的原型 Bean,然后将依赖-注入到单例范围 Bean 中。 Bean 原型实例是提供给单例作用域的唯一实例。 然而,假设你希望单例作用域 Bean 在运行时反复获得原型作用域 Bean 的一个新实例。你不能将一个原型范围的 Bean 注入到你的单例 Bean 中,因为该注入仅在 Spring 容器实例化单例 Bean 并解析和注入其依赖项时发生一次。如果在运行时不止一次需要原型 Bean 的新实例,请参见[方法注入](#beans-factory-method-injection)。 #### 1.5.4.请求、会话、应用和 WebSocket 范围 `request`、`session`、`application`和`websocket`作用域只有在使用 Web-aware Spring `ApplicationContext`实现(例如 `xmlWebApplicationContext`)时才可用。如果你在常规 Spring IOC 容器中使用这些作用域,例如`ClassPathXmlApplicationContext`,则抛出一个抱怨未知 Bean 作用域的`IllegalStateException`。 ##### 初始 Web 配置 为了支持在`request`、`session`、`application`和 ` WebSocket ` 级别(Web 范围的 bean)上对 bean 进行范围界定,在定义 bean 之前,需要进行一些较小的初始配置。(对于标准作用域:`singleton`和`prototype`,不需要这种初始设置。) 如何完成这个初始设置取决于你的特定环境 Servlet。 如果你在 Spring Web MVC 中访问范围 bean,实际上是在 Spring `DispatcherServlet`处理的请求中访问,则无需进行特殊的设置。 如果使用 Servlet 2.5Web 容器,在 Spring 的 `DispatcherServlet’之外处理请求(例如,当使用 JSF 或 Struts 时),则需要注册 `org.springframework.web.context.request.requestContextListener`。对于 Servlet 3.0+,这可以通过使用`WebApplicationInitializer`接口以编程方式完成。或者,对于较旧的容器,可以将以下声明添加到 Web 应用程序的`web.xml`文件中: ``` ... org.springframework.web.context.request.RequestContextListener ... ``` 或者,如果你的侦听器设置有问题,请考虑使用 Spring 的“RequestContextFilter”。筛选器映射依赖于周围的 Web 应用程序配置,因此你必须对其进行适当的更改。下面的清单显示了 Web 应用程序的过滤器部分: ``` ... requestContextFilter org.springframework.web.filter.RequestContextFilter requestContextFilter /* ... ``` `DispatcherServlet`,`RequestContextListener`,和`RequestContextFilter`都做完全相同的事情,即将 HTTP 请求对象绑定到服务该请求的`Thread`。这使得请求和会话范围的 bean 可以在调用链的更下游使用。 ##### 请求范围 考虑 Bean 定义的以下 XML 配置: ``` ``` Spring 容器通过对每个 HTTP 请求使用 `LoginAction’ Bean 定义来创建`LoginAction` Bean 的新实例。也就是说,“LoginAction” Bean 的作用域在 HTTP 请求级别。你可以随意更改所创建实例的内部状态,因为从相同的`loginAction` Bean 定义创建的其他实例在状态上看不到这些更改。它们是针对个人要求的。当请求完成处理时, Bean 范围内的请求被丢弃。 当使用注释驱动的组件或 Java 配置时,`@RequestScope`注释可用于将组件分配给`request`范围。下面的示例展示了如何做到这一点: Java ``` @RequestScope @Component public class LoginAction { // ... } ``` Kotlin ``` @RequestScope @Component class LoginAction { // ... } ``` ##### 会话范围 考虑 Bean 定义的以下 XML 配置: ``` ``` Spring 容器通过对单个 HTTP的生命期使用 `userPreferences’ Bean 定义来创建 Bean 的新实例。换句话说,`userPreferences` Bean 在 http`Session`级别有效地起作用。与请求范围的 bean 一样,你可以更改所创建的实例的内部状态,只要你愿意,就可以知道其他 HTTP实例也在使用从相同的 Bean 定义创建的实例,它们在状态上看不到这些更改,因为它们是特定于单个 http`Session`的。当 HTTP`Session`最终被丢弃时,作用域为该特定 HTTP`session’的 Bean 也将被丢弃。 当使用注释驱动的组件或 Java 配置时,你可以使用“@SessionScope”注释将组件分配给`session`范围。 Java ``` @SessionScope @Component public class UserPreferences { // ... } ``` Kotlin ``` @SessionScope @Component class UserPreferences { // ... } ``` ##### 应用范围 考虑 Bean 定义的以下 XML 配置: ``` ``` Spring 容器通过对整个 Web 应用程序使用一次“AppPreferences” Bean 定义来创建`AppPreferences` Bean 的新实例。也就是说,“AppPreferences” Bean 的作用域在`ServletContext`级别,并存储为常规的“servletContext”属性。这在某种程度上类似于 Spring 单例 Bean,但在两个重要方面有所不同:它是每个`ServletContext`的单例,而不是每个 Spring `ApplicationContext’(在任何给定的 Web 应用程序中可能有几个),并且它实际上是公开的,因此可以作为`ServletContext`属性显示。 当使用注释驱动的组件或 Java 配置时,你可以使用“@ApplicationScope”注释将组件分配给`application`范围。下面的示例展示了如何做到这一点: Java ``` @ApplicationScope @Component public class AppPreferences { // ... } ``` Kotlin ``` @ApplicationScope @Component class AppPreferences { // ... } ``` ##### WebSocket 范围 WebSocket 范围与 WebSocket 会话的生命周期相关联并应用于 WebSocket 应用程序上的 stomp,有关更多详细信息,请参见[WebSocket scope](web.html#websocket-stomp-websocket-scope)。 ##### 作为依赖项的作用域 bean Spring IoC 容器不仅管理对象的实例化,还管理协作者(或依赖关系)的连接。如果希望将(例如)HTTP 请求作用域 Bean 注入到另一个作用域更长的 Bean 中,则可以选择注入 AOP 代理来代替作用域 Bean。也就是说,你需要注入一个代理对象,该代理对象公开了与作用域对象相同的公共接口,但它也可以从相关作用域(例如 HTTP 请求)检索真实目标对象,并将委托方法调用到真实对象上。 | |你还可以在作用域为``、
的 bean 之间使用``,然后与引用一起通过一个中间代理,该代理是可序列化的
,因此能够在反序列化时重新获得目标单例 Bean。

当声明``的作用域< Bean 时,在共享代理上调用的每个方法
都会导致创建一个新的目标实例,然后将
调用转发到该实例。

此外,有作用域的代理并不是以
生命周期安全方式从较短的作用域中访问 bean 的唯一方法。你也可以声明你的注射点(即,将
构造函数或 setter 参数或 autowired 字段)设为`ObjectFactory`,
允许一个`getObject()`调用,以便在需要的每一次
时间按需检索当前实例——而不保留实例或单独存储实例。

作为扩展变体,你可以声明`ObjectProvider`,它提供了
几个额外的访问变量,包括`getIfAvailable`和`getIfUnique`。

这一类型的 JSR-330 变体被称为`Provider`,并与`Provider`声明和相应的`get()`每次检索尝试都调用。
有关 JSR-330 的更多详细信息,请参见[here](#beans-standard-annotations)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面示例中的配置只有一行,但理解其背后的“为什么”和“如何”非常重要: ``` (1) ``` |**1**|定义代理的那条线。| |-----|--------------------------------| 要创建这样的代理,你需要在 Bean 定义的范围内插入一个子元素``(参见[选择要创建的代理的类型](#beans-factory-scopes-other-injection-proxies)和[基于 XML 模式的配置](#xsd-schemas))。为什么在`request`、`session`和自定义范围级别范围内的 bean 定义需要``元素?考虑以下单例 Bean 定义,并将其与你需要为上述范围定义的内容进行对比(请注意,以下“用户偏好” Bean 定义目前是不完整的): ``` ``` 在前面的示例中,单例 Bean(“usermanager”)被注入了对 HTTP`Session`-作用域 Bean(“userpreferences”)的引用。这里的要点是,“usermanager” Bean 是一个单例:每个容器只实例化一次,它的依赖项(在本例中只有一个,`userPreferences` Bean)也只注入一次。这意味着`userManager` Bean 仅对完全相同的`userPreferences`对象(即最初注入它的对象)进行操作。 这不是你在将较短的作用域 Bean 注入较长的作用域 Bean 时想要的行为(例如,将一个 HTTP作用域协作 Bean 作为一个依赖项注入到单例 Bean)。相反,你需要一个`userManager`对象,并且,对于一个 http`Session`的生命周期,你需要一个`userPreferences`对象,该对象是特定于 http`Session`的。因此,容器创建了一个对象,该对象公开了与`UserPreferences`类完全相同的公共接口(理想情况下是一个`UserPreferences`实例的对象),它可以从范围机制(HTTP Request,`Session`,等等)中获取真正的 `userPreferences’对象。容器将该代理对象注入到`userManager` Bean 中,该对象不知道该`UserPreferences`引用是代理。在本例中,当“UserManager”实例调用依赖注入的`UserPreferences`对象上的方法时,它实际上是在调用代理上的方法。然后,代理从(在本例中)http`Session`中获取真正的 `userpreferences’对象,并将方法调用委托给检索到的真正的`UserPreferences`对象。 因此,在将 `request-` 和`session-scoped`bean 注入协作对象时,你需要以下(正确且完整的)配置,如下例所示: ``` ``` ###### 选择要创建的代理的类型 默认情况下,当 Spring 容器为用``元素标记的 Bean 创建代理时,将创建一个基于 CGLIB 的类代理。 | |CGLIB 代理仅拦截公共方法调用!不要在这样的代理上调用非公共方法
。它们不会被委托给实际作用域的目标对象。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| 或者,你可以通过为`proxy-target-class`元素的`false`属性的值指定`false`,来配置 Spring 容器来为此类作用域 bean 创建标准的基于 JDK 接口的代理。 Classpath 使用基于 JDK 接口的代理意味着你不需要应用程序中的额外库来影响这样的代理。然而,这也意味着范围 Bean 的类必须实现至少一个接口,并且将范围 Bean 注入其中的所有协作者必须通过其接口之一引用 Bean。下面的示例展示了一个基于接口的代理: ``` ``` 有关选择基于类或基于接口的代理的更详细信息,请参见[代理机制](#aop-proxying)。 #### 1.5.5.自定义范围 Bean 范围界定机制是可扩展的。你可以定义自己的作用域,甚至重新定义现有的作用域,尽管后者被认为是错误的实践,并且你无法覆盖内置的`singleton`和`prototype`作用域。 ##### 创建自定义范围 要将你的自定义作用域集成到 Spring 容器中,你需要实现 `org.springframework.beans.factory.config.scope` 接口,这将在本节中描述。有关如何实现自己的作用域的想法,请参见 Spring 框架本身和[`Scope`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/Scope.html)Javadoc 提供的`Scope`实现,它更详细地解释了你需要实现的方法。 `Scope`接口有四个方法来从作用域中获取对象,将它们从作用域中删除,然后销毁它们。 例如,会话作用域实现返回会话作用域 Bean(如果不存在,则该方法在将其绑定到会话以供将来引用之后,返回 Bean 的新实例)。以下方法从底层作用域返回对象: Java ``` Object get(String name, ObjectFactory objectFactory) ``` Kotlin ``` fun get(name: String, objectFactory: ObjectFactory<*>): Any ``` 例如,会话范围实现从基础会话中删除会话范围 Bean。应该返回该对象,但是如果没有找到指定名称的对象,则可以返回`null`。以下方法将该对象从基础作用域中删除: Java ``` Object remove(String name) ``` Kotlin ``` fun remove(name: String): Any ``` 以下方法注册了一个回调,当范围被销毁或范围中的指定对象被销毁时,该范围应调用该回调: Java ``` void registerDestructionCallback(String name, Runnable destructionCallback) ``` Kotlin ``` fun registerDestructionCallback(name: String, destructionCallback: Runnable) ``` 有关销毁回调的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/Scope.html#registerDestructionCallback)或 Spring 范围实现。 以下方法获得底层作用域的会话标识符: Java ``` String getConversationId() ``` Kotlin ``` fun getConversationId(): String ``` 这个标识符对于每个作用域都是不同的。对于会话范围的实现,这个标识符可以是会话标识符。 ##### 使用自定义作用域 在编写和测试一个或多个自定义`Scope`实现之后,你需要让 Spring 容器知道你的新作用域。下面的方法是在 Spring 容器中注册一个新的`Scope`的中心方法: 爪哇 ``` void registerScope(String scopeName, Scope scope); ``` Kotlin ``` fun registerScope(scopeName: String, scope: Scope) ``` 此方法在`ConfigurableBeanFactory`接口上声明,该接口可通过`BeanFactory`属性在 Spring 附带的大多数具体`ApplicationContext`实现上使用。 `registerScope(..)`方法的第一个参数是与作用域关联的唯一名称。 Spring 容器本身中的此类名称的示例是`singleton`和 `prototype’。`registerScope(..)`方法的第二个参数是你希望注册和使用的自定义`Scope`实现的实际实例。 假设你编写了你的自定义`Scope`实现,然后将其注册为下一个示例所示。 | |下一个示例使用`SimpleThreadScope`,它包含在 Spring 中,但不是默认注册的
。对于你自己的自定义`Scope`实现,指令将是相同的。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 爪哇 ``` Scope threadScope = new SimpleThreadScope(); beanFactory.registerScope("thread", threadScope); ``` Kotlin ``` val threadScope = SimpleThreadScope() beanFactory.registerScope("thread", threadScope) ``` 然后,你可以创建遵循自定义“范围”的范围规则的 Bean 定义,如下所示: ``` ``` 使用自定义的`Scope`实现,你不仅限于编程注册的范围。你还可以使用“CustomScopeConfigurer”类,以声明式的方式进行`Scope`注册,如下例所示: ``` ``` | |当将``放置在 `FactoryBean’实现的``声明中时,作用域是工厂 Bean 本身,而不是从`getObject()`返回的对象
。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.6.自定义 A 的性质 Bean Spring 框架提供了许多接口,你可以使用这些接口来自定义 Bean 的性质。本节将其分类如下: * [生命周期回调](#beans-factory-lifecycle) * [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) * [Other `Aware` Interfaces](#aware-list) #### 1.6.1.生命周期回调 要与容器的 Bean 生命周期管理交互,你可以实现 Spring `InitializingBean`和`DisposableBean`接口。容器为前者调用 `AfterPropertiesset()`,为后者调用`destroy()`,让 Bean 在初始化和销毁 bean 时执行某些操作。 | |JSR-250`@PostConstruct`和`@PreDestroy`注释通常被认为是在现代 Spring 应用程序中接收生命周期回调的最佳
实践。使用这些
注释意味着你的 bean 没有耦合到 Spring 特定的接口。
有关详细信息,请参见[Using `@PostConstruct` and `@PreDestroy`](#beans-postconstruct-and-predestroy-annotations)。

如果你不想使用 JSR-250 注释,但仍然希望删除
耦合,请考虑`init-method`和 ``` 爪哇 ``` public class ExampleBean { public void init() { // do some initialization work } } ``` Kotlin ``` class ExampleBean { fun init() { // do some initialization work } } ``` 前面的示例与下面的示例(由两个列表组成)的效果几乎完全相同: ``` ``` 爪哇 ``` public class AnotherExampleBean implements InitializingBean { @Override public void afterPropertiesSet() { // do some initialization work } } ``` Kotlin ``` class AnotherExampleBean : InitializingBean { override fun afterPropertiesSet() { // do some initialization work } } ``` 然而,前面的两个示例中的第一个不将代码与 Spring 耦合。 ##### 销毁回调 实现`org.springframework.beans.factory.DisposableBean`接口可以让 Bean 在包含它的容器被销毁时获得回调。“DisposableBean”接口指定了一个方法: ``` void destroy() throws Exception; ``` 我们建议你不要使用`DisposableBean`回调接口,因为它不必要地将代码耦合到 Spring。或者,我们建议使用[`@PreDestroy`](#beans-postconstruct-and-predestroy-annotations)注释或指定 Bean 定义支持的通用方法。对于基于 XML 的配置元数据,你可以在``上使用`destroy-method`属性。使用 爪哇 配置,可以使用`@Bean`的`destroyMethod`属性。见[接收生命周期回调](#beans-java-lifecycle-callbacks)。考虑以下定义: ``` ``` 爪哇 ``` public class ExampleBean { public void cleanup() { // do some destruction work (like releasing pooled connections) } } ``` Kotlin ``` class ExampleBean { fun cleanup() { // do some destruction work (like releasing pooled connections) } } ``` 上述定义与以下定义的效果几乎完全相同: ``` ``` 爪哇 ``` public class AnotherExampleBean implements DisposableBean { @Override public void destroy() { // do some destruction work (like releasing pooled connections) } } ``` Kotlin ``` class AnotherExampleBean : DisposableBean { override fun destroy() { // do some destruction work (like releasing pooled connections) } } ``` 然而,前面的两个定义中的第一个并不将代码耦合到 Spring。 | |你可以为``元素的`destroy-method`属性分配一个特殊的 `(推断)’值,该值指示 Spring 在特定的 Bean 类上自动检测一个公共的`close`或 `shutdown’方法。(因此,任何实现 `java.lang.autocloseable’或`java.io.Closeable`的类都将匹配。)你还可以在
元素的`default-destroy-method`属性上设置这个特殊的`(inferred)`值,以将此行为应用到整个 bean 集(参见[默认的初始化和销毁方法](#beans-factory-lifecycle-default-init-destroy-methods))。请注意,这是使用 爪哇 配置的
默认行为。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 默认的初始化和销毁方法 当你编写初始化并销毁不使用 Spring 特定的`InitializingBean`和`DisposableBean`回调接口的方法回调时,通常使用`init()`、`initialize()`、`dispose()`等名称编写方法。理想情况下,这样的生命周期回调方法的名称在整个项目中是标准化的,以便所有开发人员使用相同的方法名称并确保一致性。 你可以将 Spring 容器配置为“查找”命名的初始化,并在每个 Bean 上销毁回调方法名称。这意味着,作为应用程序开发人员,你可以编写应用程序类并使用名为 `init()’的初始化回调,而无需为每个 Bean 定义配置`init-method="init"`属性。 Spring IOC 容器在创建 Bean 时调用该方法(并且根据标准的生命周期回调契约[先前描述过](#beans-factory-lifecycle))。此特性还强制执行一个用于初始化和销毁方法回调的一致的命名约定。 假设你的初始化回调方法名为`init()`,而销毁回调方法名为`destroy()`。在下面的示例中,你的类与类类似: 爪哇 ``` public class DefaultBlogService implements BlogService { private BlogDao blogDao; public void setBlogDao(BlogDao blogDao) { this.blogDao = blogDao; } // this is (unsurprisingly) the initialization callback method public void init() { if (this.blogDao == null) { throw new IllegalStateException("The [blogDao] property must be set."); } } } ``` Kotlin ``` class DefaultBlogService : BlogService { private var blogDao: BlogDao? = null // this is (unsurprisingly) the initialization callback method fun init() { if (blogDao == null) { throw IllegalStateException("The [blogDao] property must be set.") } } } ``` 然后,你可以在类似于以下内容的 Bean 中使用该类: ``` ``` 顶层`default-init-method`元素属性上的`default-init-method`属性的存在导致 Spring IoC 容器将 Bean 类上的一个名为`init`的方法识别为初始化方法回调。当 Bean 被创建和组装时,如果 Bean 类具有这样的方法,则在适当的时间调用它。 通过使用顶层``元素上的 `default-destroy-method’属性,可以类似地(在 XML 中)配置 destroy 方法回调。 如果现有的 Bean 类已经具有根据约定命名的回调方法,则可以通过使用`init-method`本身的``属性指定(在 XML 中,即)方法名来覆盖缺省的方法。 Spring 容器保证在 Bean 提供了所有依赖项之后立即调用已配置的初始化回调。因此,在 Raw Bean 引用上调用初始化回调,这意味着 AOP 拦截器等等尚未应用到 Bean。首先完全创建目标 Bean,然后应用具有拦截链的 AOP 代理(例如)。如果目标 Bean 和代理是分开定义的,那么你的代码甚至可以绕过代理与原始目标 Bean 进行交互。因此,将拦截器应用于`init`方法将是不一致的,因为这样做将把目标 Bean 的生命周期与其代理或拦截器耦合在一起,并在你的代码直接与原始目标 Bean 交互时留下奇怪的语义。 ##### 结合生命周期机制 从 Spring 2.5 开始,你有三种选择来控制 Bean 生命周期行为: * [“初始化 bean”](#beans-factory-lifecycle-initializingbean)和[`DisposableBean’](#beans-factory-lifecycle-disposablebean)回调接口 * 自定义`init()`和`destroy()`方法 * [`@PostConstruct` and `@PreDestroy`annotations](#beans-postconstruct-and-predestroy-annotations)。你可以结合这些机制来控制给定的 Bean。 | |如果为 Bean 配置了多个生命周期机制,并且每个机制
配置了不同的方法名,那么每个配置的方法都是在
顺序中运行的,顺序在此注释之后列出。但是,如果为这些生命周期机制中的一个以上配置了相同的方法名(例如,初始化方法的’init()’),则
该方法将运行一次,如[前一节](#beans-factory-lifecycle-default-init-destroy-methods)中所解释的那样。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Bean 使用不同的初始化方法为相同的 Bean 配置的多个生命周期机制调用如下: 1. 用`@PostConstruct`注释的方法 2. `afterPropertiesSet()`由`InitializingBean`回调接口定义 3. 自定义配置的`init()`方法 销毁方法的调用顺序相同: 1. 用`@PreDestroy`注释的方法 2. `destroy()`由`DisposableBean`回调接口定义 3. 自定义配置的`destroy()`方法 ##### 启动和关闭回调 `Lifecycle`接口定义了任何对象的基本方法,这些对象具有自己的生命周期要求(例如启动和停止某些后台进程): ``` public interface Lifecycle { void start(); void stop(); boolean isRunning(); } ``` 任何 Spring-托管对象都可以实现`Lifecycle`接口。然后,当“ApplicationContext”本身接收开始和停止信号(例如,对于运行时的停止/重新启动场景)时,它将这些调用级联到在该上下文中定义的所有`Lifecycle`实现。它通过委托给`LifecycleProcessor`来实现这一点,如下面的清单所示: ``` public interface LifecycleProcessor extends Lifecycle { void onRefresh(); void onClose(); } ``` 注意,`LifecycleProcessor`本身是`Lifecycle`接口的扩展。它还添加了另外两个方法,用于对正在刷新和关闭的上下文做出反应。 | |请注意,常规的`org.springframework.context.Lifecycle`接口是用于显式启动和停止通知的普通
契约,并不意味着在上下文
刷新时间自动启动。对于特定 Bean 的自动启动(包括启动阶段)的细粒度控制,
可以考虑实现`org.springframework.context.SmartLifecycle`,而不是,

此外,请注意,停止通知不能保证在销毁之前发出。
在常规关闭时,所有`Lifecycle`bean 在
一般销毁回调被传播之前首先收到停止通知。但是,在
上下文的生命期内进行热刷新或停止刷新尝试时,只调用销毁方法。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 启动和关闭调用的顺序可能很重要。如果在任何两个对象之间存在“依赖”关系,则依赖方在其依赖项之后开始,在其依赖项之前停止。然而,有时,直接的依赖关系是未知的。你可能只知道,某种类型的对象应该先于另一种类型的对象启动。在这些情况下,`SmartLifecycle`接口定义了另一个选项,即在其超级接口上定义的`getPhase()`方法,即 ` 分阶段’。下面的清单显示了`Phased`接口的定义: ``` public interface Phased { int getPhase(); } ``` 下面的清单显示了`SmartLifecycle`接口的定义: ``` public interface SmartLifecycle extends Lifecycle, Phased { boolean isAutoStartup(); void stop(Runnable callback); } ``` 启动时,相位最低的对象首先启动。停止时,将遵循相反的顺序。因此,一个实现`SmartLifecycle`并且其`getPhase()`方法返回`Integer.MIN_VALUE`的对象将是最先启动和最后停止的对象。在频谱的另一端,“integer.max_value”的相位值将表示对象应该最后启动并首先停止(可能是因为它依赖于要运行的其他进程)。当考虑相位值时,同样重要的是要知道,对于任何不实现`SmartLifecycle`的“正常”“生命周期”对象,缺省阶段是`0`。因此,任何负相位值都表示对象应该在这些标准组件之前启动(在它们之后停止)。对于任何正相位值,反之亦然。 由`SmartLifecycle`定义的停止方法接受回调。在该实现的关闭过程完成之后,任何实现都必须调用该回调的`run()`方法。这将在必要时支持异步关闭,因为`LifecycleProcessor`接口的默认实现 `DefaultLifecycleProcessor’在每个阶段中等待对象组的超时值来调用该回调。默认的每阶段超时是 30 秒。你可以通过在上下文中定义一个名为“LifecycleProcessor”的 Bean 来覆盖默认的生命周期处理器实例。如果你只想修改超时,定义以下内容就足够了: ``` ``` 如前所述,`LifecycleProcessor`接口还定义了用于刷新和关闭上下文的回调方法。后者驱动关机过程,就好像`stop()`已被显式调用一样,但它发生在上下文关闭时。另一方面,“刷新”回调启用了“smartlifecycle”bean 的另一项功能。当刷新上下文时(在所有对象都已实例化和初始化之后),将调用该回调。这时,默认的生命周期处理器会检查每个“SmartLifecycle”对象的`isAutoStartup()`方法返回的布尔值。如果`true`,则该对象在该点启动,而不是等待对上下文或其自身的`start()`方法的显式调用(与上下文刷新不同,对于标准的上下文实现,上下文启动不会自动发生)。正如前面所描述的,`phase`值和任何“依赖”关系决定启动顺序。 ##### 在非 Web 应用程序中优雅地关闭 Spring IoC 容器 | |本节仅适用于非 Web 应用程序。 Spring 的基于 Web 的“ApplicationContext”实现已经有了适当的代码,可以在相关 Web 应用程序关闭时优雅地关闭
Spring IoC 容器。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果在非 Web 应用程序环境(例如,在富客户端桌面环境中)中使用 Spring 的 IoC 容器,请向 JVM 注册一个关闭钩子。这样做可以确保良好的关机,并在单例 bean 上调用相关的销毁方法,以便释放所有资源。你仍然必须正确地配置和实现这些 destroy 回调。 要注册关机钩子,请调用在`ConfigurableApplicationContext`接口上声明的`registerShutdownHook()`方法,如下例所示: 爪哇 ``` import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public final class Boot { public static void main(final String[] args) throws Exception { ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); // add a shutdown hook for the above context... ctx.registerShutdownHook(); // app runs here... // main method exits, hook is called prior to the app shutting down... } } ``` Kotlin ``` import org.springframework.context.support.ClassPathXmlApplicationContext fun main() { val ctx = ClassPathXmlApplicationContext("beans.xml") // add a shutdown hook for the above context... ctx.registerShutdownHook() // app runs here... // main method exits, hook is called prior to the app shutting down... } ``` #### 1.6.2.`ApplicationContextAware`和`BeanNameAware` 当`ApplicationContext`创建一个实现 `org.springframework.context.ApplicationContextAware` 接口的对象实例时,将为该实例提供一个对`ApplicationContext`的引用。下面的清单显示了`ApplicationContextAware`接口的定义: ``` public interface ApplicationContextAware { void setApplicationContext(ApplicationContext applicationContext) throws BeansException; } ``` 因此,bean 可以通过`ApplicationContext`接口或通过对该接口的一个已知子类(例如`ConfigurableApplicationContext`,该接口公开了额外的功能)的引用,以编程方式操作创建它们的`ApplicationContext`。一种用途是对其他 bean 进行程序化检索。有时这种能力是有用的。但是,通常情况下,你应该避免它,因为它将代码与 Spring 耦合,并且不遵循控制样式的反转,在这种情况下,协作者被提供给 bean 作为属性。“ApplicationContext”的其他方法提供对文件资源的访问、发布应用程序事件以及访问`MessageSource`。这些附加特征在[Additional Capabilities of the `ApplicationContext`](#context-introduction)中进行了描述。 自动布线是获得对“ApplicationContext”的引用的另一种选择。*传统的*`constructor`和`byType`自动布线模式(如[自动布线合作者](#beans-factory-autowire)中所述)可以分别为构造函数参数或 setter 方法参数提供类型 `ApplicationContext’的依赖项。为了具有更大的灵活性,包括能够自动连接字段和多个参数的方法,可以使用基于注释的自动连接功能。如果你这样做,`ApplicationContext`将自动连接到一个字段、构造函数参数或方法参数中,如果所讨论的字段、构造函数或方法带有`ApplicationContext`注释,则该参数将需要`@Autowired`类型。有关更多信息,请参见[Using `@Autowired`](#beans-autowired-annotation)。 当`ApplicationContext`创建一个实现 `org.springframework.beans.factory.beannamaware` 接口的类时,将为该类提供一个对其关联对象定义中定义的名称的引用。下面的清单显示了 BeannaMeAware 接口的定义: ``` public interface BeanNameAware { void setBeanName(String name) throws BeansException; } ``` 回调是在正常 Bean 属性的填充之后,但在初始化回调(如`InitializingBean.afterPropertiesSet()`或自定义 init-method)之前调用的。 #### 1.6.3.其它`Aware`接口 除了`ApplicationContextAware`和`BeanNameAware`(讨论了[earlier](#beans-factory-aware))之外, Spring 还提供了一系列`Aware`回调接口,这些接口使 bean 向容器表明它们需要某种基础设施依赖关系。作为一般规则,名称指示依赖类型。下表总结了最重要的`Aware`接口: | Name |注入依赖性| Explained in…​ | |--------------------------------|-----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| | `ApplicationContextAware` |声明`ApplicationContext`。| [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) | |`ApplicationEventPublisherAware`|附件`ApplicationContext`的事件发布者。|[Additional Capabilities of the `ApplicationContext`](#context-introduction)| | `BeanClassLoaderAware` |类装入器用于装入 Bean 类。| [Instantiating Beans](#beans-factory-class) | | `BeanFactoryAware` |声明`BeanFactory`。| [The `BeanFactory`](#beans-beanfactory) | | `BeanNameAware` |声明的名称 Bean。| [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) | | `LoadTimeWeaverAware` |定义了用于在加载时处理类定义的 Weaver。| [Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw) | | `MessageSourceAware` |用于解析消息的已配置策略(支持参数化和
国际化)。|[Additional Capabilities of the `ApplicationContext`](#context-introduction)| | `NotificationPublisherAware` |Spring JMX 通知发布者。| [Notifications](integration.html#jmx-notifications) | | `ResourceLoaderAware` |为低级访问资源配置了加载器。| [Resources](#resources) | | `ServletConfigAware` |current`ServletConfig`容器运行。只有在网络感知的“应用上下文”中才有效 Spring。| [Spring MVC](web.html#mvc) | | `ServletContextAware` |current`ServletContext`容器运行。仅在网络感知的“应用上下文”中有效 Spring。| [Spring MVC](web.html#mvc) | 再次注意,使用这些接口将你的代码绑定到 Spring API,并且不遵循控制样式的反转。因此,对于需要对容器进行编程访问的基础设施 bean,我们推荐使用它们。 ### 1.7. Bean 定义继承 Bean 定义可以包含大量的配置信息,包括构造函数参数、属性值和特定于容器的信息,例如初始化方法、静态工厂方法名称等。 Bean 子定义从父定义继承配置数据。子定义可以根据需要覆盖某些值或添加其他值。 Bean 使用父类和子类定义可以节省大量的输入。实际上,这是模板化的一种形式。 如果以编程方式处理`ApplicationContext`接口,则子 Bean 定义由`ChildBeanDefinition`类表示。大多数用户不会在这个级别上与他们合作。相反,它们以声明性的方式在一个类中配置 Bean 定义,例如`ClassPathXmlApplicationContext`。当你使用基于 XML 的配置元数据时,你可以通过使用`parent`属性来指示一个子 Bean 定义,并指定父 Bean 作为该属性的值。下面的示例展示了如何做到这一点: ``` (1) ``` |**1**|注意`parent`属性。| |-----|----------------------------| 子 Bean 定义使用来自父定义的 Bean 类,如果没有指定,但也可以覆盖它。在后一种情况下,子 Bean 类必须与父类兼容(也就是说,它必须接受父的属性值)。 Bean 子定义从父定义继承范围、构造函数参数值、属性值和方法重写,并带有添加新值的选项。指定覆盖相应父设置的任何作用域、初始化方法、销毁方法或`static`工厂方法设置。 其余的设置总是从子定义中获取:Dependent on、AutoWire 模式、依赖检查、单例和惰性 init。 前面的示例通过使用`abstract`属性显式地将父 Bean 定义标记为抽象。如果父定义没有指定类,则需要显式地将父 Bean 定义标记为`abstract`,如下例所示: ``` ``` 父 Bean 不能单独实例化,因为它是不完整的,并且它也显式地标记为`abstract`。当一个定义是`abstract`时,它只能作为一个纯粹的模板 Bean 定义使用,作为子定义的父定义。试图单独使用这样的`abstract`父 Bean,方法是将其称为另一个 Bean 的 ref 属性,或者使用父 Bean ID 执行显式的`getBean()`调用,返回一个错误。类似地,容器的内部“preinstantiatesingletons()”方法忽略了定义为抽象的 Bean 定义。 | |`ApplicationContext`默认情况下预先实例化所有单例。因此,如果你有一个(父) Bean 定义
,并且只打算将其用作模板,并且该定义指定了一个类,那么
是很重要的(至少对于单例 bean 来说是如此),你必须确保将*摘要*属性设置为*true*,否则,应用程序
上下文实际上(试图)预先实例化`abstract` Bean。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.8.容器延伸点 通常,应用程序开发人员不需要子类`ApplicationContext`实现类。相反, Spring IOC 容器可以通过插入特殊集成接口的实现来扩展。接下来的几节将描述这些集成接口。 #### 1.8.1.使用`BeanPostProcessor`自定义 bean `BeanPostProcessor`接口定义了回调方法,你可以实现这些方法来提供你自己的(或覆盖容器的默认)实例化逻辑、依赖项解析逻辑,等等。如果希望在 Spring 容器完成实例、配置和初始化 Bean 之后实现一些自定义逻辑,则可以插入一个或多个自定义`BeanPostProcessor`实现。 你可以配置多个`BeanPostProcessor`实例,并且可以通过设置`order`属性来控制这些`BeanPostProcessor`实例的运行顺序。只有当`BeanPostProcessor`实现`Ordered`接口时,才可以设置此属性。如果你编写自己的`BeanPostProcessor`,那么也应该考虑实现`Ordered`接口。有关更多详细信息,请参见[“BeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html)和[`Ordered`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Ordered.html)接口的 爪哇doc。另见[programmatic registration of `BeanPostProcessor` instances](#beans-factory-programmatically-registering-beanpostprocessors)的注释。 | |`BeanPostProcessor`实例对 Bean(或对象)实例进行操作。即,
Spring IOC 容器实例化一个 Bean 实例,然后`BeanPostProcessor`实例执行它们的工作。

“BeanPostProcessor”实例是每个容器的作用域。只有当你
使用容器层次结构时,这才是相关的。如果你在一个容器中定义了`BeanPostProcessor`,则
只对该容器中的 bean 进行后处理。换句话说,在一个容器中定义
的 bean 不会被在
中定义的`BeanPostProcessor`另一个容器进行后处理,即使这两个容器都是同一层次结构的一部分。

来更改实际的 Bean 定义(即,定义 Bean)、
的蓝图需要使用`BeanFactoryPostProcessor`,如[Customizing Configuration Metadata with a `BeanFactoryPostProcessor`](#beans-factory-extension-factory-postprocessors)中所述。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| `org.springframework.beans.factory.config.BeanPostProcessor`接口正好由两个回调方法组成。当这样的类在容器中注册为后处理器时,对于由容器创建的每个 Bean 实例,后处理器在调用容器初始化方法(例如`InitializingBean.afterPropertiesSet()`或任何声明的`init`方法)之前都会从容器获得一个回调,并且在任何 Bean 初始化回调之后。后处理器可以对 Bean 实例采取任何操作,包括完全忽略回调。 Bean 后处理器通常检查回调接口,或者它可以用代理包装 Bean。一些 Spring AOP 基础设施类被实现为 Bean 后处理器,以便提供代理包装逻辑。 `ApplicationContext`会自动检测在实现`BeanPostProcessor`接口的配置元数据中定义的任何 bean。“ApplicationContext”将这些 bean 注册为后处理程序,以便稍后在 Bean 创建时调用它们。 Bean 后处理器可以以与任何其他 bean 相同的方式部署在容器中。 注意,当通过在配置类上使用`@Bean`工厂方法声明`BeanPostProcessor`时,工厂方法的返回类型应该是实现类本身,或者至少是`org.springframework.beans.factory.config.BeanPostProcessor`接口,这清楚地表明了该 Bean 的后处理器性质。否则,“ApplicationContext”无法在完全创建它之前按类型自动检测它。由于`BeanPostProcessor`需要早期实例化,以应用于上下文中其他 bean 的初始化,因此这种早期类型检测非常关键。 | |通过编程方式注册`BeanPostProcessor`实例

虽然推荐的`BeanPostProcessor`注册方法是通过 `ApplicationContext’自动检测(如前所述),但你可以通过使用`addBeanPostProcessor`方法对`ConfigurableBeanFactory`实例进行编程注册。当你需要在
注册之前计算条件逻辑时,或者甚至在层次结构中跨上下文复制 Bean 后处理程序时,这可能是有用的。
但是,注意,以编程方式添加的`BeanPostProcessor`实例并不尊重
的`Ordered`接口。在这里,登记的顺序决定了执行的顺序
。还请注意,无论是否有任何
显式排序,以编程方式注册的
实例总是在通过自动检测注册的实例之前进行处理。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |`BeanPostProcessor`实例和 AOP 自动代理
实现
接口的`BeanPostProcessor`类是特殊的,并且被容器以不同的方式处理
。所有`BeanPostProcessor`实例和它们
直接引用的 bean 都在启动时实例化,作为`ApplicationContext`的特殊启动阶段
的一部分。接下来,以排序的方式注册所有`BeanPostProcessor`实例
,并将其应用于容器中的所有其他 bean。因为
自动代理是作为`BeanPostProcessor`本身实现的,所以`BeanPostProcessor`实例和它们直接引用的 bean 都不符合自动代理的条件,并且,
因此,它们不具有交织在其中的方面。,对于任何这样的 Bean,

,你应该会看到一个信息日志消息:`Bean someBean is not
eligible for getting processed by all BeanPostProcessor interfaces (for example: not
eligible for auto-proxying)`。

如果你的`BeanPostProcessor`中连接了 bean,使用自动连接或 `@resource’(这可能会退回到自动连接), Spring 在搜索类型匹配的依赖项时可能会访问意外的 bean
,因此,使它们
没有资格进行自动代理或其他类型的 Bean 后处理。例如,如果
有一个用`@Resource`注释的依赖项,其中字段或 setter 名称不
直接对应于 Bean 的声明名称,并且不使用 name 属性,则
Spring 访问其他 bean 以通过类型匹配它们。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了如何在`ApplicationContext`中编写、注册和使用`BeanPostProcessor`实例。 ##### 例如:Hello World,`BeanPostProcessor`-style 第一个例子说明了基本用法。该示例显示了一个自定义的“BeanPostProcessor”实现,该实现在容器创建每个 Bean 的时候调用`toString()`方法,并将生成的字符串打印到系统控制台。 下面的清单显示了自定义`BeanPostProcessor`实现类定义: 爪哇 ``` package scripting; import org.springframework.beans.factory.config.BeanPostProcessor; public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor { // simply return the instantiated bean as-is public Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; // we could potentially return any object reference here... } public Object postProcessAfterInitialization(Object bean, String beanName) { System.out.println("Bean '" + beanName + "' created : " + bean.toString()); return bean; } } ``` Kotlin ``` import org.springframework.beans.factory.config.BeanPostProcessor class InstantiationTracingBeanPostProcessor : BeanPostProcessor { // simply return the instantiated bean as-is override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? { return bean // we could potentially return any object reference here... } override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? { println("Bean '$beanName' created : $bean") return bean } } ``` 下面的`beans`元素使用`InstantiationTracingBeanPostProcessor`: ``` ``` 注意`InstantiationTracingBeanPostProcessor`仅仅是如何定义的。它甚至没有名称,并且,由于它是一个 Bean,所以它可以像其他任何 Bean 一样被注入依赖关系。(前面的配置还定义了由 Groovy 脚本支持的 Bean。 Spring 动态语言支持在标题为[动态语言支持](languages.html#dynamic-language)的章节中进行了详细介绍。 下面的 爪哇 应用程序运行前面的代码和配置: 爪哇 ``` import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.scripting.Messenger; public final class Boot { public static void main(final String[] args) throws Exception { ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml"); Messenger messenger = ctx.getBean("messenger", Messenger.class); System.out.println(messenger); } } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = ClassPathXmlApplicationContext("scripting/beans.xml") val messenger = ctx.getBean("messenger") println(messenger) } ``` 上述应用程序的输出类似于以下内容: ``` Bean 'messenger' created : [email protected] [email protected] ``` ##### 示例:`AutowiredAnnotationBeanPostProcessor` 结合自定义`BeanPostProcessor`实现使用回调接口或注释是扩展 Spring IoC 容器的一种常见方法。一个例子是 Spring 的`AutowiredAnnotationBeanPostProcessor`—一个`BeanPostProcessor`实现,该实现附带 Spring 分发和 AutoWires 注释字段、setter 方法和任意配置方法。 #### 1.8.2.使用`BeanFactoryPostProcessor`自定义配置元数据 ### 我们看的下一个扩展点是“org.springframework.beans.factory.config.BeanFactoryPostprocessor”。该接口的语义类似于`BeanPostProcessor`的语义,但有一个主要区别:`BeanFactoryPostProcessor`对 Bean 配置元数据进行操作。也就是说, Spring IOC 容器允许`BeanFactoryPostProcessor`读取配置元数据,并可能更改它*在此之前*容器实例化除`BeanFactoryPostProcessor`实例之外的任何 bean。 你可以配置多个`BeanFactoryPostProcessor`实例,并且可以通过设置`order`属性来控制这些`BeanFactoryPostProcessor`实例的运行顺序。但是,只有当`BeanFactoryPostProcessor`实现了 `Order’接口时,才能设置此属性。如果你编写自己的`BeanFactoryPostProcessor`,那么你也应该考虑实现`Ordered`接口。有关更多详细信息,请参见[“BeanFactoryPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/BeanFactoryPostProcessor.html)和[`Ordered`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Ordered.html)接口的 爪哇doc。 | |如果要更改实际的 Bean 实例(即,从配置元数据创建
的对象),则需要使用`BeanPostProcessor`(在[Customizing Beans by Using a `BeanPostProcessor`](#beans-factory-extension-bpp)中进行了描述)。虽然在技术上可以
处理`BeanFactoryPostProcessor`内的 Bean 实例(例如,通过使用’BeanFactory.GetBean()’),但这样做会导致过早的 Bean 实例化,违反
标准容器生命周期。这可能会导致负面的副作用,例如绕过
Bean 后处理。

同样,`BeanFactoryPostProcessor`实例是每个容器的作用域。如果使用容器层次结构,这只与
相关。如果你在一个
容器中定义了`BeanFactoryPostProcessor`,则它仅应用于该容器中的 Bean 定义。 Bean 在一个容器中的定义
不被在另一个
容器中的`BeanFactoryPostProcessor`实例进行后处理,即使这两个容器都是同一层次结构的一部分。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Bean 工厂后处理器在“ApplicationContext”内声明时自动运行,以便对定义容器的配置元数据应用更改。 Spring 包括一些预定义的 Bean 工厂后处理器,例如`PropertyOverrideConfigurer`和 `PropertySourcesPlaceHolderConfigurer’。你还可以使用自定义`BeanFactoryPostProcessor`——例如,注册自定义属性编辑器。 `ApplicationContext`会自动检测部署到其中的任何 bean,这些 bean 实现`BeanFactoryPostProcessor`接口。它在适当的时候使用这些 bean 作为 Bean 工厂的后处理器。你可以部署这些后处理器 bean,就像部署任何其他的 Bean。 | |与`BeanPostProcessor`s 一样,你通常不希望为延迟初始化配置“BeanFactoryPostProcessor”。如果没有其他 Bean 引用 ` Bean(工厂)后处理器 `,则该后处理器将根本不会实例化。因此,将其标记为惰性初始化将被忽略,并且 ` Bean(工厂)后处理器 ` 将被急切地实例化,即使你在你的``元素的声明上将 `default-lazy-init` 属性设置为`true`。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 示例:类名替换`PropertySourcesPlaceholderConfigurer`##### 通过使用标准的 爪哇`Properties`格式,可以使用`PropertySourcesPlaceholderConfigurer`将 Bean 定义中的属性值外部化到单独的文件中。这样做可以使部署应用程序的人员定制特定于环境的属性,例如数据库 URL 和密码,而无需修改主 XML 定义文件或容器的文件的复杂性或风险。 考虑以下基于 XML 的配置元数据片段,其中定义了带有占位值的`DataSource`: ``` ``` 该示例显示了从外部`Properties`文件配置的属性。在运行时,将`PropertySourcesPlaceholderConfigurer`应用于元数据,以替换数据源的某些属性。要替换的值被指定为`${property-name}`形式的占位符,它遵循 Ant 和 log4j 和 jsp el 样式。 实际值来自另一个标准 爪哇`Properties`格式的文件: ``` jdbc.driverClassName=org.hsqldb.jdbcDriver jdbc.url=jdbc:hsqldb:hsql://production:9002 jdbc.username=sa jdbc.password=root ``` 因此,`${jdbc.username}`字符串在运行时被替换为值“sa”,这同样适用于匹配属性文件中的键的其他占位符。在 Bean 定义的大多数属性和属性中,`PropertySourcesPlaceholderConfigurer`检查占位符。此外,你还可以自定义占位符前缀和后缀。 使用 Spring 2.5 中引入的`context`名称空间,你可以使用一个专用的配置元素来配置属性占位符。可以在`location`属性中以逗号分隔的列表形式提供一个或多个位置,如下例所示: ``` ``` `PropertySourcesPlaceholderConfigurer`不仅在你指定的`Properties`文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它将检查 Spring `Environment`属性和常规的 爪哇`System`属性。 | |可以使用`PropertySourcesPlaceholderConfigurer`替换类名称,其中
有时是有用的当你必须在运行时选择一个特定的实现类时。
下面的示例显示了如何做到:

`



<><1995"gt=">>><“tcase”/>>>>>“tcase的解析在它即将被创建时失败,这是在`preInstantiateSingletons()`对于非惰性-init 的`ApplicationContext`阶段期间 Bean。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 示例:`PropertyOverrideConfigurer` 另一个 Bean 工厂后处理器`PropertyOverrideConfigurer`类似于 `PropertySourcesPlaceHolderConfigurer’,但与后者不同的是,对于 Bean 属性,原始定义可以有默认值,也可以完全没有值。如果重写的 `properties’文件中没有某个 Bean 属性的条目,则使用默认的上下文定义。 请注意, Bean 定义并不知道是否被重写,因此从 XML 定义文件中并不能立即看出是否正在使用重写配置器。在多个`PropertyOverrideConfigurer`实例为相同的 Bean 属性定义不同的值的情况下,由于覆盖机制,最后一个实例获胜。 属性文件配置行采用以下格式: ``` beanName.property=value ``` 下面的清单展示了这种格式的一个示例: ``` dataSource.driverClassName=com.mysql.jdbc.Driver dataSource.url=jdbc:mysql:mydb ``` 这个示例文件可以与容器定义一起使用,该容器定义包含一个名为 `DataSource’的 Bean,它具有`driver`和`url`属性。 复合属性名称也是受支持的,只要路径的每个组件(除了被重写的最终属性)已经是非空的(大概是由构造函数初始化的)。在下面的示例中,将`tom` Bean 的`fred`属性的`sammy`属性设置为标量值`123`: ``` tom.fred.bob.sammy=123 ``` | |指定的重写值总是文字值。它们没有被翻译成
Bean 引用。当 XML Bean 定义中的原始值指定 Bean 引用时,该约定也适用。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 通过 Spring 2.5 中引入的`context`名称空间,可以使用专用的配置元素配置属性重写,如下例所示: ``` ``` #### 1.8.3.使用`FactoryBean`自定义实例化逻辑 你可以为本身是工厂的对象实现`org.springframework.beans.factory.FactoryBean`接口。 `FactoryBean`接口是可插入 Spring IoC 容器的实例化逻辑的一个点。如果你的复杂初始化代码更好地用 爪哇 来表达,而不是(可能)使用冗长的 XML,那么你可以创建自己的“FactoryBean”,在该类中编写复杂的初始化,然后将你的自定义`FactoryBean`插入到容器中。 `FactoryBean`接口提供了三种方法: * `T getObject()`:返回这个工厂创建的对象的实例。这个实例可以被共享,这取决于这个工厂返回的是单例还是原型。 * `boolean isSingleton()`:返回`true`如果这个`FactoryBean`返回单例,否则返回 `false’。此方法的默认实现返回`true`。 * `Class getObjectType()`:返回由`getObject()`方法返回的对象类型或`null`如果类型事先不知道的话。 `FactoryBean`概念和接口在 Spring 框架内的许多地方被使用。50 多个实现方式的`FactoryBean`与 Spring 本身相关联的接口。 当你需要向容器请求一个实际的`FactoryBean`实例本身,而不是它生成的 Bean 实例时,在调用`id`方法的`getBean()`时,在 Bean 的`id`前加上符号。因此,对于给定的`FactoryBean`和`id`的`myBean`,在容器上调用`getBean("myBean")`将返回`FactoryBean`的乘积,而调用`getBean("&myBean")`将返回 `FactoryBean’实例本身。 ### 1.9.基于注释的容器配置 在配置 Spring 方面,注释是否比 XML 更好? 基于注释的配置的引入提出了这样一个问题:这种方法是否比 XML“更好”。简短的回答是“视情况而定。”长期的答案是,每种方法都有其优点和缺点,通常,由开发人员来决定哪种策略更适合他们。由于它们的定义方式,注释在其声明中提供了大量的上下文,从而导致更短和更简洁的配置。然而,XML 擅长将组件连接起来,而不会涉及它们的源代码或重新编译它们。一些开发人员倾向于让连接更接近源代码,而另一些开发人员则认为,带注释的类不再是 POJO,此外,配置变得分散且更难控制。 无论如何选择, Spring 都可以容纳这两种风格,甚至可以将它们混合在一起。值得指出的是,通过其[爪哇Config](#beans-java)选项, Spring 允许以非侵入性的方式使用注释,而不涉及目标组件源代码,并且,就工具而言,[Spring Tools for Eclipse](https://spring.io/tools)支持所有配置样式。 基于注释的配置提供了 XML 设置的另一种选择,它依赖字节码元数据来连接组件,而不是角括号声明。开发人员不使用 XML 来描述 Bean 连接,而是通过使用相关类、方法或字段声明上的注释将配置移动到组件类本身。正如在[Example: The `AutowiredAnnotationBeanPostProcessor`](#beans-factory-extension-bpp-examples-aabpp)中提到的那样,结合注释使用`BeanPostProcessor`是扩展 Spring IOC 容器的一种常见方法。例如, Spring 2.0 引入了使用[`@Required`](#beans-required-annotation)注释强制执行所需属性的可能性。 Spring 2.5 使得有可能遵循相同的通用方法来驱动 Spring 的依赖注入。从本质上讲,`@Autowired`注释提供了与[自动布线合作者](#beans-factory-autowire)中描述的相同的功能,但具有更细粒度的控制和更广泛的适用性。 Spring 2.5 还增加了对 JSR-250 注释的支持,例如 `@postConstruct` 和`@PreDestroy`。 Spring 3.0 增加了对`javax.inject`包中包含的 JSR-330(用于 爪哇 的依赖注入)注释的支持,例如`@Inject`和`@Named`。有关这些注释的详细信息可以在[相关部分](#beans-standard-annotations)中找到。 | |注释注入是在 XML 注入之前执行的。因此,XML 配置
覆盖了通过这两种方法连接的属性的注释。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| 与往常一样,你可以将后处理器注册为单独的 Bean 定义,但也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式地注册它们(请注意包含`context`名称空间): ``` ``` ``元素隐式地注册了以下后处理程序: * [“配置 classpostprocessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/ConfigurationClassPostProcessor.html) * [“自动 WireDannotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html) * [“CommonAnnotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.html) * [“坚持注解后置处理器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html) * [“EventListenerMethodProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/EventListenerMethodProcessor.html) | |``只在与其定义相同的
应用程序上下文中查找 bean 上的注释。这意味着,如果你将置于`WebApplicationContext`中的`DispatcherServlet`中,
则只检查控制器中的`@Autowired`bean,而不是你的服务。有关更多信息,请参见[DispatcherServlet](web.html#mvc-servlet)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.9.1.@required `@Required`注释适用于 Bean 属性设置器方法,如下例所示: 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; @Required public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` class SimpleMovieLister { @Required lateinit var movieFinder: MovieFinder // ... } ``` 该注释表明,受影响的 Bean 属性必须在配置时通过 Bean 定义中的显式属性值或通过自动布线来填充。如果未填充受影响的 Bean 属性,则容器抛出一个异常。这允许急切和明确的失败,避免`NullPointerException`实例或类似的情况。我们仍然建议你将断言放入 Bean 类本身(例如,放入 init 方法)。即使在容器之外使用类,这样做也会强制执行那些必需的引用和值。 | |[“RequiredAnNotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html)必须注册为 Bean,以启用对`@Required`注释的支持。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |在 Spring Framework5.1 中,`@Required`注释和`RequiredAnnotationBeanPostProcessor`正式地被
弃用,这有利于使用构造函数注入用于
所需的设置(或`InitializingBean.afterPropertiesSet()`的自定义实现或带有 Bean 属性设定器方法的自定义`@PostConstruct`方法)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.9.2.使用`@Autowired` | |JSR330 的`@Inject`注释可以用来代替 Spring 的`@Autowired`注释。有关更多详细信息,请参见[here](#beans-standard-annotations)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以将`@Autowired`注释应用于构造函数,如下例所示: 爪哇 ``` public class MovieRecommender { private final CustomerPreferenceDao customerPreferenceDao; @Autowired public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) { this.customerPreferenceDao = customerPreferenceDao; } // ... } ``` Kotlin ``` class MovieRecommender @Autowired constructor( private val customerPreferenceDao: CustomerPreferenceDao) ``` | |从 Spring Framework4.3 开始,如果目标 Bean 只定义了一个要开始使用的构造函数,那么在这样的构造函数上的`@Autowired`注释就不再是
所必需的了。但是,如果
有几个构造函数可用,并且没有主/缺省构造函数,则至少
其中一个构造函数必须用`@Autowired`进行注释,以便指示
容器使用哪个。详见[构造器分辨率](#beans-autowired-annotation-constructor-resolution)上的讨论。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 还可以将`@Autowired`注释应用于*传统的*setter 方法,如下例所示: 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; @Autowired public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` class SimpleMovieLister { @Autowired lateinit var movieFinder: MovieFinder // ... } ``` 你还可以将注释应用于具有任意名称和多个参数的方法,如下例所示: 爪哇 ``` public class MovieRecommender { private MovieCatalog movieCatalog; private CustomerPreferenceDao customerPreferenceDao; @Autowired public void prepare(MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) { this.movieCatalog = movieCatalog; this.customerPreferenceDao = customerPreferenceDao; } // ... } ``` Kotlin ``` class MovieRecommender { private lateinit var movieCatalog: MovieCatalog private lateinit var customerPreferenceDao: CustomerPreferenceDao @Autowired fun prepare(movieCatalog: MovieCatalog, customerPreferenceDao: CustomerPreferenceDao) { this.movieCatalog = movieCatalog this.customerPreferenceDao = customerPreferenceDao } // ... } ``` 你还可以将`@Autowired`应用于字段,甚至将其与构造函数混合使用,如下例所示: 爪哇 ``` public class MovieRecommender { private final CustomerPreferenceDao customerPreferenceDao; @Autowired private MovieCatalog movieCatalog; @Autowired public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) { this.customerPreferenceDao = customerPreferenceDao; } // ... } ``` Kotlin ``` class MovieRecommender @Autowired constructor( private val customerPreferenceDao: CustomerPreferenceDao) { @Autowired private lateinit var movieCatalog: MovieCatalog // ... } ``` | |确保你的目标组件(例如,`MovieCatalog`或`CustomerPreferenceDao`)
由你为`@Autowired`使用的类型一致地声明-注释
注入点。否则,注入可能会由于运行时的“未找到类型匹配”错误而失败。

对于通过 Classpath 扫描找到的 XML 定义的 bean 或组件类,容器
通常预先知道具体的类型。但是,对于`@Bean`工厂方法,你需要
来确保声明的返回类型具有足够的表达能力。对于实现多个接口的组件
,或者对于由其
实现类型可能引用的组件,考虑在工厂
方法上声明最特定的返回类型(至少与引用你的 Bean 的注入点所要求的特定类型一样)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 还可以指示 Spring 通过将`@Autowired`注释添加到需要该类型数组的字段或方法,从 `ApplicationContext’中提供特定类型的所有 bean,如下例所示: 爪哇 ``` public class MovieRecommender { @Autowired private MovieCatalog[] movieCatalogs; // ... } ``` Kotlin ``` class MovieRecommender { @Autowired private lateinit var movieCatalogs: Array // ... } ``` 这同样适用于类型化集合,如下例所示: 爪哇 ``` public class MovieRecommender { private Set movieCatalogs; @Autowired public void setMovieCatalogs(Set movieCatalogs) { this.movieCatalogs = movieCatalogs; } // ... } ``` Kotlin ``` class MovieRecommender { @Autowired lateinit var movieCatalogs: Set // ... } ``` | |你的目标 bean 可以实现`org.springframework.core.Ordered`接口或使用
`@Order`或标准`@Priority`注释,如果你希望数组或列表中的项
按特定顺序排序的话。否则,它们的顺序遵循在容器中对应的目标 Bean 定义的注册顺序


可以在目标类级别和`@Order`方法上声明`@Bean`注释,
可能用于单独的 Bean 定义(在多个定义
使用相同的 Bean 类的情况下)。`@Order`值可能会影响注入点的优先级,
但要注意,它们不会影响单例启动顺序,这是由依赖关系和`@DependsOn`声明确定的正交关系。,

注意,标准`javax.annotation.Priority`注释在 `@ Bean ` 级别不可用,因为它不能在方法上声明。它的语义可以通过`@Order`值与`@Primary`值结合,在单个 Bean 上为每个类型建模`@Primary`。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 即使类型`Map`实例也可以自动连线,只要所期望的键类型是`String`。映射值包含预期类型的所有 bean,键包含相应的 Bean 名称,如下例所示: 爪哇 ``` public class MovieRecommender { private Map movieCatalogs; @Autowired public void setMovieCatalogs(Map movieCatalogs) { this.movieCatalogs = movieCatalogs; } // ... } ``` Kotlin ``` class MovieRecommender { @Autowired lateinit var movieCatalogs: Map // ... } ``` 默认情况下,当给定的注入点没有匹配的候选 bean 可用时,自动布线失败。在声明的数组、集合或映射的情况下,需要至少一个匹配元素。 默认的行为是将带注释的方法和字段视为指示所需的依赖项。你可以改变这种行为,如下例所示,通过将一个不可满足的注入点标记为非必需的,使框架能够跳过该点(即,通过将`required`中的`@Autowired`属性设置为`false`): 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; @Autowired(required = false) public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` class SimpleMovieLister { @Autowired(required = false) var movieFinder: MovieFinder? = null // ... } ``` 如果一个非必需的方法的依赖项(或者在多个参数的情况下,它的一个依赖项)是不可用的,那么它将不会被调用。在这种情况下,不需要的字段将根本不会被填充,从而保留其默认值。 注入的构造函数和工厂方法参数是一种特殊情况,因为`@Autowired`中的`required`属性具有某种不同的含义,这是由于 Spring 的构造函数解析算法可能会处理多个构造函数。默认情况下,构造函数和工厂方法参数实际上是必需的,但在单个构造函数场景中需要一些特殊规则,例如,如果没有匹配的 bean 可用,则将多元素注入点(数组、集合、映射)解析为空实例。这允许一种常见的实现模式,在这种模式中,所有依赖项都可以在唯一的多参数构造函数中声明——例如,在没有`@Autowired`注释的情况下,声明为单个公共构造函数。 | |Bean 类中只有一个构造函数可以声明`@Autowired`,其`required`属性设置为`true`,表示*The*构造函数在用作 Spring
Bean 时自动连接。因此,如果`required`属性保留在其默认值`true`处,
只能用`@Autowired`注释单个构造函数。如果多个构造函数
声明注释,它们都必须声明`required=false`,以便将
视为自动布线的候选项(类似于 XML 中的`autowire=constructor`)。
将选择通过在 Spring 容器中匹配
bean 而能够满足最多依赖项的构造函数。如果不能满足所有候选项,
则将使用主/缺省构造函数(如果存在)。类似地,如果类
声明了多个构造函数,但其中没有一个被`@Autowired`注释,那么将使用
主/缺省构造函数(如果存在)。如果一个类只声明一个
构造函数,那么它将始终被使用,即使没有注释。注意,一个
带注释的构造函数并不一定是公共的。

`required`属性在 setter 方法上的`@Autowired`注释上是推荐的。将`required`属性设置为`false`表示
属性不是自动布线所必需的,如果
不能自动布线,则忽略该属性。而`@Required`则更强,因为它强制使用容器支持的任何方式来设置
属性,并且如果没有定义值,
将引发相应的异常。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 或者,你可以通过 爪哇8 的`java.util.Optional`表示特定依赖项的非必需性质,如下例所示: ``` public class SimpleMovieLister { @Autowired public void setMovieFinder(Optional movieFinder) { ... } } ``` 在 Spring Framework5.0 中,你还可以使用`@Nullable`注释(在任何包中的任何类型——例如,来自 JSR-305 的`javax.annotation.Nullable`)或仅利用 Kotlin 内建空安全支持: 爪哇 ``` public class SimpleMovieLister { @Autowired public void setMovieFinder(@Nullable MovieFinder movieFinder) { ... } } ``` Kotlin ``` class SimpleMovieLister { @Autowired var movieFinder: MovieFinder? = null // ... } ``` 对于众所周知的可解析依赖关系的接口,也可以使用`@Autowired`:`BeanFactory`,`ApplicationContext`,`Environment`,`ResourceLoader`,这些类型必须通过使用 XML 或 Spring `@Bean`方法显式地“连接起来”。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.9.3.使用`@Primary`微调基于注释的自动连线 由于按类型自动布线可能会导致多个候选者,因此通常需要对选择过程有更多的控制。实现这一点的一种方法是使用 Spring 的“@primary”注释。`@Primary`表示当多个 bean 是要自动连接到单值依赖项的候选 bean 时,应该给予特定的 Bean 优先权。如果在候选者之间正好存在一个主 Bean,则它成为自动连线值。 考虑以下配置,该配置将`firstMovieCatalog`定义为主要的`MovieCatalog`: 爪哇 ``` @Configuration public class MovieConfiguration { @Bean @Primary public MovieCatalog firstMovieCatalog() { ... } @Bean public MovieCatalog secondMovieCatalog() { ... } // ... } ``` Kotlin ``` @Configuration class MovieConfiguration { @Bean @Primary fun firstMovieCatalog(): MovieCatalog { ... } @Bean fun secondMovieCatalog(): MovieCatalog { ... } // ... } ``` 在前面的配置中,下面的`MovieRecommender`与“FirstMoviecatalog”自动连线: 爪哇 ``` public class MovieRecommender { @Autowired private MovieCatalog movieCatalog; // ... } ``` Kotlin ``` class MovieRecommender { @Autowired private lateinit var movieCatalog: MovieCatalog // ... } ``` 相应的 Bean 定义如下: ``` ``` #### 1.9.4.使用限定符对基于注释的自动连线进行微调 `@Primary`是一种在可以确定一个主要候选者的情况下,通过多个实例使用自动布线的有效方法。当需要对选择过程进行更多控制时,可以使用 Spring 的`@Qualifier`注释。你可以将限定符值与特定的参数关联起来,从而缩小类型匹配的集合,以便为每个参数选择一个特定的 Bean。在最简单的情况下,这可以是一个简单的描述性值,如以下示例所示: 爪哇 ``` public class MovieRecommender { @Autowired @Qualifier("main") private MovieCatalog movieCatalog; // ... } ``` Kotlin ``` class MovieRecommender { @Autowired @Qualifier("main") private lateinit var movieCatalog: MovieCatalog // ... } ``` 你还可以对单个构造函数参数或方法参数指定`@Qualifier`注释,如以下示例所示: 爪哇 ``` public class MovieRecommender { private MovieCatalog movieCatalog; private CustomerPreferenceDao customerPreferenceDao; @Autowired public void prepare(@Qualifier("main") MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) { this.movieCatalog = movieCatalog; this.customerPreferenceDao = customerPreferenceDao; } // ... } ``` Kotlin ``` class MovieRecommender { private lateinit var movieCatalog: MovieCatalog private lateinit var customerPreferenceDao: CustomerPreferenceDao @Autowired fun prepare(@Qualifier("main") movieCatalog: MovieCatalog, customerPreferenceDao: CustomerPreferenceDao) { this.movieCatalog = movieCatalog this.customerPreferenceDao = customerPreferenceDao } // ... } ``` 下面的示例显示了相应的 Bean 定义。 ``` (1) (2) ``` |**1**|具有`main`限定符值的 Bean 与构造函数参数连线,即
具有相同的限定符值。| |-----|----------------------------------------------------------------------------------------------------------------------------| |**2**|具有`action`限定符值的 Bean 与构造函数参数连线,即
具有相同的限定符值。| 对于回退匹配, Bean 名称被认为是缺省限定符值。因此,可以使用`id`的`main`来定义 Bean,而不是使用嵌套的限定符元素,从而导致相同的匹配结果。然而,尽管可以使用此约定按名称引用特定的 bean,但`@Autowired`基本上是关于类型驱动的注入和可选的语义限定符。这意味着,即使使用 Bean Name Fallback,限定符值在类型匹配集合中也始终具有狭窄的语义。它们在语义上不表示对唯一 Bean `id`的引用。好的限定符值是`main`或`EMEA`或`persistent`,表示独立于 Bean `id`的特定组件的特征,在匿名 Bean 定义的情况下,例如前面示例中的定义,该特征可以自动生成。 限定符也适用于类型化集合,如前面讨论的那样——例如,适用于 `set`。在本例中,根据声明的限定符,所有匹配的 bean 都作为集合注入。这意味着限定词不必是唯一的。相反,它们构成了过滤标准。例如,你可以使用相同的限定符值“action”来定义多个`MovieCatalog`bean,所有这些 bean 都被注入到`Set`注释为`@Qualifier("action")`的 bean 中。 | |在类型匹配的
候选项中,让限定符值根据目标 Bean 名称进行选择,不需要在注入点进行`@Qualifier`注释。
如果没有其他分辨率指示符(例如限定符或主标记),则对于非惟一依赖关系情况,
, Spring 将注入点名称
(即字段名称或参数名称)与目标 Bean 名称匹配,并选择
相同名称的候选项,如果有的话。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 也就是说,如果打算通过名称表示注释驱动的注入,则不主要使用`@Autowired`,即使它能够通过 Bean 在类型匹配候选者中选择名称。相反,使用 JSR-250`@Resource`注释,该注释在语义上定义为通过其唯一名称来标识特定的目标组件,声明的类型与匹配过程无关。`@Autowired`具有相当不同的语义:在按类型选择候选 bean 之后,指定的`String`限定符值仅被考虑在那些类型选择的候选值中(例如,将`account`限定符与标记有相同限定符标签的 bean 匹配)。 对于本身被定义为集合的 bean,`Map`或数组类型,`@Resource`是一个很好的解决方案,通过唯一的名称引用特定的集合或数组 Bean。也就是说,从 4.3 开始,你也可以通过 Spring 的“@autowired”类型匹配算法来匹配集合、`Map`和数组类型,只要元素类型信息保留在`@Bean`返回类型签名或集合继承层次结构中。在这种情况下,你可以使用限定符值在相同类型的集合中进行选择,如上一段所概述的那样。 截至 4.3,`@Autowired`还考虑用于注入的自引用(即,对当前注入的 Bean 的引用)。请注意,自我注入是一种回退。对其他组件的常规依赖总是具有优先权的。从这个意义上说,自我参照不参与常规的候选人选择,因此,特别是从来没有主要的。相反,它们最终总是排在最靠后的位置。在实践中,你应该仅将自引用作为最后的手段(例如,通过 Bean 的事务代理调用同一实例上的其他方法)。考虑在这样的场景中将受影响的方法分解为单独的委托 Bean。或者,可以使用`@Resource`,这可以获得返回当前 Bean 的代理的唯一名称。 | |试图在相同的配置类上注入来自`@Bean`方法的结果实际上也是一个自引用场景。在实际需要的方法签名中(而不是配置类中的自动连线字段
),懒惰地解析此类引用
,或者将受影响的`@Bean`方法声明为`static`,
将它们与包含的配置类实例及其生命周期分离。否则,这样的 bean 仅在后备阶段被考虑,在其他配置类上匹配的 bean
被选为主要候选者(如果可用的话)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| `@Autowired`应用于字段、构造函数和多参数方法,允许在参数级别上通过限定符注释进行缩小。与此相反,`@Resource`仅支持具有单个参数的字段和 Bean 属性设置器方法。因此,如果注入目标是构造函数或多参数方法,则应该坚持使用限定符。 你可以创建自己的自定义限定符注释。要做到这一点,请定义一个注释,并在你的定义中提供`@Qualifier`注释,如下例所示: 爪哇 ``` @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Genre { String value(); } ``` Kotlin ``` @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class Genre(val value: String) ``` 然后,你可以在 AutoWired 字段和参数上提供自定义限定符,如下例所示: 爪哇 ``` public class MovieRecommender { @Autowired @Genre("Action") private MovieCatalog actionCatalog; private MovieCatalog comedyCatalog; @Autowired public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) { this.comedyCatalog = comedyCatalog; } // ... } ``` Kotlin ``` class MovieRecommender { @Autowired @Genre("Action") private lateinit var actionCatalog: MovieCatalog private lateinit var comedyCatalog: MovieCatalog @Autowired fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) { this.comedyCatalog = comedyCatalog } // ... } ``` 接下来,可以为候选 Bean 定义提供信息。你可以添加 `标记作为``标记的子元素,然后指定`type`和 `value’以匹配自定义限定符注释。类型与注释的完全限定类名称匹配。另外,如果不存在名称冲突的风险,为了方便起见,你可以使用较短的类名。下面的示例演示了这两种方法: ``` ``` 在[Classpath Scanning and Managed Components](#beans-classpath-scanning)中,你可以看到一种基于注释的替代方法,用于在 XML 中提供限定符元数据。具体见[提供带有注释的限定符元数据](#beans-scanning-qualifiers)。 在某些情况下,使用没有值的注释可能就足够了。当注释服务于更通用的目的并且可以跨几种不同类型的依赖关系应用时,这可能是有用的。例如,你可以提供一个脱机目录,当没有可用的 Internet 连接时可以对其进行搜索。首先,定义简单的注释,如下例所示: 爪哇 ``` @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface Offline { } ``` Kotlin ``` @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class Offline ``` 然后将注释添加到要自动连接的字段或属性中,如以下示例所示: 爪哇 ``` public class MovieRecommender { @Autowired @Offline (1) private MovieCatalog offlineCatalog; // ... } ``` |**1**|这一行添加了`@Offline`注释。| |-----|-----------------------------------------| Kotlin ``` class MovieRecommender { @Autowired @Offline (1) private lateinit var offlineCatalog: MovieCatalog // ... } ``` |**1**|这一行添加了`@Offline`注释。| |-----|-----------------------------------------| 现在 Bean 定义只需要一个限定符`type`,如下例所示: ``` (1) ``` |**1**|此元素指定限定符。| |-----|-------------------------------------| 你还可以定义自定义限定符注释,这些注释除了接受简单的`value`属性之外,还接受命名属性。 Bean 如果随后在要自动连线的字段或参数上指定了多个属性值,则定义必须匹配所有这样的属性值才能被视为自动连线候选值。作为示例,请考虑以下注释定义: 爪哇 ``` @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Qualifier public @interface MovieQualifier { String genre(); Format format(); } ``` Kotlin ``` @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) @Retention(AnnotationRetention.RUNTIME) @Qualifier annotation class MovieQualifier(val genre: String, val format: Format) ``` 在这种情况下,`Format`是一个枚举,定义如下: 爪哇 ``` public enum Format { VHS, DVD, BLURAY } ``` Kotlin ``` enum class Format { VHS, DVD, BLURAY } ``` 要自动连接的字段将使用自定义限定符进行注释,并包括两个属性的值:`genre`和`format`,如下例所示: 爪哇 ``` public class MovieRecommender { @Autowired @MovieQualifier(format=Format.VHS, genre="Action") private MovieCatalog actionVhsCatalog; @Autowired @MovieQualifier(format=Format.VHS, genre="Comedy") private MovieCatalog comedyVhsCatalog; @Autowired @MovieQualifier(format=Format.DVD, genre="Action") private MovieCatalog actionDvdCatalog; @Autowired @MovieQualifier(format=Format.BLURAY, genre="Comedy") private MovieCatalog comedyBluRayCatalog; // ... } ``` Kotlin ``` class MovieRecommender { @Autowired @MovieQualifier(format = Format.VHS, genre = "Action") private lateinit var actionVhsCatalog: MovieCatalog @Autowired @MovieQualifier(format = Format.VHS, genre = "Comedy") private lateinit var comedyVhsCatalog: MovieCatalog @Autowired @MovieQualifier(format = Format.DVD, genre = "Action") private lateinit var actionDvdCatalog: MovieCatalog @Autowired @MovieQualifier(format = Format.BLURAY, genre = "Comedy") private lateinit var comedyBluRayCatalog: MovieCatalog // ... } ``` 最后, Bean 定义应该包含匹配的限定符值。这个示例还演示了你可以使用 Bean 元属性而不是 `` 元素。如果可用,则``元素及其属性优先,但是,如果不存在这样的限定符,则自动连接机制将落在 `标记内提供的值上,如下例中的最后两个 Bean 定义所示: ``` ``` #### 1.9.5.使用泛型作为自动连线限定符 除了`@Qualifier`注释之外,你还可以使用 爪哇 泛型类型作为一种隐含的限定形式。例如,假设你有以下配置: 爪哇 ``` @Configuration public class MyConfiguration { @Bean public StringStore stringStore() { return new StringStore(); } @Bean public IntegerStore integerStore() { return new IntegerStore(); } } ``` Kotlin ``` @Configuration class MyConfiguration { @Bean fun stringStore() = StringStore() @Bean fun integerStore() = IntegerStore() } ``` 假设前面的 bean 实现了一个泛型接口(即`Store`和 `store`),则可以`@Autowire`将`Store`接口和泛型用作限定符,如下例所示: 爪哇 ``` @Autowired private Store s1; // qualifier, injects the stringStore bean @Autowired private Store s2; // qualifier, injects the integerStore bean ``` Kotlin ``` @Autowired private lateinit var s1: Store // qualifier, injects the stringStore bean @Autowired private lateinit var s2: Store // qualifier, injects the integerStore bean ``` 当自动连接列表、`Map`实例和数组时,通用限定符也会应用。以下示例自动连接通用`List`: 爪哇 ``` // Inject all Store beans as long as they have an generic // Store beans will not appear in this list @Autowired private List> s; ``` Kotlin ``` // Inject all Store beans as long as they have an generic // Store beans will not appear in this list @Autowired private lateinit var s: List> ``` #### 1.9.6.使用`CustomAutowireConfigurer` [“CustomAutoWireConfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.html)是一个`BeanFactoryPostProcessor`,它允许你注册自己的自定义限定符注释类型,即使它们没有使用 Spring 的`@Qualifier`注释。下面的示例展示了如何使用`CustomAutowireConfigurer`: ``` example.CustomQualifier ``` `AutowireCandidateResolver`通过以下方式确定 AutoWire 候选项: * 每个 Bean 定义的`autowire-candidate`值 * 在``元素上可用的任何`default-autowire-candidates`模式 * 存在`@Qualifier`注释和在`CustomAutowireConfigurer`中注册的任何自定义注释 当多个 bean 符合自动连接候选项的条件时,“主”的确定如下:如果在候选项中恰好有一个 Bean 定义将`primary`属性设置为`true`,则选中它。 #### 1.9.7.注入`@Resource` Spring 还通过在字段或 Bean 属性设置器方法上使用 JSR-250`@Resource`注释(`javax.annotation.resource`)来支持注入。这是 爪哇 EE 中的一种常见模式:例如,在 JSF 管理的 Bean 和 JAX-WS 端点中。 Spring 对于 Spring-托管对象也支持这种模式。 `@Resource`接受一个 name 属性。默认情况下, Spring 将该值解释为要注入的 Bean 名称。换句话说,它遵循副名语义,如下例所示: 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; @Resource(name="myMovieFinder") (1) public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } } ``` |**1**|这一行注入一个`@Resource`。| |-----|--------------------------------| Kotlin ``` class SimpleMovieLister { @Resource(name="myMovieFinder") (1) private lateinit var movieFinder:MovieFinder } ``` |**1**|这一行注入一个`@Resource`。| |-----|--------------------------------| 如果没有显式指定名称,则缺省名称将从字段名称或 setter 方法派生。在字段的情况下,它采用字段名称。对于 setter 方法,它使用 Bean 属性名。下面的示例将把名为`movieFinder`的 Bean 注入到其 setter 方法中: 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; @Resource public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } } ``` Kotlin ``` class SimpleMovieLister { @Resource private lateinit var movieFinder: MovieFinder } ``` | |注释提供的名称通过`CommonAnnotationBeanPostProcessor`知道的 `applicationContext’解析为 Bean 名称。
如果显式地配置 Spring 的[“SimplejndibeanFactory”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jndi/support/SimpleJndiBeanFactory.html),则可以通过 JNDI 解析名称。但是,我们建议你依赖默认的行为,并且
使用 Spring 的 JNDI 查找功能来保持间接的级别。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在不指定显式名称的`@Resource`用法的独占情况下,并且类似于`@Autowired`,`@Resource`找到一个主类型匹配,而不是一个特定的命名 Bean,并解决众所周知的可解析依赖项:`BeanFactory`,`applicationContext`,`ResourceLoader`,`ApplicationEventPublisher`接口。 因此,在下面的示例中,`customerPreferenceDao`字段首先查找名为“CustomerPreferenceDAO”的 Bean,然后返回到类型 `CustomerPreferenceDAO’的主类型匹配: 爪哇 ``` public class MovieRecommender { @Resource private CustomerPreferenceDao customerPreferenceDao; @Resource private ApplicationContext context; (1) public MovieRecommender() { } // ... } ``` |**1**|基于已知的可解析依赖类型:“ApplicationContext”注入`context`字段。| |-----|---------------------------------------------------------------------------------------------------| Kotlin ``` class MovieRecommender { @Resource private lateinit var customerPreferenceDao: CustomerPreferenceDao @Resource private lateinit var context: ApplicationContext (1) // ... } ``` |**1**|基于已知的可解析依赖类型:“ApplicationContext”注入`context`字段。| |-----|---------------------------------------------------------------------------------------------------| #### 1.9.8.使用`@Value` `@Value`通常用于注入外部化属性: 爪哇 ``` @Component public class MovieRecommender { private final String catalog; public MovieRecommender(@Value("${catalog.name}") String catalog) { this.catalog = catalog; } } ``` Kotlin ``` @Component class MovieRecommender(@Value("\${catalog.name}") private val catalog: String) ``` 具有以下配置: Java ``` @Configuration @PropertySource("classpath:application.properties") public class AppConfig { } ``` Kotlin ``` @Configuration @PropertySource("classpath:application.properties") class AppConfig ``` 以及下面的`application.properties`文件: ``` catalog.name=MovieCatalog ``` 在这种情况下,`catalog`参数和字段将等于`MovieCatalog`值。 Spring 提供了一个默认的宽松内含价值解析器。它将尝试解析属性值,如果无法解析,则将注入属性名(例如`${catalog.name}`)作为该值。如果希望对不存在的值保持严格控制,则应该声明`PropertySourcesPlaceholderConfigurer` Bean,如下例所示: Java ``` @Configuration public class AppConfig { @Bean public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun propertyPlaceholderConfigurer() = PropertySourcesPlaceholderConfigurer() } ``` | |当使用 JavaConfig 配置`PropertySourcesPlaceholderConfigurer`时,`@ Bean ` 方法必须是`static`。| |---|---------------------------------------------------------------------------------------------------------------| 如果无法解决任何`${}`占位符,则使用上述配置可确保初始化失败。也可以使用“setPlaceHolderPrefix”、`setPlaceholderSuffix`或`setValueSeparator`等方法来定制占位符。 | |Spring 在默认情况下,引导配置一个`PropertySourcesPlaceholderConfigurer` Bean,即
将从`application.properties`和`application.yml`文件获得属性。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如到`Integer`或`int`)。多个逗号分隔的值可以自动转换为`String`数组,无需额外的工作。 可以提供如下默认值: Java ``` @Component public class MovieRecommender { private final String catalog; public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { this.catalog = catalog; } } ``` Kotlin ``` @Component class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String) ``` Spring `BeanPostProcessor`在幕后使用`ConversionService`来处理将`String`中的`String`值转换为目标类型的过程。如果你想为自己的自定义类型提供转换支持,可以提供自己的“ConversionService” Bean 实例,如下例所示: Java ``` @Configuration public class AppConfig { @Bean public ConversionService conversionService() { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); conversionService.addConverter(new MyCustomConverter()); return conversionService; } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun conversionService(): ConversionService { return DefaultFormattingConversionService().apply { addConverter(MyCustomConverter()) } } } ``` 当`@Value`包含[“spel”表达式](#expressions)时,将在运行时动态计算值,如下例所示: Java ``` @Component public class MovieRecommender { private final String catalog; public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { this.catalog = catalog; } } ``` Kotlin ``` @Component class MovieRecommender( @Value("#{systemProperties['user.catalog'] + 'Catalog' }") private val catalog: String) ``` SPEL 还支持使用更复杂的数据结构: Java ``` @Component public class MovieRecommender { private final Map countOfMoviesPerCatalog; public MovieRecommender( @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; } } ``` Kotlin ``` @Component class MovieRecommender( @Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map) ``` #### 1.9.9.使用`@PostConstruct`和`@PreDestroy` `CommonAnnotationBeanPostProcessor`不仅可以识别`@Resource`注释,还可以识别 JSR-250 生命周期注释:`javax.annotation.PostConstruct`和 `javax.annotation.predestroy’。在 Spring 2.5 中引入的,对这些注释的支持为[初始化回调](#beans-factory-lifecycle-initializingbean)和[销毁回调](#beans-factory-lifecycle-disposablebean)中描述的生命周期回调机制提供了一种替代方案。如果在 Spring `ApplicationContext`中注册了 `CommonAnnotationBeanPostProcessor’,则携带这些注释之一的方法在生命周期的同一点被调用,作为相应的 Spring 生命周期接口方法或显式声明的回调方法。在下面的示例中,缓存在初始化时被预先填充,在销毁时被清除: Java ``` public class CachingMovieLister { @PostConstruct public void populateMovieCache() { // populates the movie cache upon initialization... } @PreDestroy public void clearMovieCache() { // clears the movie cache upon destruction... } } ``` Kotlin ``` class CachingMovieLister { @PostConstruct fun populateMovieCache() { // populates the movie cache upon initialization... } @PreDestroy fun clearMovieCache() { // clears the movie cache upon destruction... } } ``` 有关结合各种生命周期机制的影响的详细信息,请参见[结合生命周期机制](#beans-factory-lifecycle-combined-effects)。 | |和`@Resource`一样,`@PostConstruct`和`@PreDestroy`注释类型也是从 JDK6 到 8 的标准 Java 库中
的一部分。然而,整个`javax.annotation`包在 JDK9 中与核心 Java 模块分离,并最终在
JDK11 中删除。如果需要,现在需要通过 Maven `javax.annotation-api`Central 获得
工件,只需像任何其他库一样添加到应用程序的 Classpath 中。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.10. Classpath 扫描和管理组件 本章中的大多数示例都使用 XML 来指定在 Spring 容器中产生每个`BeanDefinition`的配置元数据。上一节([基于注释的容器配置](#beans-annotation-config))演示了如何通过源级注释提供大量配置元数据。然而,即使在这些示例中,“基本” Bean 定义也是在 XML 文件中明确定义的,而注释仅驱动依赖注入。本节描述了用于通过扫描 Classpath 隐式地检测候选组件的选项。 Bean 候选组件是与筛选条件匹配的类,并且具有与容器注册的相应定义。这消除了使用 XML 来执行 Bean 注册的需要。相反,你可以使用注释(例如,`@Component`)、AspectJ 类型表达式或你自己的自定义筛选条件来选择哪些类具有在容器中注册的 Bean 定义。 | |从 Spring 3.0 开始, Spring JavaConfig 项目提供的许多特性都是
核心 Spring 框架的一部分。这允许你使用 Java 来定义 bean,而不是使用传统的 XML 文件
。看看`@Configuration`、`@Bean`、`@import’和`@DependsOn`注释,以了解如何使用这些新功能。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.10.1.`@Component`和进一步的原型注释 `@Repository`注释是满足存储库角色或原型(也称为数据访问对象或 DAO)的任何类的标记。此标记的用途之一是异常的自动转换,如[异常转换](data-access.html#orm-exception-translation)中所述。 Spring 提供了进一步的原型注释:`@Component`,`@Service`,和 `@controller`。`@Component`是任何 Spring-托管组件的通用原型。@repository`、`@Service`和`@Controller`是`@Component`的专门化,用于更具体的用例(分别在持久性、服务层和表示层中)。因此,你可以使用“@Component”对组件类进行注释,但是,通过使用`@Repository`、`@Service`或`@Controller`对它们进行注释,你的类更适合于通过工具进行处理或与方面进行关联。例如,这些原型注释为切入点提供了理想的目标。`@Repository`、`@Service`和`@Controller`还可以在 Spring 框架的未来版本中携带额外的语义。因此,如果你在使用`@Component`或`@Service`作为服务层时进行选择,`@Service`显然是更好的选择。类似地,如前所述,`@Repository`已经被支持作为持久性层中自动异常转换的标记。 #### 1.10.2.使用元注释和组合注释 Spring 提供的许多注释可以在你自己的代码中用作元注释。元注释是一种可以应用于另一种注释的注释。例如,所提到的`@Service`注释[earlier](#beans-stereotype-annotations)被元注释为`@Component`,如下例所示: Java ``` @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component (1) public @interface Service { // ... } ``` |**1**|`@Component`导致`@Service`被以与`@Component`相同的方式处理。| |-----|---------------------------------------------------------------------------------| Kotlin ``` @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @Component (1) annotation class Service { // ... } ``` |**1**|`@Component`导致`@Service`被以与`@Component`相同的方式处理。| |-----|---------------------------------------------------------------------------------| 你还可以合并元注释来创建“组合注释”。例如,来自 Spring MVC 的`@RestController`注释由`@Controller`和 `@responsebody’组成。 此外,合成注释可以选择从元注释中重新声明属性以允许定制。当你只想公开元注释属性的一个子集时,这可能特别有用。例如, Spring 的“@SessionScope”注释将作用域名称硬编码为`session`,但仍然允许自定义`proxyMode`。下面的清单显示了“SessionScope”注释的定义: Java ``` @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Scope(WebApplicationContext.SCOPE_SESSION) public @interface SessionScope { /** * Alias for {@link Scope#proxyMode}. *

Defaults to {@link ScopedProxyMode#TARGET_CLASS}. */ @AliasFor(annotation = Scope.class) ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; } ``` Kotlin ``` @Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented @Scope(WebApplicationContext.SCOPE_SESSION) annotation class SessionScope( @get:AliasFor(annotation = Scope::class) val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS ) ``` 然后,你可以使用`@SessionScope`,而不声明`proxyMode`,如下所示: Java ``` @Service @SessionScope public class SessionScopedService { // ... } ``` Kotlin ``` @Service @SessionScope class SessionScopedService { // ... } ``` 还可以重写`proxyMode`的值,如下例所示: Java ``` @Service @SessionScope(proxyMode = ScopedProxyMode.INTERFACES) public class SessionScopedUserService implements UserService { // ... } ``` Kotlin ``` @Service @SessionScope(proxyMode = ScopedProxyMode.INTERFACES) class SessionScopedUserService : UserService { // ... } ``` 有关更多详细信息,请参见[Spring Annotation Programming Model](https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model)维基页面。 #### 1.10.3.自动检测类并注册 Bean 定义 Spring 可以自动地检测到原型类并用`ApplicationContext`注册相应的 `BeanDefinition’实例。例如,以下两个类可以进行这种自动检测: Java ``` @Service public class SimpleMovieLister { private MovieFinder movieFinder; public SimpleMovieLister(MovieFinder movieFinder) { this.movieFinder = movieFinder; } } ``` Kotlin ``` @Service class SimpleMovieLister(private val movieFinder: MovieFinder) ``` Java ``` @Repository public class JpaMovieFinder implements MovieFinder { // implementation elided for clarity } ``` Kotlin ``` @Repository class JpaMovieFinder : MovieFinder { // implementation elided for clarity } ``` 要自动检测这些类并注册相应的 bean,你需要在`@Configuration`类中添加 `@ComponentScan`,其中`basePackages`属性是这两个类的公共父包。(或者,你可以指定一个逗号或分号或空格分隔的列表,其中包括每个类的父包。 Java ``` @Configuration @ComponentScan(basePackages = "org.example") public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = ["org.example"]) class AppConfig { // ... } ``` | |为了简洁起见,前面的示例可以使用`value`注释(即`@ComponentScan("org.example")`)的`value`属性。| |---|------------------------------------------------------------------------------------------------------------------------------------------| 以下替代方案使用 XML: ``` ``` | |使用``隐式启用了 `` 的功能。在使用``时,通常不需要包含 `` 元素。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |Classpath 包的扫描需要在 Classpath 中存在相应的目录
条目。当你使用 Ant 构建 JAR 时,请确保没有
激活 jar 任务的仅文件开关。另外,在某些环境中,基于安全策略,目录可能不会
公开,例如,在
jdk1.7.0\_45 或更高版本上的独立应用程序(这需要在你的清单中进行“信任库”设置——参见[https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources](https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)),
在 JDK9 的模块路径(拼图)上
, Spring 的 Classpath 扫描通常按预期工作。
但是,请确保你的组件类在`module-info`描述符中导出。如果你希望 Spring 调用类的非公共成员,请确保
它们是“打开的”(即它们在你的`opens`描述符中使用`module-info`声明,而不是 `exports’声明)。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 此外,当你使用 Component-Scan 元素时,`AutowiredAnnotationBeanPostProcessor`和 `CommonAnnotationBeanPostProcessor’都是隐式包含的。这意味着这两个组件是自动检测和连接在一起的——所有这些都没有 XML 提供的任何 Bean 配置元数据。 | |你可以禁用`AutowiredAnnotationBeanPostProcessor`和 `commonAnnotationBeanPostProcessor’的注册,方法是使用`annotation-config`属性
,其值为`false`。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.10.4.使用筛选器自定义扫描 默认情况下,使用`@Component`、`@Repository`、`@Service`、`@Controller`、`@configuration’或自身使用`@Component`进行注释的自定义注释的类是唯一检测到的候选组件。但是,你可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为`includeFilters`或`excludeFilters`注释的属性(或在 XML 配置中添加``或<``元素的子元素)。每个筛选器元素都需要`type`和`expression`属性。下表介绍了筛选选项: | Filter Type | Example Expression |说明| |--------------------|----------------------------|------------------------------------------------------------------------------------------| |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`注释并使用“存根”存储库的配置: Java ``` @Configuration @ComponentScan(basePackages = "org.example", includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"), excludeFilters = @Filter(Repository.class)) public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = "org.example", includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])], excludeFilters = [Filter(Repository::class)]) class AppConfig { // ... } ``` 下面的清单显示了等效的 XML: ``` ``` | |你还可以通过在
注释上设置`useDefaultFilters=false`或通过提供`use-default-filters="false"`作为 `` 元素的属性来禁用默认过滤器。这有效地禁用了对带有
注释或 meta 注释的类`@Component`、`@Repository`、`@Service`、`@Controller`、`@restcontroller` 或`@Configuration`的自动检测。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.10.5.在组件中定义 Bean 元数据 Spring 组件还可以向容器贡献 Bean 定义元数据。你可以使用与`@Bean`注释类中定义 Bean 元数据相同的`@Configuration`注释来完成此操作。下面的示例展示了如何做到这一点: 爪哇 ``` @Component public class FactoryMethodComponent { @Bean @Qualifier("public") public TestBean publicInstance() { return new TestBean("publicInstance"); } public void doWork() { // Component method implementation omitted } } ``` Kotlin ``` @Component class FactoryMethodComponent { @Bean @Qualifier("public") fun publicInstance() = TestBean("publicInstance") fun doWork() { // Component method implementation omitted } } ``` 前面的类是一个 Spring 组件,在其“dowork()”方法中具有特定于应用程序的代码。然而,它还贡献了 Bean 定义,该定义具有引用方法`publicInstance()`的工厂方法。`@Bean`注释标识了工厂方法和其他 Bean 定义属性,例如通过`@Qualifier`注释的限定符值。可以指定的其他方法级注释是 `@scope’、`@Lazy`和自定义限定符注释。 | |除了组件初始化的作用外,还可以将`@Lazy`注释放置在标记有`@Autowired`或`@Inject`的注入点上。在这种情况下,
将导致注入一个延迟分辨率代理。然而,这样的代理方法
是相当有限的。对于复杂的惰性交互,特别是结合
和可选依赖项的情况,我们建议使用`ObjectProvider`代替。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 正如前面所讨论的,支持自动连线字段和方法,同时还支持`@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); } } ``` Kotlin ``` @Component class FactoryMethodComponent { companion object { private var i: Int = 0 } @Bean @Qualifier("public") fun publicInstance() = TestBean("publicInstance") // use of a custom qualifier and autowiring of method parameters @Bean protected fun protectedInstance( @Qualifier("public") spouse: TestBean, @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { this.spouse = spouse this.country = country } @Bean private fun privateInstance() = TestBean("privateInstance", i++) @Bean @RequestScope fun requestScopedInstance() = TestBean("requestScopedInstance", 3) } ``` 该示例将`String`方法参数`country`自动连接到另一个名为`privateInstance`的 Bean 上的`age`属性的值。 Spring 表达式语言元素通过记号`#{ }`来定义该属性的值。对于`@Value`注释,表达式解析程序预先配置为在解析表达式文本时查找 Bean 名称。 在 Spring Framework4.3 中,还可以声明类型为 `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()); } } ``` Kotlin ``` @Component class FactoryMethodComponent { @Bean @Scope("prototype") fun prototypeInstance(injectionPoint: InjectionPoint) = TestBean("prototypeInstance for ${injectionPoint.member}") } ``` 普通 Spring 组件中的`@Bean`方法的处理方式与 Spring `@Configuration`类中的方法的处理方式不同。不同之处在于,`@Component`类不会用 CGlib 进行增强,以拦截方法和字段的调用。CGLIB 代理是在`@Configuration`中调用`@Bean`方法中的方法或字段创建 Bean 对协作对象的元数据引用的一种方法。这样的方法不是用普通的 爪哇 语义来调用的,而是通过容器来提供 Spring bean 的通常的生命周期管理和代理,即使是通过对`@Bean`方法的编程调用来引用其他 bean 时也是如此。相反,在普通的`@Component`类中调用`@Bean`方法中的方法或字段具有标准的 爪哇 语义,没有应用特殊的 CGlib 处理或其他约束。 | |你可以将`@Bean`方法声明为`static`,这样就可以调用它们,而不需要将其包含的配置类创建为实例。在定义后处理器 bean(例如,类型`BeanFactoryPostProcessor`或`BeanPostProcessor`)时,这使得
具有特殊意义,因为这样的 bean 在容器
生命周期的早期就被初始化了,并且应该避免在那个时间点触发配置的其他部分,

对静态`@Bean`方法的调用永远不会被容器拦截,甚至在 `@configuration’类中也不会被拦截,(如本节前面所述),由于技术
的限制:CGLIB 子类只能覆盖非静态方法。因此,
直接调用另一个`@Bean`方法具有标准的 爪哇 语义,结果
在一个独立的实例中被直接从工厂方法本身返回。

`@Bean`方法的 爪哇 语言可见性对
Spring 容器中的结果 Bean 定义没有立即的影响。你可以自由地声明你的
工厂方法,这在非 `@configuration’类中是合适的,在任何地方也适用于静态
方法。但是,`@Bean`类中的常规`@Configuration`方法需要
才能被重写——也就是说,它们不能被声明为`private`或`final`。

@ Bean ` 方法也可以在给定组件的基类上发现,或者
配置类,以及在接口
中声明的 8 个默认方法上由组件或配置类实现。这允许在组成复杂的配置安排时有很多
的灵活性,即使是多个
继承也可以通过 爪哇8 的默认方法实现,截至 Spring 4.2,

最后,对于相同的
Bean,单个类可以容纳多个`@Bean`方法,根据运行时可用的
依赖关系,作为使用的多个工厂方法的安排。这与在其他配置场景中选择“greediest”
构造函数或工厂方法的算法相同:具有
的变量在构造时选择最大数量的可满足依赖项,
类似于容器如何在多个`@Autowired`构造函数之间进行选择。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.10.6.命名自动检测到的组件 当组件作为扫描过程的一部分被自动检测时,其名称由该扫描仪已知的`BeanNameGenerator`策略生成。默认情况下,任何包含名称`value`的 Spring 原型注释(`@component`,`@Repository`,`@Service`,和 `@controller`)都将该名称提供给相应的 Bean 定义。 如果这样的注释不包含名称`value`或任何其他检测到的组件(例如由自定义过滤器发现的组件),则默认的 Bean Name Generator 返回未大写的非限定类名称。例如,如果检测到以下组件类,则名称将为`myMovieLister`和`movieFinderImpl`: 爪哇 ``` @Service("myMovieLister") public class SimpleMovieLister { // ... } ``` Kotlin ``` @Service("myMovieLister") class SimpleMovieLister { // ... } ``` 爪哇 ``` @Repository public class MovieFinderImpl implements MovieFinder { // ... } ``` Kotlin ``` @Repository class MovieFinderImpl : MovieFinder { // ... } ``` 如果不想依赖默认的 Bean 命名策略,则可以提供自定义的 Bean 命名策略。首先,实现[“BeannameGenerator”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/support/BeanNameGenerator.html)接口,并确保包含一个默认的无参数构造函数。然后,在配置扫描仪时提供完全限定的类名,如下面的示例注释和 Bean 定义所示。 | |如果由于多个具有
相同的非限定类名称(即名称相同但位于
不同包中的类)的自动检测组件而导致命名冲突,则可能需要为生成的 Bean 名称配置一个`BeanNameGenerator`默认为
完全限定类名称的`BeanNameGenerator`类。截至 Spring 框架 5.2.3,位于包 `org.SpringFramework.Context.Annotation’中的 `FullyQualifiedAnnotationBeannameGenerator’可用于此目的。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 爪哇 ``` @Configuration @ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class) public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class) class AppConfig { // ... } ``` ``` ``` 作为一条一般规则,当其他组件可能对其进行显式引用时,可以考虑使用注释来指定名称。另一方面,只要容器负责布线,自动生成的名称就足够了。 #### 1.10.7.为自动检测的组件提供一个范围 与一般的 Spring-管理组件一样,自动检测组件的默认和最常见的作用域是`singleton`。然而,有时你需要一个不同的作用域,该作用域可以由`@Scope`注释指定。你可以在注释中提供作用域的名称,如下例所示: 爪哇 ``` @Scope("prototype") @Repository public class MovieFinderImpl implements MovieFinder { // ... } ``` Kotlin ``` @Scope("prototype") @Repository class MovieFinderImpl : MovieFinder { // ... } ``` | |`@Scope`注释仅在具体的 Bean 类(对于注释的
组件)或工厂方法(对于`@Bean`方法)上进行内省。与 XML Bean
定义相反,没有 Bean 定义继承的概念,并且在类级别上的继承
层次结构对于元数据目的是无关的。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 有关 Spring 上下文中的“请求”或“会话”等特定于 Web 的作用域的详细信息,请参见[Request, Session, Application, and WebSocket Scopes](#beans-factory-scopes-other)。与这些作用域的预构建注释一样,你也可以使用 Spring 的元注释方法来编写自己的范围注释:例如,使用`@Scope("prototype")`进行自定义注释的元注释,也可能声明自定义范围代理模式。 | |为了提供用于范围解析的自定义策略,而不是依赖于
基于注释的方法,你可以实现[ScopeMetaDataResolver](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/ScopeMetadataResolver.html)接口。一定要包含一个默认的无参数构造函数。然后可以在配置扫描仪时提供
完全限定的类名,如以下
注释和 Bean 定义的示例所示:| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 爪哇 ``` @Configuration @ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class) public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class) class AppConfig { // ... } ``` ``` ``` 当使用某些非单例作用域时,可能需要为作用域对象生成代理。推理在[作为依赖项的作用域 bean](#beans-factory-scopes-other-injection)中进行了描述。为此,在 Component-Scan 元素上提供了一个作用域-Proxy 属性。这三个可能的值是:`no`,`interfaces`,和`targetClass`。例如,以下配置将生成标准的 JDK 动态代理: 爪哇 ``` @Configuration @ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES) public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES) class AppConfig { // ... } ``` ``` ``` #### 1.10.8.提供带有注释的限定符元数据 `@Qualifier`注释在[使用限定符对基于注释的自动连线进行微调](#beans-autowired-annotation-qualifiers)中讨论。该部分中的示例演示了在解析 AutoWire 候选项时使用`@Qualifier`注释和自定义限定符注释来提供细粒度的控制。因为这些示例是基于 XML Bean 定义的,所以通过在 XML 中使用`qualifier`或`meta`元素中的`bean`子元素,在候选 Bean 定义上提供了限定符元数据。当依赖 Classpath 扫描来自动检测组件时,你可以在候选类上为限定符元数据提供类型级别的注释。以下三个示例演示了这种技术: 爪哇 ``` @Component @Qualifier("Action") public class ActionMovieCatalog implements MovieCatalog { // ... } ``` Kotlin ``` @Component @Qualifier("Action") class ActionMovieCatalog : MovieCatalog ``` 爪哇 ``` @Component @Genre("Action") public class ActionMovieCatalog implements MovieCatalog { // ... } ``` Kotlin ``` @Component @Genre("Action") class ActionMovieCatalog : MovieCatalog { // ... } ``` 爪哇 ``` @Component @Offline public class CachingMovieCatalog implements MovieCatalog { // ... } ``` Kotlin ``` @Component @Offline class CachingMovieCatalog : MovieCatalog { // ... } ``` | |与大多数基于注释的替代方法一样,请记住注释元数据是
绑定到类定义本身的,而使用 XML 允许多个相同类型的 bean
在其限定符元数据中提供变体,因为
元数据是按实例而不是按类提供的。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.10.9.生成候选组件的索引 Classpath 虽然扫描非常快,但通过在编译时创建候选的静态列表,可以提高大型应用程序的启动性能。在这种模式下,所有组件扫描的目标模块都必须使用这种机制。 | |你现有的`@ComponentScan`或``指令必须保持
不变,以请求上下文扫描某些包中的候选项。当“ApplicationContext”检测到这样的索引时,它会自动使用它,而不是扫描
Classpath。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 要生成索引,请向每个包含组件的模块添加一个附加依赖项,这些组件是组件扫描指令的目标。下面的示例展示了如何使用 Maven 来实现这一点: ``` org.springframework spring-context-indexer 5.3.16 true ``` 对于 Gradle 4.5 或更早的版本,应该在`compileOnly`配置中声明依赖项,如下例所示: ``` dependencies { compileOnly "org.springframework:spring-context-indexer:5.3.16" } ``` 对于 Gradle 4.6 及更高版本,依赖关系应该在`annotationProcessor`配置中声明,如以下示例所示: ``` dependencies { annotationProcessor "org.springframework:spring-context-indexer:5.3.16" } ``` `spring-context-indexer`工件生成一个`META-INF/spring.components`文件,该文件包含在 jar 文件中。 | |在 IDE 中使用此模式时,`spring-context-indexer`必须将
注册为注释处理器,以确保在更新
候选组件时索引是最新的。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |当在 Classpath 上找到`META-INF/spring.components`文件
时,将自动启用索引。如果对于某些库(或用例)
有部分可用的索引,但无法为整个应用程序构建索引,则可以通过将
设置为 `true’,返回到常规的 Classpath
安排(就像根本没有索引一样),可以作为 JVM 系统属性,也可以通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.11.使用 JSR330 标准注释 从 Spring 3.0 开始, Spring 提供了对 JSR-330 标准注释(依赖注入)的支持。以与 Spring 注释相同的方式扫描这些注释。要使用它们,你需要在你的 Classpath 中有相关的罐子。 | |如果你使用 Maven,则`javax.inject`工件在标准 Maven
存储库([https://repo1.maven.org/maven2/javax/inject/javax.inject/1/](https://repo1.maven.org/maven2/javax/inject/javax.inject/1/))中可用。
你可以将以下依赖项添加到你的文件 POM.xml:




javax=2583”/>”javav=2587“/>”>“>”gt="gt=2587| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.11.1.依赖注入与`@Inject`和`@Named` 而不是`@Autowired`,你可以使用`@javax.inject.Inject`,如下所示: 爪哇 ``` import javax.inject.Inject; public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } public void listMovies() { this.movieFinder.findMovies(...); // ... } } ``` Kotlin ``` import javax.inject.Inject class SimpleMovieLister { @Inject lateinit var movieFinder: MovieFinder fun listMovies() { movieFinder.findMovies(...) // ... } } ``` 与`@Autowired`一样,你可以在字段级、方法级和构造函数参数级使用`@Inject`。此外,你可以将注入点声明为“provider”,允许按需访问较短范围的 bean,或者通过`Provider.get()`调用延迟访问其他 bean。下面的示例提供了前面示例的一个变体: 爪哇 ``` import javax.inject.Inject; import javax.inject.Provider; public class SimpleMovieLister { private Provider movieFinder; @Inject public void setMovieFinder(Provider movieFinder) { this.movieFinder = movieFinder; } public void listMovies() { this.movieFinder.get().findMovies(...); // ... } } ``` Kotlin ``` import javax.inject.Inject class SimpleMovieLister { @Inject lateinit var movieFinder: MovieFinder fun listMovies() { movieFinder.findMovies(...) // ... } } ``` 如果你希望为应该注入的依赖项使用限定名称,那么你应该使用`@Named`注释,如下例所示: 爪哇 ``` import javax.inject.Inject; import javax.inject.Named; public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(@Named("main") MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` import javax.inject.Inject import javax.inject.Named class SimpleMovieLister { private lateinit var movieFinder: MovieFinder @Inject fun setMovieFinder(@Named("main") movieFinder: MovieFinder) { this.movieFinder = movieFinder } // ... } ``` 与`@Autowired`一样,`@Inject`也可以与`java.util.Optional`或 `@nullable’一起使用。这在这里甚至更适用,因为`@Inject`不具有`required`属性。以下两个示例展示了如何使用`@Inject`和 `@nullable’: ``` public class SimpleMovieLister { @Inject public void setMovieFinder(Optional movieFinder) { // ... } } ``` 爪哇 ``` public class SimpleMovieLister { @Inject public void setMovieFinder(@Nullable MovieFinder movieFinder) { // ... } } ``` Kotlin ``` class SimpleMovieLister { @Inject var movieFinder: MovieFinder? = null } ``` #### 1.11.2.`@Named`和`@ManagedBean`:`@Component`注释的标准等价物 而不是`@Component`,你可以使用`@javax.inject.Named`或`javax.annotation.ManagedBean`,如下例所示: 爪哇 ``` import javax.inject.Inject; import javax.inject.Named; @Named("movieListener") // @ManagedBean("movieListener") could be used as well public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` import javax.inject.Inject import javax.inject.Named @Named("movieListener") // @ManagedBean("movieListener") could be used as well class SimpleMovieLister { @Inject lateinit var movieFinder: MovieFinder // ... } ``` 在不指定组件名称的情况下使用`@Component`是非常常见的。@named 可以以类似的方式使用,如下例所示: 爪哇 ``` import javax.inject.Inject; import javax.inject.Named; @Named public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } ``` Kotlin ``` import javax.inject.Inject import javax.inject.Named @Named class SimpleMovieLister { @Inject lateinit var movieFinder: MovieFinder // ... } ``` 当你使用`@Named`或`@ManagedBean`时,你可以使用组件扫描,其方式与使用 Spring 注释时的方式完全相同,如下例所示: 爪哇 ``` @Configuration @ComponentScan(basePackages = "org.example") public class AppConfig { // ... } ``` Kotlin ``` @Configuration @ComponentScan(basePackages = ["org.example"]) class AppConfig { // ... } ``` | |与`@Component`相反,JSR-330`@Named`和 JSR-250`@ManagedBean`注释是不可组合的。你应该使用 Spring 的原型模型来构建
自定义组件注释。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.11.3.JSR-330 标准注释的局限性 当你使用标准注释时,你应该知道一些重要的特性是不可用的,如下表所示: | Spring | javax.inject.\* |javax.inject 限制/注释| |-------------------|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | @Autowired | @Inject |`@Inject`没有“required”属性。可以与 爪哇8 的`Optional`一起使用。| | @Component |@Named / @ManagedBean|JSR-330 不提供可组合的模型,只提供一种识别命名组件的方法。| |@Scope("singleton")| @Singleton |JSR-330 缺省作用域类似于 Spring 的`prototype`。然而,为了使其
与 Spring 的一般默认值保持一致,在 Spring
容器中声明的 JSR-330 Bean 默认为`singleton`。为了使用`singleton`、
以外的作用域,你应该使用 Spring 的`@Scope`注释。`javax.inject`还提供了[@Scope](https://download.oracle.com/javaee/6/api/javax/inject/Scope.html)注释。
尽管如此,这个注释仅用于创建你自己的注释。| | @Qualifier | @Qualifier / @Named |`javax.inject.Qualifier`只是构建自定义限定符的元注释。
具体`String`限定符(就像 Spring 的`@Qualifier`与一个值)可以关联
到`javax.inject.Named`。| | @Value | \- |没有等效的| | @Required | \- |没有等效的| | @Lazy | \- |没有等效的| | ObjectFactory | Provider |`javax.inject.Provider`是 Spring 的`ObjectFactory`,
的直接替代方法,只使用更短的`get()`方法名。它也可以与
Spring 的`@Autowired`结合使用,或者与非注释的构造函数和 setter 方法结合使用。| ### 1.12.基于 爪哇 的容器配置 本节介绍如何在 爪哇 代码中使用注释来配置 Spring 容器。它包括以下主题: * [Basic Concepts: `@Bean` and `@Configuration`](#beans-java-basic-concepts) * [Instantiating the Spring Container by Using `AnnotationConfigApplicationContext`](#beans-java-instantiating-container) * [Using the `@Bean` Annotation](#beans-java-bean-annotation) * [Using the `@Configuration` annotation](#beans-java-configuration-annotation) * [编写基于 爪哇 的配置](#beans-java-composing-configuration-classes) * [Bean Definition Profiles](#beans-definition-profiles) * [“PropertySource”抽象](#beans-property-source-abstraction) * [Using `@PropertySource`](#beans-using-propertysource) * [语句中的占位符解析](#beans-placeholder-resolution-in-statements) #### 1.12.1.基本概念:`@Bean`和`@Configuration` Spring 新的 爪哇-Configuration 支持中的核心构件是“@configuration”-带注释的类和`@Bean`-带注释的方法。 `@Bean`注释用于指示方法实例化、配置和初始化一个新对象,该对象将由 Spring IOC 容器管理。对于那些熟悉 Spring 的``XML 配置的人来说,`@Bean`注释所起的作用与``元素相同。你可以使用`@Bean`-带注释的方法处理任何 Spring `@component`。然而,它们最常用于`@Configuration`bean。 用`@Configuration`注释一个类表明,它的主要目的是作为 Bean 定义的来源。此外,`@Configuration`类通过调用同一类中的其他`@Bean`方法来定义 Bean 之间的依赖关系。最简单的`@Configuration`类如下: 爪哇 ``` @Configuration public class AppConfig { @Bean public MyService myService() { return new MyServiceImpl(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun myService(): MyService { return MyServiceImpl() } } ``` 前面的`AppConfig`类等价于下面的 Spring ``XML: ``` ``` 完整的 @ 配置 VS“Lite”@ Bean 模式? 当`@Bean`方法在没有使用 `@Configuration’注释的类中声明时,它们被称为正在以“精简”模式进行处理。 Bean 在`@Component`中声明的方法,甚至在一个普通的旧类中声明的方法,都被认为是“lite”,具有不同的包含类的主要目的,而`@Bean`方法是那里的一种奖励。例如,服务组件可以在每个适用的组件类上通过一个额外的`@Bean`方法向容器公开管理视图。在这样的场景中,`@Bean`方法是一种通用的工厂方法机制。 与 full`@Configuration`不同,lite`@Bean`方法不能声明 Bean 之间的依赖关系。相反,它们对包含它们的组件的内部状态进行操作,并可选地对它们可能声明的参数进行操作。因此,这样的`@Bean`方法不应该调用其他 `@ Bean ` 方法。每个这样的方法实际上只是用于特定 Bean 引用的工厂方法,没有任何特殊的运行时语义。这里的积极的副作用是,在运行时不需要应用 CGLIB 子类,因此在类设计方面没有限制(即,包含的类可能是`final`等)。 在常见的场景中,`@Bean`方法要在`@Configuration`类中声明,以确保始终使用“完全”模式,并确保跨方法引用因此被重定向到容器的生命周期管理。这可以防止通过常规的 爪哇 调用意外调用相同的“@ Bean”方法,这有助于减少在“精简”模式下操作时很难追踪到的细微错误。 下面的部分将深入讨论`@Bean`和`@Configuration`注释。然而,首先,我们介绍了通过使用基于 爪哇 的配置来创建 Spring 容器的各种方法。 #### 1.12.2.使用`AnnotationConfigApplicationContext`### 实例化 Spring 容器 下面的部分记录了 Spring 的`AnnotationConfigApplicationContext`,在 Spring 3.0 中介绍了它。这种通用的`ApplicationContext`实现不仅能够接受 `@Configuration’类作为输入,而且还能够接受用 JSR-330 元数据注释的普通`@Component`类和类。 当`@Configuration`类被提供为输入时,`@Configuration`类本身被注册为 Bean 定义,并且类中所有声明的`@Bean`方法也被注册为 Bean 定义。 当`@Component`和 JSR-330 类被提供时,它们被注册为 Bean 定义,并且假定在必要的情况下,在那些类中使用诸如`@Autowired`或`@Inject`的 DI 元数据。 ##### 简单的构造 就像 Spring XML 文件在实例化 `ClassPathXMLApplicationContext’时用作输入一样,在实例化`@Configuration`时,可以使用`AnnotationConfigApplicationContext`类作为输入。这允许完全无 XML 地使用 Spring 容器,如下例所示: 爪哇 ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = AnnotationConfigApplicationContext(AppConfig::class.java) val myService = ctx.getBean() myService.doStuff() } ``` 如前所述,`AnnotationConfigApplicationContext`并不限于仅与`@Configuration`类一起工作。任何`@Component`或 JSR-330 注释类都可以作为构造函数的输入提供,如下例所示: 爪哇 ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = AnnotationConfigApplicationContext(MyServiceImpl::class.java, Dependency1::class.java, Dependency2::class.java) val myService = ctx.getBean() myService.doStuff() } ``` 前面的示例假设`MyServiceImpl`、`Dependency1`和`Dependency2`使用 Spring 依赖注入注释,例如`@Autowired`。 ##### 通过使用`register(Class…​)`#### 以编程方式构建容器 你可以使用 no-arg 构造函数实例化`AnnotationConfigApplicationContext`,然后使用`register()`方法对其进行配置。当以编程方式构建`AnnotationConfigApplicationContext`时,这种方法特别有用。下面的示例展示了如何做到这一点: 爪哇 ``` public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(AppConfig.class, OtherConfig.class); ctx.register(AdditionalConfig.class); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); myService.doStuff(); } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = AnnotationConfigApplicationContext() ctx.register(AppConfig::class.java, OtherConfig::class.java) ctx.register(AdditionalConfig::class.java) ctx.refresh() val myService = ctx.getBean() myService.doStuff() } ``` ##### 使用`scan(String…​)`启用组件扫描 要启用组件扫描,你可以对`@Configuration`类作如下注释: 爪哇 ``` @Configuration @ComponentScan(basePackages = "com.acme") (1) public class AppConfig { // ... } ``` |**1**|此注释使组件扫描成为可能。| |-----|-------------------------------------------| Kotlin ``` @Configuration @ComponentScan(basePackages = ["com.acme"]) (1) class AppConfig { // ... } ``` |**1**|此注释使组件扫描成为可能。| |-----|-------------------------------------------| | |有经验的 Spring 用户可能熟悉来自
Spring 的`context:`名称空间的等效 XML 声明,如以下示例所示:




<>>>>
<2722"| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在前面的示例中,扫描`com.acme`包以查找任何 `@component’-注释的类,并且这些类被注册为容器内的 Spring Bean 定义。`AnnotationConfigApplicationContext`公开了 `scan’方法,以允许相同的组件扫描功能,如下例所示: 爪哇 ``` public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.scan("com.acme"); ctx.refresh(); MyService myService = ctx.getBean(MyService.class); } ``` Kotlin ``` fun main() { val ctx = AnnotationConfigApplicationContext() ctx.scan("com.acme") ctx.refresh() val myService = ctx.getBean() } ``` | |请记住,`@Configuration`类是[meta-annotated](#beans-meta-annotations)和`@Component`类,因此它们是组件扫描的候选对象。在前面的示例中,假设
在`com.acme`包(或下面的任何包
)中声明了`AppConfig`,则在调用`scan()`时将其拾取。在`refresh()`时,其所有`@Bean`方法都被处理并注册为容器内的 Bean 定义。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 对`AnnotationConfigWebApplicationContext`#### 的 Web 应用程序的支持 一个`WebApplicationContext`的`AnnotationConfigApplicationContext`变种可与`AnnotationConfigWebApplicationContext`一起使用。你可以在配置 Spring `ContextLoaderListener` Servlet 侦听器、 Spring MVC`DispatcherServlet’等时使用此实现。下面的`web.xml`片段配置了典型的 Spring MVC Web 应用程序(请注意使用`contextClass`context-param 和 init-param): ``` contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation com.acme.AppConfig org.springframework.web.context.ContextLoaderListener dispatcher org.springframework.web.servlet.DispatcherServlet contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation com.acme.web.MvcConfig dispatcher /app/* ``` | |对于编程用例,`GenericWebApplicationContext`可以用作
的`AnnotationConfigWebApplicationContext`的替代。有关详细信息,请参见[GenericWebApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/GenericWebApplicationContext.html)爪哇doc。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.12.3.使用`@Bean`注释 `@Bean`是一个方法级的注释,是 XML``元素的直接模拟。该注释支持``提供的一些属性,例如: * [init-method](#beans-factory-lifecycle-initializingbean) * [destroy-method](#beans-factory-lifecycle-disposablebean) * [autowiring](#beans-factory-autowire) * `name`. 你可以在`@Configuration`-注释的类中使用`@Bean`注释或在 `@component`-注释的类中使用`@Bean`注释。 ##### 宣告 A Bean 要声明 Bean,你可以使用`@Bean`注释对方法进行注释。你可以使用此方法在`ApplicationContext`中注册一个 Bean 定义,该定义的类型指定为方法的返回值。默认情况下, Bean 名称与方法名称相同。下面的示例显示了`@Bean`方法声明: 爪哇 ``` @Configuration public class AppConfig { @Bean public TransferServiceImpl transferService() { return new TransferServiceImpl(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun transferService() = TransferServiceImpl() } ``` 前面的配置与下面的 Spring XML 完全等价: ``` ``` 这两个声明使得名为`transferService`的 Bean 在“ApplicationContext”中可用,并绑定到类型`TransferServiceImpl`的对象实例,如以下文本图像所示: ``` transferService -> com.acme.TransferServiceImpl ``` 你也可以使用默认方法来定义 bean。这允许通过在默认方法上实现带有 Bean 定义的接口来组合 Bean 配置。 爪哇 ``` public interface BaseConfig { @Bean default TransferServiceImpl transferService() { return new TransferServiceImpl(); } } @Configuration public class AppConfig implements BaseConfig { } ``` 你还可以使用接口(或基类)返回类型声明你的`@Bean`方法,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean public TransferService transferService() { return new TransferServiceImpl(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun transferService(): TransferService { return TransferServiceImpl() } } ``` 但是,这将预先类型预测的可见性限制为指定的接口类型(“TransferService”)。然后,只在受影响的单例 Bean 被实例化之后,使用容器已知的完整类型。非惰性单例 bean 将根据其声明顺序进行实例化,因此你可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试使用未声明的类型进行匹配(例如`@Autowired TransferServiceImpl`,它仅在`transferService` Bean 被实例化之后进行解析)。 | |如果你始终使用声明的服务接口来引用你的类型,那么你的“@ Bean”返回类型可以安全地加入该设计决策。但是,对于实现多个接口的组件
,或者对于由其
实现类型可能引用的组件,更安全的做法是声明尽可能具体的返回类型
(至少与引用你的 Bean 的注入点所要求的特定类型一样)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### Bean 相依性 带注释的`@Bean`方法可以具有任意数量的参数,这些参数描述了构建 Bean 所需的依赖关系。例如,如果我们的`TransferService`需要`AccountRepository`,则可以使用方法参数实现该依赖关系,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun transferService(accountRepository: AccountRepository): TransferService { return TransferServiceImpl(accountRepository) } } ``` 解析机制与基于构造函数的依赖注入几乎相同。有关更多详细信息,请参见[有关部分](#beans-constructor-injection)。 ##### 接收生命周期回调 使用`@Bean`注释定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的`@PostConstruct`和`@PreDestroy`注释。详情见[JSR-250 注解](#beans-postconstruct-and-predestroy-annotations)。 常规的 Spring [lifecycle](#beans-factory-nature)回调也完全支持。如果 Bean 实现了`InitializingBean`、`DisposableBean`或`Lifecycle`,则容器调用它们各自的方法。 标准的`*Aware`接口集合(如[BeanFactoryAware](#beans-beanfactory),[BeanNameAware](#beans-factory-aware),[MessagesourceAware](#context-functionality-messagesource),[应用情境软件](#beans-factory-aware),等等)也是完全支持的。 `@Bean`注释支持指定任意的初始化和销毁回调方法,很像 Spring XML 的`init-method`和`destroy-method`元素上的属性,如下例所示: 爪哇 ``` public class BeanOne { public void init() { // initialization logic } } public class BeanTwo { public void cleanup() { // destruction logic } } @Configuration public class AppConfig { @Bean(initMethod = "init") public BeanOne beanOne() { return new BeanOne(); } @Bean(destroyMethod = "cleanup") public BeanTwo beanTwo() { return new BeanTwo(); } } ``` Kotlin ``` class BeanOne { fun init() { // initialization logic } } class BeanTwo { fun cleanup() { // destruction logic } } @Configuration class AppConfig { @Bean(initMethod = "init") fun beanOne() = BeanOne() @Bean(destroyMethod = "cleanup") fun beanTwo() = BeanTwo() } ``` | |默认情况下,使用 爪哇 配置定义的具有公共`close`或`shutdown`方法的 bean 将自动加入销毁回调。如果你有一个公共的 `close’或`shutdown`方法,并且不希望在容器
关闭时调用它,你可以将`@Bean(destroyMethod="")`添加到你的 Bean 定义中,以禁用
默认`(inferred)`模式。

对于你通过 JNDI 获得的资源,你可能希望在默认情况下这样做,因为其
生命周期是在应用程序之外管理的。特别是,对于`DataSource`,要确保始终执行
,因为它在 爪哇 EE 应用程序服务器上是有问题的。

下面的示例展示了如何防止自动销毁回调对于一个“数据源”:

爪哇

`
@ Bean
公共数据源数据源()抛出名称异常
返回 jndiource(“mydr”=“2815”/>“2816”/r=“2817”/>“<<19><<<<<>>>”gt=“<19"r="<19>>通过
使用 Spring 的`JndiTemplate`或`JndiLocatorDelegate`助手或直接使用 jndi`initialcontext` 用法,但不使用`JndiObjectFactoryBean`变体(这将强制
将返回类型声明为`FactoryBean`类型,而不是实际的目标
类型,使得在其他`@Bean`方法中使用交叉引用调用变得更加困难,这些方法
打算在此引用所提供的资源)。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 在上面的例子`BeanOne`的情况下,在构造过程中直接调用`init()`方法将同样有效,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean public BeanOne beanOne() { BeanOne beanOne = new BeanOne(); beanOne.init(); return beanOne; } // ... } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun beanOne() = BeanOne().apply { init() } // ... } ``` | |当你直接在 爪哇 中工作时,你可以对对象执行任何你喜欢的操作,并且执行
,并不总是需要依赖于容器的生命周期。| |---|---------------------------------------------------------------------------------------------------------------------------------------------| ##### 指定 Bean 范围 Spring 包括`@Scope`注释,以便可以指定 Bean 的范围。 ###### 使用`@Scope`注释 你可以指定使用`@Bean`注释定义的 bean 应该具有特定的作用域。你可以使用[Bean Scopes](#beans-factory-scopes)部分中指定的任何标准作用域。 默认的作用域是`singleton`,但是你可以使用`@Scope`注释来覆盖此范围,如下例所示: 爪哇 ``` @Configuration public class MyConfiguration { @Bean @Scope("prototype") public Encryptor encryptor() { // ... } } ``` Kotlin ``` @Configuration class MyConfiguration { @Bean @Scope("prototype") fun encryptor(): Encryptor { // ... } } ``` ###### `@Scope`和`scoped-proxy` Spring 通过[scoped proxies](#beans-factory-scopes-other-injection)提供了一种处理作用域依赖关系的方便方法。在使用 XML 配置时,创建这样的代理的最简单的方法是``元素。使用`@Scope`注释在 爪哇 中配置 bean,可以提供与`proxyMode`属性相同的支持。默认值是`ScopedProxyMode.DEFAULT`,这通常表示除非在组件扫描指令级配置了不同的默认值,否则不应创建范围代理。可以指定 `ScopedProxyMode.target_class`,`ScopedProxyMode.INTERFACES`或`ScopedProxyMode.NO`。 如果你使用 爪哇 将范围代理示例从 XML 引用文档(参见[scoped proxies](#beans-factory-scopes-other-injection))移植到我们的`@Bean`,它类似于以下内容: 爪哇 ``` // an HTTP Session-scoped bean exposed as a proxy @Bean @SessionScope public UserPreferences userPreferences() { return new UserPreferences(); } @Bean public Service userService() { UserService service = new SimpleUserService(); // a reference to the proxied userPreferences bean service.setUserPreferences(userPreferences()); return service; } ``` Kotlin ``` // an HTTP Session-scoped bean exposed as a proxy @Bean @SessionScope fun userPreferences() = UserPreferences() @Bean fun userService(): Service { return SimpleUserService().apply { // a reference to the proxied userPreferences bean setUserPreferences(userPreferences()) } } ``` ##### 自定义 Bean 命名 默认情况下,配置类使用`@Bean`方法的名称作为结果 Bean 的名称。但是,可以使用`name`属性重写此功能,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean("myThing") public Thing thing() { return new Thing(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean("myThing") fun thing() = Thing() } ``` ##### Bean 别名 如[Naming Beans](#beans-beanname)中所讨论的,有时希望给出一个 Bean 多个名称,也称为 Bean 别名。为此目的,`@Bean`注释的`name`属性接受字符串数组。下面的示例显示了如何为 Bean 设置多个别名: 爪哇 ``` @Configuration public class AppConfig { @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"}) public DataSource dataSource() { // instantiate, configure and return DataSource bean... } } ``` Kotlin ``` @Configuration class AppConfig { @Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource") fun dataSource(): DataSource { // instantiate, configure and return DataSource bean... } } ``` ##### Bean 描述 有时,提供对 A Bean 的更详细的文本描述是有帮助的。当公开 bean(可能通过 JMX)以进行监视时,这可能特别有用。 要向`@Bean`添加描述,可以使用[`@Description`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Description.html)注释,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean @Description("Provides a basic example of a bean") public Thing thing() { return new Thing(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean @Description("Provides a basic example of a bean") fun thing() = Thing() } ``` #### 1.12.4.使用`@Configuration`注释 `@Configuration`是一个类级注释,指示对象是 Bean 定义的源。`@Configuration`类通过`@Bean`-带注释的方法声明 bean。对`@Configuration`类上的`@Bean`方法的调用也可用于定义 Bean 之间的依赖关系。一般介绍见[Basic Concepts: `@Bean` and `@Configuration`](#beans-java-basic-concepts)。 ##### 注入相互 Bean 的依赖关系 当 bean 彼此之间存在依赖关系时,表示这种依赖关系就像让一个方法调用另一个方法一样简单,如下例所示: 爪哇 ``` @Configuration public class AppConfig { @Bean public BeanOne beanOne() { return new BeanOne(beanTwo()); } @Bean public BeanTwo beanTwo() { return new BeanTwo(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun beanOne() = BeanOne(beanTwo()) @Bean fun beanTwo() = BeanTwo() } ``` 在前面的示例中,`beanOne`通过构造函数注入接收对`beanTwo`的引用。 | |这种声明互 Bean 依赖关系的方法仅在`@Bean`方法
在`@Configuration`类中声明时才有效。不能使用普通的`@Component`类来声明 Bean 之间的依赖关系
。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 查找方法注入 如前所述,[查找方法注入](#beans-factory-method-injection)是一个很少使用的高级特性。在单作用域 Bean 对原型作用域 Bean 具有依赖性的情况下,它是有用的。对这种类型的配置使用 爪哇 为实现这种模式提供了一种自然的方法。下面的示例展示了如何使用查找方法注入: 爪哇 ``` public abstract class CommandManager { public Object process(Object commandState) { // grab a new instance of the appropriate Command interface Command command = createCommand(); // set the state on the (hopefully brand new) Command instance command.setState(commandState); return command.execute(); } // okay... but where is the implementation of this method? protected abstract Command createCommand(); } ``` Kotlin ``` abstract class CommandManager { fun process(commandState: Any): Any { // grab a new instance of the appropriate Command interface val command = createCommand() // set the state on the (hopefully brand new) Command instance command.setState(commandState) return command.execute() } // okay... but where is the implementation of this method? protected abstract fun createCommand(): Command } ``` 通过使用 爪哇 配置,你可以创建`CommandManager`的子类,其中抽象`createCommand()`方法以这样一种方式被重写,即它会查找一个新的(原型)命令对象。下面的示例展示了如何做到这一点: 爪哇 ``` @Bean @Scope("prototype") public AsyncCommand asyncCommand() { AsyncCommand command = new AsyncCommand(); // inject dependencies here as required return command; } @Bean public CommandManager commandManager() { // return new anonymous implementation of CommandManager with createCommand() // overridden to return a new prototype Command object return new CommandManager() { protected Command createCommand() { return asyncCommand(); } } } ``` Kotlin ``` @Bean @Scope("prototype") fun asyncCommand(): AsyncCommand { val command = AsyncCommand() // inject dependencies here as required return command } @Bean fun commandManager(): CommandManager { // return new anonymous implementation of CommandManager with createCommand() // overridden to return a new prototype Command object return object : CommandManager() { override fun createCommand(): Command { return asyncCommand() } } } ``` ##### 有关基于 爪哇 的配置如何在内部工作的更多信息 ##### 考虑以下示例,该示例显示了一个`@Bean`带注释的方法被调用了两次: 爪哇 ``` @Configuration public class AppConfig { @Bean public ClientService clientService1() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientService clientService2() { ClientServiceImpl clientService = new ClientServiceImpl(); clientService.setClientDao(clientDao()); return clientService; } @Bean public ClientDao clientDao() { return new ClientDaoImpl(); } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun clientService1(): ClientService { return ClientServiceImpl().apply { clientDao = clientDao() } } @Bean fun clientService2(): ClientService { return ClientServiceImpl().apply { clientDao = clientDao() } } @Bean fun clientDao(): ClientDao { return ClientDaoImpl() } } ``` `clientDao()`在`clientService1()`中调用过一次,在`clientService2()`中调用过一次。由于此方法创建了`ClientDaoImpl`的新实例并返回它,因此你通常希望有两个实例(每个服务一个)。这肯定是有问题的:在 Spring 中,实例化的 bean 默认具有`singleton`作用域。这就是神奇之处:所有`@Configuration`类在启动时都用`CGLIB`子类。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存的(作用域)bean。 | |根据你的 Bean 的范围,行为可能是不同的。我们在这里讨论的是
关于单身汉的问题。| |---|--------------------------------------------------------------------------------------------------------------| | |从 Spring 3.2 开始,不再需要将 CGLIB 添加到你的 Classpath 中,因为 CGLIB类已经在下重新打包,并直接包含在 Spring-core jar 中。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |由于 CGlib 在
启动时动态添加特性,因此有一些限制。特别是,配置类不能是最终的。但是,正如
of4.3,在配置类上允许使用任何构造函数,包括使用 `@autowired’或为默认注入使用单个非默认构造函数声明。

如果你希望避免任何 CGLIB 施加的限制,请考虑在非 `@configuration` 类上声明你的`@Bean`方法(例如,在普通的`@Component`类上)。
方法之间的跨方法调用不会被截获,因此你有
在构造函数或方法级别完全依赖于依赖注入。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.12.5.编写基于 爪哇 的配置 Spring 的基于 爪哇 的配置特性允许你编写注释,这可以降低配置的复杂性。 ##### 使用`@Import`注释 正如在 Spring XML 文件中使用``元素来帮助模块化配置一样,`@Import`注释允许从另一个配置类加载`@Bean`定义,如下例所示: 爪哇 ``` @Configuration public class ConfigA { @Bean public A a() { return new A(); } } @Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b() { return new B(); } } ``` Kotlin ``` @Configuration class ConfigA { @Bean fun a() = A() } @Configuration @Import(ConfigA::class) class ConfigB { @Bean fun b() = B() } ``` 现在,在实例化上下文时,不需要同时指定`ConfigA.class`和`ConfigB.class`,只需要显式地提供`ConfigB`,如下例所示: 爪哇 ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); // now both beans A and B will be available... A a = ctx.getBean(A.class); B b = ctx.getBean(B.class); } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = AnnotationConfigApplicationContext(ConfigB::class.java) // now both beans A and B will be available... val a = ctx.getBean() val b = ctx.getBean() } ``` 这种方法简化了容器实例化,因为只需要处理一个类,而不是要求你在构建过程中记住可能大量的“@configuration”类。 | |在 Spring Framework4.2 中,`@Import`还支持对常规组件
类的引用,类似于`AnnotationConfigApplicationContext.register`方法。
如果你想要避免组件扫描,通过使用几个
配置类作为切入点来显式地定义所有组件,这是特别有用的。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ###### 对导入的`@Bean`定义注入依赖项 前面的例子是可行的,但过于简单化了。在大多数实际场景中,跨配置类的 bean 彼此具有依赖性。在使用 XML 时,这不是一个问题,因为不涉及编译器,你可以声明 `ref=“somebean”`,并信任 Spring 在容器初始化期间解决它。当使用`@Configuration`类时,Java 编译器对配置模型施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。 幸运的是,解决这个问题很简单。作为[我们已经讨论过了](#beans-java-dependencies),`@Bean`方法可以具有任意数量的描述 Bean 依赖关系的参数。考虑以下更现实的场景,其中包含几个`@Configuration`类,每个类取决于在其他类中声明的 bean: Java ``` @Configuration public class ServiceConfig { @Bean public TransferService transferService(AccountRepository accountRepository) { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { @Bean public AccountRepository accountRepository(DataSource dataSource) { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } ``` Kotlin ``` import org.springframework.beans.factory.getBean @Configuration class ServiceConfig { @Bean fun transferService(accountRepository: AccountRepository): TransferService { return TransferServiceImpl(accountRepository) } } @Configuration class RepositoryConfig { @Bean fun accountRepository(dataSource: DataSource): AccountRepository { return JdbcAccountRepository(dataSource) } } @Configuration @Import(ServiceConfig::class, RepositoryConfig::class) class SystemTestConfig { @Bean fun dataSource(): DataSource { // return new DataSource } } fun main() { val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) // everything wires up across configuration classes... val transferService = ctx.getBean() transferService.transfer(100.00, "A123", "C456") } ``` 还有另一种方法可以达到同样的效果。请记住,`@Configuration`类最终只是容器中的另一个 Bean:这意味着它们可以利用与任何其他 Bean 相同的 `@autowired’和`@Value`注入和其他特征。 | |确保以这种方式注入的依赖关系仅是最简单的依赖关系。`@Configuration`类在上下文的初始化过程中很早就被处理了,并且强制以这种方式注入依赖项
可能会导致意外的早期初始化。只要有可能,就求助于
基于参数的注入,就像在前面的示例中一样。

此外,要特别小心`BeanPostProcessor`和`BeanFactoryPostProcessor`定义
通过`@Bean`。这些方法通常应该声明为`static @Bean`方法,而不是触发其包含的配置类的
实例化。否则,`@Autowired`和`@Value`可能不会在配置类本身上工作,因为可以在
之前将其创建为 Bean 实例。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例显示了一个 Bean 如何能够自动连接到另一个 Bean: Java ``` @Configuration public class ServiceConfig { @Autowired private AccountRepository accountRepository; @Bean public TransferService transferService() { return new TransferServiceImpl(accountRepository); } } @Configuration public class RepositoryConfig { private final DataSource dataSource; public RepositoryConfig(DataSource dataSource) { this.dataSource = dataSource; } @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } } @Configuration @Import({ServiceConfig.class, RepositoryConfig.class}) public class SystemTestConfig { @Bean public DataSource dataSource() { // return new DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); // everything wires up across configuration classes... TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } ``` Kotlin ``` import org.springframework.beans.factory.getBean @Configuration class ServiceConfig { @Autowired lateinit var accountRepository: AccountRepository @Bean fun transferService(): TransferService { return TransferServiceImpl(accountRepository) } } @Configuration class RepositoryConfig(private val dataSource: DataSource) { @Bean fun accountRepository(): AccountRepository { return JdbcAccountRepository(dataSource) } } @Configuration @Import(ServiceConfig::class, RepositoryConfig::class) class SystemTestConfig { @Bean fun dataSource(): DataSource { // return new DataSource } } fun main() { val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) // everything wires up across configuration classes... val transferService = ctx.getBean() transferService.transfer(100.00, "A123", "C456") } ``` | |在`@Configuration`类中的构造函数注入仅在 Spring
Framework4.3 时支持。还请注意,如果目标
Bean 只定义了一个构造函数,则不需要指定`@Autowired`。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| []()完全合格的导入 bean,便于导航 在前面的场景中,使用`@Autowired`可以很好地工作并提供所需的模块化,但是确定 AutoWired Bean 定义的确切声明位置仍然有些模棱两可。例如,作为查看`ServiceConfig`的开发人员,你如何准确地知道`@Autowired AccountRepository` Bean 在哪里声明?它在代码中不是显式的,这可能就很好了。请记住,[Spring Tools for Eclipse](https://spring.io/tools)提供了工具,可以呈现显示所有事物如何连接的图形,这可能就是你所需要的。另外,你的 Java IDE 可以很容易地找到`AccountRepository`类型的所有声明和用法,并快速向你显示返回该类型的`@Bean`方法的位置。 如果这种歧义是不可接受的,并且你希望在 IDE 中从一个`@Configuration`类直接导航到另一个类,请考虑自动连接配置类本身。下面的示例展示了如何做到这一点: Java ``` @Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { // navigate 'through' the config class to the @Bean method! return new TransferServiceImpl(repositoryConfig.accountRepository()); } } ``` Kotlin ``` @Configuration class ServiceConfig { @Autowired private lateinit var repositoryConfig: RepositoryConfig @Bean fun transferService(): TransferService { // navigate 'through' the config class to the @Bean method! return TransferServiceImpl(repositoryConfig.accountRepository()) } } ``` 在前面的情况中,其中`AccountRepository`是完全显式定义的。然而,`ServiceConfig`现在与`RepositoryConfig`紧密耦合。这是一种权衡。通过使用基于接口或基于抽象类的`@Configuration`类,可以在一定程度上减轻这种紧密耦合。考虑以下示例: Java ``` @Configuration public class ServiceConfig { @Autowired private RepositoryConfig repositoryConfig; @Bean public TransferService transferService() { return new TransferServiceImpl(repositoryConfig.accountRepository()); } } @Configuration public interface RepositoryConfig { @Bean AccountRepository accountRepository(); } @Configuration public class DefaultRepositoryConfig implements RepositoryConfig { @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(...); } } @Configuration @Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config! public class SystemTestConfig { @Bean public DataSource dataSource() { // return DataSource } } public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); TransferService transferService = ctx.getBean(TransferService.class); transferService.transfer(100.00, "A123", "C456"); } ``` Kotlin ``` import org.springframework.beans.factory.getBean @Configuration class ServiceConfig { @Autowired private lateinit var repositoryConfig: RepositoryConfig @Bean fun transferService(): TransferService { return TransferServiceImpl(repositoryConfig.accountRepository()) } } @Configuration interface RepositoryConfig { @Bean fun accountRepository(): AccountRepository } @Configuration class DefaultRepositoryConfig : RepositoryConfig { @Bean fun accountRepository(): AccountRepository { return JdbcAccountRepository(...) } } @Configuration @Import(ServiceConfig::class, DefaultRepositoryConfig::class) // import the concrete config! class SystemTestConfig { @Bean fun dataSource(): DataSource { // return DataSource } } fun main() { val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) val transferService = ctx.getBean() transferService.transfer(100.00, "A123", "C456") } ``` 现在`ServiceConfig`相对于具体的 `defaultRepositoryConfig’是松耦合的,并且内置的 IDE 工具仍然很有用:你可以轻松地获得`RepositoryConfig`实现的类型层次结构。通过这种方式,导航`@Configuration`类及其依赖关系与导航基于接口的代码的通常过程没有什么不同。 | |如果你想要影响某些 bean 的启动创建顺序,请考虑
声明其中一些为`@Lazy`(用于在 First Access 上而不是在 Startup 上创建)
或作为`@DependsOn`某些其他 bean(确保特定的其他 bean 是在当前 Bean 之前创建的
,而后者的直接依赖所隐含的意义则更大)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 有条件地包含`@Configuration`类或`@Bean`方法 基于某些任意的系统状态,有条件地启用或禁用完整的`@Configuration`类或甚至单个`@Bean`方法通常是有用的。一个常见的例子是,只有在 Spring `Environment`中启用了特定配置文件时,才使用`@Profile`注释来激活 bean(有关详细信息,请参见[Bean Definition Profiles](#beans-definition-profiles))。 `@Profile`注释实际上是通过使用一种更灵活的注释[`@Conditional`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Conditional.html)来实现的。`@Conditional`注释表示特定的 `org.springframework.context.annotation.condition` 实现,在注册`@Bean`之前应该参考这些实现。 `Condition`接口的实现提供了一个`matches(…​)`方法,该方法返回`true`或`false`。例如,下面的清单显示了用于`@Profile`的实际 ` 条件’实现: Java ``` @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // Read the @Profile annotation attributes MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().acceptsProfiles(((String[]) value))) { return true; } } return false; } return true; } ``` Kotlin ``` override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean { // Read the @Profile annotation attributes val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name) if (attrs != null) { for (value in attrs["value"]!!) { if (context.environment.acceptsProfiles(Profiles.of(*value as Array))) { return true } } return false } return true } ``` 有关更多详细信息,请参见[`@Conditional`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Conditional.html)Javadoc。 ##### 结合 Java 和 XML 配置 Spring 的`@Configuration`类支持并不是为了 100% 地完全替代 Spring XML。一些工具,例如 Spring XML 名称空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,你可以选择:以“以 XML 为中心”的方式实例化容器,例如使用“ClassPathXMLApplicationContext”,或者以“以 Java 为中心”的方式实例化容器,使用“AnnotationConfigApplicationContext”和注释来根据需要导入 XML。 ###### 以 XML 为中心使用`@Configuration`类 最好从 XML 引导 Spring 容器,并以特别的方式包括 `@Configuration’类。例如,在使用 Spring XML 的大型现有代码库中,更容易根据需要创建`@Configuration`类,并从现有的 XML 文件中包含它们。在本节的后面,我们将介绍在这种“以 XML 为中心”的情况下使用`@Configuration`类的选项。 []()将`@Configuration`类声明为普通 Spring ``元素 请记住,`@Configuration`类最终是容器中的 Bean 定义。在本系列示例中,我们创建了一个名为`@Configuration`的`AppConfig`类,并将其包含在`system-test-config.xml`中,作为``的定义。因为打开了 ``,容器识别 `@configuration’注释并正确处理`@Bean`中声明的`AppConfig`方法。 下面的示例展示了一个普通的 Java 配置类: Java ``` @Configuration public class AppConfig { @Autowired private DataSource dataSource; @Bean public AccountRepository accountRepository() { return new JdbcAccountRepository(dataSource); } @Bean public TransferService transferService() { return new TransferService(accountRepository()); } } ``` Kotlin ``` @Configuration class AppConfig { @Autowired private lateinit var dataSource: DataSource @Bean fun accountRepository(): AccountRepository { return JdbcAccountRepository(dataSource) } @Bean fun transferService() = TransferService(accountRepository()) } ``` 下面的示例显示了`system-test-config.xml`文件示例的一部分: ``` ``` 下面的示例显示了一个可能的`jdbc.properties`文件: ``` jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password= ``` Java ``` public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml"); TransferService transferService = ctx.getBean(TransferService.class); // ... } ``` Kotlin ``` fun main() { val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml") val transferService = ctx.getBean() // ... } ``` | |在`system-test-config.xml`文件中,`AppConfig```不声明`id`元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 Bean
引用过它,并且不太可能通过名称从容器中显式地获取它,类似地,
Bean 也只能通过类型自动连线,所以显式 Bean `id`并不是严格要求的。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| []()使用 \获取`@Configuration`类 因为`@Configuration`是用`@Component`进行元注释的,所以`@Configuration`-注释的类自动成为组件扫描的候选类。使用与前面示例中描述的相同的场景,我们可以重新定义`system-test-config.xml`以利用组件扫描。注意,在这种情况下,我们不需要显式声明 ``,因为``启用了相同的功能。 下面的示例显示了修改后的`system-test-config.xml`文件: ``` ``` ###### `@Configuration`以类为中心使用`@ImportResource`###### 在以`@Configuration`类为配置容器的主要机制的应用程序中,仍然可能需要至少使用一些 XML。在这些场景中,你可以使用`@ImportResource`并只定义所需的 XML。这样就实现了一种“以 Java 为中心”的方法来配置容器,并将 XML 保持在最低限度。下面的示例(包括一个配置类、一个定义 Bean 的 XML 文件、一个属性文件和`main`类)展示了如何使用`@ImportResource`注释来实现根据需要使用 XML 的“以 Java 为中心”的配置: Java ``` @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") public class AppConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource() { return new DriverManagerDataSource(url, username, password); } } ``` Kotlin ``` @Configuration @ImportResource("classpath:/com/acme/properties-config.xml") class AppConfig { @Value("\${jdbc.url}") private lateinit var url: String @Value("\${jdbc.username}") private lateinit var username: String @Value("\${jdbc.password}") private lateinit var password: String @Bean fun dataSource(): DataSource { return DriverManagerDataSource(url, username, password) } } ``` ``` properties-config.xml ``` ``` jdbc.properties jdbc.url=jdbc:hsqldb:hsql://localhost/xdb jdbc.username=sa jdbc.password= ``` Java ``` public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); TransferService transferService = ctx.getBean(TransferService.class); // ... } ``` Kotlin ``` import org.springframework.beans.factory.getBean fun main() { val ctx = AnnotationConfigApplicationContext(AppConfig::class.java) val transferService = ctx.getBean() // ... } ``` ### 1.13.环境抽象 [`Environment`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/env/Environment.html)接口是集成在容器中的一个抽象,它为应用程序环境的两个关键方面建模:[profiles](#beans-definition-profiles)和[properties](#beans-property-source-abstraction)。 Bean 配置文件是由 Bean 个定义组成的一个命名的逻辑组,只有在给定的配置文件处于活动状态时才向容器注册。可以将 bean 分配给一个配置文件,无论是用 XML 定义的还是用注释定义的。相对于配置文件,`Environment`对象的作用是确定哪些配置文件(如果有的话)当前处于活动状态,以及默认情况下哪些配置文件(如果有的话)应该处于活动状态。 属性在几乎所有的应用程序中都扮演着重要的角色,并且可能来自各种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、 Servlet 上下文参数、ad-hoc`Properties`对象、`Map`对象,等等。与属性相关的`Environment`对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从它们解析属性。 #### 1.13.1. Bean 定义配置文件 Bean 定义配置文件在核心容器中提供了一种机制,该机制允许在不同的环境中注册不同的 bean。“环境”这个词对不同的用户来说可能意味着不同的东西,这个功能可以帮助解决许多用例,包括: * 在开发中使用内存中的数据源,而不是在 QA 或生产中从 JNDI 中查找相同的数据源。 * 仅在将应用程序部署到性能环境时注册监视基础结构。 * 为客户 A 和客户 B 的部署注册定制的 bean 实现。 考虑实际应用程序中需要“数据源”的第一个用例。在测试环境中,配置可能类似于以下内容: Java ``` @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("my-schema.sql") .addScript("my-test-data.sql") .build(); } ``` Kotlin ``` @Bean fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("my-schema.sql") .addScript("my-test-data.sql") .build() } ``` 现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录中。我们的`dataSource` Bean 现在看起来像以下清单: Java ``` @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } ``` Kotlin ``` @Bean(destroyMethod = "") fun dataSource(): DataSource { val ctx = InitialContext() return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource } ``` 问题是如何根据当前环境在使用这两种变体之间进行切换。 Spring 随着时间的推移,用户已经设计了许多方法来完成这一工作,通常依赖于系统环境变量和 XML语句的组合,这些语句包含令牌,这些令牌根据环境变量的值解析为正确的配置文件路径。 Bean 定义简档是容器的核心特征,其提供了解决该问题的方法。 如果我们推广前面的例子中所示的特定于环境的定义 Bean 的用例,那么我们最终需要在某些上下文中注册某些 Bean 定义,而不是在其他上下文中注册。可以说,你希望在情况 A 中注册 Bean 定义的特定配置文件,而在情况 B 中注册不同的配置文件。我们从更新配置开始,以反映这种需求。 ##### 使用`@Profile` [`@Profile`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Profile.html)注释使你可以指示,当一个或多个指定的配置文件处于活动状态时,组件有资格进行注册。使用前面的示例,我们可以按以下方式重写`dataSource`配置: Java ``` @Configuration @Profile("development") public class StandaloneDataConfig { @Bean public DataSource dataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } } ``` Kotlin ``` @Configuration @Profile("development") class StandaloneDataConfig { @Bean fun dataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build() } } ``` Java ``` @Configuration @Profile("production") public class JndiDataConfig { @Bean(destroyMethod="") public DataSource dataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } ``` Kotlin ``` @Configuration @Profile("production") class JndiDataConfig { @Bean(destroyMethod = "") fun dataSource(): DataSource { val ctx = InitialContext() return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource } } ``` | |如前所述,对于`@Bean`方法,你通常会选择使用程序化的
JNDI 查找,方法是使用 Spring 的`JndiTemplate`/`jndilocatordelegate`helpers 或
直接的 JNDI`InitialContext`用法,但不使用`JndiObjectFactoryBean`变体,这将迫使你将返回类型声明为`FactoryBean`类型。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 配置文件字符串可以包含一个简单的配置文件名(例如,`production`)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,`production & us-east`)。配置文件表达式中支持以下操作符: * `!`:配置文件的逻辑“not” * `&`:配置文件的逻辑“和” * `|`:配置文件的逻辑“或” | |You cannot mix the `&` and `|` operators without using parentheses. For example,`production & us-east |“eu-central”不是一个有效的表达方式。它必须表示为“Production&(美国东部)”| eu-central)`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以使用`@Profile`作为[meta-annotation](#beans-meta-annotations)来创建自定义组合注释。下面的示例定义了一个自定义的“@production”注释,你可以将其用作“@profile(“production”)”的插入替换: Java ``` @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Profile("production") public @interface Production { } ``` Kotlin ``` @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) @Profile("production") annotation class Production ``` | |如果`@Configuration`类被标记为`@Profile`,则所有`@Bean`方法和与该类关联的 `@import’注释都将被忽略,除非
中的一个或多个指定的配置文件处于活动状态。如果一个`@Component`或`@Configuration`类被标记为
并带有`@Profile({"p1", "p2"})`,则除非
配置文件“p1”或“p2”已被激活,否则该类不会被注册或处理。如果给定概要文件的前缀是
not 运算符,则只有在概要文件不是
活动的情况下,才会注册带注释的元素。例如,给定`@Profile({"p1", "!p2"})`,如果配置文件
’P1’是活动的,或者如果配置文件’P2’不是活动的,就会发生注册。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| `@Profile`还可以在方法级别上声明仅包括一个特定 Bean 配置类的一个特定 Bean(例如,对于特定 Bean 的可选变体),如以下示例所示: Java ``` @Configuration public class AppConfig { @Bean("dataSource") @Profile("development") (1) public DataSource standaloneDataSource() { return new EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build(); } @Bean("dataSource") @Profile("production") (2) public DataSource jndiDataSource() throws Exception { Context ctx = new InitialContext(); return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); } } ``` |**1**|`standaloneDataSource`方法仅在`development`配置文件中可用。| |-----|---------------------------------------------------------------------------------| |**2**|`jndiDataSource`方法仅在`production`配置文件中可用。| Kotlin ``` @Configuration class AppConfig { @Bean("dataSource") @Profile("development") (1) fun standaloneDataSource(): DataSource { return EmbeddedDatabaseBuilder() .setType(EmbeddedDatabaseType.HSQL) .addScript("classpath:com/bank/config/sql/schema.sql") .addScript("classpath:com/bank/config/sql/test-data.sql") .build() } @Bean("dataSource") @Profile("production") (2) fun jndiDataSource() = InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource } ``` |**1**|`standaloneDataSource`方法仅在`development`配置文件中可用。| |-----|---------------------------------------------------------------------------------| |**2**|`jndiDataSource`方法仅在`production`配置文件中可用。| | |对于`@Profile`on`@Bean`方法,可能会应用一个特殊的场景:在
重载`@Bean`相同 Java 方法名的方法的情况下(类似于构造函数
重载),需要在所有`@Profile`重载方法上一致地声明
条件。如果条件不一致,在重载的方法中,只有
第一个声明上的条件才重要。因此,`@Profile`可以不使用

上选择具有特定参数签名的重载方法。相同 Bean 的所有工厂方法之间的解析遵循 Spring 的
构造函数在创建时的解析算法。

如果你想定义具有不同配置文件条件的替代 bean,
使用不同的 Java 方法名称,通过使用`@Bean`名称
属性指向相同的 Bean 名称,如前面的例子所示。如果所有的参数签名都是
相同的(例如,所有的变量都有 no-arg 工厂方法),这是唯一的
在有效的 Java 类中表示这样的安排的方式,在第一个位置
(因为只能有一个方法的特定名称和参数签名)。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### XML Bean 定义配置文件 XML 对应的是`profile`元素的``属性。我们前面的示例配置可以在两个 XML 文件中重写,如下所示: ``` ``` ``` ``` 也可以避免在同一个文件中分割和嵌套``元素,如下例所示: ``` ``` `spring-bean.xsd`已被限制为仅允许将此类元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会在 XML 文件中造成混乱。 | |XML 对应方不支持前面描述的配置文件表达式。但是,
可以使用`!`操作符来否定配置文件。通过嵌套配置文件,也可以应用逻辑
“and”,如下例所示:

``
xmlns.w3.org/2001/xmlschema-instance“<xmlns:jdbc=”http:///WWW.springframework.jdbc/jdbc=“schemframework=”<99"/schemframework=">应用程序。[“标准服务环境”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/StandardServletEnvironment.html)填充了额外的默认属性源,包括 Servlet config 和 Servlet
上下文参数。它可以选择性地启用[jndipropertysource’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jndi/JndiPropertySource.html)。
有关详细信息,请参见 Javadoc。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 具体地说,当使用`StandardEnvironment`时,如果在运行时存在`my-property`系统属性或`my-property`环境变量,则对`env.containsProperty("my-property")`的调用将返回 true。 | |执行的搜索是分层次的。默认情况下,系统属性优先于
环境变量。因此,如果`my-property`属性在
调用`env.getProperty("my-property")`期间恰好在这两个地方都设置了,则系统属性值“wins”并返回。,
注意,属性值不是合并
,而是完全被前面的一个条目覆盖,

对于一个常见的`StandardServletEnvironment`,完整的层次结构如下,
优先级最高的条目位于顶部:

1。ServletConfig 参数(如果适用——例如,在`DispatcherServlet`上下文的情况下)

2。ServletContext 参数

3。ENV 变量

4。JVM 系统属性(`-d` 命令行参数)

5。JVM 系统环境(操作系统环境变量)| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 最重要的是,整个机制是可配置的。也许你有一个想要集成到此搜索中的自定义属性源。为此,实现并实例化你自己的`PropertySource`,并将其添加到当前`PropertySources`的`PropertySources`集合中。下面的示例展示了如何做到这一点: Java ``` ConfigurableApplicationContext ctx = new GenericApplicationContext(); MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); sources.addFirst(new MyPropertySource()); ``` Kotlin ``` val ctx = GenericApplicationContext() val sources = ctx.environment.propertySources sources.addFirst(MyPropertySource()) ``` 在前面的代码中,`MyPropertySource`已被添加到搜索中具有最高优先级的位置。如果它包含一个`my-property`属性,则检测并返回该属性,这有利于在任何其他`my-property`中的任何`PropertySource`属性。[“可变 PropertySources”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/env/MutablePropertySources.html)API 公开了许多方法,这些方法允许对属性源集合进行精确操作。 #### 1.13.3.使用`@PropertySource` [@PropertySource](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/PropertySource.html)注释提供了一种方便的声明性机制,用于将`PropertySource`添加到 Spring 的`Environment`中。 给定一个名为`app.properties`的文件,该文件包含键-值对`testbean.name=myTestBean`,下面的`@Configuration`类使用`@PropertySource`的方式使得对`testBean.getName()`的调用返回`myTestBean`: Java ``` @Configuration @PropertySource("classpath:/com/myco/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } } ``` Kotlin ``` @Configuration @PropertySource("classpath:/com/myco/app.properties") class AppConfig { @Autowired private lateinit var env: Environment @Bean fun testBean() = TestBean().apply { name = env.getProperty("testbean.name")!! } } ``` 在`@PropertySource`资源位置中存在的任何`${…​}`占位符都将根据已经针对该环境注册的一组属性源进行解析,如下例所示: Java ``` @Configuration @PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties") public class AppConfig { @Autowired Environment env; @Bean public TestBean testBean() { TestBean testBean = new TestBean(); testBean.setName(env.getProperty("testbean.name")); return testBean; } } ``` Kotlin ``` @Configuration @PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties") class AppConfig { @Autowired private lateinit var env: Environment @Bean fun testBean() = TestBean().apply { name = env.getProperty("testbean.name")!! } } ``` 假设`my.placeholder`存在于已经注册的一个属性源中(例如,系统属性或环境变量),则将占位符解析为相应的值。如果不是,则将`default/path`用作默认值。如果没有指定默认值,并且无法解析某个属性,则抛出“非法 argumenTexception”。 | |根据 Java8 约定,`@PropertySource`注释是可重复的。
然而,所有此类`@PropertySource`注释都需要在相同的
级别上声明,可以直接在配置类上声明,也可以在
相同的自定义注释中声明元注释。混合直接注释和元注释不是
推荐的,因为直接注释有效地覆盖了元注释。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.13.4.语句中的占位符解析 从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。现在已经不是这样了。因为`Environment`抽象集成在整个容器中,所以很容易通过它来路由占位符的解析。这意味着你可以以你喜欢的任何方式配置解析过程。你可以更改通过系统属性和环境变量进行搜索的优先级,或者完全删除它们。你还可以将自己的属性源添加到该组合中,视情况而定。 具体地说,无论`customer`属性在哪里定义,只要它在`Environment`中可用,以下语句都可以工作: ``` ``` ### 1.14.注册`LoadTimeWeaver` Spring 使用`LoadTimeWeaver`在类被加载到 Java 虚拟机时对其进行动态转换。 要启用加载时编织,可以将`@EnableLoadTimeWeaving`添加到一个 `@Configuration’类中,如下例所示: Java ``` @Configuration @EnableLoadTimeWeaving public class AppConfig { } ``` Kotlin ``` @Configuration @EnableLoadTimeWeaving class AppConfig ``` 或者,对于 XML 配置,你可以使用`context:load-time-weaver`元素: ``` ``` 一旦为`ApplicationContext`配置,在该`ApplicationContext`内的任何 Bean 都可以实现`LoadTimeWeaverAware`,从而接收到对加载时 Weaver 实例的引用。这在与[Spring’s JPA support](data-access.html#orm-jpa)的结合中特别有用,其中 JPA 类转换可能需要进行加载时的编织。有关更多详细信息,请咨询[“本地包含 rentitymanagerfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.html)Javadoc。有关 AspectJ 加载时编织的更多信息,请参见[Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw)。 ### 1.15.`ApplicationContext`的附加功能 正如[章节介绍](#beans)中所讨论的,`org.springframework.beans.factory`包提供了用于管理和操作 bean 的基本功能,包括以编程方式。`org.springframework.context`包添加了[` 应用上下文’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationContext.html)接口,该接口扩展了`BeanFactory`接口,此外还扩展了其他接口,以便以更面向应用程序框架的风格提供附加功能。许多人使用`ApplicationContext`完全是声明式的,甚至不是以编程方式创建它,而是依靠`ContextLoader`之类的支持类来自动实例化“ApplicationContext”,作为 Java EE Web 应用程序正常启动过程的一部分。 为了以更面向框架的风格增强`BeanFactory`功能,上下文包还提供了以下功能: * 通过`MessageSource`接口访问 i18n 样式的消息。 * 通过`ResourceLoader`接口访问资源,例如 URL 和文件。 * 事件发布,即通过使用`ApplicationEventPublisher`接口来实现`ApplicationListener`接口的 bean。 * 通过“HierarChicalBeanFactory”接口,加载多个(分层的)上下文,使每个上下文都集中在一个特定的层上,例如应用程序的 Web 层。 #### 1.15.1.使用`MessageSource`的国际化 `ApplicationContext`接口扩展了一个名为`MessageSource`的接口,因此提供了国际化(“i18n”)功能。 Spring 还提供了 `HierarChicalMessageSource’接口,该接口可以按层次解析消息。这些接口一起提供了 Spring 影响消息解析的基础。在这些接口上定义的方法包括: * `String getMessage(String code, Object[] args, String default, Locale loc)`:用于从`MessageSource`中检索消息的基本方法。当未找到指定区域设置的消息时,将使用默认消息。使用标准库提供的`MessageFormat`功能,传入的任何参数都将成为替换值。 * `String getMessage(String code, Object[] args, Locale loc)`:与以前的方法基本相同,但有一个区别:不能指定默认消息。如果找不到消息,则抛出`NoSuchMessageException`。 * `String getMessage(MessageSourceResolvable resolvable, Locale locale)`:前面的方法中使用的所有属性也包装在一个名为“MessageSourceResolvable”的类中,你可以在这个方法中使用它。 当加载`ApplicationContext`时,它会自动搜索上下文中定义的`MessageSource` Bean。 Bean 必须具有`messageSource`的名称。如果找到了这样的 Bean,那么对前面的方法的所有调用都被委托给消息源。如果找不到消息源,`ApplicationContext`将尝试查找包含 Bean 同名的父消息。如果是,则使用 Bean 作为`MessageSource`。如果“ApplicationContext”找不到任何消息源,则实例化一个空的“delegatingmessagesource”,以便能够接受对上述定义的方法的调用。 Spring 提供了三个`MessageSource`实现方式,`ResourceBundleMessageSource`、`ReloadableResourceBundleMessageSource`和`StaticMessageSource`。它们都实现`HierarchicalMessageSource`以执行嵌套消息传递。`StaticMessageSource`很少使用,但提供了向源添加消息的编程方法。下面的示例显示`ResourceBundleMessageSource`: ``` format exceptions windows ``` 该示例假定你在 Classpath 中定义了三个资源包,分别称为`format`、`exceptions`和`windows`。通过`ResourceBundle`对象以 JDK 标准的方式处理任何解析消息的请求。为了示例的目的,假设上述两个资源包文件的内容如下: ``` # in format.properties message=Alligators rock! ``` ``` # in exceptions.properties argument.required=The {0} argument is required. ``` 下一个示例显示了运行`MessageSource`功能的程序。请记住,所有`ApplicationContext`实现也是`MessageSource`实现,因此可以强制转换到`MessageSource`接口。 Java ``` public static void main(String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); String message = resources.getMessage("message", null, "Default", Locale.ENGLISH); System.out.println(message); } ``` Kotlin ``` fun main() { val resources = ClassPathXmlApplicationContext("beans.xml") val message = resources.getMessage("message", null, "Default", Locale.ENGLISH) println(message) } ``` 上述程序的输出结果如下: ``` Alligators rock! ``` 总而言之,`MessageSource`是在一个名为`beans.xml`的文件中定义的,该文件存在于 Classpath 的根。`messageSource` Bean 定义通过其`basenames`属性引用了许多资源包。在列表中传递给`basenames`属性的三个文件作为文件存在于 Classpath 的根目录中,分别称为`format.properties`、`exceptions.properties`和 `windows.properties’。 下一个示例显示传递给消息查找的参数。这些参数被转换为`String`对象,并插入到查找消息中的占位符中。 ``` ``` 爪哇 ``` public class Example { private MessageSource messages; public void setMessages(MessageSource messages) { this.messages = messages; } public void execute() { String message = this.messages.getMessage("argument.required", new Object [] {"userDao"}, "Required", Locale.ENGLISH); System.out.println(message); } } ``` Kotlin ``` class Example { lateinit var messages: MessageSource fun execute() { val message = messages.getMessage("argument.required", arrayOf("userDao"), "Required", Locale.ENGLISH) println(message) } } ``` 调用`execute()`方法的结果如下: ``` The userDao argument is required. ``` 关于国际化(“i18n”), Spring 的各种`MessageSource`实现遵循与标准 JDK`ResourceBundle’相同的语言环境解析和后备规则。简而言之,继续前面定义的示例`messageSource`,如果你想要针对英国语言环境解析消息,那么可以分别创建名为`format_en_GB.properties`、`exceptions_en_GB.properties`和 `windows_en_gb.properties’的文件。 通常,区域设置解析由应用程序的周围环境管理。在下面的示例中,将手动指定用于解析(英式)消息的区域设置: ``` # in exceptions_en_GB.properties argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required. ``` 爪哇 ``` public static void main(final String[] args) { MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); String message = resources.getMessage("argument.required", new Object [] {"userDao"}, "Required", Locale.UK); System.out.println(message); } ``` Kotlin ``` fun main() { val resources = ClassPathXmlApplicationContext("beans.xml") val message = resources.getMessage("argument.required", arrayOf("userDao"), "Required", Locale.UK) println(message) } ``` 运行上述程序的结果如下: ``` Ebagum lad, the 'userDao' argument is required, I say, required. ``` 你还可以使用`MessageSourceAware`接口获取对已定义的任何“MessageSource”的引用。在实现`MessageSourceAware`接口的 `ApplicationContext’中定义的任何 Bean 都会在创建和配置 Bean 时注入应用程序上下文的`MessageSource`。 | |因为 Spring 的`MessageSource`是基于 爪哇 的`ResourceBundle`,所以它不合并具有相同基名的
bundle,而只使用找到的第一个 bundle。
具有相同基名的后续消息 bundle 将被忽略。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |作为`ResourceBundleMessageSource`的替代方案, Spring 提供了一个“ReloadableResourceBundleMessageSource”类。这种变体支持相同的 bundle
文件格式,但比基于标准 JDK 的“ResourceBundleMessageSource”实现更灵活。特别地,它允许从任何 Spring 资源位置读取
文件(不仅是从 Classpath),并且支持 hot
重新加载 bundle 属性文件(同时在它们之间有效地缓存它们)。
有关详细信息,请参见[“重新提供资源,加强信息来源”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ReloadableResourceBundleMessageSource.html)爪哇doc。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.15.2.标准和自定义事件 `ApplicationContext`中的事件处理是通过`ApplicationEvent`类和`ApplicationListener`接口提供的。如果实现了“ApplicationListener”接口的 Bean 被部署到上下文中,那么每当一个“ApplicationEvent”被发布到`ApplicationContext`时, Bean 就会被通知。本质上,这是标准的观察者设计模式。 | |截至 Spring 4.2,事件基础结构已经得到了显著的改进,并且提供了
和[基于注释的模型](#context-functionality-events-annotation)以及
发布任意事件的能力(即,不一定从
扩展的对象`ApplicationEvent`)。当这样的对象被发布时,我们将它包装为
事件。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下表描述了 Spring 提供的标准事件: | Event |解释| |----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `ContextRefreshedEvent` |当`ApplicationContext`被初始化或刷新时发布(例如,通过
在`ConfigurableApplicationContext`接口上使用`refresh()`方法)。
在这里,“初始化”意味着加载所有 bean,检测到后处理器 bean
并激活,预先实例化单例,`ApplicationContext`对象为<3300“gt r=”准备好使用。只要上下文尚未关闭,就可以多次触发
刷新,前提是所选的`ApplicationContext`实际上支持这样的
“热”刷新。例如,`XmlWebApplicationContext`支持热刷新,但 `GenericApplicationContext’不支持。| | `ContextStartedEvent` |当`ApplicationContext`通过使用 `ConfigurableApplicationContext’接口上的方法启动时发布。在这里,“started”表示所有`Lifecycle`bean 都接收到一个显式的开始信号。通常,此信号用于在显式停止后重新启动 bean
,但也可用于启动未被
配置为自动启动的组件(例如,在
初始化上尚未启动的组件)。| | `ContextStoppedEvent` |在“ConfigurableApplicationContext”接口上使用方法停止时发布。在这里,“stopped”表示所有`Lifecycle`bean 都接收到一个明确的停止信号。可以通过“start()”调用重新启动已停止的上下文。| | `ContextClosedEvent` |当`ApplicationContext`正在通过`close()`接口上的
方法
关闭`ApplicationContext`时或通过 JVM 关机钩子关闭时发布。在这里,
“closed”表示所有单例 bean 都将被销毁。一旦上下文被关闭,
它就到达了生命周期的终点,不能刷新或重新启动。| | `RequestHandledEvent` |一个特定于 Web 的事件,告诉所有 bean 一个 HTTP 请求已被服务。此
事件将在请求完成后发布。此事件仅适用于使用 Spring 的
的`DispatcherServlet`的 Web 应用程序。| |`ServletRequestHandledEvent`|添加 Servlet 特定上下文信息的`RequestHandledEvent`子类。| 你还可以创建和发布自己的自定义事件。下面的示例展示了一个扩展 Spring 的`ApplicationEvent`基类的简单类: 爪哇 ``` public class BlockedListEvent extends ApplicationEvent { private final String address; private final String content; public BlockedListEvent(Object source, String address, String content) { super(source); this.address = address; this.content = content; } // accessor and other methods... } ``` Kotlin ``` class BlockedListEvent(source: Any, val address: String, val content: String) : ApplicationEvent(source) ``` 要发布自定义`ApplicationEvent`,请在 `ApplicationEventPublisher’上调用`publishEvent()`方法。通常,这是通过创建一个实现“ApplicationEventPublisherAware”的类并将其注册为 Spring Bean 来完成的。下面的示例展示了这样一个类: 爪哇 ``` public class EmailService implements ApplicationEventPublisherAware { private List blockedList; private ApplicationEventPublisher publisher; public void setBlockedList(List blockedList) { this.blockedList = blockedList; } public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } public void sendEmail(String address, String content) { if (blockedList.contains(address)) { publisher.publishEvent(new BlockedListEvent(this, address, content)); return; } // send email... } } ``` Kotlin ``` class EmailService : ApplicationEventPublisherAware { private lateinit var blockedList: List private lateinit var publisher: ApplicationEventPublisher fun setBlockedList(blockedList: List) { this.blockedList = blockedList } override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) { this.publisher = publisher } fun sendEmail(address: String, content: String) { if (blockedList!!.contains(address)) { publisher!!.publishEvent(BlockedListEvent(this, address, content)) return } // send email... } } ``` 在配置时, Spring 容器检测到`EmailService`实现了 `ApplicationEventPublisherAware’,并自动调用 `setApplicationEventPublisher()’。实际上,传入的参数是 Spring 容器本身。你正在通过其“ApplicationEventPublisher”接口与应用程序上下文交互。 要接收自定义`ApplicationEvent`,你可以创建一个实现 `ApplicationListener’的类,并将其注册为 Spring Bean。下面的示例展示了这样一个类: 爪哇 ``` public class BlockedListNotifier implements ApplicationListener { private String notificationAddress; public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress; } public void onApplicationEvent(BlockedListEvent event) { // notify appropriate parties via notificationAddress... } } ``` Kotlin ``` class BlockedListNotifier : ApplicationListener { lateinit var notificationAddres: String override fun onApplicationEvent(event: BlockedListEvent) { // notify appropriate parties via notificationAddress... } } ``` 请注意,`ApplicationListener`通常是用自定义事件的类型参数化的(在前面的示例中是 `blockedlistevent’)。这意味着“onApplicationEvent()”方法可以保持类型安全,从而避免了向下转换的任何需要。你可以注册任意多的事件侦听器,但请注意,默认情况下,事件侦听器会同步接收事件。这意味着`publishEvent()`方法将阻塞,直到所有侦听器都完成了对事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到一个事件时,如果事务上下文可用,它将在发布者的事务上下文中进行操作。如果需要另一种事件发布策略,请参阅 爪哇doc 获取 Spring 的[“应用 EventMulticaster”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/ApplicationEventMulticaster.html)接口和[SimpleApplication EventMulticaster’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/SimpleApplicationEventMulticaster.html)实现的配置选项。 下面的示例显示了用于注册和配置上述每个类的 Bean 定义: ``` [email protected] [email protected] [email protected] ``` 将所有这些放在一起,当调用`emailService` Bean 的`sendEmail()`方法时,如果有任何应该被阻止的电子邮件消息,则会发布类型为 `blockedlistevent’的自定义事件。将`blockedListNotifier` Bean 注册为 ` 应用监听器’,并接收`BlockedListEvent`,此时它可以通知适当的当事人。 | |Spring 的事件机制设计用于在相同的应用程序上下文中 Spring bean
之间进行简单的通信。然而,对于更复杂的 Enterprise
集成需求,单独维护的[Spring Integration](https://projects.spring.io/spring-integration/)项目提供了
构建轻量级、[以模式为导向](https://www.enterpriseintegrationpatterns.com)事件驱动的
架构的完整支持,这些架构建立在著名的 Spring 编程模型上。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 基于注释的事件侦听器 你可以使用“@EventListener”注释在托管 Bean 的任何方法上注册事件侦听器。`BlockedListNotifier`可以重写如下: 爪哇 ``` public class BlockedListNotifier { private String notificationAddress; public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress; } @EventListener public void processBlockedListEvent(BlockedListEvent event) { // notify appropriate parties via notificationAddress... } } ``` Kotlin ``` class BlockedListNotifier { lateinit var notificationAddress: String @EventListener fun processBlockedListEvent(event: BlockedListEvent) { // notify appropriate parties via notificationAddress... } } ``` 方法签名再次声明它监听的事件类型,但是,这一次,使用灵活的名称,并且不实现特定的监听器接口。只要实际的事件类型在其实现层次结构中解析你的泛型参数,就可以通过泛型来缩小事件类型的范围。 如果你的方法应该监听多个事件,或者如果你想在完全不使用参数的情况下定义它,那么也可以在注释本身上指定事件类型。下面的示例展示了如何做到这一点: 爪哇 ``` @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { // ... } ``` Kotlin ``` @EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class) fun handleContextStart() { // ... } ``` 还可以通过使用注释的`condition`属性添加额外的运行时过滤,该注释定义了[“spel”表达式](#expressions),该属性应该与实际调用特定事件的方法匹配。 下面的示例展示了只有当事件的 `Content’属性等于`my-event`时,我们的通知程序才能被重写以被调用: 爪哇 ``` @EventListener(condition = "#blEvent.content == 'my-event'") public void processBlockedListEvent(BlockedListEvent blEvent) { // notify appropriate parties via notificationAddress... } ``` Kotlin ``` @EventListener(condition = "#blEvent.content == 'my-event'") fun processBlockedListEvent(blEvent: BlockedListEvent) { // notify appropriate parties via notificationAddress... } ``` 每个`SpEL`表达式都针对一个专用上下文进行计算。下表列出了对上下文可用的项,以便你可以将它们用于条件事件处理: | Name | Location |说明| Example | |---------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| | Event | root object |实际`ApplicationEvent`。| `#root.event` or `event` | |Arguments array| root object |用于调用方法的参数(作为对象数组)。| `#root.args` or `args`; `args[0]` to access the first argument, etc. | |*Argument name*|evaluation context|任何方法参数的名称。如果由于某种原因,名称不可用
(例如,因为编译的字节代码中没有调试信息),则单独的
参数也可以使用`#a<#arg>`语法,其中`<#arg>`表示
参数索引(从 0 开始)。|`#blEvent` or `#a0` (you can also use `#p0` or `#p<#arg>` parameter notation as an alias)| 请注意,`#root.event`允许你访问底层事件,即使你的方法签名实际上引用了已发布的任意对象。 如果需要发布一个事件作为处理另一个事件的结果,则可以更改方法签名以返回应该发布的事件,如下例所示: 爪哇 ``` @EventListener public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) { // notify appropriate parties via notificationAddress and // then publish a ListUpdateEvent... } ``` Kotlin ``` @EventListener fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent { // notify appropriate parties via notificationAddress and // then publish a ListUpdateEvent... } ``` | |[异步侦听器](#context-functionality-events-async)不支持此功能。| |---|-----------------------------------------------------------------------------------------------| `handleBlockedListEvent()`方法为它处理的每个 `blockedlistevent’发布一个新的`ListUpdateEvent`。如果需要发布多个事件,可以返回`Collection`或一个事件数组。 ##### 异步侦听器 如果你希望一个特定的侦听器异步地处理事件,那么可以重用[regular `@Async` support](integration.html#scheduling-annotation-support-async)。下面的示例展示了如何做到这一点: 爪哇 ``` @EventListener @Async public void processBlockedListEvent(BlockedListEvent event) { // BlockedListEvent is processed in a separate thread } ``` Kotlin ``` @EventListener @Async fun processBlockedListEvent(event: BlockedListEvent) { // BlockedListEvent is processed in a separate thread } ``` 在使用异步事件时要注意以下限制: * 如果异步事件侦听器抛出`Exception`,则不会将其传播给调用方。有关更多详细信息,请参见[“Asyncuncaughtexceptionhandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.html)。 * 异步事件侦听器方法不能通过返回值来发布后续事件。如果需要发布另一个事件作为处理的结果,则注入[“ApplicationEventPublisher”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationEventPublisher.html)手动发布该事件。 ##### 命令听众 如果需要在调用另一个侦听器之前调用一个侦听器,则可以将`@Order`注释添加到方法声明中,如下例所示: 爪哇 ``` @EventListener @Order(42) public void processBlockedListEvent(BlockedListEvent event) { // notify appropriate parties via notificationAddress... } ``` Kotlin ``` @EventListener @Order(42) fun processBlockedListEvent(event: BlockedListEvent) { // notify appropriate parties via notificationAddress... } ``` ##### 一般事件 你还可以使用泛型来进一步定义事件的结构。考虑使用 `EntityCreateDevent` 其中`T`是实际创建的实体的类型。例如,你可以创建以下监听器定义,以便只接收 `person’的`EntityCreatedEvent`: 爪哇 ``` @EventListener public void onPersonCreated(EntityCreatedEvent event) { // ... } ``` Kotlin ``` @EventListener fun onPersonCreated(event: EntityCreatedEvent) { // ... } ``` 由于类型擦除,只有当触发的事件解析了事件侦听器过滤的通用参数(即,类似于 `Class PersonCreateDevent Extends EntityCreateDevent{…}`)时,这种方法才有效。 在某些情况下,如果所有事件都遵循相同的结构,这可能会变得非常乏味(前面示例中的事件就是这种情况)。在这种情况下,你可以实现`ResolvableTypeProvider`以指导超出运行时环境所提供的框架。以下事件展示了如何做到这一点: 爪哇 ``` public class EntityCreatedEvent extends ApplicationEvent implements ResolvableTypeProvider { public EntityCreatedEvent(T entity) { super(entity); } @Override public ResolvableType getResolvableType() { return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource())); } } ``` Kotlin ``` class EntityCreatedEvent(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider { override fun getResolvableType(): ResolvableType? { return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource())) } } ``` | |这不仅适用于`ApplicationEvent`,还适用于作为
事件发送的任意对象。| |---|--------------------------------------------------------------------------------------------------| #### 1.15.3.对低级资源的方便访问 为了优化应用程序上下文的使用和理解,你应该熟悉 Spring 的`Resource`抽象,如[资源](#resources)中所述。 应用程序上下文是`ResourceLoader`,它可以用来加载`Resource`对象。a`Resource`本质上是 JDK`java.net.URL`类的一个功能更丰富的版本。实际上,在`Resource`的实现中,在适当的情况下包装`java.net.URL`的实例。a`Resource`可以以透明的方式从几乎任何位置获得低级资源,包括从 Classpath、文件系统位置、标准 URL 可描述的任何地方以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合实际的应用程序上下文类型。 你可以配置部署到应用程序上下文中的 Bean,以实现特殊的回调接口`ResourceLoaderAware`,在初始化时自动回调,并将应用程序上下文本身作为`ResourceLoader`传入。还可以公开类型`Resource`的属性,用于访问静态资源。它们像其他任何属性一样被注入其中。你可以将这些`Resource`属性指定为简单的`String`路径,并依赖于在部署 Bean 时将这些文本字符串自动转换为实际的`Resource`对象。 提供给`ApplicationContext`构造函数的位置路径实际上是资源字符串,并且以简单的形式根据特定的上下文实现进行适当的处理。例如,`ClassPathXmlApplicationContext`将简单的位置路径视为 Classpath 位置。你还可以使用带有特殊前缀的位置路径(资源字符串)来强制从 Classpath 或 URL 加载定义,而不管实际的上下文类型是什么。 #### 1.15.4.应用程序启动跟踪 `ApplicationContext`管理 Spring 应用程序的生命周期,并提供围绕组件的丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段。 使用特定的指标跟踪应用程序的启动步骤可以帮助了解在启动阶段花费的时间,但也可以将其用作更好地理解整个上下文生命周期的一种方式。 `AbstractApplicationContext`(及其子类)用一个 `ApplicationStartup’进行检测,它收集关于各种启动阶段的`StartupStep`数据: * 应用程序上下文生命周期(基本包扫描、配置类管理) * bean 生命周期(实例化、智能初始化、后处理) * 应用程序事件处理 下面是`AnnotationConfigApplicationContext`中的一个插装示例: 爪哇 ``` // create a startup step and start recording StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan"); // add tagging information to the current step scanPackages.tag("packages", () -> Arrays.toString(basePackages)); // perform the actual phase we're instrumenting this.scanner.scan(basePackages); // end the current step scanPackages.end(); ``` Kotlin ``` // create a startup step and start recording val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") // add tagging information to the current step scanPackages.tag("packages", () -> Arrays.toString(basePackages)) // perform the actual phase we're instrumenting this.scanner.scan(basePackages) // end the current step scanPackages.end() ``` 应用程序上下文已经通过多个步骤进行了检测。一旦记录了这些启动步骤,就可以用特定的工具收集、显示和分析这些步骤。有关现有启动步骤的完整列表,你可以查看[专用附录部分](#application-startup-steps)。 默认的`ApplicationStartup`实现是一个无操作的变体,以减少开销。这意味着默认情况下,在应用程序启动期间不会收集任何指标。 Spring Framework 附带了一种使用 爪哇 飞行记录器跟踪启动步骤的实现:“FlightRecorderApplicationStartup”。要使用这个变体,你必须在创建它之后立即将它的实例配置为`ApplicationContext`。 如果开发人员提供自己的“AbstractApplicationContext”子类,或者如果他们希望收集更精确的数据,那么他们也可以使用`ApplicationStartup`基础架构。 | |`ApplicationStartup`仅在应用程序启动期间使用,并且用于
核心容器;这绝不是对 爪哇 探查器或
等度量类库的替代。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 要开始收集自定义`StartupStep`,组件可以直接从应用程序上下文获得`ApplicationStartup`实例,使其组件实现`ApplicationStartupAware`,或者在任何注入点上请求`ApplicationStartup`类型。 | |开发人员在创建自定义启动步骤时不应使用`"spring.*"`名称空间。
此名称空间是为内部 Spring 使用而保留的,并且可能会发生更改。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 1.15.5.用于 Web 应用程序的方便的应用程序上下文实例化 例如,你可以使用“contextloader”来声明性地创建`ApplicationContext`实例。当然,你也可以通过使用`ApplicationContext`实现之一以编程方式创建`ApplicationContext`实例。 你可以使用`ContextLoaderListener`注册`ApplicationContext`,如下例所示: ``` contextConfigLocation /WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml org.springframework.web.context.ContextLoaderListener ``` 侦听器检查`contextConfigLocation`参数。如果参数不存在,侦听器将使用`/WEB-INF/applicationContext.xml`作为默认值。当参数确实存在时,侦听器使用预定义的分隔符(逗号、分号和空格)分隔`String`,并将这些值用作搜索应用程序上下文的位置。 Ant-样式的路径模式也被支持。例如`/WEB-INF/*Context.xml`(对于所有名称以 `context.xml’结尾且位于`WEB-INF`目录中的文件)和`/WEB-INF/**/*Context.xml`(对于`WEB-INF`任意子目录中的所有此类文件)。 #### 1.15.6.将 Spring `ApplicationContext`部署为 爪哇 EE RAR 文件 可以将 Spring `ApplicationContext`部署为 RAR 文件,将上下文及其所需的 Bean 类和库 JAR 封装在 爪哇 EE RAR 部署单元中。这相当于引导一个独立的`ApplicationContext`(仅托管在 爪哇 EE 环境中)能够访问 爪哇 EE 服务器设施。RAR 部署是部署无头 WAR 文件的一种更自然的替代方案——实际上,这是一个没有任何 HTTP 入口点的 WAR 文件,仅用于在 爪哇 EE 环境中引导 Spring“ApplicationContext”。 对于不需要 HTTP 入口点,而只包含消息端点和计划作业的应用程序上下文,RAR 部署是理想的。在这样的上下文中,Bean 可以使用应用服务器资源,例如 JTA 事务管理器和绑定 JNDI 的 JDBC“数据源”实例和 JMS`ConnectionFactory`实例,还可以通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施,向平台的 JMX 服务器注册。应用程序组件还可以通过 Spring 的`TaskExecutor`抽象与应用程序服务器的 JCA`WorkManager`进行交互。 有关 RAR 部署所涉及的配置细节,请参见[“SpringContextResourceAdapter”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/context/SpringContextResourceAdapter.html)类的 爪哇doc。 将 Spring ApplicationContext 作为 爪哇 EE RAR 文件进行简单部署: 1. 将所有应用程序类打包到一个 RAR 文件中(这是一个标准的 jar 文件,具有不同的文件扩展名)。 2. 将所有必需的库 JAR 添加到 RAR 归档的根目录中。 3. 添加 `meta-inf/ra.xml` 部署描述符(如[javadoc for `SpringContextResourceAdapter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/context/SpringContextResourceAdapter.html)所示)和相应的 Spring xml Bean 定义文件(通常为 `meta-inf/applicationcontext.xml`)。 4. 将生成的 RAR 文件放入应用程序服务器的部署目录中。 | |这样的 RAR 部署单元通常是独立的。它们不向外部世界公开组件
,甚至不向同一应用程序的其他模块公开组件。与基于
RAR 的`ApplicationContext`的交互通常通过它与
其他模块共享的 JMS 目的地进行。基于 RAR 的`ApplicationContext`也可以,例如,调度一些作业
或对文件系统中的新文件做出反应(或类似)。如果它需要允许来自外部的同步
访问,则它可以(例如)导出 RMI 端点,这可以由同一台机器上的其他应用程序模块使用
。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 1.16.the`BeanFactory` `BeanFactory`API 为 Spring 的 IOC 功能提供了底层基础。它的特定契约主要用于与 Spring 和相关的第三方框架的其他部分的集成,并且它的`DefaultListableBeanFactory`实现是更高级别`GenericApplicationContext`容器内的一个关键委托。 `BeanFactory`和相关接口(如`BeanFactoryAware`,`InitializingBean`,`DisaBleBean`)是其他框架组件的重要集成点。通过不需要任何注释,甚至不需要反射,它们允许容器与其组件之间进行非常有效的交互。应用程序级 bean 可能使用相同的回调接口,但通常更喜欢声明性依赖注入,或者通过注释,或者通过编程配置。 请注意,核心`BeanFactory`API 级别及其`DefaultListableBeanFactory`实现不对要使用的配置格式或任何组件注释进行假设。所有这些风格都是通过扩展(例如`XmlBeanDefinitionReader`和`AutowiredAnnotationBeanPostProcessor`)来实现的,并在共享的`BeanDefinition`对象上作为核心元数据表示进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。 #### 1.16.1.`BeanFactory`还是`ApplicationContext`? 本节解释`BeanFactory`和 `ApplicationContext’容器级别之间的区别以及对引导的影响。 除非你有充分的理由不这样做,否则你应该使用`ApplicationContext`,并使用 `GenericApplicationContext’及其子类`AnnotationConfigApplicationContext`作为自定义引导的常见实现。这些是 Spring 核心容器的主要入口点,用于所有常见目的:加载配置文件,触发 Classpath 扫描,以编程方式注册 Bean 定义和注释的类,以及(截至 5.0)注册功能 Bean 定义。 因为`ApplicationContext`包含了`BeanFactory`的所有功能,所以一般建议使用普通的`BeanFactory`,除非需要对 Bean 处理进行完全控制。在`ApplicationContext`(例如“GenericApplicationContext”实现)中,根据约定(即通过 Bean name 或 Bean type——特别是后处理器)检测几种 bean,而普通的`DefaultListableBeanFactory`对任何特殊的 bean 都是不可知的。 对于许多扩展的容器特性,例如注释处理和 AOP 代理,[“BeanPostProcessor”扩展点](#beans-factory-extension-bpp)是必不可少的。如果只使用普通的`DefaultListableBeanFactory`,则默认情况下不会检测到并激活这样的后处理器。这种情况可能会令人困惑,因为你的 Bean 配置实际上没有任何问题。相反,在这种情况下,容器需要通过额外的设置进行完全引导。 下表列出了`BeanFactory`和 `ApplicationContext’接口和实现提供的功能。 |特征|`BeanFactory`|`ApplicationContext`| |------------------------------------------------------------|-------------|--------------------| |Bean 实例化/布线| Yes | Yes | |集成的生命周期管理| No | Yes | |自动`BeanPostProcessor`注册| No | Yes | |自动`BeanFactoryPostProcessor`注册| No | Yes | |方便`MessageSource`访问(用于国际化)| No | Yes | |内置`ApplicationEvent`发布机制| No | Yes | 要显式地用`DefaultListableBeanFactory`注册 Bean 后处理器,需要以编程方式调用`addBeanPostProcessor`,如下例所示: 爪哇 ``` DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); // populate the factory with bean definitions // now register any needed BeanPostProcessor instances factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); factory.addBeanPostProcessor(new MyBeanPostProcessor()); // now start using the factory ``` Kotlin ``` val factory = DefaultListableBeanFactory() // populate the factory with bean definitions // now register any needed BeanPostProcessor instances factory.addBeanPostProcessor(AutowiredAnnotationBeanPostProcessor()) factory.addBeanPostProcessor(MyBeanPostProcessor()) // now start using the factory ``` 要将`BeanFactoryPostProcessor`应用于普通的`DefaultListableBeanFactory`,需要调用其`postProcessBeanFactory`方法,如下例所示: 爪哇 ``` DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); reader.loadBeanDefinitions(new FileSystemResource("beans.xml")); // bring in some property values from a Properties file PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer(); cfg.setLocation(new FileSystemResource("jdbc.properties")); // now actually do the replacement cfg.postProcessBeanFactory(factory); ``` Kotlin ``` val factory = DefaultListableBeanFactory() val reader = XmlBeanDefinitionReader(factory) reader.loadBeanDefinitions(FileSystemResource("beans.xml")) // bring in some property values from a Properties file val cfg = PropertySourcesPlaceholderConfigurer() cfg.setLocation(FileSystemResource("jdbc.properties")) // now actually do the replacement cfg.postProcessBeanFactory(factory) ``` 在这两种情况下,显式的注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种`ApplicationContext`变体比普通的 `DefaultListableBeanFactory’更受欢迎,特别是在典型的 Enterprise 设置中依赖`BeanFactoryPostProcessor`和`BeanPostProcessor`实例实现扩展容器功能的情况下。 | |一个`AnnotationConfigApplicationContext`已经注册了所有常见的注释后处理器
,并且可以通过配置注释在
下面引入额外的处理器,例如`@EnableTransactionManagement`。
在 Spring 的基于注释的配置模型的抽象级,
后处理器的概念变成了仅仅是内部容器的详细信息。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ## 2. Resources 本章介绍了 Spring 如何处理资源,以及如何使用 Spring 中的资源。它包括以下主题: * [Introduction](#resources-introduction) * [The `Resource` Interface](#resources-resource) * [Built-in `Resource` Implementations](#resources-implementations) * [The `ResourceLoader` Interface](#resources-resourceloader) * [The `ResourcePatternResolver` Interface](#resources-resourcepatternresolver) * [The `ResourceLoaderAware` Interface](#resources-resourceloaderaware) * [作为依赖关系的资源](#resources-as-dependencies) * [应用程序上下文和资源路径](#resources-app-ctx) ### 2.1.导言 遗憾的是,爪哇 的标准`java.net.URL`类和用于各种 URL 前缀的标准处理程序,对于所有对低级资源的访问都不够充分。例如,没有标准的`URL`实现可用于访问需要从 Classpath 或相对于“servletContext”获得的资源。虽然可以为专门的`URL`前缀注册新的处理程序(类似于现有的用于`http:`等前缀的处理程序),但这通常是相当复杂的,而且`URL`接口仍然缺乏一些理想的功能,例如检查所指向的资源是否存在的方法。 ### 2.2.`Resource`接口 Spring 的`Resource`接口位于`org.springframework.core.io.`包中,这意味着它是一个更有能力的接口,用于抽象对低级资源的访问。下面的清单提供了`Resource`接口的概述。有关更多详细信息,请参见[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)爪哇doc。 ``` public interface Resource extends InputStreamSource { boolean exists(); boolean isReadable(); boolean isOpen(); boolean isFile(); URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; ReadableByteChannel readableChannel() throws IOException; long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String relativePath) throws IOException; String getFilename(); String getDescription(); } ``` 正如`Resource`接口的定义所示,它扩展了`InputStreamSource`接口。下面的清单显示了`InputStreamSource`接口的定义: ``` public interface InputStreamSource { InputStream getInputStream() throws IOException; } ``` 来自`Resource`接口的一些最重要的方法是: * `getInputStream()`:定位并打开资源,返回一个`InputStream`用于从资源中读取。预计每次调用都会返回一个新的“inputstream”。这是来电者的责任,以关闭该流。 * `exists()`:返回一个`boolean`,指示此资源是否实际以物理形式存在。 * `isOpen()`:返回一个`boolean`,指示此资源是否表示具有开放流的句柄。如果`true`,则`InputStream`不能多次读取,必须只读一次,然后关闭,以避免资源泄漏。对于所有常见的资源实现,返回`false`,但`InputStreamResource`除外。 * `getDescription()`:返回此资源的描述,用于在使用该资源时进行错误输出。这通常是完全限定的文件名或资源的实际 URL。 其他方法允许你获得代表资源的实际`URL`或`File`对象(如果底层实现是兼容的并且支持该功能)。 `Resource`接口的一些实现还实现了用于支持对其进行写入的资源的扩展[“可写资源”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/WritableResource.html)接口。 Spring 本身广泛地使用`Resource`抽象,在需要资源时作为许多方法签名中的参数类型。在一些 Spring API 中的其他方法(例如各种`ApplicationContext`实现的构造函数)采用一个 `string’,该 string’以朴素或简单的形式用于创建适合该上下文实现的`Resource`,或者通过`String`路径上的特殊前缀,让调用者指定必须创建和使用特定的`Resource`实现。 虽然`Resource`接口在 Spring 和 Spring 中被大量使用,但实际上,在你自己的代码中将其本身用作一个通用实用程序类来访问资源是非常方便的,即使你的代码不知道或不关心 Spring 的任何其他部分。虽然这将你的代码耦合到 Spring,但它实际上只将它耦合到这一小组实用程序类,它可以作为`URL`的更强大的替代,并且可以被认为与你将用于此目的的任何其他库等同。 | |`Resource`抽象不会取代功能。它将它封装在
可能的位置。例如,`UrlResource`包装一个 URL,并使用包装好的`URL`来完成其
工作。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 2.3.内置`Resource`实现 Spring 包括几个内置`Resource`实现方式: * [`UrlResource`](#resources-implementations-urlresource) * [“classpathresource”](#resources-implementations-classpathresource) * [“文件系统资源”](#resources-implementations-filesystemresource) * [`PathResource`](#resources-implementations-pathresource) * [“ServletContextResource”](#resources-implementations-servletcontextresource) * [InputStreamResource](#resources-implementations-inputstreamresource) * [ByteArrayResource](#resources-implementations-bytearrayresource) 有关 Spring 中可用的`Resource`实现的完整列表,请参阅[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)爪哇doc 中的“所有已知实现类”部分。 #### 2.3.1.`UrlResource` `UrlResource`封装`java.net.URL`,并可用于访问通常通过 URL 可访问的任何对象,例如文件、HTTPS 目标、FTP 目标和其他对象。所有的 URL 都有一个标准化的`String`表示,使得适当的标准化前缀被用来从另一个 URL 类型中指示一个 URL 类型。这包括用于访问文件系统路径的“file:”,用于通过 HTTPS 协议访问资源的`https:`,用于通过 FTP 访问资源的`ftp:`,以及其他。 `UrlResource`是由 爪哇 代码通过显式地使用`UrlResource`构造函数创建的,但是当你调用一个 API 方法时,它通常是隐式创建的,该 API 方法接受一个旨在表示路径的`String`参数。对于后一种情况,爪哇Beans`PropertyEditor`最终决定创建哪种类型的`Resource`。如果路径字符串包含一个众所周知的前缀(对于属性编辑器来说,就是)(例如`classpath:`),那么它将为该前缀创建一个适当的专门的`Resource`。但是,如果它不能识别前缀,它将假设字符串是一个标准的 URL 字符串,并创建一个`UrlResource`。 #### 2.3.2.`ClassPathResource` 这个类表示应该从 Classpath 获得的资源。它使用线程上下文类加载器、给定类加载器或给定类来加载资源。 如果类路径资源驻留在文件系统中,而不是驻留在 jar 中且尚未(通过 Servlet 引擎或任何环境)扩展到文件系统的 Classpath 资源,则此`Resource`实现支持解析为`java.io.File`。为了解决这个问题,各种`Resource`实现总是支持分辨率为`java.net.URL`。 `ClassPathResource`是由 爪哇 代码通过显式地使用`ClassPathResource`构造函数创建的,但是当你调用一个 API 方法时,它通常是隐式地创建的,该 API 方法接受一个表示路径的 `string’参数。对于后一种情况,爪哇Beans`PropertyEditor` 识别字符串路径上的特殊前缀`classpath:`,并在这种情况下创建`ClassPathResource`。 #### 2.3.3.`FileSystemResource` 这是`Resource`句柄的`java.io.File`实现。它还支持 `java.蔚来.file.path` 句柄,应用 Spring 的标准基于字符串的路径转换,但通过`java.nio.file.Files`API 执行所有操作。对于纯粹的基于“java.蔚来.path.path”的支持,可以使用`PathResource`。`FileSystemResource`支持分辨率为`File`和`URL`。 #### 2.3.4.`PathResource` 这是用于`Resource`句柄的`java.nio.file.Path`实现,通过`Path`API 执行所有操作和转换。它以`File`和`URL`的形式支持分辨率,并且还实现了扩展的`WritableResource`接口。`PathResource`实际上是一种纯粹的基于`java.nio.path.Path`的替代方法,与`FileSystemResource`具有不同的`createRelative`行为。 #### 2.3.5.`ServletContextResource` 这是`Resource`资源的`ServletContext`实现,用于解释相关 Web 应用程序根目录中的相对路径。 它始终支持流访问和 URL 访问,但仅当 Web 应用程序归档被扩展并且资源在文件系统上时才允许`java.io.File`访问。它是否被扩展并在文件系统上或直接从 jar 或类似数据库的其他地方(这是可以想象的)访问,实际上取决于 Servlet 容器。 #### 2.3.6.`InputStreamResource` `InputStreamResource`是给定`InputStream`的`Resource`实现。只有在不适用特定的`Resource`实现的情况下,才应该使用它。特别地,在可能的情况下优选`ByteArrayResource`或任何基于文件的`Resource`实现方式。 与其他`Resource`实现不同,这是对已经打开的资源的描述符。因此,它从`isOpen()`返回`true`。如果你需要将资源描述符保存在某个地方,或者如果你需要多次读取流,请不要使用它。 #### 2.3.7.`ByteArrayResource` 这是给定字节数组的`Resource`实现。它为给定字节数组创建一个“BytearrayInputStream”。 它对于从任何给定的字节数组加载内容都很有用,而不必使用一次性`InputStreamResource`。 ### 2.4.`ResourceLoader`接口 `ResourceLoader`接口旨在通过可以返回(即加载)`Resource`实例的对象来实现。下面的清单显示了`ResourceLoader`接口定义: ``` public interface ResourceLoader { Resource getResource(String location); ClassLoader getClassLoader(); } ``` 所有应用程序上下文都实现`ResourceLoader`接口。因此,可以使用所有应用程序上下文来获得`Resource`实例。 当你在特定的应用程序上下文中调用`getResource()`,并且指定的位置路径没有特定的前缀时,你将返回一个适合该特定应用程序上下文的`Resource`类型。例如,假设对`ClassPathXmlApplicationContext`实例运行了以下代码片段: 爪哇 ``` Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); ``` Kotlin ``` val template = ctx.getResource("some/resource/path/myTemplate.txt") ``` 对于`ClassPathXmlApplicationContext`,该代码返回`ClassPathResource`。如果对`FileSystemXmlApplicationContext`实例运行相同的方法,它将返回`FileSystemResource`。对于`WebApplicationContext`,它将返回一个“servletContextResource”。类似地,它将为每个上下文返回适当的对象。 因此,你可以以适合特定应用程序上下文的方式加载资源。 另一方面,你也可以通过指定特殊的`classpath:`前缀来强制使用`ClassPathResource`,而不考虑应用程序的上下文类型,如下例所示: 爪哇 ``` Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); ``` Kotlin ``` val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt") ``` 类似地,你可以通过指定任何标准的 `java.net.url’前缀来强制使用`UrlResource`。以下示例使用`file`和`https`前缀: 爪哇 ``` Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); ``` Kotlin ``` val template = ctx.getResource("file:///some/resource/path/myTemplate.txt") ``` 爪哇 ``` Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt"); ``` Kotlin ``` val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt") ``` 下表总结了将`String`对象转换为`Resource`对象的策略: | Prefix | Example |解释| |----------|--------------------------------|----------------------------------------------------------------------------------------------------------------------| |classpath:|`classpath:com/myapp/config.xml`|从 Classpath 加载。| | file: | `file:///data/config.xml` |从文件系统加载为`URL`。另见[“文件系统资源”警告](#resources-filesystemresource-caveats)。| | https: | `https://myserver/logo.png` |加载为`URL`。| | (none) | `/data/config.xml` |取决于底层`ApplicationContext`。| ### 2.5.`ResourcePatternResolver`接口 `ResourcePatternResolver`接口是`ResourceLoader`接口的扩展,它定义了将位置模式(例如, Ant 样式的路径模式)解析为`Resource`对象的策略。 ``` public interface ResourcePatternResolver extends ResourceLoader { String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; Resource[] getResources(String locationPattern) throws IOException; } ``` 如上面所示,该接口还为来自类路径的所有匹配资源定义了一个特殊的`classpath*:`资源前缀。请注意,在这种情况下,资源位置应该是一个没有占位符的路径——例如,“ Classpath *:/config/beans.xml”。 jar 类路径中的文件或不同的目录可以包含具有相同路径和相同名称的多个文件。有关使用`classpath*:`资源前缀的通配符支持的更多详细信息,请参见[应用程序上下文构造函数资源路径中的通配符](#resources-app-ctx-wildcards-in-resource-paths)及其子节。 可以检查传入的-in`ResourceLoader`(例如,通过[“资源装载器意识”](#resources-resourceloaderaware)语义提供的一个)是否也实现了此扩展接口。 `PathMatchingResourcePatternResolver`是一个独立的实现,可以在`ApplicationContext`之外使用,并且`ResourceArrayPropertyEditor`也用于填充`Resource[]` Bean 属性。`PathMatchingResourcePatternResolver`能够将指定的资源定位路径解析为一个或多个匹配的`Resource`对象。源路径可以是一个简单的路径,该路径具有到目标“资源”的一对一映射,或者可替换地包含特殊的`classpath*:`前缀和/或内部 Ant 样式的正则表达式(使用 Spring 的 `org.springframework.util.antpathmatcher’实用程序进行匹配)。后者实际上都是通配符。 | |在任何标准`ApplicationContext`中,默认的`ResourceLoader`实际上是
的一个实例`PathMatchingResourcePatternResolver`,它实现了`ResourcePatternResolver`接口。对于`ApplicationContext`实例本身也是如此,它也
实现了`ResourcePatternResolver`接口并将其委托给默认的 `pathmatchingresourcepatternresolver’。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 2.6.`ResourceLoaderAware`接口 `ResourceLoaderAware`接口是一种特殊的回调接口,用于标识期望提供`ResourceLoader`引用的组件。下面的清单显示了`ResourceLoaderAware`接口的定义: ``` public interface ResourceLoaderAware { void setResourceLoader(ResourceLoader resourceLoader); } ``` 当一个类实现`ResourceLoaderAware`并被部署到应用程序上下文中(作为 Spring 管理的 Bean)时,应用程序上下文将其识别为`ResourceLoaderAware`。然后,应用程序上下文调用`setResourceLoader(ResourceLoader)`,将自身作为参数提供(请记住, Spring 中的所有应用程序上下文都实现`ResourceLoader`接口)。 由于`ApplicationContext`是`ResourceLoader`, Bean 还可以实现 `ApplicationContextAware’接口,并直接使用提供的应用程序上下文来加载资源。然而,一般来说,如果这就是你所需要的,那么最好使用专门的`ResourceLoader`接口。该代码将仅耦合到资源加载接口(可被视为实用程序接口),而不耦合到整个 Spring `ApplicationContext’接口。 在应用程序组件中,还可以依赖`ResourceLoader`的自动布线作为实现`ResourceLoaderAware`接口的替代方案。*传统的*`constructor’和`byType`自动布线模式(如[自动布线合作者](#beans-factory-autowire)中所述)能够分别为构造函数参数或 setter 方法参数提供`ResourceLoader`。要获得更大的灵活性(包括自动连接字段和多个参数方法的能力),请考虑使用基于注释的自动连接功能。在这种情况下,`ResourceLoader`将自动连接到一个字段、构造函数参数或方法参数中,只要所讨论的字段、构造函数或方法带有`ResourceLoader`注释,这些参数就需要`@Autowired`类型。有关更多信息,请参见[Using `@Autowired`](#beans-autowired-annotation)。 | |要为包含通配符
的资源路径加载一个或多个`Resource`对象,或者使用特殊的`classpath*:`资源前缀,请考虑将[“ResourcePatternResolver”](#resources-resourcepatternresolver)的实例自动连接到你的
应用程序组件中,而不是`ResourceLoader`。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 2.7.作为依赖关系的资源 如果 Bean 本身将通过某种动态过程来确定和提供资源路径,那么 Bean 使用`ResourceLoader`或 `ResourcePatternResolver’接口来加载资源可能是有意义的。例如,考虑加载某种类型的模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,那么完全取消使用 `ResourceLoader’接口(或`ResourcePatternResolver`接口)是有意义的,让 Bean 公开它需要的`Resource`属性,并期望将它们注入其中。 然后注入这些属性的原因是,所有应用程序上下文都注册并使用一个特殊的 爪哇Beans`PropertyEditor`,它可以将`String`路径转换为`Resource`对象。例如,下面的`MyBean`类具有类型`template`的`Resource`属性。 爪哇 ``` package example; public class MyBean { private Resource template; public setTemplate(Resource template) { this.template = template; } // ... } ``` Kotlin ``` class MyBean(var template: Resource) ``` 在 XML 配置文件中,`template`属性可以为该资源配置一个简单的字符串,如下例所示: ``` ``` 请注意,资源路径没有前缀。因此,由于应用程序上下文本身将被用作`ResourceLoader`,因此根据应用程序上下文的确切类型,资源将通过 `classpathresource’、`FileSystemResource`或`ServletContextResource`加载。 如果需要强制使用特定的`Resource`类型,则可以使用前缀。下面的两个示例展示了如何强制执行`ClassPathResource`和`UrlResource`(后者用于访问文件系统中的文件): ``` ``` ``` ``` 如果对`MyBean`类进行重构以用于注释驱动的配置,则`myTemplate.txt`的路径可以存储在一个名为`template.path`的键下——例如,在 Spring `Environment`提供的属性文件中(参见[环境抽象](#beans-environment))。然后可以使用属性占位符通过`@Value`注释引用模板路径(参见[Using `@Value`](#beans-value-annotations))。 Spring 将检索模板路径的值作为字符串,而一个特殊的`PropertyEditor`将把字符串转换为一个`Resource`对象,以注入到`MyBean`构造函数中。下面的示例演示了如何实现这一点。 爪哇 ``` @Component public class MyBean { private final Resource template; public MyBean(@Value("${template.path}") Resource template) { this.template = template; } // ... } ``` Kotlin ``` @Component class MyBean(@Value("\${template.path}") private val template: Resource) ``` 如果我们希望支持在 Classpath 中的多个位置的相同路径下发现的多个模板——例如,在 Classpath 中的多个 JAR 中——我们可以使用特殊的`classpath*:`前缀和通配符将`templates.path`键定义为 ` Classpath *:/config/templates/*.TXT`。如果我们按如下方式重新定义`MyBean`类, Spring 将把模板路径模式转换为`Resource`对象的数组,这些对象可以被注入到`MyBean`构造函数中。 爪哇 ``` @Component public class MyBean { private final Resource[] templates; public MyBean(@Value("${templates.path}") Resource[] templates) { this.templates = templates; } // ... } ``` Kotlin ``` @Component class MyBean(@Value("\${templates.path}") private val templates: Resource[]) ``` ### 2.8.应用程序上下文和资源路径 本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式、如何使用通配符以及其他详细信息。 #### 2.8.1.构造应用程序上下文 应用程序上下文构造函数(对于特定的应用程序上下文类型)通常使用字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。 当这样的位置路径不具有前缀时,特定的`Resource`类型从该路径构建并用于加载 Bean 定义所依赖的并且适合于特定的应用程序上下文。例如,考虑下面的示例,它创建了一个“ClassPathXMLApplicationContext”: 爪哇 ``` ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); ``` Kotlin ``` val ctx = ClassPathXmlApplicationContext("conf/appContext.xml") ``` Bean 定义是从 Classpath 加载的,因为使用了`ClassPathResource`。但是,请考虑以下示例,该示例创建了`FileSystemXmlApplicationContext`: 爪哇 ``` ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); ``` Kotlin ``` val ctx = FileSystemXmlApplicationContext("conf/appContext.xml") ``` 现在 Bean 定义是从文件系统位置(在本例中,相对于当前工作目录)加载的。 请注意,在位置路径上使用特殊的`classpath`前缀或标准的 URL 前缀覆盖了为加载 Bean 定义而创建的默认类型`Resource`。考虑以下示例: 爪哇 ``` ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); ``` Kotlin ``` val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml") ``` 使用`FileSystemXmlApplicationContext`加载来自 Classpath 的 Bean 定义。然而,它仍然是`FileSystemXmlApplicationContext`。如果随后将其用作“ResourceLoader”,则任何未带前缀的路径仍将被视为文件系统路径。 ##### 构造`ClassPathXmlApplicationContext`实例——快捷方式 `ClassPathXmlApplicationContext`公开了许多构造函数,以实现方便的实例化。基本思想是,你可以只提供一个字符串数组,该字符串数组仅包含 XML 文件本身的文件名(不包含前导路径信息),还可以提供`Class`。然后,`ClassPathXmlApplicationContext`从提供的类派生路径信息。 考虑以下目录布局: ``` com/ example/ services.xml repositories.xml MessengerService.class ``` 下面的示例展示了如何实例化由名为`services.xml`和`repositories.xml`(位于 Classpath 上)的文件中定义的 bean 组成的`ClassPathXmlApplicationContext`实例: 爪哇 ``` ApplicationContext ctx = new ClassPathXmlApplicationContext( new String[] {"services.xml", "repositories.xml"}, MessengerService.class); ``` Kotlin ``` val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java) ``` 有关各种构造函数的详细信息,请参见[“ClassPathXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)Javadoc。 #### 2.8.2.应用程序上下文构造函数资源路径中的通配符 ### 应用程序上下文构造函数值中的资源路径可以是简单的路径(如前面所示),其中每个路径都具有到目标`Resource`的一对一映射,或者,或者,也可以包含特殊的`classpath*:`前缀或内部 Ant 样式的模式(通过使用 Spring 的`PathMatcher`实用工具进行匹配)。后者实际上都是通配符。 这种机制的一个用途是当你需要执行组件样式的应用程序组装时。所有组件都可以*发布*上下文定义片段到一个众所周知的位置路径,并且,当最终的应用程序上下文使用前缀为 ` Classpath ** 的相同路径创建时:`,所有组件片段都会被自动拾取。 请注意,此通配符是特定于在应用程序上下文构造函数中使用资源路径的(或者当你直接使用`PathMatcher`实用程序类层次结构时),并在构建时解析。它与`Resource`类型本身无关。你不能使用`classpath*:`前缀来构造一个实际的`Resource`,因为一个资源一次只指向一个资源。 ##### Ant-样式模式 路径位置可以包含 Ant 样式的模式,如下例所示: ``` /WEB-INF/*-context.xml com/mycompany/**/applicationContext.xml file:C:/some/path/*-context.xml classpath:com/mycompany/**/applicationContext.xml ``` 当路径位置包含 Ant 样式的模式时,解析器遵循一个更复杂的过程来尝试解析通配符。它为到最后一个非通配符段的路径生成`Resource`,并从中获得一个 URL。如果此 URL 不是`jar:`URL 或特定于容器的变体(例如 WebLogic 中的`zip:`,WebSphere 中的`wsjar`,以此类推),则从中获得一个`java.io.File`,并通过遍历文件系统来解析通配符。在 jar URL 的情况下,解析器要么从它获取 `java.net.jarurlconnection’,要么手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。 ###### 对可移植性的影响 如果指定的路径已经是`file`URL(由于基本的 `ResourceLoader’是一个文件系统,所以隐式地或显式地),那么通配符将保证以完全可移植的方式工作。 如果指定的路径是`classpath`位置,则解析器必须通过进行`Classloader.getResource()`调用来获得最后一个非通配符路径段 URL。因为这只是路径的一个节点(而不是末尾的文件),所以它实际上是未定义的(在 `classloader’Javadoc 中),在这种情况下返回的正是哪种类型的 URL。在实践中,它总是表示目录(其中 Classpath 资源解析为文件系统位置)或某种类型的 jar URL(其中 Classpath 资源解析为 jar 位置)的`java.io.File`。不过,这种操作仍然存在移植性方面的问题。 如果为最后一个非通配符段获得了 jar URL,则解析器必须能够从其获得`java.net.JarURLConnection`或手动解析 jar URL,以便能够遍历 jar 中的内容并解析通配符。这在大多数环境中确实有效,但在其他环境中失败,我们强烈建议在依赖它之前,在你的特定环境中对来自 JAR 的资源的通配符解析进行彻底测试。 ##### `classpath*:`前缀 在构建基于 XML 的应用程序上下文时,位置字符串可以使用特殊的`classpath*:`前缀,如下例所示: Java ``` ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); ``` Kotlin ``` val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml") ``` 这个特殊的前缀指定必须获得所有与给定名称匹配的 Classpath 资源(在内部,这基本上是通过调用 `classloader.getResources(…)’实现的),然后将其合并以形成最终的应用程序上下文定义。 | |通配符 Classpath 依赖于底层“classloader”的`getResources()`方法。由于现在大多数应用程序服务器都提供它们自己的`ClassLoader`实现,因此行为可能会有所不同,特别是在处理 jar 文件时。一个
检查`classpath*`是否有效的简单测试是使用`ClassLoader`在 Classpath 上的 jar 内从
加载一个文件:`getclass().getclassloader().getresources(“”)`。使用具有相同名称但驻留在两个不同位置的
文件尝试此测试——例如,在 Classpath 上具有相同名称和相同路径但位于不同 JAR 中的
文件。如果返回了
不合适的结果,请检查应用服务器文档中的设置
,这些设置可能会影响`ClassLoader`行为。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你还可以在位置路径的其余部分(例如,`classpath*:META-INF/*-beans.xml`)中将`classpath*:`前缀与`PathMatcher`模式相结合。在这种情况下,解析策略相当简单:在最后一个非通配符路径段上使用`ClassLoader.getResources()`调用,以获取类装入器层次结构中的所有匹配资源,然后关闭每个资源,前面描述的`PathMatcher`解析策略也用于通配符的子路径。 ##### 与通配符有关的其他注释 注意`classpath*:`,当与 Ant 样式的模式相结合时,在模式开始之前,仅对至少一个根目录可靠地工作,除非实际的目标文件驻留在文件系统中。这意味着,像“ Classpath *:*.xml”这样的模式可能不会从 jar 文件的根目录检索文件,而只会从扩展目录的根目录检索文件。 Spring 检索 Classpath 条目的能力源于 JDK 的 `classloader.getResources()’方法,该方法仅返回空字符串的文件系统位置(指示要搜索的潜在根)。 Spring 还在 jar 文件中评估 `urlclassloader` 运行时配置和`java.class.path`清单,但这不能保证导致可移植行为。 | |Classpath 包的扫描需要在 Classpath 中存在相应的目录
条目。当你使用 Ant 构建 JAR 时,不要激活 jar 任务的`files-only`开关。此外,在某些环境中, Classpath 目录可能不会基于安全性
策略而被公开——例如,在 JDK1.7.0\_45
上的独立应用程序以及更高的环境中(这需要在你的清单中设置“可信库”)。参见[https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources](https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources))。

在 JDK9 的模块路径(拼图)上, Spring 的 Classpath 扫描通常如预期的那样工作。
将资源放入专用目录在这里也是非常值得推荐的,
在搜索 jar 文件根级别时避免了前面提到的可移植性问题。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Ant-样式模式具有资源,如果要搜索的根包在多个 Classpath 位置可用,则不能保证找到匹配的资源。考虑以下资源位置示例: ``` com/mycompany/package1/service-context.xml ``` 现在考虑一个 Ant 风格的路径,可能有人会使用它来尝试找到该文件: ``` classpath:com/mycompany/**/service-context.xml ``` 这样的资源可能仅存在于 Classpath 中的一个位置中,但是当使用诸如前面的示例的路径来尝试解析它时,解析器工作于由`getResource("com/mycompany");`返回的(第一个)URL。如果此基本包节点存在于多个`ClassLoader`位置中,则所需的资源可能不存在于第一个位置中。因此,在这种情况下,你应该更喜欢使用具有相同 Ant 样式模式的`classpath*:`,该模式搜索包含 `com.mycompany’基本包的所有 Classpath 位置:`classpath*:com/mycompany/**/service-context.xml`。 #### 2.8.3.`FileSystemResource`注意事项 不附加到`FileSystemResource`的`FileSystemApplicationContext`(即当`FileSystemApplicationContext`不是实际的`ResourceLoader`时)按预期处理绝对路径和相对路径。相对路径是相对于当前工作目录的,而绝对路径是相对于文件系统的根的。 但是,由于向后兼容性(历史原因)的原因,当“文件系统映射上下文”是`ResourceLoader`时,这种情况就会改变。“filesystemapplicationContext”强制所有附加的`FileSystemResource`实例将所有位置路径视为相对的,无论它们是否以前导斜杠开始。在实践中,这意味着以下示例是等效的: Java ``` ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/context.xml"); ``` Kotlin ``` val ctx = FileSystemXmlApplicationContext("conf/context.xml") ``` Java ``` ApplicationContext ctx = new FileSystemXmlApplicationContext("/conf/context.xml"); ``` Kotlin ``` val ctx = FileSystemXmlApplicationContext("/conf/context.xml") ``` 下面的例子也是等价的(尽管它们不同是有意义的,因为一种情况是相对的,另一种情况是绝对的): Java ``` FileSystemXmlApplicationContext ctx = ...; ctx.getResource("some/resource/path/myTemplate.txt"); ``` Kotlin ``` val ctx: FileSystemXmlApplicationContext = ... ctx.getResource("some/resource/path/myTemplate.txt") ``` Java ``` FileSystemXmlApplicationContext ctx = ...; ctx.getResource("/some/resource/path/myTemplate.txt"); ``` Kotlin ``` val ctx: FileSystemXmlApplicationContext = ... ctx.getResource("/some/resource/path/myTemplate.txt") ``` 在实践中,如果需要真正的绝对文件系统路径,则应避免使用带有`FileSystemResource`或`FileSystemXmlApplicationContext`的绝对路径,并使用`UrlResource`的 URL 前缀强制使用`file:`。下面的例子说明了如何做到这一点: Java ``` // actual context type doesn't matter, the Resource will always be UrlResource ctx.getResource("file:///some/resource/path/myTemplate.txt"); ``` Kotlin ``` // actual context type doesn't matter, the Resource will always be UrlResource ctx.getResource("file:///some/resource/path/myTemplate.txt") ``` Java ``` // force this FileSystemXmlApplicationContext to load its definition via a UrlResource ApplicationContext ctx = new FileSystemXmlApplicationContext("file:///conf/context.xml"); ``` Kotlin ``` // force this FileSystemXmlApplicationContext to load its definition via a UrlResource val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml") ``` ## 3. 验证、数据绑定和类型转换 # 将验证视为业务逻辑有其优点和缺点, Spring 提供了一种验证(和数据绑定)设计,该设计不排除其中的任何一种。具体地说,验证不应该绑定到 Web 层,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题, Spring 提供了`Validator`契约,该契约在应用程序的每一层中都是基本的且非常可用的。 数据绑定对于让用户输入与应用程序的域模型(或用于处理用户输入的任何对象)动态绑定非常有用。 Spring 提供了恰如其分的名称`DataBinder`来做到这一点。`Validator`和 `databinder’组成了`validation`包,该包主要用于但不限于 Web 层。 `BeanWrapper`是 Spring 框架中的一个基本概念,并在许多地方使用。但是,你可能不需要直接使用`BeanWrapper`。但是,由于这是参考文档,我们认为可能需要进行一些解释。我们将在本章中解释`BeanWrapper`,因为如果你打算使用它,那么在尝试将数据绑定到对象时,你很可能会使用它。 Spring 的`DataBinder`和较低级别的`BeanWrapper`都使用`PropertyEditorSupport`实现来解析和格式化属性值。`PropertyEditor`和 `PropertYeditorSupport’类型是 JavaBeans 规范的一部分,本章也对此进行了说明。 Spring 3 引入了一个`core.convert`包,该包提供了一般的类型转换功能,以及用于格式化 UI 字段值的更高级别的“格式”包。你可以使用这些包作为“propertyeditorsupport”实现的更简单的替代方案。本章还对这些问题进行了讨论。 Spring 通过设置基础设施和 Spring 自己的`Validator`合同的适配器支持 Java Bean 验证。应用程序可以全局启用 Bean 验证一次,如[Java Bean Validation](#validation-beanvalidation)中所述,并专门用于所有验证需求。在 Web 层中,应用程序还可以根据`DataBinder`注册控制器-Local Spring `Validator’实例,如[Configuring a `DataBinder`](#validation-binder)中所述,这对于插入自定义验证逻辑是有用的。 ### 3.1.通过使用 Spring 的验证器接口进行验证 Spring 具有`Validator`接口,可以使用该接口来验证对象。“validator”接口通过使用`Errors`对象来工作,这样在验证时,验证器可以向`Errors`对象报告验证失败。 考虑以下一个小数据对象的示例: Java ``` public class Person { private String name; private int age; // the usual getters and setters... } ``` Kotlin ``` class Person(val name: String, val age: Int) ``` 下一个示例通过实现`org.springframework.validation.Validator`接口的以下两个方法,为`Person`类提供了验证行为: * `supports(Class)`:这个`Validator`可以验证所提供的`Class`实例吗? * `validate(Object, org.springframework.validation.Errors)`:验证给定的对象,在验证错误的情况下,用给定的`Errors`对象注册那些对象。 实现`Validator`相当简单,特别是当你知道 Spring 框架还提供的 `Validationutils’助手类时。下面的示例为`Person`实例实现`Validator`: Java ``` public class PersonValidator implements Validator { /** * This Validator validates only Person instances */ public boolean supports(Class clazz) { return Person.class.equals(clazz); } public void validate(Object obj, Errors e) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); Person p = (Person) obj; if (p.getAge() < 0) { e.rejectValue("age", "negativevalue"); } else if (p.getAge() > 110) { e.rejectValue("age", "too.darn.old"); } } } ``` Kotlin ``` class PersonValidator : Validator { /** * This Validator validates only Person instances */ override fun supports(clazz: Class<*>): Boolean { return Person::class.java == clazz } override fun validate(obj: Any, e: Errors) { ValidationUtils.rejectIfEmpty(e, "name", "name.empty") val p = obj as Person if (p.age < 0) { e.rejectValue("age", "negativevalue") } else if (p.age > 110) { e.rejectValue("age", "too.darn.old") } } } ``` 在`ValidationUtils`类上的`static``rejectIfEmpty(..)`方法用于拒绝`name`属性,如果它是`null`或空字符串。看看[“验证”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/ValidationUtils.html)Javadoc,看看除了前面显示的示例之外,它还提供了什么功能。 虽然可以实现单个`Validator`类来验证富对象中的每个嵌套对象,但最好是将每个嵌套对象类的验证逻辑封装在其自己的`Validator`实现中。“rich”对象的一个简单示例是`Customer`,它由两个`String`属性(第一个和第二个名称)和一个复杂的`Address`对象组成。`Address`对象可以独立于`Customer`对象使用,因此已经实现了一个不同的`AddressValidator`对象。如果你希望你的`CustomerValidator`重用`AddressValidator`类中包含的逻辑而不使用复制粘贴,则可以在你的`CustomerValidator`中使用依赖注入或实例化`AddressValidator`,如下例所示: Java ``` public class CustomerValidator implements Validator { private final Validator addressValidator; public CustomerValidator(Validator addressValidator) { if (addressValidator == null) { throw new IllegalArgumentException("The supplied [Validator] is " + "required and must not be null."); } if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException("The supplied [Validator] must " + "support the validation of [Address] instances."); } this.addressValidator = addressValidator; } /** * This Validator validates Customer instances, and any subclasses of Customer too */ public boolean supports(Class clazz) { return Customer.class.isAssignableFrom(clazz); } public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); Customer customer = (Customer) target; try { errors.pushNestedPath("address"); ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); } finally { errors.popNestedPath(); } } } ``` Kotlin ``` class CustomerValidator(private val addressValidator: Validator) : Validator { init { if (addressValidator == null) { throw IllegalArgumentException("The supplied [Validator] is required and must not be null.") } if (!addressValidator.supports(Address::class.java)) { throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.") } } /* * This Validator validates Customer instances, and any subclasses of Customer too */ override fun supports(clazz: Class<>): Boolean { return Customer::class.java.isAssignableFrom(clazz) } override fun validate(target: Any, errors: Errors) { ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required") ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required") val customer = target as Customer try { errors.pushNestedPath("address") ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors) } finally { errors.popNestedPath() } } } ``` 将验证错误报告给传递给验证器的`Errors`对象。在 Spring Web MVC 的情况下,你可以使用``标记来检查错误消息,但是你也可以自己检查`Errors`对象。有关其提供的方法的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/Errors.html)。 ### 3.2.将代码解析为错误消息 我们涵盖了数据库和验证。本节介绍输出与验证错误对应的消息。在[前一节](#validator)中显示的示例中,我们拒绝了`name`和`age`字段。如果我们希望通过使用“MessageSource”输出错误消息,那么我们可以使用在拒绝字段时提供的错误代码(本例中是“name”和“age”)来输出错误消息。当你从`Errors`接口调用(通过使用`ValidationUtils`类等直接或间接调用)`rejectValue`或其他`reject`方法之一时,底层实现不仅注册了你传入的代码,而且还注册了许多额外的错误代码。`MessageCodesResolver`决定了哪个错误编码`Errors`接口寄存器。默认情况下,使用“DefaultMessageCodesResolver”,它(例如)不仅用给出的代码注册消息,还注册包含传递给拒绝方法的字段名称的消息。因此,如果通过使用`rejectValue("age", "too.darn.old")`拒绝字段,除了`too.darn.old`代码外, Spring 还会注册`too.darn.old.age`和 `too.darn.old.age.INT’(第一个包括字段名称,第二个包括字段的类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。 有关`MessageCodesResolver`和默认策略的更多信息,可以分别在[“MessageCodesResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/MessageCodesResolver.html)和[“DefaultMessageCodesResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/DefaultMessageCodesResolver.html)的 Javadoc 中找到。 ### 3.3. Bean 操纵和`BeanWrapper` `org.springframework.beans`包遵循 JavaBeans 标准。JavaBean 是一个具有缺省无参数构造函数的类,它遵循一个命名约定,其中(例如)一个名为`bingoMadness`的属性将具有一个 setter 方法`setBingoMadness(..)`和一个 getter 方法`getBingoMadness()`。有关 JavaBeans 和规范的更多信息,请参见[javabeans](https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html)。 Bean 包中一个非常重要的类是`BeanWrapper`接口及其相应的实现。正如从 Javadoc 中引用的,“BeanWrapper”提供了设置和获取属性值(单独或批量)、获取属性描述符和查询属性以确定它们是否可读或可写的功能。此外,`BeanWrapper`提供了对嵌套属性的支持,使子属性上的属性的设置具有无限的深度。“BeanWrapper”还支持添加标准 JavaBeans`PropertyChangeListeners`和`VetoableChangeListeners`的功能,而不需要在目标类中支持代码。最后但并非最不重要的是,`BeanWrapper`提供了对设置索引属性的支持。`BeanWrapper`通常不被应用程序代码直接使用,而是由 `databinder’和`BeanFactory`使用。 `BeanWrapper`的工作方式在一定程度上由其名称表示:它封装了一个 Bean 以在该 Bean 上执行操作,例如设置和检索属性。 #### 3.3.1.设置和获取基本和嵌套属性 设置和获取属性是通过`setPropertyValue`和的重载方法变量`BeanWrapper`完成的。有关详细信息,请访问他们的 Javadoc。下表显示了这些约定的一些示例: | Expression |解释| |----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `name` |指示与`name`或`isName()`和`setName(..)`方法对应的属性`name`。| | `account.name` |指示属性`account`的嵌套属性`name`,它对应于
(例如)`getAccount().setName()`或`getAccount().getName()`方法。| | `account[2]` |指示索引属性`account`的*第三次*元素。索引属性
可以是类型`array`,`list`,或其他自然有序的集合。| |`account[COMPANYNAME]`|指示由`account``Map`属性的`COMPANYNAME`键索引的映射项的值。| (如果你不打算直接使用`BeanWrapper`,那么下一节对你来说并不是至关重要的。如果只使用`DataBinder`和`BeanFactory`及其默认实现,则应跳过[section on `PropertyEditors`](#beans-beans-conversion)。) 以下两个示例类使用`BeanWrapper`来获取和设置属性: Java ``` public class Company { private String name; private Employee managingDirector; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Employee getManagingDirector() { return this.managingDirector; } public void setManagingDirector(Employee managingDirector) { this.managingDirector = managingDirector; } } ``` Kotlin ``` class Company { var name: String? = null var managingDirector: Employee? = null } ``` Java ``` public class Employee { private String name; private float salary; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public float getSalary() { return salary; } public void setSalary(float salary) { this.salary = salary; } } ``` Kotlin ``` class Employee { var name: String? = null var salary: Float? = null } ``` 以下代码片段展示了如何检索和操作实例化`Company`s 和`Employee`s 的一些属性的示例: Java ``` BeanWrapper company = new BeanWrapperImpl(new Company()); // setting the company name.. company.setPropertyValue("name", "Some Company Inc."); // ... can also be done like this: PropertyValue value = new PropertyValue("name", "Some Company Inc."); company.setPropertyValue(value); // ok, let's create the director and tie it to the company: BeanWrapper jim = new BeanWrapperImpl(new Employee()); jim.setPropertyValue("name", "Jim Stravinsky"); company.setPropertyValue("managingDirector", jim.getWrappedInstance()); // retrieving the salary of the managingDirector through the company Float salary = (Float) company.getPropertyValue("managingDirector.salary"); ``` Kotlin ``` val company = BeanWrapperImpl(Company()) // setting the company name.. company.setPropertyValue("name", "Some Company Inc.") // ... can also be done like this: val value = PropertyValue("name", "Some Company Inc.") company.setPropertyValue(value) // ok, let's create the director and tie it to the company: val jim = BeanWrapperImpl(Employee()) jim.setPropertyValue("name", "Jim Stravinsky") company.setPropertyValue("managingDirector", jim.wrappedInstance) // retrieving the salary of the managingDirector through the company val salary = company.getPropertyValue("managingDirector.salary") as Float? ``` #### 3.3.2.内置`PropertyEditor`实现 Spring 使用`PropertyEditor`的概念来实现 ` 对象’与`String`之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,`Date`可以以人类可读的方式表示(如`String`:`'2007-14-09'`),而我们仍然可以将人类可读的表单转换回原始日期(或者,更好的是,将在人类可读表单中输入的任何日期转换回`Date`对象)。这种行为可以通过注册“java.beans.propertyeditor”类型的自定义编辑器来实现。在`BeanWrapper`上注册自定义编辑器,或者在特定的 IoC 容器中注册自定义编辑器(如前一章中提到的),可以让它了解如何将属性转换为所需的类型。有关“PropertyEditor”的更多信息,请参见[the javadoc of the `java.beans` package from Oracle](https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html)。 Spring 中使用属性编辑的几个示例: * 在 bean 上设置属性是通过使用`PropertyEditor`实现来完成的。当使用`String`作为在 XML 文件中声明的某个 Bean 的属性的值时, Spring(如果相应属性的 setter 具有`Class`参数)使用`ClassEditor`来尝试将参数解析为`Class`对象。 * 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种`PropertyEditor`实现完成的,你可以在 `CommandController’的所有子类中手动绑定这些实现。 Spring 具有许多内置`PropertyEditor`的实现方式,以使生活变得容易。它们都位于`org.springframework.beans.propertyeditors`包中。默认情况下,大多数(但不是所有,如下表所示)是由“BeanWrapperImpl”注册的。在属性编辑器以某种方式可配置的情况下,你仍然可以注册自己的变体以覆盖默认的变体。下表描述了 Spring 提供的各种`PropertyEditor`实现: | Class |解释| |-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |`ByteArrayPropertyEditor`|字节数组的编辑器。将字符串转换为其对应的字节
表示形式。默认情况下由`BeanWrapperImpl`注册。| | `ClassEditor` |解析将类表示为实际类的字符串,反之亦然。当未找到
类时,将抛出`IllegalArgumentException`。默认情况下,由“BeanWrapperImpl”注册。| | `CustomBooleanEditor` |`Boolean`属性的可自定义属性编辑器。默认情况下,由“BeanWrapperImpl”注册,但可以通过将其自定义实例注册为
自定义编辑器来重写。| |`CustomCollectionEditor` |集合的属性编辑器,将任何源`Collection`转换为给定的目标 `collection’类型。| | `CustomDateEditor` |用于`java.util.Date`的可自定义属性编辑器,支持自定义`DateFormat`。不
默认注册。必须根据需要以适当的格式进行用户注册。| | `CustomNumberEditor` |用于任何`Number`子类的可自定义属性编辑器,例如`Integer`,`Long`,`Float`,或 `double’。默认情况下,由`BeanWrapperImpl`注册,但可以由
将其自定义实例注册为自定义编辑器来覆盖。| | `FileEditor` |将字符串解析为`java.io.File`对象。默认情况下,由“BeanWrapperImpl”注册。| | `InputStreamEditor` |一种单向属性编辑器,它可以获取一个字符串并产生(通过
中间值`ResourceEditor`和`Resource`)一个`InputStream`,这样`InputStream`属性就可以直接设置为字符串。请注意,默认用法不会为你关闭
的`InputStream`。默认情况下,由`BeanWrapperImpl`注册。| | `LocaleEditor` |可以将字符串解析为`Locale`对象,反之亦然(字符串格式为 `[language]_[country]_[variant]`,与 `locale’的`toString()`方法相同)。也接受空格作为分隔符,作为下划线的替代。
默认情况下,由`BeanWrapperImpl`注册。| | `PatternEditor` |可以将字符串解析为`java.util.regex.Pattern`对象,反之亦然。| | `PropertiesEditor` |可以将字符串(格式为 `java.util.properties’类的 Javadoc 中定义的格式)转换为`Properties`对象。默认情况下,由`BeanWrapperImpl`注册
。| | `StringTrimmerEditor` |编辑字符串的属性编辑器。可选地允许将空字符串
转换为`null`值。默认情况下未注册——必须是用户注册的。| | `URLEditor` |可以将 URL 的字符串表示解析为实际的`URL`对象。
默认情况下,由`BeanWrapperImpl`注册。| Spring 使用`java.beans.PropertyEditorManager`设置可能需要的属性编辑器的搜索路径。搜索路径还包括`sun.bean.editors`,其中包括`PropertyEditor`类型的实现,例如`Font`,`Color`,以及大多数原始类型。还请注意,标准 JavaBeans 基础设施会自动发现`PropertyEditor`类(无需显式地注册它们),如果它们与它们处理的类在同一个包中,并且与该类具有相同的名称,并附加`Editor`。例如,可以有以下的类和包结构,这将足以使`SomethingEditor`类被识别并用作`PropertyEditor`类型属性的`Something`。 ``` com chank pop Something SomethingEditor // the PropertyEditor for the Something class ``` 注意,这里也可以使用标准的`BeanInfo`JavaBeans 机制(在一定程度上描述了[here](https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html))。下面的示例使用`BeanInfo`机制显式地用关联类的属性注册一个或多个 `PropertyEditor’实例: ``` com chank pop Something SomethingBeanInfo // the BeanInfo for the Something class ``` 下面引用的`SomethingBeanInfo`类的 Java 源代码将`CustomNumberEditor`与`age`类的`age`属性关联起来: Java ``` public class SomethingBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() { try { final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { @Override public PropertyEditor createPropertyEditor(Object bean) { return numberPE; } }; return new PropertyDescriptor[] { ageDescriptor }; } catch (IntrospectionException ex) { throw new Error(ex.toString()); } } } ``` Kotlin ``` class SomethingBeanInfo : SimpleBeanInfo() { override fun getPropertyDescriptors(): Array { try { val numberPE = CustomNumberEditor(Int::class.java, true) val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) { override fun createPropertyEditor(bean: Any): PropertyEditor { return numberPE } } return arrayOf(ageDescriptor) } catch (ex: IntrospectionException) { throw Error(ex.toString()) } } } ``` ##### 注册额外的自定义`PropertyEditor`实现 ##### 当将 Bean 属性设置为字符串值时, Spring IOC 容器最终使用标准 JavaBeans`PropertyEditor`实现将这些字符串转换为属性的复杂类型。 Spring 预先寄存器一些自定义的`PropertyEditor`实现(例如,将表示为字符串的类名转换为`Class`对象)。此外,Java 的标准 JavaBeans`PropertyEditor`查找机制允许对一个类的`PropertyEditor`进行适当的命名,并将其放置在与它所支持的类相同的包中,以便可以自动找到它。 如果需要注册其他自定义`PropertyEditors`,可以使用几种机制。通常不方便或不推荐的最手动的方法是使用“configurableBeanFactory”接口的方法,假设你有引用。另一种(稍微更方便的)机制是使用一种特殊的 Bean 工厂后处理器,称为`CustomEditorConfigurer`。尽管你可以使用带有`BeanFactory`实现的 Bean 工厂后处理器,但`CustomEditorConfigurer`具有嵌套的属性设置,因此我们强烈建议你将其用于 `ApplicationContext’,在这里你可以以类似的方式将其部署到任何其他 Bean 实现,并且可以自动检测和应用它。 注意,所有 Bean 工厂和应用程序上下文通过使用`BeanWrapper`自动使用许多内置的属性编辑器来处理属性转换。在[上一节](#beans-beans-conversion)中列出了`BeanWrapper`寄存器的标准属性编辑器。此外,`ApplicationContext`s 还覆盖或添加额外的编辑器,以便以适合特定应用程序上下文类型的方式处理资源查找。 标准 JavaBeans`PropertyEditor`实例用于将以字符串表示的属性值转换为属性的实际复杂类型。你可以使用 Bean 工厂后处理程序“CustomEditorConfigurer”方便地向`PropertyEditor`实例添加对额外`ApplicationContext`实例的支持。 考虑以下示例,它定义了一个名为`ExoticType`的用户类和另一个名为`DependsOnExoticType`的类,它需要`ExoticType`设置为一个属性: Java ``` package example; public class ExoticType { private String name; public ExoticType(String name) { this.name = name; } } public class DependsOnExoticType { private ExoticType type; public void setType(ExoticType type) { this.type = type; } } ``` Kotlin ``` package example class ExoticType(val name: String) class DependsOnExoticType { var type: ExoticType? = null } ``` 在正确设置之后,我们希望能够将 type 属性分配为字符串,然后`PropertyEditor`将其转换为实际的 `exoticType’实例。 Bean 以下定义显示了如何设置这种关系: ``` ``` `PropertyEditor`实现可能看起来类似于以下内容: Java ``` // converts string representation to ExoticType object package example; public class ExoticTypeEditor extends PropertyEditorSupport { public void setAsText(String text) { setValue(new ExoticType(text.toUpperCase())); } } ``` Kotlin ``` // converts string representation to ExoticType object package example import java.beans.PropertyEditorSupport class ExoticTypeEditor : PropertyEditorSupport() { override fun setAsText(text: String) { value = ExoticType(text.toUpperCase()) } } ``` 最后,下面的示例展示了如何使用`CustomEditorConfigurer`将新的`PropertyEditor`注册到 `ApplicationContext’中,然后可以根据需要使用它: ``` ``` ###### 使用`PropertyEditorRegistrar` 用 Spring 容器注册属性编辑器的另一种机制是创建和使用`PropertyEditorRegistrar`。当你需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用。你可以编写一个相应的注册表,并在每种情况下重新使用它。`PropertYeditorRegistry’实例与一个名为 `PropertYeditorRegistry’的接口一起工作,该接口由 Spring (和)实现。`PropertyEditorRegistrar`实例在与`CustomEditorConfigurer`(描述为[here](#beans-beans-conversion-customeditor-registration))结合使用时特别方便,它公开了一个名为`setPropertyEditorRegistrars(..)`的属性。`PropertyEditorRegistrar`以这种方式添加到`CustomEditorConfigurer`的实例可以很容易地与`DataBinder`和 Spring MVC 控制器共享。此外,它避免了在自定义编辑器上进行同步的需要:一个`PropertyEditorRegistrar`被期望为每个 Bean 创建尝试创建新的`PropertyEditor`实例。 下面的示例展示了如何创建你自己的`PropertyEditorRegistrar`实现: Java ``` package com.foo.editors.spring; public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { public void registerCustomEditors(PropertyEditorRegistry registry) { // it is expected that new PropertyEditor instances are created registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); // you could register as many custom property editors as are required here... } } ``` Kotlin ``` package com.foo.editors.spring import org.springframework.beans.PropertyEditorRegistrar import org.springframework.beans.PropertyEditorRegistry class CustomPropertyEditorRegistrar : PropertyEditorRegistrar { override fun registerCustomEditors(registry: PropertyEditorRegistry) { // it is expected that new PropertyEditor instances are created registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor()) // you could register as many custom property editors as are required here... } } ``` 另请参见`org.springframework.beans.support.ResourceEditorRegistrar`中的示例 `PropertyDitorRegistrar’实现。请注意,在“RegisterCustomEditors”方法的实现中,它如何为每个属性编辑器创建新的实例。 下一个示例展示了如何配置`CustomEditorConfigurer`,并将我们的 `CustomPropertyDitorRegistrarer’实例注入其中: ``` ``` 最后(对于那些使用[Spring’s MVC web framework](web.html#mvc)的人来说,有点偏离本章的重点),使用`PropertyEditorRegistrars`结合数据绑定`Controllers`(例如`SimpleFormController`)可以非常方便。下面的示例在`initBinder(..)`方法的实现中使用`PropertyEditorRegistrar`: Java ``` public final class RegisterUserController extends SimpleFormController { private final PropertyEditorRegistrar customPropertyEditorRegistrar; public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { this.customPropertyEditorRegistrar = propertyEditorRegistrar; } protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder) throws Exception { this.customPropertyEditorRegistrar.registerCustomEditors(binder); } // other methods to do with registering a User } ``` Kotlin ``` class RegisterUserController( private val customPropertyEditorRegistrar: PropertyEditorRegistrar) : SimpleFormController() { protected fun initBinder(request: HttpServletRequest, binder: ServletRequestDataBinder) { this.customPropertyEditorRegistrar.registerCustomEditors(binder) } // other methods to do with registering a User } ``` 这种`PropertyEditor`的注册风格可以导致简洁的代码(`initBinder(..)`的实现只有一行长),并让普通的`PropertyEditor`注册代码封装在一个类中,然后根据需要在多个 `controllers’之间共享。 ### 3.4. Spring 类型转换 Spring 3 介绍了一种`core.convert`包,其提供了一种通用的类型转换系统。系统定义了实现类型转换逻辑的 SPI 和在运行时执行类型转换的 API。在 Spring 容器中,可以使用该系统作为`PropertyEditor`实现的替代方案,以将外部化的 Bean 属性值字符串转换为所需的属性类型。你还可以在应用程序中需要类型转换的任何地方使用公共 API。 #### 3.4.1.转换器 SPI 实现类型转换逻辑的 SPI 是简单且强类型的,如下接口定义所示: ``` package org.springframework.core.convert.converter; public interface Converter { T convert(S source); } ``` 要创建自己的转换器,请实现`Converter`接口,并将`S`参数化为要转换的类型,将`T`参数化为要转换的类型。如果需要将`S`的集合或数组转换为`T`的阵列或集合,那么也可以透明地应用这样的转换器,前提是已经注册了一个委托数组或集合转换器(默认情况下`DefaultConversionService`是这样做的)。 对于每个对`convert(S)`的调用,源参数保证不为空。如果转换失败,你的“转换器”可能会抛出任何未经检查的异常。具体地说,它应该抛出“IllegalArgumentException”来报告无效的源值。注意确保你的`Converter`实现是线程安全的。 为了方便起见,在`core.convert.support`包中提供了几种转换器实现方式。这些包括从字符串到数字的转换器和其他常见类型的转换器。下面的清单显示了`StringToInteger`类,这是一个典型的`Converter`实现: ``` package org.springframework.core.convert.support; final class StringToInteger implements Converter { public Integer convert(String source) { return Integer.valueOf(source); } } ``` #### 3.4.2.使用`ConverterFactory` 当需要对整个类层次结构集中转换逻辑时(例如,从`String`转换为`Enum`对象时),可以实现 `converterfactory’,如下例所示: ``` package org.springframework.core.convert.converter; public interface ConverterFactory { Converter getConverter(Class targetType); } ``` 参数化 S 为你要转换的类型,R 为定义你可以转换为的类的的基本类型。然后实现`getConverter(Class)`,其中 t 是 r 的一个子类。 以`StringToEnumConverterFactory`为例: ``` package org.springframework.core.convert.support; final class StringToEnumConverterFactory implements ConverterFactory { public Converter getConverter(Class targetType) { return new StringToEnumConverter(targetType); } private final class StringToEnumConverter implements Converter { private Class enumType; public StringToEnumConverter(Class enumType) { this.enumType = enumType; } public T convert(String source) { return (T) Enum.valueOf(this.enumType, source.trim()); } } } ``` #### 3.4.3.使用`GenericConverter` 当你需要复杂的`Converter`实现时,请考虑使用“GenericConverter”接口。使用比`Converter`更灵活但不那么强类型的签名,`GenericConverter`支持在多个源类型和目标类型之间进行转换。此外,`GenericConverter`提供了在实现转换逻辑时可以使用的源和目标字段上下文。这样的上下文允许类型转换由字段注释或在字段签名上声明的通用信息驱动。下面的清单显示了`GenericConverter`的接口定义: ``` package org.springframework.core.convert.converter; public interface GenericConverter { public Set getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } ``` 要实现`GenericConverter`,请让`getConvertibleTypes()`返回受支持的源目标类型对。然后实现`convert(Object, TypeDescriptor, TypeDescriptor)`来包含你的转换逻辑。源`TypeDescriptor`提供对保存被转换的值的源字段的访问。目标`TypeDescriptor`提供对要设置转换值的目标字段的访问。 `GenericConverter`的一个很好的例子是在 Java 数组和集合之间转换的转换器。这样的`ArrayToCollectionConverter`内省声明目标集合类型的字段,以解析集合的元素类型。这允许在目标字段上设置集合之前,将源数组中的每个元素转换为集合元素类型。 | |因为`GenericConverter`是一个更复杂的 SPI 接口,所以只有在需要时才应该使用
。青睐`Converter`或`ConverterFactory`对于基本类型
转换需要。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 使用`ConditionalGenericConverter` 有时,只有当特定条件为真时,你才希望`Converter`运行。例如,只有在目标字段上存在特定的注释时,你才可能希望运行`Converter`,或者,只有在目标类上定义了特定的方法(例如`static valueOf`方法)时,才希望运行`Converter`。 ``` public interface ConditionalConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { } ``` `ConditionalGenericConverter`的一个很好的例子是`IdToEntityConverter`,它在持久实体标识符和实体引用之间转换。这样的`IdToEntityConverter`可能只有在目标实体类型声明了静态查找方法(例如,`FindAccount(long)`)时才匹配。你可以在“Matches”(typedescriptor,typedescriptor)的实现中执行这样的查找方法检查。 #### 3.4.4.`ConversionService`api `ConversionService`定义了用于在运行时执行类型转换逻辑的统一 API。转换器通常在以下 facade 接口后面运行: ``` package org.springframework.core.convert; public interface ConversionService { boolean canConvert(Class sourceType, Class targetType); T convert(Object source, Class targetType); boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); } ``` 大多数`ConversionService`实现还实现了`ConverterRegistry`,它提供了用于注册转换器的 SPI。在内部,`ConversionService`实现委托给其注册的转换器来执行类型转换逻辑。 在`core.convert.support`包中提供了一个健壮的`ConversionService`实现。`GenericConversionService`是适用于大多数环境的通用实现。`ConversionServiceFactory`为创建常见的`ConversionService`配置提供了一个方便的工厂。 #### 3.4.5.配置`ConversionService` `ConversionService`是一种无状态对象,设计用于在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或`ApplicationContext`)配置一个`ConversionService`实例。 Spring 获取`ConversionService`并在框架需要执行类型转换时使用它。你还可以将这个“ConversionService”注入到你的任何 bean 中,并直接调用它。 | |如果没有`ConversionService`被注册到 Spring,则使用原来的基于`PropertyEditor`的
系统。| |---|------------------------------------------------------------------------------------------------------------| 要用 Spring 注册一个默认的`ConversionService`,请添加以下 Bean 定义,并使用`id`的`conversionService`: ``` ``` 默认的`ConversionService`可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。要用你自己的定制转换器补充或覆盖默认转换器,请设置`converters`属性。属性值可以实现`Converter`、`ConverterFactory`或`GenericConverter`接口中的任意一个。 ``` ``` 在 Spring MVC 应用程序中使用`ConversionService`也是很常见的。参见 Spring MVC 章节中的[转换和格式化](web.html#mvc-config-conversion)。 在某些情况下,你可能希望在转换过程中应用格式化。有关使用`FormattingConversionServiceFactoryBean`的详细信息,请参见[The `FormatterRegistry` SPI](#format-FormatterRegistry-SPI)。 #### 3.4.6.以编程方式使用`ConversionService` 要以编程方式处理`ConversionService`实例,你可以像处理任何其他实例一样,对它注入一个引用 Bean。下面的示例展示了如何做到这一点: Java ``` @Service public class MyService { public MyService(ConversionService conversionService) { this.conversionService = conversionService; } public void doIt() { this.conversionService.convert(...) } } ``` Kotlin ``` @Service class MyService(private val conversionService: ConversionService) { fun doIt() { conversionService.convert(...) } } ``` 对于大多数用例,你可以使用`convert`方法来指定`targetType`,但是它不适用于更复杂的类型,例如参数化元素的集合。例如,如果你想要将`List`的`Integer`转换为`List`的`List`的`String`,则需要提供源类型和目标类型的正式定义。 幸运的是,`TypeDescriptor`提供了各种选项,以使这样做很简单,如下例所示: Java ``` DefaultConversionService cs = new DefaultConversionService(); List input = ... cs.convert(input, TypeDescriptor.forObject(input), // List type descriptor TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); ``` Kotlin ``` val cs = DefaultConversionService() val input: List = ... cs.convert(input, TypeDescriptor.forObject(input), // List type descriptor TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) ``` 请注意,`DefaultConversionService`会自动注册适合于大多数环境的转换器。这包括集合转换器、标量转换器和基本`Object`-到 ` 字符串’转换器。通过在`DefaultConversionService`类上使用静态`addDefaultConverters`方法,可以用任何`ConverterRegistry`注册相同的转换器。 值类型的转换器可重用于数组和集合,因此不需要创建特定的转换器来将`Collection`的`S`转换为`T`的 `collection’的`T`,假设标准的集合处理是适当的。 ### 3.5. Spring 字段格式 如上一节所讨论的,[`core.convert`](#core-convert)是一种通用的类型转换系统。它提供了一个统一的`ConversionService`API 以及一个强类型的`Converter`SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring 容器使用此系统绑定 Bean 属性值。此外, Spring 表达式语言和`DataBinder`都使用此系统绑定字段值。例如,当 SPEL 需要强制`Short`到`Long`以完成`expression.setValue(Object bean, Object value)`尝试时,`core.convert`系统执行强制。 现在考虑典型的客户机环境(例如 Web 或桌面应用程序)的类型转换需求。在这样的环境中,通常从`String`转换为支持客户端回发过程,以及返回`String`以支持视图呈现过程。此外,你经常需要本地化`String`值。更通用的`core.convert``Converter`SPI 并不直接解决此类格式要求。为了直接解决这些问题, Spring 3 引入了一个方便的`Formatter`SPI,它为客户机环境提供了`PropertyEditor`实现的简单而健壮的替代方案。 通常,当需要实现通用类型转换逻辑时,可以使用`Converter`SPI——例如,用于在`java.util.Date`和`Long`之间进行转换。当你在客户机环境(例如 Web 应用程序)中工作并且需要解析和打印本地化字段值时,可以使用`Formatter`SPI。`ConversionService`为两个 SPI 提供了统一的类型转换 API。 #### 3.5.1.`Formatter`SPI 实现字段格式逻辑的`Formatter`SPI 是简单且强类型的。下面的清单显示了`Formatter`接口定义: ``` package org.springframework.format; public interface Formatter extends Printer, Parser { } ``` `Formatter`扩展自`Printer`和`Parser`构建块接口。下面的清单显示了这两个接口的定义: ``` public interface Printer { String print(T fieldValue, Locale locale); } ``` ``` import java.text.ParseException; public interface Parser { T parse(String clientValue, Locale locale) throws ParseException; } ``` 要创建自己的`Formatter`,请实现前面显示的`Formatter`接口。将`T`参数化为你希望格式化的对象类型——例如,“java.util.date”。实现`print()`操作来打印`T`的实例,以便在客户机语言环境中显示。实现`parse()`操作,从客户机语言环境返回的格式化表示中解析 `t’的实例。如果解析尝试失败,你的`Formatter`应该抛出`ParseException`或`IllegalArgumentException`。注意确保你的`Formatter`实现是线程安全的。 `format`子包提供了几个`Formatter`实现作为一种便利。`number`包提供`NumberStyleFormatter`、`CurrencyStyleFormatter`和 `percentStyleFormatter` 来格式化使用`Number`的对象。`datetime`包提供了一个`DateFormatter`来将`java.util.Date`对象格式化为`java.text.DateFormat`。 下面的`DateFormatter`是`Formatter`实现的示例: 爪哇 ``` package org.springframework.format.datetime; public final class DateFormatter implements Formatter { private String pattern; public DateFormatter(String pattern) { this.pattern = pattern; } public String print(Date date, Locale locale) { if (date == null) { return ""; } return getDateFormat(locale).format(date); } public Date parse(String formatted, Locale locale) throws ParseException { if (formatted.length() == 0) { return null; } return getDateFormat(locale).parse(formatted); } protected DateFormat getDateFormat(Locale locale) { DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); dateFormat.setLenient(false); return dateFormat; } } ``` Kotlin ``` class DateFormatter(private val pattern: String) : Formatter { override fun print(date: Date, locale: Locale) = getDateFormat(locale).format(date) @Throws(ParseException::class) override fun parse(formatted: String, locale: Locale) = getDateFormat(locale).parse(formatted) protected fun getDateFormat(locale: Locale): DateFormat { val dateFormat = SimpleDateFormat(this.pattern, locale) dateFormat.isLenient = false return dateFormat } } ``` Spring 团队欢迎社区驱动的`Formatter`贡献。参见[GitHub Issues](https://github.com/spring-projects/spring-framework/issues)。 #### 3.5.2.注解驱动的格式 字段格式可以根据字段类型或注释进行配置。要将注释绑定到`Formatter`,请实现`AnnotationFormatterFactory`。下面的清单显示了`AnnotationFormatterFactory`接口的定义: ``` package org.springframework.format; public interface AnnotationFormatterFactory
{ Set> getFieldTypes(); Printer getPrinter(A annotation, Class fieldType); Parser getParser(A annotation, Class fieldType); } ``` 要创建一个实现: 1. 将 a 参数化为你希望与其关联的格式化逻辑的字段`annotationType`,例如`org.springframework.format.annotation.DateTimeFormat`。 2. 让`getFieldTypes()`返回可以使用注释的字段类型。 3. 让`getPrinter()`返回一个`Printer`来打印带注释字段的值。 4. 有`getParser()`返回一个`Parser`来解析带注释字段的`clientValue`。 下面的示例`AnnotationFormatterFactory`实现将`@NumberFormat`注释绑定到格式化程序,以便指定数字样式或模式: 爪哇 ``` public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory { public Set> getFieldTypes() { return new HashSet>(asList(new Class[] { Short.class, Integer.class, Long.class, Float.class, Double.class, BigDecimal.class, BigInteger.class })); } public Printer getPrinter(NumberFormat annotation, Class fieldType) { return configureFormatterFrom(annotation, fieldType); } public Parser getParser(NumberFormat annotation, Class fieldType) { return configureFormatterFrom(annotation, fieldType); } private Formatter configureFormatterFrom(NumberFormat annotation, Class fieldType) { if (!annotation.pattern().isEmpty()) { return new NumberStyleFormatter(annotation.pattern()); } else { Style style = annotation.style(); if (style == Style.PERCENT) { return new PercentStyleFormatter(); } else if (style == Style.CURRENCY) { return new CurrencyStyleFormatter(); } else { return new NumberStyleFormatter(); } } } } ``` Kotlin ``` class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory { override fun getFieldTypes(): Set> { return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java) } override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer { return configureFormatterFrom(annotation, fieldType) } override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser { return configureFormatterFrom(annotation, fieldType) } private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter { return if (annotation.pattern.isNotEmpty()) { NumberStyleFormatter(annotation.pattern) } else { val style = annotation.style when { style === NumberFormat.Style.PERCENT -> PercentStyleFormatter() style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter() else -> NumberStyleFormatter() } } } } ``` 要触发格式设置,你可以使用 @NumberFormat 对字段进行注释,如下例所示: 爪哇 ``` public class MyModel { @NumberFormat(style=Style.CURRENCY) private BigDecimal decimal; } ``` Kotlin ``` class MyModel( @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal ) ``` ##### 格式注释 API `org.springframework.format.annotation`包中存在一个可移植格式注释 API。你可以使用`@NumberFormat`来格式化`Number`字段,例如`Double`和 `long’,以及`@DateTimeFormat`来格式化`java.util.Date`、`java.util.Calendar`、`Long`(用于毫秒时间戳)以及 jsr-310`java.time`。 下面的示例使用`@DateTimeFormat`将`java.util.Date`格式化为 ISO 日期: 爪哇 ``` public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; } ``` Kotlin ``` class MyModel( @DateTimeFormat(iso=ISO.DATE) private val date: Date ) ``` #### 3.5.3.`FormatterRegistry`SPI `FormatterRegistry`是用于注册格式化程序和转换器的 SPI。“formattingConversionService”是`FormatterRegistry`的一种实现,适用于大多数环境。你可以通过编程或声明性地将此变体配置为 Spring Bean,例如通过使用`FormattingConversionServiceFactoryBean`。因为该实现还实现了`ConversionService`,所以你可以直接将其配置为与 Spring 的`DataBinder`和 Spring 表达式语言一起使用。 下面的清单显示了`FormatterRegistry`SPI: ``` package org.springframework.format; public interface FormatterRegistry extends ConverterRegistry { void addPrinter(Printer printer); void addParser(Parser parser); void addFormatter(Formatter formatter); void addFormatterForFieldType(Class fieldType, Formatter formatter); void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory); } ``` 如前面的清单所示,你可以通过字段类型或注释注册格式化程序。 `FormatterRegistry`SPI 允许你集中配置格式化规则,而不是在你的控制器上重复此类配置。例如,你可能希望强制所有日期字段以特定的方式格式化,或者使用特定注释的字段以特定的方式格式化。使用共享的`FormatterRegistry`,你可以定义这些规则一次,并且在需要格式化时应用它们。 #### 3.5.4.`FormatterRegistrar`SPI `FormatterRegistrar`是用于通过 FormatterRegistry 注册格式化程序和转换器的 SPI。下面的清单显示了它的接口定义: ``` package org.springframework.format; public interface FormatterRegistrar { void registerFormatters(FormatterRegistry registry); } ``` 当为给定的格式分类(例如日期格式)注册多个相关的转换器和格式化程序时,`FormatterRegistrar`是有用的。在声明性注册不足的情况下——例如,当格式化程序需要在与其本身的``不同的特定字段类型下进行索引时,或者当注册`Printer`/` 解析器 ` 对时,它也是有用的。下一节提供了有关转换器和格式化程序注册的更多信息。 #### 3.5.5.在 Spring MVC 中配置格式 参见 Spring MVC 章节中的[转换和格式化](web.html#mvc-config-conversion)。 ### 3.6.配置全局日期和时间格式 默认情况下,不带`@DateTimeFormat`注释的日期和时间字段将使用`DateFormat.SHORT`样式从字符串转换。如果你愿意,你可以通过定义自己的全局格式来更改这一点。 要做到这一点,请确保 Spring 不注册默认格式化程序。相反,在以下帮助下手动注册格式化程序: * `org.springframework.format.datetime.standard.DateTimeFormatterRegistrar` * `org.springframework.format.datetime.DateFormatterRegistrar` 例如,下面的 爪哇 配置注册了一个全局`yyyyMMdd`格式: 爪哇 ``` @Configuration public class AppConfig { @Bean public FormattingConversionService conversionService() { // Use the DefaultFormattingConversionService but do not register defaults DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); // Ensure @NumberFormat is still supported conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // Register JSR-310 date conversion with a specific global format DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); registrar.registerFormatters(conversionService); // Register date conversion with a specific global format DateFormatterRegistrar registrar = new DateFormatterRegistrar(); registrar.setFormatter(new DateFormatter("yyyyMMdd")); registrar.registerFormatters(conversionService); return conversionService; } } ``` Kotlin ``` @Configuration class AppConfig { @Bean fun conversionService(): FormattingConversionService { // Use the DefaultFormattingConversionService but do not register defaults return DefaultFormattingConversionService(false).apply { // Ensure @NumberFormat is still supported addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory()) // Register JSR-310 date conversion with a specific global format val registrar = DateTimeFormatterRegistrar() registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")) registrar.registerFormatters(this) // Register date conversion with a specific global format val registrar = DateFormatterRegistrar() registrar.setFormatter(DateFormatter("yyyyMMdd")) registrar.registerFormatters(this) } } } ``` 如果你更喜欢基于 XML 的配置,那么可以使用“formattingConversionServiceFactoryBean”。下面的示例展示了如何做到这一点: ```
``` 注意,在 Web 应用程序中配置日期和时间格式时还需要考虑其他因素。请参阅[WebMVC 转换和格式化](web.html#mvc-config-conversion)或[WebFlux 转换和格式化](web-reactive.html#webflux-config-conversion)。 ### 3.7.爪哇 Bean 验证 Spring 框架为[爪哇 Bean Validation](https://beanvalidation.org/)API 提供支持。 #### 3.7.1. Bean 验证概述 Bean 验证通过约束声明和元数据为 爪哇 应用程序提供了一种通用的验证方式。要使用它,你需要使用声明性验证约束注释域模型属性,然后由运行时强制执行这些约束。有内置的约束,你也可以定义自己的自定义约束。 考虑以下示例,其中显示了一个简单的`PersonForm`模型,该模型具有两个属性: 爪哇 ``` public class PersonForm { private String name; private int age; } ``` Kotlin ``` class PersonForm( private val name: String, private val age: Int ) ``` Bean 验证允许你声明约束,如下例所示: 爪哇 ``` public class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; } ``` Kotlin ``` class PersonForm( @get:NotNull @get:Size(max=64) private val name: String, @get:Min(0) private val age: Int ) ``` Bean 验证验证器然后基于声明的约束对该类的实例进行验证。有关 API 的一般信息,请参见[Bean Validation](https://beanvalidation.org/)。有关特定的约束,请参见[Hibernate Validator](https://hibernate.org/validator/)文档。要了解如何将 Bean 验证提供者设置为 Spring Bean,请继续阅读。 #### 3.7.2.配置 Bean 验证提供程序 Spring 为 Bean 验证 API 提供了充分的支持,包括将 Bean 验证提供者引导为 Spring Bean。这使你可以在应用程序中需要验证的任何地方插入 `javax.validation.validatorfactory’或`javax.validation.Validator`。 可以使用`LocalValidatorFactoryBean`将默认验证器配置为 Spring Bean,如下例所示: 爪哇 ``` import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @Configuration public class AppConfig { @Bean public LocalValidatorFactoryBean validator() { return new LocalValidatorFactoryBean(); } } ``` XML ``` ``` 前面示例中的基本配置触发 Bean 验证,以通过使用其默认的引导程序机制来初始化。 Bean 验证提供程序,例如 Hibernate 验证器,预计将存在于 Classpath 中并被自动检测。 ##### 注入验证器 `LocalValidatorFactoryBean`实现了`javax.validation.ValidatorFactory`和 `javax.validation.validator’,以及 Spring 的`org.springframework.validation.Validator`。你可以向需要调用验证逻辑的 bean 中注入对这两个接口中任一个的引用。 如果你更愿意直接使用 Bean 验证 API,则可以插入对`javax.validation.Validator`的引用,如下例所示: 爪哇 ``` import javax.validation.Validator; @Service public class MyService { @Autowired private Validator validator; } ``` Kotlin ``` import javax.validation.Validator; @Service class MyService(@Autowired private val validator: Validator) ``` 如果你的 Bean 需要 Spring 验证 API,则可以插入对`org.springframework.validation.Validator`的引用,如下例所示: 爪哇 ``` import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; } ``` Kotlin ``` import org.springframework.validation.Validator @Service class MyService(@Autowired private val validator: Validator) ``` ##### 配置自定义约束 Bean 每个验证约束由两部分组成: * 声明约束及其可配置属性的`@Constraint`注释。 * 实现约束行为的`javax.validation.ConstraintValidator`接口的实现。 要将声明与实现关联,每个`@Constraint`注释引用一个对应的`ConstraintValidator`实现类。在运行时,当域模型中遇到约束注释时,一个“constraintValidatorFactory”实例化引用的实现。 默认情况下,`LocalValidatorFactoryBean`配置一个`SpringConstraintValidatorFactory`,它使用 Spring 来创建`ConstraintValidator`实例。这使你的自定义“约束 Validator”像任何其他 Spring Bean 一样受益于依赖注入。 下面的示例显示了一个自定义`@Constraint`声明,以及一个使用 Spring 进行依赖项注入的关联的 `constraintvalidator’实现: 爪哇 ``` @Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=MyConstraintValidator.class) public @interface MyConstraint { } ``` Kotlin ``` @Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD) @Retention(AnnotationRetention.RUNTIME) @Constraint(validatedBy = MyConstraintValidator::class) annotation class MyConstraint ``` 爪哇 ``` import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @Autowired; private Foo aDependency; // ... } ``` Kotlin ``` import javax.validation.ConstraintValidator class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator { // ... } ``` 正如前面的示例所示,`ConstraintValidator`实现可以像其他任何 Spring Bean 一样具有其依赖项 `@autowired’。 ##### Spring-驱动方法验证 可以通过“MethodValidationPostProcessor” Bean 定义,将 Bean Validation1.1(以及 Hibernate Validator4.3 作为自定义扩展)支持的方法验证功能集成到 Spring 上下文中: 爪哇 ``` import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; @Configuration public class AppConfig { @Bean public MethodValidationPostProcessor validationPostProcessor() { return new MethodValidationPostProcessor(); } } ``` XML ``` ``` 为了有资格进行 Spring 驱动的方法验证,所有目标类都需要使用 Spring 的`@Validated`注释进行注释,该注释还可以选择性地声明要使用的验证组。有关 Hibernate 验证器和 Bean 验证 1.1 提供程序的设置细节,请参见[`MethodValidationPostProcessor’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.html)。 | |方法验证依赖于[AOP Proxies](#aop-introduction-proxies)周围的
目标类,或者是接口上的方法的 JDK 动态代理,或者是 CGlib 代理。
使用代理有一定的限制,其中一些在[Understanding AOP Proxies](#aop-understanding-aop-proxies)中进行了描述。此外,请记住
始终在代理类上使用方法和访问器;直接字段访问将不起作用。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 附加配置选项 对于大多数情况,默认的`LocalValidatorFactoryBean`配置就足够了。对于各种 Bean 验证构造,有许多配置选项,从消息插值到遍历解析。有关这些选项的更多信息,请参见[localvalidatorfactorybean’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.html)爪哇doc。 #### 3.7.3.配置`DataBinder` 自 Spring 3 起,可以使用`Validator`配置`DataBinder`实例。一旦配置好,你就可以通过调用`binder.validate()`来调用`Validator`。任何验证“错误”都会自动添加到活页夹的`BindingResult`中。 下面的示例展示了如何以编程方式使用`DataBinder`在绑定到目标对象后调用验证逻辑: 爪哇 ``` Foo target = new Foo(); DataBinder binder = new DataBinder(target); binder.setValidator(new FooValidator()); // bind to the target object binder.bind(propertyValues); // validate the target object binder.validate(); // get BindingResult that includes any validation errors BindingResult results = binder.getBindingResult(); ``` Kotlin ``` val target = Foo() val binder = DataBinder(target) binder.validator = FooValidator() // bind to the target object binder.bind(propertyValues) // validate the target object binder.validate() // get BindingResult that includes any validation errors val results = binder.bindingResult ``` 你还可以通过 `databinder.addvalidators’和`dataBinder.replaceValidators`配置带有多个`Validator`实例的`DataBinder`。当将全局配置的 Bean 验证与在 Databinder 实例上本地配置的 Spring `Validator`相结合时,这是有用的。见[Spring MVC Validation Configuration](web.html#mvc-config-validation)。 #### 3.7.4. Spring MVC3 验证 参见 Spring MVC 章节中的[Validation](web.html#mvc-config-validation)。 ## 4. Spring 表达式语言 Spring 表达式语言(简称“SPEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。该语言语法类似于 Unified EL,但提供了额外的功能,最明显的是方法调用和基本的字符串模板功能。 虽然还有其他几种 爪哇 表达式语言可用——OGNL、MVEL 和 JBossEL,仅举几例—— Spring 表达式语言的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,这种语言可以在 Spring 产品组合中的所有产品中使用。其语言特性是由 Spring 投资组合中的项目的需求驱动的,包括[Spring Tools for Eclipse](https://spring.io/tools)中的代码完成支持的工具需求。也就是说,SPEL 基于一种与技术无关的 API,该 API 允许在需要时集成其他表达式语言实现。 虽然 SPEL 是 Spring 投资组合中表达式求值的基础,但它不直接绑定到 Spring,可以独立使用。为了自成一体,本章中的许多示例使用 SPEL,就好像它是一种独立的表达语言。这需要创建一些引导基础设施类,比如解析器。 Spring 大多数用户不需要处理此基础结构,并且可以仅使用作者表达式字符串进行求值。这种典型用途的一个例子是将 SPEL 集成到创建 XML 或基于注释的 Bean 定义中,如[Expression support for defining bean definitions](#expressions-beandef)所示。 本章介绍了表达式语言的特点、API 和语言语法。在一些地方,`Inventor`和`Society`类被用作表达式求值的目标对象。这些类声明和用于填充它们的数据在本章的末尾列出。 表达式语言支持以下功能: * 字面表达式 * 布尔运算符和关系运算符 * 正则表达式 * 类表达式 * 访问属性、数组、列表和映射 * 方法调用 * 关系运算符 * 任务分配 * 调用构造函数 * Bean 参考文献 * 阵列构造 * 内联列表 * 内联地图 * 三元算符 * 变量 * 用户定义的函数 * 集合投影 * 收藏选择 * 模板化表达式 ### 4.1.评价 这一节介绍了 SPEL 接口的简单用法及其表达式语言。完整的语言引用可以在[语言参考](#expressions-language-ref)中找到。 下面的代码引入了 SPEL API 来计算字面字符串表达式“Hello World”。 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'"); (1) String message = (String) exp.getValue(); ``` |**1**|消息变量的值是`'Hello World'`。| |-----|-----------------------------------------------------| Kotlin ``` val parser = SpelExpressionParser() val exp = parser.parseExpression("'Hello World'") (1) val message = exp.value as String ``` |**1**|消息变量的值是`'Hello World'`。| |-----|-----------------------------------------------------| 你最有可能使用的 SPEL 类和接口位于 `org.springframework.expression` 包及其子包中,例如`spel.support`。 `ExpressionParser`接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。`Expression`接口负责计算先前定义的表达式字符串。当分别调用`parser.parseExpression`和`exp.getValue`时,可以抛出两个异常,`ParseException`和 `evaluationexception’。 SPEL 支持多种功能,例如调用方法、访问属性和调用构造函数。 在下面的方法调用示例中,我们在字符串字面上调用`concat`方法: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1) String message = (String) exp.getValue(); ``` |**1**|`message`的值现在是“Hello World!”。| |-----|---------------------------------------------| Kotlin ``` val parser = SpelExpressionParser() val exp = parser.parseExpression("'Hello World'.concat('!')") (1) val message = exp.value as String ``` |**1**|`message`的值现在是“Hello World!”。| |-----|---------------------------------------------| 下面调用 爪哇Bean 属性的示例调用`String`属性`Bytes`: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); // invokes 'getBytes()' Expression exp = parser.parseExpression("'Hello World'.bytes"); (1) byte[] bytes = (byte[]) exp.getValue(); ``` |**1**|这一行将字面值转换为字节数组。| |-----|-----------------------------------------------| Kotlin ``` val parser = SpelExpressionParser() // invokes 'getBytes()' val exp = parser.parseExpression("'Hello World'.bytes") (1) val bytes = exp.value as ByteArray ``` |**1**|这一行将字面值转换为字节数组。| |-----|-----------------------------------------------| SPEL 还通过使用标准的点记号(例如“prop1.prop2.prop3”)和相应的属性值设置来支持嵌套属性。也可以访问公共字段。 下面的示例展示了如何使用点记号来获取文字的长度: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); // invokes 'getBytes().length' Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1) int length = (Integer) exp.getValue(); ``` |**1**|`'Hello World'.bytes.length`给出了字面的长度。| |-----|-------------------------------------------------------------| Kotlin ``` val parser = SpelExpressionParser() // invokes 'getBytes().length' val exp = parser.parseExpression("'Hello World'.bytes.length") (1) val length = exp.value as Int ``` |**1**|`'Hello World'.bytes.length`给出了字面的长度。| |-----|-------------------------------------------------------------| 可以调用字符串的构造函数,而不是使用字符串文字,如下例所示: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1) String message = exp.getValue(String.class); ``` |**1**|从字面上构造一个新的`String`,并使其成为大写。| |-----|--------------------------------------------------------------------| Kotlin ``` val parser = SpelExpressionParser() val exp = parser.parseExpression("new String('hello world').toUpperCase()") (1) val message = exp.getValue(String::class.java) ``` |**1**|从字面上构造一个新的`String`,并使其成为大写。| |-----|--------------------------------------------------------------------| 注意通用方法的使用:`public T getValue(Class desiredResultType)`。使用此方法就不需要将表达式的值强制转换为所需的结果类型。如果不能将该值强制转换为`T`类型或使用注册的类型转换器进行转换,则抛出`EvaluationException`。 SPEL 更常见的用法是提供一个表达式字符串,该表达式字符串是针对特定的对象实例(称为根对象)计算的。下面的示例展示了如何从`Inventor`类的实例检索`name`属性或创建布尔条件: 爪哇 ``` // Create and set a calendar GregorianCalendar c = new GregorianCalendar(); c.set(1856, 7, 9); // The constructor arguments are name, birthday, and nationality. Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("name"); // Parse name as an expression String name = (String) exp.getValue(tesla); // name == "Nikola Tesla" exp = parser.parseExpression("name == 'Nikola Tesla'"); boolean result = exp.getValue(tesla, Boolean.class); // result == true ``` Kotlin ``` // Create and set a calendar val c = GregorianCalendar() c.set(1856, 7, 9) // The constructor arguments are name, birthday, and nationality. val tesla = Inventor("Nikola Tesla", c.time, "Serbian") val parser = SpelExpressionParser() var exp = parser.parseExpression("name") // Parse name as an expression val name = exp.getValue(tesla) as String // name == "Nikola Tesla" exp = parser.parseExpression("name == 'Nikola Tesla'") val result = exp.getValue(tesla, Boolean::class.java) // result == true ``` #### 4.1.1.理解`EvaluationContext` 在计算表达式以解析属性、方法或字段并帮助执行类型转换时,使用`EvaluationContext`接口。 Spring 提供了两种实现方式。 * `SimpleEvaluationContext`:对于不需要完整的 SPEL 语言语法的表达式类别,公开了基本的 SPEL 语言特性和配置选项的子集,并且应该进行有意义的限制。示例包括但不限于数据绑定表达式和基于属性的过滤器。 * `StandardEvaluationContext`:公开了全套 SPEL 语言特性和配置选项。你可以使用它来指定一个默认的根对象,并配置所有可用的与评估相关的策略。 `SimpleEvaluationContext`被设计成只支持 SPEL 语言语法的一个子集。它排除了 爪哇 类型引用、构造函数和 Bean 引用。它还要求你显式地选择对表达式中的属性和方法的支持级别。默认情况下,`create()`静态工厂方法仅允许对属性的读访问。你还可以获得一个构建器,以配置所需的确切支持级别,目标是以下一种或几种组合: * 仅自定义`PropertyAccessor`(无反射) * 只读访问的数据绑定属性 * 用于读写的数据绑定属性 ##### 类型转换 默认情况下,SPEL 使用 Spring Core 中可用的转换服务。这种转换服务与许多用于公共转换的内置转换器一起提供,但也是完全可扩展的,因此你可以在类型之间添加自定义转换。此外,它还具有泛型意识。这意味着,当你在表达式中处理泛型类型时,SPEL 会尝试转换,以维护它遇到的任何对象的类型正确性。 这在实践中意味着什么?假设使用`setValue()`的赋值用于设置`List`属性。该属性的类型实际上是`List`。SPEL 认识到,在将列表中的元素放入其中之前,需要将其转换为`Boolean`。下面的示例展示了如何做到这一点: 爪哇 ``` class Simple { public List booleanList = new ArrayList(); } Simple simple = new Simple(); simple.booleanList.add(true); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); // "false" is passed in here as a String. SpEL and the conversion service // will recognize that it needs to be a Boolean and convert it accordingly. parser.parseExpression("booleanList[0]").setValue(context, simple, "false"); // b is false Boolean b = simple.booleanList.get(0); ``` Kotlin ``` class Simple { var booleanList: MutableList = ArrayList() } val simple = Simple() simple.booleanList.add(true) val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() // "false" is passed in here as a String. SpEL and the conversion service // will recognize that it needs to be a Boolean and convert it accordingly. parser.parseExpression("booleanList[0]").setValue(context, simple, "false") // b is false val b = simple.booleanList[0] ``` #### 4.1.2.解析器配置 可以通过使用解析器配置对象来配置 SPEL 表达式解析器。配置对象控制一些表达式组件的行为。例如,如果你将索引放入一个数组或集合中,并且指定索引处的元素是`null`,那么 SPEL 可以自动创建该元素。这在使用由一系列属性引用组成的表达式时很有用。如果你将索引放入一个数组或列表中,并指定一个超出该数组或列表当前大小的索引,那么 SPEL 可以自动增加数组或列表以适应该索引。为了在指定的索引处添加元素,在设置指定值之前,SPEL 将尝试使用元素类型的默认构造函数创建元素。如果元素类型没有默认的构造函数,`null`将被添加到数组或列表中。如果没有知道如何设置该值的内置或自定义转换器,`null`将保留在指定索引处的数组或列表中。下面的示例演示了如何自动增加列表: 爪哇 ``` class Demo { public List list; } // Turn on: // - auto null reference initialization // - auto collection growing SpelParserConfiguration config = new SpelParserConfiguration(true, true); ExpressionParser parser = new SpelExpressionParser(config); Expression expression = parser.parseExpression("list[3]"); Demo demo = new Demo(); Object o = expression.getValue(demo); // demo.list will now be a real collection of 4 entries // Each entry is a new empty String ``` Kotlin ``` class Demo { var list: List? = null } // Turn on: // - auto null reference initialization // - auto collection growing val config = SpelParserConfiguration(true, true) val parser = SpelExpressionParser(config) val expression = parser.parseExpression("list[3]") val demo = Demo() val o = expression.getValue(demo) // demo.list will now be a real collection of 4 entries // Each entry is a new empty String ``` #### 4.1.3.SPEL 编译 Spring Framework4.1 包括基本表达式编译器。表达式通常被解释,这在评估期间提供了很大的动态灵活性,但并不提供最佳性能。对于偶尔使用表达式来说,这是很好的,但是,当被其他组件(例如 Spring Integration)使用时,性能可能是非常重要的,并且不需要真正的动态性。 SPEL 编译器旨在解决这一需求。在求值过程中,编译器生成一个 爪哇 类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式求值。由于缺少表达式的类型,编译器在执行编译时使用在表达式的解释求值过程中收集的信息。例如,它并不完全从表达式中知道属性引用的类型,但是在第一次解释求值期间,它会找出它是什么。当然,如果各种表达式元素的类型随时间变化,基于这种派生信息的编译可能会在以后引起麻烦。由于这个原因,编译最适合那些类型信息不会在重复求值时发生变化的表达式。 考虑以下基本表达式: ``` someArray[0].someProperty.someOtherProperty < 0.1 ``` 由于前面的表达式涉及到数组访问、一些属性反引用和数字操作,因此性能增益可以非常明显。在运行 50000 次迭代的 MicroBenchmark 示例中,使用解释器进行计算需要 75ms,而使用表达式的编译版本只需要 3ms。 ##### 编译器配置 默认情况下,编译器不会打开,但你可以通过两种不同的方式打开它。你可以通过使用解析器配置过程([前面讨论过](#expressions-parser-configuration))打开它,或者在将 SPEL 用法嵌入到另一个组件中时使用 Spring 属性来打开它。本节讨论这两种选择。 编译器可以在三种模式中的一种进行操作,这三种模式在“org.springframework.expression.spel.spelcompilermode”枚举中被捕获。模式如下: * `OFF`(默认):编译器已关闭。 * `IMMEDIATE`:在即时模式下,表达式被尽快编译。这通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前面所述),表达式求值的调用者将收到一个异常。 * `MIXED`:在混合模式下,表达式会随着时间的推移在解释模式和编译模式之间默默地切换。经过一定数量的解释运行后,它们会切换到编译表单,如果编译表单出了问题(例如前面描述的类型更改),表达式会自动再次切换回解释表单。稍后,它可能会生成另一个已编译的表单并切换到它。基本上,用户在`IMMEDIATE`模式下获得的异常情况是在内部处理的。 `IMMEDIATE`模式的存在是因为`MIXED`模式可能会导致具有副作用的表达式出现问题。如果编译的表达式在部分成功后爆炸,那么它可能已经做了一些影响系统状态的事情。如果发生了这种情况,调用者可能不希望它以解释模式静默地重新运行,因为表达式的一部分可能运行了两次。 选择模式后,使用`SpelParserConfiguration`配置解析器。下面的示例展示了如何做到这一点: 爪哇 ``` SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.getClass().getClassLoader()); SpelExpressionParser parser = new SpelExpressionParser(config); Expression expr = parser.parseExpression("payload"); MyMessage message = new MyMessage(); Object payload = expr.getValue(message); ``` Kotlin ``` val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, this.javaClass.classLoader) val parser = SpelExpressionParser(config) val expr = parser.parseExpression("payload") val message = MyMessage() val payload = expr.getValue(message) ``` 在指定编译器模式时,还可以指定一个类装入器(允许传递空)。编译的表达式是在一个子类加载器中定义的,这个子类加载器是在所提供的任何类型下创建的。重要的是要确保,如果指定了类装入器,它就可以看到表达式求值过程中涉及的所有类型。如果没有指定类装入器,则使用默认的类装入器(通常是在表达式求值期间运行的线程的上下文类装入器)。 配置编译器的第二种方法是当 SPEL 嵌入到其他组件中时使用,并且可能无法通过配置对象对其进行配置。在这些情况下,可以通过 JVM 系统属性(或通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制)将`spring.expression.compiler.mode`属性设置为一个 ` 编译器模式 `enum 值(`off’,`immediate`,或`mixed`)。 ##### 编译器的限制 自 Spring 框架 4.1 以来,基本的编译框架已经到位。然而,该框架还不支持编译所有类型的表达式。最初的重点是可能在性能关键上下文中使用的常见表达式。以下表达式目前无法编译: * 涉及赋值的表达式 * 依赖于转换服务的表达式 * 使用自定义解析器或访问器的表达式 * 使用选择或投影的表达式 将来会有更多类型的表达式可以编译。 ### 4.2. Bean 定义中的表达式 你可以使用基于 XML 或基于注释的配置元数据的 SPEL 表达式来定义`BeanDefinition`实例。在这两种情况下,定义表达式的语法都是`#{ }`形式。 #### 4.2.1.XML 配置 可以通过使用表达式来设置属性或构造函数参数的值,如下例所示: ``` ``` 应用程序上下文中的所有 bean 都可以作为预定义的变量使用它们的公共 Bean 名称。这包括用于访问运行时环境的标准上下文 bean,如`environment`(类型为 `org.springframework.core.ENV.environment`)以及`systemProperties`和 `systemenvironment’(类型为`Map`)。 下面的示例显示了将`systemProperties` Bean 作为 SPEL 变量的访问权限: ``` ``` 请注意,在这里你不必在预定义的变量前加上`#`符号。 还可以按名称引用其他 Bean 属性,如下例所示: ``` ``` #### 4.2.2.注释配置 要指定默认值,可以将`@Value`注释放置在字段、方法和方法或构造函数参数上。 下面的示例设置字段的默认值: 爪哇 ``` public class FieldValueTestBean { @Value("#{ systemProperties['user.region'] }") private String defaultLocale; public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } public String getDefaultLocale() { return this.defaultLocale; } } ``` Kotlin ``` class FieldValueTestBean { @Value("#{ systemProperties['user.region'] }") var defaultLocale: String? = null } ``` 下面的示例展示了等价的但在属性 setter 方法上的方法: 爪哇 ``` public class PropertyValueTestBean { private String defaultLocale; @Value("#{ systemProperties['user.region'] }") public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } public String getDefaultLocale() { return this.defaultLocale; } } ``` Kotlin ``` class PropertyValueTestBean { @Value("#{ systemProperties['user.region'] }") var defaultLocale: String? = null } ``` AutoWired 方法和构造函数也可以使用`@Value`注释,如下例所示: 爪哇 ``` public class SimpleMovieLister { private MovieFinder movieFinder; private String defaultLocale; @Autowired public void configure(MovieFinder movieFinder, @Value("#{ systemProperties['user.region'] }") String defaultLocale) { this.movieFinder = movieFinder; this.defaultLocale = defaultLocale; } // ... } ``` Kotlin ``` class SimpleMovieLister { private lateinit var movieFinder: MovieFinder private lateinit var defaultLocale: String @Autowired fun configure(movieFinder: MovieFinder, @Value("#{ systemProperties['user.region'] }") defaultLocale: String) { this.movieFinder = movieFinder this.defaultLocale = defaultLocale } // ... } ``` 爪哇 ``` public class MovieRecommender { private String defaultLocale; private CustomerPreferenceDao customerPreferenceDao; public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, @Value("#{systemProperties['user.country']}") String defaultLocale) { this.customerPreferenceDao = customerPreferenceDao; this.defaultLocale = defaultLocale; } // ... } ``` Kotlin ``` class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao, @Value("#{systemProperties['user.country']}") private val defaultLocale: String) { // ... } ``` ### 4.3.语言参考 本节描述 Spring 表达式语言的工作方式。它涵盖以下主题: * [字面表达式](#expressions-ref-literal) * [属性、数组、列表、映射和索引器](#expressions-properties-arrays) * [Inline Lists](#expressions-inline-lists) * [Inline Maps](#expressions-inline-maps) * [阵列构造](#expressions-array-construction) * [Methods](#expressions-methods) * [Operators](#expressions-operators) * [Types](#expressions-types) * [Constructors](#expressions-constructors) * [Variables](#expressions-ref-variables) * [Functions](#expressions-ref-functions) * [Bean References](#expressions-bean-references) * [三元运算符(if-then-else)](#expressions-operator-ternary) * [猫王操作员](#expressions-operator-elvis) * [安全导航操作员](#expressions-operator-safe-navigation) #### 4.3.1.字面表达式 所支持的文字表达式的类型包括字符串、数值(INT、实数、十六进制)、布尔表达式和空表达式。字符串用单引号分隔。要将单引号本身放入字符串中,请使用两个单引号字符。 下面的清单显示了文字的简单用法。通常,它们不会像这样孤立地使用,而是作为更复杂表达式的一部分使用——例如,在逻辑比较运算符的一侧使用文字。 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); // evals to "Hello World" String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); // evals to 2147483647 int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); Object nullValue = parser.parseExpression("null").getValue(); ``` Kotlin ``` val parser = SpelExpressionParser() // evals to "Hello World" val helloWorld = parser.parseExpression("'Hello World'").value as String val avogadrosNumber = parser.parseExpression("6.0221415E+23").value as Double // evals to 2147483647 val maxValue = parser.parseExpression("0x7FFFFFFF").value as Int val trueValue = parser.parseExpression("true").value as Boolean val nullValue = parser.parseExpression("null").value ``` 数字支持使用负号、指数记号和小数点。默认情况下,通过使用`Double.parseDouble()`解析实数。 #### 4.3.2.属性、数组、列表、映射和索引器 使用属性引用进行导航很容易。要做到这一点,请使用一个句号来指示嵌套的属性值。`Inventor`类的实例`pupin`和`tesla`的实例使用[示例中使用的类](#expressions-example-classes)部分中列出的数据填充。为了导航“向下”的对象图,并获得特斯拉的出生年份和普平的出生城市,我们使用以下表达式: 爪哇 ``` // evals to 1856 int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context); String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context); ``` Kotlin ``` // evals to 1856 val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String ``` | |允许对财产名称的第一个字母不区分大小写。因此,上述示例中的
表达式可以分别写为`Birthdate.Year + 1900`和 `placeofbirth.city’。此外,可以选择通过
方法调用访问属性——例如,`getPlaceOfBirth().getCity()`而不是 `placeofbirth.city’。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 数组和列表的内容是通过使用方括号表示法获得的,如下例所示: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); // Inventions Array // evaluates to "Induction motor" String invention = parser.parseExpression("inventions[3]").getValue( context, tesla, String.class); // Members List // evaluates to "Nikola Tesla" String name = parser.parseExpression("members[0].name").getValue( context, ieee, String.class); // List and Array navigation // evaluates to "Wireless communication" String invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String.class); ``` Kotlin ``` val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() // Inventions Array // evaluates to "Induction motor" val invention = parser.parseExpression("inventions[3]").getValue( context, tesla, String::class.java) // Members List // evaluates to "Nikola Tesla" val name = parser.parseExpression("members[0].name").getValue( context, ieee, String::class.java) // List and Array navigation // evaluates to "Wireless communication" val invention = parser.parseExpression("members[0].inventions[6]").getValue( context, ieee, String::class.java) ``` 映射的内容是通过在括号中指定文字键的值来获得的。在下面的示例中,因为`officers`映射的键是字符串,所以我们可以指定字符串字面值: 爪哇 ``` // Officer's Dictionary Inventor pupin = parser.parseExpression("officers['president']").getValue( societyContext, Inventor.class); // evaluates to "Idvor" String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( societyContext, String.class); // setting values parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( societyContext, "Croatia"); ``` Kotlin ``` // Officer's Dictionary val pupin = parser.parseExpression("officers['president']").getValue( societyContext, Inventor::class.java) // evaluates to "Idvor" val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( societyContext, String::class.java) // setting values parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( societyContext, "Croatia") ``` #### 4.3.3.内联列表 你可以使用`{}`符号在表达式中直接表示列表。 爪哇 ``` // evaluates to a Java list containing the four numbers List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context); List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context); ``` Kotlin ``` // evaluates to a Java list containing the four numbers val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*> val listOfLists = parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context) as List<*> ``` `{}`本身意味着空列表。出于性能原因,如果列表本身完全由固定的文字组成,则创建一个常量列表来表示表达式(而不是在每个求值上构建一个新的列表)。 #### 4.3.4.内联地图 你也可以使用`{key:value}`符号在表达式中直接表示映射。下面的示例展示了如何做到这一点: 爪哇 ``` // evaluates to a Java map containing the two entries Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context); Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); ``` Kotlin ``` // evaluates to a Java map containing the two entries val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, *> val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> ``` `{:}`本身意味着一个空地图。出于性能原因,如果映射本身由固定的文字或其他嵌套的常量结构(列表或映射)组成,则创建一个常量映射来表示表达式(而不是在每个求值上构建一个新的映射)。对映射键的引用是可选的(除非该键包含一个句号(`.`))。上面的示例不使用引号键。 #### 4.3.5.阵列构造 你可以使用熟悉的 爪哇 语法构建数组,也可以提供一个初始化器,以便在构建时填充数组。下面的示例展示了如何做到这一点: 爪哇 ``` int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); // Array with initializer int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context); ``` Kotlin ``` val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray // Array with initializer val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray // Multi dimensional array val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array ``` 在构造多维数组时,当前不能提供初始化器。 #### 4.3.6.方法 你可以通过使用典型的 爪哇 编程语法调用方法。你还可以在字面值上调用方法。还支持变量参数。以下示例展示了如何调用方法: 爪哇 ``` // string literal, evaluates to "bc" String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); // evaluates to true boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( societyContext, Boolean.class); ``` Kotlin ``` // string literal, evaluates to "bc" val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java) // evaluates to true val isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( societyContext, Boolean::class.java) ``` #### 4.3.7.操作员 Spring 表达式语言支持以下几种运算符: * [关系运算符](#expressions-operators-relational) * [逻辑运算符](#expressions-operators-logical) * [数学运算符](#expressions-operators-mathematical) * [赋值运算符](#expressions-assignment) ##### 关系运算符 关系运算符(相等、不相等、小于、小于或等于、大于和大于或等于)通过使用标准运算符表示法来支持。下面的列表显示了几个操作符的示例: 爪哇 ``` // evaluates to true boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class); // evaluates to false boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class); // evaluates to true boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class); ``` Kotlin ``` // evaluates to true val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java) // evaluates to false val falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean::class.java) // evaluates to true val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java) ``` | |与`null`的大于或小于比较遵循一个简单的规则:`null`被视为
nothing(即不为零)。因此,任何其他值总是大于
`null`(`x>null` 总是`true`),并且没有其他值总是小于 nothing
(`x如果你更喜欢数字比较,
,避免基于数字的`null`比较
,而倾向于与零进行比较(例如,`X > 0`或`X < 0`)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 除了标准的关系运算符,SPEL 还支持`instanceof`和基于正则表达式的`matches`运算符。下面的清单展示了这两个方面的例子: 爪哇 ``` // evaluates to false boolean falseValue = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean.class); // evaluates to true boolean trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); // evaluates to false boolean falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); ``` Kotlin ``` // evaluates to false val falseValue = parser.parseExpression( "'xyz' instanceof T(Integer)").getValue(Boolean::class.java) // evaluates to true val trueValue = parser.parseExpression( "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) // evaluates to false val falseValue = parser.parseExpression( "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) ``` | |在使用基本类型时要小心,因为它们会立即 Boxed 到它们的
包装器类型。例如,`1 instanceof T(int)`计算为`false`,而 `1instanceof t(integer)` 计算为`true`,正如预期的那样。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 每个符号运算符也可以指定为纯字母等价的。这避免了所使用的符号对于嵌入表达式的文档类型(例如在 XML 文档中)具有特殊含义的问题。对应的文本是: * `lt`(`<`) * `gt`(`>`) * `le`(`<=`) * `ge`(`>=`) * `eq`(`==`) * `ne`(`!=`) * `div`(`/`) * `mod`(`%`) * `not`(`!`). 所有的文本运算符都是不区分大小写的。 ##### 逻辑运算符 SPEL 支持以下逻辑运算符: * `and`(`&&`) * `or`(`||`) * `not`(`!`) 下面的示例展示了如何使用逻辑运算符: 爪哇 ``` // -- AND -- // evaluates to false boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class); // evaluates to true String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"; boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); // -- OR -- // evaluates to true boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class); // evaluates to true String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"; boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); // -- NOT -- // evaluates to false boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class); // -- AND and NOT -- String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); ``` Kotlin ``` // -- AND -- // evaluates to false val falseValue = parser.parseExpression("true and false").getValue(Boolean::class.java) // evaluates to true val expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')" val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) // -- OR -- // evaluates to true val trueValue = parser.parseExpression("true or false").getValue(Boolean::class.java) // evaluates to true val expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')" val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) // -- NOT -- // evaluates to false val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java) // -- AND and NOT -- val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')" val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) ``` ##### 数学运算符 在数字和字符串上都可以使用加法运算符。你可以只在数字上使用减法、乘法和除法运算符.你还可以在数字上使用模运算符“%”和指数幂运算符“^”。强制执行标准操作符优先级。下面的示例显示了正在使用的数学运算符: 爪哇 ``` // Addition int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 String testString = parser.parseExpression( "'test' + ' ' + 'string'").getValue(String.class); // 'test string' // Subtraction int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4 double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000 // Multiplication int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6 double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0 // Division int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2 double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0 // Modulus int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3 int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1 // Operator precedence int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21 ``` Kotlin ``` // Addition val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 val testString = parser.parseExpression( "'test' + ' ' + 'string'").getValue(String::class.java) // 'test string' // Subtraction val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4 val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000 // Multiplication val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0 // Division val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2 val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0 // Modulus val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3 val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 // Operator precedence val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 ``` ##### 赋值运算符 要设置属性,请使用赋值运算符。这通常在对`setValue`的调用中完成,但也可以在对`getValue`的调用中完成。下面的清单显示了使用赋值操作符的两种方式: 爪哇 ``` Inventor inventor = new Inventor(); EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic"); // alternatively String aleks = parser.parseExpression( "name = 'Aleksandar Seovic'").getValue(context, inventor, String.class); ``` Kotlin ``` val inventor = Inventor() val context = SimpleEvaluationContext.forReadWriteDataBinding().build() parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic") // alternatively val aleks = parser.parseExpression( "name = 'Aleksandar Seovic'").getValue(context, inventor, String::class.java) ``` #### 4.3.8.类型 你可以使用特殊的`T`操作符来指定`java.lang.Class`(类型)的实例。静态方法也可以通过使用此操作符来调用。“StandardDeValuationContext”使用`TypeLocator`来查找类型,而“StandardTypeLocator”(可以替换)是在理解“java.lang”包的情况下构建的。这意味着对`T()`包中的类型的引用不需要完全限定,但是所有其他类型的引用必须是完全限定的。下面的示例展示了如何使用`T`运算符: 爪哇 ``` Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); boolean trueValue = parser.parseExpression( "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR") .getValue(Boolean.class); ``` Kotlin ``` val dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class::class.java) val stringClass = parser.parseExpression("T(String)").getValue(Class::class.java) val trueValue = parser.parseExpression( "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR") .getValue(Boolean::class.java) ``` #### 4.3.9.构造者 你可以使用`new`操作符调用构造函数。除了位于`java.lang`包中的类型(`integer`,`Float`,`String`,以此类推)之外,你应该为所有类型使用完全限定的类名。下面的示例展示了如何使用“new”操作符调用构造函数: 爪哇 ``` Inventor einstein = p.parseExpression( "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor.class); // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor( 'Albert Einstein', 'German'))").getValue(societyContext); ``` Kotlin ``` val einstein = p.parseExpression( "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") .getValue(Inventor::class.java) // create new Inventor instance within the add() method of List p.parseExpression( "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") .getValue(societyContext) ``` #### 4.3.10.变量 你可以使用`#variableName`语法在表达式中引用变量。变量是通过在`EvaluationContext`实现上使用`setVariable`方法来设置的。 | |有效的变量名必须由以下一个或多个受支持的
字符组成。

* 字母:`A`至`Z`至
`0`* 数字:`0`至<<>>>>11”/><<<>>>>>><>>>>>><<<<>>>>>>>>>>>>>>>| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了如何使用变量。 爪哇 ``` Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); context.setVariable("newName", "Mike Tesla"); parser.parseExpression("name = #newName").getValue(context, tesla); System.out.println(tesla.getName()) // "Mike Tesla" ``` Kotlin ``` val tesla = Inventor("Nikola Tesla", "Serbian") val context = SimpleEvaluationContext.forReadWriteDataBinding().build() context.setVariable("newName", "Mike Tesla") parser.parseExpression("name = #newName").getValue(context, tesla) println(tesla.name) // "Mike Tesla" ``` ##### `#this`和`#root`变量 总是定义`#this`变量,并引用当前的求值对象(针对该对象解析不合格的引用)。始终定义`#root`变量,并引用根上下文对象。虽然`#this`可以随着表达式的组成部分的求值而变化,但`#root`总是指根。以下示例展示了如何使用`#this`和`#root`变量: 爪哇 ``` // create an array of integers List primes = new ArrayList(); primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); // create parser and set variable 'primes' as the array of integers ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess(); context.setVariable("primes", primes); // all prime numbers > 10 from the list (using selection ?{...}) // evaluates to [11, 13, 17] List primesGreaterThanTen = (List) parser.parseExpression( "#primes.?[#this>10]").getValue(context); ``` Kotlin ``` // create an array of integers val primes = ArrayList() primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17)) // create parser and set variable 'primes' as the array of integers val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataAccess() context.setVariable("primes", primes) // all prime numbers > 10 from the list (using selection ?{...}) // evaluates to [11, 13, 17] val primesGreaterThanTen = parser.parseExpression( "#primes.?[#this>10]").getValue(context) as List ``` #### 4.3.11.职能 你可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SPEL。函数是通过`EvaluationContext`注册的。下面的示例展示了如何注册用户定义的函数: 爪哇 ``` Method method = ...; EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("myFunction", method); ``` Kotlin ``` val method: Method = ... val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() context.setVariable("myFunction", method) ``` 例如,考虑以下逆转字符串的实用工具方法: 爪哇 ``` public abstract class StringUtils { public static String reverseString(String input) { StringBuilder backwards = new StringBuilder(input.length()); for (int i = 0; i < input.length(); i++) { backwards.append(input.charAt(input.length() - 1 - i)); } return backwards.toString(); } } ``` Kotlin ``` fun reverseString(input: String): String { val backwards = StringBuilder(input.length) for (i in 0 until input.length) { backwards.append(input[input.length - 1 - i]) } return backwards.toString() } ``` 然后,你可以注册并使用前面的方法,如下例所示: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable("reverseString", StringUtils.class.getDeclaredMethod("reverseString", String.class)); String helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String.class); ``` Kotlin ``` val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() context.setVariable("reverseString", ::reverseString::javaMethod) val helloWorldReversed = parser.parseExpression( "#reverseString('hello')").getValue(context, String::class.java) ``` #### 4.3.12. Bean 参考文献 如果计算上下文已配置了 Bean 解析器,则可以使用`@`符号从表达式中查找 bean。下面的示例展示了如何做到这一点: 爪哇 ``` ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); // This will end up calling resolve(context,"something") on MyBeanResolver during evaluation Object bean = parser.parseExpression("@something").getValue(context); ``` Kotlin ``` val parser = SpelExpressionParser() val context = StandardEvaluationContext() context.setBeanResolver(MyBeanResolver()) // This will end up calling resolve(context,"something") on MyBeanResolver during evaluation val bean = parser.parseExpression("@something").getValue(context) ``` 要访问工厂 Bean 本身,你应该在 Bean 名称前加上`&`符号。下面的示例展示了如何做到这一点: Java ``` ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); // This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation Object bean = parser.parseExpression("&foo").getValue(context); ``` Kotlin ``` val parser = SpelExpressionParser() val context = StandardEvaluationContext() context.setBeanResolver(MyBeanResolver()) // This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation val bean = parser.parseExpression("&foo").getValue(context) ``` #### 4.3.13.三元运算符(if-then-else) 可以使用三值运算符在表达式中执行 if-then-else 条件逻辑。下面的清单展示了一个最小示例: Java ``` String falseString = parser.parseExpression( "false ? 'trueExp' : 'falseExp'").getValue(String.class); ``` Kotlin ``` val falseString = parser.parseExpression( "false ? 'trueExp' : 'falseExp'").getValue(String::class.java) ``` 在这种情况下,布尔`false`将返回字符串值`'falseExp'`。下面是一个更现实的例子: Java ``` parser.parseExpression("name").setValue(societyContext, "IEEE"); societyContext.setVariable("queryName", "Nikola Tesla"); expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; String queryResultString = parser.parseExpression(expression) .getValue(societyContext, String.class); // queryResultString = "Nikola Tesla is a member of the IEEE Society" ``` Kotlin ``` parser.parseExpression("name").setValue(societyContext, "IEEE") societyContext.setVariable("queryName", "Nikola Tesla") expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'" val queryResultString = parser.parseExpression(expression) .getValue(societyContext, String::class.java) // queryResultString = "Nikola Tesla is a member of the IEEE Society" ``` 请参阅下一节关于 Elvis 操作符的内容,以获得更短的三元运算符语法。 #### 4.3.14.猫王操作员 Elvis 运算符是三元运算符语法的缩写,在[Groovy](http://www.groovy-lang.org/operators.html#_elvis_operator)语言中使用。使用三元运算符语法,你通常必须重复一个变量两次,如下例所示: ``` String name = "Elvis Presley"; String displayName = (name != null ? name : "Unknown"); ``` 相反,你可以使用猫王操作符(该操作符的名称与猫王的发型相似)。下面的示例展示了如何使用 Elvis 操作符: Java ``` ExpressionParser parser = new SpelExpressionParser(); String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class); System.out.println(name); // 'Unknown' ``` Kotlin ``` val parser = SpelExpressionParser() val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java) println(name) // 'Unknown' ``` 下面的清单展示了一个更复杂的示例: Java ``` ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Nikola Tesla tesla.setName(null); name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Elvis Presley ``` Kotlin ``` val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() val tesla = Inventor("Nikola Tesla", "Serbian") var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Nikola Tesla tesla.setName(null) name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Elvis Presley ``` | |可以使用 Elvis 操作符在表达式中应用默认值。下面的
示例展示了如何在`@Value`表达式中使用 Elvis 操作符:

``
@value(“#{systemproperties[’pop3.port’]:25}”)
``
如果没有定义,这将注入一个系统属性 =“4429”/>。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 4.3.15.安全导航操作员 安全导航操作符用于避免`NullPointerException`并来自[Groovy](http://www.groovy-lang.org/operators.html#_safe_navigation_operator)语言。通常,当你有一个对象的引用时,在访问对象的方法或属性之前,你可能需要验证它不是空的。为了避免这种情况,安全导航操作符将返回 null,而不是抛出异常。下面的示例展示了如何使用安全导航操作符: Java ``` ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); System.out.println(city); // Smiljan tesla.setPlaceOfBirth(null); city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); System.out.println(city); // null - does not throw NullPointerException!!! ``` Kotlin ``` val parser = SpelExpressionParser() val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() val tesla = Inventor("Nikola Tesla", "Serbian") tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan")) var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) println(city) // Smiljan tesla.setPlaceOfBirth(null) city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) println(city) // null - does not throw NullPointerException!!! ``` #### 4.3.16.收藏选择 选择是一种功能强大的表达式语言特性,它允许你通过从源集合的条目中进行选择来将其转换为另一个集合。 选择使用`.?[selectionExpression]`的语法。它对集合进行过滤,并返回一个新的集合,该集合包含原始元素的一个子集。例如,Selection 可以让我们很容易地获得一份塞尔维亚发明家的名单,如下例所示: Java ``` List list = (List) parser.parseExpression( "members.?[nationality == 'Serbian']").getValue(societyContext); ``` Kotlin ``` val list = parser.parseExpression( "members.?[nationality == 'Serbian']").getValue(societyContext) as List ``` 对于数组和实现`java.lang.Iterable`或 `java.util.map’的任何东西,都支持选择。对于一个列表或数组,选择标准是根据每个单独的元素进行评估的。针对映射,根据每个映射条目(Java 类型`Map.Entry`的对象)评估选择条件。每个 map 条目都有其`key`和`value`可作为属性访问,以便在选择中使用。 下面的表达式返回一个新的映射,该映射由原始映射的那些元素组成,其中条目的值小于 27: Java ``` Map newMap = parser.parseExpression("map.?[value<27]").getValue(); ``` Kotlin ``` val newMap = parser.parseExpression("map.?[value<27]").getValue() ``` 除了返回所有选定的元素外,你还可以只检索第一个或最后一个元素。要获得与所选内容匹配的第一个元素,语法为 `.^[SelectionExpression]`。要获得最后一个匹配的选择,语法是 `.$[SelectionExpression]`。 #### 4.3.17.集合投影 投影让集合驱动子表达式的求值,其结果是一个新的集合。投影的语法是`.![projectionExpression]`。例如,假设我们有一个发明家名单,但想要他们出生的城市名单。实际上,我们希望对发明家列表中的每个条目进行“出生地.城市”评估。下面的示例使用投影来实现这一点: Java ``` // returns ['Smiljan', 'Idvor' ] List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]"); ``` Kotlin ``` // returns ['Smiljan', 'Idvor' ] val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> ``` 对于数组和实现`java.lang.Iterable`或 `java.util.map’的任何东西,都支持投影。当使用映射来驱动投影时,该投影表达式针对映射中的每个条目进行求值(表示为 Java`Map.Entry`)。在映射上进行投影的结果是一个列表,该列表由针对每个映射条目的投影表达式的求值组成。 #### 4.3.18.表达式模板化 表达式模板允许将文字与一个或多个求值块混合。每个求值块都用你可以定义的前缀和后缀字符分隔。一个常见的选择是使用`#{ }`作为分隔符,如下例所示: Java ``` String randomPhrase = parser.parseExpression( "random number is #{T(java.lang.Math).random()}", new TemplateParserContext()).getValue(String.class); // evaluates to "random number is 0.7038186818312008" ``` Kotlin ``` val randomPhrase = parser.parseExpression( "random number is #{T(java.lang.Math).random()}", TemplateParserContext()).getValue(String::class.java) // evaluates to "random number is 0.7038186818312008" ``` 通过将文本`'random number is '`与在`#{ }`分隔符内求值表达式的结果连接在一起来计算字符串(在这种情况下,调用`random()`方法的结果)。`parseExpression()`方法的第二个参数类型为`ParserContext`。`ParserContext`接口用于影响表达式的解析方式,以支持表达式模板功能。`TemplateParserContext`的定义如下: Java ``` public class TemplateParserContext implements ParserContext { public String getExpressionPrefix() { return "#{"; } public String getExpressionSuffix() { return "}"; } public boolean isTemplate() { return true; } } ``` Kotlin ``` class TemplateParserContext : ParserContext { override fun getExpressionPrefix(): String { return "#{" } override fun getExpressionSuffix(): String { return "}" } override fun isTemplate(): Boolean { return true } } ``` ### 4.4.示例中使用的类 这一节列出了在这一章的例子中使用的类。 Inventor.java ``` package org.spring.samples.spel.inventor; import java.util.Date; import java.util.GregorianCalendar; public class Inventor { private String name; private String nationality; private String[] inventions; private Date birthdate; private PlaceOfBirth placeOfBirth; public Inventor(String name, String nationality) { GregorianCalendar c= new GregorianCalendar(); this.name = name; this.nationality = nationality; this.birthdate = c.getTime(); } public Inventor(String name, Date birthdate, String nationality) { this.name = name; this.nationality = nationality; this.birthdate = birthdate; } public Inventor() { } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNationality() { return nationality; } public void setNationality(String nationality) { this.nationality = nationality; } public Date getBirthdate() { return birthdate; } public void setBirthdate(Date birthdate) { this.birthdate = birthdate; } public PlaceOfBirth getPlaceOfBirth() { return placeOfBirth; } public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) { this.placeOfBirth = placeOfBirth; } public void setInventions(String[] inventions) { this.inventions = inventions; } public String[] getInventions() { return inventions; } } ``` Inventor.kt ``` class Inventor( var name: String, var nationality: String, var inventions: Array? = null, var birthdate: Date = GregorianCalendar().time, var placeOfBirth: PlaceOfBirth? = null) ``` placeofbirth.java ``` package org.spring.samples.spel.inventor; public class PlaceOfBirth { private String city; private String country; public PlaceOfBirth(String city) { this.city=city; } public PlaceOfBirth(String city, String country) { this(city); this.country = country; } public String getCity() { return city; } public void setCity(String s) { this.city = s; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } } ``` PlaceofBirth.kt ``` class PlaceOfBirth(var city: String, var country: String? = null) { ``` Society.java ``` package org.spring.samples.spel.inventor; import java.util.*; public class Society { private String name; public static String Advisors = "advisors"; public static String President = "president"; private List members = new ArrayList(); private Map officers = new HashMap(); public List getMembers() { return members; } public Map getOfficers() { return officers; } public String getName() { return name; } public void setName(String name) { this.name = name; } public boolean isMember(String name) { for (Inventor inventor : members) { if (inventor.getName().equals(name)) { return true; } } return false; } } ``` Society.kt ``` package org.spring.samples.spel.inventor import java.util.* class Society { val Advisors = "advisors" val President = "president" var name: String? = null val members = ArrayList() val officers = mapOf() fun isMember(name: String): Boolean { for (inventor in members) { if (inventor.name == name) { return true } } return false } } ``` ## 5. Spring 面向方面的编程 AOP 面向方面编程(Aspect-oriented Programming, AOP)通过提供关于程序结构的另一种思考方式,补充了面向对象编程。OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。方面支持跨多个类型和对象的关注(例如事务管理)的模块化。(在文献 AOP 中,这类担忧通常被称为“跨领域”担忧。 Spring 的关键组件之一是 AOP 框架。虽然 Spring IOC 容器不依赖于 AOP(这意味着如果你不想使用 AOP),但 AOP 补充了 Spring IOC 以提供非常有能力的中间件解决方案。 Spring AOP 带 AspectJ 切入点 Spring 提供了通过使用[基于模式的方法](#aop-schema)或[@AspectJ 注释样式](#aop-ataspectj)来编写自定义方面的简单而强大的方法。这两种样式都提供完全类型的建议和 AspectJ PointCut 语言的使用,同时仍然使用 Spring AOP 进行编织。 本章讨论了基于模式和 @AspectJ 的支持 AOP。较低级别的 AOP 支持在[接下来的一章](#aop-api)中进行了讨论。 AOP 在 Spring 框架中用于: * 提供声明性的 Enterprise 服务。最重要的这类服务是[声明式事务管理](data-access.html#transaction-declarative)。 * 让用户实现自定义方面,用 AOP 补充他们对 OOP 的使用。 | |如果你只对通用声明性服务或其他预打包的
声明性中间件服务如池感兴趣,则不需要直接使用
Spring AOP,并且可以跳过本章的大部分内容。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 5.1. AOP 概念 让我们首先定义一些核心的概念和术语。这些术语不是 Spring 特定的。遗憾的是, AOP 术语并不是特别直观。然而,如果 Spring 使用自己的术语,那将更加令人困惑。 * 方面:跨多个类的关注的模块化。事务管理是 EnterpriseJava 应用程序中横切关注点的一个很好的示例。在 Spring AOP 中,方面是通过使用常规类([基于模式的方法](#aop-schema))或使用 `@Aspect` 注释([@AspectJ style](#aop-ataspectj))的常规类来实现的。 * 连接点:程序执行过程中的一个点,如方法的执行或异常的处理。在 Spring AOP 中,连接点总是表示方法的执行。 * 建议:某一方面在某一特定连接点上采取的行动。不同类型的建议包括“周围”、“之前”和“之后”的建议。(通知类型将在后面讨论。)许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护拦截器的链。 * 切入点:匹配连接点的谓词。通知与切入点表达式关联,并在与切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,并且 Spring 默认情况下使用 AspectJ PointCut 表达式语言。 * 介绍:代表类型声明附加的方法或字段。 Spring AOP 让你将新的接口(和相应的实现)引入到任何建议的对象。例如,你可以使用一个介绍来使 Bean 实现一个 `ismodified’接口,以简化缓存。(在 AspectJ 社区中,介绍称为类型间声明。 * 目标对象:由一个或多个方面提供建议的对象。也称为“已知对象”。由于 Spring AOP 是通过使用运行时代理来实现的,因此该对象始终是代理对象。 * AOP 代理:由 AOP 框架创建的一个对象,以便实现方面契约(通知方法执行等)。在 Spring 框架中, AOP 代理是 JDK 动态代理或 CGLIB 代理。 * 编织:将方面与其他应用程序类型或对象连接起来,以创建一个建议的对象。这可以在编译时(例如使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP 与其他纯 爪哇 AOP 框架一样,在运行时执行编织。 Spring AOP 包括以下类型的建议: * 建议之前:在连接点之前运行的建议,但不具有阻止执行流继续到连接点的能力(除非抛出异常)。 * 返回建议后:在连接点正常完成后运行的建议(例如,如果方法返回时没有抛出异常)。 * 抛出建议后:如果方法通过抛出异常退出,则要运行建议。 * 在(最后)建议之后:无论连接点以何种方式退出(正常或异常返回),都要运行建议。 * 围绕建议:围绕连接点(如方法调用)的建议。这是最有力的建议。建议可以在方法调用之前和之后执行自定义行为。它还负责选择是继续进行连接点,还是通过返回自己的返回值或抛出异常来快捷所建议的方法执行。 围绕建议的建议是最普遍的一种建议。由于 Spring AOP 像 AspectJ 一样,提供了一系列完整的建议类型,因此我们建议你使用能够实现所需行为的功能最小的建议类型。例如,如果你只需要用一个方法的返回值来更新一个缓存,那么实现一个返回后的建议比一个环绕建议要好,尽管环绕建议可以完成同样的事情。使用最特定的建议类型可以提供一个更简单的编程模型,并且出错的可能性更小。例如,你不需要在用于 around advice 的`JoinPoint`上调用`proceed()`方法,因此,你不能失败地调用它。 所有通知参数都是静态类型的,因此你可以使用适当类型的通知参数(例如,方法执行中返回值的类型),而不是`Object`数组。 由切入点匹配的连接点的概念是 AOP 的关键,它区别于仅提供截获的旧技术。切入点使建议能够独立于面向对象的层次结构而具有针对性。例如,你可以将一个提供声明性事务管理的建议应用于一组跨越多个对象的方法(例如服务层中的所有业务操作)。 ### 5.2. Spring AOP 能力和目标 Spring AOP 是用纯 爪哇 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类装入器层次结构,因此适合在 Servlet 容器或应用服务器中使用。 Spring AOP 目前仅支持方法执行连接点(建议在 Spring bean 上执行方法)。未实现字段截取,尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段截取的支持。如果需要建议字段访问和更新连接点,请考虑使用 AspectJ 之类的语言。 Spring AOP 对 AOP 的方法与大多数其他 AOP 框架的方法不同。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当有能力)。相反,目的是提供 AOP 实现和 Spring IOC 之间的紧密集成,以帮助解决 Enterprise 应用程序中的常见问题。 因此,例如, Spring 框架的 AOP 功能通常与 Spring IOC 容器一起使用。方面是通过使用正常的 Bean 定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现方式的一个关键区别。对于 Spring AOP,你无法轻松或有效地完成某些事情,例如建议非常细粒度的对象(通常是域对象)。AspectJ 是这种情况下的最佳选择。然而,我们的经验是 Spring AOP 为 Enterprise爪哇 应用程序中的大多数问题提供了极好的解决方案,而这些问题是 AOP 能够解决的。 Spring AOP 从不努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为, Spring AOP 这样的基于代理的框架和 AspectJ 这样的成熟框架都是有价值的,它们是互补的,而不是竞争的。 Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以使 AOP 在一致的基于 Spring 的应用程序体系结构中的所有使用成为可能。这种集成不会影响 Spring AOP API 或 AOP Alliance API。 Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见[接下来的一章](#aop-api)。 | |Spring 框架的核心原则之一是非侵犯性。这
是这样一种想法,即你不应该被迫将特定于框架的类和
接口引入到你的业务或域模型中。然而,在某些地方, Spring Framework
确实为你提供了在
代码库中引入 Spring 特定于 Framework 的依赖项的选项。为你提供此类选项的理由是,在某些情况下,
可能更易于阅读或以
的方式编码某些特定的功能。然而, Spring 框架(几乎)总是为你提供选择:你有
做出明智决定的自由,决定哪个选项最适合你的特定使用
情况或场景。

与本章相关的一个这样的选择是选择哪种 AOP 框架(和
哪种 AOP 样式)。你可以选择 AspectJ、 Spring AOP 或两者。你
还可以选择 @AspectJ 注释样式方法或 Spring XML
配置样式方法。本章选择首先介绍
@AspectJ-style 方法,这一事实不应被视为表明 Spring 团队
更喜欢 @AspectJ 注释风格的方法,而不是 Spring XML 配置风格的方法。

参见[Choosing which AOP Declaration Style to Use](#aop-choosing)有关
每种风格的“为什么和为什么”的更完整讨论。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 5.3. AOP 代理 Spring AOP 对于 AOP 代理,默认使用标准的 JDK 动态代理。这使得可以代理任何接口(或一组接口)。 Spring AOP 还可以使用 CGLIB 代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象不实现接口,则使用 CGLIB。由于按接口而不是按类编程是一种很好的做法,所以业务类通常实现一个或多个业务接口。在以下情况下,[强制使用 CGlib](#aop-proxying)是可能的:你需要建议一个未在接口上声明的方法,或者你需要将代理对象作为具体类型传递给方法。 理解 Spring AOP 是基于代理的这一事实是很重要的。请参阅[Understanding AOP Proxies](#aop-understanding-aop-proxies),以全面了解此实现细节的实际含义。 ### 5.4.@AspectJ 支持 @AspectJ 引用了一种将方面声明为带有注释的常规 爪哇 类的风格。@AspectJ 样式是由[AspectJ project](https://www.eclipse.org/aspectj)作为 AspectJ5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ5 相同的注释。然而, AOP 运行时仍然是纯的 Spring AOP,并且对 AspectJ 编译器或 Weaver 没有依赖关系。 | |使用 AspectJ 编译器和 Weaver 可以使用完整的 AspectJ 语言,
在[Using AspectJ with Spring Applications](#aop-using-aspectj)中进行了讨论。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.4.1.启用 @AspectJ 支持 要在 Spring 配置中使用 @AspectJ 方面,你需要启用 Spring 支持,以基于 @AspectJ 方面配置 Spring AOP,并根据这些方面是否提供了建议来自动代理 bean。通过自动代理,我们的意思是,如果 Spring 确定 Bean 是由一个或多个方面建议的,则它会自动为该 Bean 生成代理,以拦截方法调用并确保根据需要运行建议。 可以通过 XML 或 爪哇 风格的配置来启用 @AspectJ 支持。在这两种情况下,你都需要确保 AspectJ 的`aspectjweaver.jar`库位于应用程序的 Classpath(版本 1.8 或更高版本)上。这个库可以在 AspectJ 发行版的“lib”目录中获得,也可以从 Maven 中央存储库获得。 ##### 通过 爪哇 配置启用 @AspectJ 支持 要使用 爪哇`@Configuration`启用 @AspectJ 支持,请添加`@EnableAspectJAutoProxy`注释,如下例所示: 爪哇 ``` @Configuration @EnableAspectJAutoProxy public class AppConfig { } ``` Kotlin ``` @Configuration @EnableAspectJAutoProxy class AppConfig ``` ##### 通过 XML 配置启用 @AspectJ 支持 要通过基于 XML 的配置启用 @AspectJ 支持,请使用`aop:aspectj-autoproxy`元素,如下例所示: ``` ``` 这假定你使用[基于 XML 模式的配置](#xsd-schemas)中描述的模式支持。有关如何导入`aop`命名空间中的标记,请参见[the AOP schema](#xsd-schemas-aop)。 #### 5.4.2.声明一个方面 启用了 @AspectJ 支持后, Spring 将自动检测 Bean 在应用程序上下文中定义的具有 @AspectJ 方面的类(具有`@Aspect`注释)的任何 Bean,并用于配置 Spring AOP。接下来的两个示例展示了一个不太有用的方面所需的最小定义。 这两个示例中的第一个示例显示了应用程序上下文中的常规 Bean 定义,该定义指向具有`@Aspect`注释的 Bean 类: ``` ``` 两个示例中的第二个示例显示了`NotVeryUsefulAspect`类定义,该定义使用`org.aspectj.lang.annotation.Aspect`注释; 爪哇 ``` package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { } ``` Kotlin ``` package org.xyz import org.aspectj.lang.annotation.Aspect; @Aspect class NotVeryUsefulAspect ``` 方面(用`@Aspect`注释的类)可以具有与任何其他类相同的方法和字段。它们还可以包含切入点、建议和 Introduction(类型间)声明。 | |通过组件扫描自动检测方面

你可以在你的 Spring XML 配置中将方面类注册为常规 bean,
通过`@Bean`类中的`@Bean`方法,或者通过
类自动检测它们,或者通过
Classpath 扫描—与任何其他 Spring 管理的 Bean 相同。然而,需要注意的是,在 Classpath 中,“@ 方面”注释对于自动检测是不够的。为了达到
的目的,你需要添加一个单独的`@Component`注释(或者,根据 Spring 的组件扫描仪的规则,添加一个自定义的
原型注释)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |与其他方面的通知方面?

在 Spring AOP 中,方面本身不能成为来自其他
方面的通知的对象。类上的`@Aspect`注释将其标记为一个方面,因此将
从自动代理中排除。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.4.3.声明切入点 切入点确定感兴趣的连接点,从而使我们能够控制通知何时运行。 Spring AOP 仅支持 Spring bean 的方法执行连接点,因此可以将切入点视为匹配 Spring bean 上方法的执行。切入点声明有两个部分:一个包含名称和任何参数的签名,以及一个切入点表达式,该表达式确定我们感兴趣的确切方法执行。在 AOP 的 @AspectJ 注释样式中,切入点签名由一个常规方法定义提供,切入点表达式由`@Pointcut`注释表示(充当切入点签名的方法必须具有`void`返回类型)。 一个示例可能有助于明确切入点签名和切入点表达式之间的区别。下面的示例定义了一个名为`anyOldTransfer`的切入点,该切入点与任何名为`transfer`的方法的执行相匹配: 爪哇 ``` @Pointcut("execution(* transfer(..))") // the pointcut expression private void anyOldTransfer() {} // the pointcut signature ``` Kotlin ``` @Pointcut("execution(* transfer(..))") // the pointcut expression private fun anyOldTransfer() {} // the pointcut signature ``` 形成`@Pointcut`注释的值的 pointcut 表达式是一个正则 AspectJ pointcut 表达式。有关 AspectJ 的 PointCut 语言的完整讨论,请参见[AspectJ 编程指南](https://www.eclipse.org/aspectj/doc/released/progguide/index.html)(以及扩展,[AspectJ5 开发者笔记本](https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html))或 AspectJ 上的一本书(例如 Colyer 等人的*Eclipse AspectJ*,或 Ramnivas Laddad 的*AspectJ 在行动*)。 ##### 支持的切入点指示符 Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示器: * `execution`:用于匹配方法执行连接点。这是在使用 Spring AOP 时要使用的主要切入点指示器。 * `within`:对某些类型内的连接点的匹配进行限制(在使用 Spring AOP 时在匹配类型内声明的方法的执行)。 * `this`:限制匹配到联结点(在使用 Spring AOP 时执行方法),其中 Bean 引用( Spring AOP 代理)是给定类型的实例。 * `target`:将匹配限制到连接点(在使用 Spring AOP 时执行方法),其中目标对象(正在代理的应用程序对象)是给定类型的实例。 * `args`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中参数是给定类型的实例。 * `@target`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中执行对象的类具有给定类型的注释。 * `@args`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。 * `@within`:限制与具有给定注释的类型内的连接点的匹配(在使用 Spring AOP 时,在具有给定注释的类型中声明的方法的执行)。 * `@annotation`:将匹配限制为连接点的主题(在 Spring AOP 中运行的方法)具有给定注释的连接点。 其他切入点类型 完整的 AspectJ PointCut 语言支持在 Spring 中不支持的额外的 PointCut 指示器:,,,,,”4544“/>,”flowr=“<4546”,">。在由 Spring AOP 解释的切入点表达式中使用这些切入点指示符将导致抛出`IllegalArgumentException`。 Spring AOP 支持的切入点指示器的集合可以在将来的版本中进行扩展,以支持更多的 AspectJ 切入点指示器。 由于 Spring AOP 将匹配限制为仅与方法执行连接点匹配,因此前面对切入点指示器的讨论给出了比 AspectJ 编程指南中所能找到的更窄的定义。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点,`this`和`target`都指向相同的对象:执行方法的对象。 Spring AOP 是一种基于代理的系统,并在代理对象本身(其被绑定到)和代理背后的目标对象(其被绑定到)之间进行区分。 | |由于 Spring 的 AOP 框架的基于代理的性质,根据定义,目标对象
内的调用不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法
调用。使用 CGlib,对
代理的公共和受保护方法调用将被截获(如果需要,甚至还会截获包可见的方法)。但是,
通过代理进行的公共交互应该始终通过公共签名来设计。
注意,切入点定义通常与任何截获的方法相匹配。
如果切入点严格地说是仅用于公共的,即使在 CGLIB 代理场景中,
通过代理进行的潜在的非公共交互,

如果你的拦截需要在目标
类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的[原生 AspectJ 编织](#aop-aj-ltw)而不是 Spring 的基于代理的 AOP 框架的
。这构成了具有不同特征的 AOP 使用
的不同模式,因此在做出决定之前,请务必使自己熟悉编织
。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring AOP 还支持名为`bean`的附加 PCD。此 PCD 允许你将连接点的匹配限制为特定的命名 Spring Bean 或命名的 Spring bean 的集合(当使用通配符时)。`bean`pcd 具有以下形式: 爪哇 ``` bean(idOrNameOfBean) ``` Kotlin ``` bean(idOrNameOfBean) ``` `idOrNameOfBean`令牌可以是任何 Spring Bean 的名称。提供了使用`*`字符的有限通配符支持,因此,如果你为 Spring bean 建立了一些命名约定,则可以编写`bean`PCD 表达式来选择它们。与其他切入点指示符的情况一样,`bean`PCD 也可以与`&&`(和)、`||`(或)和`!`(否定)操作符一起使用。 | |`bean`PCD 仅在 Spring AOP 中支持,而在
本地 AspectJ 编织中不支持。它是标准 PCD 的 Spring 特定扩展,
AspectJ 定义了该扩展,因此,不可用对于在`@Aspect`模型中声明的方面,

`bean`PCD 在实例级运行(构建在 Spring Bean 名称
概念上)
基于实例的切入点指示器是 Spring 的
基于代理的 AOP 框架及其与 Spring Bean 工厂(其中
工厂)的紧密集成的一种特殊功能。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 结合切入点表达式 可以使用`&&,``||`和`!`合并切入点表达式。你也可以按名称引用切入点表达式。下面的示例显示了三个切入点表达式: 爪哇 ``` @Pointcut("execution(public * *(..))") private void anyPublicOperation() {} (1) @Pointcut("within(com.xyz.myapp.trading..*)") private void inTrading() {} (2) @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {} (3) ``` |**1**|如果方法执行连接点表示任何公共方法的执行
,则`anyPublicOperation`匹配。| |-----|----------------------------------------------------------------------------------------------------------------| |**2**|如果方法执行在交易模块中,则`inTrading`匹配。| |**3**|如果方法执行表示
交易模块中的任何公共方法,则`tradingOperation`匹配。| Kotlin ``` @Pointcut("execution(public * *(..))") private fun anyPublicOperation() {} (1) @Pointcut("within(com.xyz.myapp.trading..*)") private fun inTrading() {} (2) @Pointcut("anyPublicOperation() && inTrading()") private fun tradingOperation() {} (3) ``` |**1**|如果方法执行连接点表示任何公共方法的执行
,则`anyPublicOperation`匹配。| |-----|----------------------------------------------------------------------------------------------------------------| |**2**|如果方法执行在交易模块中,则`inTrading`匹配。| |**3**|如果方法执行表示
交易模块中的任何公共方法,则`tradingOperation`匹配。| 正如前面所示,从较小的命名组件中构建更复杂的切入点表达式是一种最佳实践。当按名称引用切入点时,将应用普通的 爪哇 可见性规则(你可以看到相同类型的私有切入点,层次结构中的受保护切入点,任何地方的公共切入点,等等)。可见性不影响切入点匹配。 ##### 共享公共切入点定义 在使用 Enterprise 应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议为此目的定义一个`CommonPointcuts`方面来捕获公共切入点表达式。这样的方面通常类似于以下示例: 爪哇 ``` package com.xyz.myapp; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; @Aspect public class CommonPointcuts { /** * A join point is in the web layer if the method is defined * in a type in the com.xyz.myapp.web package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.web..*)") public void inWebLayer() {} /** * A join point is in the service layer if the method is defined * in a type in the com.xyz.myapp.service package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.service..*)") public void inServiceLayer() {} /** * A join point is in the data access layer if the method is defined * in a type in the com.xyz.myapp.dao package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.dao..*)") public void inDataAccessLayer() {} /** * A business service is the execution of any method defined on a service * interface. This definition assumes that interfaces are placed in the * "service" package, and that implementation types are in sub-packages. * * If you group service interfaces by functional area (for example, * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))" * could be used instead. * * Alternatively, you can write the expression using the 'bean' * PCD, like so "bean(*Service)". (This assumes that you have * named your Spring service beans in a consistent fashion.) */ @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") public void businessService() {} /** * A data access operation is the execution of any method defined on a * dao interface. This definition assumes that interfaces are placed in the * "dao" package, and that implementation types are in sub-packages. */ @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))") public void dataAccessOperation() {} } ``` Kotlin ``` package com.xyz.myapp import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Pointcut @Aspect class CommonPointcuts { /** * A join point is in the web layer if the method is defined * in a type in the com.xyz.myapp.web package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.web..*)") fun inWebLayer() { } /** * A join point is in the service layer if the method is defined * in a type in the com.xyz.myapp.service package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.service..*)") fun inServiceLayer() { } /** * A join point is in the data access layer if the method is defined * in a type in the com.xyz.myapp.dao package or any sub-package * under that. */ @Pointcut("within(com.xyz.myapp.dao..*)") fun inDataAccessLayer() { } /** * A business service is the execution of any method defined on a service * interface. This definition assumes that interfaces are placed in the * "service" package, and that implementation types are in sub-packages. * * If you group service interfaces by functional area (for example, * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))" * could be used instead. * * Alternatively, you can write the expression using the 'bean' * PCD, like so "bean(*Service)". (This assumes that you have * named your Spring service beans in a consistent fashion.) */ @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") fun businessService() { } /** * A data access operation is the execution of any method defined on a * dao interface. This definition assumes that interfaces are placed in the * "dao" package, and that implementation types are in sub-packages. */ @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))") fun dataAccessOperation() { } } ``` 你可以在需要切入点表达式的任何地方引用在这样的方面中定义的切入点。例如,要使服务层具有事务性,你可以编写以下内容: ``` ``` ``和``元素在[Schema-based AOP Support](#aop-schema)中讨论。事务元素在[事务管理](data-access.html#transaction)中讨论。 ##### 例子 Spring AOP 用户很可能最常使用`execution`切入点指示符。执行表达式的格式如下: ``` execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) ``` 除了返回类型模式(前一个代码片段中的“ret-type-pattern”)、名称模式和参数模式以外的所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,才能匹配连接点。`*’是最常用的返回类型模式。它匹配任何返回类型。只有当方法返回给定的类型时,完全限定的类型名才匹配。名称模式与方法名匹配。可以使用`*`通配符作为名称模式的全部或部分。如果指定了声明类型模式,请包含一个尾随`.`,以将其连接到名称模式组件。参数模式稍微复杂一些:`()`匹配不接受参数的方法,而`(..)`匹配任意数量(零或更多)的参数。`(*)`模式匹配一个接受任何类型的一个参数的方法。`(*,字符串)` 匹配一个接受两个参数的方法。第一个可以是任何类型,而第二个必须是`String`。有关更多信息,请参阅 AspectJ 编程指南的[语言语义学](https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html)部分。 以下示例展示了一些常见的切入点表达式: * 任何公开方式的执行: ``` execution(public * *(..)) ``` * 执行名称以`set`开头的任何方法: ``` execution(* set*(..)) ``` * 由`AccountService`接口定义的任何方法的执行: ``` execution(* com.xyz.service.AccountService.*(..)) ``` * 在`service`包中定义的任何方法的执行: ``` execution(* com.xyz.service.*.*(..)) ``` * 在服务包或其子包中定义的任何方法的执行: ``` execution(* com.xyz.service..*.*(..)) ``` * 服务包中的任何连接点(仅在 Spring AOP 中执行方法): ``` within(com.xyz.service.*) ``` * 服务包或其子包中的任何连接点(方法仅在 Spring AOP 中执行): ``` within(com.xyz.service..*) ``` * 代理实现“AccountService”接口的任何连接点(仅在 Spring AOP 中执行方法): ``` this(com.xyz.service.AccountService) ``` | |`this`更常用的是绑定形式。关于如何使代理对象在建议主体中可用,请参见[声明建议](#aop-advice)一节。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| * 目标对象实现`AccountService`接口的任何连接点(仅在 Spring AOP 中执行方法): ``` target(com.xyz.service.AccountService) ``` | |`target`更常用的是绑定形式。关于如何使目标对象在建议主体中可用,请参见[声明建议](#aop-advice)节
。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| * 获取单个参数并且在运行时传递的参数是`Serializable`的任何连接点(仅在 Spring AOP 中执行方法): ``` args(java.io.Serializable) ``` | |`args`更常用的是绑定形式。关于如何使方法参数在建议主体中可用,请参见[声明建议](#aop-advice)节
。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| 请注意,本例中给出的切入点与`execution(* *(java.io.Serializable))`不同。如果在运行时传递的参数是 `Serializable’,则 ARGS 版本匹配,如果方法签名声明一个类型为`Serializable`的参数,则执行版本匹配。 * 目标对象具有“@Transactional”注释的任何连接点(仅在 Spring AOP 中执行方法): ``` @target(org.springframework.transaction.annotation.Transactional) ``` | |也可以在绑定形式中使用`@target`。关于
如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)小节。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| * 任何连接点(仅在 Spring AOP 中执行方法),其中目标对象的声明类型具有`@Transactional`注释: ``` @within(org.springframework.transaction.annotation.Transactional) ``` | |也可以在绑定形式中使用`@within`。关于
如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)小节。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| * 任何连接点(仅在 Spring AOP 中执行方法),其中执行方法具有“@transactional”注释: ``` @annotation(org.springframework.transaction.annotation.Transactional) ``` | |也可以在绑定形式中使用`@annotation`。关于如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)节
。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| * 任何接受单个参数的连接点(仅在 Spring AOP 中执行方法),其中传递的参数的运行时类型具有`@Classified`注释: ``` @args(com.xyz.security.Classified) ``` | |也可以在绑定形式中使用`@args`。请参阅[声明建议](#aop-advice)部分
如何使注释对象在建议主体中可用。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------| * 命名为“TradeService”的 Spring Bean 上的任何连接点(方法仅在 Spring AOP 中执行): ``` bean(tradeService) ``` * 具有与通配符表达式`*Service`匹配的名称的 Spring bean 上的任何连接点(方法仅在 Spring AOP 中执行): ``` bean(*Service) ``` ##### 写出好的切入点 在编译过程中,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态地)匹配给定的切入点是一个昂贵的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中进行测试以确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点被重写为 DNF(析取范式),切入点的组件被排序,以便首先检查那些更便宜的评估组件。这意味着你不必担心理解各种切入点指示器的性能,并且可以在切入点声明中以任何顺序提供它们。 然而,AspectJ 只能根据所告知的内容工作。为了获得最佳的匹配性能,你应该考虑他们试图实现的目标,并在定义中尽可能地缩小匹配的搜索空间。现有的指示器自然分为以下三类:Kinded、范围界定和上下文关联: * Kinded 指示器选择一种特殊的连接点:`execution’,`get`,`set`,`call`,和`handler`。 * 范围指示器选择一组感兴趣的连接点(可能有多种):`within`和`withincode` * 上下文指示符基于上下文匹配(并可选绑定):“this”、`target`和`@annotation` 写得好的切入点应该至少包括前两种类型(Kinded 和 Scoping)。你可以包括上下文指示符,以便根据连接点上下文进行匹配,也可以将该上下文绑定,以便在建议中使用。仅提供 Kinded 指示器或仅提供上下文指示器可以工作,但由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指示器的匹配速度非常快,使用它们意味着 AspectJ 可以非常快地删除不应进一步处理的连接点组。一个好的切入点应该总是包括一个,如果可能的话。 #### 5.4.4.声明建议 通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或周围运行。PointCut 表达式可以是对已命名的 PointCut 的简单引用,也可以是已声明的 PointCut 表达式。 ##### 建议之前 你可以使用`@Before`注释在一个方面的建议之前声明: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doAccessCheck() { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before @Aspect class BeforeExample { @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") fun doAccessCheck() { // ... } } ``` 如果我们使用一个就地切入点表达式,我们可以将前面的示例重写为下面的示例: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { @Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Before @Aspect class BeforeExample { @Before("execution(* com.xyz.myapp.dao.*.*(..))") fun doAccessCheck() { // ... } } ``` ##### 在返回建议后 返回通知后,当匹配的方法执行正常返回时运行。你可以使用`@AfterReturning`注释来声明它: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doAccessCheck() { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterReturning @Aspect class AfterReturningExample { @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") fun doAccessCheck() { // ... } } ``` | |你可以在同一个方面中拥有多个通知声明(以及其他成员)
。在这些
示例中,我们只显示了一个通知声明,以集中显示每个通知声明的效果。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 有时,你需要在建议主体中访问返回的实际值。可以使用绑定返回值的`@AfterReturning`形式来获得访问权限,如下例所示: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; @Aspect public class AfterReturningExample { @AfterReturning( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterReturning @Aspect class AfterReturningExample { @AfterReturning( pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()", returning = "retVal") fun doAccessCheck(retVal: Any) { // ... } } ``` `returning`属性中使用的名称必须与通知方法中的参数名称对应。当方法执行返回时,返回值将作为相应的参数值传递给通知方法。`returning`子句还限制只匹配那些返回指定类型的值的方法执行(在本例中,`Object`,它匹配任何返回值)。 请注意,在返回建议后使用时,不可能返回完全不同的参考。 ##### 在提出建议之后 抛出建议后,当匹配的方法执行通过抛出异常退出时,将运行该建议。你可以使用`@AfterThrowing`注释来声明它,如下例所示: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doRecoveryActions() { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterThrowing @Aspect class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") fun doRecoveryActions() { // ... } } ``` 通常,你希望仅在抛出给定类型的异常时才运行该建议,并且你还经常需要访问建议主体中抛出的异常。你可以使用`throwing`属性来限制匹配(如果需要的话,使用`Throwable`作为异常类型),并将抛出的异常绑定到一个通知参数。下面的示例展示了如何做到这一点: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; @Aspect public class AfterThrowingExample { @AfterThrowing( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.AfterThrowing @Aspect class AfterThrowingExample { @AfterThrowing( pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()", throwing = "ex") fun doRecoveryActions(ex: DataAccessException) { // ... } } ``` `throwing`属性中使用的名称必须与通知方法中的参数名称对应。当一个方法通过抛出一个异常而退出执行时,该异常将作为相应的参数值传递给通知方法。`throwing`子句还限制只匹配那些抛出指定类型异常的方法执行(本例中为 `DataAccessException’)。 | |注意,`@AfterThrowing`并不表示一般的异常处理回调。
具体地说,一个`@AfterThrowing`通知方法只应该从连接点(用户声明的目标方法)本身接收异常
,而不是从伴随的 `@afterreturn` 方法接收异常。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 建议 当匹配的方法执行退出时,After(最终)通知将运行。它是通过使用`@After`注释声明的。建议后必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。下面的示例展示了如何在“最终建议”之后使用它: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doReleaseLock() { // ... } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.After @Aspect class AfterFinallyExample { @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") fun doReleaseLock() { // ... } } ``` | |注意,AspectJ 中的`@After`通知被定义为“after finally advice”,类似于 try-catch 语句中的 finally 块。它将针对任何结果被调用,
正常返回或从连接点(用户声明的目标方法)抛出的异常,
与`@AfterReturning`相反,后者仅适用于成功的正常返回。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 围绕建议 最后一种建议是*周围*建议。围绕建议运行“围绕”一个匹配的方法的执行。它有机会在方法运行之前和之后都进行工作,并确定何时、如何以及即使方法实际运行了。如果你需要以线程安全的方式共享方法执行之前和之后的状态,通常会使用“周围建议”——例如,启动和停止计时器。 | |始终使用满足你的要求的功能最小的通知形式。

例如,如果*周围*通知足以满足你的需求,则不要使用*周围*通知。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 通过对带有`@Around`注释的方法进行注释来声明“建议周围”。方法应该声明`Object`作为其返回类型,并且方法的第一个参数必须是`ProceedingJoinPoint`类型。在通知方法的主体中,你必须在`ProceedingJoinPoint`上调用`proceed()`才能运行底层方法。在没有参数的情况下调用`proceed()`将导致调用者的原始参数在调用时被提供给底层方法。对于高级用例,有一个重载的`proceed()`方法的变体,它接受一个参数数组。当调用底层方法时,数组中的值将被用作该方法的参数。 | |当使用`Object[]`调用`proceed`时,
的行为与 AspectJ 编译器编译的 around 建议的`proceed`的行为略有不同。对于使用传统 AspectJ 语言编写的 around
通知,传递到 `Proceed’的参数数量必须与传递到 around 通知的参数数量匹配(而不是底层连接点的参数数量
),并且在
给定的参数位置中传递以继续进行的值取代了
值绑定到的实体在连接点处的原始值(不要担心)

Spring 所采用的方法更简单,并且更好地匹配其基于代理的,
仅执行的语义。你只需要在编译为 Spring 编写的 `@AspectJ’方面并使用与 AspectJ编译器和 Weaver 的参数时,才需要注意到这种差异。有一种方法可以在
Spring AOP 和 AspectJ 之间编写 100% 兼容的这样的方面,这在[以下关于建议参数的一节](#aop-ataspectj-advice-proceeding-with-the-call)中进行了讨论。| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| around 通知返回的值是该方法的调用者看到的返回值。例如,一个简单的缓存方面可以从缓存返回一个值,如果它有一个值,或者调用`proceed()`(如果没有,则返回该值)。请注意,`proceed`可以在 around 建议的主体内被调用一次,多次,或者根本不调用。所有这些都是合法的。 | |如果你将 around advice 方法的返回类型声明为`void`,则`null`将始终返回给调用方,实际上忽略了
of`proceed()`的任何调用的结果。因此,建议使用 around advice 方法声明
类型的`Object`。通知方法通常应该返回从
调用`proceed()`返回的值,即使底层方法具有`void`返回类型。
但是,根据使用情况,通知可以选择返回一个缓存的值、一个包装的值或其他
值。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了如何使用“周围建议”: 爪哇 ``` import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { @Around("com.xyz.myapp.CommonPointcuts.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } } ``` Kotlin ``` import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Around import org.aspectj.lang.ProceedingJoinPoint @Aspect class AroundExample { @Around("com.xyz.myapp.CommonPointcuts.businessService()") fun doBasicProfiling(pjp: ProceedingJoinPoint): Any { // start stopwatch val retVal = pjp.proceed() // stop stopwatch return retVal } } ``` ##### 建议参数 Spring 提供完全类型的建议,这意味着你在建议签名中声明所需的参数(正如我们在前面的返回和抛出示例中看到的那样),而不是始终使用`Object[]`数组。在这一节的后面部分,我们将看到如何使参数和其他上下文值可用于建议主体。首先,我们来看看如何编写通用的建议,以了解该建议目前建议的方法。 ###### 访问当前`JoinPoint` 任何通知方法都可以声明一个类型为 `org.aspectj.lang.joinpoint’的参数作为其第一个参数。注意,around advice 需要声明类型`ProceedingJoinPoint`的第一个参数,这是`JoinPoint`的子类。 `JoinPoint`接口提供了许多有用的方法: * `getArgs()`:返回方法参数。 * `getThis()`:返回代理对象。 * `getTarget()`:返回目标对象。 * `getSignature()`:返回所建议方法的描述。 * `toString()`:打印所建议的方法的有用描述。 有关更多详细信息,请参见[javadoc](https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lang/JoinPoint.html)。 ###### 将参数传递给建议 我们已经了解了如何绑定返回值或异常值(在返回后和抛出建议后使用)。要使参数值对建议主体可用,你可以使用`args`的绑定形式。如果在`args`表达式中使用参数名代替类型名,则在调用通知时将相应参数的值作为参数值传递。举个例子应该能更清楚地说明这一点。假设你想建议以`Account`对象作为第一个参数的 DAO 操作的执行,并且你需要访问建议主体中的帐户。你可以写下: 爪哇 ``` @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") public void validateAccount(Account account) { // ... } ``` Kotlin ``` @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") fun validateAccount(account: Account) { // ... } ``` 切入点表达式的`args(account,..)`部分有两个目的。首先,它只限制匹配那些方法执行,其中该方法至少接受一个参数,并且传递给该参数的参数是`Account`的实例。其次,它通过`account`参数使实际的`Account`对象对通知可用。 另一种编写方法是声明一个切入点,该切入点在匹配连接点时“提供”`Account`对象值,然后从通知中引用命名的切入点。其内容如下: 爪哇 ``` @Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") private void accountDataAccessOperation(Account account) {} @Before("accountDataAccessOperation(account)") public void validateAccount(Account account) { // ... } ``` Kotlin ``` @Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") private fun accountDataAccessOperation(account: Account) { } @Before("accountDataAccessOperation(account)") fun validateAccount(account: Account) { // ... } ``` 有关更多详细信息,请参见 AspectJ 编程指南。 代理对象(“this”)、目标对象(“target”)和注释(“@within”、“@target”、`@annotation`和`@args`)都可以以类似的方式绑定。接下来的两个示例展示了如何匹配带有`@Auditable`注释的方法的执行并提取审计代码: 这两个示例中的第一个示例显示了`@Auditable`注释的定义: 爪哇 ``` @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); } ``` Kotlin ``` @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class Auditable(val value: AuditCode) ``` 这两个示例中的第二个示例显示了与`@Auditable`方法的执行相匹配的建议: 爪哇 ``` @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... } ``` Kotlin ``` @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") fun audit(auditable: Auditable) { val code = auditable.value() // ... } ``` ###### 建议参数和泛型 Spring AOP 可以处理在类声明和方法参数中使用的泛型。假设你有一个通用类型,如下所示: 爪哇 ``` public interface Sample { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection param); } ``` Kotlin ``` interface Sample { fun sampleGenericMethod(param: T) fun sampleGenericCollectionMethod(param: Collection) } ``` 可以通过将通知参数与要截取方法的参数类型绑定,将方法类型的截取限制为某些参数类型: 爪哇 ``` @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") public void beforeSampleMethod(MyType param) { // Advice implementation } ``` Kotlin ``` @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") fun beforeSampleMethod(param: MyType) { // Advice implementation } ``` 这种方法不适用于泛型集合。因此,你不能如下定义切入点: 爪哇 ``` @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") public void beforeSampleMethod(Collection param) { // Advice implementation } ``` Kotlin ``` @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") fun beforeSampleMethod(param: Collection) { // Advice implementation } ``` 要实现此工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何一般地处理`null`值。要实现类似的功能,你必须将参数键入`Collection`,并手动检查元素的类型。 ###### 确定参数名称 通知调用中的参数绑定依赖于将切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称进行匹配。参数名称不能通过 爪哇 反射获得,因此 Spring AOP 使用以下策略来确定参数名称: * 如果参数名称是由用户显式指定的,则使用指定的参数名称。通知和切入点注释都有一个可选的`argNames`属性,你可以使用该属性指定带注释方法的参数名称。这些参数名称在运行时可用。下面的示例展示了如何使用`argNames`属性: 爪哇 ``` @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean,auditable") public void audit(Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code and bean } ``` Kotlin ``` @Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable") fun audit(bean: Any, auditable: Auditable) { val code = auditable.value() // ... use code and bean } ``` 如果第一个参数是`JoinPoint`、`ProceedingJoinPoint`或 `joinpoint.staticpart`type,则可以从`argNames`属性的值中省略参数的名称。例如,如果你修改前面的通知以接收连接点对象,则`argNames`属性不需要包括它: 爪哇 ``` @Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean,auditable") public void audit(JoinPoint jp, Object bean, Auditable auditable) { AuditCode code = auditable.value(); // ... use code, bean, and jp } ``` Kotlin ``` @Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable") fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) { val code = auditable.value() // ... use code, bean, and jp } ``` 对`JoinPoint`、`proceedingjoinpoint’和`JoinPoint.StaticPart`类型的第一个参数的特殊处理对于不收集任何其他连接点上下文的建议实例特别方便。在这种情况下,你可以省略`argNames`属性。例如,以下建议不需要声明`argNames`属性: 爪哇 ``` @Before("com.xyz.lib.Pointcuts.anyPublicMethod()") public void audit(JoinPoint jp) { // ... use jp } ``` Kotlin ``` @Before("com.xyz.lib.Pointcuts.anyPublicMethod()") fun audit(jp: JoinPoint) { // ... use jp } ``` * 使用`argNames`属性有点笨拙,因此,如果没有指定`argNames`属性, Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要类已经编译了调试信息(“-g:vars”至少),就存在此信息。使用此标志进行编译的结果是:(1)你的代码稍微容易理解(逆向工程),(2)类文件的大小稍微大一些(通常不重要),(3)你的编译器不应用删除未使用的局部变量的优化。换句话说,你应该不会遇到任何困难,通过建设与这一标志。 | |如果一个 @AspectJ 方面已经由 AspectJ 编译器编译,甚至
都没有调试信息,那么你就不需要添加`argNames`属性,因为编译器
保留了所需的信息。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| * Spring AOP 如果在没有必要的调试信息的情况下编译了代码,则尝试推断绑定变量到参数的配对(例如,如果在切入点表达式中只绑定了一个变量,并且通知方法只使用了一个参数,则该配对是显而易见的)。如果给定可用信息,变量的绑定是模棱两可的,则抛出一个`AmbiguousBindingException`。 * 如果上述所有策略都失败,则抛出一个`IllegalArgumentException`。 ###### 继续进行辩论 我们在前面提到,我们将描述如何使用在 Spring AOP 和 AspectJ 上一致工作的参数编写`proceed`调用。解决方案是确保通知签名按顺序绑定每个方法参数。下面的示例展示了如何做到这一点: 爪哇 ``` @Around("execution(List find*(..)) && " + "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " + "args(accountHolderNamePattern)") public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable { String newPattern = preProcess(accountHolderNamePattern); return pjp.proceed(new Object[] {newPattern}); } ``` Kotlin ``` @Around("execution(List find*(..)) && " + "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " + "args(accountHolderNamePattern)") fun preProcessQueryPattern(pjp: ProceedingJoinPoint, accountHolderNamePattern: String): Any { val newPattern = preProcess(accountHolderNamePattern) return pjp.proceed(arrayOf(newPattern)) } ``` 在许多情况下,无论如何都要进行这种绑定(如前面的示例)。 ##### 建议订购 当多个建议都希望在同一个连接点运行时会发生什么情况? Spring AOP 遵循与 AspectJ 相同的优先规则来确定通知执行的顺序。优先级最高的建议首先“在途中”运行(因此,如果给出了两条之前的建议,优先级最高的建议将首先运行)。在从联结点“退出”时,最高优先级的通知最后运行(因此,给定两条后通知,具有最高优先级的通知将运行第二条)。 当在不同方面中定义的两个通知都需要在相同的连接点上运行时,除非你另有指定,否则执行的顺序是未定义的。你可以通过指定优先级来控制执行的顺序。这是通过在 Aspect 类中实现`org.springframework.core.Ordered`接口或使用`@Order`注释来以正常的方式完成的。给定两个方面,从`Ordered.getOrder()`返回较低的值(或注释值)的方面具有较高的优先权。 | |特定方面的每种不同的通知类型在概念上都意味着将
直接应用到连接点。因此,`@AfterThrowing`通知方法不是
应该从随附的`@After`/`@afterreturn` 方法接收异常的方法。

在 Spring framework5.2.7 中,在相同的`@Aspect`类中定义的通知方法,如果
需要在相同的连接点上运行,则根据其在
中的通知类型,按照以下顺序,从最高优先级到最低优先级:`@Around`,`@After`,<@afterreturn`,。但是,请注意,在相同的方面中,当使用任何`@AfterReturning`或`@AfterThrowing`通知方法
时,在 AspectJ 的`@After`的“after finally advice”语义之后,将有效地调用
通知方法。
当两个相同类型的通知(例如,
)时,两个`@After`通知方法)
在同一个`@Aspect`类中定义的
类都需要在相同的连接点上运行,其排序
是未定义的(因为没有办法通过
反射来检索源代码的声明顺序,用于 javac 编译的类)。考虑在每个`@Aspect`类中的每个连接点将这样的通知方法折叠成一个
通知方法,或者将这些通知片段重构为
单独的`@Aspect`类,你可以通过`Ordered`或`@Order`在方面级别订购这些类。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.4.5.介绍 引入(在 AspectJ 中称为类型间声明)使方面能够声明建议的对象实现了给定的接口,并代表这些对象提供了该接口的实现。 你可以使用`@DeclareParents`注释进行介绍。此注释用于声明匹配的类型有一个新的父类型(因此命名)。例如,给定一个名为`UsageTracked`的接口和一个名为 `DefaultUsageTracked’的接口的实现,以下方面声明服务接口的所有实现者也实现`UsageTracked`接口(例如,通过 JMX 进行统计): 爪哇 ``` @Aspect public class UsageTracking { @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) public static UsageTracked mixin; @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } } ``` Kotlin ``` @Aspect class UsageTracking { companion object { @DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class) lateinit var mixin: UsageTracked } @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") fun recordUsage(usageTracked: UsageTracked) { usageTracked.incrementUseCount() } } ``` 要实现的接口由注释字段的类型决定。`@DeclareParents`注释的 `value’属性是一个 AspectJ 类型模式。匹配类型的任何 Bean 实现`UsageTracked`接口。注意,在前面示例的 before 通知中,服务 bean 可以直接用作`UsageTracked`接口的实现。如果以编程方式访问 Bean,你将编写以下内容: 爪哇 ``` UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); ``` Kotlin ``` val usageTracked = context.getBean("myService") as UsageTracked ``` #### 5.4.6.方面实例化模型 | |这是一个高级话题。如果你刚开始使用 AOP,则可以安全地跳过
它,直到稍后。| |---|---------------------------------------------------------------------------------------------------------| 默认情况下,在应用程序上下文中,每个方面都有一个实例。AspectJ 将其称为单例实例化模型。可以用替代的生命周期来定义方面。 Spring 目前不支持 AspectJ 的`perthis`和`pertarget`实例化模型;`percflow`、`percflowbelow`和`pertypewithin`。 你可以通过在`@Aspect`注释中指定`perthis`子句来声明`perthis`方面。考虑以下示例: 爪哇 ``` @Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())") public class MyAspect { private int someState; @Before("com.xyz.myapp.CommonPointcuts.businessService()") public void recordServiceUsage() { // ... } } ``` Kotlin ``` @Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())") class MyAspect { private val someState: Int = 0 @Before("com.xyz.myapp.CommonPointcuts.businessService()") fun recordServiceUsage() { // ... } } ``` 在前面的示例中,`perthis`子句的效果是,为执行业务服务的每个唯一服务对象创建一个方面实例(每个在切入点表达式匹配的连接点绑定到`this`的唯一对象)。第一次在服务对象上调用方法时,将创建方面实例。当服务对象超出范围时,方面就超出了范围。在创建方面实例之前,它中的任何建议都不会运行。一旦创建了方面实例,其中声明的通知就会在匹配的连接点上运行,但仅当服务对象是与该方面相关联的对象时才会运行。有关`per`子句的更多信息,请参见 AspectJ 编程指南。 `pertarget`实例化模型的工作方式与`perthis`完全相同,但它在匹配的连接点上为每个唯一的目标对象创建一个方面实例。 #### 5.4.7. AOP 例 既然你已经了解了所有组成部分是如何工作的,那么我们可以将它们组合在一起来做一些有用的事情。 业务服务的执行有时会由于并发性问题(例如,死锁失败者)而失败。如果该操作被重试,则很可能在下一次尝试中成功。对于在这种情况下(幂等运算不需要返回给用户以解决冲突)适合重试的业务服务,我们希望透明地重试该操作,以避免客户端看到“悲观锁定失败异常”。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过一个方面来实现。 因为我们想要重试操作,所以我们需要使用 around advice,这样我们就可以多次调用`proceed`。下面的清单展示了基本方面的实现: 爪哇 ``` @Aspect public class ConcurrentOperationExecutor implements Ordered { private static final int DEFAULT_MAX_RETRIES = 2; private int maxRetries = DEFAULT_MAX_RETRIES; private int order = 1; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } @Around("com.xyz.myapp.CommonPointcuts.businessService()") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; PessimisticLockingFailureException lockFailureException; do { numAttempts++; try { return pjp.proceed(); } catch(PessimisticLockingFailureException ex) { lockFailureException = ex; } } while(numAttempts <= this.maxRetries); throw lockFailureException; } } ``` Kotlin ``` @Aspect class ConcurrentOperationExecutor : Ordered { private val DEFAULT_MAX_RETRIES = 2 private var maxRetries = DEFAULT_MAX_RETRIES private var order = 1 fun setMaxRetries(maxRetries: Int) { this.maxRetries = maxRetries } override fun getOrder(): Int { return this.order } fun setOrder(order: Int) { this.order = order } @Around("com.xyz.myapp.CommonPointcuts.businessService()") fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { var numAttempts = 0 var lockFailureException: PessimisticLockingFailureException do { numAttempts++ try { return pjp.proceed() } catch (ex: PessimisticLockingFailureException) { lockFailureException = ex } } while (numAttempts <= this.maxRetries) throw lockFailureException } } ``` 注意,方面实现了`Ordered`接口,这样我们就可以将方面的优先级设置为高于事务通知的优先级(每次重试时,我们都希望有一个新的事务)。`maxRetries`和`order`属性都是由 Spring 配置的。主要的动作发生在`doConcurrentOperation`周围的建议中。请注意,目前,我们将重试逻辑应用于每个`businessService()`。我们尝试继续,如果`PessimisticLockingFailureException`失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。 相应的 Spring 配置如下: ``` ``` 为了细化方面,使其仅重试幂等运算,我们可以定义以下 ` 幂等’注释: 爪哇 ``` @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // marker annotation } ``` Kotlin ``` @Retention(AnnotationRetention.RUNTIME) annotation class Idempotent// marker annotation ``` 然后,我们可以使用注释来注释服务操作的实现。对只重试幂等运算的方面的更改涉及细化切入点表达式,使只有`@Idempotent`运算匹配,如下所示: 爪哇 ``` @Around("com.xyz.myapp.CommonPointcuts.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { // ... } ``` Kotlin ``` @Around("com.xyz.myapp.CommonPointcuts.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { // ... } ``` ### 5.5.基于模式的 AOP 支持 如果你更喜欢基于 XML 的格式, Spring 还提供了使用`aop`名称空间标记来定义方面的支持。支持与使用 @AspectJ 样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点讨论该语法,并请读者参考上一节中的讨论([@AspectJ 支持](#aop-ataspectj)),以了解如何编写切入点表达式和绑定建议参数。 要使用本节中描述的 AOP 命名空间标记,你需要导入[基于 XML 模式的配置](#xsd-schemas)中描述的 ` Spring- AOP ` 模式。有关如何导入`aop`命名空间中的标记,请参见[the AOP schema](#xsd-schemas-aop)。 在你的 Spring 配置中,所有方面和顾问元素都必须放置在``元素中(在应用程序上下文配置中,可以有多个``元素)。``元素可以包含 pointcut、advisor 和方面元素(请注意,这些元素必须按顺序声明)。 | |配置的``样式大量使用了 Spring 的[auto-proxying](#aop-autoproxy)机制。如果你已经通过使用“BeannaMeAutoProxyCreator”或类似的方式使用了显式自动代理,那么这可能会导致问题(例如建议
不被编织)。推荐的使用模式是
只使用``样式,或者只使用`AutoProxyCreator`样式和
样式,永远不要混合它们。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.5.1.声明一个方面 当你使用模式支持时,一个方面是在 Spring 应用程序上下文中定义为 Bean 的常规 爪哇 对象。在对象的字段和方法中捕获状态和行为,在 XML 中捕获切入点和通知信息。 你可以通过使用``元素来声明一个方面,并通过使用`ref`属性来引用 backing Bean,如下例所示: ``` ... ... ``` 支持方面的 Bean(在这种情况下是 `Abean’)当然可以像任何其他 Spring Bean 一样被配置和注入依赖关系。 #### 5.5.2.声明切入点 你可以在``元素中声明一个命名的切入点,让切入点定义在多个方面和顾问之间共享。 表示服务层中任何业务服务的执行的切入点可以定义如下: ``` ``` 请注意,PointCut 表达式本身使用与[@AspectJ 支持](#aop-ataspectj)中描述的相同的 AspectJ PointCut 表达式语言。如果使用基于模式的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下: ``` ``` 假设你有`CommonPointcuts`中描述的[共享公共切入点定义](#aop-common-pointcuts)方面。 然后在一个方面中声明一个切入点与声明一个顶级切入点非常相似,如下例所示: ``` ... ``` 与 @AspectJ 方面几乎相同,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,下面的切入点收集`this`对象作为连接点上下文,并将其传递给通知: ``` ... ``` 必须声明通知,以通过包括匹配名称的参数来接收收集的连接点上下文,如下所示: 爪哇 ``` public void monitor(Object service) { // ... } ``` Kotlin ``` fun monitor(service: Any) { // ... } ``` 在组合 PointCut 子表达式时,`&&`在 XML 文档中很难处理,因此可以分别使用`and`、`or`和`not`关键字来代替`&&`、`||’和`!`。例如,前面的切入点可以更好地编写如下: ``` ... ``` 请注意,以这种方式定义的切入点由其 XML`id`引用,并且不能用作命名的切入点来形成复合切入点。因此,基于模式的定义样式中的命名切入点支持比 @AspectJ 样式提供的支持更有限。 #### 5.5.3.声明建议 基于模式的 AOP 支持使用与 @AspectJ 样式相同的五种建议,并且它们具有完全相同的语义。 ##### 建议之前 在匹配的方法执行之前运行建议之前。通过使用``元素,它在 `` 内声明,如下例所示: ``` ... ``` 这里,`dataAccessOperation`是在顶部定义的切入点的`id`级别。要定义 PointCut 内联,将`pointcut-ref`属性替换为`pointcut`属性,如下所示: ``` ... ``` 正如我们在讨论 @AspectJ 风格时所指出的,使用命名切入点可以显著提高代码的可读性。 `method`属性标识了提供建议主体的方法。该方法必须为包含该建议的方面元素所引用的 Bean 定义。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前, Bean 方面的`doAccessCheck`方法被调用。 ##### 在返回建议后 返回通知后,当匹配的方法执行正常完成时运行。它是在``中声明的,与通知之前的方式相同。下面的示例展示了如何声明它: ``` ... ``` 正如在 @AspectJ 样式中一样,你可以在建议主体中获得返回值。为此,使用`returning`属性指定返回值应传递到的参数的名称,如下例所示: ``` ... ``` `doAccessCheck`方法必须声明一个名为`retVal`的参数。该参数的类型以与`@AfterReturning`相同的方式限制匹配。例如,你可以如下声明方法签名: 爪哇 ``` public void doAccessCheck(Object retVal) {... ``` Kotlin ``` fun doAccessCheck(retVal: Any) {... ``` ##### 在提出建议之后 抛出建议后,当匹配的方法执行通过抛出异常退出时,将运行该建议。它是通过使用`after-throwing`元素在``内声明的,如下例所示: ``` ... ``` 正如在 @AspectJ 样式中一样,你可以在建议主体中获得抛出的异常。要做到这一点,请使用`throwing`属性指定应向其传递异常的参数的名称,如下例所示: ``` ... ``` `doRecoveryActions`方法必须声明一个名为`dataAccessEx`的参数。该参数的类型以与“@afterthrowing”相同的方式限制匹配。例如,方法签名可以声明如下: 爪哇 ``` public void doRecoveryActions(DataAccessException dataAccessEx) {... ``` Kotlin ``` fun doRecoveryActions(dataAccessEx: DataAccessException) {... ``` ##### 在(最终)建议之后 无论匹配的方法执行如何退出,在(最终)通知之后都会运行。你可以使用`after`元素来声明它,如下例所示: ``` ... ``` ##### 围绕建议 最后一种建议是*周围*建议。围绕建议运行“围绕”一个匹配的方法的执行。它有机会在方法运行之前和之后都进行工作,并确定何时、如何以及即使方法实际运行了。如果你需要以线程安全的方式共享方法执行之前和之后的状态,通常会使用“周围建议”——例如,启动和停止计时器。 | |始终使用功能最小的通知形式来满足你的要求。

例如,如果*周围*通知足以满足你的需求,则不要使用*周围*通知。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 你可以使用`aop:around`元素声明通知周围的内容。通知方法应该声明`Object`作为其返回类型,并且方法的第一个参数必须是类型`ProceedingJoinPoint`。在 advice 方法的主体中,你必须在`ProceedingJoinPoint`上调用 `proceed()’,才能运行底层方法。在没有参数的情况下调用`proceed()`将导致调用者的原始参数在调用时被提供给底层方法。对于高级用例,有一个重载的`proceed()`方法的变体,它接受一个参数数组。当调用底层方法时,数组中的值将被用作该方法的参数。关于使用`Object[]`调用 `proceed’的注释,请参见[Around Advice](#aop-ataspectj-around-advice)。 下面的示例展示了如何在 XML 中声明有关建议的内容: ``` ... ``` `doBasicProfiling`通知的实现可以与 @AspectJ 示例中的实现完全相同(当然要减去注释),如下例所示: 爪哇 ``` public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } ``` Kotlin ``` fun doBasicProfiling(pjp: ProceedingJoinPoint): Any { // start stopwatch val retVal = pjp.proceed() // stop stopwatch return pjp.proceed() } ``` ##### 建议参数 基于模式的声明风格支持完全类型的通知,其方式与 @AspectJ 支持中所描述的相同——通过将切入点参数按名称与通知方法参数进行匹配。详见[建议参数](#aop-ataspectj-advice-params)。如果你希望显式地指定建议方法的参数名称(而不是依赖于前面描述的检测策略),那么你可以通过使用建议元素的`arg-names`属性来这样做,其处理方式与通知注释中的`argNames`属性相同(如[确定参数名称](#aop-ataspectj-advice-params-names)中所述)。下面的示例展示了如何在 XML 中指定参数名称: ``` ``` `arg-names`属性接受以逗号分隔的参数名列表。 下面稍微更详细的基于 XSD 的方法的示例展示了一些与多个强类型参数一起使用的建议: 爪哇 ``` package x.y.service; public interface PersonService { Person getPerson(String personName, int age); } public class DefaultPersonService implements PersonService { public Person getPerson(String name, int age) { return new Person(name, age); } } ``` Kotlin ``` package x.y.service interface PersonService { fun getPerson(personName: String, age: Int): Person } class DefaultPersonService : PersonService { fun getPerson(name: String, age: Int): Person { return Person(name, age) } } ``` 接下来是方面。请注意,`profile(..)`方法接受许多强类型参数,其中第一个参数恰好是用于继续方法调用的连接点。该参数的存在表明 `profile’将用作`around`通知,如下例所示: 爪哇 ``` package x.y; import org.aspectj.lang.ProceedingJoinPoint; import org.springframework.util.StopWatch; public class SimpleProfiler { public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable { StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'"); try { clock.start(call.toShortString()); return call.proceed(); } finally { clock.stop(); System.out.println(clock.prettyPrint()); } } } ``` Kotlin ``` import org.aspectj.lang.ProceedingJoinPoint import org.springframework.util.StopWatch class SimpleProfiler { fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any { val clock = StopWatch("Profiling for '$name' and '$age'") try { clock.start(call.toShortString()) return call.proceed() } finally { clock.stop() println(clock.prettyPrint()) } } } ``` 最后,下面的示例 XML 配置会影响针对特定连接点的上述建议的执行: ``` ``` 考虑以下驱动程序脚本: 爪哇 ``` import org.springframework.beans.factory.BeanFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import x.y.service.PersonService; public final class Boot { public static void main(final String[] args) throws Exception { BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); PersonService person = (PersonService) ctx.getBean("personService"); person.getPerson("Pengo", 12); } } ``` Kotlin ``` fun main() { val ctx = ClassPathXmlApplicationContext("x/y/plain.xml") val person = ctx.getBean("personService") as PersonService person.getPerson("Pengo", 12) } ``` 有了这样的引导类,我们将获得类似于以下标准输出的输出: ``` StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 ----------------------------------------- ms % Task name ----------------------------------------- 00000 ? execution(getFoo) ``` ##### 建议订购 当多条通知需要在相同的连接点(执行方法)上运行时,排序规则如[Advice Ordering](#aop-ataspectj-advice-ordering)中所述。通过``元素中的`order`属性,或者通过向支持方面的 Bean 添加`@Order`注释,或者通过使 Bean 实现`Ordered`接口,确定方面之间的优先级。 | |与在相同的`@Aspect`类中定义的通知方法的优先规则相反,当在相同的``元素中定义的两条通知都需要
在相同的连接点上运行时,优先级由在附件``元素中声明通知
元素的顺序决定,从最高到最低
优先级。

例如,给定一个`around`通知和一个`before`通知,该通知在相同的 `元素中定义,该元素适用于相同的连接点,以确保`around`通知具有比`before`通知更高的优先级,``元素必须是在``元素之前声明的
元素。

作为一般的经验法则,如果你发现在同一个``元素中有多个定义
的建议适用于相同的连接点,考虑将
这样的建议方法在每个接入点的``元素
中折叠成一个建议方法,或者将建议片段重构为单独的``元素,你可以在方面级别订购
元素。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.5.4.介绍 介绍(在 AspectJ 中称为类型间声明)让一个方面声明被建议的对象实现一个给定的接口,并代表这些对象提供该接口的实现。 你可以通过在`aop:aspect`中使用`aop:declare-parents`元素来进行介绍。你可以使用`aop:declare-parents`元素来声明匹配的类型有一个新的父类型(因此命名)。例如,给定一个名为`UsageTracked`的接口和一个名为 `DefaultUsageTracked’的接口的实现,以下方面声明服务接口的所有实现者也实现`UsageTracked`接口。(例如,为了通过 JMX 公开统计信息。 ``` ``` 然后,支持`usageTracking` Bean 的类将包含以下方法: 爪哇 ``` public void recordUsage(UsageTracked usageTracked) { usageTracked.incrementUseCount(); } ``` Kotlin ``` fun recordUsage(usageTracked: UsageTracked) { usageTracked.incrementUseCount() } ``` 要实现的接口由`implement-interface`属性决定。`types-matching`属性的值是 AspectJ 类型模式。匹配类型的任何 Bean 实现`UsageTracked`接口。注意,在前面示例的 before 通知中,服务 bean 可以直接用作`UsageTracked`接口的实现。要以编程方式访问 Bean,你可以编写以下内容: 爪哇 ``` UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); ``` Kotlin ``` val usageTracked = context.getBean("myService") as UsageTracked ``` #### 5.5.5.方面实例化模型 对于模式定义的方面,唯一支持的实例化模型是单例模型。其他实例化模型可能会在未来的版本中得到支持。 #### 5.5.6.顾问 “顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像是一个独立的小方面,只有一条建议。通知本身由 Bean 表示,并且必须实现[Advice Types in Spring](#aop-api-advice-types)中描述的通知接口之一。顾问可以利用 AspectJ 切入点表达式。 Spring 支持带有``元素的 advisor 概念。你最常看到它与事务性建议一起使用,在 Spring 中,事务性建议也有自己的名称空间支持。下面的示例展示了一个顾问: ``` ``` 除了前面示例中使用的`pointcut-ref`属性外,你还可以使用 `pointcut’属性来内联地定义一个 pointcut 表达式。 要定义 advisor 的优先级,以便该建议可以参与排序,请使用`order`属性来定义 advisor 的`Ordered`值。 #### 5.5.7. AOP 模式示例 本节展示了在使用模式支持重写时,来自[An AOP Example](#aop-ataspectj-example)的并发锁定失败重试示例的外观。 业务服务的执行有时会由于并发性问题(例如,死锁失败者)而失败。如果该操作被重试,则很可能在下一次尝试中成功。对于在这种情况下(幂等运算不需要返回给用户以解决冲突)适合重试的业务服务,我们希望透明地重试该操作,以避免客户端看到“悲观锁定失败异常”。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过一个方面来实现。 因为我们想要重试操作,所以我们需要使用 around advice,这样我们就可以多次调用`proceed`。下面的清单展示了基本的方面实现(这是一个使用模式支持的常规 Java 类): Java ``` public class ConcurrentOperationExecutor implements Ordered { private static final int DEFAULT_MAX_RETRIES = 2; private int maxRetries = DEFAULT_MAX_RETRIES; private int order = 1; public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } public int getOrder() { return this.order; } public void setOrder(int order) { this.order = order; } public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { int numAttempts = 0; PessimisticLockingFailureException lockFailureException; do { numAttempts++; try { return pjp.proceed(); } catch(PessimisticLockingFailureException ex) { lockFailureException = ex; } } while(numAttempts <= this.maxRetries); throw lockFailureException; } } ``` Kotlin ``` class ConcurrentOperationExecutor : Ordered { private val DEFAULT_MAX_RETRIES = 2 private var maxRetries = DEFAULT_MAX_RETRIES private var order = 1 fun setMaxRetries(maxRetries: Int) { this.maxRetries = maxRetries } override fun getOrder(): Int { return this.order } fun setOrder(order: Int) { this.order = order } fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { var numAttempts = 0 var lockFailureException: PessimisticLockingFailureException do { numAttempts++ try { return pjp.proceed() } catch (ex: PessimisticLockingFailureException) { lockFailureException = ex } } while (numAttempts <= this.maxRetries) throw lockFailureException } } ``` 注意,方面实现了`Ordered`接口,这样我们就可以将方面的优先级设置为高于事务通知的优先级(每次重试时,我们都希望有一个新的事务)。`maxRetries`和`order`属性都是由 Spring 配置的。主要的操作发生在`doConcurrentOperation`around advice 方法中。我们试图继续。如果我们使用`PessimisticLockingFailureException`失败,我们将再次尝试,除非我们已经用尽了所有的重试尝试。 | |这个类与 @AspectJ 示例中使用的类相同,但删除了
注释。| |---|------------------------------------------------------------------------------------------------------| Spring 相应的配置如下: ``` ``` 请注意,目前我们假设所有业务服务都是幂等的。如果不是这样的话,我们可以通过引入`Idempotent`注释并使用该注释来注释服务操作的实现,从而细化方面,使其仅重试真正的幂等运算,如下例所示: Java ``` @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // marker annotation } ``` Kotlin ``` @Retention(AnnotationRetention.RUNTIME) annotation class Idempotent { // marker annotation } ``` 对只重试幂等运算的方面的更改涉及细化切入点表达式,使只有`@Idempotent`运算匹配,如下所示: ``` ``` ### 5.6.选择使用哪种 AOP 声明样式 一旦你确定一个方面是实现给定需求的最佳方法,你如何在使用 Spring AOP 或 AspectJ 以及使用方面语言(代码)样式、@AspectJ 注释样式或 Spring XML 样式之间做出决定?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对 AOP 的熟悉程度。 #### 5.6.1. Spring AOP 或完全 AspectJ? 用最简单的能起作用的东西。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/Weaver。如果只需要建议在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果你需要建议不是由 Spring 容器管理的对象(例如,通常是域对象),则需要使用 AspectJ。如果你希望建议连接点而不是简单的方法执行(例如,字段 get 或设置连接点等等),还需要使用 AspectJ。 在使用 AspectJ 时,你可以选择 AspectJ 语言语法(也称为“代码样式”)或 @AspectJ 注释样式。显然,如果你不使用 Java5+,那么已经为你做出了这样的选择:使用代码样式。如果方面在你的设计中起着很大的作用,并且你能够为 Eclipse 使用[AspectJ 开发工具](https://www.eclipse.org/ajdt/)插件,那么 AspectJ 语言语法是首选选项。它更简洁,更简单,因为该语言是专门为写作方面而设计的。如果你不使用 Eclipse,或者只有几个方面在你的应用程序中不起主要作用,那么你可能想要考虑使用 @AspectJ 样式,在 IDE 中坚持常规的 Java 编译,并在构建脚本中添加一个方面编织阶段。 #### 5.6.2. Spring AOP 的 @AspectJ 或 XML? 如果你选择使用 Spring AOP,那么你可以选择 @AspectJ 或 XML 样式。需要考虑的权衡因素有很多。 现有 Spring 用户可能最熟悉 XML 风格,并且它由真正的 POJO 支持。当使用 AOP 作为配置 Enterprise 服务的工具时,XML 可以是一个很好的选择(一个很好的测试是,你是否认为切入点表达式是你可能希望独立更改的配置的一部分)。使用 XML 风格,可以从配置中更清楚地看出系统中存在哪些方面。 XML 样式有两个缺点。首先,它没有完全封装它在一个地方处理的需求的实现。干原理认为,系统中的任何知识都应该有一个单一的、明确的、权威的表示。在使用 XML 样式时,关于需求是如何实现的知识在配置文件中的 Backing Bean 类和 XML 的声明中进行了分割。当你使用 @AspectJ 样式时,此信息被封装在一个模块中:Aspect。其次,与 @AspectJ 风格相比,XML 风格在表达的内容上略有限制:只支持“单例”方面实例化模型,并且不可能合并 XML 中声明的命名切入点。例如,在 @AspectJ 样式中,你可以编写如下内容: Java ``` @Pointcut("execution(* get*())") public void propertyAccess() {} @Pointcut("execution(org.xyz.Account+ *(..))") public void operationReturningAnAccount() {} @Pointcut("propertyAccess() && operationReturningAnAccount()") public void accountPropertyAccess() {} ``` Kotlin ``` @Pointcut("execution(* get*())") fun propertyAccess() {} @Pointcut("execution(org.xyz.Account+ *(..))") fun operationReturningAnAccount() {} @Pointcut("propertyAccess() && operationReturningAnAccount()") fun accountPropertyAccess() {} ``` 在 XML 样式中,你可以声明前两个切入点: ``` ``` XML 方法的缺点是,你无法通过合并这些定义来定义“AccountPropertyAccess”切入点。 @AspectJ 样式支持额外的实例化模型和更丰富的切入点组合。它具有保持方面为模块化单元的优点。它还具有这样的优点: Spring AOP 和 AspectJ 都可以理解(并因此使用)@AspectJ 方面。因此,如果你后来决定需要 AspectJ 的功能来实现额外的需求,则可以轻松地迁移到经典的 AspectJ 设置。总的来说, Spring 团队对于自定义方面更喜欢 @AspectJ 风格,而不是简单地配置 Enterprise 服务。 ### 5.7.混合方面类型 通过使用自动代理支持、模式定义的``方面、``声明的顾问,甚至在相同配置中使用其他样式的代理和拦截器,完全可以混合 @AspectJ 样式的方面。所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。 ### 5.8.代理机制 Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。JDK 动态代理是内置在 JDK 中的,而 CGlib 是一种常见的开源类定义库(重新打包为`spring-core`)。 如果要代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。由目标类型实现的所有接口都是代理的。如果目标对象不实现任何接口,则创建一个 CGLIB 代理。 如果你想强制使用 CGlib 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),你可以这样做。但是,你应该考虑以下问题: * 对于 CGlib,不能通知`final`方法,因为它们不能在运行时生成的子类中被重写。 * 从 Spring 4.0 开始,Proxied 对象的构造函数不再被调用两次,因为 CGlib 代理实例是通过 ObjeNesis 创建的。只有当你的 JVM 不允许绕过构造函数时,你才可能在 Spring 的 AOP 支持中看到双重调用和相应的调试日志条目。 要强制使用 CGLIB 代理,请将``元素的`proxy-target-class`属性的值设置为 true,如下所示: ``` ``` 要在使用 @AspectJ 自动代理支持时强制进行 CGLIB 代理,请将``元素的 `proxy-target-class’属性设置为`true`,如下所示: ``` ``` | |多个``节在运行时折叠成一个统一的自动代理创建器
,该创建器在运行时应用*最强*代理设置,该设置指定了任何<节(通常来自不同的 XML Bean 定义文件)。
这也适用于``和``元素。
要清楚,
元素,在``上使用`proxy-target-class="true"`,,或``元素强制使用 CGLIB
代理*对于他们三个人来说*。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.8.1.理解 AOP 代理 Spring AOP 是基于代理的。在编写自己的方面或使用 Spring 框架中提供的基于 Spring AOP 的方面之前,掌握最后一条语句的实际含义的语义是非常重要的。 首先考虑这样一个场景:你有一个简单的、未被代理的、没有什么特别之处的、直接的对象引用,如下面的代码片段所示: Java ``` public class SimplePojo implements Pojo { public void foo() { // this next method invocation is a direct call on the 'this' reference this.bar(); } public void bar() { // some logic... } } ``` Kotlin ``` class SimplePojo : Pojo { fun foo() { // this next method invocation is a direct call on the 'this' reference this.bar() } fun bar() { // some logic... } } ``` 如果你在对象引用上调用一个方法,那么该方法将直接在该对象引用上调用,如下面的图像和列表所示: ![aop proxy plain pojo call](images/aop-proxy-plain-pojo-call.png) Java ``` public class Main { public static void main(String[] args) { Pojo pojo = new SimplePojo(); // this is a direct method call on the 'pojo' reference pojo.foo(); } } ``` Kotlin ``` fun main() { val pojo = SimplePojo() // this is a direct method call on the 'pojo' reference pojo.foo() } ``` 当客户机代码的引用是代理时,情况会略有变化。考虑以下图表和代码片段: ![aop proxy call](images/aop-proxy-call.png) Java ``` public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); Pojo pojo = (Pojo) factory.getProxy(); // this is a method call on the proxy! pojo.foo(); } } ``` Kotlin ``` fun main() { val factory = ProxyFactory(SimplePojo()) factory.addInterface(Pojo::class.java) factory.addAdvice(RetryAdvice()) val pojo = factory.proxy as Pojo // this is a method call on the proxy! pojo.foo() } ``` 这里需要理解的关键是,`main(..)`类的`Main`方法中的客户端代码引用了代理。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(在本例中是`SimplePojo`引用),它可能对自身进行的任何方法调用,例如`this.bar()`或 `this.foo()’,都将针对`this`引用而不是代理调用。这具有重要的意义。这意味着,自我调用不会导致与方法调用相关的建议有机会运行。 好吧,那么我们该怎么做呢?最好的方法(这里不严格使用术语“best”)是重构代码,这样就不会发生自我调用。这确实需要你做一些工作,但这是最好的、侵入性最小的方法。下一种做法绝对是可怕的,我们不愿指出这一点,恰恰是因为它太可怕了。你可以(尽管这对我们来说是痛苦的)将你的类中的逻辑完全绑定到 Spring AOP,如下例所示: Java ``` public class SimplePojo implements Pojo { public void foo() { // this works, but... gah! ((Pojo) AopContext.currentProxy()).bar(); } public void bar() { // some logic... } } ``` Kotlin ``` class SimplePojo : Pojo { fun foo() { // this works, but... gah! (AopContext.currentProxy() as Pojo).bar() } fun bar() { // some logic... } } ``` 这将你的代码完全耦合到 Spring AOP,并且它使类本身意识到这样一个事实,即它是在 AOP 上下文中使用的,这与 AOP 完全相反。在创建代理时,它还需要一些额外的配置,如下例所示: Java ``` public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); factory.setExposeProxy(true); Pojo pojo = (Pojo) factory.getProxy(); // this is a method call on the proxy! pojo.foo(); } } ``` Kotlin ``` fun main() { val factory = ProxyFactory(SimplePojo()) factory.addInterface(Pojo::class.java) factory.addAdvice(RetryAdvice()) factory.isExposeProxy = true val pojo = factory.proxy as Pojo // this is a method call on the proxy! pojo.foo() } ``` 最后,必须指出的是,AspectJ 不存在这种自我调用问题,因为它不是基于代理的 AOP 框架。 ### 5.9.程序化地创建 @AspectJ 代理 除了在配置中使用``或``声明方面之外,还可以通过编程方式创建代理来为目标对象提供建议。有关 Spring 的 AOP API 的全部详细信息,请参见[next chapter](#aop-api)。在这里,我们希望重点关注通过使用 @AspectJ Aspects 自动创建代理的能力。 你可以使用`org.springframework.aop.aspectj.annotation.AspectJProxyFactory`类为一个或多个 @AspectJ 方面建议的目标对象创建代理。这个类的基本用法非常简单,如下例所示: Java ``` // create a factory that can generate a proxy for the given target object AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); // add an aspect, the class must be an @AspectJ aspect // you can call this as many times as you need with different aspects factory.addAspect(SecurityManager.class); // you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect factory.addAspect(usageTracker); // now get the proxy object... MyInterfaceType proxy = factory.getProxy(); ``` Kotlin ``` // create a factory that can generate a proxy for the given target object val factory = AspectJProxyFactory(targetObject) // add an aspect, the class must be an @AspectJ aspect // you can call this as many times as you need with different aspects factory.addAspect(SecurityManager::class.java) // you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect factory.addAspect(usageTracker) // now get the proxy object... val proxy = factory.getProxy() ``` 有关更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.html)。 ### 5.10.在 Spring 应用程序中使用 AspectJ 到目前为止,我们在本章中所讨论的一切都是纯粹的 Spring AOP。在本节中,我们将研究如何使用 AspectJ 编译器或 Weaver,而不是 Spring AOP,如果你的需求超出了 Spring AOP 单独提供的功能。 Spring 附带了一个小的 AspectJ 方面库,它可以在你的发行版中以`spring-aspects.jar`的形式独立提供。你需要将此添加到你的 Classpath 中,以便使用其中的方面。[Using AspectJ to Dependency Inject Domain Objects with Spring](#aop-atconfigurable)和[Other Spring aspects for AspectJ](#aop-ajlib-other)讨论这个库的内容以及如何使用它。[Configuring AspectJ Aspects by Using Spring IoC](#aop-aj-configure)讨论了如何使用 AspectJ 编译器注入 AspectJ 方面的依赖关系。最后,[Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw)为使用 AspectJ 的 Spring 应用程序提供了加载时编织的介绍。 #### 5.10.1.使用 AspectJ 到依赖注入域对象 Spring Spring 容器实例化和配置在应用程序上下文中定义的 bean。还可以要求 Bean 工厂配置预先存在的对象,给定 Bean 定义的名称,该定义包含要应用的配置。` Spring-方面。该支持旨在用于在任何容器的控制范围之外创建的对象。域对象通常属于这一类,因为它们通常是用“new”操作符以编程方式创建的,或者是数据库查询的结果,由 ORM 工具创建的。 `@Configurable`注释将一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,你可以将其纯粹用作标记注释,如下例所示: Java ``` package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable public class Account { // ... } ``` Kotlin ``` package com.xyz.myapp.domain import org.springframework.beans.factory.annotation.Configurable @Configurable class Account { // ... } ``` 当以这种方式用作标记接口时, Spring 通过使用与完全限定类型名称(`com.xyz.myapp.domain.account`)同名的 Bean 定义(通常是原型范围)来配置带注释类型(`account’,在这种情况下)的新实例。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略`id`属性,如下例所示: ``` ``` 如果要显式地指定要使用的原型 Bean 定义的名称,可以直接在注释中这样做,如下例所示: 爪哇 ``` package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable("account") public class Account { // ... } ``` Kotlin ``` package com.xyz.myapp.domain import org.springframework.beans.factory.annotation.Configurable @Configurable("account") class Account { // ... } ``` Spring 现在查找一个名为`account`的 Bean 定义,并使用该定义来配置新的`Account`实例。 你还可以使用自动布线来避免指定专门的 Bean 定义。要使 Spring 应用自动布线,请使用`autowire`注释的`@Configurable`属性。你可以指定`@Configurable(autowire=Autowire.BY_TYPE)`或 `@configurable(autowire=autowire.by_name)`,分别根据类型或名称进行自动布线。作为一种选择,最好是在字段或方法级别上通过`@Autowired`或`@Inject`为你的`@Configurable`bean 指定显式的、注释驱动的依赖注入(有关更多详细信息,请参见[基于注释的容器配置](#beans-annotation-config))。 最后,你可以通过使用`dependencyCheck`属性(例如,`@configurable(autowire=autowire.by_name,dependencycheck=true)’),在新创建和配置的对象中启用 Spring 依赖项检查对象引用。如果将此属性设置为`true`,则 Spring 在配置后验证已设置所有属性(不是原语或集合)。 请注意,使用注释本身并不会产生任何效果。是`spring-aspects.jar`中的 `AnnotationBeanConfigureRespect’作用于注释的存在。实质上,该方面表示,“在从使用`@Configurable`注释的类型的新对象的初始化返回后,根据注释的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”指的是新实例化的对象(例如,用`new`操作符实例化的对象)以及正在进行反序列化(例如,通过[readResolve()](https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html))的`Serializable`对象。 | |上面一段中的关键短语之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的
精确语义是
。在这种上下文中,“在初始化之后”意味着依赖项是在对象构造完成之后注入的
。这意味着依赖项
在类的构造函数主体中不可用。如果希望在构造函数主体运行之前注入
依赖项,从而使
在构造函数主体中可用,则需要在 `@configurable’声明中定义该依赖项,如下:

爪哇

``
@configurable(preconstruction=“true)<<5018"/>`


``gt r=“5023”true(preconstruction=“5024”/>”><25"r=ttr>“>你可以找到更多的信息 ``` 在配置方面之前创建的`@Configurable`对象的实例将导致向调试日志发出消息,并且不会发生对象的配置。一个示例可能是 Spring 配置中的 Bean,该配置在 Spring 初始化域对象时创建域对象。在这种情况下,可以使用 `Depends-on` Bean 属性来手动指定 Bean 取决于配置方面。下面的示例展示了如何使用`depends-on`属性: ``` ``` | |不要通过 Bean 配置器方面激活`@Configurable`处理,除非你
确实意味着在运行时依赖其语义。特别是,要确保在 Bean 类上使用
而不是在容器中注册为常规 Spring bean
的类上使用`@Configurable`。这样做会导致双重初始化,一次通过
容器,一次通过方面。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 单元测试`@Configurable`对象 `@Configurable`支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果`@Configurable`类型没有被 AspectJ 编织,则注释在单元测试期间没有影响。你可以在测试对象中设置 mock 或 stub 属性引用,并按常规进行操作。如果`@Configurable`类型已由 AspectJ 编织,则仍然可以正常地在容器之外进行单元测试,但是每次构造`@Configurable`对象时,都会看到一条警告消息,表明该对象尚未由 Spring 进行配置。 ##### 处理多个应用程序上下文 用于实现`AnnotationBeanConfigurerAspect`支持的`@Configurable`是 AspectJ 单例方面。单例方面的作用域与`static`成员的作用域相同:每个类装入器只有一个方面实例定义类型。这意味着,如果在相同的类装入器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义`@EnableSpringConfigured` Bean,在哪里将`spring-aspects.jar`放置在 Classpath 上。 考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了公共业务服务、支持这些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含专门针对该 Servlet 的定义)。所有这些上下文都在相同的类装入器层次结构中共存,因此`AnnotationBeanConfigurerAspect`只能保存对其中之一的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义`@EnableSpringConfigured` Bean。这定义了你可能希望注入到域对象中的服务。结果是,你无法通过使用 @configurable 机制(这可能不是你想要做的事情)来配置具有对子( Servlet 特定)上下文中定义的 bean 的引用的域对象。 当在同一个容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序通过使用自己的类装入器加载`spring-aspects.jar`中的类型(例如,在`spring-aspects.jar`中放置`WEB-INF/lib`)。如果`spring-aspects.jar`仅添加到容器范围的 Classpath(因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的方面实例(这可能不是你想要的)。 #### 5.10.2.AspectJ 的其他 Spring 方面 除了`@Configurable`方面,`spring-aspects.jar`还包含一个 AspectJ 方面,你可以使用该方面来驱动 Spring 的事务管理,用于使用`@Transactional`注释的类型和方法。这主要用于希望在 Spring 容器之外使用 Spring 框架的事务支持的用户。 解释`@Transactional`注释的方面是 `AnnotationTransactionAspect’。当你使用这个方面时,你必须注释实现类(或该类中的方法或两者中的方法),而不是类实现的接口(如果有的话)。AspectJ 遵循 爪哇 的规则,即接口上的注释不会被继承。 类上的`@Transactional`注释指定了类中任何公共操作的执行的默认事务语义。 类中方法上的`@Transactional`注释重写了类注释给出的默认事务语义(如果存在的话)。任何可见性的方法都可以被注释,包括私有方法。直接对非公共方法进行注释是获得用于执行此类方法的事务划分的唯一方法。 | |Spring Framework4.2 以来,`spring-aspects`提供了一个类似的方面,该方面为标准
注释提供了完全相同的功能。查看“jtaAnnotationTransactionAspect”以获取更多详细信息。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 对于希望使用 Spring 配置和事务管理支持但不想(或不能)使用注释的 AspectJ 程序员,`spring-aspects.jar`还包含`abstract`方面,你可以扩展以提供自己的切入点定义。有关更多信息,请参见`AbstractBeanConfigurerAspect`和 `AbstractTransactionAspect’方面的来源。作为示例,下面的摘录展示了如何编写一个方面,通过使用匹配完全限定类名称的原型 Bean 定义来配置在域模型中定义的对象的所有实例: ``` public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect { public DomainObjectConfiguration() { setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver()); } // the creation of a new bean (any object in the domain model) protected pointcut beanCreation(Object beanInstance) : initialization(new(..)) && CommonPointcuts.inDomainModel() && this(beanInstance); } ``` #### 5.10.3.使用 Spring IOC 配置 AspectJ 方面 当你在 Spring 应用程序中使用 AspectJ 方面时,很自然地,既希望也希望能够使用 Spring 配置这样的方面。AspectJ 运行时本身负责方面的创建,通过 Spring 配置 AspectJ 创建的方面的方法取决于方面使用的 AspectJ 实例化模型(`per-xxx`子句)。 大多数 AspectJ 方面都是单例方面。这些方面的配置很容易。你可以创建一个 Bean 定义,将方面类型作为常规引用,并包括`factory-method="aspectOf"` Bean 属性。这确保了 Spring 通过向 AspectJ 查询来获得 Aspect 实例,而不是试图创建实例本身。下面的示例展示了如何使用`factory-method="aspectOf"`属性: ``` (1) ``` |**1**|注意`factory-method="aspectOf"`属性| |-----|----------------------------------------------| 非单例方面更难配置。然而,可以通过创建原型 Bean 定义并使用来自 ` Spring-方面. jar ` 的`@Configurable`支持来实现这一点,以配置 AspectJ 运行时创建的 Bean 方面实例。 如果你有一些要与 AspectJ 一起使用的 @AspectJ 方面(例如,对域模型类型使用加载时编织)和其他要与 Spring AOP 一起使用的 @AspectJ 方面,并且这些方面都是在 Spring 中配置的,你需要告诉 Spring AOP @AspectJ 自动代理支持程序,配置中定义的 @AspectJ 方面的哪些确切子集应该用于自动代理。你可以通过在``声明中使用一个或多个``元素来实现此目的。 Spring AOP 自动代理配置中,每个``元素指定一个名称模式,并且只有名称与至少一个模式匹配的 bean 才被使用。下面的示例展示了如何使用``元素: ``` ``` | |不要被``元素的名称所误导。使用它
将导致创建 Spring AOP 代理。此处使用了 Aspect
声明的 @AspectJ 样式,但不涉及 AspectJ 运行时。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 5.10.4. Spring 框架中的 AspectJ 加载时编织 加载时编织(LOAD-TIME WEAWING,LTW)是指将 AspectJ 方面编织到应用程序的类文件中的过程,这些类文件正被加载到 爪哇 虚拟机中。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 和仅使用 AspectJ 配置 LTW 的详细信息(完全不涉及 Spring),请参见[AspectJ 开发环境指南的 LTW 部分](https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html)。 Spring 框架给 AspectJ LTW 带来的价值在于,能够对编织过程进行更细粒度的控制。“Vanilla”AspectJ LTW 是通过使用 爪哇(5+)代理来实现的,在启动 JVM 时,通过指定 VM 参数来打开该代理。因此,它是一个 JVM 范围内的设置,在某些情况下可能很好,但通常有点太粗糙了。 Spring-启用的 LTW 允许你在每个“类加载器”的基础上切换 LTW,这是更细粒度的,并且在“单 JVM 多应用程序”环境(例如在典型的应用程序服务器环境中发现的)中更有意义。 此外,[在某些环境中](#aop-aj-ltw-environments),这种支持使加载时编织成为可能,而无需对应用服务器的启动脚本进行任何修改,而添加`-javaagent:path/to/aspectjweaver.jar`或(正如我们在本节后面描述的)`-javaagent:path/to/spring-instrument.jar`所需的修改。开发人员将应用程序上下文配置为支持加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。 既然销售介绍已经结束,让我们先来看一个使用 Spring 的 AspectJ LTW 的快速示例,然后是关于示例中引入的元素的详细细节。有关完整的示例,请参见[PetClinic 样本应用程序](https://github.com/spring-projects/spring-petclinic)。 ##### 第一个例子 假设你是一个应用程序开发人员,负责诊断系统中某些性能问题的原因。我们将切换到一个简单的分析方面,让我们快速获得一些性能指标,而不是推出一个分析工具。然后,我们可以立即将更细粒度的分析工具应用到该特定区域。 | |这里展示的示例使用 XML 配置。你还可以配置
并使用 @AspectJ 和[爪哇 配置](#beans-java)。具体来说,你可以使用“@enableLoadTimeWeaving”注释作为``的替代方法(有关详细信息,请参见[below](#aop-aj-ltw-spring))。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 下面的示例展示了分析方面,这并不新奇。这是一个基于时间的探查器,它使用了方面声明的 @AspectJ 风格: 爪哇 ``` package foo; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Pointcut; import org.springframework.util.StopWatch; import org.springframework.core.annotation.Order; @Aspect public class ProfilingAspect { @Around("methodsToBeProfiled()") public Object profile(ProceedingJoinPoint pjp) throws Throwable { StopWatch sw = new StopWatch(getClass().getSimpleName()); try { sw.start(pjp.getSignature().getName()); return pjp.proceed(); } finally { sw.stop(); System.out.println(sw.prettyPrint()); } } @Pointcut("execution(public * foo..*.*(..))") public void methodsToBeProfiled(){} } ``` Kotlin ``` package foo import org.aspectj.lang.ProceedingJoinPoint import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Around import org.aspectj.lang.annotation.Pointcut import org.springframework.util.StopWatch import org.springframework.core.annotation.Order @Aspect class ProfilingAspect { @Around("methodsToBeProfiled()") fun profile(pjp: ProceedingJoinPoint): Any { val sw = StopWatch(javaClass.simpleName) try { sw.start(pjp.getSignature().getName()) return pjp.proceed() } finally { sw.stop() println(sw.prettyPrint()) } } @Pointcut("execution(public * foo..*.*(..))") fun methodsToBeProfiled() { } } ``` 我们还需要创建一个`META-INF/aop.xml`文件,以通知 AspectJ Weaver 我们要将`ProfilingAspect`编织到类中。这种文件约定,即在 爪哇 Classpath 上存在一个名为`META-INF/aop.xml`的文件(或多个文件)是标准的 AspectJ。下面的示例显示了`aop.xml`文件: ``` ``` 现在我们可以转到配置的 Spring 特定部分。我们需要配置`LoadTimeWeaver`(稍后会进行说明)。这个加载时 Weaver 是负责将一个或多个`META-INF/aop.xml`文件中的方面配置编织到应用程序中的类中的基本组件。好的方面是,它不需要大量的配置(你可以指定更多的选项,但这些选项将在后面详细说明),如下例所示: ``` ``` 现在,所有必需的工件(方面、`META-INF/aop.xml`文件和 Spring 配置)都已就绪,我们可以使用`main(..)`方法创建以下驱动程序类,以演示 LTW 的实际操作: 爪哇 ``` package foo; import org.springframework.context.support.ClassPathXmlApplicationContext; public final class Main { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class); EntitlementCalculationService entitlementCalculationService = (EntitlementCalculationService) ctx.getBean("entitlementCalculationService"); // the profiling aspect is 'woven' around this method execution entitlementCalculationService.calculateEntitlement(); } } ``` Kotlin ``` package foo import org.springframework.context.support.ClassPathXmlApplicationContext fun main() { val ctx = ClassPathXmlApplicationContext("beans.xml") val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService // the profiling aspect is 'woven' around this method execution entitlementCalculationService.calculateEntitlement() } ``` 我们还有最后一件事要做。这一部分的介绍中确实提到,人们可以根据 Spring 的“类加载器”有选择地切换 LTW,这是事实。然而,对于这个示例,我们使用一个 爪哇 代理( Spring 提供的)来切换 LTW。我们使用以下命令运行前面显示的`Main`类: ``` java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main ``` `-javaagent`是用于指定和启用[测试在 JVM 上运行的程序的代理](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html)的标志。 Spring 框架附带了这样的代理,`InstrumentationSavingAgent`,该代理被包装在 ` Spring-文书。 jar ` 中,该代理是作为前面示例中的`-javaagent`参数的值提供的。 执行`Main`程序的输出与下一个示例类似。(我已经在`Thread.sleep(..)`实现中引入了`calculateEntitlement()`语句,这样探查器实际上捕获了 0 毫秒以外的内容(`01234`毫秒不是 AOP 引入的开销)。下面的清单显示了我们运行探查器时得到的输出: ``` Calculating entitlement StopWatch 'ProfilingAspect': running time (millis) = 1234 ------ ----- ---------------------------- ms % Task name ------ ----- ---------------------------- 01234 100% calculateEntitlement ``` 由于这个 LTW 是通过使用完全成熟的 AspectJ 来实现的,因此我们不仅限于为 Spring bean 提供建议。以下对`Main`程序的微小更改产生了相同的结果: 爪哇 ``` package foo; import org.springframework.context.support.ClassPathXmlApplicationContext; public final class Main { public static void main(String[] args) { new ClassPathXmlApplicationContext("beans.xml", Main.class); EntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService(); // the profiling aspect will be 'woven' around this method execution entitlementCalculationService.calculateEntitlement(); } } ``` Kotlin ``` package foo import org.springframework.context.support.ClassPathXmlApplicationContext fun main(args: Array) { ClassPathXmlApplicationContext("beans.xml") val entitlementCalculationService = StubEntitlementCalculationService() // the profiling aspect will be 'woven' around this method execution entitlementCalculationService.calculateEntitlement() } ``` 请注意,在前面的程序中,我们如何引导 Spring 容器,然后完全在 Spring 的上下文之外创建`StubEntitlementCalculationService`的新实例。剖析建议仍被广泛接受。 诚然,这个例子过于简单化了。然而, Spring 中 LTW 支持的基础已经在前面的示例中介绍了,本节的其余部分详细解释了每一位配置和使用背后的“为什么”。 | |本例中使用的`ProfilingAspect`可能是基本的,但它非常有用。这是开发时方面的一个很好的示例,开发人员可以在开发过程中使用
,然后很容易地将
从正在部署的应用程序的构建中排除到 UAT 或生产中。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### Aspects 你在 LTW 中使用的方面必须是 AspectJ 方面。你可以用 AspectJ 语言本身编写它们,也可以用 @AspectJ-style 编写方面。那么你的方面就是有效的 AspectJ 和 Spring AOP 方面。此外,编译的方面类需要在 Classpath 上可用。 ##### META-INF/ AOP.xml 通过使用 爪哇 Classpath 上的一个或多个`META-INF/aop.xml`文件(直接或更典型地在 jar 文件中)来配置 AspectJ LTW 基础设施。 该文件的结构和内容在[AspectJ 参考文档](https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html)的 LTW 部分中有详细说明。因为`aop.xml`文件是 100%AspectJ,所以我们在这里不再进一步描述它。 ##### 所需的库(JAR) 至少,你需要以下库来使用 Spring 框架对 AspectJ LTW 的支持: * `spring-aop.jar` * `aspectjweaver.jar` 如果使用[Spring-provided agent to enable instrumentation](#aop-aj-ltw-environments-generic),还需要: * `spring-instrument.jar` ##### Spring 配置 Spring LTW 支持中的关键组件是`LoadTimeWeaver`接口(在 `org.springframework.instrument.classloading` 包中),以及 Spring 发行版附带的众多 IT 实现。`LoadTimeWeaver`负责在运行时将一个或多个`java.lang.instrument.ClassFileTransformers`添加到`ClassLoader`中,这为各种有趣的应用程序打开了大门,其中之一恰好是方面的 LTW。 | |如果你不熟悉运行时类文件转换的想法,请在继续之前查看`java.lang.instrument`包的
爪哇doc API 文档。
尽管该文档并不全面,但至少你可以看到关键接口
和类(供你阅读本节时参考)。| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 为特定的`ApplicationContext`配置`LoadTimeWeaver`就像添加一行一样简单。(注意,你几乎肯定需要使用“ApplicationContext”作为 Spring 容器——通常,`BeanFactory`是不够的,因为 LTW 支持使用`BeanFactoryPostProcessors`。 要启用 Spring 框架的 LTW 支持,你需要配置`LoadTimeWeaver`,这通常是通过使用`@EnableLoadTimeWeaving`注释来完成的,如下所示: 爪哇 ``` @Configuration @EnableLoadTimeWeaving public class AppConfig { } ``` Kotlin ``` @Configuration @EnableLoadTimeWeaving class AppConfig { } ``` 或者,如果你更喜欢基于 XML 的配置,可以使用 `` 元素。请注意,该元素是在“上下文”名称空间中定义的。下面的示例展示了如何使用``: ``` ``` 前面的配置为你自动定义并注册了许多 LTW 特定的基础设施 bean,例如`LoadTimeWeaver`和`AspectJWeavingEnabler`。默认的`LoadTimeWeaver`是`DefaultContextLoadTimeWeaver`类,它试图修饰自动检测到的`LoadTimeWeaver`。“自动检测”的`LoadTimeWeaver`的确切类型取决于你的运行时环境。下表总结了各种`LoadTimeWeaver`实现: |运行时环境|`LoadTimeWeaver` implementation| |-----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| |运行在[Apache Tomcat](https://tomcat.apache.org/)中| `TomcatLoadTimeWeaver` | |在[GlassFish](https://eclipse-ee4j.github.io/glassfish/)中运行(仅限于 EAR 部署)| `GlassFishLoadTimeWeaver` | |在 Red Hat 的[JBoss AS](https://www.jboss.org/jbossas/)或[WildFly](https://www.wildfly.org/)中运行| `JBossLoadTimeWeaver` | |运行在 IBM 的[WebSphere](https://www-01.ibm.com/software/webservers/appserv/was/)中| `WebSphereLoadTimeWeaver` | |运行在 Oracle 的[WebLogic](https://www.oracle.com/technetwork/middleware/weblogic/overview/index-085209.html)中| `WebLogicLoadTimeWeaver` | |JVM 以 Spring `InstrumentationSavingAgent`(`java-javaagent:path/to/ Spring-instrument. jar `)开始|`InstrumentationLoadTimeWeaver`| |fallback,期望底层类装入器遵循常见的约定
(即`addTransformer`和可选的`getThrowawayClassLoader`方法)| `ReflectiveLoadTimeWeaver` | 请注意,该表仅列出了使用`DefaultContextLoadTimeWeaver`时自动检测到的`LoadTimeWeavers`。你可以精确地指定要使用的`LoadTimeWeaver`实现。 要使用 爪哇 配置指定特定的`LoadTimeWeaver`,请实现 `loadTimeWeavingConfigurer’接口并覆盖`getLoadTimeWeaver()`方法。下面的示例指定了`ReflectiveLoadTimeWeaver`: 爪哇 ``` @Configuration @EnableLoadTimeWeaving public class AppConfig implements LoadTimeWeavingConfigurer { @Override public LoadTimeWeaver getLoadTimeWeaver() { return new ReflectiveLoadTimeWeaver(); } } ``` Kotlin ``` @Configuration @EnableLoadTimeWeaving class AppConfig : LoadTimeWeavingConfigurer { override fun getLoadTimeWeaver(): LoadTimeWeaver { return ReflectiveLoadTimeWeaver() } } ``` 如果使用基于 XML 的配置,则可以将完全限定的类名指定为`weaver-class`元素上的``属性的值。下面的示例再次指定了`ReflectiveLoadTimeWeaver`: ``` ``` 由配置定义和注册的`LoadTimeWeaver`可以在以后通过使用众所周知的名称`loadTimeWeaver`从 Spring 容器中检索。请记住,`LoadTimeWeaver`仅作为 Spring 的 LTW 基础结构的一种机制存在,用于添加一个或多个`ClassFileTransformers`。执行 LTW 的实际 `classfileTransformer’是`ClassPreProcessorAgentAdapter`(来自`org.aspectj.weaver.loadtime`包)类。有关更多详细信息,请参见“classpreprocessoragentapter”类的类级 爪哇doc,因为实际实现编织的细节超出了本文的范围。 配置的最后一个属性还有待讨论:`aspectjWeaving`属性(如果使用 XML,则为`aspectj-weaving`属性)。此属性控制是否启用 LTW。它接受三个可能的值中的一个,如果属性不存在,默认值为“autodetect”。下表总结了三个可能的值: |Annotation Value| XML Value |解释| |----------------|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| | `ENABLED` | `on` |AspectJ 编织是在,和方面是编织在加载时,视情况而定。| | `DISABLED` | `off` |LTW 关闭。任何方面都不是在加载时编织的。| | `AUTODETECT` |`autodetect`|如果 Spring LTW 基础设施能够找到至少一个`META-INF/aop.xml`文件,
则 AspectJ Weaving 处于打开状态。否则,它就关闭了。这是默认值。| ##### 特定于环境的配置 最后一节包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的任何附加设置和配置。 ###### Tomcat,JBoss,WebSphere,WebLogic Tomcat、JBoss/Wildfly、IBMWebSphere Application Server 和 Oracle WebLogic Server 都提供了一个通用的应用`ClassLoader`,其能够进行本地检测。 Spring 的本机 LTW 可以利用那些类装入器实现来提供 AspectJ 编织。你可以简单地启用加载时编织,如[前面描述的](#aop-using-aspectj)。具体地说,你不需要修改 JVM 启动脚本以添加 `-javaAgent:path/to/ Spring-instrument. jar `。 请注意,在 JBoss 上,你可能需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一种快速的解决方法是将名为`WEB-INF/jboss-scanning.xml`的文件添加到工件中,该文件具有以下内容: ``` ``` ###### 通用 爪哇 应用程序 当在特定`LoadTimeWeaver`实现不支持的环境中需要类插装时,JVM 代理是通用的解决方案。对于这样的情况, Spring 提供了`InstrumentationLoadTimeWeaver`,它需要一个 Spring 特定的(但非常通用的)JVM 代理,`spring-instrument.jar`,通过常见的`@EnableLoadTimeWeaving`和``设置自动检测。 要使用它,你必须使用 Spring 代理通过提供以下 JVM 选项来启动虚拟机: ``` -javaagent:/path/to/spring-instrument.jar ``` 请注意,这需要修改 JVM 启动脚本,这可能会阻止你在应用程序服务器环境中使用该脚本(取决于你的服务器和操作策略)。也就是说,对于每个 JVM 部署一个应用程序,例如独立的启动应用程序,通常在任何情况下都可以控制整个 JVM 设置。 ### 5.11.更多资源 有关 AspectJ 的更多信息,请访问[AspectJ website](https://www.eclipse.org/aspectj)。 *Eclipse AspectJ*由 Adrian Colyer 等人(Addison-Wesley,2005)为 AspectJ 语言提供了全面的介绍和参考。 *AspectJ 在行动*,第二版由 Ramnivas Laddad(曼宁,2009 年)强烈推荐。这本书的重点是 AspectJ,但(在一定程度上)探讨了许多一般性的 AOP 主题。 ## 6. Spring AOP API 上一章用 @AspectJ 和基于模式的方面定义描述了 Spring 对 AOP 的支持。在这一章中,我们讨论了较低级别的 API Spring AOP。对于常见的应用程序,我们建议使用 Spring AOP 和 AspectJ 切入点,如前一章所述。 ### 6.1. Spring 中的切入点 API 本节描述 Spring 如何处理关键的切入点概念。 #### 6.1.1.概念 Spring 的切入点模型能够独立于建议类型实现切入点重用。你可以用相同的切入点针对不同的建议。 `org.springframework.aop.Pointcut`接口是中心接口,用于针对特定类和方法的建议。完整的界面如下: ``` public interface Pointcut { ClassFilter getClassFilter(); MethodMatcher getMethodMatcher(); } ``` 将`Pointcut`接口拆分成两部分,可以重用类和方法匹配部分以及细粒度的组合操作(例如与另一个方法匹配程序执行“合并”)。 `ClassFilter`接口用于将切入点限制为给定的一组目标类。如果`matches()`方法总是返回 true,那么所有目标类都是匹配的。下面的清单显示了`ClassFilter`接口定义: ``` public interface ClassFilter { boolean matches(Class clazz); } ``` `MethodMatcher`接口通常更重要。完整的界面如下: ``` public interface MethodMatcher { boolean matches(Method m, Class targetClass); boolean isRuntime(); boolean matches(Method m, Class targetClass, Object... args); } ``` `matches(Method, Class)`方法用于测试此切入点是否与目标类上的给定方法匹配。当创建 AOP 代理以避免需要对每个方法调用进行测试时,可以执行此评估。如果两个参数`matches`方法返回给定方法的`true`,而 MethodMatcher 的`isRuntime()`方法返回`true`,则在每个方法调用时都会调用三个参数匹配方法。这使得切入点可以查看在目标通知开始之前立即传递给方法调用的参数。 大多数`MethodMatcher`实现都是静态的,这意味着它们的`isRuntime()`方法返回`false`。在这种情况下,三参数`matches`方法永远不会被调用。 | |如果可能的话,尝试使切入点是静态的,允许 AOP 框架在创建 AOP 代理时缓存切入点求值的
结果。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.1.2.切入点上的操作 Spring 支持在切入点上的操作(特别是,联合和交叉)。 UNION 是指任何一个切入点都匹配的方法。交集表示两个切入点匹配的方法。联合通常更有用。你可以使用 `org.springframework. AOP.support.pointcuts’类中的静态方法或使用相同包中的 `composablepointcut’类来编写切入点。然而,使用 AspectJ 切入点表达式通常是一种更简单的方法。 #### 6.1.3.AspectJ 表达式切入点 自 2.0 以来, Spring 使用的最重要的切入点类型是 `org.SpringFramework. AOP.AspectJ.AspectJExpressionPointCut`。这是一个切入点,它使用 AspectJ 提供的库来解析 AspectJ PointCut 表达式字符串。 有关受支持的 AspectJ 切入点原语的讨论,请参见[上一章](#aop)。 #### 6.1.4.方便的切入点实现 Spring 提供了几种方便的切入点实现方式。你可以直接使用其中的一些;其他的则打算在特定于应用程序的切入点中进行子类。 ##### 静态切入点 静态切入点基于方法和目标类,不能考虑方法的参数。静态切入点对于大多数用途来说已经足够了,也是最好的。 Spring 可以仅在第一次调用方法时计算一次静态切入点。在那之后,就不需要在每次方法调用时再次计算切入点了。 本节的其余部分描述了 Spring 中包含的一些静态切入点实现。 ###### 正则表达式切入点 指定静态切入点的一个明显方法是正则表达式。 Spring 之外的几个 AOP 框架使这成为可能。org.springframework. AOP.support.jdkregexpMethodPointCut 是一种通用的正则表达式切入点,它使用了 JDK 中对正则表达式的支持。 使用`JdkRegexpMethodPointcut`类,你可以提供模式字符串的列表。如果其中任何一个是匹配的,则切入点计算为`true`。(因此,得到的切入点实际上是指定模式的合并。 下面的示例展示了如何使用`JdkRegexpMethodPointcut`: ``` .*set.* .*absquatulate ``` Spring 提供了一个名为`RegexpMethodPointcutAdvisor`的方便类,它还允许我们引用一个`Advice`(请记住,一个`Advice`可以是拦截器,在通知之前,抛出通知,等等)。在幕后, Spring 使用了`JdkRegexpMethodPointcut`。使用`RegexpMethodPointcutAdvisor`简化了连接,因为 Bean 封装了切入点和建议,如下例所示: ``` .*set.* .*absquatulate ``` 你可以使用`RegexpMethodPointcutAdvisor`与任何`Advice`类型。 ###### 属性驱动的切入点 静态切入点的一种重要类型是元数据驱动的切入点。这使用了元数据属性的值(通常是源级元数据)。 ##### 动态切入点 动态切入点比静态切入点的评估成本更高。它们考虑了方法参数和静态信息。这意味着每次方法调用都必须对它们进行求值,并且不能缓存结果,因为参数会发生变化。 主要的例子是`control flow`切入点。 ###### 控制流切入点 Spring 控制流切点在概念上类似于 AspectJ切点,尽管不那么强大。(目前无法指定切入点运行在与另一个切入点匹配的连接点之下。)控制流切入点匹配当前调用堆栈。例如,如果连接点是由`com.mycompany.web`包中的方法调用的,或者是由`SomeCaller`类调用的,那么它可能会触发。控制流的切入点是通过使用`org.springframework.aop.support.ControlFlowPointcut`类来指定的。 | |与
其他动态切入点相比,在运行时计算控制流切入点的成本要高得多。在 爪哇1.4 中,开销大约是其他动态
切入点的 5 倍。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.1.5.切入点超类 Spring 提供了有用的切入点超类,以帮助你实现自己的切入点。 因为静态切入点是最有用的,所以你可能应该子类“staticmethodmatcherpointcut”。这只需要实现一个抽象方法(尽管你可以重写其他方法来定制行为)。下面的示例展示了如何子类`StaticMethodMatcherPointcut`: 爪哇 ``` class TestStaticPointcut extends StaticMethodMatcherPointcut { public boolean matches(Method m, Class targetClass) { // return true if custom criteria match } } ``` Kotlin ``` class TestStaticPointcut : StaticMethodMatcherPointcut() { override fun matches(method: Method, targetClass: Class<*>): Boolean { // return true if custom criteria match } } ``` 还有用于动态切入点的超类。你可以对任何通知类型使用自定义切入点。 #### 6.1.6.自定义切入点 因为 Spring AOP 中的切入点是 爪哇 类,而不是语言特性(如 AspectJ),所以你可以声明自定义的切入点,无论是静态的还是动态的。 Spring 中的自定义切入点可以是任意复杂的。但是,如果可以的话,我们建议你使用 AspectJ PointCut 表达式语言。 | |Spring 的后续版本可能会提供对 JAC 提供的“语义切入点”的支持——例如,“所有改变目标对象中实例变量的方法”。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 6.2. Spring 中的咨询 API 现在我们可以研究 Spring AOP 如何处理建议。 #### 6.2.1.建议生命周期 每个建议都是 A Spring Bean。一个建议实例可以在所有被建议的对象之间共享,或者对于每个被建议的对象是唯一的。这对应于每个类或每个实例的建议。 每堂课的建议是最常用的。它适用于一般的建议,例如事务顾问。这些不依赖于代理对象的状态或添加新的状态。他们只是根据方法和论据行事。 每个实例的建议适合于介绍,以支持 mixin。在这种情况下,建议将状态添加到代理对象。 你可以在同一个 AOP 代理中混合使用共享和每个实例的建议。 #### 6.2.2. Spring 中的建议类型 Spring 提供了几种通知类型,并且是可扩展的,以支持任意的通知类型。本节介绍基本概念和标准建议类型。 ##### 围绕建议的拦截 Spring 中最基本的建议类型是围绕建议的拦截。 Spring 与 AOP `Alliance`接口兼容,用于围绕使用方法拦截的建议。实现`MethodInterceptor`并围绕建议实现的类也应该实现以下接口: ``` public interface MethodInterceptor extends Interceptor { Object invoke(MethodInvocation invocation) throws Throwable; } ``` `MethodInvocation`方法的`invoke()`参数公开了被调用的方法、目标连接点、 AOP 代理以及方法的参数。“invoke()”方法应该返回调用的结果:连接点的返回值。 下面的示例展示了一个简单的`MethodInterceptor`实现: 爪哇 ``` public class DebugInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println("Before: invocation=[" + invocation + "]"); Object rval = invocation.proceed(); System.out.println("Invocation returned"); return rval; } } ``` Kotlin ``` class DebugInterceptor : MethodInterceptor { override fun invoke(invocation: MethodInvocation): Any { println("Before: invocation=[$invocation]") val rval = invocation.proceed() println("Invocation returned") return rval } } ``` 注意调用`proceed()`的`MethodInvocation`方法。这会沿着拦截器链朝向连接点。大多数拦截器调用这个方法并返回它的返回值。但是,`MethodInterceptor`与任何 around advice 一样,可以返回不同的值或抛出异常,而不是调用 proceed 方法。然而,你不想在没有充分理由的情况下这样做。 | |`MethodInterceptor`实现提供了与其他 AOP 联盟兼容的 AOP
实现的互操作性。在本节的剩余部分
中讨论的其他通知类型以 Spring 特定的方式实现了常见的 AOP 概念。虽然在使用最特定的建议类型时有
的优点,但如果
你可能希望在另一个 AOP 框架中运行该方面,则坚持使用`MethodInterceptor`周围的建议。请注意,PointCuts
目前在框架之间不是可互操作的,并且 AOP 联盟目前没有
定义 PointCut 接口。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 建议之前 一种更简单的建议类型是在给出建议之前给出的。这不需要`MethodInvocation`对象,因为只有在输入方法之前才调用它。 before 通知的主要优点是,不需要调用`proceed()`方法,因此,不可能因疏忽而无法继续进行拦截器链。 下面的清单显示了`MethodBeforeAdvice`接口: ``` public interface MethodBeforeAdvice extends BeforeAdvice { void before(Method m, Object[] args, Object target) throws Throwable; } ``` ( Spring 的 API 设计将允许先字段后通知,尽管通常的对象适用于字段截取,并且 Spring 不太可能实现它。) 请注意,返回类型是`void`。before advice 可以在连接点运行之前插入自定义行为,但不能更改返回值。如果 before 通知抛出异常,它将停止拦截器链的进一步执行。异常向拦截器链传播备份。如果它未被选中,或者在被调用方法的签名上,它将直接传递给客户机。否则,它将被 AOP 代理包装在一个未经检查的异常中。 下面的示例显示了 Spring 中的 before 通知,该通知计算了所有方法调用: Java ``` public class CountingBeforeAdvice implements MethodBeforeAdvice { private int count; public void before(Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; } } ``` Kotlin ``` class CountingBeforeAdvice : MethodBeforeAdvice { var count: Int = 0 override fun before(m: Method, args: Array, target: Any?) { ++count } } ``` | |在建议可以与任何切入点一起使用之前。| |---|--------------------------------------------| ##### 抛出建议 如果连接点引发异常,则在连接点返回后调用 Throws 通知。 Spring 提供打印抛出建议。请注意,这意味着“org.springframework. AOP.throwsadvice”接口不包含任何方法。它是一个标记接口,标识给定对象实现了一个或多个类型抛出建议方法。这些措施应采取以下形式: ``` afterThrowing([Method, args, target], subclassOfThrowable) ``` 只有最后一个论点是必需的。方法签名可以有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。接下来的两个列表展示了抛出建议的示例类。 如果抛出了`RemoteException`(包括从子类),则调用以下通知: Java ``` public class RemoteThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } } ``` Kotlin ``` class RemoteThrowsAdvice : ThrowsAdvice { fun afterThrowing(ex: RemoteException) { // Do something with remote exception } } ``` 与前面的建议不同,下一个示例声明了四个参数,这样它就可以访问被调用的方法、方法参数和目标对象。如果抛出`ServletException`,将调用以下通知: Java ``` public class ServletThrowsAdviceWithArguments implements ThrowsAdvice { public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments } } ``` Kotlin ``` class ServletThrowsAdviceWithArguments : ThrowsAdvice { fun afterThrowing(m: Method, args: Array, target: Any, ex: ServletException) { // Do something with all arguments } } ``` 最后一个示例说明了如何在一个同时处理`RemoteException`和`ServletException`的类中使用这两个方法。任何数量的抛出建议方法都可以合并到一个类中。下面的清单展示了最后一个示例: Java ``` public static class CombinedThrowsAdvice implements ThrowsAdvice { public void afterThrowing(RemoteException ex) throws Throwable { // Do something with remote exception } public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { // Do something with all arguments } } ``` Kotlin ``` class CombinedThrowsAdvice : ThrowsAdvice { fun afterThrowing(ex: RemoteException) { // Do something with remote exception } fun afterThrowing(m: Method, args: Array, target: Any, ex: ServletException) { // Do something with all arguments } } ``` | |如果一个 throws-advice 方法本身抛出一个异常,它将重写
原始异常(也就是说,它将更改抛出给用户的异常)。覆盖的
异常通常是一个 runtimeException,它与任何方法
签名兼容。但是,如果一个 throws-advice 方法抛出一个检查过的异常,它必须
匹配目标方法声明的异常,因此在某种程度上
耦合到特定的目标方法签名。*Do not throw an undeclared checked
exception that is incompatible with the target method’s signature!*| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | |抛出建议可以与任何切入点一起使用。| |---|--------------------------------------------| ##### 在返回建议后 Spring 中的 after returningadvice 必须实现 `org.springframework. AOP.afterreturningadvice` 接口,下面的列表显示了该接口: ``` public interface AfterReturningAdvice extends Advice { void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable; } ``` 返回后通知可以访问返回值(它不能对其进行修改)、调用的方法、方法的参数和目标。 返回建议后,将计算所有未抛出异常的成功方法调用: Java ``` public class CountingAfterReturningAdvice implements AfterReturningAdvice { private int count; public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable { ++count; } public int getCount() { return count; } } ``` Kotlin ``` class CountingAfterReturningAdvice : AfterReturningAdvice { var count: Int = 0 private set override fun afterReturning(returnValue: Any?, m: Method, args: Array, target: Any?) { ++count } } ``` 此建议不会更改执行路径。如果它抛出一个异常,它将被抛出到拦截器链中,而不是返回值。 | |返回后,建议可以与任何切入点一起使用。| |---|-----------------------------------------------------| ##### 介绍建议 Spring 将介绍建议视为一种特殊的拦截建议。 Introduction 需要一个`IntroductionAdvisor`和一个`IntroductionInterceptor`来实现以下接口: ``` public interface IntroductionInterceptor extends MethodInterceptor { boolean implementsInterface(Class intf); } ``` 继承自 AOP Alliance 的`invoke()`方法`MethodInterceptor`接口必须实现该介绍。也就是说,如果被调用的方法位于一个引入的接口上,则引入拦截器负责处理方法调用——它不能调用`proceed()`。 介绍建议不能与任何切入点一起使用,因为它只适用于类,而不是方法级别。你只能使用“介绍顾问”提供的介绍建议,它有以下方法: ``` public interface IntroductionAdvisor extends Advisor, IntroductionInfo { ClassFilter getClassFilter(); void validateInterfaces() throws IllegalArgumentException; } public interface IntroductionInfo { Class[] getInterfaces(); } ``` 没有`MethodMatcher`,因此,没有`Pointcut`与介绍建议相关联。只有类过滤是合乎逻辑的。 `getInterfaces()`方法返回此顾问引入的接口。 在内部使用`validateInterfaces()`方法来查看是否可以通过配置的`IntroductionInterceptor`实现引入的接口。 考虑 Spring 测试套件中的一个示例,并假设我们希望将以下接口引入一个或多个对象: Java ``` public interface Lockable { void lock(); void unlock(); boolean locked(); } ``` Kotlin ``` interface Lockable { fun lock() fun unlock() fun locked(): Boolean } ``` 这说明了一个 mixin。我们希望能够将建议的对象强制转换为`Lockable`,无论它们的类型如何,并调用锁定和解锁方法。如果我们调用`lock()`方法,我们希望所有 setter 方法都抛出一个`LockedException`。因此,我们可以添加一个方面,该方面提供了使对象不可变的能力,而不需要它们对此有任何了解: AOP 的一个很好的示例。 首先,我们需要一个`IntroductionInterceptor`来完成繁重的工作。在这种情况下,我们扩展`org.springframework.aop.support.DelegatingIntroductionInterceptor`便利类。我们可以直接实现`IntroductionInterceptor`,但是在大多数情况下使用 `delegatingintroductionInterceptor’是最好的。 `DelegatingIntroductionInterceptor`的设计目的是将介绍委派给所介绍的接口的实际实现,从而隐藏了拦截的使用。可以使用构造函数参数将委托设置为任何对象。默认的委托(当使用无参数构造函数时)是`this`。因此,在下一个示例中,委托是`LockMixin`的`DelegatingIntroductionInterceptor`子类。给定一个委托(默认情况下是委托本身),`DelegatingIntroductionInterceptor`实例将查找委托实现的所有接口(除了 `introductionInterceptor’),并支持针对其中任何一个接口的介绍。像`LockMixin`这样的子类可以调用`suppressInterface(Class intf)`方法来抑制不应该公开的接口。然而,无论`IntroductionInterceptor`准备支持多少个接口,“IntroductionAdvisor”都使用控件来控制哪些接口实际上是公开的。引入的接口掩盖了目标对同一接口的任何实现。 因此,`LockMixin`扩展了`DelegatingIntroductionInterceptor`并实现了`Lockable`本身。超类自动获取`Lockable`可以支持的引言,因此我们不需要指定它。我们可以以这种方式引入任意数量的接口。 注意`locked`实例变量的使用。这实际上为目标对象中的状态添加了额外的状态。 下面的示例显示了`LockMixin`类的示例: Java ``` public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { private boolean locked; public void lock() { this.locked = true; } public void unlock() { this.locked = false; } public boolean locked() { return this.locked; } public Object invoke(MethodInvocation invocation) throws Throwable { if (locked() && invocation.getMethod().getName().indexOf("set") == 0) { throw new LockedException(); } return super.invoke(invocation); } } ``` Kotlin ``` class LockMixin : DelegatingIntroductionInterceptor(), Lockable { private var locked: Boolean = false fun lock() { this.locked = true } fun unlock() { this.locked = false } fun locked(): Boolean { return this.locked } override fun invoke(invocation: MethodInvocation): Any? { if (locked() && invocation.method.name.indexOf("set") == 0) { throw LockedException() } return super.invoke(invocation) } } ``` 通常,你不需要重写`invoke()`方法。“delegatingintroductionInterceptor”实现(如果引入了该方法,则调用`delegate`方法,否则将继续进行连接)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用 setter 方法。 所需的介绍只需要持有一个不同的“lockmixin”实例并指定引入的接口(在这种情况下,只需要“lockable”)。一个更复杂的示例可能会引用介绍拦截器(它将被定义为原型)。在这种情况下,不存在与`LockMixin`相关的配置,因此我们使用`new`创建它。下面的示例显示了我们的`LockMixinAdvisor`类: Java ``` public class LockMixinAdvisor extends DefaultIntroductionAdvisor { public LockMixinAdvisor() { super(new LockMixin(), Lockable.class); } } ``` Kotlin ``` class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java) ``` 我们可以非常简单地应用此 advisor,因为它不需要配置。(然而,在没有“introductionAdvisor”的情况下,不可能使用`IntroductionInterceptor`。)与通常的介绍一样,advisor 必须是每个实例,因为它是有状态的。对于每个被建议的对象,我们需要一个`LockMixinAdvisor`的不同实例,因此也需要一个 `lockmixin’实例。顾问是被建议对象状态的一部分。 我们可以通过使用`Advised.addAdvisor()`方法或 XML 配置中的(推荐的方式),以编程方式应用此 Advisor,就像任何其他的 Advisor 一样。下面讨论的所有代理创建选择,包括“自动代理创建”,正确处理介绍和有状态的混合。 ### 6.3. Spring 中的顾问 API 在 Spring 中,advisor 是一个方面,该方面仅包含与切入点表达式相关联的单个通知对象。 除了介绍的特殊情况外,任何 advisor 都可以与任何 advice 一起使用。org.SpringFramework. AOP.Support.DefaultPointCutavisor 是最常用的 advisor 类。它可以与`MethodInterceptor`、`BeforeAdvice`或 `throwsadvice’一起使用。 在相同的 AOP 代理中混合 Spring 中的顾问和建议类型是可能的。例如,你可以在一个代理配置中使用围绕建议、抛出建议和在建议之前的拦截。 Spring 自动创建必要的拦截链。 ### 6.4.使用`ProxyFactoryBean`创建 AOP 代理 如果你为你的业务对象使用 Spring IOC 容器(an`ApplicationContext`或`BeanFactory`)(而且你应该是!),那么你希望使用 Spring 的 AOP `FactoryBean’实现中的一个。(请记住,工厂 Bean 引入了间接层,允许它创建不同类型的对象。 | |Spring AOP 支持还在覆盖件下使用工厂 bean。| |---|----------------------------------------------------------------| 在 Spring 中创建 AOP 代理的基本方法是使用 `org.SpringFramework. AOP.Framework.ProxyFactoryBean’。这样就可以完全控制切入点、应用的任何建议以及它们的排序。然而,如果你不需要这种控制,那么有一些更简单的选项是更好的。 #### 6.4.1.基础知识 与其他 Spring `FactoryBean`实现方式一样,`ProxyFactoryBean`引入了间接的级别。如果定义了名为`ProxyFactoryBean`的`foo`,则引用`foo`的对象不会看到`ProxyFactoryBean`实例本身,而是由`getObject()`方法的实现在`ProxyFactoryBean`中创建的对象。该方法创建一个 AOP 代理来包装目标对象。 使用`ProxyFactoryBean`或另一个 IoC-aware 类来创建 AOP 代理的最重要的好处之一是,IoC 也可以管理建议和切入点。 AOP 这是一个强大的特性,使得能够使用其他框架很难实现的某些方法成为可能。例如,建议本身可以引用应用程序对象(除了目标,这应该在任何 AOP 框架中可用),从而受益于依赖注入提供的所有可插拔性。 #### 6.4.2.Javabean 属性 与 Spring 提供的大多数`FactoryBean`实现一样,`ProxyFactoryBean’类本身是一个 JavaBean。它的特性用于: * 指定要代理的目标。 * 指定是否使用 CGLIB(稍后将进行说明,还请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 一些键属性继承自`org.springframework.aop.framework.ProxyConfig`( Spring 中所有 AOP 代理工厂的超类)。这些关键属性包括以下内容: * `proxyTargetClass`:`true`如果要代理目标类,而不是目标类的接口。如果将此属性值设置为`true`,则创建 CGLIB 代理(但也请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 * `optimize`:控制是否对通过 CGLIB 创建的代理应用积极的优化。除非你完全了解相关的 AOP 代理如何处理优化,否则你不应随意使用此设置。这目前仅用于 CGlib 代理。它对 JDK 动态代理没有任何影响。 * `frozen`:如果代理配置是`frozen`,则不再允许对配置进行更改。这既是一种轻微的优化,也适用于在创建代理后不希望调用者能够操作代理(通过`Advised`接口)的情况。此属性的默认值为“false”,因此允许更改(例如添加额外的建议)。 * `exposeProxy`:确定当前代理是否应该在 `ThreadLocal’中公开,以便目标可以访问它。如果目标需要获得代理,并且`exposeProxy`属性设置为`true`,则目标可以使用 `aopContext.currentProxy()’方法。 特定于`ProxyFactoryBean`的其他属性包括以下内容: * `proxyInterfaces`:由`String`接口名称组成的数组。如果不提供此选项,则使用目标类的 CGLIB 代理(但也请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 * `interceptorNames`:要应用的`String`数组、拦截器或其他通知名称。订购是重要的,在先到先得的基础上。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。 这些名称是当前工厂中的 Bean 名称,包括来自祖先工厂的 Bean 名称。这里不能提及 Bean 引用,因为这样做会导致“proxyFactoryBean”忽略建议的单例设置。 你可以在拦截器名称后面附加一个星号。这样做会导致所有的 advisor bean 的应用程序的名称都以要应用的星号之前的部分开始。你可以在[使用“全球”顾问](#aop-global-advisors)中找到使用此功能的示例。 * 单例:无论工厂是否应该返回单个对象,无论调用`getObject()`方法的频率如何。几个`FactoryBean`实现提供了这样的方法。默认值是`true`。如果你想使用有状态的建议--例如,对于有状态的 mixin--使用原型建议以及单例值“false”。 #### 6.4.3.基于 JDK 和 CGLIB 的代理 这一节是关于`ProxyFactoryBean`如何选择为特定目标对象创建基于 JDK 的代理或基于 CGlib 的代理的权威文档。 | |`ProxyFactoryBean`关于创建基于 JDK-或 CGLIB 的
代理的行为在 Spring 的 1.2.x 和 2.0 版本之间发生了变化。`ProxyFactoryBean`现在
在自动检测接口方面表现出与 `TransactionProxyFactoryBean’类类似的语义。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 如果要代理的目标对象的类(以下简称为目标类)不实现任何接口,则创建一个基于 CGLIB 的代理。这是最简单的场景,因为 JDK 代理是基于接口的,没有接口意味着 JDK 代理甚至是不可能的。你可以通过设置`interceptorNames`属性来插入目标 Bean 并指定拦截器列表。请注意,即使“proxyFactoryBean”的`proxyTargetClass`属性已设置为`false`,也会创建基于 CGLIB 的代理。(这样做没有意义,最好从 Bean 定义中删除,因为它最好是多余的,最坏的情况是令人困惑。 如果目标类实现了一个(或多个)接口,那么创建的代理类型取决于`ProxyFactoryBean`的配置。 如果`ProxyFactoryBean`的`proxyTargetClass`属性已设置为`true`,则创建一个基于 CGLIB 的代理。这是有道理的,也符合最少令人惊讶的原则。即使“proxyFactoryBean”的`proxyInterfaces`属性已被设置为一个或多个完全限定的接口名称,但将`proxyTargetClass`属性设置为`true`的事实将导致基于 CGLIB 的代理生效。 如果`proxyInterfaces`的`ProxyFactoryBean`属性已被设置为一个或多个完全限定的接口名称,则将创建一个基于 JDK 的代理。创建的代理实现了在`proxyInterfaces`属性中指定的所有接口。如果目标类实现了比`proxyInterfaces`属性中指定的接口多得多的接口,那就很好了,但是这些额外的接口不是由返回的代理实现的。 如果`proxyInterfaces`的`ProxyFactoryBean`属性尚未设置,但是目标类确实实现了一个(或多个)接口,则 `ProxyFactoryBean` 自动检测目标类确实实现了至少一个接口的事实,并创建了一个基于 JDK 的代理。实际代理的接口是目标类实现的所有接口。实际上,这与向`proxyInterfaces`属性提供目标类实现的每个接口的列表是一样的。然而,它的工作量要少得多,而且不太容易出现印刷错误。 #### 6.4.4.代理接口 以`ProxyFactoryBean`的一个简单示例为例。这个例子涉及到: * 被代理的目标 Bean。这是示例中的`personTarget` Bean 定义。 * 用于提供建议的`Advisor`和`Interceptor`。 * AOP 代理 Bean 定义来指定目标对象(`personTarget` Bean)、代理的接口以及应用的建议。 下面的清单展示了这个示例: ``` myAdvisor debugInterceptor ``` 请注意,`interceptorNames`属性接受`String`的列表,该列表保存当前工厂中拦截器或顾问的 Bean 名称。你可以在返回之前、之后使用 Advisors、Interceptors,并抛出建议对象。对顾问的排序意义重大。 | |你可能想知道为什么列表中没有 Bean 引用。其原因是
,如果`ProxyFactoryBean`的单例属性设置为`false`,则它必须能够
返回独立的代理实例。如果任何顾问本身是一个原型,则需要返回一个
独立实例,因此必须能够从工厂获得
原型的实例。仅有引用是不够的。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 前面显示的`person` Bean 定义可以用来代替`Person`实现,如下所示: Java ``` Person person = (Person) factory.getBean("person"); ``` Kotlin ``` val person = factory.getBean("person") as Person; ``` 在同一 IoC 上下文中的其他 bean 可以表示对它的强类型依赖,就像普通的 Java 对象一样。下面的示例展示了如何做到这一点: ``` ``` 本例中的`PersonUser`类公开了类型`Person`的属性。就其而言, AOP 代理可以透明地代替“真实的”人实现。然而,它的类将是一个动态代理类。可以将它强制转换到`Advised`接口(稍后讨论)。 你可以通过使用匿名内部 Bean 来隐藏目标和代理之间的区别。只有`ProxyFactoryBean`的定义是不同的。仅为完整起见,才列入该建议。下面的示例展示了如何使用匿名内部 Bean: ``` myAdvisor debugInterceptor ``` Bean 使用匿名内部具有的优点是只有一个类型`Person`的对象。如果我们希望防止应用程序上下文的用户获得对非建议对象的引用,或者需要避免使用 Spring IOC 自动布线的任何歧义,那么这是有用的。可以说,`ProxyFactoryBean`的定义是自包含的,这也有一个优点。然而,有时能够从工厂获得不建议的目标实际上可能是一种优势(例如,在某些测试场景中)。 #### 6.4.5.代理类 如果你需要代理一个类,而不是一个或多个接口,该怎么办? 想象一下,在我们早期的示例中,没有`Person`接口。我们需要建议一个名为`Person`的类,它没有实现任何业务接口。在这种情况下,可以将 Spring 配置为使用 CGlib 代理,而不是动态代理。为此,将前面显示的`ProxyFactoryBean`上的 `ProxyTargetClass’属性设置为`true`。虽然最好使用接口编程,而不是类编程,但在使用遗留代码时,向不实现接口的类提供建议的能力可能是有用的。(一般来说, Spring 不是规定性的。尽管它使应用好的实践变得容易,但它避免了强制使用特定的方法。 如果你愿意,你可以在任何情况下强制使用 CGlib,即使你确实有接口。 CGLIB 代理的工作原理是在运行时生成目标类的一个子类。 Spring 将此生成的子类配置为将方法调用委托给原始目标。子类用于实现 decorator 模式,并在建议中进行编织。 CGLIB 代理通常应该对用户透明。然而,有一些问题需要考虑: * 不能通知`Final`方法,因为它们不能被重写。 * 没有必要将 CGlib 添加到你的 Classpath 中。截至 Spring 3.2,CGlib 被重新包装并包括在 Spring-core jar 中。换句话说,基于 CGLIB 的 AOP 工作是“开箱即用”的,JDK 动态代理也是如此。 CGlib 代理和动态代理之间的性能差别不大。在这种情况下,业绩不应是决定性的考虑因素。 #### 6.4.6.使用“全球”顾问 通过将星号附加到拦截器名称中,所有具有 Bean 名称的、与星号之前的部分相匹配的顾问都将添加到顾问链中。如果你需要添加一组标准的“全球”顾问,这可能会派上用场。以下示例定义了两个全球顾问: ``` global* ``` ### 6.5.简明代理定义 特别是在定义事务代理时,你可能会使用许多类似的代理定义。 Bean 父定义和子定义以及内部 Bean 定义的使用可以导致更干净和更简洁的代理定义。 首先,我们为代理创建一个父模板 Bean 定义,如下所示: ``` PROPAGATION_REQUIRED ``` 这本身从来不是实例化的,因此它实际上可能是不完整的。然后,需要创建的每个代理都是一个子定义 Bean,该定义将代理的目标包装为内部定义 Bean,因为该目标本身永远不会被使用。以下示例显示了这样的子 Bean: ``` ``` 你可以从父模板重写属性。在下面的示例中,我们重写事务传播设置: ``` PROPAGATION_REQUIRED,readOnly PROPAGATION_REQUIRED,readOnly PROPAGATION_REQUIRED,readOnly PROPAGATION_REQUIRED ``` 请注意,在父 Bean 示例中,我们通过将`abstract`属性设置为`true`,显式地将父 Bean 定义标记为抽象的,如所描述的[previously](#beans-child-bean-definitions),以便它实际上可能永远不会被实例化。默认情况下,应用程序上下文(但不是简单的 Bean 工厂)预先实例化所有单例。因此,重要的是(至少对于单例 bean 而言),如果你有一个(父) Bean 定义,只打算用作模板,并且该定义指定了一个类,那么你必须确保将`abstract`属性设置为`true`。否则,应用程序上下文实际上会尝试预先实例化它。 ### 6.6.用`ProxyFactory`以编程方式创建 AOP 代理 使用 Spring 以编程方式创建 AOP 代理是很容易的。这允许你使用 Spring AOP 而不依赖 Spring IOC。 由目标对象实现的接口是自动代理的。下面的清单展示了为目标对象创建代理的过程,其中包括一个拦截器和一个顾问: Java ``` ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl); factory.addAdvice(myMethodInterceptor); factory.addAdvisor(myAdvisor); MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy(); ``` Kotlin ``` val factory = ProxyFactory(myBusinessInterfaceImpl) factory.addAdvice(myMethodInterceptor) factory.addAdvisor(myAdvisor) val tb = factory.proxy as MyBusinessInterface ``` 第一步是构造一个类型为 `org.SpringFramework. AOP.Framework.ProxyFactory’的对象。你可以使用目标对象来创建这个,就像前面的示例一样,或者指定要在替代构造函数中代理的接口。 你可以添加建议(将拦截器作为一种专门的建议)、顾问或两者,并在`ProxyFactory`的生命周期中对它们进行操作。如果你添加了“IntroductionInterceptionAroundVisor”,则可以使代理实现其他接口。 在`ProxyFactory`(继承自`AdvisedSupport`)上也有方便的方法,允许你添加其他通知类型,例如 before 和 throws advisedsupport。 | |AOP 在大多数
应用程序中,将代理创建与 IOC 框架集成是最佳实践。我们建议你使用 AOP,
将配置从 Java 代码中外部化,就像你通常应该使用的那样。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 6.7.操作被建议的对象 无论你如何创建 AOP 代理,你都可以通过使用 `org.springframework. AOP.framework.adved’interface 来操作它们。 AOP 任何代理都可以被强制转换到该接口,无论它实现了哪个其他接口。该接口包括以下方法: Java ``` Advisor[] getAdvisors(); void addAdvice(Advice advice) throws AopConfigException; void addAdvice(int pos, Advice advice) throws AopConfigException; void addAdvisor(Advisor advisor) throws AopConfigException; void addAdvisor(int pos, Advisor advisor) throws AopConfigException; int indexOf(Advisor advisor); boolean removeAdvisor(Advisor advisor) throws AopConfigException; void removeAdvisor(int index) throws AopConfigException; boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException; boolean isFrozen(); ``` Kotlin ``` fun getAdvisors(): Array @Throws(AopConfigException::class) fun addAdvice(advice: Advice) @Throws(AopConfigException::class) fun addAdvice(pos: Int, advice: Advice) @Throws(AopConfigException::class) fun addAdvisor(advisor: Advisor) @Throws(AopConfigException::class) fun addAdvisor(pos: Int, advisor: Advisor) fun indexOf(advisor: Advisor): Int @Throws(AopConfigException::class) fun removeAdvisor(advisor: Advisor): Boolean @Throws(AopConfigException::class) fun removeAdvisor(index: Int) @Throws(AopConfigException::class) fun replaceAdvisor(a: Advisor, b: Advisor): Boolean fun isFrozen(): Boolean ``` `getAdvisors()`方法为添加到工厂的每个 Advisor、Interceptor 或其他通知类型返回一个`Advisor`。如果你添加了`Advisor`,则此索引处返回的顾问就是你添加的对象。如果你添加了一个拦截器或其他通知类型, Spring 将其包装在一个带有切入点的 Advisor 中,该切入点总是返回`true`。因此,如果你添加了一个`MethodInterceptor`,则为该索引返回的 advisor 是一个`DefaultPointcutAdvisor`,它返回你的 `MethodInterceptor’和一个匹配所有类和方法的切入点。 `addAdvisor()`方法可用于添加任何`Advisor`。通常,持有切入点和建议的顾问是通用的`DefaultPointcutAdvisor`,你可以将其用于任何建议或切入点(但不用于介绍)。 默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。唯一的限制是,不可能添加或删除 IntroductionAdvisor,因为来自工厂的现有代理不会显示接口更改。(你可以从工厂获得一个新的代理,以避免此问题。 下面的示例显示了将 AOP 代理强制转换到`Advised`接口并检查和操作其建议: Java ``` Advised advised = (Advised) myObject; Advisor[] advisors = advised.getAdvisors(); int oldAdvisorCount = advisors.length; System.out.println(oldAdvisorCount + " advisors"); // Add an advice like an interceptor without a pointcut // Will match all proxied methods // Can use for interceptors, before, after returning or throws advice advised.addAdvice(new DebugInterceptor()); // Add selective advice using a pointcut advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice)); assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length); ``` Kotlin ``` val advised = myObject as Advised val advisors = advised.advisors val oldAdvisorCount = advisors.size println("$oldAdvisorCount advisors") // Add an advice like an interceptor without a pointcut // Will match all proxied methods // Can use for interceptors, before, after returning or throws advice advised.addAdvice(DebugInterceptor()) // Add selective advice using a pointcut advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice)) assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size) ``` | |在生产中修改对
业务对象的建议是否可取(没有双关语意思)是值得怀疑的,尽管毫无疑问存在合法的使用情况。
但是,它在开发中(例如在测试中)可能非常有用。我们有时发现
能够以拦截器或其他
建议的形式添加测试代码非常有用,从而进入我们想要测试的方法调用。(例如,建议可以
获取为该方法创建的事务的内部,也许可以运行 SQL 来检查
数据库是否已正确更新,然后标记该事务以进行回滚。)| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 根据创建代理的方式,通常可以设置`frozen`标志。在这种情况下,`Advised``isFrozen()`方法返回`true`,任何通过添加或删除修改建议的尝试都会导致`AopConfigException`。在某些情况下,冻结被建议对象的状态的能力是有用的(例如,可以防止调用代码删除安全拦截器)。 ### 6.8.使用“自动代理”功能 到目前为止,我们已经考虑了通过使用`ProxyFactoryBean`或类似的工厂 Bean 显式地创建 AOP 代理。 Spring 还允许我们使用“自动代理” Bean 定义,其可以自动代理选择 Bean 定义。这是建立在 Spring 的“ Bean 后处理器”基础设施上的,该基础设施允许在容器加载时修改任何 Bean 定义。 在这个模型中,你在 XML Bean 定义文件中设置了一些特殊的 Bean 定义,以配置自动代理基础设施。这允许你声明有资格进行自动代理的目标。你不需要使用`ProxyFactoryBean`。 有两种方法可以做到这一点: * 通过使用在当前上下文中引用特定 bean 的自动代理创建器。 * 一个值得单独考虑的自动代理创建的特殊情况:由源级元数据属性驱动的自动代理创建。 #### 6.8.1.自动代理 Bean 定义 本节介绍了由“org.springframework. AOP.framework.autoproxy”软件包提供的自动代理创建者。 ##### `BeanNameAutoProxyCreator` `BeanNameAutoProxyCreator`类是一个`BeanPostProcessor`类,它自动为名称与文字值或通配符匹配的 bean 创建 AOP 代理。下面的示例展示了如何创建`BeanNameAutoProxyCreator` Bean: ``` myInterceptor ``` 与`ProxyFactoryBean`一样,有一个`interceptorNames`属性,而不是拦截器列表,以允许原型顾问的正确行为。被命名的“拦截器”可以是顾问,也可以是任何类型的建议。 与一般的自动代理一样,使用`BeanNameAutoProxyCreator`的主要目的是以最小的配置量将相同的配置一致地应用于多个对象。它是将声明式事务应用于多个对象的流行选择。 Bean 名称匹配的定义,例如前面示例中的`jdkMyBean`和`onlyJdk`,是与目标类完全一致的旧定义 Bean。 AOP 代理由`BeanNameAutoProxyCreator`自动创建。同样的建议也适用于所有匹配的 bean。请注意,如果使用了 Advisors(而不是前面示例中的拦截器),那么切入点可能会以不同的方式应用于不同的 bean。 ##### `DefaultAdvisorAutoProxyCreator` 一个更通用且功能极其强大的自动代理创建器是“DefaultVisorAutoProxyCreator”。这自动地在当前上下文中应用合格的顾问,而不需要在自动代理顾问的 Bean 定义中包括特定的 Bean 名称。它提供了与`BeanNameAutoProxyCreator`相同的优点,即配置一致和避免重复。 使用这一机制涉及: * 指定`DefaultAdvisorAutoProxyCreator` Bean 定义。 * 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点来进行评估,以检查每个建议对候选人 Bean 定义的资格。 `DefaultAdvisorAutoProxyCreator`会自动计算每个 Advisor 中包含的切入点,以查看它应该对每个业务对象应用什么(如果有的话)建议(例如示例中的`businessObject1`和`businessObject2`)。 这意味着可以将任意数量的顾问自动应用到每个业务对象。如果任何顾问中没有切入点与业务对象中的任何方法匹配,则不代理该对象。 Bean 在为新的业务对象添加定义时,如果有必要,会自动代理它们。 通常,自动代理的优点是使调用者或依赖项不可能获得未通知的对象。在这个 `ApplicationContext’上调用`getBean("businessObject1")`返回一个 AOP 代理,而不是目标业务对象。(前面展示的“内心 Bean”成语也提供了这一好处。 下面的示例创建了`DefaultAdvisorAutoProxyCreator` Bean 和本节讨论的其他元素: ``` ``` 如果你希望将相同的建议一致地应用于许多业务对象,那么`DefaultAdvisorAutoProxyCreator`非常有用。一旦基础设施定义到位,你就可以添加新的业务对象,而不需要包括特定的代理配置。你还可以很容易地删除其他方面(例如,跟踪或性能监视方面),只需对配置进行最小的更改。 `DefaultAdvisorAutoProxyCreator`提供了对过滤和排序的支持(通过使用命名约定,以便只对某些顾问进行评估,这允许在同一工厂中使用多个配置不同的顾问或自动代理创建者)。如果出现问题,Advisors 可以实现`org.springframework.core.Ordered`接口,以确保正确的排序。前面示例中使用的`TransactionAttributeSourceAdvisor`具有可配置的订单值。默认设置是无序的。 ### 6.9.使用`TargetSource`实现 Spring 提供了`TargetSource`的概念,在 `org.SpringFramework. AOP.targetSource` 接口中表示。这个接口负责返回实现连接点的“目标对象”。 AOP 代理每次处理方法调用时,都会对`TargetSource`实现请求一个目标实例。 使用 Spring AOP 的开发人员通常不需要直接使用`TargetSource`实现,但是这提供了一种支持池、热插拔和其他复杂目标的强大手段。例如,池`TargetSource`可以通过使用池来管理实例,为每次调用返回不同的目标实例。 如果没有指定`TargetSource`,则使用默认实现来包装本地对象。每次调用都返回相同的目标(如你所料)。 本节的其余部分描述了 Spring 提供的标准目标源,以及如何使用它们。 | |当使用自定义目标源时,你的目标通常需要是一个原型
,而不是一个单例定义 Bean。这允许 Spring 在需要时创建新的目标
实例。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 6.9.1.热插拔目标源 `org.springframework.aop.target.HotSwappableTargetSource`的存在是为了让 AOP 代理的目标被切换,同时让调用者保留对它的引用。 更改目标源的目标将立即生效。“HotSwappleTargetSource”是线程安全的。 你可以在 HotswappleTargetSource 上使用`swap()`方法来更改目标,如下例所示: Java ``` HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); Object oldTarget = swapper.swap(newTarget); ``` Kotlin ``` val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource val oldTarget = swapper.swap(newTarget) ``` 下面的示例展示了所需的 XML 定义: ``` ``` 前面的`swap()`调用更改了可交换对象的目标 Bean。持有对该 Bean 的引用的客户不知道该更改,但立即开始命中新目标。 虽然此示例不添加任何建议(不需要添加建议来使用`TargetSource`),但任何`TargetSource`都可以与任意建议一起使用。 #### 6.9.2.汇集目标源 使用池目标源提供了类似于无状态会话 EJB 的编程模型,在这种模型中,将维护一个由相同实例组成的池,方法调用将释放池中的对象。 Spring 池和 SLSB 池之间的一个关键区别是 Spring 池可以应用于任何 POJO。 Spring 与一般情况下一样,该服务可以以非侵入性的方式应用。 Spring 提供了对 Commons Pool2.2 的支持,其提供了相当有效的池实现。你需要应用程序的 Classpath 上的`commons-pool` jar 才能使用此功能。你还可以子类 `org.springframework. AOP.target.abstractpoolingtargetSource’来支持任何其他池 API。 | |Commons Pool1.5+ 也受到支持,但在 Spring Framework4.2 中已不受欢迎。| |---|---------------------------------------------------------------------------------| 下面的清单展示了一个配置示例: ``` ... properties omitted ``` 请注意,目标对象(在前面的示例中是“BusinessObjectTarget”)必须是一个原型。这使得`PoolingTargetSource`实现可以根据需要创建目标的新实例来增加池。请参阅[Javadoc’AbstractPoolingTargetSource’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/target/AbstractPoolingTargetSource.html)和你希望使用的具体子类,以获取有关其属性的信息。`maxSize`是最基本的,并且总是保证存在。 在这种情况下,`myInterceptor`是需要在相同的 IOC 上下文中定义的拦截器的名称。但是,你不需要指定拦截器来使用池。如果你只想要池,而不想要其他的建议,那么根本不要设置“拦截器名称”属性。 你可以将 Spring 配置为能够将任何池对象强制转换到 `org.springframework. AOP.target.poolingconfig` 接口,该接口通过介绍公开有关池的配置和当前大小的信息。你需要定义类似于以下内容的顾问: ``` ``` 这个顾问是通过调用“AbstractPoolingTargetSource”类上的方便方法获得的,因此使用`MethodInvokingFactoryBean`。此顾问的名称(这里是“poolconfigAdvisor”)必须位于公开池对象的`ProxyFactoryBean`中的拦截器名称列表中。 强制转换的定义如下: Java ``` PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject"); System.out.println("Max pool size is " + conf.getMaxSize()); ``` Kotlin ``` val conf = beanFactory.getBean("businessObject") as PoolingConfig println("Max pool size is " + conf.maxSize) ``` | |通常不需要池无状态的服务对象。我们不认为它应该是
的默认选择,因为大多数无状态对象自然是线程安全的,并且如果资源被缓存,实例
池是有问题的。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 通过使用自动代理,可以获得更简单的池。你可以设置任何自动代理创建者使用的`TargetSource`实现。 #### 6.9.3.原型目标源 设置“原型”目标源类似于设置池`TargetSource`。在这种情况下,每个方法调用都会创建一个新的目标实例。尽管在现代 JVM 中创建新对象的成本并不高,但连接新对象(满足其 IoC 依赖关系)的成本可能更高。因此,如果没有很好的理由,就不应该使用这种方法。 为此,你可以修改前面显示的`poolTargetSource`定义,如下所示(为了清楚起见,我们还更改了名称): ``` ``` 唯一的属性是目标的名称 Bean。继承在“TargetSource”实现中使用,以确保一致的命名。与池目标源一样,目标 Bean 必须是原型 Bean 定义。 #### 6.9.4.`ThreadLocal`目标来源 如果需要为每个传入请求(每个线程)创建一个对象,`ThreadLocal`目标源是有用的。`ThreadLocal`的概念提供了一个 JDK 范围内的功能,可以在线程旁边透明地存储资源。设置“ThreadlocalTargetSource”与解释其他类型的目标源几乎相同,如下例所示: ``` ``` | |当
实例在多线程和多类加载器环境中不正确地使用它们时,会出现严重的问题(可能导致内存泄漏)。你
应该始终考虑在其他类中包装 ThreadLocal,并且永远不要直接使用
`ThreadLocal`本身(包装类中除外)。此外,你还应该
始终记住正确地设置和取消设置(后者只涉及对 `threadlocal.set(null)’的调用)线程本地的资源。在
任何情况下都应该进行重置,因为不进行重置可能会导致有问题的行为。 Spring 的“ThreadLocal”支持为你实现了这一点,并且应该始终考虑使用“ThreadLocal”实例,而不使用其他适当的处理代码。| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 6.10.定义新的建议类型 Spring AOP 被设计为可扩展的。虽然拦截实现策略目前在内部使用,但除了围绕建议、抛出建议之前、返回建议之后的拦截之外,还可能支持任意的建议类型。 `org.springframework.aop.framework.adapter`包是一个 SPI 包,它允许在不改变核心框架的情况下添加对新的自定义建议类型的支持。对自定义`Advice`类型的唯一限制是,它必须实现 `org.aopalliance. AOP.advice` 标记接口。 有关更多信息,请参见[`org.springframework.aop.framework.adapter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/framework/adapter/package-frame.html)Javadoc。 ## 7. 零安全 虽然 Java 不允许你用它的类型系统来表示空安全性,但是 Spring 框架现在在`org.springframework.lang`包中提供了以下注释,允许你声明 API 和字段的空性: * [`@Nullable`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/Nullable.html):表示特定参数、返回值或字段可以是`null`的注释。 * [`@NonNull`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNull.html):表示特定参数、返回值或字段不能`null`的注释(对于参数/返回值和分别应用`@NonNullApi`和`@NonNullFields`的字段不需要)。 * [`@NonNullApi`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNullApi.html):在包级别的注释,声明非 null 作为参数和返回值的默认语义。 * [@nonnullfields’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNullFields.html):在包级别的注释,声明非 null 作为字段的默认语义。 Spring 框架本身利用了这些注释,但它们也可以在任何基于 Spring 的 Java 项目中用于声明空安全的 API 和可选的空安全字段。泛型类型参数、varargs 和数组元素的可空性目前还不受支持,但应该在即将发布的版本中得到支持,有关最新信息,请参见[SPR-15942](https://jira.spring.io/browse/SPR-15942)。预计在 Spring 框架版本(包括较小的版本)之间会对可否定性声明进行微调。在方法体中使用的类型的可空性不在此特性的范围内。 | |诸如 Reactor 和 Spring Data 之类的其他公共库提供了空安全 API,
使用类似的空性安排,为
Spring 应用程序开发人员提供了一致的整体体验。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ### 7.1.用例 除了为 Spring Framework API nullability 提供显式声明外,IDE 还可以使用这些注释(例如 Idea 或 Eclipse)来提供与空安全相关的有用的警告,以便在运行时避免`NullPointerException`。 它们还用于在 Kotlin 项目中使 Spring API 为空安全的,因为 Kotlin 原生地支持[null-safety](https://kotlinlang.org/docs/reference/null-safety.html)。更多详细信息请参见[Kotlin support documentation](languages.html#kotlin-null-safety)。 ### 7.2.JSR-305 元注释 Spring 注释是用[JSR 305](https://jcp.org/en/jsr/detail?id=305)注释(一种休眠但广泛传播的 JSR)进行元注释的。JSR-305 元注释允许 Idea 或 Kotlin 之类的工具供应商以通用的方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。 为了利用 Spring 空安全 API,没有必要也不建议向项目 Classpath 添加 JSR-305 依赖项。只有基于 Spring 的库等在其代码库中使用空安全注释的项目才应该添加`com.google.code.findbugs:jsr305:3.0.2`带有`compileOnly` Gradle 配置或 Maven `provided`作用域,以避免编译警告。 ## 8. 数据缓冲区和编解码器 Java 蔚来提供了`ByteBuffer`,但是许多库在上面构建自己的字节缓冲区 API,特别是在网络操作中,重用缓冲区和/或使用直接缓冲区有利于性能。例如,Netty 具有`ByteBuf`层次结构, Undertow 使用 XNIO, Jetty 使用带有要释放的回调的池字节缓冲区,以此类推。`spring-core`模块提供了一组用于处理各种字节缓冲区 API 的抽象,如下所示: * [“数据库工厂”](#databuffers-factory)抽象了数据缓冲区的创建。 * [`DataBuffer`](#databuffers-buffer)表示一个字节缓冲区,它可能是[pooled](#databuffers-buffer-pooled)。 * [` 数据库’](#databuffers-utils)提供了用于数据缓冲区的实用方法。 * [Codecs](#codecs)将数据缓冲流解码或编码到更高级别的对象中。 ### 8.1.`DataBufferFactory` `DataBufferFactory`用于以以下两种方式之一创建数据缓冲区: 1. 分配一个新的数据缓冲区,如果已知,可以选择预先指定容量,这是更有效的,即使`DataBuffer`的实现可以按需增长和收缩。 2. 包装现有的`byte[]`或`java.nio.ByteBuffer`,它使用`DataBuffer`实现来装饰给定数据,并且不涉及分配。 注意,WebFlux 应用程序不会直接创建`DataBufferFactory`,而是通过客户端的`ServerHttpResponse`或`ClientHttpRequest`访问它。工厂的类型取决于底层客户机或服务器,例如,对于反应堆网络,“NettyDatabufferFactory”,对于其他网络,`DefaultDataBufferFactory`。 ### 8.2.`DataBuffer` `DataBuffer`接口提供了与`java.nio.ByteBuffer`类似的操作,但也带来了一些额外的好处,其中一些是受 netty`ByteBuf`的启发。以下是部分福利清单: * 以独立的位置读写,即不需要调用`flip()`来交替读写。 * 容量随需求增加,如`java.lang.StringBuilder`。 * 通过[“PooledDatabuffer”](#databuffers-buffer-pooled)池缓冲区和引用计数。 * 将缓冲区查看为`java.nio.ByteBuffer`,`InputStream`,或`OutputStream`。 * 确定给定字节的索引或最后一个索引。 ### 8.3.`PooledDataBuffer` 正如在[ByteBuffer](https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html)的 Javadoc 中所解释的,字节缓冲区可以是直接的,也可以是非直接的。直接缓冲区可以驻留在 Java 堆之外,从而消除了对本机 I/O 操作的复制需求。这使得直接缓冲区在通过套接字接收和发送数据时特别有用,但它们的创建和发布也更昂贵,这就产生了池缓冲区的想法。 `PooledDataBuffer`是`DataBuffer`的扩展,它有助于引用计数,这对于字节缓冲池来说是必不可少的。它是如何工作的?当分配了`PooledDataBuffer`时,引用计数为 1。调用`retain()`来增加计数,而调用`release()`来减少计数。只要计数大于 0,缓冲区就保证不会被释放。当计数减少到 0 时,可以释放池中的缓冲区,这实际上可能意味着将为缓冲区保留的内存返回到内存池。 请注意,在大多数情况下,与其直接在`PooledDataBuffer`上进行操作,不如使用`DataBufferUtils`中的便利方法,这些方法仅在`PooledDataBuffer`的实例的情况下才将释放或保留应用于 `databuffer’。 ### 8.4.`DataBufferUtils` `DataBufferUtils`提供了许多对数据缓冲区进行操作的实用方法: * 如果底层的 Byte Buffer API 支持,那么将数据缓冲流连接到一个可能没有复制的缓冲区中,例如通过复合缓冲区。 * 将`InputStream`或蔚来`Channel`转换为`Flux`,反之亦然,将 publisher转换为`OutputStream`或蔚来`Channel`。 * 如果缓冲区是“PooledDatabuffer”的实例,则释放或保留`DataBuffer`的方法。 * 从一个字节流中跳过或获取,直到一个特定的字节计数。 ### 8.5.编解码器 `org.springframework.core.codec`包提供了以下策略接口: * `Encoder`将`Publisher`编码到数据缓冲流中。 * `Decoder`将`Publisher`解码为更高级别的对象流。 `spring-core`模块提供`byte[]`、`ByteBuffer`、`DataBuffer`、`Resource`和 `string’编码器和解码器实现。`spring-web`模块添加了 JacksonJSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器。参见 WebFlux 部分中的[Codecs](web-reactive.html#webflux-codecs)。 ### 8.6.使用`DataBuffer` 在使用数据缓冲区时,必须特别注意确保缓冲区被释放,因为它们可能是[pooled](#databuffers-buffer-pooled)。我们将使用编解码器来说明这是如何工作的,但这些概念更普遍地适用。让我们来看看编解码器内部必须做什么来管理数据缓冲区。 a`Decoder`是在创建更高级别的对象之前最后读取输入数据缓冲区的方法,因此它必须按以下方式释放它们: 1. 如果`Decoder`只读取每个输入缓冲区并准备立即释放它,则可以通过`DataBufferUtils.release(dataBuffer)`执行。 2. 如果`Decoder`使用`Flux`或`Mono`运算符,如`flatMap`,`reduce`,以及其他在内部预取和缓存数据项的运算符,或使用诸如 `filter’,`skip`等运算符的运算符,则使用 `doondiscard(pooleddatuffer.class,必须将 databufferutils::release)添加到合成链中,以确保此类缓冲区在被丢弃之前被释放,也可能是由于错误或取消信号的结果。 3. 如果`Decoder`以任何其他方式保持一个或多个数据缓冲区,则必须确保它们在完全读取时被释放,或者在缓存的数据缓冲区被读取和释放之前发生错误或取消信号的情况下被释放。 请注意,`DataBufferUtils#join`提供了一种安全有效的方法,可以将数据缓冲流聚合到单个数据缓冲区中。同样,`skipUntilByteCount`和“takeuntilbytecount”也是供解码器使用的额外安全方法。 `Encoder`分配其他人必须读取(并释放)的数据缓冲区。所以`Encoder`不需要做太多的事情。但是,如果在用数据填充缓冲区时出现序列化错误,`Encoder`必须注意释放数据缓冲区。例如: Java ``` DataBuffer buffer = factory.allocateBuffer(); boolean release = true; try { // serialize and populate buffer.. release = false; } finally { if (release) { DataBufferUtils.release(buffer); } } return buffer; ``` Kotlin ``` val buffer = factory.allocateBuffer() var release = true try { // serialize and populate buffer.. release = false } finally { if (release) { DataBufferUtils.release(buffer) } } return buffer ``` `Encoder`的使用者负责释放它接收到的数据缓冲区。在 WebFlux 应用程序中,`Encoder`的输出用于写到 HTTP 服务器的响应,或写到客户端的 HTTP 请求,在这种情况下,释放数据缓冲区是负责将代码写到服务器响应,或写到客户端请求。 请注意,在 Netty 上运行时,有[缓冲区泄漏故障排除](https://github.com/netty/netty/wiki/Reference-counted-objects#troubleshooting-buffer-leaks)的调试选项。 ## 9. 伐木 自 Spring Framework5.0 以来, Spring 自带了在`spring-jcl`模块中实现的 Commons 日志记录桥。该实现检查 Classpath 中是否存在日志 4j2.x API 和 SLF4j1.7API,并使用其中发现的第一个作为日志实现,如果 log4j2.x 和 SLF4j 都不可用,则返回到 Java 平台的核心日志记录工具(也称为*JUL*或`java.util.logging`)。 在 Classpath 中放置 log4j2.x 或 logback(或另一个 SLF4j 提供程序),不需要任何额外的桥接器,并让框架自动适应你的选择。有关更多信息,请参见[Spring Boot Logging Reference Documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging)。 | |Spring 的 Commons 日志记录变体仅用于核心框架和扩展中的基础设施日志记录
目的。

对于应用程序代码中的日志记录需求,请直接使用 log4j2.x、SLF4j 或 jul。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 可以通过`org.apache.commons.logging.LogFactory`检索`Log`实现,如下面的示例所示。 爪哇 ``` public class MyBean { private final Log log = LogFactory.getLog(getClass()); // ... } ``` Kotlin ``` class MyBean { private val log = LogFactory.getLog(javaClass) // ... } ``` ## 10. 附录 ### 10.1.XML 模式 附录的这一部分列出了与核心容器相关的 XML 模式。 #### 10.1.1.`util`模式 顾名思义,`util`标记处理常见的实用程序配置问题,例如配置集合、引用常量等等。要在`util`模式中使用标记,你需要在 Spring XML 配置文件的顶部具有以下前导符(代码片段中的文本引用了正确的模式,以便你可以使用`util`命名空间中的标记): ``` ``` ##### 使用`` 考虑以下 Bean 定义: ``` ``` 前面的配置使用 Spring `FactoryBean`实现(“fieldretrievingFactoryBean”)将 Bean 上的`isolation`属性的值设置为`java.sql.Connection.TRANSACTION_SERIALIZABLE`常量的值。这一切都很好,但它很冗长,(不必要地)向最终用户暴露了 Spring 的内部管道。 以下基于 XML 模式的版本更简洁,清楚地表达了开发人员的意图(“注入这个常量值”),并且读起来更好: ``` ``` ###### 从字段值设置 Bean 属性或构造函数参数 [“fieldretrievingfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html)是一个`FactoryBean`,用于检索`static`或非静态字段的值。它通常用于检索`public``static`常量,然后可用于为另一个 Bean 设置属性值或构造函数参数。 下面的示例展示了如何使用`static`)属性公开[`staticField`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)字段: ``` ``` 还有一种方便用法表单,其中`static`字段被指定为 Bean 名称,如下例所示: ``` ``` 这确实意味着,在 Bean `id`中不再有任何选择(因此,任何其他涉及它的 Bean 也必须使用这个更长的名称),但是这种形式的定义非常简洁,并且非常方便地用作内部 Bean,因为`id`不必为 Bean 引用指定,如下面的示例所示: ``` ``` 你还可以访问另一个 Bean 的非静态(实例)字段,如[“fieldretrievingfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html)类的 API 文档中所述。 在 Spring 中,很容易将枚举值作为属性或构造函数参数注入到 bean 中。实际上,你不需要做任何事情,也不需要知道任何关于 Spring 内部(或者甚至关于类,例如`FieldRetrievingFactoryBean`)的事情。下面的例举枚举显示了注入一个枚举值是多么容易: 爪哇 ``` package javax.persistence; public enum PersistenceContextType { TRANSACTION, EXTENDED } ``` Kotlin ``` package javax.persistence enum class PersistenceContextType { TRANSACTION, EXTENDED } ``` 现在考虑下面的类型`PersistenceContextType`的 setter 和相应的 Bean 定义: 爪哇 ``` package example; public class Client { private PersistenceContextType persistenceContextType; public void setPersistenceContextType(PersistenceContextType type) { this.persistenceContextType = type; } } ``` Kotlin ``` package example class Client { lateinit var persistenceContextType: PersistenceContextType } ``` ``` ``` ##### 使用`` 考虑以下示例: ``` ``` 前面的配置使用 Spring `FactoryBean`实现(’PropertyPathFactoryBean’)来创建 Bean(类型为`int`),该 Bean(类型为`testBean.age`),其值等于`testBean` Bean 的`age`属性。 现在考虑下面的示例,它添加了``元素: ``` ``` ``元素的`path`属性的值遵循’beanname.beanproperty’的形式。在这种情况下,它获取名为 `TestBean’的 Bean 的`age`属性。`age`属性的值是`10`。 ###### 使用``设置 Bean 属性或构造函数参数 ##### `PropertyPathFactoryBean`是一个`FactoryBean`,它计算给定目标对象上的属性路径。目标对象可以直接指定,也可以通过 Bean 名称指定。然后,你可以在另一个 Bean 定义中使用该值作为属性值或构造函数参数。 下面的示例按名称显示了针对另一个 Bean 使用的路径: ``` ``` 在下面的示例中,根据内部 Bean 对路径进行求值: ``` ``` 还有一种快捷方式,其中 Bean 名称是属性路径。下面的示例显示了快捷方式: ``` ``` 这种形式确实意味着在 Bean 的名称中没有选择。对它的任何引用也必须使用相同的`id`,这是路径。如果用作内部 Bean,则根本不需要引用它,如下例所示: ``` ``` 你可以在实际定义中专门设置结果类型。对于大多数用例来说,这并不是必需的,但它有时是有用的。有关此功能的更多信息,请参见 爪哇doc。 ##### 使用`` 考虑以下示例: ``` ``` 前面的配置使用 Spring `FactoryBean`实现(“propertiesFactoryBean”)实例化一个`java.util.Properties`实例,该实例的值来自提供的[`Resource`](#resources)位置)。 下面的示例使用`util:properties`元素来进行更简洁的表示: ``` ``` ##### 使用`` 考虑以下示例: ``` [email protected] [email protected] [email protected] [email protected] ``` 前面的配置使用 Spring `FactoryBean`实现(“listFactoryBean”)来创建`java.util.List`实例,并使用从提供的`sourceList`中获取的值对其进行初始化。 下面的示例使用``元素来进行更简洁的表示: ``` [email protected] [email protected] [email protected] [email protected] ``` 还可以使用``元素上的`list-class`属性显式地控制实例化和填充的`List`的确切类型。例如,如果我们确实需要一个`java.util.LinkedList`来实例化,我们可以使用以下配置: ``` [email protected] [email protected] [email protected] d'[email protected] ``` 如果没有提供`list-class`属性,则容器选择一个`List`实现。 ##### 使用`` 考虑以下示例: ``` ``` 前面的配置使用 Spring `FactoryBean`实现来创建`java.util.Map`实例,该实例是用从提供的`'sourceMap'`中获取的键值对初始化的。 下面的示例使用``元素来进行更简洁的表示: ``` ``` 还可以使用``元素上的`'map-class'`属性显式地控制实例化和填充的`Map`的确切类型。例如,如果我们确实需要一个`java.util.TreeMap`来实例化,我们可以使用以下配置: ``` ``` 如果没有提供`'map-class'`属性,则容器选择一个`Map`实现。 ##### 使用`` 考虑以下示例: ``` [email protected] [email protected] [email protected] [email protected] ``` 前面的配置使用 Spring `FactoryBean`实现(“setFactoryBean”)创建`java.util.Set`实例,该实例初始化后的值取自所提供的`sourceSet`。 下面的示例使用``元素来进行更简洁的表示: ``` [email protected] [email protected] [email protected] [email protected] ``` 还可以使用``元素上的`set-class`属性显式地控制实例化和填充的`Set`的确切类型。例如,如果我们确实需要一个`java.util.TreeSet`来实例化,我们可以使用以下配置: ``` [email protected] [email protected] [email protected] [email protected] ``` 如果没有提供`set-class`属性,则容器选择一个`Set`实现。 #### 10.1.2.`aop`模式 `aop`标记用于配置 Spring 中的所有内容,包括 Spring 自己的基于代理的 AOP 框架和 Spring 与 AspectJ AOP 框架的集成。这些标签在标题为[Aspect Oriented Programming with Spring](#aop)的一章中得到了全面的介绍。 为了完整起见,要使用`aop`模式中的标记,你需要在 Spring XML 配置文件的顶部具有以下前导符(代码片段中的文本引用了正确的模式,以便你可以使用`aop`名称空间中的标记): ``` ``` #### 10.1.3.`context`模式 `context`标记处理的是与管道相关的`ApplicationContext`配置——也就是说,通常不是对最终用户很重要的 bean,而是在 Spring 中执行许多“grunt”工作的 bean,例如`BeanfactoryPostProcessors`。下面的代码片段引用了正确的模式,因此`context`名称空间中的元素对你是可用的: ``` ``` ##### 使用`` 此元素激活替换`${…​}`占位符,这些占位符是根据指定的属性文件解析的(如[Spring resource location](#resources))。这个元素是一个方便的机制,它为你设置了[`PropertySourcesPlaceHolderConfigurer’](#beans-factory-placeholderconfigurer)。如果你需要更多地控制特定的“PropertySourcesPlaceHolderConfigurer”设置,那么你可以自己将其明确定义为 Bean。 ##### 使用`` 此元素激活 Spring 基础结构以检测 Bean 类中的注释: * Spring 的[@configuration](#beans-factory-metadata)模型 * [“@autowired”/“@inject”](#beans-annotation-config),`@Value`,和`@Lookup` * JSR-250 的`@Resource`,`@PostConstruct`,和`@PreDestroy`(如果有) * JAX-WS 的`@WebServiceRef`和 EJB3 的`@EJB`(如果可用) * JPA 的`@PersistenceContext`和`@PersistenceUnit`(如果有) * Spring 的[@eventlistener](#context-functionality-events-annotation) 或者,你可以选择显式地为这些注释激活单独的`BeanPostProcessors`。 | |此元素不激活 Spring 的[@transactional`](data-access.html#transaction-declarative-annotations)注释的处理;
你可以为此目的使用[``](data-access.html#tx-decl-explained)元素。类似地, Spring 的[缓存注释](integration.html#cache-annotations)也需要显式地[enabled](integration.html#cache-annotation-enable)。| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ##### 使用`` 这个元素在[基于注释的容器配置](#beans-annotation-config)一节中有详细介绍。 ##### 使用`` 这个元素在[load-time weaving with AspectJ in the Spring Framework](#aop-aj-ltw)一节中有详细介绍。 ##### 使用`` 这个元素在[using AspectJ to dependency inject domain objects with Spring](#aop-atconfigurable)一节中有详细介绍。 ##### 使用`` 这个元素在[配置基于注释的 MBean 导出](integration.html#jmx-context-mbeanexport)一节中有详细介绍。 #### 10.1.4.Beans 模式 最后但并非最不重要的是,我们有`beans`模式中的元素。这些要素自该框架诞生之日起就存在于 Spring 中。这里没有显示`beans`模式中的各种元素的示例,因为它们在[详细介绍依赖关系和配置](#beans-factory-properties-detailed)(实际上,在整个[chapter](#beans)中)中得到了相当全面的覆盖。 请注意,你可以向``XML 定义添加零个或多个键值对。使用这个额外的元数据所做的事情完全取决于你自己的自定义逻辑(因此,通常只有当你编写自己的自定义元素时才会使用,该自定义元素在标题为[XML 模式创作](#xml-custom)的附录中进行了描述)。 下面的示例在周围的``的上下文中显示了``元素(请注意,如果没有任何逻辑来解释它,元数据实际上就没有用了)。 ``` (1) ``` |**1**|这就是`meta`元素的示例| |-----|----------------------------------| 在前面的示例中,你可以假设有一些逻辑使用 Bean 定义,并设置了一些使用所提供的元数据的缓存基础设施。 ### 10.2.XML 模式创作 自版本 2.0 以来, Spring 已经提供了一种机制,用于在用于定义和配置 bean 的基本 Spring XML 格式中添加基于模式的扩展。本节介绍如何编写你自己的定制 XML Bean 定义解析器,并将这些解析器集成到 Spring IOC 容器中。 Spring 的可扩展 XML 配置机制是基于 XML 模式的,为了便于编写使用模式感知 XML 编辑器的配置文件。如果你不熟悉 Spring 标准 Spring 发行版附带的当前 XML 配置扩展,那么你应该首先阅读关于[XML Schemas](#xsd-schemas)的上一节。 要创建新的 XML 配置扩展: 1. [Author](#xsd-custom-schema)描述自定义元素的 XML 模式。 2. [Code](#xsd-custom-namespacehandler)一个自定义的`NamespaceHandler`实现。 3. [Code](#xsd-custom-parser)一个或多个`BeanDefinitionParser`实现(这是完成实际工作的地方)。 4. [Register](#xsd-custom-registration)带有 Spring 的新工件。 对于一个统一的示例,我们创建了一个 XML 扩展(一个自定义 XML 元素),它允许我们配置类型为“SimpleDateFormat”的对象(来自`java.text`包)。完成后,我们将能够如下定义 Bean 类型`SimpleDateFormat`的定义: ``` ``` (我们将在本附录后面列出更详细的例子。第一个简单示例的目的是让你了解制作自定义扩展的基本步骤。) #### 10.2.1.创建模式 创建用于 Spring 的 IOC 容器的 XML 配置扩展,首先要创建一个 XML 模式来描述该扩展。对于我们的示例,我们使用以下模式来配置`SimpleDateFormat`对象: ``` (1) ``` |**1**|所指示的行包含所有可识别标记
的扩展基础(这意味着它们具有`id`属性,我们可以将其用作
容器中的 Bean 标识符)。我们可以使用这个属性,因为我们导入了 Spring 提供的“beans”命名空间。| |-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 前面的模式允许我们使用``元素在 XML 应用程序上下文文件中直接配置`SimpleDateFormat`对象,如下例所示: ``` ``` 请注意,在我们创建了基础结构类之后,前面的 XML 片段与下面的 XML 片段本质上是相同的: ``` ``` 前面两个片段中的第二个片段在容器中创建了一个 Bean(由类型为 `SimpleDateFormat’的名称`dateFormat`标识),并设置了几个属性。 | |基于模式的创建配置格式的方法允许与具有模式感知 XML 编辑器的 IDE 紧密集成
。通过使用适当编写的模式,
可以使用自动补全功能,让用户在枚举中定义的几个配置选项
之间进行选择。| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| #### 10.2.2.编码 a`NamespaceHandler` 除了模式之外,我们还需要一个`NamespaceHandler`来解析 Spring 在解析配置文件时遇到的这个特定名称空间的所有元素。对于这个示例,“namespaceHandler”应该负责解析`myns:dateformat`元素。 `NamespaceHandler`接口具有三种方法: * `init()`:允许初始化`NamespaceHandler`,并在使用处理程序之前由 Spring 调用。 * `BeanDefinition parse(Element, ParserContext)`:当 Spring 遇到顶级元素(不嵌套在 Bean 定义或不同的名称空间中)时调用。该方法本身可以注册 Bean 定义,返回 Bean 定义,或者两者兼而有之。 * `BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext)`:当 Spring 遇到不同名称空间的属性或嵌套元素时调用。 Bean 定义的一个或多个装饰(例如)与[scopes that Spring supports](#beans-factory-scopes)一起使用。我们首先突出显示一个简单的示例,而不使用装饰,然后我们在一个更高级的示例中显示装饰。 尽管你可以为整个命名空间编写自己的`NamespaceHandler`代码(因此提供了解析命名空间中每个元素的代码),通常情况下, Spring XML 配置文件中的每个顶级 XML 元素都会导致一个 Bean 定义(就像我们的情况一样,单个``元素会导致一个`SimpleDateFormat` Bean 定义)。 Spring 支持此场景的许多方便类的特征。在下面的示例中,我们使用`NamespaceHandlerSupport`类: 爪哇 ``` package org.springframework.samples.xml; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class MyNamespaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser()); } } ``` Kotlin ``` package org.springframework.samples.xml import org.springframework.beans.factory.xml.NamespaceHandlerSupport class MyNamespaceHandler : NamespaceHandlerSupport { override fun init() { registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser()) } } ``` 你可能会注意到,在这个类中实际上并没有大量的解析逻辑。的确,`NamespaceHandlerSupport`类有一个内置的委托概念。它支持注册任意数量的`BeanDefinitionParser`实例,当需要解析名称空间中的元素时,它将委托给这些实例。这种对关注点的清晰分离使`NamespaceHandler`能够处理其名称空间中所有自定义元素的解析的编排,同时将任务委托给`BeanDefinitionParsers`来执行 XML 解析的繁重工作。这意味着每个`BeanDefinitionParser`只包含用于解析单个自定义元素的逻辑,正如我们在下一步中所看到的那样。 #### 10.2.3.使用`BeanDefinitionParser` 如果`BeanDefinitionParser`遇到已映射到特定 Bean 定义解析器(本例中为 `DateFormat’)的类型的 XML 元素,则使用`BeanDefinitionParser`。换句话说,`BeanDefinitionParser`负责解析模式中定义的一个不同的顶级 XML 元素。在解析器中,我们可以访问 XML 元素(因此也可以访问它的子元素),这样我们就可以解析自定义的 XML 内容,正如你在下面的示例中所看到的那样: 爪哇 ``` package org.springframework.samples.xml; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; import org.w3c.dom.Element; import java.text.SimpleDateFormat; public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1) protected Class getBeanClass(Element element) { return SimpleDateFormat.class; (2) } protected void doParse(Element element, BeanDefinitionBuilder bean) { // this will never be null since the schema explicitly requires that a value be supplied String pattern = element.getAttribute("pattern"); bean.addConstructorArgValue(pattern); // this however is an optional property String lenient = element.getAttribute("lenient"); if (StringUtils.hasText(lenient)) { bean.addPropertyValue("lenient", Boolean.valueOf(lenient)); } } } ``` |**1**|我们使用 Spring 提供的`AbstractSingleBeanDefinitionParser`来处理大量
创建单个`BeanDefinition`的基本繁重工作。| |-----|--------------------------------------------------------------------------------------------------------------------------------------------------| |**2**|我们为`AbstractSingleBeanDefinitionParser`超类提供了我们的
单变量`BeanDefinition`所表示的类型。| Kotlin ``` package org.springframework.samples.xml import org.springframework.beans.factory.support.BeanDefinitionBuilder import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser import org.springframework.util.StringUtils import org.w3c.dom.Element import java.text.SimpleDateFormat class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1) override fun getBeanClass(element: Element): Class<*>? { (2) return SimpleDateFormat::class.java } override fun doParse(element: Element, bean: BeanDefinitionBuilder) { // this will never be null since the schema explicitly requires that a value be supplied val pattern = element.getAttribute("pattern") bean.addConstructorArgValue(pattern) // this however is an optional property val lenient = element.getAttribute("lenient") if (StringUtils.hasText(lenient)) { bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient)) } } } ``` |**1**|我们使用 Spring 提供的`AbstractSingleBeanDefinitionParser`来处理大量
创建单个`BeanDefinition`的基本繁重工作。| |-----|--------------------------------------------------------------------------------------------------------------------------------------------------| |**2**|我们为`AbstractSingleBeanDefinitionParser`超类提供了我们的
单变量`BeanDefinition`所表示的类型。| 在这个简单的例子中,这就是我们需要做的所有事情。我们的“BeanDefinition”的创建由`AbstractSingleBeanDefinitionParser`超类处理, Bean 定义的唯一标识符的提取和设置也是如此。 #### 10.2.4.注册处理程序和模式 编码完成了。剩下要做的就是使 Spring XML 解析基础结构了解我们的自定义元素。我们通过在两个特殊用途的属性文件中注册自定义的“NamespaceHandler”和自定义的 XSD 文件来实现这一目的。这些属性文件都放置在应用程序中的`META-INF`目录中,并且可以与 jar 文件中的二进制类一起分发。 Spring XML 解析基础结构通过使用这些特殊的属性文件自动获取你的新扩展名,其格式将在接下来的两节中详细介绍。 ##### 写作`META-INF/spring.handlers` 名为`spring.handlers`的属性文件包含 XML 模式 URI 到名称空间处理程序类的映射。对于我们的示例,我们需要编写以下内容: ``` http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler ``` (`:`字符在 爪哇 Properties 格式中是一个有效的分隔符,因此需要使用反斜杠转义 URI 中的 `:’字符。 键-值对的第一部分(键)是与自定义名称空间扩展关联的 URI,并且需要完全匹配`targetNamespace`属性的值,如在自定义 XSD 模式中所指定的那样。 ##### 撰写“meta-inf/ Spring.schemas” 名为`spring.schemas`的属性文件包含 XML 模式位置(与模式声明一起,在使用模式作为`xsi:schemaLocation`属性的一部分的 XML 文件中)到 Classpath 资源的映射。需要此文件以防止 Spring 绝对必须使用默认的`EntityResolver`,该默认`EntityResolver`需要互联网访问才能检索模式文件。如果在此属性文件中指定了映射, Spring 将在 Classpath 上搜索模式(在本例中,在`org.springframework.samples.xml`包中搜索 `myns.XSD`)。下面的代码片段显示了我们需要为自定义模式添加的行: ``` http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd ``` (请记住,`:`字符必须转义。 鼓励你在 Classpath 上的`NamespaceHandler`和`BeanDefinitionParser`类旁边部署你的 XSD 文件(或多个文件)。 #### 10.2.5.在 Spring XML 配置中使用自定义扩展 使用你自己实现的自定义扩展与使用 Spring 提供的“自定义”扩展之一没有什么不同。下面的示例在 Spring XML 配置文件中使用了在前面的步骤中开发的自定义``元素: ``` (1) ``` |**1**|我们的习惯 Bean。| |-----|----------------| #### 10.2.6.更详细的例子 本节介绍了定制 XML 扩展的一些更详细的示例。 ##### 在自定义元素中嵌套自定义元素 本节中展示的示例展示了如何编写满足以下配置的目标所需的各种工件: ``` ``` 前面的配置将自定义扩展嵌套在彼此内部。由``元素实际配置的类是`Component`类(如下一个示例所示)。请注意`Component`类不会公开`components`属性的 setter 方法。这使得使用 setter injection 为`Component`类配置 Bean 定义变得困难(或者说是不可能的)。下面的清单显示了`Component`类: 爪哇 ``` package com.foo; import java.util.ArrayList; import java.util.List; public class Component { private String name; private List components = new ArrayList (); // mmm, there is no setter method for the 'components' public void addComponent(Component component) { this.components.add(component); } public List getComponents() { return components; } public String getName() { return name; } public void setName(String name) { this.name = name; } } ``` Kotlin ``` package com.foo import java.util.ArrayList class Component { var name: String? = null private val components = ArrayList() // mmm, there is no setter method for the 'components' fun addComponent(component: Component) { this.components.add(component) } fun getComponents(): List { return components } } ``` 这个问题的典型解决方案是创建一个自定义的`FactoryBean`,它为`components`属性公开一个 setter 属性。下面的清单显示了这样一个自定义的“FactoryBean”: 爪哇 ``` package com.foo; import org.springframework.beans.factory.FactoryBean; import java.util.List; public class ComponentFactoryBean implements FactoryBean { private Component parent; private List children; public void setParent(Component parent) { this.parent = parent; } public void setChildren(List children) { this.children = children; } public Component getObject() throws Exception { if (this.children != null && this.children.size() > 0) { for (Component child : children) { this.parent.addComponent(child); } } return this.parent; } public Class getObjectType() { return Component.class; } public boolean isSingleton() { return true; } } ``` Kotlin ``` package com.foo import org.springframework.beans.factory.FactoryBean import org.springframework.stereotype.Component class ComponentFactoryBean : FactoryBean { private var parent: Component? = null private var children: List? = null fun setParent(parent: Component) { this.parent = parent } fun setChildren(children: List) { this.children = children } override fun getObject(): Component? { if (this.children != null && this.children!!.isNotEmpty()) { for (child in children!!) { this.parent!!.addComponent(child) } } return this.parent } override fun getObjectType(): Class? { return Component::class.java } override fun isSingleton(): Boolean { return true } } ``` 这很好地工作,但它向最终用户暴露了大量的管道系统。我们要做的是编写一个自定义扩展,以隐藏所有这些管道。如果我们坚持使用[前面描述的步骤](#xsd-custom-introduction),那么我们首先创建 XSD 模式来定义自定义标记的结构,如下面的清单所示: ``` ``` 同样在[前面描述的过程](#xsd-custom-introduction)之后,我们将创建一个自定义`NamespaceHandler`: 爪哇 ``` package com.foo; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class ComponentNamespaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser()); } } ``` Kotlin ``` package com.foo import org.springframework.beans.factory.xml.NamespaceHandlerSupport class ComponentNamespaceHandler : NamespaceHandlerSupport() { override fun init() { registerBeanDefinitionParser("component", ComponentBeanDefinitionParser()) } } ``` 接下来是自定义`BeanDefinitionParser`。请记住,我们正在创建一个`BeanDefinition`来描述`ComponentFactoryBean`。下面的清单显示了我们的自定义`BeanDefinitionParser`实现: 爪哇 ``` package com.foo; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; import java.util.List; public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser { protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { return parseComponentElement(element); } private static AbstractBeanDefinition parseComponentElement(Element element) { BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class); factory.addPropertyValue("parent", parseComponent(element)); List childElements = DomUtils.getChildElementsByTagName(element, "component"); if (childElements != null && childElements.size() > 0) { parseChildComponents(childElements, factory); } return factory.getBeanDefinition(); } private static BeanDefinition parseComponent(Element element) { BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class); component.addPropertyValue("name", element.getAttribute("name")); return component.getBeanDefinition(); } private static void parseChildComponents(List childElements, BeanDefinitionBuilder factory) { ManagedList children = new ManagedList(childElements.size()); for (Element element : childElements) { children.add(parseComponentElement(element)); } factory.addPropertyValue("children", children); } } ``` Kotlin ``` package com.foo import org.springframework.beans.factory.config.BeanDefinition import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.beans.factory.support.BeanDefinitionBuilder import org.springframework.beans.factory.support.ManagedList import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser import org.springframework.beans.factory.xml.ParserContext import org.springframework.util.xml.DomUtils import org.w3c.dom.Element import java.util.List class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() { override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? { return parseComponentElement(element) } private fun parseComponentElement(element: Element): AbstractBeanDefinition { val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java) factory.addPropertyValue("parent", parseComponent(element)) val childElements = DomUtils.getChildElementsByTagName(element, "component") if (childElements != null && childElements.size > 0) { parseChildComponents(childElements, factory) } return factory.getBeanDefinition() } private fun parseComponent(element: Element): BeanDefinition { val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java) component.addPropertyValue("name", element.getAttribute("name")) return component.beanDefinition } private fun parseChildComponents(childElements: List, factory: BeanDefinitionBuilder) { val children = ManagedList(childElements.size) for (element in childElements) { children.add(parseComponentElement(element)) } factory.addPropertyValue("children", children) } } ``` 最后,需要通过修改`META-INF/spring.handlers`和`META-INF/spring.schemas`文件,将各种工件注册到 Spring XML 基础结构中,如下所示: ``` # in 'META-INF/spring.handlers' http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler ``` ``` # in 'META-INF/spring.schemas' http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd ``` ##### “正常”元素上的自定义属性 编写自己的自定义解析器和相关的工件并不难。然而,这有时并不是正确的做法。考虑这样一个场景,你需要将元数据添加到已经存在的 Bean 定义中。在这种情况下,你当然不希望不得不编写自己的整个自定义扩展。相反,你只是希望向现有的 Bean 定义元素添加一个附加属性。 通过另一个示例,假设你为访问集群[JCache](https://jcp.org/en/jsr/detail?id=107)的服务对象定义了 Bean 定义,并且你希望确保命名的 JCache 实例在周围的集群中急切地启动。下面的清单显示了这样的定义: ``` ``` 然后,我们可以在解析“jcache:cache-name”属性时创建另一个`BeanDefinition`。这`BeanDefinition`然后为我们初始化命名的 JCache。我们还可以修改“checkingAccountService”` 的现有,使其对这个新的 JCache 具有依赖性-初始化。下面的清单显示了我们的`JCacheInitializer`: 爪哇 ``` package com.foo; public class JCacheInitializer { private String name; public JCacheInitializer(String name) { this.name = name; } public void initialize() { // lots of JCache API calls to initialize the named cache... } } ``` Kotlin ``` package com.foo class JCacheInitializer(private val name: String) { fun initialize() { // lots of JCache API calls to initialize the named cache... } } ``` 现在,我们可以进入自定义扩展。首先,我们需要编写描述自定义属性的 XSD 模式,如下所示: ``` ``` 接下来,我们需要创建关联的`NamespaceHandler`,如下所示: Java ``` package com.foo; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class JCacheNamespaceHandler extends NamespaceHandlerSupport { public void init() { super.registerBeanDefinitionDecoratorForAttribute("cache-name", new JCacheInitializingBeanDefinitionDecorator()); } } ``` Kotlin ``` package com.foo import org.springframework.beans.factory.xml.NamespaceHandlerSupport class JCacheNamespaceHandler : NamespaceHandlerSupport() { override fun init() { super.registerBeanDefinitionDecoratorForAttribute("cache-name", JCacheInitializingBeanDefinitionDecorator()) } } ``` 接下来,我们需要创建解析器。请注意,在本例中,因为我们要解析一个 XML 属性,所以我们写一个`BeanDefinitionDecorator`,而不是`BeanDefinitionParser`。下面的清单显示了我们的`BeanDefinitionDecorator`实现: Java ``` package com.foo; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.BeanDefinitionDecorator; import org.springframework.beans.factory.xml.ParserContext; import org.w3c.dom.Attr; import org.w3c.dom.Node; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator { private static final String[] EMPTY_STRING_ARRAY = new String[0]; public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder, ParserContext ctx) { String initializerBeanName = registerJCacheInitializer(source, ctx); createDependencyOnJCacheInitializer(holder, initializerBeanName); return holder; } private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder, String initializerBeanName) { AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition()); String[] dependsOn = definition.getDependsOn(); if (dependsOn == null) { dependsOn = new String[]{initializerBeanName}; } else { List dependencies = new ArrayList(Arrays.asList(dependsOn)); dependencies.add(initializerBeanName); dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY); } definition.setDependsOn(dependsOn); } private String registerJCacheInitializer(Node source, ParserContext ctx) { String cacheName = ((Attr) source).getValue(); String beanName = cacheName + "-initializer"; if (!ctx.getRegistry().containsBeanDefinition(beanName)) { BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class); initializer.addConstructorArg(cacheName); ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition()); } return beanName; } } ``` Kotlin ``` package com.foo import org.springframework.beans.factory.config.BeanDefinitionHolder import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.beans.factory.support.BeanDefinitionBuilder import org.springframework.beans.factory.xml.BeanDefinitionDecorator import org.springframework.beans.factory.xml.ParserContext import org.w3c.dom.Attr import org.w3c.dom.Node import java.util.ArrayList class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator { override fun decorate(source: Node, holder: BeanDefinitionHolder, ctx: ParserContext): BeanDefinitionHolder { val initializerBeanName = registerJCacheInitializer(source, ctx) createDependencyOnJCacheInitializer(holder, initializerBeanName) return holder } private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder, initializerBeanName: String) { val definition = holder.beanDefinition as AbstractBeanDefinition var dependsOn = definition.dependsOn dependsOn = if (dependsOn == null) { arrayOf(initializerBeanName) } else { val dependencies = ArrayList(listOf(*dependsOn)) dependencies.add(initializerBeanName) dependencies.toTypedArray() } definition.setDependsOn(*dependsOn) } private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String { val cacheName = (source as Attr).value val beanName = "$cacheName-initializer" if (!ctx.registry.containsBeanDefinition(beanName)) { val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java) initializer.addConstructorArg(cacheName) ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition()) } return beanName } } ``` 最后,我们需要通过修改`META-INF/spring.handlers`和`META-INF/spring.schemas`文件,在 Spring XML 基础结构中注册各种工件,如下所示: ``` # in 'META-INF/spring.handlers' http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler ``` ``` # in 'META-INF/spring.schemas' http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd ``` ### 10.3.应用程序启动步骤 附录的这一部分列出了核心容器所使用的现有`StartupSteps`。 | |关于每个启动步骤的名称和详细信息不是公共契约的一部分,并且
可能会发生更改;这被认为是核心容器的实现细节,并且将随着
其行为的改变。| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Name |说明| Tags | |----------------------------------------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| | `spring.beans.instantiate` |Bean 及其依赖关系的实例化。|`beanName` the name of the bean, `beanType` the type required at the injection point.| | `spring.beans.smart-initialize` |初始化`SmartInitializingSingleton`bean。| `beanName` the name of the bean. | |`spring.context.annotated-bean-reader.create` |创建`AnnotatedBeanDefinitionReader`。| | | `spring.context.base-packages.scan` |扫描基本包装。| `packages` array of base packages for scanning. | | `spring.context.beans.post-process` |beans 后处理阶段。| | | `spring.context.bean-factory.post-process` |调用`BeanFactoryPostProcessor`bean。| `postProcessor` the current post-processor. | |`spring.context.beandef-registry.post-process`|调用`BeanDefinitionRegistryPostProcessor`bean。| `postProcessor` the current post-processor. | | `spring.context.component-classes.register` |通过`AnnotationConfigApplicationContext#register`注册组件类。| `classes` array of given classes for registration. | | `spring.context.config-classes.enhance` |使用 CGlib 代理增强配置类。| `classCount` count of enhanced classes. | | `spring.context.config-classes.parse` |配置类解析阶段使用`ConfigurationClassPostProcessor`。| `classCount` count of processed classes. | | `spring.context.refresh` |应用程序上下文刷新阶段。| |