参考文档的这一部分涵盖了对Spring框架绝对不可或缺的所有技术。

其中最重要的是Spring框架的控制反转(IoC)容器。在对Spring框架的IoC容器进行彻底处理之后,紧接着是对Spring的面向方面编程(AOP)技术的全面介绍。Spring框架拥有自己的AOP框架,该框架在概念上易于理解,并且成功地解决了Java企业编程中80%的AOP需求。

本文还介绍了Spring与AspectJ的集成(就特性而言,它是目前最丰富的 - ,当然也是 企业空间中最成熟的AOP实现)。

AOT处理可用于提前优化您的应用程序。它通常用于使用GraalVM的本机映像部署。

1. The IoC Container

本章介绍了Spring的控制反转(IOC)容器。

1.1. Introduction to the Spring IoC Container and Beans

本章介绍了控制反转(IoC)原则的Spring框架实现。IoC也称为依赖注入(DI)。这是一个过程,在此过程中,对象仅通过构造函数参数、工厂方法的参数或在构造对象实例或从工厂方法返回对象实例后在对象实例上设置的属性来定义其依赖项(即,它们使用的其他对象)。然后,容器在创建Bean时注入这些依赖项。这个过程基本上是Bean本身的逆(由此得名,控制反转),通过使用类的直接构造或诸如服务定位器模式之类的机制来控制其依赖项的实例化或位置。

org.springmework.Beansorg.springmework.context包是Spring框架的IoC容器的基础。BeanFactory接口提供了能够管理任何类型对象的高级配置机制。ApplicationContextBeanFactory的子接口。它补充道:

  • 更轻松地与Spring的AOP功能集成

  • 消息资源处理(用于国际化)

  • 活动发布

  • 应用层特定的上下文,如用于Web应用程序的WebApplicationContext

简而言之,BeanFactory提供了配置框架和基本功能,ApplicationContext添加了更多特定于企业的功能。ApplicationContextBeanFactory的完整超集,仅在本章描述Spring的IoC容器时使用。有关使用BeanFactory而不是ApplicationContext的更多信息,请参阅BeanFactory接口部分。

在Spring中,构成应用程序主干并由Spring IOC容器管理的对象称为Bean。Bean是由Spring IOC容器实例化、组装和管理的对象。否则,Bean只是应用程序中众多对象中的一个。Bean以及它们之间的依赖关系反映在容器使用的配置元数据中。

1.2. Container Overview

org.springframework.context.ApplicationContext接口表示Spring IOC容器,负责实例化、配置和组装Bean。容器通过读取配置元数据获取关于实例化、配置和组装哪些对象的指令。配置元数据以XML、Java注释或Java代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。

ApplicationContext接口的几个实现随Spring一起提供。在独立应用程序中,通常创建ClassPathXmlApplicationContextFileSystemXmlApplicationContext.虽然XML一直是定义配置元数据的传统格式,但您可以通过提供少量的XML配置来声明性地启用对这些附加元数据格式的支持,从而指示容器使用Java注释或代码作为元数据格式。

在大多数应用程序场景中,实例化一个或多个Spring IOC容器实例不需要显式的用户代码。例如,在Web应用程序场景中,应用程序的web.xml文件中简单的八行(大约)行样板Web描述符XML通常就足够了(请参阅Web应用程序的便捷ApplicationContext实例化)。如果您使用Spring Tools for Eclipse(一个基于Eclipse的开发环境),只需点击几下鼠标或敲击几下键盘,就可以轻松地创建这个样板配置。

下图显示了Spring如何工作的高级视图。您的应用程序类与配置元数据结合在一起,这样,在创建和初始化ApplicationContext之后,您就拥有了一个完全配置且可执行的系统或应用程序。

container magic
Figure 1. The Spring IoC container

1.2.1. Configuration Metadata

如上图所示,Spring IOC容器使用一种形式的配置元数据。该配置元数据表示作为应用程序开发人员如何告诉Spring容器实例化、配置和组装应用程序中的对象。

传统上,配置元数据是以简单直观的XML格式提供的,本章的大部分内容都使用这种格式来传达Spring IOC容器的关键概念和特性。

XML-based metadata is not the only allowed form of configuration metadata. The Spring IoC container itself is totally decoupled from the format in which this configuration metadata is actually written. These days, many developers choose Java-based configuration for their Spring applications.

有关将其他形式的元数据与Spring容器一起使用的信息,请参阅:

  • 基于注释的配置:Spring2.5引入了对基于注释的配置元数据的支持。

  • 基于Java的配置:从Spring3.0开始,由Spring JavaConfig项目提供的许多特性成为核心Spring框架的一部分。因此,您可以使用Java文件而不是XML文件在应用程序类外部定义Bean。要使用这些新功能,请参阅@configuration@Bean@Import和@DependsOn注释。

Spring配置由容器必须管理的至少一个且通常不止一个的Bean定义组成。基于XML的配置元数据将这些Bean配置为顶级<;Bean/元素中的<;Bean/元素。Java配置通常在@Configuration类中使用@Bean注释的方法。

这些Bean定义对应于组成应用程序的实际对象。通常,您定义服务层对象、数据访问对象(DAO)、表示对象(如StrutsAction实例)、基础结构对象(如HibernateSessionFaciles、JMS队列等)。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是DAO和业务逻辑的责任。但是,您可以使用Spring与AspectJ的集成来配置在IoC容器控制之外创建的对象。请参阅使用AspectJ通过Spring依赖注入域对象

以下示例显示了基于XML的配置元数据的基本结构:

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

    <bean id="..." class="..."> (1) (2)
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>
             
             
1 The id attribute is a string that identifies the individual bean definition.
2 The class attribute defines the type of the bean and uses the fully qualified classname.

id属性的值引用协作对象。本例中未显示用于引用协作对象的XML。有关详细信息,请参阅依赖项。

1.2.2. Instantiating a Container

提供给ApplicationContext构造函数的一个或多个位置路径是允许容器从各种外部资源(如本地文件系统、JavaCLASSPATH等)加载配置元数据的资源字符串。

Java
Kotlin
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

             
             

在了解了Spring的IOC容器之后,您可能想更多地了解Spring的资源抽象(如Resources中所述),它提供了一种从URI语法中定义的位置读取InputStream的便捷机制。特别是,资源路径用于构造应用程序上下文,如应用程序上下文和资源路径中所述。

以下示例显示了服务层对象(services.xml)配置文件:

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

    <!-- services -->

    <bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for services go here -->

</beans>
             
             

以下示例显示数据访问对象daos.xml文件:

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

    <bean id="accountDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>
             
             

在前面的示例中,服务层由PetStoreServiceImpl类和JpaAccount tDaoJpaItemDao类型的两个数据访问对象组成(基于JPA对象-关系映射标准)。属性名称元素引用JavaBean属性的名称,ref元素引用另一个Bean定义的名称。idref元素之间的这种链接表示协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参阅依赖项

Composing XML-based Configuration Metadata

让Bean定义跨越多个XML文件可能很有用。通常,每个单独的XML配置文件代表体系结构中的一个逻辑层或模块。

您可以使用应用程序上下文构造函数从所有这些XML片段加载Bean定义。此构造函数接受多个资源位置,如上一节所示。或者,使用一次或多次出现的<;import/>;元素从另一个或多个文件加载Bean定义。以下示例显示了如何执行此操作:

<beans>
    <import resource="services.xml"/>
    <import resource="resources/messageSource.xml"/>
    <import resource="/resources/themeSource.xml"/>

    <bean id="bean1" class="..."/>
    <bean id="bean2" class="..."/>
</beans>
              
              

在前面的示例中,外部Bean定义从三个文件加载:services.xmlMessageSource.xmlhemeSource.xml。所有位置路径都相对于执行导入的定义文件,因此services.xml必须与执行导入的文件位于相同的目录或类路径位置,而MessageSource.xmlhemeSource.xml必须位于导入文件位置下方的资源位置。如您所见,前导斜杠被忽略。然而,考虑到这些路径是相对的,最好根本不使用斜杠。根据Spring模式,要导入的文件的内容,包括顶级<;Beans/>;元素,必须是有效的XMLBean定义。

可以使用相对“../”路径引用父目录中的文件,但不建议这样做。这样做会在当前应用程序之外的文件上创建依赖项。特别是,对于classpath:URL(例如,classpath:../services.xml),不建议使用此引用,其中运行时解析过程选择“最近的”类路径根目录,然后查看其父目录。类路径配置更改可能会导致选择不同的错误目录。

您可以始终使用完全限定的资源位置,而不是相对路径:例如,文件:C:/config/services.xmlclasspath:/config/services.xml。但是,请注意,您正在将应用程序的配置耦合到特定的绝对位置。例如,通过在运行时根据 系统属性解析的“${jvm​}”占位符,为这样的绝对位置 - 保留间接性通常是更可取的。

命名空间本身提供了导入指令功能。除了普通的Bean定义之外,还有更多的配置特性可以在由SpringBean- 提供的一系列 名称空间中使用,例如上下文util名称空间。

The Groovy Bean Definition 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
        }
    }
}

              
              

这种配置风格在很大程度上等同于XMLBean定义,甚至支持Spring的XML配置命名空间。它还允许通过portBeans指令导入XML Bean定义文件。

1.2.3. Using the Container

ApplicationContext是高级工厂的接口,能够维护不同Bean及其依赖项的注册表。通过使用T getBean(字符串名称,Class<;T>;Required dType)方法,您可以检索Bean的实例。

ApplicationContext允许您读取和访问Bean定义,如下面的示例所示:

Java
Kotlin
// 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<String> userList = service.getUsernameList();

             
             

使用Groovy配置时,引导看起来非常相似。它有一个不同的上下文实现类,该实现类支持Groovy(但也理解XMLBean定义)。下面的示例显示了Groovy配置:

Java
Kotlin
ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");

             
             

最灵活的变体是GenericApplicationContext与读取器委托 - 结合使用,以及用于xml文件的XmlBeanDefinitionReader,如下面的示例所示:

Java
Kotlin
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

             
             

您还可以对Groovy文件使用GroovyBeanDefinitionReader,如下面的示例所示:

Java
Kotlin
GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();

             
             

您可以在相同的ApplicationContext上混合和匹配这些读取器委托,从不同的配置源读取Bean定义。

然后可以使用getBean检索Bean的实例。ApplicationContext接口有几个用于检索Bean的其他方法,但在理想情况下,您的应用程序代码永远不应该使用它们。实际上,您的应用程序代码根本不应该调用getBean()方法,因此完全不依赖于SpringAPI。例如,Spring与Web框架的集成为各种Web框架组件(如控制器和JSF管理的Bean)提供了依赖注入,允许您通过元数据(如自动装配注释)声明对特定Bean的依赖关系。

1.3. Bean Overview

一个Spring IOC容器管理一个或多个Bean。这些Bean是使用您提供给容器的配置元数据创建的(例如,以XML<;Bean/>;定义的形式)。

在容器本身内,这些Bean定义表示为BeanDefinition对象,其中包含(以及其他信息)以下元数据:

  • 包限定的类名:通常是要定义的Bean的实际实现类。

  • Bean行为配置元素,说明Bean在容器中的行为方式(作用域、生命周期回调等)。

  • 对Bean执行其工作所需的其他Bean的引用。这些引用也称为协作者或依赖项。

  • 要在新创建的对象 - 中设置的其他配置设置,例如池的大小限制或在管理连接池的Bean中使用的连接数量。

该元数据转换为组成每个Bean定义的一组属性。下表介绍了这些属性:

Table 1. The bean definition
Property Explained in…​

班级

实例化Bean

名字

命名Bean

范围

Bean作用域

构造函数参数

依赖项注入

属性

依赖项注入

自动配线模式

自动连接协作者

延迟初始化模式

延迟初始化的Bean

初始化方法

初始化回调

销毁方法

销毁回调

除了包含如何创建特定Bean的Bean定义之外,ApplicationContext实现还允许注册在容器外部(由用户)创建的现有对象。这是通过getBeanFactory()方法访问ApplicationContext的BeanFactory来实现的,该方法返回DefaultListableBeanFactory实现。DefaultListableBeanFactory通过registerSingleton(..)registerBeanDefinition(..)方法支持此注册。但是,典型的应用程序只使用通过常规Bean定义元数据定义的Bean。

需要尽早注册Bean元数据和手动提供的Singleton实例,以便容器在自动装配和其他自省步骤期间能够正确地对它们进行推理。虽然在某种程度上支持覆盖现有的元数据和现有的单例实例,但不正式支持在运行时注册新的Bean(与对工厂的实时访问并发),并且可能导致并发访问异常、Bean容器中的状态不一致,或者两者兼而有之。

1.3.1. Naming Beans

每个Bean都有一个或多个标识符。这些标识符在托管Bean的容器中必须是唯一的。一个Bean通常只有一个标识符。但是,如果它需要一个以上的别名,则多余的可以被视为别名。

在基于XML的配置元数据中,您可以使用id属性、名称属性或同时使用这两个属性来指定Bean标识符。id属性允许您只指定一个id。通常,这些名称是字母数字的(‘myBean’、‘omeService’等),但它们也可以包含特殊字符。如果要为Bean引入其他别名,还可以在name属性中指定它们,用逗号()、分号()或空格分隔。作为历史记录,在Spring3.1之前的版本中,id属性被定义为xsd:id类型,这限制了可能的字符。从3.1开始,它被定义为xsd:字符串类型。请注意,Beanid唯一性仍然由容器强制实现,尽管不再由XML解析器强制实现。

您不需要为Bean提供名称id。如果您没有显式地提供名称id,容器将为该Bean生成一个唯一的名称。但是,如果您希望通过使用ref元素或Service Locator样式查找来按名称引用该Bean,则必须提供一个名称。不提供名称的动机与使用内部Bean自动生成协作者有关。

Bean Naming Conventions

约定是在命名Bean时使用标准的Java约定,例如字段名称。也就是说,Bean名称以小写字母开头,并且从那里开始采用驼峰大小写。此类名称的示例包括帐户管理器帐户服务userDao登录控制器等。

始终如一地命名Bean使您的配置更易于阅读和理解。此外,如果您使用Spring AOP,则在将建议应用于按名称相关的一组Bean时会有很大帮助。

With component scanning in the classpath, Spring generates bean names for unnamed components, following the rules described earlier: essentially, taking the simple class name and turning its initial character to lower-case. However, in the (unusual) special case when there is more than one character and both the first and second characters are upper case, the original casing gets preserved. These are the same rules as defined by java.beans.Introspector.decapitalize (which Spring uses here).
Aliasing a Bean outside the Bean Definition

在Bean定义本身中,您可以为Bean提供多个名称,方法是使用由id属性指定的最多一个名称和name属性中的任意数量的其他名称。这些名称可以是相同Bean的等效别名,并且在某些情况下非常有用,例如,通过使用特定于组件本身的Bean名称,让应用程序中的每个组件引用公共依赖项。

然而,指定实际定义Bean的所有别名并不总是足够的。有时需要为在其他地方定义的Bean引入别名。这通常是在大型系统中的情况,其中配置在每个子系统之间拆分,每个子系统具有其自己的对象定义集。在基于XML的配置元数据中,您可以使用<;alias/>;元素来实现这一点。以下示例显示了如何执行此操作:

<alias name="fromName" alias="toName"/>
              
              

在这种情况下,在使用该别名定义之后,名为from名称的Bean(在同一容器中)也可能被称为toName

例如,子系统A的配置元数据可以通过子系统A-DataSource的名称来引用数据源。子系统B的配置元数据可以通过子系统B-DataSource的名称来引用数据源。在构建使用这两个子系统的主应用程序时,主应用程序使用名称myApp-DataSource引用数据源。要使这三个名称引用同一对象,您可以将以下别名定义添加到配置元数据:

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>
              
              

现在,每个组件和主应用程序都可以通过唯一且保证不与任何其他定义冲突的名称引用数据源(有效地创建了一个名称空间),但它们引用的是同一个Bean。

Java-configuration

如果您使用Java配置,@Bean注释可以用来提供别名。有关详细信息,请参阅使用@Bean注释

1.3.2. Instantiating Beans

Bean定义本质上是创建一个或多个对象的诀窍。当被询问时,容器查看命名Bean的配方,并使用由该Bean定义封装的配置元数据来创建(或获取)实际对象。

如果使用基于XML的配置元数据,则在<;Bean/>;元素的属性中指定要实例化的对象的类型(或类)。此属性(在内部,它是BeanDefinition实例上的Class属性)通常是必需的。(有关异常,请参阅使用实例工厂方法实例化Bean定义继承。)您可以通过以下两种方式之一使用Class属性:

  • 通常,在容器本身通过反射地调用其构造函数直接创建Bean的情况下,指定要构造的Bean类,这在某种程度上相当于带有new操作符的Java代码。

  • 指定包含用来创建对象的静态工厂方法的实际类,在不太常见的情况下,容器调用类上的静态工厂方法来创建Bean。调用静态工厂方法返回的对象类型可以是同一个类,也可以完全是另一个类。

Nested class names

如果要为嵌套类配置Bean定义,则可以使用嵌套类的二进制名称或源名称。

例如,如果com.Example包中有一个名为Something的类,而这个Something类有一个名为OtherThing静态类,则它们可以用美元符号($)或圆点(.)分隔。因此,Bean定义中的<代码>类 属性的值应该是<代码>com.example.SomeThing$OtherThing或com.example.SomeThing.OtherThing.

Instantiation with a Constructor

当您通过构造函数方法创建一个Bean时,所有普通类都可以由Spring使用并与其兼容。也就是说,正在开发的类不需要实现任何特定接口,也不需要以特定方式编码。只需指定Bean类就足够了。然而,根据您对特定Bean使用的IoC类型,您可能需要一个默认(空)构造函数。

Spring IOC容器几乎可以管理您希望它管理的任何类。它并不局限于管理真正的JavaBean。大多数Spring用户更喜欢只有一个默认(无参数)构造函数以及根据容器中的属性建模的适当setter和getter的实际JavaBean。您还可以在容器中包含更多奇异的非Bean样式的类。例如,如果您需要使用绝对不符合JavaBean规范的遗留连接池,Spring也可以管理它。

使用基于XML的配置元数据,您可以按如下方式指定Bean类:

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>
              
              

有关向构造函数提供参数(如果需要)和在构造对象后设置对象实例属性的机制的详细信息,请参阅注入依赖项

Instantiation with a Static Factory Method

在定义使用静态工厂方法创建的Bean时,使用属性指定包含静态工厂方法的类,并使用名为工厂方法的属性指定工厂方法本身的名称。您应该能够调用此方法(使用可选参数,如后面所述)并返回活动对象,该对象随后被视为是通过构造函数创建的。此类Bean定义的一个用途是在遗留代码中调用静态工厂。

下面的Bean定义指定将通过调用工厂方法来创建Bean。该定义没有指定返回对象的类型(类),而是指定了包含工厂方法的类。在本例中,createInstance()方法必须是静态方法。以下示例显示如何指定工厂方法:

<bean id="clientService" class="examples.ClientService" factory-method="createInstance"/>
              
              

下面的示例显示了一个可以与前面的Bean定义一起使用的类:

Java
Kotlin
public class ClientService {
    private static ClientService clientService = new ClientService();
    private ClientService() {}

    public static ClientService createInstance() {
        return clientService;
    }
}

              
              

有关在对象从工厂返回后向工厂方法提供(可选)参数和设置对象实例属性的机制的详细信息,请参阅依赖项和配置详细信息

Instantiation by Using an Instance Factory Method

与通过静态工厂方法实例化类似,使用实例工厂方法实例化将调用容器中现有Bean的非静态方法来创建新Bean。要使用此机制,请将属性保留为空,并在Factory-Bean属性中指定当前(或父或祖先)容器中的Bean的名称,该容器包含要调用以创建对象的实例方法。使用Factory-method属性设置工厂方法本身的名称。以下示例显示如何配置此类Bean:

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService" factory-bean="serviceLocator" factory-method="createClientServiceInstance"/>
              
              

下面的示例显示了相应的类:

Java
Kotlin
public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

              
              

一个工厂类还可以包含多个工厂方法,如下例所示:

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService" factory-bean="serviceLocator" factory-method="createClientServiceInstance"/>

<bean id="accountService" factory-bean="serviceLocator" factory-method="createAccountServiceInstance"/>
              
              

下面的示例显示了相应的类:

Java
Kotlin
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;
    }
}

              
              

这种方法表明,工厂Bean本身可以通过依赖注入(DI)进行管理和配置。请参阅依赖项和配置的详细信息

In Spring documentation, "factory bean" refers to a bean that is configured in the Spring container and that creates objects through an instance or static factory method. By contrast, FactoryBean (notice the capitalization) refers to a Spring-specific FactoryBean implementation class.
Determining a Bean’s Runtime Type

确定特定Bean的运行时类型不是一件容易的事。Bean元数据定义中指定的类只是一个初始类引用,可能与已声明的工厂方法组合在一起,或者是一个可能导致Bean的不同运行时类型的FactoryBean类,或者在实例级工厂方法(通过指定的Factory-Bean名称解析)的情况下根本没有设置。此外,AOP代理可以用基于接口的代理包装Bean实例,并有限地公开目标Bean的实际类型(仅其实现的接口)。

要了解特定Bean的实际运行时类型,推荐的方法是为指定的Bean名称调用BeanFactory.getType。这会考虑以上所有情况,并返回BeanFactory.getBean调用将为相同的Bean名称返回的对象类型。

1.4. Dependencies

典型的企业应用程序并不由单个对象(或者用Spring的话来说就是Bean)组成。即使是最简单的应用程序也有几个对象一起工作,以呈现最终用户所看到的连贯的应用程序。下一节将解释如何从定义大量独立的Bean定义到完全实现的应用程序,在该应用程序中,对象协作以实现某个目标。

1.4.1. Dependency Injection

依赖项注入(DI)是一个过程,在此过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖项(即,它们使用的其他对象)。然后,容器在创建Bean时注入这些依赖项。这个过程基本上是Bean本身的逆(由此得名,控制反转),通过使用类的直接构造或服务定位器模式来控制其依赖项的实例化或位置。

使用依赖注入原则,代码更干净,当对象被提供依赖项时,解耦更有效。该对象不查找其依赖项,并且不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖于接口或抽象基类时,这允许在单元测试中使用存根或模拟实现。

DI存在于两个主要变体中:基于构造函数的依赖注入基于Setter的依赖注入

Constructor-based Dependency Injection

基于构造函数的DI是通过容器调用具有多个参数的构造函数来实现的,每个参数代表一个依赖项。使用特定的参数调用静态工厂方法来构造Bean几乎是等价的,本讨论以类似的方式对待构造函数和静态工厂方法的参数。下面的示例显示了一个只能通过构造函数注入进行依赖项注入的类:

Java
Kotlin
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...
}

              
              

请注意,这个类没有什么特别之处。它是一个POJO,不依赖于容器特定的接口、基类或注释。

Constructor Argument Resolution

构造函数参数解析匹配通过使用参数的类型进行。如果Bean定义的构造函数参数中不存在潜在的多义性,则在Bean定义中定义构造函数参数的顺序就是实例化Bean时将这些参数提供给相应构造函数的顺序。请考虑以下类:

Java
Kotlin
package x.y;

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

               
               

假设ThingTwoThingThree类通过继承不相关,则不存在潜在的歧义。因此,以下配置可以很好地工作,并且您不需要在<;Construction tor-arg/>;元素中显式指定构造函数参数索引或类型。

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>

    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>
               
               

当引用另一个Bean时,类型是已知的,并且可以进行匹配(就像前面的例子一样)。当使用简单类型时,例如<;Value>;True<;/Value>;,Spring无法确定值的类型,因此在没有帮助的情况下无法根据类型进行匹配。请考虑以下类:

Java
Kotlin
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;
    }
}

               
               
Constructor argument type matching

在上述方案中,如果您使用type属性显式指定构造函数参数的类型,则容器可以使用与简单类型匹配的类型,如下面的示例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>
                 
                 
Constructor argument index

您可以使用index属性显式指定构造函数参数的索引,如下面的示例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>
                 
                 

除了解决多个简单值的多义性外,指定索引还可以解决构造函数具有两个相同类型参数的多义性。

The index is 0-based.
Constructor argument name

还可以使用构造函数参数名称来消除值的歧义,如下面的示例所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>
                 
                 

请记住,要使此功能开箱即用,必须在编译代码时启用调试标志,以便Spring可以从构造函数中查找参数名称。如果不能或不想用调试标志编译代码,可以使用@ConstructorPropertiesJDK注释显式命名构造函数参数。然后,样例类必须如下所示:

Java
Kotlin
package examples;

public class ExampleBean {

    // Fields omitted

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

                 
                 
Setter-based Dependency Injection

基于setter的DI是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化Bean之后调用Bean上的setter方法来完成的。

下面的示例显示了一个只能使用纯setter注入进行依赖项注入的类。这个类是传统的Java。它是一个POJO,不依赖于容器特定的接口、基类或注释。

Java
Kotlin
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...
}

              
              

ApplicationContext为其管理的Bean支持基于构造函数和基于setter的DI。在已经通过构造函数方法注入了一些依赖项之后,它还支持基于setter的DI。以BeanDefinition的形式配置依赖项,将其与PropertyEditor实例结合使用以将属性从一种格式转换为另一种格式。然而,大多数Spring用户并不直接使用这些类(即以编程方式),而是使用基于Java的@configuration类中的XMLBean定义、带注释的组件(即用@Component@Controller等标注的类)或@Bean方法。然后在内部将这些源代码转换为BeanDefinition的实例,并用于加载整个Spring IOC容器实例。

Constructor-based or setter-based DI?

因为您可以混合使用基于构造函数和基于setter的DI,所以对强制依赖项使用构造函数,对可选依赖项使用setter方法或配置方法,这是一个很好的经验法则。请注意,在setter方法上使用@AuTower注释可以使该属性成为必需的依赖项;然而,使用带有参数编程验证的构造函数注入更可取。

Spring团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变的对象,并确保所需的依赖项不是。此外,构造函数注入的组件始终以完全初始化的状态返回给客户端(调用)代码。另外,大量的构造函数参数是一种糟糕的代码气味,这意味着类可能有太多的责任,应该进行重构以更好地解决适当的关注点分离问题。

Setter注入主要应该仅用于可以在类中分配合理缺省值的可选依赖项。否则,必须在代码使用依赖项的任何地方执行非空检查。Setter注入的一个好处是setter方法使该类的对象可以在以后重新配置或重新注入。因此,通过JMX MBean进行管理是setter注入的一个引人注目的用例。

使用对特定类最有意义的依赖注入样式。有时,在处理您没有源代码的第三方类时,会为您做出选择。例如,如果第三方类没有公开任何setter方法,那么构造函数注入可能是DI的唯一可用形式。

Dependency Resolution Process

容器执行Bean依赖解析,如下所示:

  • 创建ApplicationContext并使用描述所有Bean的配置元数据进行初始化。配置元数据可以由XML、Java代码或注释指定。

  • 对于每个Bean,它的依赖项以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用该方法而不是普通的构造函数)。这些依赖项在实际创建Bean时提供给Bean。

  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个Bean的引用。

  • 作为值的每个属性或构造函数参数都会从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,如intlong字符串布尔等等。

在创建容器时,Spring容器会验证每个Bean的配置。但是,在实际创建Bean之前,不会设置Bean属性本身。单例作用域并设置为预实例化(默认)的Bean是在创建容器时创建的。作用域在Bean Scope中定义。否则,仅在请求时才创建Bean。创建Bean可能会导致创建Bean图,因为创建并分配了Bean的依赖项及其依赖项(等等)。请注意,这些依赖项之间的解析不匹配可能会在后期出现 - ,即在第一次创建受影响的Bean时出现。

Circular dependencies

如果您主要使用构造函数注入,则可能会创建无法解析的循环依赖场景。

例如:A类通过构造函数注入需要B类的实例,B类通过构造函数注入需要A类的实例。如果将A类和B类的Bean配置为相互注入,则SpringIOC容器会在运行时检测到此循环引用,并抛出BeanCurrentlyInCreationException.

一种可能的解决方案是编辑一些要由setter而不是构造函数配置的类的源代码。或者,避免构造函数注入,只使用setter注入。换句话说,尽管不推荐这样做,但您可以使用setter注入配置循环依赖项。

与典型情况(没有循环依赖关系)不同,Bean A和Bean B之间的循环依赖关系迫使其中一个Bean在自身完全初始化之前注入另一个Bean(经典的鸡和蛋场景)。

一般来说,您可以相信Spring会做正确的事情。它在容器加载时检测配置问题,例如对不存在的Bean和循环依赖项的引用。在实际创建Bean时,Spring设置属性并尽可能晚地解析依赖项。这意味着正确加载的Spring容器稍后可能会在您请求对象时生成异常,如果创建该对象或其依赖项 - 时出现问题。例如,由于缺少或无效的属性,该Bean抛出异常。这可能会延迟某些配置问题的可见性,这就是为什么ApplicationContext实现默认预实例化单例Bean的原因。在实际需要这些Bean之前创建它们会耗费一些前期时间和内存,您会在创建ApplicationContext时发现配置问题,而不是在以后。您仍然可以覆盖此默认行为,以便让单例Bean延迟初始化,而不是急于预先实例化。

如果不存在循环依赖关系,则当一个或多个协作Bean被注入依赖Bean时,每个协作Bean在被注入依赖Bean之前被完全配置。这意味着,如果Bean A依赖于Bean B,则在调用Bean A的setter方法之前,Spring IOC容器将完全配置Bean B。换句话说,将实例化Bean(如果它不是预实例化的单例),设置其依赖项,并调用相关的生命周期方法(如配置的init方法InitializingBean回调方法)。

Examples of Dependency Injection

以下示例将基于XML的配置元数据用于基于setter的DI。Spring XML配置文件的一小部分指定了一些Bean定义,如下所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne">
        <ref bean="anotherExampleBean"/>
    </property>

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
              
              

下面的示例显示了相应的ExampleBean类:

Java
Kotlin
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;
    }
}

              
              

在前面的示例中,声明setter以与XML文件中指定的属性匹配。下面的示例使用基于构造函数的DI:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
              
              

下面的示例显示了相应的ExampleBean类:

Java
Kotlin
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;
    }
}

              
              

在Bean定义中指定的构造函数参数用作ExampleBean的构造函数的参数。

现在考虑此示例的一个变体,其中,Spring被告知调用静态工厂方法来返回对象的实例,而不是使用构造函数:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
              
              

下面的示例显示了相应的ExampleBean类:

Java
Kotlin
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;
    }
}

              
              

静态工厂方法的参数由<;Construction tor-arg/>;元素提供,就像实际使用了构造函数一样。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是相同的)。实例(非静态)工厂方法可以以基本上相同的方式使用(除了使用Factory-Bean属性而不是属性),因此我们在这里不讨论这些细节。

1.4.2. Dependencies and Configuration in Detail

上一节所述,您可以将Bean属性和构造函数参数定义为对其他托管Bean(协作者)的引用,或者定义为内联定义的值。为此,Spring基于XML的配置元数据支持其<;Property/&>code>和<;Construction tor-Arg/<;元素中的子元素类型。

Straight Values (Primitives, Strings, and so on)

<;Property/&>元素的属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring的转换服务用于将这些值从字符串转换为属性或参数的实际类型。下面的示例显示了设置的各种值:

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="misterkaoli"/>
</bean>
              
              

以下示例使用p名称空间进行更简洁的XML配置:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" p:driverClassName="com.mysql.jdbc.Driver" p:url="jdbc:mysql://localhost:3306/mydb" p:username="root" p:password="misterkaoli"/>

</beans>
              
              

前面的XML更加简洁。然而,除非您在创建Bean定义时使用支持自动属性完成的IDE(例如IntelliJ IdeaSpring Tools for Eclipse),否则会在运行时而不是设计时发现输入错误。强烈建议您提供此类IDE帮助。

您还可以配置java.util.Properties实例,如下所示:

<bean id="mappings" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
            jdbc.driver.className=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>
              
              

通过使用JavaBeansPropertyEditor机制,Spring容器将<;Value/>;元素中的文本转换为java.util.Properties实例。这是一个很好的捷径,也是Spring团队确实倾向于使用嵌套的<;Value/>;元素而不是属性样式的少数地方之一。

The idref element

idref元素只是将容器中另一个Bean的id(字符串值-而不是引用)传递给&arg/<;<;Property/&>元素的一种不出错的方法。以下示例显示如何使用它:

<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>
               
               

前面的Bean定义片断(在运行时)与以下片断完全相同:

<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>
               
               

第一种形式比第二种形式更可取,因为使用idref标记允许容器在部署时验证引用的命名Bean是否实际存在。在第二个变体中,不对传递给客户端Bean的Target Name属性的值执行验证。只有在实际实例化客户端Bean时才会发现拼写错误(最有可能导致致命的结果)。如果客户端Bean是原型Bean,则该拼写错误和产生的异常可能要在部署容器很久之后才能发现。

The local attribute on the idref element is no longer supported in the 4.0 beans XSD, since it does not provide value over a regular bean reference any more. Change your existing idref local references to idref bean when upgrading to the 4.0 schema.

<;idref/>;元素带来值的一个常见地方(至少在Spring2.0之前的版本中)是在ProxyFactoryBeanBean定义中的AOP拦截器的配置中。在指定拦截器名称时使用<;idref/>;元素可以防止您错误拼写拦截器ID。

References to Other Beans (Collaborators)

ref元素是<;构造函数-arg/>;<;Property/>;定义元素中的最后一个元素。在这里,您将Bean的指定属性的值设置为对容器管理的另一个Bean(协作者)的引用。引用的Bean是要设置其属性的Bean的依赖项,在设置属性之前会根据需要对其进行初始化。(如果协作者是单例Bean,则它可能已经被容器初始化。)所有引用最终都是对另一个对象的引用。作用域和验证取决于您是否通过Bean属性指定另一个对象的ID或名称。

通过<;ref/>;标记的Bean属性指定目标Bean是最通用的形式,并且允许创建对同一容器或父容器中的任何Bean的引用,而不管它是否在相同的XML文件中。Bean属性的值可以与目标Bean的id属性相同,也可以与目标Bean的name属性中的值之一相同。下面的示例显示如何使用ref元素:

<ref bean="someBean"/>
              
              

通过属性指定目标Bean将创建对当前容器的父容器中的Bean的引用。属性的值可以与目标Bean的id属性相同,也可以与目标Bean的name属性中的某个值相同。目标Bean必须位于当前Bean的父容器中。主要当您具有容器层次,并且希望使用与父Bean具有相同名称的代理将现有Bean包装在父容器中时,应主要使用此Bean引用变量。以下清单对显示了如何使用属性:

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required here -->
</bean>
              
              
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>
              
              
The local attribute on the ref element is no longer supported in the 4.0 beans XSD, since it does not provide value over a regular bean reference any more. Change your existing ref local references to ref bean when upgrading to the 4.0 schema.
Inner Beans

<;Property/<;<;Construction-arg/>;元素中的<;Bean/元素定义内部Bean,如下面的示例所示:

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>
              
              

内部Bean定义不需要定义的ID或名称。如果指定,容器不会使用这样的值作为标识符。容器在创建时还忽略作用域标志,因为内部Bean始终是匿名的,并且总是使用外部Bean创建的。不可能独立访问内部Bean,也不可能将它们注入到协作Bean中,而不是注入到封闭Bean中。

作为特殊情况,可以从自定义作用域 - 接收销毁回调,例如,对于包含在单例Bean中的请求作用域内部Bean。内部Bean实例的创建绑定到其包含的Bean,但销毁回调允许它参与请求作用域的生命周期。这不是一种常见的情况。内部Bean通常只是共享它们的包含Bean的作用域。

Collections

<;list/&>、<;set/&>、<;map/&>;<;props/&>元素分别设置JavaCollection类型listsetMapProperties的属性和参数。以下示例显示如何使用它们:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>
              
              

映射键或值或设置值的值也可以是以下元素之一:

bean | ref | idref | list | set | map | props | value | null
              
              
Collection Merging

Spring容器还支持合并集合。应用程序开发人员可以定义父<;list/<;<;map/&>;<;set/&>;<;props/&>;元素,并使<;list/&>、<;map/&>;<;set/&>;<;props/>;元素继承并重写父集合的值。也就是说,子集合的值是父集合和子集合的元素合并的结果,子集合的集合元素覆盖父集合中指定的值。

关于合并的这一节讨论父-子Bean机制。不熟悉父Bean和子Bean定义的读者可能希望在继续之前阅读相关小节。

下面的示例演示集合合并:

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">[email protected]</prop>
                <prop key="support">[email protected]</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">[email protected]</prop>
                <prop key="support">[email protected]</prop>
            </props>
        </property>
    </bean>
<beans>
               
               

请注意,在Bean定义的adminEmails属性的<;props/&>元素上使用了merge=true属性。当容器解析并实例化Bean时,得到的实例有一个adminEmailsProperties集合,其中包含合并子adminEmail集合和父adminEmail集合的结果。下面的清单显示了结果:

Properties集合的值集继承父<;道具/中的所有属性元素,而Support值的子级值重写父集合中的值。

此合并行为类似于<;list/&>、<;map/&>;<;set/<;集合类型。在<;list/元素的特定情况下,保留与list集合类型(即有序值集合的概念)相关联的语义。父级的值在子级列表的所有值之前。对于MapSetProperties集合类型,不存在排序。因此,作为容器内部使用的关联MapSetProperties实现类型基础的集合类型没有有效的排序语义。

Limitations of Collection Merging

不能合并不同的集合类型(如映射列表)。如果您确实尝试这样做,则会抛出适当的异常。必须在较低的继承子定义上指定merge属性。在父集合定义上指定merge属性是多余的,并且不会导致所需的合并。

Strongly-typed collection

由于Java对泛型类型的支持,您可以使用强类型集合。也就是说,可以声明Collection类型,使其只能包含(例如)字符串元素。如果使用Spring将强类型的集合依赖项注入到Bean中,则可以利用Spring的类型转换支持,以便在将强类型集合实例添加到集合之前,将其元素转换为适当的类型。下面的Java类和Bean定义说明了如何做到这一点:

Java
Kotlin
public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}

               
               
<beans>
    <bean id="something" class="x.y.SomeClass">
        <property name="accounts">
            <map>
                <entry key="one" value="9.99"/>
                <entry key="two" value="2.75"/>
                <entry key="six" value="3.99"/>
            </map>
        </property>
    </bean>
</beans>
               
               

SomethingBean的Account属性准备注入时,有关强类型Map<;字符串,Float&>元素类型的泛型信息可通过反射获得。因此,Spring的类型转换基础结构将各种Value元素识别为浮动类型,并且字符串值(9.992.753.99)被转换为实际的浮动类型。

Null and Empty String Values

Spring将属性等的空参视为空字符串。以下基于XML的配置元数据片段将电子邮件属性设置为空的字符串值(“”)。

<bean class="ExampleBean">
    <property name="email" value=""/>
</bean>
              
              

上面的示例相当于下面的Java代码:

Java
Kotlin
exampleBean.setEmail("");

              
              

<;空/>;元素处理值。下面的清单显示了一个示例:

<bean class="ExampleBean">
    <property name="email">
        <null/>
    </property>
</bean>
              
              

前面的配置相当于以下Java代码:

Java
Kotlin
exampleBean.setEmail(null);

              
              
XML Shortcut with the p-namespace

P名称空间允许您使用Bean元素的属性(而不是嵌套的<;Property/>;元素)来描述与Bean协作的属性值,或者同时使用两者。

下面的示例显示了两个解析为相同结果的XML片段(第一个使用标准XML格式,第二个使用p命名空间):

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="[email protected]"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean" p:email="[email protected]"/>
</beans>
              
              

该示例显示了在Bean定义中名为Email的p名称空间中的属性。这将告诉Spring包含一个属性声明。如前所述,p名称空间没有模式定义,因此您可以将属性的名称设置为属性名称。

下一个示例包括另外两个Bean定义,这两个Bean定义都引用另一个Bean:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>

    <bean name="john-modern" class="com.example.Person" p:name="John Doe" p:spouse-ref="jane"/>

    <bean name="jane" class="com.example.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
</beans>
              
              

此示例不仅包括使用p名称空间的属性值,还使用特殊格式声明属性引用。第一个Bean定义使用<;Property name=“spourth”ref=“Jane”/>;创建从BeanJohn到BeanJane的引用,而第二个Bean定义使用p:spourte-ref=“Jane”作为属性来执行完全相同的操作。在本例中,配偶是属性名,而-ref部分表示这不是一个直接的值,而是对另一个Bean的引用。

The p-namespace is not as flexible as the standard XML format. For example, the format for declaring property references clashes with properties that end in Ref, whereas the standard XML format does not. We recommend that you choose your approach carefully and communicate this to your team members to avoid producing XML documents that use all three approaches at the same time.
XML Shortcut with the c-namespace

与带有p名称空间的XML快捷方式类似,在Spring3.1中引入的c名称空间允许内联属性来配置构造函数参数,而不是嵌套的构造函数参数元素。

下面的示例使用c:命名空间执行与from基于构造函数的依赖项注入相同的操作:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- traditional declaration with optional argument names -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="[email protected]"/>
    </bean>

    <!-- c-namespace declaration with argument names -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo" c:thingThree-ref="beanThree" c:email="[email protected]"/>

</beans>
              
              

c:命名空间使用与p:命名空间(Bean引用的尾随-ref)相同的约定来按名称设置构造函数参数。类似地,它需要在XML文件中声明,即使它没有在XSD模式中定义(它存在于Spring核心中)。

对于构造函数参数名称不可用的极少数情况(通常是在没有调试信息的情况下编译字节码),可以使用回退到参数索引,如下所示:

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree" c:_2="[email protected]"/>
              
              
Due to the XML grammar, the index notation requires the presence of the leading _, as XML attribute names cannot start with a number (even though some IDEs allow it). A corresponding index notation is also available for <constructor-arg> elements but not commonly used since the plain order of declaration is usually sufficient there.

在实践中,构造函数解析机制在匹配参数方面非常有效,因此,除非确实需要,否则我们建议在整个配置中使用名称表示法。

Compound Property Names

您可以在设置Bean属性时使用复合或嵌套属性名称,只要路径中除最终属性名称以外的所有组件都不为。考虑以下Bean定义:

<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>
              
              

Something Bean有一个fred属性,该属性有一个bob属性,该属性有一个Sammy属性,最后一个Sammy属性被设置为值123。为了使其正常工作,在构造Bean之后,某物fred属性和fredbob属性不能为。否则,抛出NullPointerException异常。

1.4.3. Using depends-on

如果一个Bean是另一个Bean的依赖项,这通常意味着一个Bean被设置为另一个Bean的属性。通常,您可以通过基于XML的配置元数据中的<;ref/>;元素实现这一点。然而,有时Bean之间的依赖关系不那么直接。例如,当需要触发类中的静态初始化器时,例如用于数据库驱动程序注册。Dependons属性可以显式地强制在初始化使用该元素的Bean之前初始化一个或多个Bean。下面的示例使用Dependents-on属性来表示对单个Bean的依赖关系:

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
             
             

要表示对多个Bean的依赖关系,请提供一个Bean名称列表作为Dependers-on属性的值(逗号、空格和分号是有效的分隔符):

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
             
             
The depends-on attribute can specify both an initialization-time dependency and, in the case of singleton beans only, a corresponding destruction-time dependency. Dependent beans that define a depends-on relationship with a given bean are destroyed first, prior to the given bean itself being destroyed. Thus, depends-on can also control shutdown order.

1.4.4. Lazy-initialized Beans

默认情况下,作为初始化过程的一部分,ApplicationContext实现急于创建和配置所有SingletonBean。通常,这种预实例化是可取的,因为配置或周围环境中的错误可以立即发现,而不是几小时甚至几天后。当这种行为不受欢迎时,您可以通过将Bean定义标记为延迟初始化来阻止单例Bean的预实例化。延迟初始化的Bean告诉IoC容器在第一次请求时创建Bean实例,而不是在启动时创建。

在XML中,此行为由<;bean/>;元素上的lazy-init属性控制,如下面的示例所示:

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
             
             

如果上述配置由ApplicationContext使用,则ApplicationContext启动时不会急于预实例化lazyBean,而会急于预实例化not.lazyBean。

但是,当延迟初始化的Bean是未延迟初始化的单例Bean的依赖项时,ApplicationContext会在启动时创建延迟初始化的Bean,因为它必须满足单例的依赖项。延迟初始化的Bean被注入到其他地方未延迟初始化的单例Bean中。

您还可以通过使用<;Beans/>;元素的default-lazy-init属性在容器级别控制延迟初始化,如下面的示例所示:

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>
             
             

1.4.5. Autowiring Collaborators

Spring容器可以自动绑定协作Bean之间的关系。您可以通过检查ApplicationContext的内容,让Spring自动为您的Bean解析协作者(其他Bean)。自动配线具有以下优势:

  • 自动装配可以显著减少指定属性或构造函数参数的需要。(其他机制,如本章其他部分讨论的Bean模板,在这方面也很有价值。)

  • 自动配置可以随着对象的发展而更新配置。例如,如果需要向类添加依赖项,则无需修改配置即可自动满足该依赖项。因此,自动连接在开发期间可能特别有用,而不会在代码库变得更加稳定时否定切换到显式连接的选项。

在使用基于XML的配置元数据时(请参阅依赖项注入),您可以使用<;Bean/>;元素的auTower属性为Bean定义指定自动绑定模式。自动装配功能有四种模式。您可以为每个Bean指定自动装配,因此可以选择自动装配哪些Bean。下表描述了四种自动装配模式:

Table 2. Autowiring modes
Mode Explanation

(默认)无自动布线。Bean引用必须由ref元素定义。对于较大的展开,不建议更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。

别名

按属性名称自动连接。Spring查找与需要自动绑定的属性同名的Bean。例如,如果将一个Bean定义设置为按名称自动布线,并且它包含一个master属性(即它有一个setMaster(..)方法),则Spring会查找一个名为master的Bean定义,并使用它来设置该属性。

byType

如果容器中只存在一个属性类型的Bean,则允许对该属性进行自动绑定。如果存在多个Bean,则抛出致命异常,指示您不能对该Bean使用byType自动装配。如果没有匹配的Bean,则不会发生任何事情(未设置该属性)。

构造函数

类似于byType,但适用于构造函数参数。如果容器中没有恰好一个构造函数参数类型的Bean,则会引发致命错误。

使用byType构造函数自动装配模式,可以连接数组和类型化集合。在这种情况下,容器内与预期类型匹配的所有自动布线候选对象都会被提供以满足依赖关系。如果预期的键类型为字符串,则可以自动连接强类型的Map实例。自动配置的Map实例的值由匹配预期类型的所有Bean实例组成,Map实例的键包含相应的Bean名称。

Limitations and Disadvantages of Autowiring

当在整个项目中一致地使用自动装配时,它的效果最好。如果通常不使用自动装配,那么使用它只连接一个或两个Bean定义可能会让开发人员感到困惑。

考虑自动装配的局限性和缺点:

  • 属性构造函数参数设置中的显式依赖项始终覆盖自动装配。您不能自动绑定简单属性,如原语、字符串(以及这些简单属性的数组)。这种限制是设计出来的。

  • 自动配线不如显式配线精确。不过,正如前面的表中所指出的,在可能产生意外结果的歧义情况下,Spring会谨慎地避免猜测。不再显式地记录您的Spring托管对象之间的关系。

  • 连接信息对于可能从Spring容器生成文档的工具可能不可用。

  • 容器中的多个Bean定义可能与要自动连接的setter方法或构造函数参数指定的类型匹配。对于数组、集合或Map实例,这不一定是问题。但是,对于需要单个值的依赖项,这种模糊性不会被任意解决。如果没有唯一的Bean定义可用,则抛出异常。

在后一种情况下,您有几种选择:

  • 放弃自动布线,转而使用显式布线。

  • 下一节中所述,通过将Bean定义的auTower-Candiate属性设置为FALSE来避免对其进行自动装配。

  • 将单个Bean定义指定为主要候选,方法是将其<;Bean/>;元素的主要属性设置为true

  • 实现基于注释的配置提供的更细粒度的控件,如基于注释的容器配置中所述。

Excluding a Bean from Autowiring

在每个Bean的基础上,您可以从自动装配中排除Bean。在Spring的XML格式中,将<;Bean/>;元素的auTower-Candiate属性设置为FALSE。容器使得特定的Bean定义对自动装配基础设施不可用(包括注释风格的配置,如@Autwire)。

The autowire-candidate attribute is designed to only affect type-based autowiring. It does not affect explicit references by name, which get resolved even if the specified bean is not marked as an autowire candidate. As a consequence, autowiring by name nevertheless injects a bean if the name matches.

您还可以根据Bean名称的模式匹配来限制自动布线候选对象。顶层<;Beans/>;元素接受其Default-autwire-Candients属性中的一个或多个模式。例如,要将自动布线候选状态限制为名称以Repository结尾的任何Bean,请提供*Repository的值。要提供多个模式,请在逗号分隔的列表中定义它们。对于Bean定义的auTower-Candidate属性,显式的trueFalse值始终优先。对于此类Bean,模式匹配规则不适用。

对于您永远不想通过自动装配注入到其他Bean中的Bean,这些技术非常有用。这并不意味着被排除的Bean本身不能使用自动装配进行配置。相反,Bean本身并不是自动装配其他Bean的候选对象。

1.4.6. Method Injection

在大多数应用场景中,容器中的大多数Bean都是单例。当一个Singleton Bean需要与另一个Singleton Bean协作或非Singleton Bean需要与另一个非Singleton Bean协作时,通常通过将一个Bean定义为另一个Bean的属性来处理依赖关系。当Bean的生命周期不同时,就会出现问题。假设单例Bean A需要使用非单例(原型)Bean B,可能是在A的每个方法调用上。容器只创建一次单例Bean A,因此只有一次机会设置属性。容器不能在每次需要时都向Bean A提供Bean B的新实例。

一个解决方案是放弃某种控制权的反转。您可以使Bean A知道容器,方法是实现ApplicationConextAware接口,并在Bean A需要时调用getBean(“B”)容器请求(通常是新的)Bean B实例。以下示例显示了此方法:

Java
Kotlin
// 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; } } 
             
             

前面的内容并不可取,因为业务代码知道并耦合到了Spring框架。方法注入是Spring IOC容器的一个有点高级的特性,它使您能够干净利落地处理这种用例。

您可以在这篇博客文章中阅读更多关于方法注入动机的内容。

Lookup Method Injection

查找方法注入是容器覆盖容器管理的Bean上的方法并返回容器中另一个命名Bean的查找结果的能力。查找通常涉及一个原型Bean,如上一节中描述的场景。Spring框架通过使用CGLIB库中的字节码生成来动态生成覆盖该方法的子类来实现该方法注入。

  • 要使这个动态子类化起作用,Spring Bean容器子类的类不能是最终,要覆盖的方法也不能是最终

  • 对具有抽象方法的类进行单元测试需要您自己子类化该类,并提供抽象方法的存根实现。

  • 具体的方法对于组件扫描也是必要的,这需要提取具体的类。

  • 另一个关键限制是,查找方法不能与工厂方法一起工作,尤其不能与配置类中的@Bean方法一起工作,因为在这种情况下,容器不负责创建实例,因此不能动态地创建运行时生成的子类。

在前面代码片段中的CommandManager类的情况下,Spring容器动态覆盖createCommand()方法的实现。CommandManager类没有任何Spring依赖项,如修改后的示例所示:

Java
Kotlin
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();
}

              
              

在包含要注入的方法的客户端类(本例中为CommandManager)中,要注入的方法需要以下形式的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);
              
              

如果该方法是抽象,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖原始类中定义的具体方法。请考虑以下示例:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>
              
              

标识为命令管理器的Bean在需要myCommandBean的新实例时调用其自己的createCommand()方法。如果确实需要的话,您必须小心地将myCommandBean部署为原型。如果它是Singleton,则每次都会返回相同的myCommandBean实例。

或者,在基于注释的组件模型中,您可以通过@Lookup注释声明一个查找方法,如下面的示例所示:

Java
Kotlin
public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    protected abstract Command createCommand();
}

              
              

或者,更贴切地说,您可以依赖于根据查找方法的声明返回类型解析目标Bean:

Java
Kotlin
public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup
    protected abstract Command createCommand();
}

              
              

请注意,您通常应该使用具体的存根实现来声明这种带注释的查找方法,以便它们与Spring的组件扫描规则兼容,其中抽象类在默认情况下被忽略。此限制不适用于显式注册或显式导入的Bean类。

访问不同作用域的目标Bean的另一种方法是对象工厂/提供者注入点。请参阅作用域Bean作为依赖项

您可能还会发现ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)很有用。

Arbitrary Method Replacement

与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一个方法实现替换托管Bean中的任意方法。在您真正需要此功能之前,您可以安全地跳过本部分的其余部分。

使用基于XML的配置元数据,您可以使用替换方法元素将已部署的Bean的现有方法实现替换为另一个方法实现。考虑下面的类,它有一个我们想要覆盖的名为culteValue的方法:

Java
Kotlin
public class MyValueCalculator {

    public String computeValue(String input) {
        // some real code...
    }

    // some other methods...
}

              
              

实现org.springframework.beans.factory.support.MethodReplacer接口的类提供新的方法定义,如下面的示例所示:

Java
Kotlin
/** * 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 ...;
    }
}

              
              

用于部署原始类并指定方法覆盖的Bean定义类似于以下示例:

<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- arbitrary method replacement -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
              
              

您可以在<;替换方法/>;元素中使用一个或多个<;arg-type/>;元素来指示被重写的方法的方法签名。只有当方法重载并且类中存在多个变量时,才需要参数的签名。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下全部匹配java.lang.String

java.lang.String
String
Str

              
              

因为参数的数量通常足以区分每种可能的选择,所以该快捷方式只允许您键入与参数类型匹配的最短字符串,从而可以节省大量的输入。

1.5. Bean Scopes

当您创建一个Bean定义时,您将创建一个用于创建由该Bean定义定义的类的实际实例的配方。Bean定义就是菜谱这一想法很重要,因为这意味着,与类一样,您可以从一个菜谱创建许多对象实例。

您不仅可以控制要插入到从特定Bean定义创建的对象中的各种依赖项和配置值,还可以控制从特定Bean定义创建的对象的范围。这种方法功能强大且灵活,因为您可以通过配置选择创建的对象的作用域,而不必在Java类级别烘焙对象的作用域。可以将Bean定义为部署在多个作用域之一中。Spring框架支持六个作用域,其中四个只有在使用可识别Web的ApplicationContext时才可用。您还可以创建自定义作用域。

下表描述了支持的作用域:

Table 3. Bean scopes
Scope Description

单例

(默认)将单个Bean定义作用域为每个Spring IOC容器的单个对象实例。

原型

将单个Bean定义的范围扩大到任意数量的对象实例。

请求

将单个Bean定义的范围限定为单个HTTP请求的生命周期。也就是说,每个HTTP请求都有其自己的Bean实例,该实例是在单个Bean定义之后创建的。仅在可识别Web的SpringApplicationContext的上下文中有效。

会话

将单个Bean定义限定为HTTP会话的生命周期。仅在可识别Web的SpringApplicationContext的上下文中有效。

应用程序

将单个Bean定义的范围限定为ServletContext的生命周期。仅在可识别Web的SpringApplicationContext的上下文中有效。

WebSocket

将单个Bean定义限定为WebSocket的生命周期。仅在可识别Web的SpringApplicationContext的上下文中有效。

As of Spring 3.0, a thread scope is available but is not registered by default. For more information, see the documentation for SimpleThreadScope. For instructions on how to register this or any other custom scope, see Using a Custom Scope.

1.5.1. The Singleton Scope

只管理单个Bean的一个共享实例,并且对具有与该Bean定义匹配的一个或多个ID的Bean的所有请求都会导致由Spring容器返回该特定的Bean实例。

换句话说,当您定义一个Bean定义并且它的作用域为单例时,Spring IOC容器恰好创建由该Bean定义定义的对象的一个实例。这个实例存储在这样的单例Bean的缓存中,该命名Bean的所有后续请求和引用都返回缓存的对象。下图显示了Singleton作用域的工作原理:

singleton

Spring的单例Bean概念不同于四人组(Gang of Four,GoF)模式一书中定义的单例模式。GoF Singleton对对象的作用域进行硬编码,使得每个ClassLoader只创建特定类的一个实例。最好将Spring Singleton的范围描述为按容器和按Bean。这意味着,如果您在单个Spring容器中为特定类定义了一个Bean,则Spring容器将创建该Bean定义所定义的类的一个且仅一个实例。单例作用域是Spring中的默认作用域。要将Bean定义为XML中的单例,您可以定义Bean,如下例所示:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
             
             

1.5.2. The Prototype Scope

Bean部署的非Singleton Prototype作用域在每次请求特定Bean时都会创建一个新的Bean实例。也就是说,该Bean被注入到另一个Bean中,或者您通过容器上的getBean()方法调用来请求它。通常,您应该对所有有状态Bean使用Prototype作用域,对无状态Bean使用Singleton作用域。

下图说明了Spring Prototype作用域:

prototype

(数据访问对象(DAO)通常不配置为原型,因为典型的DAO不包含任何会话状态。对我们来说,重用单例关系图的核心更加容易。)

下面的示例将Bean定义为XML中的原型:

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
             
             

与其他作用域不同,Spring不管理原型Bean的整个生命周期。容器实例化、配置和以其他方式组装原型对象,并将其交给客户端,而不进一步记录该原型实例。因此,尽管在所有对象上调用初始化生命周期回调方法而不考虑范围,但在原型的情况下,不调用配置的销毁生命周期回调。客户端代码必须清理原型作用域的对象,并释放原型Bean拥有的昂贵资源。要让Spring容器释放由Prototype作用域的Bean持有的资源,可以尝试使用定制的Bean后处理器,它包含对需要清理的Bean的引用。

在某些方面,对于原型作用域的Bean,Spring容器的角色是Javanew操作符的替代。超过这一点的所有生命周期管理都必须由客户端处理。(有关Spring容器中的Bean的生命周期的详细信息,请参阅生命周期回调。)

1.5.3. Singleton Beans with Prototype-bean Dependencies

当您使用具有对原型Bean的依赖关系的单例作用域的Bean时,请注意依赖关系是在实例化时解析的。因此,如果您将一个原型作用域的Bean注入到单例作用域的Bean中,则会实例化一个新的原型Bean,然后将其依赖项注入到单例Bean中。原型实例是曾经提供给单例作用域的Bean的唯一实例。

然而,假设您希望单例作用域的Bean在运行时重复获取Prototype作用域的Bean的新实例。您不能将原型作用域的Bean注入到Singleton Bean中,因为这种注入只发生一次,当Spring容器实例化Singleton Bean并解析和注入它的依赖项时。如果您在运行时多次需要原型Bean的新实例,请参阅方法注入

1.5.4. Request, Session, Application, and WebSocket Scopes

请求会话应用程序WebSocket作用域只有在使用Web感知的SpringApplicationContext实现(如XmlWebApplicationContext)时才可用。如果将这些作用域与常规的Spring IOC容器(如ClassPathXmlApplicationContext)一起使用,则会抛出一个IllegalStateException,它会报告未知的Bean作用域。

Initial Web Configuration

为了支持请求会话应用程序WebSocket级别的Bean的作用域(Web作用域的Bean),在定义Bean之前需要进行一些较小的初始配置。(标准作用域SingletonPrototype不需要此初始设置。)

如何完成这个初始设置取决于您特定的Servlet环境。

如果在Spring Web MVC中访问作用域Bean,实际上是在由SpringDispatcherServlet处理的请求中,则不需要特殊设置。DispatcherServlet已公开所有相关状态。

如果使用Servlet Web容器,并且请求在Spring的<代码>Dispatcher Servlet 之外处理(例如,在使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener<代码>ServletRequestListener 。这可以通过使用WebApplicationInitializer接口以编程方式完成。或者,将以下声明添加到Web应用程序的web.xml文件中:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>
              
              

或者,如果您的侦听器设置有问题,可以考虑使用Spring的RequestConextFilter。过滤器映射取决于周围的Web应用程序配置,因此您必须对其进行适当的更改。下面的清单显示了Web应用程序的筛选器部分:

<web-app>
    ...
    <filter>
        <filter-name>requestContextFilter</filter-name>
        <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>
              
              

DispatcherServletRequestContextListenerRequestContextFilter都执行完全相同的操作,即将HTTP请求对象绑定到为该请求提供服务的线程。这使得请求和会话作用域的Bean在调用链的更下游可用。

Request scope

考虑以下Bean定义的XML配置:

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>
              
              

Spring容器通过为每个HTTP请求使用loginActionBean定义来创建LoginActionBean的新实例。也就是说,loginActionBean的作用域是在HTTP请求级别。您可以随意更改创建的实例的内部状态,因为从相同的loginActionBean定义创建的其他实例看不到这些状态更改。它们是针对个别请求的。当请求完成处理时,范围为该请求的Bean将被丢弃。

当使用注释驱动的组件或Java配置时,@RequestScope注释可用于将组件分配给请求作用域。以下示例显示了如何执行此操作:

Java
Kotlin
@RequestScope
@Component
public class LoginAction {
    // ...
}

              
              
Session Scope

考虑以下Bean定义的XML配置:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>
              
              

Spring容器通过在单个HTTP会话的生命周期中使用UserPreferencesBean定义来创建UserPreferencesBean的新实例。换句话说,userPreferencesBean的作用域实际上是在HTTP会话级别。与使用请求作用域的Bean一样,您可以随心所欲地更改创建的实例的内部状态,因为您知道,也在使用从相同的用户首选项Bean定义创建的实例的其他HTTP会话实例看不到这些状态更改,因为它们是特定于单个HTTP会话的。当HTTP会话最终被丢弃时,作用域为该特定HTTP会话的Bean也将被丢弃。

当使用注释驱动的组件或Java配置时,您可以使用@SessionScope注释将组件分配给会话作用域。

Java
Kotlin
@SessionScope
@Component
public class UserPreferences {
    // ...
}

              
              
Application Scope

考虑以下Bean定义的XML配置:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>
              
              

Spring容器通过对整个Web应用程序使用一次AppPreferencesBean定义来创建AppPreferencesBean的新实例。也就是说,appPreferencesBean的作用域是ServletContext级别,并存储为常规的ServletContext属性。这在一定程度上类似于Spring Singleton Bean,但有两个重要的区别:它是按ServletContext,而不是按SpringApplicationContext的单例(在任何给定的Web应用程序中可能有多个),并且它实际上是公开的,因此作为ServletContext属性可见。

当使用注释驱动的组件或Java配置时,可以使用@ApplicationScope注释将组件分配给应用程序作用域。以下示例显示了如何执行此操作:

Java
Kotlin
@ApplicationScope
@Component
public class AppPreferences {
    // ...
}

              
              
WebSocket Scope

WebSocket作用域与WebSocket会话的生命周期相关,并适用于践踏WebSocket应用程序,有关详细信息,请参阅WebSocket Scope

Scoped Beans as Dependencies

Spring IOC容器不仅管理对象(Bean)的实例化,还管理协作者(或依赖项)的连接。如果您想要(例如)将一个HTTP请求作用域的Bean注入到另一个较长生命周期作用域的Bean中,您可以选择注入一个AOP代理来代替作用域的Bean。也就是说,您需要注入一个代理对象,该对象公开与作用域对象相同的公共接口,但也可以从相关作用域检索实际目标对象(如HTTP请求),并将方法调用委托给实际对象。

您还可以在作用域为Singleton的Bean之间使用<;aop:Scope-Proxy/&,然后引用通过一个可序列化的中间代理,从而能够在反序列化时重新获得目标单例Bean。

当对Prototype作用域的Bean声明<;aop:Scope-Proxy/&时,对共享代理的每个方法调用都会导致创建一个新的目标实例,然后将调用转发到该目标实例。

此外,作用域代理并不是以生命周期安全的方式从较短作用域访问Bean的唯一方式。您还可以将注入点(即构造函数或设置器参数或自动连接的字段)声明为ObjectFactory<;MyTargetBean>;,,从而允许<代码>getObject() 调用在每次需要 - 时按需检索当前实例,而无需保留该实例或单独存储该实例。

作为扩展变量,您可以声明ObjectProvider<;MyTargetBean>;,它提供了几个附加的访问变量,包括getIfAvailablegetIfUnique

它的JSR-330变体称为提供程序,并与提供程序声明和对应的get()调用一起用于每次检索尝试。有关JSR-330的整体详细信息,请参阅此处

以下示例中的配置只有一行,但重要的是要了解其背后的“为什么”以及“如何”:

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

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> (1)
    </bean>

    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>
              
              
1 The line that defines the proxy.

要创建这样的代理,您需要将子<;aop:Scope-Proxy/>;元素插入到限定了作用域的Bean定义中(请参阅选择要创建的代理的类型基于XML架构的配置)。为什么请求会话和自定义作用域级别的Bean的定义需要<;aop:Scope-Proxy/&>元素?考虑以下单例Bean定义,并将其与您需要为前述作用域定义的Bean定义进行对比(请注意,以下用户首选项Bean定义是不完整的):

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
              
              

在前面的示例中,单例Bean(userManager)被注入了对HTTP会话作用域Bean(userPreferences)的引用。这里的重点是userManagerBean是一个单例:每个容器只实例化一次,它的依赖项(在本例中只有一个,即userPreferencesBean)也只注入一次。这意味着userManagerBean只对完全相同的userPreferences对象(即最初注入它的那个对象)进行操作。

将寿命较短的作用域Bean注入生命周期较长的作用域Bean(例如,将HTTP会话作用域的协作Bean作为依赖项注入Singleton Bean)时,这不是您想要的行为。相反,您需要一个userManager对象,并且在HTTP会话的生存期内,您需要一个特定于HTTP会话userPreferences对象。因此,容器创建了一个公开与UserPreferences类完全相同的公共接口的对象(理想情况下是一个UserPreferences实例的对象),它可以从作用域机制(HTTP请求、会话等)中获取真正的UserPreferences对象。容器将该代理对象注入到userManagerBean中,该Bean不知道该UserPreferences引用是一个代理。在本例中,当UserManager实例调用依赖项注入的UserPreferences对象上的方法时,它实际上是在调用代理上的方法。然后,代理从(在本例中)HTTP会话获取实际的UserPreferences对象,并将方法调用委托给检索到的实际UserPreferences对象。

因此,在将请求会话范围的Bean注入协作对象时,您需要进行以下(正确且完整的)配置,如下例所示:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
              
              
Choosing the Type of Proxy to Create

默认情况下,当Spring容器为使用<;aop:Scope-Proxy/&/code>元素标记的Bean创建代理时,将创建一个基于CGLIB的类代理。

CGLIB代理只拦截公共方法调用!不要在这样的代理上调用非公共方法。它们不会委托给实际的作用域目标对象。

或者,您可以将Spring容器配置为为此类作用域Bean创建标准的基于JDK接口的代理,方法是为<;aop:Scope-Proxy/&元素的Proxy-Target-Class属性的值指定False。使用基于JDK接口的代理意味着您不需要应用程序类路径中的其他库来影响此类代理。然而,这也意味着作用域Bean的类必须实现至少一个接口,并且注入作用域Bean的所有协作者必须通过其接口之一引用该Bean。以下示例显示了基于接口的代理:

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
               
               

有关选择基于类或基于接口的代理的更多详细信息,请参阅代理机制

1.5.5. Custom Scopes

Bean作用域机制是可扩展的。您可以定义自己的作用域,甚至可以重新定义现有作用域,尽管后者被认为是不好的做法,您不能覆盖内置的单例原型作用域。

Creating a Custom Scope

要将您的定制作用域集成到Spring容器中,您需要实现org.springframework.beans.factory.config.Scope接口,本节将对其进行描述。有关如何实现您自己的作用域的想法,请参阅Spring框架本身提供的Scope实现和Scopejavadoc,后者更详细地解释了需要实现的方法。

Scope接口有四个方法,用于从作用域中获取对象、从作用域中移除对象以及销毁对象。

例如,会话作用域实现返回会话作用域的Bean(如果它不存在,则在将其绑定到会话以供将来引用之后,该方法返回该Bean的新实例)。下面的方法从基础作用域返回对象:

Java
Kotlin
Object get(String name, ObjectFactory<?> objectFactory) 
              
              

例如,会话作用域实现从底层会话中删除了会话作用域的Bean。应该返回对象,但如果找不到指定名称的对象,也可以返回NULL。下面的方法将对象从基础作用域中移除:

Java
Kotlin
Object remove(String name) 
              
              

下面的方法注册一个回调,作用域在销毁它或销毁作用域中的指定对象时应调用该回调:

Java
Kotlin
void registerDestructionCallback(String name, Runnable destructionCallback) 
              
              

有关销毁回调的更多信息,请参见javadoc或Spring范围实现。

以下方法获取基础作用域的会话标识符:

Java
Kotlin
String getConversationId() 
              
              

此标识符因每个作用域而异。对于会话作用域实现,该标识符可以是会话标识符。

Using a Custom Scope

在编写和测试一个或多个定制的Scope实现之后,您需要让Spring容器知道您的新作用域。以下方法是向Spring容器注册新的作用域的主要方法:

Java
Kotlin
void registerScope(String scopeName, Scope scope);

              
              

此方法在ConfigurableBeanFactory接口上声明,该接口可通过Spring附带的大多数具体ApplicationContext实现的BeanFactory属性访问。

registerScope(..)方法的第一个参数是与作用域关联的唯一名称。Spring容器本身中此类名称的示例是SingletonPrototyperegisterScope(..)方法的第二个参数是您希望注册和使用的自定义Scope实现的实际实例。

假设您编写了定制的Scope实现,然后注册它,如下例所示。

The next example uses SimpleThreadScope, which is included with Spring but is not registered by default. The instructions would be the same for your own custom Scope implementations.
Java
Kotlin
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

              
              

然后,您可以创建符合自定义作用域的作用域规则的Bean定义,如下所示:

<bean id="..." class="..." scope="thread">
              
              

使用自定义的范围实现,您不受范围的编程注册的限制。您还可以使用CustomScopeConfigurer类以声明方式进行范围注册,如下面的示例所示:

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

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>
              
              
When you place <aop:scoped-proxy/> within a <bean> declaration for a FactoryBean implementation, it is the factory bean itself that is scoped, not the object returned from getObject().

1.6. Customizing the Nature of a Bean

Spring框架提供了许多接口,您可以使用它们来自定义Bean的性质。本节按如下方式对它们进行分组:

1.6.1. Lifecycle Callbacks

要与容器对Bean生命周期的管理进行交互,您可以实现SpringInitializingBeanDisposableBean接口。容器为前者调用After PropertiesSet(),为后者调用Destroy(),以便让Bean在初始化和销毁Bean时执行某些操作。

JSR-250@PostConstruct@PreDestroy注释通常被认为是在现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注释意味着您的Bean不会耦合到特定于Spring的接口。详情请参见Using@PostConstruct@PreDestroy

如果您不想使用JSR-250注释,但仍然想要删除耦合,请考虑init-方法销毁方法Bean定义元数据。

在内部,Spring框架使用BeanPostProcessor实现来处理它可以找到的任何回调接口并调用适当的方法。如果您需要定制特性或其他生命周期行为,Spring默认不提供,您可以自己实现一个BeanPostProcessor。有关详细信息,请参阅容器扩展点

除了初始化和销毁回调之外,Spring管理的对象还可以实现Lifeccle接口,以便这些对象可以参与由容器自身的生命周期驱动的启动和关闭过程。

本节介绍了生命周期回调接口。

Initialization Callbacks

org.springframework.beans.factory.InitializingBean接口允许Bean在容器设置了Bean的所有必要属性后执行初始化工作。InitializingBean接口指定单个方法:

void afterPropertiesSet() throws Exception;

              
              

我们建议您不要使用InitializingBean接口,因为它不必要地将代码耦合到Spring。或者,我们建议使用@PostConstruct注释或指定POJO初始化方法。对于基于XML的配置元数据,您可以使用init-method属性来指定具有空无参数签名的方法的名称。通过Java配置,您可以使用@BeaninitMethod属性。请参阅接收生命周期回调。请考虑以下示例:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
              
              
Java
Kotlin
public class ExampleBean {

    public void init() {
        // do some initialization work
    }
}

              
              

上面的示例与下面的示例(由两个清单组成)具有几乎完全相同的效果:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
              
              
Java
Kotlin
public class AnotherExampleBean implements InitializingBean {

    @Override
    public void afterPropertiesSet() {
        // do some initialization work
    }
}

              
              

但是,前面两个示例中的第一个没有将代码耦合到Spring。

Destruction Callbacks

实现org.springframework.beans.factory.DisposableBean接口允许Bean在包含它的容器被销毁时获得回调。DisposableBean接口指定单个方法:

void destroy() throws Exception;

              
              

我们建议您不要使用DisposableBean回调接口,因为它不必要地将代码耦合到Spring。或者,我们建议使用@PreDestroy注释或指定Bean定义支持的泛型方法。对于基于XML的配置元数据,您可以在<;Bean/>;上使用销毁方法属性。通过Java配置,您可以使用@BeandelestyMethod属性。请参阅接收生命周期回调。请考虑以下定义:

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
              
              
Java
Kotlin
public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

              
              

上述定义与以下定义几乎具有完全相同的效果:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
              
              
Java
Kotlin
public class AnotherExampleBean implements DisposableBean {

    @Override
    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

              
              

然而,前面两个定义中的第一个并没有将代码耦合到Spring。

You can assign the destroy-method attribute of a <bean> element a special (inferred) value, which instructs Spring to automatically detect a public close or shutdown method on the specific bean class. (Any class that implements java.lang.AutoCloseable or java.io.Closeable would therefore match.) You can also set this special (inferred) value on the default-destroy-method attribute of a <beans> element to apply this behavior to an entire set of beans (see Default Initialization and Destroy Methods). Note that this is the default behavior with Java configuration.
Default Initialization and Destroy Methods

在编写不使用特定于Spring的InitializingBeanDisposableBean回调接口的初始化和销毁方法时,通常会使用init()初始化()Dispose()等名称编写方法。理想情况下,这种生命周期回调方法的名称在整个项目中都是标准化的,以便所有开发人员使用相同的方法名称并确保一致性。

您可以将Spring容器配置为“查找”每个Bean上的命名初始化和销毁回调方法名。这意味着,作为应用程序开发人员,您可以编写应用程序类并使用名为init()的初始化回调,而不必为每个Bean定义配置init-method=“init”属性。在创建Bean时(并且根据前面描述的标准生命周期回调契约),Spring IOC容器调用该方法。此功能还强制执行初始化和销毁方法回调的一致命名约定。

假设您的初始化回调方法命名为init(),销毁回调方法命名为Destroy()。然后,您的类与下例中的类类似:

Java
Kotlin
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.");
        }
    }
}

              
              

然后,您可以在如下所示的Bean中使用该类:

<beans default-init-method="init">

    <bean id="blogService" class="com.something.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>
              
              

在顶级<;Beans/&>元素属性上存在Default-init-method属性会导致Spring IOC容器将Bean类上名为init的方法识别为初始化方法回调。在创建和组装Bean时,如果Bean类具有这样的方法,则会在适当的时间调用它。

您可以通过在顶级<;Beans/>;元素上使用Default-Destroy-Method属性来配置类似的销毁方法回调(即在XML中)。

如果现有的Bean类已经具有命名与约定不同的回调方法,您可以使用<;Bean/&>本身的<;Bean/&>属性来指定(在XML中)方法名称,从而覆盖缺省值。

Spring容器保证在为Bean提供所有依赖项之后立即调用已配置的初始化回调。因此,在原始Bean引用上调用初始化回调,这意味着AOP拦截器等尚未应用于该Bean。首先完全创建一个目标Bean,然后应用一个带有拦截器链的AOP代理(例如)。如果分别定义目标Bean和代理,您的代码甚至可以绕过代理与原始目标Bean交互。因此,将拦截器应用于init方法是不一致的,因为这样做会将目标Bean的生命周期耦合到它的代理或拦截器,并在代码直接与原始目标Bean交互时留下奇怪的语义。

Combining Lifecycle Mechanisms

从Spring2.5开始,您有三种选择来控制Bean生命周期行为:

If multiple lifecycle mechanisms are configured for a bean and each mechanism is configured with a different method name, then each configured method is run in the order listed after this note. However, if the same method name is configured — for example, init() for an initialization method — for more than one of these lifecycle mechanisms, that method is run once, as explained in the preceding section.

为同一个Bean配置的多个生命周期机制具有不同的初始化方法,调用方式如下:

  1. 使用@PostConstruct注释的方法

  2. InitializingBean回调接口定义的postPropertiesSet()

  3. 自定义配置的init()方法

销毁方法的调用顺序相同:

  1. 使用@PreDestroy注释的方法

  2. 销毁()DisposableBean回调接口定义

  3. 自定义配置的Destroy()方法

Startup and Shutdown Callbacks

Lifeccle接口为任何具有自己的生命周期要求(如启动和停止某些后台进程)的对象定义基本方法:

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

              
              

任何由Spring管理的对象都可以实现生命周期接口。然后,当ApplicationContext本身接收到启动和停止信号时(例如,对于运行时的停止/重新启动场景),它会将这些调用级联到该上下文中定义的所有生命周期实现。它通过委托LifecycleProcessor来实现这一点,如下面的清单所示:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

              
              

注意,生命周期处理器本身就是生命周期接口的扩展。它还添加了另外两种方法,用于响应正在刷新和关闭的上下文。

请注意,常规org.springframework.context.Lifecycle接口是显式启动和停止通知的普通约定,并不意味着在上下文刷新时自动启动。要对特定Bean的自动启动进行细粒度控制(包括启动阶段),请考虑实现org.springframework.context.SmartLifecycle

另外,请注意,停止通知不能保证在销毁之前发出。在常规关闭时,在传播一般销毁回调之前,所有生命周期Bean首先会收到停止通知。但是,在上下文生存期内的热刷新或停止的刷新尝试中,仅调用销毁方法。

启动和关闭调用的顺序可能很重要。如果任意两个对象之间存在“依赖”关系,则依赖方在其依赖项之后开始,并在其依赖项之前停止。然而,有时,直接依赖关系是未知的。您可能只知道某一类型的对象应该先于另一类型的对象开始。在这些情况下,SmartLifeccle接口定义了另一个选项,即在其超级接口分阶段上定义的get阶段()方法。下面的清单显示了分阶段接口的定义:

public interface Phased {

    int getPhase();
}

              
              

下面的清单显示了SmartLifeccle接口的定义:

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

              
              

启动时,具有最低阶段的对象首先启动。当停止时,遵循相反的顺序。因此,实现SmartLifeccle并且其getPhase()方法返回Integer.MIN_Value的对象将是最先启动、最后停止的对象之一。在频谱的另一端,相位值Integer.MAX_VALUE将指示对象应该最后启动并首先停止(可能是因为它依赖于其他正在运行的进程)。在考虑阶段值时,还必须知道没有实现SmartLifeccle的任何“正常”生命周期对象的默认阶段是0。因此,任何负的相位值都表示对象应在这些标准分量之前开始(并在它们之后停止)。对于任何正相位值,反之亦然。

SmartLifeccle定义的Stop方法接受回调。任何实现都必须在该实现的关闭过程完成后调用该回调的run()方法。这允许在必要时进行异步关闭,因为LifecycleProcessor接口的默认实现DefaultLifecycleProcessor等待每个阶段中的对象组调用该回调的超时值。每个阶段的默认超时时间为30秒。您可以通过在上下文中定义名为lifecycleProcessor的Bean来覆盖默认的生命周期处理器实例。如果您只想修改超时,则定义以下内容就足够了:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>
              
              

如前所述,LifecycleProcessor接口还定义了用于刷新和关闭上下文的回调方法。后者驱动关闭进程,就好像已经显式调用了top(),但它发生在上下文关闭时。另一方面,“刷新”回调启用了SmartLifeccleBean的另一个功能。当刷新上下文时(在所有对象被实例化和初始化之后),该回调被调用。此时,默认的生命周期处理器检查每个SmartLifeccle对象的isAutoStartup()方法返回的布尔值。如果为True,则在该点启动对象,而不是等待显式调用上下文或其自己的Start()方法(与上下文刷新不同,标准上下文实现不会自动启动上下文)。阶段值和任何“依赖关系”决定了启动顺序,如前所述。

Shutting Down the Spring IoC Container Gracefully in Non-Web Applications

本节仅适用于非Web应用程序。Spring的基于Web的ApplicationContext实现已经有了代码,可以在相关Web应用程序关闭时优雅地关闭Spring IOC容器。

如果您在非Web应用程序环境中使用Spring的IOC容器(例如,在富客户端桌面环境中),请向JVM注册一个关闭钩子。这样做可以确保正常关闭,并在单例Bean上调用相关的销毁方法,以便释放所有资源。您仍然必须正确配置和实现这些销毁回调。

若要注册关闭挂钩,请调用在ConfigurableApplicationContext接口上声明的registerShutdown Hook()方法,如下面的示例所示:

Java
Kotlin
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... } } 
              
              

1.6.2. ApplicationContextAware and 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。这些附加功能在ApplicationContext附加功能中进行了描述。

自动绑定是获取对ApplicationContext的引用的另一种方法。传统构造函数byType自动装配模式(如AuTower Collaborator中所述)可以分别为构造函数实参或setter方法参数提供ApplicationContext类型的依赖项。为了获得更大的灵活性,包括自动绑定字段和多参数方法的能力,可以使用基于注释的自动绑定特性。如果这样做,则ApplicationContext将自动连接到需要ApplicationContext类型的字段、构造函数实参或方法参数中,前提是有问题的字段、构造函数或方法带有@AuTower注释。有关更多信息,请参阅Using@Autwire

ApplicationContext创建实现org.springframework.beans.factory.BeanNameAware接口的类时,将为该类提供对其关联对象定义中定义的名称的引用。下面的清单显示了BeanNameAware接口的定义:

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

             
             

在填充普通Bean属性之后,但在初始化回调(如InitializingBean.afterPropertiesSet()或定制初始化方法)之前调用回调。

1.6.3. Other Aware Interfaces

除了ApplicationContextAwareBeanNameAware(前面已经讨论过)之外,Spring还提供了大量的感知回调接口,让Bean向容器指示它们需要某种基础设施依赖。作为一般规则,名称指示依赖项类型。下表总结了最重要的感知接口:

Table 4. Aware interfaces
Name Injected Dependency Explained in…​

ApplicationConextAware

声明ApplicationContext

ApplicationConextAwareBeanNameAware

ApplicationEventPublisherAware

封闭的ApplicationContext的事件发布者。

ApplicationContext的其他功能

BeanClassLoaderAware

用于加载Bean类的类加载器。

实例化Bean

BeanFactoryAware

声明BeanFactory

BeanFactory接口

BeanNameAware

声明Bean的名称。

ApplicationConextAwareBeanNameAware

LoadTimeWeverAware

定义的编织器,用于在加载时处理类定义。

在Spring框架中使用AspectJ进行加载时编织

MessageSourceAware

已配置解析消息的策略(支持参数化和国际化)。

ApplicationContext的其他功能

NotificationPublisherAware

Spring JMX通知发布器。

通知

ResourceLoaderAware

已配置加载程序以进行资源的低级别访问。

资源

ServletConfigAware

运行容器的当前ServletConfig。仅在可识别Web的SpringApplicationContext中有效。

Spring MVC

ServletContextAware

容器运行的当前ServletContext。仅在可识别Web的SpringApplicationContext中有效。

Spring MVC

再次注意,使用这些接口将您的代码绑定到Spring API,并且不遵循控制反转风格。因此,我们建议将它们用于需要以编程方式访问容器的基础设施Bean。

1.7. Bean Definition Inheritance

Bean定义可以包含大量配置信息,包括构造函数参数、属性值和容器特定的信息,如初始化方法、静态工厂方法名称等。子Bean定义从父定义继承配置数据。子定义可以根据需要覆盖某些值或添加其他值。使用父Bean和子Bean定义可以节省大量的输入。实际上,这是一种模板形式。

如果以编程方式使用ApplicationContext接口,则子Bean定义由ChildBeanDefinition类表示。大多数用户不会在此级别上使用它们。相反,它们在类(如ClassPathXmlApplicationContext)中以声明方式配置Bean定义。当您使用基于XML的配置元数据时,您可以通过使用属性来指示子Bean定义,并将父Bean指定为该属性的值。以下示例显示了如何执行此操作:

<bean id="inheritedTestBean" abstract="true" class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithDifferentClass" class="org.springframework.beans.DerivedTestBean" parent="inheritedTestBean" init-method="initialize">  (1)
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>
            
            
1 Note the parent attribute.

如果未指定,子Bean定义将使用父定义中的Bean类,但也可以覆盖它。在后一种情况下,子Bean类必须与父Bean类兼容(即,它必须接受父Bean类的属性值)。

子Bean定义继承父Bean的作用域、构造函数参数值、属性值和方法覆盖,并可以选择添加新值。您指定的任何作用域、初始化方法、销毁方法或静态工厂方法设置都会覆盖相应的父设置。

其余的设置总是从子定义中获取的:依赖、自动配线模式、依赖检查、单例和惰性初始化。

前面的示例通过使用抽象属性显式地将父Bean定义标记为抽象。如果父定义没有指定类,则需要将父Bean定义显式标记为抽象,如下例所示:

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean" parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>
            
            

父Bean不能单独实例化,因为它是不完整的,而且它还被显式标记为抽象。当定义是抽象时,它只能用作纯模板Bean定义,作为子定义的父定义。尝试单独使用此类抽象父Bean,方法是将其引用为另一个Bean的ref属性,或者使用父级Bean ID执行显式的getBean()调用,将返回错误。同样,容器的内部preInstantiateSingletons()方法会忽略定义为抽象的Bean定义。

ApplicationContext pre-instantiates all singletons by default. Therefore, it is important (at least for singleton beans) that if you have a (parent) bean definition which you intend to use only as a template, and this definition specifies a class, you must make sure to set the abstract attribute to true, otherwise the application context will actually (attempt to) pre-instantiate the abstract bean.

1.8. Container Extension Points

通常,应用程序开发人员不需要子类ApplicationContext实现类。相反,可以通过插入特殊集成接口的实现来扩展Spring IOC容器。接下来的几节将描述这些集成接口。

1.8.1. Customizing Beans by Using a BeanPostProcessor

BeanPostProcessor接口定义回调方法,您可以实现这些回调方法来提供您自己的(或覆盖容器的默认)实例化逻辑、依赖项解析逻辑等。如果您想在Spring容器完成实例化、配置和初始化一个Bean之后实现一些定制逻辑,您可以插入一个或多个定制的BeanPostProcessor实现。

您可以配置多个BeanPostProcessor实例,并且可以通过设置Order属性来控制这些BeanPostProcessor实例的运行顺序。只有当BeanPostProcessor实现有序接口时,才能设置此属性。如果您编写自己的BeanPostProcessor,也应该考虑实现Order接口。有关详细信息,请参阅BeanPostProcessor有序接口的javadoc。另请参阅编程注册BeanPostProcessor实例的说明。

BeanPostProcessor实例对Bean(或对象)实例进行操作。也就是说,Spring IOC容器实例化一个Bean实例,然后BeanPostProcessor实例执行它们的工作。

BeanPostProcessor实例的作用域为每个容器。这仅在您使用容器层次结构时才相关。如果在一个容器中定义BeanPostProcessor,则它只对该容器中的Bean进行后处理。换句话说,一个容器中定义的Bean不会由另一个容器中定义的BeanPostProcessor进行后处理,即使这两个容器属于同一层次结构。

要更改实际的Bean定义(即定义Bean的蓝图),您需要使用BeanFactoryPostProcessor,如使用BeanFactoryPostProcessor定制配置元数据中所述。

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的初始化,因此这种早期类型检测至关重要。

Programmatically registering BeanPostProcessor instances
While the recommended approach for BeanPostProcessor registration is through ApplicationContext auto-detection (as described earlier), you can register them programmatically against a ConfigurableBeanFactory by using the addBeanPostProcessor method. This can be useful when you need to evaluate conditional logic before registration or even for copying bean post processors across contexts in a hierarchy. Note, however, that BeanPostProcessor instances added programmatically do not respect the Ordered interface. Here, it is the order of registration that dictates the order of execution. Note also that BeanPostProcessor instances registered programmatically are always processed before those registered through auto-detection, regardless of any explicit ordering.
BeanPostProcessor instances and AOP auto-proxying

实现BeanPostProcessor接口的类是特殊的,容器以不同的方式对待它们。它们直接引用的所有BeanPostProcessor实例和Bean在启动时被实例化,作为ApplicationContext的特殊启动阶段的一部分。接下来,以排序的方式注册所有BeanPostProcessor实例,并将其应用于容器中的所有其他Bean。因为AOP自动代理是作为BeanPostProcessor本身实现的,所以无论是BeanPostProcessor实例还是它们直接引用的Bean都不符合自动代理的条件,因此不包含方面。

对于任何这样的Bean,您应该看到一条信息性日志消息:Bean某个Bean不适合由所有BeanPostProcessor接口处理(例如:不适合自动代理)

如果通过使用自动装配或@Resource将Bean连接到BeanPostProcessor中(这可能会退回到自动装配),则在搜索类型匹配的依赖项候选项时,Spring可能会访问意外的Bean,从而使它们不符合自动代理或其他类型的Bean后处理的条件。例如,如果您有一个用@Resource注释的依赖项,其中字段或setter名称不直接对应于Bean声明的名称,并且没有使用名称属性,则Spring访问其他Bean以按类型匹配它们。

以下示例显示如何在ApplicationContext中编写、注册和使用BeanPostProcessor实例。

Example: Hello World, BeanPostProcessor-style

第一个示例说明了基本用法。该示例显示了一个定制的BeanPostProcessor实现,该实现在容器创建每个Bean时调用它的toString()方法,并将结果字符串输出到系统控制台。

下面的清单显示了定制的BeanPostProcessor实现类定义:

Java
Kotlin
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; } } 
              
              

下面的Bean元素使用InstantiationTracingBeanPostProcessor

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

    <lang:groovy id="messenger" script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!-- when the above bean (messenger) is instantiated, this custom BeanPostProcessor implementation will output the fact to the system console -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>
              
              

请注意InstantiationTracingBeanPostProcessor是如何定义的。它甚至没有名称,而且因为它是一个Bean,所以可以像注入任何其他Bean一样注入依赖项。(前面的配置还定义了一个由Groovy脚本支持的Bean。有关Spring动态语言支持的详细信息,请参阅动态语言支持一章。)

下面的Java应用程序运行前面的代码和配置:

Java
Kotlin
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); } } 
              
              

前面应用程序的输出类似于以下内容:

Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
Example: The AutowiredAnnotationBeanPostProcessor

将回调接口或注释与定制的BeanPostProcessor实现结合使用是扩展Spring IOC容器的一种常见方法。一个例子是SpringBeanPostProcessor<AutowiredAnnotationBeanPostProcessor — a>BeanPostProcessor实现,它与Spring发行版一起提供,并自动绑定带注释的字段、setter方法和任意配置方法。

1.8.2. Customizing Configuration Metadata with a BeanFactoryPostProcessor

我们关注的下一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor.此接口的语义类似于BeanPostProcessor的语义,但有一个主要区别:BeanFactoryPostProcessor操作Bean配置元数据。也就是说,Spring IOC容器允许BeanFactoryPostProcessor读取配置元数据,并可能在容器实例化除BeanFactoryPostProcessor实例以外的任何Bean之前更改配置元数据。

您可以配置多个BeanFactoryPostProcessor实例,并且可以通过设置Order属性来控制这些BeanFactoryPostProcessor实例的运行顺序。但是,只有当BeanFactoryPostProcessor实现Order接口时,才能设置此属性。如果您编写自己的BeanFactoryPostProcessor,也应该考虑实现Order接口。有关更多详细信息,请参阅BeanFactoryPostProcessor有序接口的javadoc。

如果希望更改实际的Bean实例(即从配置元数据创建的对象),则需要使用BeanPostProcessor(前面在使用BeanPostProcessor定制Bean中进行了描述)。虽然在BeanFactoryPostProcessor中使用Bean实例在技术上是可能的(例如,使用BeanFactory.getBean()),但这样做会导致Bean过早实例化,从而违反标准容器生命周期。这可能会导致负面影响,例如绕过Bean后处理。

此外,BeanFactoryPostProcessor实例的作用域为每个容器。这仅在您使用容器层次结构时才相关。如果在一个容器中定义BeanFactoryPostProcessor,则它仅应用于该容器中的Bean定义。一个容器中的Bean定义不会由另一个容器中的BeanFactoryPostProcessor实例进行后处理,即使这两个容器属于同一层次结构。

当在ApplicationContext中声明Bean工厂后处理器时,它将自动运行,以便将更改应用于定义容器的配置元数据。Spring包括许多预定义的Bean工厂后处理器,例如PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer.例如,您还可以使用定制的BeanFactoryPostProcessor - 来注册定制的属性编辑器。

ApplicationContext自动检测部署到其中实现BeanFactoryPostProcessor接口的任何Bean。它在适当的时候使用这些Bean作为Bean工厂的后处理器。您可以像部署任何其他Bean一样部署这些后处理器Bean。

As with BeanPostProcessors , you typically do not want to configure BeanFactoryPostProcessors for lazy initialization. If no other bean references a Bean(Factory)PostProcessor, that post-processor will not get instantiated at all. Thus, marking it for lazy initialization will be ignored, and the Bean(Factory)PostProcessor will be instantiated eagerly even if you set the default-lazy-init attribute to true on the declaration of your <beans /> element.
Example: The Class Name Substitution PropertySourcesPlaceholderConfigurer

通过使用标准的Java<PropertySourcesPlaceholderConfigurer>Properties格式,可以使用代码将Bean定义中的属性值外部化到单独的文件中。这样做使部署应用程序的人员能够定制特定于环境的属性,如数据库URL和密码,而无需修改容器的一个或多个主XML定义文件的复杂性或风险。

考虑以下基于XML的配置元数据片段,其中定义了具有占位符值的DataSource

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
              
              

该示例显示从外部Properties文件配置的属性。在运行时,会将PropertySourcesPlaceholderConfigurer应用于替换数据源的某些属性的元数据。要替换的值被指定为${Property-name}形式的占位符,该形式遵循Ant、log4j和JSPEL样式。

实际值来自标准JavaProperties格式的另一个文件:

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

因此,${jdbc.username}字符串在运行时被替换为值‘sa’,这同样适用于与属性文件中的键匹配的其他占位符值。PropertySourcesPlaceholderConfigurer检查Bean定义的大多数属性和属性中的占位符。此外,您还可以自定义占位符前缀和后缀。

使用Spring2.5中引入的上下文命名空间,您可以使用专用的配置元素来配置属性占位符。您可以在Location属性中以逗号分隔列表的形式提供一个或多个位置,如下例所示:

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>
              
              

PropertySourcesPlaceholderConfigurer不仅在您指定的属性文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它将检查SpringEnvironment属性和常规Java系统属性。

您可以使用PropertySourcesPlaceholderConfigurer来替换类名,当您必须在运行时选择特定的实现类时,这有时很有用。以下示例显示了如何执行此操作:

<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/something/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.something.DefaultStrategy</value>
    </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>
                   
                   

如果无法在运行时将类解析为有效的类,则在将要创建该Bean时,该Bean的解析将失败,这是在非惰性初始化Bean的ApplicationContextpreInstantiateSingletons()阶段。

Example: The PropertyOverrideConfigurer

PropertyOverrideConfigurer是另一个Bean工厂后处理器,它类似于PropertySourcesPlaceholderConfigurer,,但与后者不同的是,原始定义可以有缺省值,也可以根本没有Bean属性的值。如果覆盖的Properties文件没有某个Bean属性的条目,则使用默认上下文定义。

请注意,Bean定义没有意识到被覆盖,因此从XML定义文件中看不出正在使用覆盖配置器。如果多个PropertyOverrideConfigurer实例为同一个Bean属性定义了不同的值,由于覆盖机制,最后一个将获胜。

属性文件配置行采用以下格式:

beanName.property=value

下面的清单显示了该格式的一个示例:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

此示例文件可以与容器定义一起使用,该容器定义包含名为DataSource的Bean,该Bean具有Driverurl属性。

还支持复合属性名称,只要路径的每个组件(被覆盖的最后一个属性除外)都已经是非空的(假设是由构造函数初始化的)。在下面的示例中,tomBean的fred属性的bob属性的Sammy属性设置为标量值123

tom.fred.bob.sammy=123
Specified override values are always literal values. They are not translated into bean references. This convention also applies when the original value in the XML bean definition specifies a bean reference.

通过在Spring2.5中引入的上下文命名空间,可以使用专用的配置元素配置属性覆盖,如下面的示例所示:

<context:property-override location="classpath:override.properties"/>
              
              

1.8.3. Customizing Instantiation Logic with a FactoryBean

您可以为本身是工厂的对象实现org.springframework.beans.factory.FactoryBean接口。

FactoryBean接口是可插入点到Spring IOC容器的实例化逻辑中。如果您有更好地用Java表示的复杂初始化代码,而不是(可能)冗长的XML,您可以创建自己的FactoryBean,在该类中编写复杂的初始化,然后将定制的FactoryBean插入容器。

FactoryBean<;T>;接口提供三种方法:

  • T getObject():返回该工厂创建的对象的实例。该实例可能是共享的,这取决于该工厂返回的是单例还是原型。

  • boolean isSingleton():如果这个FactoryBean返回单例,则返回True,否则返回False。此方法的默认实现返回true

  • Class<;?>;getObtType():返回getObject()方法返回的对象类型,如果事先不知道类型,则返回NULL

FactoryBean概念和接口在Spring框架中的许多地方使用。超过50个FactoryBean接口的实现随Spring本身一起提供。

如果需要向容器请求实际的FactoryBean实例本身,而不是它生成的Bean,则在调用ApplicationContextgetBean()方法时,在Bean的id前面加上与号(&;)。因此,对于idmyBean的给定FactoryBean,在容器上调用getBean(“myBean”)将返回FactoryBean的乘积,而调用getBean(“&;myBean”)将返回FactoryBean实例本身。

1.9. Annotation-based Container Configuration

Are annotations better than XML for configuring Spring?

基于注释的配置的引入引发了这样一个问题:这种方法是否比XML“更好”。简短的回答是“视情况而定”。长篇大论的答案是,每种方法都有其优缺点,通常情况下,由开发人员决定哪种策略更适合他们。由于它们的定义方式,注释在其声明中提供了许多上下文,从而导致更短和更简洁的配置。然而,XML擅长在不接触组件源代码或重新编译组件的情况下将其组合在一起。一些开发人员喜欢将连接放在靠近源代码的地方,而另一些开发人员则认为带注释的类不再是POJO,而且配置变得分散且更难控制。

无论选择哪种风格,Spring都可以适应这两种风格,甚至可以将它们混合在一起。值得一提的是,通过其JavaConfig选项,Spring允许以非侵入性的方式使用批注,而无需接触目标组件源代码,并且在工具方面,所有配置样式都受Spring Tools for Eclipse支持。

XML设置的另一种选择是基于注释的配置,它依赖于用于连接组件的字节码元数据,而不是尖括号声明。开发人员不是使用XML来描述Bean连接,而是通过使用相关类、方法或字段声明上的注释将配置移动到组件类本身。正如示例中提到的:将<AutowiredAnnotationBeanPostProcessor,>BeanPostProcessor与注释结合使用是扩展Spring IOC容器的一种常见方法。例如,Spring2.5引入了一种基于注释的方法来驱动Spring的依赖注入。从本质上讲,@Autwire注释提供了与AuTower Collaborator中描述的相同功能,但具有更细粒度的控制和更广泛的适用性。Spring2.5还增加了对JSR-250注释的支持,比如@PostConstruct@PreDestroy。Spring3.0添加了对jakarta.inject包中包含的JSR-330(Java的依赖项注入)批注的支持,如@Inject@命名的。有关这些注释的详细信息,请参阅相关部分

注释注入是在XML注入之前执行的。因此,XML配置覆盖了通过这两种方法连接的属性的注释。

像往常一样,您可以将后处理器注册为单独的Bean定义,但也可以通过在基于XML的Spring配置中包含以下标记来隐式注册它们(请注意,包含了上下文名称空间):

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

    <context:annotation-config/>

</beans>
            
            

<;context:annotation-config/>;元素隐式注册以下后处理器:

<;context:annotation-config/>;只在定义它的同一应用程序上下文中查找Bean上的注释。这意味着,如果您将<;context:annotation-config/>;放在DispatcherServletWebApplicationContext中,它只检查控制器中的@AutwireBean,而不检查您的服务。有关详细信息,请参阅DispatcherServlet

1.9.1. Using @Autowired

在本节中包含的示例中,可以使用JSR330的@Inject批注来代替Spring的@AuTower批注。有关更多详细信息,请参阅此处

您可以将@AuTower批注应用于构造函数,如下面的示例所示:

Java
Kotlin
public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

             
             

从Spring Framework4.3开始,如果目标Bean一开始只定义了一个构造函数,则不再需要在这样的构造函数上使用@AuTower注释。但是,如果有多个构造函数可用,并且没有主/默认构造函数,则必须使用@Autwire注释至少一个构造函数,以便指示容器使用哪个构造函数。有关详细信息,请参阅构造函数解析的讨论。

您还可以对传统的setter方法应用@AuTower批注,如下面的示例所示:

Java
Kotlin
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

             
             

还可以将批注应用于具有任意名称和多个参数的方法,如下面的示例所示:

Java
Kotlin
public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

             
             

您也可以将@AuTower应用于字段,甚至可以将其与构造函数混合使用,如下例所示:

Java
Kotlin
public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

             
             

确保您的目标组件(例如,MovieCatalogCustomerPferenceDao)一致地由您用于@AuTower注解的注入点的类型声明。否则,注入可能会在运行时由于“未找到类型匹配”错误而失败。

对于通过类路径扫描找到的XML定义的Bean或组件类,容器通常预先知道具体类型。然而,对于@Bean工厂方法,您需要确保声明的返回类型具有足够的表现力。对于实现多个接口的组件或其实现类型可能引用的组件,请考虑在工厂方法上声明最具体的返回类型(至少与引用您的Bean的注入点所要求的那样具体)。

您还可以指示Spring从ApplicationContext中提供特定类型的所有Bean,方法是向需要该类型数组的字段或方法添加@AuTower注释,如下面的示例所示:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

             
             

类型化集合也是如此,如下面的示例所示:

Java
Kotlin
public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

             
             

如果希望按特定顺序对数组或列表中的项进行排序,则目标Bean可以实现org.springfrawork.core.Orded接口,或者使用@Order或标准的@Priority注释。否则,它们的顺序遵循容器中相应目标Bean定义的注册顺序。

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

注意,在@Bean级别上,标准的jakarta.注解注释不可用,因为它不能在方法上声明。它的语义可以通过@Order值与每种类型的单个Bean上的@Primary相结合来建模。

即使是类型化的Map实例也可以自动连接,只要预期的键类型是字符串。映射值包含预期类型的所有Bean,键包含相应的Bean名称,如下例所示:

Java
Kotlin
public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

             
             

默认情况下,当没有匹配的候选Bean可用于给定的注入点时,自动装配将失败。对于声明的数组、集合或映射,需要至少一个匹配的元素。

默认行为是将带注释的方法和字段视为指示必需的依赖项。您可以更改此行为,如下面的示例所示,通过将无法满足的注入点标记为非必需(即,将@Autwire中的Required属性设置为False),使框架能够跳过该注入点:

Java
Kotlin
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

             
             

如果非必需方法的依赖项(或其依赖项之一,如果有多个参数)不可用,则根本不会调用该方法。在这种情况下,根本不会填充非必填字段,而是保留其缺省值。

换句话说,将Required属性设置为FALSE表示对应的属性对于自动连接而言是可选的,如果无法自动连接该属性,则将忽略该属性。这允许为属性分配缺省值,这些缺省值可以通过依赖项注入选择性地覆盖。

注入的构造函数和工厂方法参数是一个特例,因为由于Spring的构造函数解析算法可能会处理多个构造函数,@AuTower中的Required属性的含义略有不同。构造函数和工厂方法参数在默认情况下是有效的,但在单构造函数场景中有一些特殊规则,例如,如果没有匹配的Bean,则多元素注入点(数组、集合、映射)解析为空实例。这允许使用通用的实现模式,其中所有依赖项都可以在唯一的多参数构造函数中声明--例如,声明为单个公共构造函数,而不使用@Autwire注释。

任何给定Bean类的只有一个构造函数可以声明@AuTower,并将Required属性设置为true,指示构造函数在用作Spring Bean时自动连接。因此,如果将Required属性保留为其缺省值true,则只有一个构造函数可以用@Autwire进行注释。如果多个构造函数声明了注释,则它们都必须声明Required=FALSE才能被视为自动装配的候选函数(类似于XML中的autwire=Construction tor)。将选择能够由Spring容器中的匹配Bean满足的依赖项数量最多的构造函数。如果所有候选函数都不能满足要求,则将使用主/默认构造函数(如果存在)。类似地,如果一个类声明了多个构造函数,但没有一个用@AuTower注释,则将使用主/默认构造函数(如果存在)。如果一个类一开始只声明了一个构造函数,那么即使没有注释,它也会一直被使用。请注意,带注释的构造函数不必是公共的。

或者,您可以通过Java 8的java.util.Optional来表示特定依赖项的非必需性质,如下面的示例所示:

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

             
             

从Spring Framework5.0开始,您还可以使用< >@Nullable注释(任何类型的包JSR- 中的任何类型的注释,例如来自JSR-305的javax.Annotation.Nullable),或者仅仅利用Kotlin内置的空安全支持:

Java
Kotlin
public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

             
             

对于众所周知的可解析依赖的接口,您也可以使用@AuTowerBeanFactoryApplicationContextEnvironmentResourceLoaderApplicationEventPublisherMessageSource。这些接口及其扩展接口(如ConfigurableApplicationContextResourcePatternResolver)是自动解析的,不需要特殊设置。下面的示例自动生成一个ApplicationContext对象:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

             
             

@AuTower@Inject@Value@Resource注释由SpringBeanPostProcessor实现处理。这意味着您不能在您自己的BeanPostProcessorBeanFactoryPostProcessor类型(如果有)中应用这些批注。这些类型必须通过使用XML或Spring@Bean方法显式‘连接起来’。

1.9.2. Fine-tuning Annotation-based Autowiring with @Primary

由于按类型自动组合可能会导致多个候选人,因此通常有必要对选择过程进行更多控制。实现这一点的一种方法是使用Spring的@Primary注释。@Primary指示当多个Bean是自动连接到单值依赖项的候选时,应该优先考虑特定的Bean。如果候选对象中只存在一个主Bean,则它将成为自动配置值。

考虑将FirstMovieCatalog定义为主MovieCatalog的以下配置:

Java
Kotlin
@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...
}

             
             

在上述配置中,以下MovieRecommenderFirstMovieCatalog一起自动配置:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

             
             

相应的Bean定义如下:

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
             
             

1.9.3. Fine-tuning Annotation-based Autowiring with Qualifiers

@Primary是使用按类型自动装配的一种有效方法,在可以确定一个主要候选者的情况下具有多个实例。当您需要对选择过程进行更多控制时,可以使用Spring的@限定符注释。您可以将限定符值与特定参数相关联,从而缩小类型匹配的范围,以便为每个参数选择一个特定的Bean。在最简单的情况下,这可以是一个简单的描述性值,如下例所示:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

             
             
1 The bean with the main qualifier value is wired with the constructor argument that is qualified with the same value.
2 The bean with the action qualifier value is wired with the constructor argument that is qualified with the same value.

您还可以在各个构造函数参数或方法参数上指定@限定符注释,如下例所示:

Java
Kotlin
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;
    }

    // ...
}

             
             
1 This line adds the @Offline annotation.

下面的示例显示了相应的Bean定义。

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main"/> (1)

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> (2)

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
             
             

对于备用匹配,Bean名称被视为默认限定符值。因此,您可以使用mainid而不是嵌套的限定符元素来定义Bean,从而产生相同的匹配结果。然而,尽管您可以使用此约定通过名称来引用特定的Bean,但@Autwire本质上是关于带有可选语义限定符的类型驱动注入。这意味着,即使使用Bean名称回退,限定符的值在类型匹配集中也总是具有缩小的语义。它们不在语义上表示对唯一Beanid的引用。好的限定符的值是mainEMEAPersistent,表示独立于Beanid的特定组件的特征,这些特征可以在匿名Bean定义的情况下自动生成,如上例中的定义。

限定符也适用于类型化集合,例如, - 适用于set<;MovieCatalog>;。在这种情况下,根据声明的限定符,所有匹配的Bean都作为集合注入。这意味着限定符不必是唯一的。相反,它们构成了过滤标准。例如,您可以使用相同的限定符值“action”定义多个MovieCatalogBean,所有这些Bean都被注入到集<;MovieCatalog>;中,该集合用@Qualifier(“action”)注释。

在类型匹配的候选对象中,让限定符值针对目标Bean名称进行选择,在注入点不需要@限定符注释。如果没有其他解析指示符(如限定符或主标记),对于非唯一依赖情况,Spring会将注入点名称(即字段名或参数名)与目标Bean名称进行匹配,并选择同名的候选对象(如果有的话)。

也就是说,如果您打算按名称表示注释驱动的注入,则不要主要使用@Autwire,即使它能够按类型匹配的候选对象中的Bean名称进行选择。相反,可以使用JSR-250@Resource注释,该注释在语义上定义为通过唯一名称标识特定的目标组件,声明的类型与匹配过程无关。@AuTower具有非常不同的语义:在按类型选择候选Bean之后,指定的字符串限定符值只在那些类型选择的候选中考虑(例如,将Account限定符与标记了相同限定符标签的Bean进行匹配)。

对于定义为集合、映射或数组类型的Bean,@Resource是一个很好的解决方案,它通过唯一的名称引用特定的集合或数组Bean。也就是说,从4.3开始,您也可以通过Spring的@Autwire类型匹配算法匹配集合、Map和数组类型,只要元素类型信息保留在@Bean返回类型签名或集合继承层次结构中。在这种情况下,您可以使用限定符值在相同类型的集合中进行选择,如上一段所述。

从4.3开始,@Autwire还考虑了注入的自引用(即对当前注入的Bean的引用)。请注意,自我注入是一种退路。对其他组件的常规依赖项始终具有优先级。从这个意义上说,自我引用不参与常规的候选人选择,因此尤其不是主要的。相反,它们最终总是排在最低的优先级。在实践中,您应该只将自引用用作最后手段(例如,通过Bean的事务代理调用同一实例上的其他方法)。在这种情况下,可以考虑将受影响的方法分解到单独的委托Bean中。或者,您可以使用@Resource,它可以通过当前Bean的唯一名称获取一个返回当前Bean的代理。

尝试将@Bean方法的结果注入到相同的配置类中也是一种有效的自引用场景。要么在实际需要的方法签名中懒洋洋地解析这种引用(与配置类中的自动连接字段相反),要么将受影响的@Bean方法声明为静态,将它们从包含的配置类实例及其生命周期中分离出来。否则,此类Bean仅在备用阶段被考虑,其他配置类上的匹配Bean被选为主要候选(如果可用)。

@AuTower适用于字段、构造函数和多参数方法,允许在参数级别通过限定符注释缩小范围。相反,@Resource仅支持具有单个参数的字段和Bean属性设置器方法。因此,如果注入目标是构造函数或多参数方法,则应坚持使用限定符。

您可以创建自己的自定义限定符注释。为此,请定义一个批注并在定义中提供@限定符批注,如下面的示例所示:

Java
Kotlin
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

    String value();
}

             
             
1 This line adds the @Offline annotation.

然后,您可以为自动连接的字段和参数提供自定义限定符,如下例所示:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    @Genre("Action")
    private MovieCatalog actionCatalog;

    private MovieCatalog comedyCatalog;

    @Autowired
    public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
        this.comedyCatalog = comedyCatalog;
    }

    // ...
}

             
             
1 This element specifies the qualifier.

接下来,您可以提供候选Bean定义的信息。您可以将<;限定符/标记添加为<;Bean/>;标记的子元素,然后指定类型以匹配您的自定义限定符注释。该类型与批注的完全限定类名匹配。或者,为了方便起见,如果不存在名称冲突的风险,则可以使用短类名称。下面的示例演示了这两种方法:

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>
             
             

Classpath Scanning and Managed Components中,您可以看到在XML中提供限定符元数据的基于注释的替代方法。具体地说,请参阅为限定符元数据提供注释

在某些情况下,使用不带值的注释可能就足够了。当注释服务于更一般的用途并且可以应用于几种不同类型的依赖项时,这会很有用。例如,您可以提供一个脱机目录,在没有可用的Internet连接时可以搜索该目录。首先,定义简单注释,如下例所示:

Java
Kotlin
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}

             
             

然后将注释添加到要自动连接的字段或属性,如下例所示:

Java
Kotlin
public class MovieRecommender {

    @Autowired
    @Offline (1)
    private MovieCatalog offlineCatalog;

    // ...
}

             
             

现在,Bean定义只需要一个限定符type,如下例所示:

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/> (1)
    <!-- inject any dependencies required by this bean -->
</bean>
             
             

除了简单的属性之外,您还可以定义接受命名属性的自定义限定符批注,或者接受命名属性。如果随后在要自动连接的字段或参数上指定了多个属性值,则Bean定义必须与所有这些属性值匹配才能被视为自动连接候选对象。例如,考虑以下注释定义:

Java
Kotlin
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

    String genre();

    Format format();
}

             
             

在本例中,格式是一个枚举,定义如下:

Java
Kotlin
public enum Format {
    VHS, DVD, BLURAY
}

             
             

要自动绑定的字段使用自定义限定符进行注释,并包含两个属性的值:genre格式,如下例所示:

Java
Kotlin
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;

    // ...
}

             
             

最后,Bean定义应该包含匹配的限定符值。该示例还演示了您可以使用Bean元属性来代替<;限定符/>;元素。如果可用,则优先使用<;限定符/<;元素及其属性,但如果不存在<;meta/>;标记中提供的值,则自动装配机制将依赖于此类限定符,如下例中的最后两个Bean定义所示:

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

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

</beans>
             
             

1.9.4. Using Generics as Autowiring Qualifiers

除了@限定符注释之外,您还可以使用Java泛型类型作为隐式限定形式。例如,假设您有以下配置:

Java
Kotlin
@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

             
             

假设前面的Bean实现了泛型接口(即Store<;String>;Store<;Integer>;),您可以@AutwireStore接口,将泛型用作限定符,如下例所示:

Java
Kotlin
@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean

             
             

当自动连接列表、映射实例和数组时,泛型限定符也适用。下面的示例自动生成泛型列表

Java
Kotlin
// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;

             
             

1.9.5. Using CustomAutowireConfigurer

CustomAutowireConfigurer是一个 BeanFactoryPostProcessor,它允许您注册自己的定制限定符批注类型,即使它们没有用Spring的 @限定符批注进行批注。以下示例显示如何使用 CustomAuTower ireConfigurer

<bean id="customAutowireConfigurer" class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>
             
             

AuowireCandidate Resolver通过以下方式确定自动布线候选对象:

  • 每个Bean定义的AUTOWIRE-CADERATE值

  • <;Beans/&>元素上可用的任何默认自动配线候选模式

  • 存在@限定符批注和向CustomAuTower ireConfigurer注册的任何自定义批注

当多个Bean符合自动布线候选条件时,“主”的确定如下:如果候选中恰好有一个Bean定义的属性设置为true,则选择它。

1.9.6. Injection with @Resource

Spring还通过在字段或Bean属性设置器方法上使用JSR-250@Resource注释(jakarta.nottation.Resource)来支持注入。这是Jakarta EE中的常见模式:例如,在JSF管理的Bean和JAX-WS端点中。对于Spring管理的对象,Spring也支持这种模式。

@Resource接受名称属性。默认情况下,Spring将该值解释为要注入的Bean名称。换句话说,它遵循按名称语义,如下例所示:

Java
Kotlin
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") (1)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

             
             
1 This line injects a @Resource.

如果未显式指定名称,则默认名称派生自字段名或setter方法。如果是字段,则采用字段名。对于setter方法,它接受Bean属性名。下面的示例将把名为movieFinder的Bean注入其setter方法:

Java
Kotlin
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

             
             
1 The context field is injected based on the known resolvable dependency type: ApplicationContext.
The name provided with the annotation is resolved as a bean name by the ApplicationContext of which the CommonAnnotationBeanPostProcessor is aware. The names can be resolved through JNDI if you configure Spring’s SimpleJndiBeanFactory explicitly. However, we recommend that you rely on the default behavior and use Spring’s JNDI lookup capabilities to preserve the level of indirection.

@Resource使用而未指定显式名称的独占情况下,类似于@AuTower@Resource查找主类型匹配而不是特定的命名Bean,并解析众所周知的可解析依赖项:BeanFactoryApplicationContextResourceLoaderApplicationEventPublisherMessageSource接口。

因此,在下面的示例中,CustomerPferenceDao字段首先查找名为“CustomerPferenceDao”的Bean,然后回退到类型CustomerPferenceDao的主类型匹配:

Java
Kotlin
public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; (1)

    public MovieRecommender() {
    }

    // ...
}

             
             
1 The context field is injected based on the known resolvable dependency type: ApplicationContext.

1.9.7. Using @Value

@Value通常用于注入外部化属性:

Java
Kotlin
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

             
             

具有以下配置:

Java
Kotlin
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

             
             

和以下Applation.Properties文件:

catalog.name=MovieCatalog

             
             

在这种情况下,目录参数和字段将等于MovieCatalog值。

默认的宽松嵌入式值解析器是由Spring提供的。它将尝试解析属性值,如果无法解析,则将属性名称(例如${Catalog.name})作为值注入。如果您希望严格控制不存在的值,则应该声明一个PropertySourcesPlaceholderConfigurerBean,如下例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

             
             
When configuring a PropertySourcesPlaceholderConfigurer using JavaConfig, the @Bean method must be static.

如果无法解析任何${}占位符,使用上述配置可确保Spring初始化失败。还可以使用setPlaceholderPrefixsetPlaceholderSuffixsetValueSeparator等方法来自定义占位符。

Spring Boot configures by default a PropertySourcesPlaceholderConfigurer bean that will get properties from application.properties and application.yml files.

Spring提供的内置转换器支持允许自动处理简单的类型转换(例如,到Integerint)。多个逗号分隔值可以自动转换为字符串数组,而不需要额外的工作。

可以提供如下缺省值:

Java
Kotlin
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
        this.catalog = catalog;
    }
}

             
             

SpringBeanPostProcessor在后台使用ConversionService来处理将@Value中的字符串值转换为目标类型的过程。如果您想为您自己的自定义类型提供转换支持,您可以提供您自己的ConversionServiceBean实例,如下例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}

             
             

@Value包含Spel表达式时,将在运行时动态计算值,如下例所示:

Java
Kotlin
@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}

             
             

SPEL还支持使用更复杂的数据结构:

Java
Kotlin
@Component
public class MovieRecommender {

    private final Map<String, Integer> countOfMoviesPerCatalog;

    public MovieRecommender( @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
        this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
    }
}

             
             

1.9.8. Using @PostConstruct and @PreDestroy

Java
Kotlin
public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}

             
             

有关组合各种生命周期机制的效果的详细信息,请参阅组合生命周期机制。

@Resource一样,@PostConstruct@PreDestroy注释类型是从JDK 6到8的标准Java库的一部分。然而,整个javax.注解包从JDK 9中的核心Java模块中分离出来,并最终从JDK 11中删除。从Jakarta EE 9开始,该包现在位于jakarta.notation中。如果需要,现在需要通过Maven Central获取jakarta.notation-api构件,只需像添加其他库一样将其添加到应用程序的类路径。

1.10. Classpath Scanning and Managed Components

本章中的大多数示例使用XML来指定在Spring容器中生成每个BeanDefinition的配置元数据。上一节(基于注释的容器配置)演示了如何通过源代码级别的注释提供大量配置元数据。然而,即使在这些示例中,“基本”Bean定义也是在XML文件中显式定义的,而注释仅驱动依赖项注入。本节介绍通过扫描类路径隐式检测候选组件的选项。候选组件是与筛选条件匹配并在容器中注册了相应的Bean定义的类。这样就不需要使用XML来执行Bean注册。相反,您可以使用注释(例如,@Component)、AspectJ类型表达式或您自己的定制筛选条件来选择哪些类注册了容器中的Bean定义。

从Spring3.0开始,由Spring JavaConfig项目提供的许多特性都是核心Spring框架的一部分。这允许您使用Java而不是使用传统的XML文件来定义Bean。查看@configuration@Bean@Import@DependsOn注释,了解如何使用这些新功能的示例。

1.10.1. @Component and Further Stereotype Annotations

@Repository注释是实现存储库角色或构造型(也称为数据访问对象或DAO)的任何类的标记。此标记的用途之一是自动转换异常,如异常转换中所述。

Spring提供了进一步的原型注释:@Component@Service@Controller@Component是任何Spring管理的组件的通用构造型。@Repository@Service@Controller@Component针对更具体用例(分别位于持久层、服务层和表示层)的专门化。因此,您可以使用@Component来注释您的组件类,但是,通过使用@Repository@Service@控制器来注释它们,您的类更适合由工具处理或与方面关联。例如,这些构造型注释是切入点的理想目标。@Repository@Service@Controller还可以在Spring框架的未来版本中提供额外的语义。因此,如果您选择使用@Component@Service作为服务层,@Service显然是更好的选择。类似地,如前所述,@Repository已经被支持作为持久层中自动异常转换的标记。

1.10.2. Using Meta-annotations and Composed Annotations

Spring提供的许多批注可以在您自己的代码中用作元批注。元批注是可以应用于其他批注的批注。例如,前面提到的@Service批注用@Component进行了元批注,如下例所示:

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

    // ...
}

             
             
1 The @Component causes @Service to be treated in the same way as @Component.

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

此外,合成批注可以选择性地重新声明元批注中的属性,以允许定制。当您只想公开元注释属性的一个子集时,这可能特别有用。例如,Spring的@SessionScope注释将作用域名硬编码为会话,但仍然允许定制proxyModel。下面的清单显示了SessionScope注释的定义:

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

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

}

             
             

然后,您可以使用@SessionScope,而无需声明proxyMode,如下所示:

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

             
             

您还可以重写proxyMode的值,如下面的示例所示:

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

             
             

有关更多详细信息,请参阅Spring Annotation Programming Model维基页面。

1.10.3. Automatically Detecting Classes and Registering Bean Definitions

Spring可以自动检测构造型类,并使用ApplicationContext注册相应的BeanDefinition实例。例如,以下两个类别有资格进行此类自动检测:

Java
Kotlin
@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

             
             
Java
Kotlin
@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}

             
             

要自动检测这些类并注册相应的Bean,您需要将@ComponentScan添加到您的@Configuration类中,其中BasPackages属性是这两个类的公共父包。(或者,您可以指定一个逗号、分号或空格分隔的列表,其中包括每个类的父包。)

Java
Kotlin
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
    // ...
}

             
             
For brevity, the preceding example could have used the value attribute of the annotation (that is, @ComponentScan("org.example")).

以下替代方案使用XML:

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

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

</beans>
             
             
The use of <context:component-scan> implicitly enables the functionality of <context:annotation-config>. There is usually no need to include the <context:annotation-config> element when using <context:component-scan>.

扫描类路径包需要在类路径中存在相应的目录条目。当您使用Ant构建JAR时,请确保没有激活JAR任务的仅文件开关。此外,根据安全策略,类路径目录可能不会在某些class=“bare”>https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).- - 环境中公开。例如,JDK 1.7.0_45及更高版本上的独立应用程序(这需要在清单 - 中设置‘Trusted- ’。请参阅

在JDK 9的模块路径(Jigsaw)上,Spring的类路径扫描通常按预期工作。但是,请确保在模块信息描述符中导出组件类。如果您希望Spring调用类的非公共成员,请确保它们是“打开的”(即,它们在模块信息描述符中使用Open声明,而不是exports声明)。

此外,当您使用Component-Scan元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor都被隐式包括在内。这意味着自动检测这两个组件并将它们连接在一起 - ,而不需要在XML语言中提供任何Bean配置元数据。

You can disable the registration of AutowiredAnnotationBeanPostProcessor and CommonAnnotationBeanPostProcessor by including the annotation-config attribute with a value of false.

1.10.4. Using Filters to Customize Scanning

默认情况下,使用@Component@Repository@Service@控制器@Configuration或本身使用@Component注释的自定义注释的类是唯一检测到的候选组件。但是,您可以通过应用自定义筛选器来修改和扩展此行为。将它们添加为@ComponentScan批注的cludeFiltersexcludeFilters属性(或作为<;上下文:Include-Filter/&><;上下文:&><;元素的子元素在XML配置中添加)。每个筛选器元素都需要类型表达式属性。下表描述了过滤选项:

Table 5. Filter Types
Filter Type Example Expression Description

批注(默认)

org.example.SomeAnnotation

要在目标组件的类型级别呈现呈现元呈现的批注。

可分配

org.example.SomeClass

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

AspectJ

org.Example..*服务+

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

正则化

org\.Example\.Default.*

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

自定义

org.example.MyTypeFilter

org.springframework.core.type.TypeFilter接口的自定义实现。

以下示例显示该配置忽略所有@Repository注释,而改用“存根”存储库:

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

             
             

下面的清单显示了等价的XML:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex" expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>
             
             
You can also disable the default filters by setting useDefaultFilters=false on the annotation or by providing use-default-filters="false" as an attribute of the <component-scan/> element. This effectively disables automatic detection of classes annotated or meta-annotated with @Component, @Repository, @Service, @Controller, @RestController, or @Configuration.

1.10.5. Defining Bean Metadata within Components

Spring组件还可以向容器提供Bean定义元数据。您可以使用相同的@Bean注释来实现这一点,该注释用于在@configuration注释类中定义Bean元数据。以下示例显示了如何执行此操作:

Java
Kotlin
@Component
public class FactoryMethodComponent {

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

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

             
             

前面的类是一个Spring组件,它的doWork()方法中有特定于应用程序的代码。但是,它还提供了一个具有工厂方法的Bean定义,该工厂方法引用方法Public Instance()@Bean注释标识工厂方法和其他Bean定义属性,例如通过@Qualifier注释标识限定符值。可以指定的其他方法级批注有@Scope@Lazy和定制限定符批注。

In addition to its role for component initialization, you can also place the @Lazy annotation on injection points marked with @Autowired or @Inject. In this context, it leads to the injection of a lazy-resolution proxy. However, such a proxy approach is rather limited. For sophisticated lazy interactions, in particular in combination with optional dependencies, we recommend ObjectProvider<MyTargetBean> instead.

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

Java
Kotlin
@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);
    }
}

             
             

该示例将字符串方法参数Country自动绑定为另一个名为Private ateInstance的Bean的age属性的值。Spring表达式语言元素通过符号#{<;Expression>;}定义属性的值。对于@Value注释,表达式解析器被预配置为在解析表达式文本时查找Bean名称。

从Spring Framework4.3开始,您还可以声明InjectionPoint类型的工厂方法参数(或其更具体的子类:DependencyDescriptor),以访问触发当前Bean创建的请求注入点。请注意,这仅适用于实际创建的Bean实例,而不适用于注入现有实例。因此,此功能对Prototype范围的Bean最有意义。对于其他作用域,工厂方法只看到在给定作用域中触发创建新Bean实例的注入点(例如,触发创建惰性单例Bean的依赖项)。在这样的场景中,您可以使用提供的注入点元数据和语义关怀。下面的示例说明如何使用InjectionPoint

Java
Kotlin
@Component
public class FactoryMethodComponent {

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

             
             

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

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

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

@Bean方法的Java语言可见性不会对Spring容器中的结果Bean定义产生直接影响。您可以在非@configuration类中自由声明您的工厂方法,也可以在任何地方声明静态方法。但是,@Configuration类中的常规@Bean方法需要是可重写的 - ,即它们不能声明为私有FINAL

@Bean方法也可以在给定组件或配置类的基类上发现,也可以在组件或配置类实现的接口中声明的Java 8默认方法上发现。这允许在组成复杂的配置安排时有很大的灵活性,甚至可以通过从Spring4.2开始的Java 8默认方法实现多重继承。

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

1.10.6. Naming Autodetected Components

当组件作为扫描过程的一部分被自动检测时,它的Bean名称由该扫描器已知的BeanNameGenerator策略生成。默认情况下,任何包含名称的Spring构造型注释(@Component@Repository@Service@Controller)都会将该名称提供给相应的Bean定义。

如果这样的注释不包含名称或任何其他检测到的组件(例如由定制筛选器发现的组件),则默认的Bean名称生成器返回未大写的非限定类名。例如,如果检测到以下组件类,则名称为myMovieListermovieFinderImpl

Java
Kotlin
@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}

             
             
Java
Kotlin
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

             
             

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

If you run into naming conflicts due to multiple autodetected components having the same non-qualified class name (i.e., classes with identical names but residing in different packages), you may need to configure a BeanNameGenerator that defaults to the fully qualified class name for the generated bean name. As of Spring Framework 5.2.3, the FullyQualifiedAnnotationBeanNameGenerator located in package org.springframework.context.annotation can be used for such purposes.
Java
Kotlin
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}

             
             
<beans>
    <context:component-scan base-package="org.example" name-generator="org.example.MyNameGenerator" />
</beans>
             
             

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

1.10.7. Providing a Scope for Autodetected Components

与通常的Spring管理的组件一样,自动检测的组件的默认和最常见的作用域是Singleton。但是,有时您需要一个可以由@Scope注释指定的不同作用域。您可以在注释中提供作用域的名称,如下例所示:

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

             
             
@Scope annotations are only introspected on the concrete bean class (for annotated components) or the factory method (for @Bean methods). In contrast to XML bean definitions, there is no notion of bean definition inheritance, and inheritance hierarchies at the class level are irrelevant for metadata purposes.

有关特定于Web的作用域的详细信息,如Spring上下文中的“请求”或“会话”,请参阅请求、会话、应用程序和WebSocket作用域。与那些作用域的预先构建的批注一样,您也可以通过使用Spring的元批注方法来组成您自己的作用域批注:例如,使用@Scope(“Prototype”)进行元批注的定制批注,可能还声明了一个定制的作用域代理模式。

To provide a custom strategy for scope resolution rather than relying on the annotation-based approach, you can implement the ScopeMetadataResolver interface. Be sure to include a default no-arg constructor. Then you can provide the fully qualified class name when configuring the scanner, as the following example of both an annotation and a bean definition shows:
Java
Kotlin
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}

             
             
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>
             
             

在使用某些非单一作用域时,可能需要为作用域对象生成代理。推理在作用域Bean中描述为依赖项。为此,Component-Scan元素上提供了Scope-Proxy属性。这三个可能的值是:no接口Target Class。例如,以下配置会产生标准的JDK动态代理:

Java
Kotlin
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    // ...
}

             
             
<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>
             
             

1.10.8. Providing Qualifier Metadata with Annotations

带限定符的基于微调注释的自动装配中讨论了@限定符注释。该部分中的示例演示了@限定符注释和定制限定符注释的使用,以便在解析自动布线候选对象时提供细粒度的控制。因为这些示例基于XMLBean定义,所以通过使用XML中Bean元素的限定符meta子元素在候选Bean定义上提供限定符元数据。当依赖类路径扫描来自动检测组件时,您可以为限定符元数据提供候选类的类型级注释。以下三个示例演示了此技术:

Java
Kotlin
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}

             
             
Java
Kotlin
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}

             
             
Java
Kotlin
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}

             
             
As with most annotation-based alternatives, keep in mind that the annotation metadata is bound to the class definition itself, while the use of XML allows for multiple beans of the same type to provide variations in their qualifier metadata, because that metadata is provided per-instance rather than per-class.

1.10.9. Generating an Index of Candidate Components

虽然类路径扫描非常快,但是可以通过在编译时创建候选对象的静态列表来提高大型应用程序的启动性能。在此模式下,作为组件扫描目标的所有模块都必须使用此机制。

Your existing @ComponentScan or <context:component-scan/> directives must remain unchanged to request the context to scan candidates in certain packages. When the ApplicationContext detects such an index, it automatically uses it rather than scanning the classpath.

要生成索引,请向包含作为组件扫描指令目标的组件的每个模块添加额外的依赖项。以下示例显示如何使用Maven执行此操作:

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

在Gradle 4.5及更早版本中,依赖项应在配置中声明,如下例所示:

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

             
             

对于Gradle 4.6及更高版本,应在注解处理器配置中声明依赖项,如下例所示:

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

             
             

SpringContext-Indexer构件生成包含在JAR文件中的META-INF/spring.Components文件。

When working with this mode in your IDE, the spring-context-indexer must be registered as an annotation processor to make sure the index is up-to-date when candidate components are updated.
The index is enabled automatically when a META-INF/spring.components file is found on the classpath. If an index is partially available for some libraries (or use cases) but could not be built for the whole application, you can fall back to a regular classpath arrangement (as though no index were present at all) by setting spring.index.ignore to true, either as a JVM system property or via the SpringProperties mechanism.

1.11. Using JSR 330 Standard Annotations

从Spring3.0开始,Spring支持JSR-330标准注释(依赖注入)。扫描这些注释的方式与扫描Spring注释的方式相同。要使用它们,您需要在类路径中拥有相关的JAR。

如果您使用Maven,jakarta.inject构件在标准Maven库(https://repo1.maven.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/).)中可用您可以将以下依赖项添加到文件pom.xml中:

<dependency>
    <groupId>jakarta.inject</groupId>
    <artifactId>jakarta.inject-api</artifactId>
    <version>1</version>
</dependency>
                 
                 

1.11.1. Dependency Injection with @Inject and @Named

您可以使用@jakarta.inject来代替@AuTower,如下所示:

Java
Kotlin
import jakarta.inject.Inject; public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } public void listMovies() { this.movieFinder.findMovies(...); // ... } } 
             
             

@AuTower一样,您可以在字段级、方法级和构造函数参数级使用@Inject。此外,您可以将注入点声明为提供者,从而允许通过Provider.get()调用对较短作用域的Bean的按需访问或对其他Bean的延迟访问。下面的示例提供了前面示例的变体:

Java
Kotlin
import jakarta.inject.Inject; import jakarta.inject.Provider; public class SimpleMovieLister { private Provider<MovieFinder> movieFinder; @Inject public void setMovieFinder(Provider<MovieFinder> movieFinder) { this.movieFinder = movieFinder; } public void listMovies() { this.movieFinder.get().findMovies(...); // ... } } 
             
             

如果要为应该注入的依赖项使用限定名称,则应使用@Named注释,如下面的示例所示:

Java
Kotlin
import jakarta.inject.Inject; import jakarta.inject.Named; public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(@Named("main") MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } 
             
             

@AuTower一样,@Inject也可以与java.util.Optional@Nullable一起使用。这在这里甚至更适用,因为@Inject没有必需的属性。以下两个示例说明如何使用@Inject@Nullable

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        // ...
    }
}

             
             
Java
Kotlin
public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}

             
             

1.11.2. @Named and @ManagedBean: Standard Equivalents to the @Component Annotation

您可以不使用@Component,而是使用@jakarta.inint.Namejakarta.nottation.ManagedBean,如下面的示例所示:

Java
Kotlin
import jakarta.inject.Inject; import jakarta.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; } // ... } 
             
             

在不指定组件名称的情况下使用@Component是非常常见的。@Named也可以类似的方式使用,如下例所示:

Java
Kotlin
import jakarta.inject.Inject; import jakarta.inject.Named; @Named public class SimpleMovieLister { private MovieFinder movieFinder; @Inject public void setMovieFinder(MovieFinder movieFinder) { this.movieFinder = movieFinder; } // ... } 
             
             

当您使用@Named@ManagedBean时,可以使用与使用Spring批注完全相同的方式使用组件扫描,如下面的示例所示:

Java
Kotlin
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
    // ...
}

             
             
In contrast to @Component, the JSR-330 @Named and the JSR-250 @ManagedBean annotations are not composable. You should use Spring’s stereotype model for building custom component annotations.

1.11.3. Limitations of JSR-330 Standard Annotations

在使用标准注释时,您应该知道一些重要功能不可用,如下表所示:

Table 6. Spring component model elements versus JSR-330 variants
Spring jakarta.inject.* jakarta.inject restrictions / comments

@自动连接

@注入

@Inject没有‘Required’属性。可以与Java 8的可选一起使用。

@组件

@已命名/@托管Bean

JSR-330没有提供可组合的模型,只提供了一种识别命名组件的方法。

@Scope(“单例”)

@Singleton

JSR-330默认作用域类似于Spring的Prototype。然而,为了使其与Spring的常规缺省值保持一致,默认情况下,在Spring容器中声明的JSR-330 Bean是单例。为了使用Singleton以外的作用域,您应该使用Spring的@Scope注释。jakarta.inject还提供了jakarta.inject.Scope批注:然而,这个批注仅用于创建自定义批注。

@限定符

@限定符/@已命名

jakarta.inint.Qualifier只是一个用于构建自定义限定符的元注释。具体的字符串限定符(如Spring的@限定符与值)可以通过jakarta.inint.Name关联。

@Value

-

没有等价物

@懒惰

-

没有等价物

对象工厂

提供商

jakarta.inint.Provider是Spring的ObjectFactory的直接替代,只是有一个更短的get()方法名。它还可以与Spring的@AuTower结合使用,或者与非带注释的构造函数和setter方法结合使用。

1.12. Java-based Container Configuration

本节介绍如何在Java代码中使用注释来配置Spring容器。它包括以下主题:

1.12.1. Basic Concepts: @Bean and @Configuration

Spring新的Java配置支持中的中心构件是@configuration注释的类和@Bean注释的方法。

@Bean注释用于指示方法实例化、配置和初始化要由Spring IOC容器管理的新对象。对于熟悉Spring的<;Beans/XML配置的人来说,@Bean注释的作用与<;Bean/>;元素的作用相同。您可以将@Bean注释的方法与任何Spring@Component一起使用。但是,它们最常与@configurationBean一起使用。

@configuration注释一个类表明它的主要用途是作为Bean定义的源。此外,@configuration类允许通过调用同一类中的其他@Bean方法来定义Bean间的依赖关系。最简单的@Configuration类如下所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

             
             

前面的AppConfig类相当于下面的Spring<;Bean/>;XML:

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>
             
             
Full @Configuration vs “lite” @Bean mode?

@Bean方法在没有使用@configuration注释的类中声明时,它们被称为以“lite”模式处理。在@Component或甚至在普通老类中声明的Bean方法被认为是“lite”的,包含类的主要用途不同,@Bean方法是一种额外的好处。例如,服务组件可以通过每个适用组件类上的附加@Bean方法向容器公开管理视图。在这样的场景中,@Bean方法是通用的工厂方法机制。

与完整的@configuration不同,lite@Bean方法不能声明Bean间的依赖关系。相反,它们对其包含组件的内部状态进行操作,并可选地对它们可能声明的参数进行操作。因此,这样的@Bean方法不应该调用其他@Bean方法。每个这样的方法实际上只是特定Bean引用的工厂方法,没有任何特殊的运行时语义。这里的积极副作用是在运行时不必应用CGLIB子类化,因此在类设计方面没有限制(即,包含类可以是最终等等)。

在常见场景中,@Bean方法在@configuration类中声明,确保始终使用“Full”模式,从而将跨方法引用重定向到容器的生命周期管理。这可以防止通过常规Java调用意外调用相同的@Bean方法,这有助于减少在“lite”模式下操作时难以跟踪的细微错误。

@Bean@configuration注释将在以下小节中深入讨论。不过,首先,我们将介绍使用基于Java的配置创建Spring容器的各种方法。

1.12.2. Instantiating the Spring Container by Using AnnotationConfigApplicationContext

以下各节介绍了Spring3.0中引入的AnnotationConfigApplicationContext,。这个通用的ApplicationContext实现不仅能够接受@Configuration类作为输入,还能够接受普通的@Component类和用JSR-330元数据注释的类。

@configuration类作为输入提供时,@configuration类本身注册为Bean定义,类中所有声明的@Bean方法也注册为Bean定义。

当提供@Component和JSR-330类时,它们被注册为Bean定义,并假定在这些类中必要时使用DI元数据,如@Autwire@Inject

Simple Construction

与在实例化<AnnotationConfigApplicationContext.>ClassPath XmlApplicationContext时将Spring XML文件用作输入的方式大致相同,您可以在实例化ClassPath XmlApplicationContext时使用@Configuration类作为输入这允许完全无XML地使用Spring容器,如下面的示例所示:

Java
Kotlin
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

              
              

如前所述,AnnotationConfigApplicationContext不仅限于使用<代码>@配置 类。任何@Component或JSR-330注释类都可以作为输入提供给构造函数,如下面的示例所示:

Java
Kotlin
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

              
              

前面的示例假定MyServiceImplDependency1Dependency2使用了Spring依赖项注入注释,如@AuTower

Building the Container Programmatically by Using register(Class<?>…​)

您可以使用非参数构造函数实例化注册,然后使用<AnnotationConfigApplicationContext()方法对其进行配置。此方法在以编程方式构建AnnotationConfigApplicationContext.时特别有用以下示例显示了如何执行此操作:

Java
Kotlin
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();
}

              
              
Enabling Component Scanning with scan(String…​)

要启用组件扫描,您可以按如下方式注释@Configuration类:

Java
Kotlin
@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig {
    // ...
}

              
              
1 This annotation enables component scanning.

有经验的Spring用户可能熟悉Spring的上下文:命名空间中的等价XML声明,如下例所示:

<beans>
    <context:component-scan base-package="com.acme"/>
</beans>
                   
                   

在前面的示例中,扫描com.acme包以查找任何@Component注释的类,并且这些类被注册为容器中的Spring Bean定义。AnnotationConfigApplicationContext公开<代码>扫描(字符串…​)方法以允许相同的组件扫描功能,如下面的示例所示:

Java
Kotlin
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

              
              
Remember that @Configuration classes are meta-annotated with @Component, so they are candidates for component-scanning. In the preceding example, assuming that AppConfig is declared within the com.acme package (or any package underneath), it is picked up during the call to scan(). Upon refresh(), all its @Bean methods are processed and registered as bean definitions within the container.
Support for Web Applications with AnnotationConfigWebApplicationContext

AnnotationConfigApplicationContextWebApplicationContext变体随AnnotationConfigWebApplicationContext.提供您可以在配置SpringContextLoaderListenerServlet侦听器、Spring MVCDispatcherServlet等时使用此实现。以下web.xml片段配置了一个典型的Spring MVC Web应用程序(请注意contextClass上下文参数和init-param的使用):

<web-app>
    <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- Configuration locations must consist of one or more comma- or space-delimited fully-qualified @Configuration classes. Fully-qualified packages may also be specified for component-scanning -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- Bootstrap the root application context as usual using ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Declare a Spring MVC DispatcherServlet as usual -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext instead of the default XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- Again, config locations must consist of one or more comma- or space-delimited and fully-qualified @Configuration classes -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>
              
              
For programmatic use cases, a GenericWebApplicationContext can be used as an alternative to AnnotationConfigWebApplicationContext. See the GenericWebApplicationContext javadoc for details.

1.12.3. Using the @Bean Annotation

@Bean是方法级批注,是XML<;Bean/>;元素的直接模拟。该批注支持<;Bean/>;提供的一些属性,例如:

您可以在@Configuration注释的类中或在@Component注释的类中使用@Bean注释。

Declaring a Bean

要声明一个Bean,您可以用@Bean注释来注释一个方法。使用此方法在指定为方法返回值的类型的ApplicationContext中注册Bean定义。默认情况下,Bean名称与方法名称相同。下面的示例显示了@Bean方法声明:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

              
              

前面的配置完全等同于下面的Spring XML:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>
              
              

这两个声明都使名为TransService的Bean在ApplicationContext中可用,并绑定到TransferServiceImpl类型的对象实例,如下面的文本图像所示:

transferService -> com.acme.TransferServiceImpl

您还可以使用默认方法来定义Bean。这允许通过在默认方法上实现带有Bean定义的接口来组合Bean配置。

Java
public interface BaseConfig {

    @Bean
    default TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

@Configuration
public class AppConfig implements BaseConfig {

}

              
              

您还可以使用接口(或基类)返回类型声明@Bean方法,如下面的示例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

              
              

但是,这将高级类型预测的可见性限制为指定的接口类型(TransferService)。然后,只有在实例化了受影响的单例Bean之后,容器才知道完整类型(TransferServiceImpl)。非惰性单例Bean根据它们的声明顺序被实例化,因此您可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试通过非声明类型进行匹配(例如@Autwire TransferServiceImpl,它只在TransferServiceBean被实例化后才解析)。

If you consistently refer to your types by a declared service interface, your @Bean return types may safely join that design decision. However, for components that implement several interfaces or for components potentially referred to by their implementation type, it is safer to declare the most specific return type possible (at least as specific as required by the injection points that refer to your bean).
Bean Dependencies

@Bean注释的方法可以有任意数量的参数来描述构建该Bean所需的依赖项。例如,如果我们的TransferService需要AcCountRepository,我们可以使用方法参数实例化该依赖项,如下面的示例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

              
              

解析机制与基于构造函数的依赖注入基本相同。有关详细信息,请参阅相关小节

Receiving Lifecycle Callbacks

任何使用@Bean注释定义的类都支持常规的生命周期回调,并且可以使用JSR-250中的@PostConstruct@PreDestroy注释。有关详细信息,请参阅JSR-250批注

也完全支持常规的Spring生命周期回调。如果Bean实现了InitializingBeanDisposableBeanLifeccle,则容器将调用它们各自的方法。

还完全支持标准的*Aware接口集(如BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware等)。

@Bean注释支持指定任意的初始化和销毁回调方法,这与Spring XML在Bean元素上的init-方法销毁方法属性非常相似,如下面的示例所示:

Java
Kotlin
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();
    }
}

              
              

默认情况下,使用Java配置定义的具有公共CloseShutdown方法的Bean会自动登记销毁回调。如果您有一个公共的CloseShutdown方法,并且您不希望在容器关闭时调用它,您可以将@Bean(delestyMethod=“”)添加到您的Bean定义中,以禁用默认的(推断)模式。

默认情况下,您可能希望对使用JNDI获取的资源执行此操作,因为其生命周期是在应用程序外部管理的。特别是,请确保始终对DataSource执行此操作,因为众所周知,它在Jakarta EE应用服务器上是有问题的。

下面的示例说明如何阻止DataSource的自动销毁回调:

Java
Kotlin
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}

                   
                   

此外,对于@Bean方法,您通常使用编程的JNDI查找,通过使用Spring的JndiTemplateJndiLocatorDelegate帮助器或直接使用JNDIInitialContext,但不使用JndiObjectFactoryBean变量(这将迫使您将返回类型声明为FactoryBean类型,而不是实际的目标类型,这使得在其他打算引用此处提供的资源的@Bean方法中更难用于交叉引用调用)。

对于上述示例中的BeanOne,在构造过程中直接调用init()方法同样有效,如下面的示例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

              
              
When you work directly in Java, you can do anything you like with your objects and do not always need to rely on the container lifecycle.
Specifying Bean Scope

Spring包括@Scope注释,以便您可以指定Bean的作用域。

Using the @Scope Annotation

您可以指定使用@Bean注释定义的Bean应该具有特定的作用域。您可以使用Bean Scope部分中指定的任何标准作用域。

默认作用域是Singleton,但您可以使用@Scope注释覆盖它,如下面的示例所示:

Java
Kotlin
@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}

               
               
@Scope and scoped-proxy

通过作用域代理,Spring提供了一种处理作用域依赖项的便捷方法。在使用XML配置时创建此类代理的最简单方法是<;aop:Scope-Proxy/>;元素。在Java中使用@Scope注释配置Bean可以提供与proxyMode属性相同的支持。缺省值为ScopedProxyMode.DEFAULT,这通常表示除非在组件扫描指令级别配置了不同的缺省值,否则不应创建作用域代理。可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO

如果使用Java将XML参考文档(请参阅范围代理)中的范围代理示例移植到我们的@Bean,则如下所示:

Java
Kotlin
// 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;
}

               
               
Customizing Bean Naming

默认情况下,配置类使用@Bean方法的名称作为结果Bean的名称。但是,可以使用name属性覆盖此功能,如下面的示例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}

              
              
Bean Aliasing

正如命名Bean中所讨论的,有时需要为单个Bean指定多个名称,也称为Bean别名。为此,@Bean注释的name属性接受字符串数组。以下示例显示如何为Bean设置多个别名:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

              
              
Bean Description

有时,提供对Bean的更详细的文本描述是有帮助的。当出于监视目的公开Bean(可能是通过JMX)时,这可能特别有用。

要向@Bean添加描述,可以使用@Description注释,如下例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

              
              

1.12.4. Using the @Configuration annotation

@configuration是一个类级批注,表示对象是Bean定义的源。@configuration类通过@Bean注释的方法声明Bean。对@Configuration类的@Bean方法的调用也可用于定义Bean间的依赖关系。有关一般介绍,请参阅基本概念:@Bean@configuration

Injecting Inter-bean Dependencies

当Bean之间存在依赖关系时,表达这种依赖关系就像让一个Bean方法调用另一个Bean方法一样简单,如下例所示:

Java
Kotlin
@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

              
              

在前面的示例中,beanOne通过构造函数注入接收对beanTwo的引用。

This method of declaring inter-bean dependencies works only when the @Bean method is declared within a @Configuration class. You cannot declare inter-bean dependencies by using plain @Component classes.
Lookup Method Injection

如前所述,查找方法注入是一个高级特性,您应该很少使用。它在单例作用域的Bean依赖于原型作用域的Bean的情况下很有用。使用Java进行此类配置为实现此模式提供了一种自然的方法。以下示例显示如何使用查找方法注入:

Java
Kotlin
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();
}

              
              

通过使用Java配置,您可以创建CommandManager的子类,其中抽象的createCommand()方法被覆盖,从而查找新的(原型)命令对象。以下示例显示了如何执行此操作:

Java
Kotlin
@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();
        }
    }
}

              
              
Further Information About How Java-based Configuration Works Internally

考虑以下示例,其中显示了两次调用带注释的@Bean方法:

Java
Kotlin
@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();
    }
}

              
              

clientDao()clientService1()中调用了一次,在clientService2()中调用了一次。由于此方法创建了ClientDaoImpl的一个新实例并返回它,因此通常会有两个实例(每个服务一个)。这肯定会有问题:在Spring中,实例化的Bean默认情况下有一个Singleton作用域。这就是魔术的用武之地:在启动时使用CGLIB对所有@configuration类进行子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有缓存的(作用域的)Bean。

The behavior could be different according to the scope of your bean. We are talking about singletons here.

从Spring3.2开始,不再需要将CGLIB添加到您的类路径中,因为CGLIB类已经被重新打包到org.springFrawork.cglib下,并直接包含在Spring-core JAR中。

由于CGLIB在启动时动态添加功能,因此有一些限制。特别是,配置类不能是最终的。然而,从4.3开始,配置类上允许使用任何构造函数,包括使用@Autwire或用于默认注入的单个非默认构造函数声明。

如果您希望避免CGLIB强加的任何限制,请考虑在非@Configuration类上声明您的@Bean方法(例如,在普通的@Component类上声明)。然后不会截取@Bean方法之间的跨方法调用,因此您必须在构造函数或方法级别独占依赖项注入。

1.12.5. Composing Java-based Configurations

Spring的基于Java的配置特性允许您编写注释,这可以降低配置的复杂性。

Using the @Import Annotation

就像<;import/<;元素用于在Spring XML文件中帮助模块化配置一样,@Import注释允许从另一个配置类加载@Bean定义,如下面的示例所示:

Java
Kotlin
@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();
    }
}

              
              

现在,实例化上下文时不需要同时指定ConfigA.classConfigB.class,只需显式提供ConfigB即可,如下例所示:

Java
Kotlin
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);
}

              
              

这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住可能大量的@configuration类。

As of Spring Framework 4.2, @Import also supports references to regular component classes, analogous to the AnnotationConfigApplicationContext.register method. This is particularly useful if you want to avoid component scanning, by using a few configuration classes as entry points to explicitly define all your components.
Injecting Dependencies on Imported @Bean Definitions

前面的示例是可行的,但过于简单。在大多数实际场景中,Bean在配置类之间相互依赖。在使用XML时,这不是问题,因为不涉及编译器,您可以声明ref=“omeBean”并信任Spring在容器初始化期间解决它。在使用@configuration类时,Java编译器会对配置模型施加约束,因为对其他Bean的引用必须是有效的Java语法。

幸运的是,解决这个问题很简单。正如我们已经讨论的@Bean方法可以有任意数量的参数来描述Bean依赖项。考虑以下更真实的场景,其中包含几个@Configuration类,每个类都依赖于在其他类中声明的Bean:

Java
Kotlin
@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");
}

               
               

还有另一种方法可以达到同样的结果。请记住,@configuration类最终只是容器中的另一个Bean:这意味着它们可以像任何其他Bean一样利用@Autwire@Value注入和其他特性。

确保您以这种方式注入的依赖项只是最简单的一种。@Configuration类在上下文的初始化过程中很早就被处理,以这种方式强制注入依赖项可能会导致意外的提前初始化。只要有可能,就采用基于参数的注入,如上例所示。

另外,在@Bean中使用BeanPostProcessorBeanFactoryPostProcessor定义时要特别小心。这些方法通常应该声明为Static@Bean方法,而不是触发其包含的配置类的实例化。否则,<代码>@自动连接 和<代码>@值 可能无法在配置类本身上工作,因为可以在AutowiredAnnotationBeanPostProcessor.之前将其创建为Bean实例

以下示例显示如何将一个Bean自动连接到另一个Bean:

Java
Kotlin
@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");
}

               
               
Constructor injection in @Configuration classes is only supported as of Spring Framework 4.3. Note also that there is no need to specify @Autowired if the target bean defines only one constructor.
Fully-qualifying imported beans for ease of navigation

在前面的场景中,使用@Autwire可以很好地工作,并提供所需的模块化,但是准确地确定在哪里声明了autwire的Bean定义仍然有些模棱两可。例如,作为一名查看ServiceConfig的开发人员,您如何确切知道@AuTower Account RepositoryBean的声明位置呢?它在代码中并不明确,这可能是很好的。请记住,Spring Tools for Eclipse提供了可以呈现图形的工具,这些图形显示了所有东西是如何连接的,这可能就是您所需要的。此外,您的Java IDE可以轻松找到Account Repository类型的所有声明和用法,并快速向您显示返回该类型的@Bean方法的位置。

如果这种二义性是不可接受的,并且您希望从您的IDE中直接从一个@Configuration类类导航到另一个类,请考虑自动装配配置类本身。以下示例显示了如何执行此操作:

Java
Kotlin
@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());
    }
}

                 
                 

在前面的情况中,定义了Account Repository的位置是完全显式的。但是,ServiceConfig现在与RepositoryConfig紧密耦合。这就是权衡。通过使用基于接口或基于抽象类的@Configuration类,可以在一定程度上缓解这种紧密耦合。请考虑以下示例:

Java
Kotlin
@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");
}

                 
                 

现在ServiceConfig与具体的DefaultRepositoryConfig是松散耦合的,内置的IDE工具仍然有用:您可以很容易地获得RepositoryConfig实现的类型层次结构。通过这种方式,导航@configuration类及其依赖项与导航基于接口的代码的通常过程没有什么不同。

If you want to influence the startup creation order of certain beans, consider declaring some of them as @Lazy (for creation on first access instead of on startup) or as @DependsOn certain other beans (making sure that specific other beans are created before the current bean, beyond what the latter’s direct dependencies imply).
Conditionally Include @Configuration Classes or @Bean Methods

根据某些任意系统状态,有条件地启用或禁用整个@Configuration类甚至单个@Bean方法通常很有用。这方面的一个常见示例是,只有在SpringEnvironment中启用了特定的概要文件时,才使用@Profile注释来激活Bean(有关详细信息,请参阅Bean定义概要文件)。

@PROFILE注释实际上是通过使用更灵活的注释@Conditional实现的。@Conditional注释指示在注册@Bean之前应该参考的特定org.springframework.context.annotation.Condition实现。

<代码>条件 接口的实现提供<代码>匹配(…​)返回<代码>TRUE或FALSE方法。例如,下面的清单显示了用于@profile的实际条件实现:

Java
Kotlin
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // Read the @Profile annotation attributes
    MultiValueMap<String, Object> 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;
}

              
              

有关详细信息,请参阅@Conditionaljavadoc。

Combining Java and XML Configuration

Spring的@configuration类支持并不是要100%完全替代Spring XML。一些工具,如Spring XML名称空间,仍然是配置容器的理想方式。如果xml是方便或必要的,您可以选择:或者通过使用ClassPath XmlApplicationContext以“以xml为中心”的方式实例化容器,或者通过使用AnnotationConfigApplicationContext@ImportResource注释根据需要以“以JAVA为中心”的方式实例化它。

XML-centric Use of @Configuration Classes

从XML引导Spring容器并以特殊方式包含@Configuration类可能更可取。例如,在使用Spring XML的大型现有代码库中,根据需要创建@configuration类并从现有的XML文件中包含它们会更容易。在这一节的后面,我们将介绍在这种“以XML为中心”的情况下使用@Configuration类的选项。

Declaring @Configuration classes as plain Spring <bean/> elements

请记住,@configuration类最终是容器中的Bean定义。在本系列示例中,我们创建了一个名为AppConfig@Configuration类,并将其作为<;bean/>;定义包含在system-test-config.xml中。因为打开了<;context:annotation-config/>;,所以容器识别@Configuration注释,并正确处理AppConfigy中声明的@Bean方法。

以下示例显示了Java中的一个普通配置类:

Java
Kotlin
@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

    @Bean
    public TransferService transferService() {
        return new TransferService(accountRepository());
    }
}

                 
                 

以下示例显示了示例system-test-config.xml文件的一部分:

<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
                 
                 

下面的示例显示了一个可能的jdbc.properties文件:

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
Java
Kotlin
public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

                 
                 
In system-test-config.xml file, the AppConfig <bean/> does not declare an id element. While it would be acceptable to do so, it is unnecessary, given that no other bean ever refers to it, and it is unlikely to be explicitly fetched from the container by name. Similarly, the DataSource bean is only ever autowired by type, so an explicit bean id is not strictly required.
Using <context:component-scan/> to pick up @Configuration classes

因为@configuration是用@Component进行元注释的,所以@configuration注释的类自动成为组件扫描的候选对象。使用与上一个示例中描述的相同场景,我们可以重新定义system-test-config.xml以利用组件扫描。注意,在本例中,我们不需要显式声明组件,因为<<;context:annotation-config/>;,><;Context:Component-Scan/>;启用了相同的功能。

以下示例显示修改后的system-test-config.xml文件:

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
                 
                 
@Configuration Class-centric Use of XML with @ImportResource

@configuration类是配置容器的主要机制的应用程序中,仍然可能需要至少使用一些XML。在这些场景中,您可以使用@ImportResource并根据需要定义任意数量的XML。这样做可以实现“以Java为中心”的方法来配置容器,并将XML保持在最低限度。下面的示例(包括一个配置类、一个定义Bean的XML文件、一个属性文件和main类)展示了如何使用@ImportResource注释来实现根据需要使用XML的“以Java为中心”的配置:

Java
Kotlin
@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);
    }
}

               
               
properties-config.xml
<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
               
               
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
Java
Kotlin
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

               
               

1.13. Environment Abstraction

接口是集成在容器中的抽象,它对应用程序环境的两个关键方面进行建模:配置文件属性

配置文件是一个命名的、逻辑组的Bean定义,只有在给定的配置文件处于活动状态时才会注册到容器中。可以将Bean分配给配置文件,无论该配置文件是在XML中定义的还是使用注释定义的。与配置文件相关的环境对象的作用是确定哪些配置文件(如果有)当前处于活动状态,以及哪些配置文件(如果有)应在默认情况下处于活动状态。

属性播放在几乎所有的应用程序中都扮演着重要的角色,并且可能来自于各种来源:属性文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、临时属性对象、Map对象等等。Environment对象与属性相关的角色是为用户提供一个方便的服务界面,用于配置属性源并从属性源解析属性。

1.13.1. Bean Definition Profiles

Bean定义概要文件在核心容器中提供了一种机制,允许在不同环境中注册不同的Bean。“环境”这个词对不同的用户可能有不同的含义,此功能可以帮助许多用例,包括:

  • 在开发中使用内存中的数据源,而不是在QA或生产中从JNDI中查找相同的数据源。

  • 仅在将应用程序部署到性能环境中时注册监控基础架构。

  • 为客户A和客户B的部署注册定制的Bean实现。

考虑实际应用程序中需要数据源的第一个用例。在测试环境中,配置可能如下所示:

Java
Kotlin
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

             
             

现在考虑如何将该应用程序部署到QA或生产环境中,假设该应用程序的数据源已注册到生产应用程序服务器的JNDI目录中。我们的DataSourceBean现在如下所示:

Java
Kotlin
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

             
             

问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring用户设计了许多方法来实现这一点,通常依赖于系统环境变量和包含${PLACEHOLDER}内标识的XML<;语句的组合,这些内标识根据环境变量的值解析为正确的配置文件路径。Bean定义概要文件是一个核心容器特性,它为这个问题提供了解决方案。

如果我们概括上一个特定于环境的Bean定义示例中所示的用例,我们最终需要在某些上下文中注册某些Bean定义,而不是在其他上下文中注册。您可以说,您希望在情况A中注册某个Bean定义配置文件,在情况B中注册一个不同的配置文件。我们首先更新我们的配置以反映这一需求。

Using @Profile

@Profile注释允许您在一个或多个指定的配置文件处于活动状态时指示组件有资格注册。使用前面的示例,我们可以重写DataSource配置,如下所示:

Java
Kotlin
@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();
    }
}

              
              
1 The standaloneDataSource method is available only in the development profile.
2 The jndiDataSource method is available only in the production profile.
Java
Kotlin
@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");
    }
}

              
              
1 The standaloneDataSource method is available only in the development profile.
2 The jndiDataSource method is available only in the production profile.
As mentioned earlier, with @Bean methods, you typically choose to use programmatic JNDI lookups, by using either Spring’s JndiTemplate/JndiLocatorDelegate helpers or the straight JNDI InitialContext usage shown earlier but not the JndiObjectFactoryBean variant, which would force you to declare the return type as the FactoryBean type.

配置文件字符串可以包含简单的配置文件名称(例如,Products)或配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,Production&;us-East)。配置文件表达式中支持以下运算符:

  • :配置文件的逻辑“NOT”

  • &;:配置文件的逻辑“与”

  • |:配置文件的逻辑“或”

You cannot mix the & and | operators without using parentheses. For example, production & us-east | eu-central is not a valid expression. It must be expressed as production & (us-east | eu-central).

您可以使用@PROFILE作为元批注来创建自定义合成批注。下面的示例定义了一个自定义@Products批注,您可以使用该批注替代@Profile(“Products”)

Java
Kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

              
              
If a @Configuration class is marked with @Profile, all of the @Bean methods and @Import annotations associated with that class are bypassed unless one or more of the specified profiles are active. If a @Component or @Configuration class is marked with @Profile({"p1", "p2"}), that class is not registered or processed unless profiles 'p1' or 'p2' have been activated. If a given profile is prefixed with the NOT operator (!), the annotated element is registered only if the profile is not active. For example, given @Profile({"p1", "!p2"}), registration will occur if profile 'p1' is active or if profile 'p2' is not active.

还可以在方法级别声明@PROFILE,以仅包括配置类的一个特定Bean(例如,对于特定Bean的替代变体),如下面的示例所示:

Java
Kotlin
@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");
    }
}

              
              

@Bean方法上使用@PROFILE方法时,可能会出现特殊的情况:如果重载的@Bean方法具有相同的Java方法名称(类似于构造函数重载),则需要在所有重载的方法上一致声明@PROFILE条件。如果条件不一致,则只有重载方法中第一个声明的条件重要。因此,@PROFILE不能用于选择具有特定参数签名的重载方法而不是另一个。同一个Bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。

如果您希望定义具有不同配置文件条件的替代Bean,请使用指向相同Bean名称的不同Java方法名称,方法是使用@Bean名称属性,如前面的示例所示。如果参数签名都是相同的(例如,所有变量都没有无参数工厂方法),这是首先在有效的Java类中表示这种排列的唯一方式(因为特定名称和参数签名只能有一个方法)。

XML Bean Definition Profiles

对应的XML是<;Beans>;元素的配置文件属性。我们前面的示例配置可以在两个XML文件中重写,如下所示:

<beans profile="development" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
              
              
<beans profile="production" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>
              
              

还可以避免在同一文件中拆分和嵌套Bean/<>元素,如下例所示:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>
              
              

Spring-bean.xsd被约束为只允许文件中的最后一个元素。这应该有助于提供灵活性,而不会在XML文件中引起混乱。

对应的XML不支持前面描述的配置文件表达式。但是,可以使用运算符对配置文件求反。还可以通过嵌套配置文件来应用逻辑“AND”,如下例所示:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xmlns:jee="http://www.springframework.org/schema/jee" xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
                   
                   

在前面的示例中,如果Productsus-East配置文件都处于活动状态,则会公开DataSourceBean。

Activating a Profile

现在我们已经更新了我们的配置,我们仍然需要指示Spring哪个概要文件是活动的。如果我们现在启动样例应用程序,我们将看到抛出NoSuchBeanDefinitionException,因为容器找不到名为DataSource的Spring Bean。

激活配置文件可以通过几种方式完成,但最直接的方法是针对EnvironmentAPI以编程方式完成,该API可通过ApplicationContext获得。以下示例显示了如何执行此操作:

Java
Kotlin
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

              
              

此外,您还可以通过spring.profiles.active属性以声明方式激活配置文件,该属性可以通过系统环境变量、JVM系统属性、web.xml中的Servlet上下文参数指定,甚至可以作为JNDI中的一个条目指定(请参阅PropertySource抽象)。在集成测试中,可以使用Spring-test模块中的@ActiveProfiles注释来声明活动概要文件(请参阅环境概要文件的上下文配置)。

请注意,配置文件不是“非此即彼”的命题。您可以一次激活多个配置文件。通过编程方式,您可以向<…>setActiveProfiles()方法提供多个配置文件名称,该方法接受<代码>字符串配置文件​变量。以下示例激活多个配置文件:

Java
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

              
              

声明性地,spring.profiles.active可以接受逗号分隔的配置文件名称列表,如下例所示:

    -Dspring.profiles.active="profile1,profile2"
Default Profile

默认配置文件表示默认情况下启用的配置文件。请考虑以下示例:

Java
Kotlin
@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

              
              

如果没有活动的配置文件,则创建DataSource。您可以将其视为为一个或多个Bean提供默认定义的一种方式。如果启用了任何配置文件,则不会应用默认配置文件。

您可以使用Environment上的setDefaultProfiles()更改默认配置文件的名称,或者使用spring.profiles.Default属性以声明方式更改默认配置文件的名称。

1.13.2. PropertySource Abstraction

Spring的Environment抽象提供了对属性源的可配置层次结构的搜索操作。请考虑以下清单:

Java
Kotlin
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

             
             

在前面的代码片段中,我们看到了一种高级方法,用于询问Spring是否为当前环境定义了my-Property属性。为了回答这个问题,Environment对象对一组PropertySource对象执行搜索。PropertySource是对任何键-值对来源的简单抽象,SpringStandardEnvironment配置了两个PropertySource对象 - ,一个表示系统属性集(System.getProperties()),另一个表示系统环境变量集(System.getenv())。

These default property sources are present for StandardEnvironment, for use in standalone applications. StandardServletEnvironment is populated with additional default property sources including servlet config, servlet context parameters, and a JndiPropertySource if JNDI is available.

具体地说,当您使用StandardEnvironment时,如果运行时存在my-Property系统属性或my-Property环境变量,则对env.containsProperty(“my-property”)的调用返回TRUE。

执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用env.getProperty(“my-Property”)期间恰好在两个位置都设置了my-Property属性,则系统属性值“WINS”并返回。请注意,属性值不会合并,而是被前面的条目完全覆盖。

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

  1. Servlet配置参数(如果适用,例如,在< >Dispatcher Servlet 上下文的情况下)

  2. ServletContext参数(web.xml上下文参数条目)

  3. JNDI环境变量(Java:comp/env/条目)

  4. JVM系统属性(-D命令行参数)

  5. JVM系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您有一个想要集成到此搜索中的自定义属性源。为此,请实现并实例化您自己的PropertySource,并将其添加到当前EnvironmentPropertySources集中。以下示例显示了如何执行此操作:

Java
Kotlin
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

             
             

在前面的代码中,MyPropertySource在搜索中的优先级最高。如果它包含my-Property属性,则检测并返回该属性,以支持任何其他PropertySource中的任何my-Property属性。MutablePropertySourcesAPI公开了许多方法,允许对属性源集进行精确操作。

1.13.3. Using @PropertySource

@PropertySource注释为将PropertySource添加到Spring的Environment提供了一种方便的声明性机制。

给定一个名为app.properties的文件,其中包含键-值对testbean.name=myTestBean,下面的@Configuration类使用@PropertySource的方式,即调用testBean.getName()返回myTestBean

Java
Kotlin
@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;
    }
}

             
             

任何<代码>${…​}资源位置中存在的占位符将根据已针对环境注册的属性源集进行解析,如下面的示例所示:

Java
Kotlin
@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;
    }
}

             
             

假设my.PLACEHOLDER存在于已注册的属性源之一(例如,系统属性或环境变量)中,则占位符将被解析为相应值。如果不是,则使用默认/路径作为默认设置。如果未指定默认值并且无法解析属性,则会引发IllegalArgumentException异常。

The @PropertySource annotation is repeatable, according to Java 8 conventions. However, all such @PropertySource annotations need to be declared at the same level, either directly on the configuration class or as meta-annotations within the same custom annotation. Mixing direct annotations and meta-annotations is not recommended, since direct annotations effectively override meta-annotations.

1.13.4. Placeholder Resolution in Statements

过去,元素中的占位符的值只能根据JVM系统属性或环境变量进行解析。情况已经不再是这样了。因为Environment抽象集成在整个容器中,所以很容易通过它传递占位符的解析。这意味着您可以以您喜欢的任何方式配置解析过程。您可以更改搜索系统特性和环境变量的优先顺序,也可以完全删除它们。您还可以根据需要将您自己的属性源添加到组合中。

具体地说,无论在哪里定义Customer属性,只要它在环境中可用,下面的语句都可以工作:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>
             
             

1.14. Registering a LoadTimeWeaver

LoadTimeWeaver被Spring用来在类被加载到Java虚拟机(JVM)时动态转换类。

若要启用加载时编织,可以将@EnableLoadTimeWeving添加到您的一个@Configuration类中,如下面的示例所示:

Java
Kotlin
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

            
            

或者,对于XML配置,您可以使用上下文:Load-time-weaver元素:

<beans>
    <context:load-time-weaver/>
</beans>
            
            

ApplicationContext配置后,该ApplicationContext中的任何Bean都可以实现LoadTimeWeverAware,从而接收对加载时编织器实例的引用。这在结合Spring的JPA支持时尤其有用,其中JPA类转换可能需要加载时编织。有关更多详细信息,请参考LocalContainerEntityManagerFactoryBeanjavadoc。有关AspectJ加载时编织的更多信息,请参阅在Spring框架中使用AspectJ加载时编织

1.15. Additional Capabilities of the ApplicationContext

为了以更面向框架的风格增强BeanFactory功能,上下文包还提供了以下功能:

  • 通过MessageSource接口访问I18N样式的消息。

  • 通过ResourceLoader接口访问资源,如URL和文件。

  • 事件发布,即通过使用ApplicationEventPublisher接口实现ApplicationListener接口的Bean。

  • 加载多个(分层)上下文,通过HierarchicalBeanFactory接口将每个上下文集中在一个特定的层上,例如应用程序的Web层。

1.15.1. Internationalization using MessageSource

ApplicationContext接口扩展了名为MessageSource的接口,因此提供了国际化(“I18N”)功能。Spring还提供了HierarchicalMessageSource接口,该接口可以分层解析消息。这些接口共同提供了Spring实现消息解析的基础。在这些接口上定义的方法包括:

  • 字符串getMessage(字符串code,Object[]args,字符串默认,区域设置锁定):用于从MessageSource检索消息的基本方法。如果未找到指定区域设置的消息,则使用默认消息。使用标准库提供的MessageFormat功能,传入的任何参数都将成为替换值。

  • 字符串getMessage(字符串code,Object[]args,Locale loc):本质上与前面的方法相同,但有一点不同:不能指定默认消息。如果找不到消息,则抛出NoSuchMessageException异常。

  • 字符串getMessage(MessageSourceResolable Resolable,Locale Locale):前面方法中使用的所有属性也包装在一个名为MessageSourceResolable的类中,您可以将其与此方法一起使用。

当加载ApplicationContext时,它会自动搜索上下文中定义的MessageSourceBean。该Bean必须具有名称MessageSource。如果找到这样的Bean,则对前面方法的所有调用都被委托给消息源。如果未找到消息源,ApplicationContext将尝试查找包含同名Bean的父级。如果是,则使用该Bean作为MessageSource。如果ApplicationContext找不到任何消息源,则会实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

Spring提供了三个MessageSource实现:ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource。它们都实现了HierarchicalMessageSource,以便进行嵌套消息传递。StaticMessageSource很少使用,但提供了将消息添加到源的编程方法。下面的示例显示ResourceBundleMessageSource

<beans>
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>
             
             

该示例假设您在类路径中定义了三个资源包,分别称为格式异常窗口。任何解析消息的请求都以JDK标准的方式处理,即通过ResourceBundle对象解析消息。出于本例的目的,假设上述两个资源包文件的内容如下:

    # in format.properties
    message=Alligators rock!
    # in exceptions.properties
    argument.required=The {0} argument is required.

下一个示例显示了一个运行MessageSource功能的程序。请记住,所有ApplicationContext实现也都是MessageSource实现,因此可以转换为MessageSource接口。

Java
Kotlin
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);
}

             
             

上述程序的结果输出如下所示:

Alligators rock!

总而言之,MessageSource在一个名为beans.xml的文件中定义,该文件位于类路径的根目录下。MessageSourceBean定义通过其basename属性引用许多资源包。在列表中传递给basename属性的三个文件作为文件存在于类路径的根目录下,它们分别称为Format.PropertiesExceptions.Propertieswindows.properties

下一个示例显示传递给消息查找的参数。这些参数被转换为字符串对象,并插入到查找消息的占位符中。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this POJO -->
    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
             
             
Java
Kotlin
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);
    }
}

             
             

调用Execute()方法的结果如下:

The userDao argument is required.

关于国际化(“i18n”),Spring的各种MessageSource实现遵循与标准JDKResourceBundle相同的地区解析和后备规则。简而言之,继续前面定义的示例MessageSource,如果您希望根据英国(en-GB)区域设置解析消息,您将分别创建名为Format_en_GB.PropertiesExceptions_en_GB.PropertiesWindows_en_GB.Properties的文件。

通常,区域设置解析由应用程序的周围环境进行管理。在以下示例中,手动指定解析(英国)消息所依据的区域设置:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
Java
Kotlin
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);
}

             
             

运行上述程序后得到的输出如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

您还可以使用MessageSourceAware接口获取对已定义的任何MessageSource的引用。在实现MessageSourceAware接口的ApplicationContext中定义的任何Bean在创建和配置Bean时都会注入应用程序上下文的MessageSource

Because Spring’s MessageSource is based on Java’s ResourceBundle, it does not merge bundles with the same base name, but will only use the first bundle found. Subsequent message bundles with the same base name are ignored.
As an alternative to ResourceBundleMessageSource, Spring provides a ReloadableResourceBundleMessageSource class. This variant supports the same bundle file format but is more flexible than the standard JDK based ResourceBundleMessageSource implementation. In particular, it allows for reading files from any Spring resource location (not only from the classpath) and supports hot reloading of bundle property files (while efficiently caching them in between). See the ReloadableResourceBundleMessageSource javadoc for details.

1.15.2. Standard and Custom Events

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的Bean部署到上下文中,则每次将ApplicationEvent发布到ApplicationContext时,都会通知该Bean。从本质上讲,这是标准的观察者设计模式。

As of Spring 4.2, the event infrastructure has been significantly improved and offers an annotation-based model as well as the ability to publish any arbitrary event (that is, an object that does not necessarily extend from ApplicationEvent). When such an object is published, we wrap it in an event for you.

下表描述了Spring提供的标准事件:

Table 7. Built-in Events
Event Explanation

上下文刷新事件

在初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext接口上的刷新()方法)。在这里,“初始化”意味着加载了所有的Bean,检测并激活了后处理器Bean,预实例化了单例,并且ApplicationContext对象已经准备好可以使用了。只要上下文尚未关闭,就可以多次触发刷新,前提是所选的ApplicationContext确实支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。

ConextStartedEvent

在使用ConfigurableApplicationContext接口上的Start()方法启动ApplicationContext时发布。在这里,“已启动”意味着所有生命周期Bean都会收到一个显式的开始信号。通常,此信号用于在显式停止后重新启动Bean,但也可用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。

上下文停止事件

在使用ConfigurableApplicationContext接口上的Stop()方法停止ApplicationContext时发布。在这里,“停止”意味着所有生命周期Bean都会收到一个显式的停止信号。可以通过调用start()重新启动已停止的上下文。

ConextClosedEvent

通过使用ConfigurableApplicationContext接口上的Close()方法或通过JVM关闭挂钩关闭ApplicationContext时发布的。在这里,“关闭”意味着所有的独生豆都将被销毁。一旦关闭上下文,它就会到达其生命周期的尽头,并且无法刷新或重新启动。

RequestHandledEvent

一个特定于Web的事件,通知所有Bean某个HTTP请求已得到处理。此事件在请求完成后发布。此事件仅适用于使用Spring的DispatcherServlet的Web应用程序。

ServletRequestHandledEvent

添加特定于Servlet的上下文信息的RequestHandledEvent的子类。

您还可以创建和发布您自己的自定义事件。下面的示例显示了一个扩展了Spring的ApplicationEvent基类的简单类:

Java
Kotlin
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...
}

             
             

若要发布自定义ApplicationEvent,请对ApplicationEventPublisher调用PublishEvent()方法。通常,这是通过创建实现ApplicationEventPublisherAware的类并将其注册为一个Spring Bean来实现的。下面的示例显示了这样一个类:

Java
Kotlin
public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> 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...
    }
}

             
             

在配置时,Spring容器检测到EmailService实现了ApplicationEventPublisherAware,并自动调用setApplicationEventPublisher()。实际上,传入的参数是Spring容器本身。您正在通过应用程序上下文的ApplicationEventPublisher接口与其交互。

要接收自定义ApplicationEvent,您可以创建一个实现ApplicationListener的类,并将其注册为一个Spring Bean。下面的示例显示了这样一个类:

Java
Kotlin
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

             
             

请注意,ApplicationListener一般是使用您的自定义事件的类型(上一个示例中的BlockedListEvent)进行参数化的。这意味着onApplicationEvent()方法可以保持类型安全,从而避免任何向下转换的需要。您可以注册任意数量的事件侦听器,但请注意,默认情况下,事件侦听器同步接收事件。这意味着PublishEvent()方法在所有侦听器处理完事件之前都会阻塞。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中操作。如果需要使用另一种事件发布策略,请参阅用于Spring的ApplicationEventMulticaster接口和SimpleApplicationEventMulticaster实现的配置选项的javadoc。

以下示例显示了用于注册和配置上述每个类的Bean定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="[email protected]"/>
</bean>
             
             

综上所述,当调用emailServiceBean的sendEmail()方法时,如果有任何应该阻止的电子邮件消息,则发布一个BlockedListEvent类型的定制事件。BLOCKEDListNotifierBean注册为ApplicationListener,并接收BlockedListEvent,此时它可以通知相应的参与方。

Spring’s eventing mechanism is designed for simple communication between Spring beans within the same application context. However, for more sophisticated enterprise integration needs, the separately maintained Spring Integration project provides complete support for building lightweight, pattern-oriented, event-driven architectures that build upon the well-known Spring programming model.
Annotation-based Event Listeners

您可以使用@EventListener注释在托管Bean的任何方法上注册事件侦听器。BlockedListNotify可以重写如下:

Java
Kotlin
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...
    }
}

              
              

方法签名再次声明它侦听的事件类型,但这一次使用灵活的名称,并且没有实现特定的侦听器接口。只要实际事件类型解析其实现层次结构中的泛型参数,也可以通过泛型缩小事件类型的范围。

如果您的方法应该侦听多个事件,或者如果您希望在不使用任何参数的情况下定义它,则还可以在注释本身上指定事件类型。以下示例显示了如何执行此操作:

Java
Kotlin
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

              
              

下面的示例显示如何重写我们的通知程序,以便仅在事件的content属性等于my-Event时才调用:

Java
Kotlin
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

              
              

每个Spel表达式针对专用上下文进行计算。下表列出了可供上下文使用的项目,以便您可以将其用于条件事件处理:

Table 8. Event SpEL available metadata
Name Location Description Example

事件

根对象

实际的ApplicationEvent

#root.Event事件

参数数组

根对象

用于调用该方法的参数(作为对象数组)。

#root.argsargsargs[0]访问第一个参数,依此类推。

参数名称

评估背景

任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译后的字节代码中没有调试信息),也可以使用#a<;#arg&>;语法获得各个参数,其中<;#arg&>;表示参数索引(从0开始)。

#blEvent#a0(也可以使用#p0#p<;#arg&>;参数表示法作为别名)

请注意,#root.event允许您访问底层事件,即使您的方法签名实际上引用了已发布的任意对象。

如果需要将一个事件作为处理另一个事件的结果发布,则可以更改方法签名以返回应该发布的事件,如下面的示例所示:

Java
Kotlin
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

              
              
This feature is not supported for asynchronous listeners.

handleBlockedListEvent()方法为它处理的每个BlockedListEvent发布一个新的ListUpdateEvent。如果需要发布多个事件,则可以返回集合或事件数组。

Asynchronous Listeners

如果希望特定的侦听器异步处理事件,可以重用常规@async支持。以下示例显示了如何执行此操作:

Java
Kotlin
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

              
              

使用异步事件时,请注意以下限制:

  • 如果异步事件侦听器引发异常,则不会将其传播到调用方。有关更多详细信息,请参阅AsyncUncaughtExceptionHandler

  • 异步事件侦听器方法不能通过返回值发布后续事件。如果您需要发布另一个事件作为处理的结果,则注入ApplicationEventPublisher以手动发布该事件。

Ordering Listeners

如果您需要先调用一个监听器,然后再调用另一个监听器,则可以在方法声明中添加@order注释,如下例所示:

Java
Kotlin
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

              
              
Generic Events

您还可以使用泛型来进一步定义事件的结构。考虑使用EntityCreatedEvent<;T&>,其中T是创建的实际实体的类型。例如,您可以创建以下侦听器定义,以便仅接收人员EntityCreatedEvent

Java
Kotlin
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

              
              

由于类型擦除,仅当激发的事件解析事件侦听器筛选的通用参数(即,类似于类PersonCreatedEvent扩展EntiyCreatedEvent<;Person>;{…;​})。

在某些情况下,如果所有事件都遵循相同的结构(上例中的事件应该是这种情况),这可能会变得非常乏味。在这种情况下,您可以实现ResolvableTypeProvider来引导框架超出运行时环境所提供的范围。以下事件说明了如何做到这一点:

Java
Kotlin
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

              
              
This works not only for ApplicationEvent but any arbitrary object that you send as an event.

1.15.3. Convenient Access to Low-level Resources

为了更好地使用和理解应用程序上下文,您应该熟悉Spring的资源抽象,如参考资料中所述。

应用程序上下文是ResourceLoader,可用于加载资源对象。资源本质上是JDKjava.net.URL类的一个功能更丰富的版本。事实上,在适当的情况下,资源的实现包装了java.net.URL的实例。资源可以透明的方式从几乎任何位置获取低级资源,包括从类路径、文件系统位置、任何可用标准URL描述的位置以及其他一些变体。如果资源位置字符串是不带任何特殊前缀的简单路径,则这些资源来自的位置是特定的,并且适合实际的应用程序上下文类型。

您可以将部署到应用程序上下文中的Bean配置为实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调,同时将应用程序上下文本身作为ResourceLoader传入。您还可以公开用于访问静态资源的Resource类型的属性。它们被注入其中,就像任何其他属性一样。您可以将这些资源属性指定为简单的字符串路径,并在部署Bean时依赖于从这些文本字符串到实际资源对象的自动转换。

提供给ApplicationContext构造函数的一个或多个位置路径实际上是资源字符串,在简单的形式中,会根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不考虑实际的上下文类型。

1.15.4. Application Startup Tracking

ApplicationContext管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可能具有同样复杂的组件图和启动阶段。

使用特定指标跟踪应用程序启动步骤可以帮助了解启动阶段的时间花在哪里,但它也可以用作更好地了解整个环境生命周期的一种方式。

AbstractApplicationContext(及其子类)使用ApplicationStartup进行检测,它收集有关各个启动阶段的StartupStep数据:

  • 应用程序上下文生命周期(基包扫描、配置类管理)

  • Bean生命周期(实例化、智能初始化、后处理)

  • 应用程序事件处理

以下是AnnotationConfigApplicationContext:中的插装示例

Java
Kotlin
// 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();

             
             

应用程序上下文已经使用多个步骤进行了检测。一旦记录下来,就可以使用特定工具收集、显示和分析这些启动步骤。有关现有启动步骤的完整列表,您可以查看专用附录部分。

默认的ApplicationStartup实现是无操作的变体,开销最小。这意味着默认情况下,在应用程序启动期间不会收集任何指标。Spring框架附带了一个使用Java飞行记录器跟踪启动步骤的实现:FlightRecorderApplicationStartup.要使用此变体,必须在创建后立即将其实例配置为ApplicationContext

如果开发人员正在提供他们自己的AbstractApplicationContext子类,或者如果他们希望收集更准确的数据,也可以使用ApplicationStartup基础结构。

ApplicationStartup is meant to be only used during application startup and for the core container; this is by no means a replacement for Java profilers or metrics libraries like Micrometer.

要开始收集自定义StartupStep,组件可以直接从应用程序上下文获取ApplicationStartup实例,使其组件实现ApplicationStartupAware,或者在任何注入点上请求ApplicationStartup类型。

Developers should not use the "spring.*" namespace when creating custom startup steps. This namespace is reserved for internal Spring usage and is subject to change.

1.15.5. Convenient ApplicationContext Instantiation for Web Applications

例如,您可以使用ConextLoader以声明方式创建ApplicationContext实例。当然,您也可以通过使用ApplicationContext实现之一,以编程方式创建ApplicationContext实例。

您可以使用ContextLoaderListener注册一个ApplicationContext,如下例所示:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
             
             

侦听器检查contextConfigLocation参数。如果该参数不存在,则监听器默认使用/WEB-INF/ApplationConext.xml。当参数确实存在时,侦听器使用预定义的分隔符(逗号、分号和空格)分隔字符串,并使用这些值作为搜索应用程序上下文的位置。还支持蚂蚁样式的路径模式。例如/WEB-INF/*Conext.xml(对于名称以Conext.xml结尾且位于WEB-INF目录中的所有文件)和/WEB-INF/**/*Conext.xml(对于WEB-INF的任何子目录中的所有此类文件)。

1.15.6. Deploying a Spring ApplicationContext as a Jakarta EE RAR File

可以将SpringApplicationContext部署为RAR文件,将上下文及其所有必需的Bean类和库JAR封装在Jakarta EE RAR部署单元中。这相当于引导一个独立的ApplicationContext(仅托管在Jakarta EE环境中)能够访问Jakarta EE服务器设施。与实际上部署无头WAR文件 - 的场景相比,RAR部署是一种更自然的替代方案,该WAR文件没有任何HTTP入口点,仅用于在Jakarta EE环境中引导SpringApplicationContext

RAR部署非常适合不需要HTTP入口点而只包含消息端点和计划作业的应用程序上下文。这样的上下文中的 可以使用应用服务器资源,比如JTA事务管理器和绑定到JNDI的JDBCDataSource实例和JMSConnectionFactory实例,还可以通过Spring的标准事务管理以及jndi和jmx支持工具向平台的jmx服务器 - 注册。应用程序组件还可以通过Spring的TaskExecutor抽象与应用程序服务器的JCAWorkManager交互。

有关href=“0”>SpringContextResourceAdapter部署中涉及的配置详细信息,请参阅

对于将Spring ApplicationContext作为Jakarta EE RAR文件的简单部署:

  1. 将所有应用程序类打包到RAR文件中(这是一个具有不同文件扩展名的标准JAR文件)。

  2. 将所有必需的库JAR添加到RAR归档的根目录中。

  3. 添加META-INF/ra.xml部署描述符(如SpringContextResourceAdapter)的javadoc和相应的Spring XML Bean定义文件(通常为META-INF/applicationContext.xml).

  4. 将生成的RAR文件放到应用程序服务器的部署目录中。

Such RAR deployment units are usually self-contained. They do not expose components to the outside world, not even to other modules of the same application. Interaction with a RAR-based ApplicationContext usually occurs through JMS destinations that it shares with other modules. A RAR-based ApplicationContext may also, for example, schedule some jobs or react to new files in the file system (or the like). If it needs to allow synchronous access from the outside, it could (for example) export RMI endpoints, which may be used by other application modules on the same machine.

1.16. The BeanFactory API

BeanFactoryAPI为Spring的IoC功能提供了底层基础。它的特定契约主要用于与Spring和相关第三方框架的其他部分集成,其DefaultListableBeanFactory实现是更高层的GenericApplicationContext容器中的一个关键委托。

BeanFactory及相关接口(如BeanFactoryAwareInitializingBeanDisposableBean)是其他框架组件的重要集成点。由于不需要任何注释,甚至不需要反射,它们允许容器及其组件之间非常高效的交互。应用程序级别的Bean可以使用相同的回调接口,但通常更喜欢声明性依赖注入,或者通过注释或者通过编程配置。

请注意,核心BeanFactoryAPI级别及其DefaultListableBeanFactory实现并不假设要使用的配置格式或任何组件批注。所有这些特性都是通过扩展(如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor))实现的,并作为核心元数据表示对共享的BeanDefinition对象进行操作。这就是使Spring的容器如此灵活和可扩展的本质。

1.16.1. BeanFactory or ApplicationContext?

本节解释BeanFactoryApplicationContext容器级别之间的差异以及对引导的影响。

您应该使用ApplicationContext,除非您有充分的理由不这样做,GenericApplicationContext及其子类AnnotationConfigApplicationContext是自定义引导的常见实现。这些是用于所有常见目的的Spring核心容器的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册Bean定义和带注释的类,以及(从5.0开始)注册功能Bean定义。

由于ApplicationContext包括BeanFactory的所有功能,因此通常推荐使用它而不是普通的BeanFactory,除非需要完全控制Bean处理。在ApplicationContext(如GenericApplicationContext实现)中,通过约定(即按Bean名称或Bean类型 - ,特别是后处理器)检测几种Bean,而普通的DefaultListableBeanFactory不知道任何特殊的Bean。

对于许多扩展容器特性,例如批注处理和AOP代理,BeanPostProcessor扩展点是必不可少的。如果只使用普通的DefaultListableBeanFactory,默认情况下不会检测和激活这样的后处理器。这种情况可能会令人困惑,因为您的Bean配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置完全引导容器。

下表列出了BeanFactoryApplicationContext接口和实现提供的功能。

Table 9. Feature Matrix
Feature BeanFactory ApplicationContext

Bean实例化/连接

集成的生命周期管理

不是

自动BeanPostProcessor注册

不是

自动BeanFactoryPostProcessor注册

不是

方便的MessageSource访问(用于国际化)

不是

内置ApplicationEvent发布机制

不是

要使用DefaultListableBeanFactory显式注册Bean后处理器,需要以编程方式调用addBeanPostProcessor,如下例所示:

Java
Kotlin
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

             
             

要将BeanFactoryPostProcessor应用于普通的DefaultListableBeanFactory,需要调用其postProcessBeanFactory方法,如下例所示:

Java
Kotlin
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);

             
             

在这两种情况下,显式的注册步骤都不方便,这就是为什么在Spring支持的应用程序中,各种ApplicationContext变体比普通的DefaultListableBeanFactory更受欢迎,特别是在典型的企业设置中依赖BeanFactoryPostProcessorBeanPostProcessor实例来扩展容器功能时。

AnnotationConfigApplicationContext注册了所有常见的批注后处理器,并可能通过配置批注在底层引入额外的处理器,例如<代码>@EnableTransactionManagement 。在Spring的基于注释的配置模型的抽象级别,Bean后处理器的概念只是一个内部容器细节。

2. Resources

本章介绍了Spring如何处理资源,以及如何在Spring中使用资源。它包括以下主题:

2.1. Introduction

遗憾的是,Java的标准java.net.URL类和用于各种URL前缀的标准处理程序不足以满足所有对低级资源的访问。例如,没有标准化的URL实现可用于访问需要从类路径获取的资源或相对于ServletContext的资源。虽然可以为专门的URL前缀注册新的处理程序(类似于http:等前缀的现有处理程序),但这通常非常复杂,并且URL接口仍然缺乏一些所需的功能,例如检查所指向的资源是否存在的方法。

2.2. The Resource Interface

位于org.springFramework.core.io.包中的Spring的Resource接口旨在成为一个更强大的接口,用于抽象对低级资源的访问。下面的清单概述了资源接口。有关详细信息,请参阅资源javadoc。

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;
}

            
            

来自资源接口的一些最重要的方法包括:

  • getInputStream():定位并打开资源,返回InputStream以从资源读取。预计每次调用都会返回新的InputStream。关闭流是调用方的责任。

  • Existes():返回一个布尔值,指示该资源是否实际以物理形式存在。

  • isOpen():返回一个布尔值,指示此资源是否表示具有打开的流的句柄。如果为True,则InputStream不能多次读取,必须只读取一次,然后关闭以避免资源泄漏。为所有常见的资源实现返回FALSEInputStreamResource除外。

  • getDescription():返回该资源的描述,在使用该资源时用于错误输出。这通常是资源的完全限定文件名或实际URL。

其他方法允许您获取表示资源的实际URL文件对象(如果底层实现兼容并支持该功能)。

Resource接口的某些实现还为支持写入的资源实现了扩展的WritableResource接口。

当需要资源时,Spring本身广泛使用资源抽象,作为许多方法签名中的参数类型。一些SpringAPI中的其他方法(如各种ApplicationContext实现的构造函数)接受一个字符串,它以简单或简单的形式用于创建适合于该上下文实现的资源,或者通过字符串路径上的特殊前缀,让调用者指定必须创建和使用特定的资源实现。

虽然Resource接口在Spring和Spring中经常使用,但实际上在您自己的代码中将其作为通用实用程序类使用是非常方便的,用于访问资源,即使您的代码不知道或不关心Spring的任何其他部分。虽然这会将您的代码耦合到Spring,但它实际上只是将它耦合到这一小组实用程序类,这些实用程序类是URL的更强大的替代品,可以被视为与您将用于此目的的任何其他库等效。

The Resource abstraction does not replace functionality. It wraps it where possible. For example, a UrlResource wraps a URL and uses the wrapped URL to do its work.

2.3. Built-in Resource Implementations

Spring包括几个内置的资源实现:

有关Spring中可用的资源实现的完整列表,请参阅资源javadoc的“所有已知的实现类”部分。

2.3.1. UrlResource

UrlResource包装了java.net.URL,可用于访问任何通常可通过URL访问的对象,如文件、HTTPS目标、FTP目标等。所有URL都具有标准化的字符串表示形式,以便使用适当的标准化前缀来表示不同的URL类型。这包括用于访问文件系统路径的file:、用于通过HTTPS协议访问资源的https:、用于通过ftp访问资源的ftp:等。

UrlResource是由Java代码通过显式使用UrlResource构造函数创建的,但通常是在调用带有表示路径的字符串参数的API方法时隐式创建的。对于后一种情况,JavaBeansPropertyEditor最终决定创建哪种类型的资源。如果路径字符串包含众所周知的(对于属性编辑器而言)前缀(如classpath:),它将为该前缀创建适当的专用资源。但是,如果它不能识别前缀,它会假定该字符串是标准URL字符串,并创建一个UrlResource

2.3.2. ClassPathResource

此类表示应从类路径获取的资源。它使用线程上下文类加载器、给定的类加载器或给定的类来加载资源。

如果类路径资源驻留在文件系统中,但不支持驻留在JAR中且尚未(通过Servlet引擎或其他环境)扩展到文件系统的类路径资源,则此Resource实现支持将其解析为java.io.File。为了解决这个问题,各种资源实现始终支持解析为java.net.URL

ClassPathResource是由Java代码通过显式使用ClassPath Resource构造函数创建的,但通常是在调用带有表示路径的字符串参数的API方法时隐式创建的。对于后一种情况,JavaBeansPropertyEditor识别字符串路径上的特殊前缀classpath:,并在这种情况下创建一个ClassPath Resource

2.3.3. FileSystemResource

这是java.io.File句柄的资源实现。它还支持java.nio.file.Path句柄,应用Spring的标准基于字符串的路径转换,但通过java.nio.file.FilesAPI执行所有操作。对于纯基于java.nio.path.Path的支持,请改用路径资源FileSystemResource支持解析为文件URL

2.3.4. PathResource

这是java.nio.file.Path句柄的资源实现,通过路径API执行所有操作和转换。它支持解析为文件URL,还实现了扩展的WritableResource接口。路径资源实际上是具有不同createRelative行为的纯java.nio.path.Path替代FileSystemResource

2.3.5. ServletContextResource

这是ServletContext资源的资源实现,用于解释相关Web应用程序根目录中的相对路径。

它始终支持流访问和URL访问,但仅当Web应用程序存档扩展并且资源实际位于文件系统上时才允许java.io.File访问。它是在文件系统上展开还是从JAR或其他地方(如数据库)直接访问(这是可以想象的)实际上取决于Servlet容器。

2.3.6. InputStreamResource

InputStreamResource是给定InputStream资源实现。仅当没有特定的资源实现可用时才使用它。特别是,在可能的情况下,首选ByteArrayResource或任何基于文件的资源实现。

与其他资源实现不同,它是已经打开的资源的描述符。因此,它从isOpen()返回true。如果需要将资源描述符保存在某个位置,或者需要多次读取流,请不要使用它。

2.3.7. ByteArrayResource

这是给定字节数组的资源实现。它为给定的字节数组创建ByteArrayInputStream

它对于从任何给定字节数组加载内容非常有用,而不必求助于单次使用的InputStreamResource

2.4. The ResourceLoader Interface

ResourceLoader接口旨在由可以返回(即加载)资源实例的对象实现。下面的清单显示了ResourceLoader接口定义:

public interface ResourceLoader {

    Resource getResource(String location);

    ClassLoader getClassLoader();
}

            
            

所有应用程序上下文都实现ResourceLoader接口。因此,所有应用程序上下文都可用于获取资源实例。

当您在特定的应用程序上下文上调用getResource(),而指定的位置路径没有特定的前缀时,您会得到一个适合于该特定应用程序上下文的资源类型。例如,假设对ClassPath XmlApplicationContext实例运行以下代码片段:

Java
Kotlin
Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

            
            

对于ClassPath XmlApplicationContext,该代码返回ClassPath Resource。如果对FileSystemXmlApplicationContext实例运行相同的方法,它将返回FileSystemResource。对于WebApplicationContext,它将返回ServletContextResource。它同样会为每个上下文返回适当的对象。

因此,您可以以适合特定应用程序上下文的方式加载资源。

另一方面,您还可以通过指定特殊的类路径:前缀来强制使用ClassPath Resource,而不考虑应用程序上下文类型,如下面的示例所示:

Java
Kotlin
Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

            
            

类似地,您可以通过指定任何标准的java.net.URL前缀来强制使用UrlResource。以下示例使用文件https前缀:

Java
Kotlin
Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");

            
            
Java
Kotlin
Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt");

            
            

下表总结了将字符串对象转换为资源对象的策略:

Table 10. Resource strings
Prefix Example Explanation

类路径:

类路径:com/myapp/config.xml

从类路径加载。

文件:

<代码>file:///data/config.xml

作为URL从文件系统加载。另请参阅文件系统资源警告

HTTPS:

<代码>https://myserver/logo.png

作为URL加载。

(无)

/data/config.xml

取决于基础ApplicationContext

2.5. The ResourcePatternResolver Interface

ResourcePatternResolver接口是ResourceLoader接口的扩展,该接口定义了将位置模式(例如,Ant样式的路径模式)解析为资源对象的策略。

public interface ResourcePatternResolver extends ResourceLoader {

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String locationPattern) throws IOException;
}

            
            

如上所述,该接口还为类路径中的所有匹配资源定义了特殊的classpath*:资源前缀。注意,资源位置应该是不带占位符的路径,在本例中为 - ,例如classpath*:/CONFIG/beans.xml。JAR文件或类路径中的不同目录可以包含具有相同路径和相同名称的多个文件。有关类路径*:资源前缀的通配符支持的详细信息,请参阅通配符在应用程序上下文构造函数资源路径中及其小节。

可以检查传入的ResourceLoader(例如,通过ResourceLoaderAware语义提供的)是否也实现了这个扩展接口。

PathMatchingResourcePatternResolver是可在ApplicationContext外部使用的独立实现,也由ResourceArrayPropertyEditor用来填充Resource[]Bean属性。PathMatchingResourcePatternResolver能够将指定的资源位置路径解析为一个或多个匹配的<代码>资源 对象。源路径可以是具有到目标<代码>资源 的一对一映射的简单路径,或者也可以包含特殊的<代码>类路径*: 前缀和/或内部Ant样式的正则表达式(使用SpringTM的org.springframework.util.AntPathMatcher实用程序进行匹配)。后两者实际上都是通配符。

任何标准ApplicationContext中的默认ResourceLoader实际上是实现ResourcePatternResolver接口的PathMatchingResourcePatternResolver的一个实例。ApplicationContext实例本身也是如此,它还实现了ResourcePatternResolver接口并委托给默认的PathMatchingResourcePatternResolver.

2.6. The ResourceLoaderAware Interface

ResourceLoaderAware接口是一个特殊的回调接口,它标识预期提供ResourceLoader引用的组件。下面的清单显示了ResourceLoaderAware接口的定义:

public interface ResourceLoaderAware {

    void setResourceLoader(ResourceLoader resourceLoader);
}

            
            

当类实现ResourceLoaderAware并部署到应用程序上下文中(作为Spring管理的Bean)时,它将被应用程序上下文识别为ResourceLoaderAware。然后,应用程序上下文调用setResourceLoader(ResourceLoader),,并将自身作为参数提供(请记住,Spring中的所有应用程序上下文都实现ResourceLoader接口)。

由于ApplicationContextResourceLoader,该Bean还可以实现ApplicationContextAware接口,并使用提供的应用程序上下文直接加载资源。但是,通常情况下,如果需要的话,最好使用专用的ResourceLoader接口。代码将只耦合到资源加载接口(可以被视为实用程序接口),而不是整个SpringApplicationContext接口。

在应用程序组件中,您还可以依赖ResourceLoader的自动装配作为实现ResourceLoaderAware接口的替代方法。传统构造函数byType自动装配模式(如AuTower Collaborator中所述)能够分别为构造函数实参或setter方法参数提供ResourceLoader。为了获得更大的灵活性(包括自动连接字段和多参数方法的能力),可以考虑使用基于注释的自动连接特性。在这种情况下,ResourceLoader被自动连接到一个字段、构造函数实参或方法参数中,只要有问题的字段、构造函数或方法带有@Autwire注释,该字段、构造函数或方法参数就需要ResourceLoader类型。有关更多信息,请参阅Using@Autwire

To load one or more Resource objects for a resource path that contains wildcards or makes use of the special classpath*: resource prefix, consider having an instance of ResourcePatternResolver autowired into your application components instead of ResourceLoader.

2.7. Resources as Dependencies

如果Bean本身要通过某种动态过程来确定和提供资源路径,那么Bean使用ResourceLoaderResourcePatternResolver接口来加载资源可能是有意义的。例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,则完全取消使用ResourceLoader接口(或ResourcePatternResolver接口)是有意义的,让Bean公开它需要的Resource属性,并期望将它们注入其中。

然后注入这些属性很简单,因为所有应用程序上下文都注册并使用一个特殊的JavaBeansPropertyEditor,它可以将字符串路径转换为资源对象。例如,下面的MyBean类有一个模板属性,其类型为Resource

Java
Kotlin
package example;

public class MyBean {

    private Resource template;

    public setTemplate(Resource template) {
        this.template = template;
    }

    // ...
}

            
            

在XML配置文件中,可以使用该资源的简单字符串配置模板属性,如下面的示例所示:

<bean id="myBean" class="example.MyBean">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>
            
            

请注意,资源路径没有前缀。因此,由于应用程序上下文本身将被用作ResourceLoader,因此资源通过ClassPath ResourceFileSystemResourceServletConextResource加载,具体取决于应用程序上下文的确切类型。

如果需要强制使用特定的资源类型,可以使用前缀。以下两个示例展示了如何强制ClassPath ResourceUrlResource(后者用于访问文件系统中的文件):

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
            
            
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>
            
            

如果MyBean类被重构以用于注释驱动的配置,则myTemplate.txt的路径可以存储在一个名为template.Path - 的键下,在一个可供Spring环境使用的属性文件中(请参阅环境抽象)。然后,可以使用属性占位符通过@Value注释引用模板路径(请参阅Using@Value)。Spring将以字符串的形式检索模板路径的值,而特殊的PropertyEditor将把该字符串转换为资源对象,以注入MyBean构造函数。下面的示例演示了如何实现这一点。

Java
Kotlin
@Component
public class MyBean {

    private final Resource template;

    public MyBean(@Value("${template.path}") Resource template) {
        this.template = template;
    }

    // ...
}

            
            

例如,如果我们想要支持在类路径classpath*:/config/templates/*.txt.- 中的多个位置的同一路径下发现的多个模板,我们可以使用特殊的< >类路径*: 前缀和通配符来将<>模板路径 键定义为模板如果我们按如下方式重新定义MyBean类,Spring将把模板路径模式转换为资源对象的数组,这些对象可以注入到MyBean构造函数中。

Java
Kotlin
@Component
public class MyBean {

    private final Resource[] templates;

    public MyBean(@Value("${templates.path}") Resource[] templates) {
        this.templates = templates;
    }

    // ...
}

            
            

2.8. Application Contexts and Resource Paths

本节介绍如何使用资源创建应用程序上下文,包括使用XML的快捷方式、如何使用通配符以及其他详细信息。

2.8.1. Constructing Application Contexts

应用程序上下文构造函数(用于特定的应用程序上下文类型)通常将字符串或字符串数组作为资源的位置路径,例如组成上下文定义的XML文件。

当这样的位置路径没有前缀时,从该路径构建并用于加载Bean定义的特定资源类型取决于并适合于特定的应用程序上下文。例如,考虑下面的示例,它创建了一个ClassPathXmlApplicationContext

Java
Kotlin
ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

             
             

Bean定义是从类路径加载的,因为使用了ClassPath Resource。但是,请考虑下面的示例,它创建了一个FileSystemXmlApplicationContext

Java
Kotlin
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/appContext.xml");

             
             

现在,从文件系统位置(在本例中,是相对于当前工作目录)加载Bean定义。

请注意,在位置路径上使用特殊的类路径前缀或标准URL前缀会覆盖为加载Bean定义而创建的默认类型资源。请考虑以下示例:

Java
Kotlin
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

             
             

使用FileSystemXmlApplicationContext从类路径加载Bean定义。但是,它仍然是FileSystemXmlApplicationContext。如果随后将其用作ResourceLoader,则任何无前缀的路径仍被视为文件系统路径。

Constructing ClassPathXmlApplicationContext Instances — Shortcuts

ClassPathXmlApplicationContext公开了许多构造函数以实现方便的实例化。其基本思想是,您可以只提供一个字符串数组,该数组只包含XML文件本身的文件名(不包含前导路径信息),还可以提供。然后,ClassPathXmlApplicationContext从提供的类派生路径信息。

请考虑以下目录布局:

com/
  example/
    services.xml
    repositories.xml
    MessengerService.class

以下示例显示如何实例化由名为services.xmlrepositories.xml(位于类路径上)的文件中定义的Bean组成的ClassPathXmlApplicationContext实例:

Java
Kotlin
ApplicationContext ctx = new ClassPathXmlApplicationContext(
    new String[] {"services.xml", "repositories.xml"}, MessengerService.class);

              
              

有关各种构造函数的详细信息,请参阅ClassPathXmlApplicationContextjavadoc。

2.8.2. Wildcards in Application Context Constructor Resource Paths

应用程序上下文构造函数值中的资源路径可以是简单的路径(如前所述),每个路径都有到目标资源的一对一映射,或者也可以包含特殊的类路径*:前缀或内部Ant样式模式(通过使用Spring的PathMatcher工具进行匹配)。后两者实际上都是通配符。

这种机制的一个用途是当您需要进行组件样式的应用程序组装时。所有组件都可以上下文定义片段发布到众所周知的位置路径,并且,当使用前缀为classpath*:的相同路径创建最终应用程序上下文时,所有组件片段都会被自动拾取。

注意,这种通配符特定于在应用程序上下文构造函数中使用资源路径(或者当您直接使用PathMatcher实用程序类层次结构时),并在构造时解析。它与资源类型本身无关。您不能使用类路径*:前缀来构造实际的资源,因为资源一次只指向一个资源。

Ant-style Patterns

路径位置可以包含Ant样式的模式,如下例所示:

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

当路径位置包含Ant样式模式时,解析器遵循更复杂的过程来尝试解析通配符。它为到最后一个非通配符段的路径生成资源,并从中获取URL。如果此URL不是jar:URL或特定于容器的变量(如WebLogic中的zip:、WebSphere中的wsjar等等),则将从其中获取java.io.File,并使用它通过遍历文件系统来解析通配符。对于JAR URL,解析器要么从中获取java.net.JarURLConnection,要么手动解析JAR URL,然后遍历JAR文件的内容以解析通配符。

Implications on Portability

如果指定的路径已经是文件URL(隐式是因为基本ResourceLoader是文件系统路径),则保证通配符以完全可移植的方式工作。

如果指定的路径是类路径位置,则解析程序必须通过调用Classloader.getResource()来获取最后一个非通配符路径段URL。因为这只是路径的一个节点(不是末尾的文件),所以它实际上没有定义(在ClassLoaderjavadoc中)在这种情况下返回的URL类型。实际上,它始终是表示目录(其中类路径资源解析为文件系统位置)或某种JAR URL(其中类路径资源解析为JAR位置)的java.io.File。尽管如此,这个操作还是有一个可移植性的问题。

如果获得了最后一个非通配符片段的JAR URL,则解析器必须能够从中获取java.net.JarURLConnection,或者手动解析JAR URL,以便能够遍历JAR的内容并解析通配符。这在大多数环境中都有效,但在其他环境中不起作用,我们强烈建议在依赖它之前,在您的特定环境中彻底测试来自JAR的资源的通配符解析。

The classpath*: Prefix

构造基于XML的应用程序上下文时,位置字符串可以使用特殊的classpath*:前缀,如下例所示:

Java
Kotlin
ApplicationContext ctx =
    new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

              
              

这个特殊前缀指定必须获取与给定名称匹配的所有类路径资源(在内部,这基本上是通过调用ClassLoader.getResources(…)实现的​)),然后合并以形成最终的应用程序上下文定义。

The wildcard classpath relies on the getResources() method of the underlying ClassLoader. As most application servers nowadays supply their own ClassLoader implementation, the behavior might differ, especially when dealing with jar files. A simple test to check if classpath* works is to use the ClassLoader to load a file from within a jar on the classpath: getClass().getClassLoader().getResources("<someFileInsideTheJar>"). Try this test with files that have the same name but reside in two different locations — for example, files with the same name and same path but in different jars on the classpath. In case an inappropriate result is returned, check the application server documentation for settings that might affect the ClassLoader behavior.

您还可以在位置路径的其余部分(例如,classpath*:META-INF/*-beans.xml).)中将<代码>类路径*: 前缀与<代码>路径匹配器 模式组合在一起在本例中,解析策略相当简单:在最后一个非通配符路径段上使用ClassLoader.getResources()调用来获取类加载器层次结构中的所有匹配资源,然后在每个资源之外,将前面描述的相同的PathMatcher解析策略用于通配符路径。

Other Notes Relating to Wildcards

请注意,当classpath*:与Ant样式模式结合使用时,只有在模式启动之前才能可靠地使用至少一个根目录,除非实际的目标文件驻留在文件系统中。这意味着像classpath*:*.xml这样的模式可能不会从JAR文件的根目录中检索文件,而只能从展开目录的根目录中检索文件。

Spring检索类路径条目的能力源于JDK的ClassLoader.getResources()方法,该方法只返回空字符串的文件系统位置(指示要搜索的潜在根)。Spring还评估JAR文件中的URLClassLoader运行时配置和java.class.Path清单,但这不能保证会导致可移植行为。

扫描类路径包需要在类路径中存在相应的目录条目。当您使用Ant构建JAR时,不要激活JAR任务的文件开关。此外,根据某些环境 - 中的安全策略,类路径目录可能不会公开,例如JDK1.7.0_45及更高版本上的独立应用程序(这需要在清单中设置‘Trusted-Library’。参见https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).

在JDK 9的模块路径(Jigsaw)上,Spring的类路径扫描通常按预期工作。这里也强烈建议将资源放到专用目录中,以避免前面提到的在搜索JAR文件根级别时出现的可移植性问题。

如果要搜索的根包在多个类路径位置可用,则不能保证具有classpath:资源的Ant样式模式能够找到匹配的资源。请考虑以下资源位置的示例:

com/mycompany/package1/service-context.xml

现在考虑一个Ant样式的路径,有人可能会使用该路径来查找该文件:

classpath:com/mycompany/**/service-context.xml

这样的资源可能只存在于类路径中的一个位置,但是当使用像前面的示例这样的路径尝试解析它时,解析器会取消getResource(“com/myCompany”);返回的(第一个)URL。如果此基包节点存在于多个ClassLoader位置,则所需资源可能不存在于找到的第一个位置。因此,在这种情况下,您应该更喜欢将classpath*:与相同的Ant样式模式一起使用,该模式搜索包含com.myCompany基包的所有类路径位置:classpath*:com/mycompany/**/service-context.xml.

2.8.3. FileSystemResource Caveats

未附加到FileSystemApplicationContextFileSystemResource(即,当FileSystemApplicationContext不是实际的ResourceLoader时)会像您预期的那样处理绝对路径和相对路径。相对路径相对于当前工作目录,而绝对路径相对于文件系统的根。

但是,出于向后兼容(历史)原因,当FileSystemApplicationContextResourceLoader时,情况会发生变化。FileSystemApplicationContext强制所有附加的FileSystemResource实例将所有位置路径视为相对路径,无论它们是否以前斜杠开头。在实践中,这意味着以下示例是等价的:

Java
Kotlin
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");

             
             
Java
Kotlin
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");

             
             

以下示例也是等价的(尽管它们是不同的,因为一种情况是相对的,另一种情况是绝对的):

Java
Kotlin
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");

             
             
Java
Kotlin
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

             
             

在实践中,如果您需要真正的绝对文件系统路径,您应该避免使用带有FileSystemResourceFileSystemXmlApplicationContext的绝对路径,并通过使用文件:URL前缀强制使用UrlResource。以下示例说明了如何执行此操作:

Java
Kotlin
// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");

             
             
Java
Kotlin
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:///conf/context.xml");

             
             

3. Validation, Data Binding, and Type Conversion

将验证视为业务逻辑是有利有弊的,而Spring提供了一种验证(和数据绑定)设计,不排除任何一种。具体地说,验证不应该绑定到Web层,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring提供了一个Validator约定,该约定既是基本的,也是在应用程序的每一层都非常可用的。

数据绑定可用于将用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)。Spring提供了名副其实的DataBinder来实现这一点。验证器DataBinder组成验证包,主要用于但不限于Web层。

BeanWrapper是Spring框架中的一个基本概念,在很多地方都有使用。但是,您可能不需要直接使用BeanWrapper。但是,因为这是参考文档,所以我们觉得可能需要一些解释。我们在本章中解释BeanWrapper,因为如果您要使用它,很可能是在尝试将数据绑定到对象时使用它。

Spring的DataBinder和较低级别的BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。PropertyEditorPropertyEditorSupport类型是JavaBeans规范的一部分,本章也对其进行了说明。Spring3引入了core.Convert包,它提供了通用的类型转换工具,以及用于格式化UI字段值的更高级别的“Format”包。您可以使用这些包作为PropertyEditorSupport实现的更简单的替代方案。本章还对它们进行了讨论。

Spring通过设置基础设施和到Spring自己的Validator约定的适配器来支持Java Bean验证。应用程序可以全局启用一次Bean验证,如Java Bean验证中所述,并专门用于所有验证需求。在Web层,应用程序还可以为每个DataBinder注册控制器本地的Spring验证器实例,如配置DataBinder中所述,这对于插入自定义验证逻辑非常有用。

3.1. Validation by Using Spring’s Validator Interface

Spring有一个Validator接口,您可以使用它来验证对象。Validator接口使用错误对象工作,以便在验证时,验证器可以向错误对象报告验证失败。

考虑以下小型数据对象的示例:

Java
Kotlin
public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

            
            

下一个示例通过实现org.springframework.validation.Validator接口的以下两个方法为Person类提供验证行为:

  • 支持(Class):此验证器是否可以验证提供的Class的实例?

  • Valid(对象,org.springframework.validation.Errors):验证给定的对象,如果出现验证错误,则将其注册到给定的<代码>错误对象。

实现Validator相当简单,特别是在您知道了Spring框架还提供的ValidationUtils助手类的情况下。下面的示例为Person实例实现Validator

Java
Kotlin
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");
        }
    }
}

            
            

ValidationUtils类上的静态rejectIfEmpty(..)方法用于在name属性为NULL或空字符串时拒绝该属性。查看ValidationUtilsjavadoc,以了解它除了前面所示的示例之外还提供了哪些功能。

虽然当然可以实现单个Validator类来验证富对象中的每个嵌套对象,但将每个嵌套对象类的验证逻辑封装在其自己的Validator实现中可能会更好。“富”对象的一个简单示例是Customer,它由两个字符串属性(第一个和第二个名称)和一个复杂的Address对象组成。Address对象可以独立于Customer对象使用,因此实现了一个不同的AddressValidator。如果希望CustomerValidator重用AddressValidator类中包含的逻辑而不使用复制粘贴,则可以在CustomerValidator中依赖项注入或实例化AddressValidator,如下面的示例所示:

Java
Kotlin
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();
        }
    }
}

            
            

向传递给验证器的错误对象报告验证错误。对于Spring Web MVC,您可以使用<;Spring:Bind/>;标记来检查错误消息,但也可以自己检查错误对象。有关它提供的方法的更多信息,可以在javadoc中找到。

3.2. Resolving Codes to Error Messages

我们涵盖了数据绑定和验证。本节介绍如何输出与验证错误对应的消息。在上一节所示的示例中,我们拒绝了nameage字段。如果我们想使用MessageSource输出错误消息,可以使用我们在拒绝该字段时提供的错误代码(本例中为‘name’和‘age’)来实现。当您从Errors接口调用(直接或间接使用ValidationUtils类)rejectValue或其他reject方法时,底层实现不仅会注册您传入的代码,还会注册许多额外的错误代码。MessageCodesResolver确定错误接口寄存器的错误代码。默认情况下,使用DefaultMessageCodesResolver,它(例如)不仅用您给出的代码注册一条消息,而且还注册包括您传递给Reject方法的字段名的消息。因此,如果您使用rejectValue(“age”,“too.darn.old”)拒绝一个字段,那么除了too.darn.old代码之外,Spring还会注册too.darn.old.agetoo.darn.old.age.int(第一个包括字段名,第二个包括字段类型)。这样做是为了方便开发人员在确定错误消息时提供帮助。

关于 MessageCodesResolver>MessageCodesResolver DefaultMessageCodesResolver,>和默认策略的更多信息可以分别在 MessageCodesResolverDefaultMessageCodesResolver,的javadoc中找到。

3.3. Bean Manipulation and the BeanWrapper

org.springFrawork.Beans包遵循JavaBeans标准。JavaBean是具有默认无参数构造函数的类,并且遵循命名约定,例如,名为bingoMadness的属性将具有setter方法setBingoMadness(..)和getter方法getBingoMadness()。有关Java Beans及其规范的更多信息,请参阅Java Beans

Beans包中一个非常重要的类是BeanWrapper接口及其对应的实现(BeanWrapperImpl)。正如从javadoc中引用的,BeanWrapper提供了设置和获取属性值(单独或批量)、获取属性描述符以及查询属性以确定它们是否可读或可写的功能。此外,BeanWrapper提供了对嵌套属性的支持,支持在无限深度上设置子属性的属性。BeanWrapper还支持添加标准Java BeansPropertyChangeListenersVToableChangeListeners的功能,而不需要在目标类中支持代码。最后但并非最不重要的是,BeanWrapper提供了对设置索引属性的支持。BeanWrapper通常不由应用程序代码直接使用,而是由DataBinderBeanFactory使用。

BeanWrapper的工作方式部分由其名称表示:它包装一个Bean以在该Bean上执行操作,如设置和检索属性。

3.3.1. Setting and Getting Basic and Nested Properties

设置和获取属性是通过BeanWrappersetPropertyValuegetPropertyValue重载方法变量来完成的。有关详细信息,请参阅他们的Java代码。下表显示了这些约定的一些示例:

Table 11. Examples of properties
Expression Explanation

名称

指示与getName()isname()setName(..)方法对应的属性name

帐户名称

指示与getAccount().setName()getAccount().getName()方法相对应的Account属性Account的嵌套属性。

帐户[2]

指示索引属性Account第三个元素。索引属性的类型可以是数组列表或其他自然有序的集合。

帐户[COMPANYNAME]

指示由帐户映射属性的COMPANYNAME键索引的映射条目的值。

(如果您不打算直接使用BeanWrapper,那么下一节对您来说并不是特别重要。如果您只使用DataBinderBeanFactory及其默认实现,则应该跳到PropertyEdtors部分。)

以下两个示例类使用BeanWrapper获取和设置属性:

Java
Kotlin
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;
    }
}

             
             
Java
Kotlin
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;
    }
}

             
             

以下代码片段显示了如何检索和操作实例化的CompanyEmployee的某些属性的一些示例:

Java
Kotlin
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");

             
             

3.3.2. Built-in PropertyEditor Implementations

Spring使用PropertyEditor的概念来实现对象字符串之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,日期可以用人类可读的方式表示(如字符串‘2007-14-09’),同时我们仍然可以将人类可读的形式转换回原始日期(或者,更好的是,将以人类可读形式输入的任何日期转换回日期对象)。这一行为可以通过注册类型的定制编辑器实现。在BeanWrapper上注册定制编辑器,或者在特定的IOC容器中注册定制编辑器(如上一章所述),使其了解如何将属性转换为所需的类型。有关PropertyEditor的更多信息,请参阅Oracle的java.Beans包的javadoc

下面是在Spring中使用属性编辑的几个示例:

  • 通过使用PropertyEditor实现来设置Bean的属性。当您使用字符串作为在XML文件中声明的某个Bean的属性的值时,Spring(如果相应属性的setter具有Class参数)使用ClassEditor尝试将该参数解析为Class对象。

  • 在Spring的MVC框架中解析HTTP请求参数是通过使用各种PropertyEditor实现来完成的,您可以在CommandController的所有子类中手动绑定这些实现。

Spring有许多内置的PropertyEditor实现,以使工作变得简单。它们都位于org.springframework.beans.propertyeditors包中。默认情况下,大多数(但不是全部,如下表所示)由BeanWrapperImpl注册。在属性编辑器可以以某种方式配置的情况下,您仍然可以注册自己的变量来覆盖默认变量。下表介绍了Spring提供的各种PropertyEditor实现:

Table 12. Built-in PropertyEditor Implementations
Class Explanation

ByteArrayPropertyEditor

字节数组的编辑器。将字符串转换为其对应的字节表示形式。默认情况下由BeanWrapperImpl注册。

类编辑器

将表示类的字符串解析为实际类,反之亦然。如果找不到类,则抛出IllegalArgumentException异常。默认情况下,由BeanWrapperImpl注册。

CustomBoolanEditor

布尔属性的可自定义属性编辑器。默认情况下,由BeanWrapperImpl注册,但可以通过将其的自定义实例注册为自定义编辑器来覆盖。

CustomCollectionEditor

集合的属性编辑器,将任何源集合转换为给定的目标集合类型。

CustomDateEditor

java.util.Date的可定制属性编辑器,支持自定义DateFormat。默认情况下未注册。必须根据需要以适当的格式由用户注册。

CustomNumberEditor

任何数字子类的可自定义属性编辑器,如IntegerLongFloatDouble。默认情况下,由BeanWrapperImpl注册,但可以通过将其的自定义实例注册为自定义编辑器来覆盖。

文件编辑器

将字符串解析为java.io.File对象。默认情况下,由BeanWrapperImpl注册。

InputStreamEditor

单向属性编辑器,可以接受字符串并(通过中间ResourcesResource)生成InputStream,以便InputStream属性可以直接设置为字符串。请注意,默认用法不会为您关闭InputStream。默认情况下,由BeanWrapperImpl注册。

LocaleEditor

可以将字符串解析为区域设置对象,反之亦然(字符串格式为[语言]_[国家]_[变量],与区域设置toString()方法相同)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由BeanWrapperImpl注册。

模式编辑器

可以将字符串解析为java.util.regex.Pattern对象,反之亦然。

属性编辑器

可以将字符串(格式化为java.util.Properties类的javadoc中定义的格式)转换为Properties对象。默认情况下,由BeanWrapperImpl注册。

StringTrimmerEditor

修剪字符串的属性编辑器。允许将空字符串转换为NULL值。默认情况下未注册 - 必须是用户注册的。

URLEditor

可以将URL的字符串表示形式解析为实际的URL对象。默认情况下,由BeanWrapperImpl注册。

Spring使用java.beans.PropertyEditorManager来设置可能需要的属性编辑器的搜索路径。搜索路径还包括sun.bean.edtors,其中包括属性编辑器字体颜色等类型的实现,以及大多数基元类型。还要注意,如果PropertyEditor类与它们所处理的类在同一个包中,并且与该类具有相同的名称,并且附加了Editor,则标准的JavaBeans基础结构会自动发现PropertyEditor类(无需显式注册它们)。例如,可以具有以下类和包结构,这足以使SomethingEditor类被识别并用作某物类型属性的PropertyEditor

com
  chank
    pop
      Something
      SomethingEditor // the PropertyEditor for the Something class

注意,您还可以在这里使用标准的BeanInfoJavaBeans机制(在某种程度上此处进行了描述)。下面的示例使用BeanInfo机制显式注册一个或多个具有关联类的属性的PropertyEditor实例:

com
  chank
    pop
      Something
      SomethingBeanInfo // the BeanInfo for the Something class

引用的SomethingBeanInfo类的以下Java源代码将CustomNumberEditorSomething类的age属性相关联:

Java
Kotlin
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());
        }
    }
}

             
             
Registering Additional Custom PropertyEditor Implementations

当将Bean属性设置为字符串值时,Spring IOC容器最终使用标准的JavaBeansPropertyEditor实现将这些字符串转换为属性的复杂类型。Spring预先注册了许多定制的PropertyEditor实现(例如,将表示为字符串的类名转换为Class对象)。此外,Java的标准JavaBeansPropertyEditor查找机制允许对类的PropertyEditor进行适当的命名,并将其放置在与其提供支持的类相同的包中,这样就可以自动找到它。

如果需要注册其他自定义PropertyEdants,可以使用几种机制。最手动的方法(通常不方便或不推荐)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假设您有一个BeanFactory引用。另一种(稍微方便一点)机制是使用名为CustomEditorConfigurer的特殊Bean工厂后处理器。尽管您可以将Bean工厂后处理器与BeanFactory实现一起使用,但CustomEditorConfigurer具有嵌套属性设置,因此我们强烈建议您将其与ApplicationContext一起使用,在其中您可以以类似于任何其他Bean的方式部署它,并且可以在其中自动检测和应用它。

注意,通过使用BeanWrapper来处理属性转换,所有的Bean工厂和应用程序上下文都会自动使用许多内置的属性编辑器。BeanWrapper注册的标准属性编辑器在上一节中列出。此外,ApplicationContext还覆盖或添加其他编辑器,以适用于特定应用程序上下文类型的方式处理资源查找。

标准的JavaBeansPropertyEditor实例用于将表示为字符串的属性值转换为属性的实际复杂类型。您可以使用Bean工厂后处理器CustomEditorConfigurer方便地向ApplicationContext添加对其他PropertyEditor实例的支持。

考虑以下示例,它定义了一个名为ExoticType的用户类和另一个名为DependsOnExoticType的类,它需要将ExoticType设置为属性:

Java
Kotlin
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;
    }
}

              
              

正确设置后,我们希望能够将type属性作为字符串分配,PropertyEditor将其转换为实际的ExoticType实例。以下Bean定义显示了如何设置此关系:

<bean id="sample" class="example.DependsOnExoticType">
    <property name="type" value="aNameForExoticType"/>
</bean>
              
              

PropertyEditor实现可能如下所示:

Java
Kotlin
// converts string representation to ExoticType object
package example;

public class ExoticTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        setValue(new ExoticType(text.toUpperCase()));
    }
}

              
              

最后,下面的示例说明如何使用CustomEditorConfigurer将新的PropertyEditor注册到ApplicationContext,然后该应用程序将能够根据需要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="example.ExoticType" value="example.ExoticTypeEditor"/>
        </map>
    </property>
</bean>
              
              
Using PropertyEditorRegistrar

向Spring容器注册属性编辑器的另一种机制是创建并使用PropertyEditorRegister。当您需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用。您可以编写相应的注册器,并在每种情况下重复使用它。PropertyEditorRegister实例与名为PropertyEditorRegistry的接口协同工作,该接口由SpringBeanWrapper(和DataBinder)实现。PropertyEditorRegister实例与CustomEditorConfigurer(此处描述的)一起使用时特别方便,它公开了一个名为setPropertyEditorRegistrars(..)的属性。PropertyEditorRegister以这种方式添加到CustomEditorConfigurer的实例可以轻松地与DataBinder和Spring MVC控制器共享。此外,它还避免了在自定义编辑器上进行同步的需要:PropertyEditorRegister应该为每个Bean创建尝试创建新的PropertyEditor实例。

以下示例显示如何创建您自己的PropertyEditorRegister实现:

Java
Kotlin
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...
    }
}

               
               

有关<org.springframework.beans.support.ResourceEditorRegistrar>PropertyEditorRegistrator实现的示例,另请参阅代码。请注意,在其registerCustomEdants(..)方法的实现中,它如何创建每个属性编辑器的新实例。

下一个示例显示如何配置CustomEditorConfigurer并将CustomPropertyEditorRegister的实例注入其中:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>

<bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
               
               

最后(稍微偏离了本章的重点)对于使用Spring的MVC web框架的人来说,将PropertyEditorRegister与数据绑定web控制器结合使用是非常方便的。下面的示例在@InitBinder方法的实现中使用PropertyEditorRegister

Java
Kotlin
@Controller
public class RegisterUserController {

    private final PropertyEditorRegistrar customPropertyEditorRegistrar;

    RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {
        this.customPropertyEditorRegistrar = propertyEditorRegistrar;
    }

    @InitBinder
    void initBinder(WebDataBinder binder) {
        this.customPropertyEditorRegistrar.registerCustomEditors(binder);
    }

    // other methods related to registering a User
}

               
               

这种风格的PropertyEditor注册可以生成简洁的代码(@InitBinder方法的实现只有一行长),并允许将公共的PropertyEditor注册代码封装在一个类中,然后根据需要在任意多个控制器之间共享。

3.4. Spring Type Conversion

Spring3引入了一个core.Convert包,它提供了一个通用的类型转换系统。系统定义了一个实现类型转换逻辑的SPI和一个在运行时执行类型转换的API。在Spring容器中,您可以使用该系统作为PropertyEditor实现的替代方案,将外部化的Bean属性值字符串转换为所需的属性类型。您还可以在需要进行类型转换的应用程序中的任何位置使用公共API。

3.4.1. Converter SPI

实现类型转换逻辑的SPI简单且强类型,如以下接口定义所示:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    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<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }
}

             
             

3.4.2. Using ConverterFactory

当您需要集中整个类层次结构的转换逻辑时(例如,从字符串转换为枚举对象),您可以实现ConverterFactory,如下例所示:

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

             
             

将S参数化为要转换的类型,将R参数化为定义可以转换到的类的范围的基本类型。然后实现getConverter(Class<;T>;),其中T是R的子类。

StringToEnumConverterFactory为例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

             
             

3.4.3. Using GenericConverter

当您需要复杂的Converter实现时,请考虑使用GenericConverter接口。与Converter相比,GenericConverter具有更灵活但类型更少的签名,它支持在多个源类型和目标类型之间进行转换。此外,GenericConverter使源和目标字段上下文可用,您可以在实现转换逻辑时使用它们。这样的上下文允许由字段注释或在字段签名上声明的通用信息驱动类型转换。下面的清单显示了GenericConverter的接口定义:

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

             
             

若要实现<→>GenericConverter,请使getConvertibleTypes()返回受支持的源ConvertibleTypes目标类型对。然后实现Convert(Object,TypeDescriptor,TypeDescriptor)以包含您的转换逻辑。源TypeDescriptor提供对保存要转换的值的源字段的访问。目标TypeDescriptor提供对要在其中设置转换值的目标字段的访问。

GenericConverter的一个很好的例子是在Java数组和集合之间进行转换的转换器。这样的ArrayToCollectionConverter自省声明目标集合类型的字段以解析集合的元素类型。这允许在目标字段上设置集合之前将源数组中的每个元素转换为集合元素类型。

Because GenericConverter is a more complex SPI interface, you should use it only when you need it. Favor Converter or ConverterFactory for basic type conversion needs.
Using ConditionalGenericConverter

有时,您希望Converter仅在特定条件成立时才运行。例如,您可能希望仅在目标字段上存在特定批注时才运行Converter,或者可能仅在目标类上定义了特定方法(如方法的静态值)时才运行ConverterConditionalGenericConverterGenericConverterConditionalConverter接口的联合,允许您定义这样的自定义匹配条件:

public interface ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

              
              

ConditionalGenericConverter的一个很好的例子是在持久实体标识符和实体引用之间进行转换的IdToEntityConverter。只有当目标实体类型声明静态查找器方法(例如,findAccount(Long))时,这样的IdToEntityConverter才可能匹配。您可以在Matches(TypeDescriptor,TypeDescriptor)的实现中执行这样的查找器方法检查。

3.4.4. The ConversionService API

ConversionService定义了在运行时执行类型转换逻辑的统一API。转换器通常在以下门面接口后面运行:

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> 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. Configuring a ConversionService

ConversionService是一个无状态对象,设计为在应用程序启动时实例化,然后在多个线程之间共享。在Spring应用程序中,通常为每个Spring容器(或ApplicationContext)配置一个ConversionService实例。Spring获取ConversionService,并在框架需要执行类型转换时使用它。您还可以将这个ConversionService注入到您的任何Bean中并直接调用它。

If no ConversionService is registered with Spring, the original PropertyEditor-based system is used.

要向Spring注册默认的ConversionService,请添加以下具有idConversionService的Bean定义:

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>
             
             

默认的ConversionService可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。若要使用您自己的自定义转换器补充或覆盖默认转换器,请设置Converters属性。属性值可以实现ConverterConverterFactoryGenericConverter接口中的任何一个。

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>
             
             

在Spring MVC应用程序中使用ConversionService也很常见。请参阅Spring MVC一章中的转换和格式化

在某些情况下,您可能希望在转换期间应用格式。有关使用FormattingConversionServiceFactoryBean.的详细信息,请参阅FormatterRegistrySPI.

3.4.6. Using a ConversionService Programmatically

要以编程方式使用ConversionService实例,您可以像对任何其他Bean一样注入对它的引用。以下示例显示了如何执行此操作:

Java
Kotlin
@Service
public class MyService {

    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}

             
             

对于大多数用例,您可以使用指定Target TypeConvert方法,但它不能用于更复杂的类型,如参数化元素的集合。例如,如果要以编程方式将Integer列表转换为字符串列表,则需要提供源类型和目标类型的正式定义。

幸运的是,TypeDescriptor提供了各种选项来简化此操作,如下面的示例所示:

Java
Kotlin
DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ...
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

             
             

请注意,DefaultConversionService会自动注册适合大多数环境的转换器。这包括集合转换器、标量转换器和基本对象字符串的转换器。您可以使用DefaultConversionService类上的静态addDefaultConverters方法向任何ConverterRegistry注册相同的转换器。

值类型的转换器被重复用于数组和集合,因此不需要创建特定的转换器来从S集合转换为T集合,假设标准集合处理是合适的。

3.5. Spring Field Formatting

正如上一节所讨论的,core.Convert是一个通用类型转换系统。它提供了统一的ConversionService接口和一个强类型的ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。一个Spring容器使用该系统绑定Bean属性值。此外,Spring表达式语言(Spel)和DataBinder都使用此系统绑定字段值。例如,当Spel需要将强制为以完成pression.setValue(对象Bean,对象值)尝试时,core.Convert系统执行强制。

现在考虑典型客户端环境(如Web或桌面应用程序)的类型转换要求。在这种环境下,通常从字符串转换为字符串,以支持客户端回发过程,以及转换回字符串以支持视图渲染过程。此外,您经常需要本地化字符串值。更通用的core.ConvertConverterSPI不直接满足此类格式要求。为了直接解决这些问题,Spring3引入了一个方便的格式化程序SPI,它为客户端环境的PropertyEditor实现提供了一个简单而健壮的替代方案。

一般来说,当您需要实现通用类型转换逻辑 - 时,例如,在<代码>java.util.Date和之间进行转换时,可以使用ConverterSPI。当您在客户端环境(如Web应用程序)中工作并且需要分析和打印本地化的字段值时,可以使用格式化程序SPI。ConversionService为两个SPI提供统一的类型转换接口。

3.5.1. The Formatter SPI

用于实现字段格式化逻辑的格式化程序SPI简单且强类型。下面的清单显示了格式化程序接口定义:

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

             
             

格式化程序打印机解析器构建块接口扩展而来。下面的清单显示了这两个接口的定义:

public interface Printer<T> {

    String print(T fieldValue, Locale locale);
}

             
             
import java.text.ParseException; public interface Parser<T> { T parse(String clientValue, Locale locale) throws ParseException; } 
             
             

要创建您自己的格式化程序,请实现前面所示的格式化程序接口。将T参数化为您希望格式化 - 的对象类型,例如,java.util.Date。实现print()操作以打印T的实例,以便在客户端区域设置中显示。实现parse()操作,以从客户端区域设置返回的格式化表示形式解析T的实例。如果解析尝试失败,格式化程序应引发ParseExceptionIllegalArgumentException。注意确保您的格式化程序实现是线程安全的。

为了方便起见,Format子包提供了几个格式化程序实现。Numbers包提供NumberStyleForMatterCurrencyStyleForMatterPercentStyleForMatter来格式化使用java.ext.NumberFormatNumber对象。DateTime包提供了DateForMatter,以便使用java.ext.DateFormat格式化java.util.Date对象。

以下DateForMatter格式化程序实现的示例:

Java
Kotlin
package org.springframework.format.datetime;

public final class DateFormatter implements Formatter<Date> {

    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;
    }
}

             
             

Spring团队欢迎社区驱动的格式化程序贡献。请参阅GitHub问题进行贡献。

3.5.2. Annotation-driven Formatting

可以按字段类型或注释配置字段格式。若要将批注绑定到格式化程序,请实现AnnotationFormatterFactory。下面的清单显示了AnnotationFormatterFactory接口的定义:

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

             
             

要创建实施,请执行以下操作:

  1. 将A参数化为您希望将格式化逻辑注释类型<org.springframework.format.annotation.DateTimeFormat.>与其关联的字段< >注解类型

  2. getFieldTypes()返回可以使用批注的字段类型。

  3. getPrinter()返回Print以打印带注释的字段的值。

  4. getParser()返回Parser以分析带注释的字段的clientValue

下面的示例AnnotationFormatterFactory实现将@NumberFormat批注绑定到格式化程序以指定数字样式或模式:

Java
Kotlin
public final class NumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<NumberFormat> {

    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }

    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }

    private Formatter<Number> 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();
            }
        }
    }
}

             
             

要触发格式设置,可以使用@NumberFormat注释域,如下例所示:

Java
Kotlin
public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}

             
             
Format Annotation API

org.springframework.format.annotation包中有一个可移植格式的注解API。您可以使用@NumberFormat格式化数字字段,如Doublelong,并使用@DateTimeFormat格式化java.util.Datejava.util.Calendarlong(毫秒时间戳)以及JSR-310java.time

下面的示例使用@DateTimeFormatjava.util.Date格式化为ISO日期(yyyy-MM-dd):

Java
Kotlin
public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}

              
              

3.5.3. The FormatterRegistry SPI

FormatterRegistry是用于注册格式化程序和转换器的SPI。FormattingConversionServiceFormatterRegistry的实现,适用于大多数环境。您可以通过编程或声明方式将此变量配置为Spring Bean,例如使用FormattingConversionServiceFactoryBean.因为这个实现还实现了ConversionService,所以您可以直接将其配置为与Spring的DataBinder和Spring的表达式语言(Spel)一起使用。

下面的清单显示了FormatterRegistrySPI:

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<? extends Annotation> annotationFormatterFactory);
}

             
             

如前面的清单所示,您可以按字段类型或按注释注册格式化程序。

FormatterRegistrySPI允许您集中配置格式化规则,而不是在控制器之间复制此类配置。例如,您可能希望强制所有日期字段以特定方式设置格式,或强制具有特定批注的字段以特定方式设置格式。使用共享的FormatterRegistry,只需定义一次这些规则,就可以在需要格式化时应用它们。

3.5.4. The FormatterRegistrar SPI

FormatterRegister是一个SPI,用于通过FormatterRegistry注册格式化程序和转换器。下面的清单显示了它的接口定义:

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);
}

             
             

当为给定的格式类别(如日期格式)注册多个相关的转换器和格式化程序时,FormatterRegister非常有用。在声明性注册不够 - 的情况下,它也很有用,例如,当格式化程序需要在与其自身不同的特定字段类型下编制索引时,或者当注册<代码>打印机 /<代码>解析器 对时。下一节提供有关转换器和格式化程序注册的更多信息。

3.5.5. Configuring Formatting in Spring MVC

请参阅Spring MVC一章中的转换和格式化

3.6. Configuring a Global Date and Time Format

默认情况下,使用DateFormat.SHORT样式从字符串转换未使用@DateTimeFormat注释的日期和时间字段。如果您愿意,可以通过定义您自己的全局格式来更改这一点。

为此,请确保Spring不注册默认格式化程序。相反,请在以下帮助下手动注册格式化程序:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar

  • org.springframework.format.datetime.DateFormatterRegistrar

例如,以下Java配置注册全局yyyyMMdd格式:

Java
Kotlin
@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;
    }
}

            
            

如果您更喜欢基于xml的配置,您可以使用FormattingConversionServiceFactoryBean.以下示例显示了如何执行此操作:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.standard.DateTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>
            
            

注意:在Web应用程序中配置日期和时间格式时有额外的注意事项。请参阅WebMVC转换和格式WebFlux转换和格式

3.7. Java Bean Validation

Spring框架支持Java Bean验证API。

3.7.1. Overview of Bean Validation

Bean验证为Java应用程序提供了一种通过约束声明和元数据进行验证的通用方法。要使用它,您可以使用声明性验证约束注释域模型属性,然后由运行时强制执行这些约束。有内置的约束,您还可以定义自己的自定义约束。

考虑以下示例,其中显示了一个具有两个属性的简单PersonForm模型:

Java
Kotlin
public class PersonForm {
    private String name;
    private int age;
}

             
             

Bean验证允许您声明约束,如以下示例所示:

Java
Kotlin
public class PersonForm {

    @NotNull
    @Size(max=64)
    private String name;

    @Min(0)
    private int age;
}

             
             

然后,Bean验证验证器根据声明的约束来验证此类的实例。有关该接口的一般信息,请参阅Bean验证。有关特定约束,请参阅Hibernate Validator文档。要了解如何将Bean验证提供程序设置为Spring Bean,请继续阅读。

3.7.2. Configuring a Bean Validation Provider

Spring提供了对Bean验证API的完全支持,包括将Bean验证提供程序引导为Spring Bean。这允许您在应用程序中需要验证的任何地方注入jakarta.validation.ValidatorFactoryjakarta.validation.Validator

您可以使用LocalValidatorFactoryBean将默认Validator配置为Spring Bean,如下例所示:

Java
XML
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; @Configuration public class AppConfig { @Bean public LocalValidatorFactoryBean validator() { return new LocalValidatorFactoryBean(); } } 
             
             

上例中的基本配置使用其缺省引导机制触发Bean验证进行初始化。类路径中应该存在Bean验证提供程序,如Hibernate Validator,并被自动检测到。

Injecting a Validator

LocalValidatorFactoryBean同时实现jakarta.validation.ValidatorFactoryjakarta.validation.Validator,以及Spring的org.springframework.validation.Validator.您可以将对这些接口的引用注入到需要调用验证逻辑的Bean中。

如果您更喜欢直接使用Bean验证API,可以注入对jakarta.validation.Validator的引用,如下例所示:

Java
Kotlin
import jakarta.validation.Validator; @Service public class MyService { @Autowired private Validator validator; } 
              
              

如果您的Bean需要org.springframework.validation.Validator验证API,则可以注入对该API的引用,如下例所示:

Java
Kotlin
import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; } 
              
              
Configuring Custom Constraints

每个Bean验证约束由两部分组成:

  • 声明约束及其可配置属性的@Constraint批注。

  • 实现约束行为的jakarta.validation.ConstraintValidator接口的实现。

为了将声明与实现相关联,每个@Constraint注释都引用一个对应的ConstraintValidator实现类。在运行时,当域模型中遇到约束注释时,ConstraintValidatorFactory实例化引用的实现。

默认情况下,LocalValidatorFactoryBean配置一个SpringConstraintValidatorFactory,该工厂使用Spring创建ConstraintValidator实例。这使您的定制ConstraintValidators像任何其他Spring Bean一样从依赖项注入中受益。

下面的示例显示了一个自定义@Constraint声明,后跟一个使用Spring进行依赖项注入的关联ConstraintValidator实现:

Java
Kotlin
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}

              
              
Java
Kotlin
import jakarta.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator { @Autowired; private Foo aDependency; // ... } 
              
              

如前面的示例所示,ConstraintValidator实现可以像任何其他Spring Bean一样具有其依赖项@Autwire

Spring-driven Method Validation

您可以通过MethodValidationPostProcessorBean定义将Bean验证1.1支持的方法验证功能(以及Hibernate Validator 4.3支持的自定义扩展)集成到Spring上下文中:

Java
XML
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; @Configuration public class AppConfig { @Bean public MethodValidationPostProcessor validationPostProcessor() { return new MethodValidationPostProcessor(); } } 
              
              

为了有资格进行Spring驱动的方法验证,所有目标类都需要用Spring的@valated注释进行注释,该注释还可以选择性地声明要使用的验证组。有关Hibernate验证器和Bean valation1.1提供程序的设置详细信息,请参阅MethodValidationPostProcessor

方法验证依赖于目标类周围的AOP代理,要么是接口上方法的JDK动态代理,要么是CGLIB代理。使用代理有一定的限制,了解AOP代理中介绍了其中一些限制。此外,请记住始终在代理类上使用方法和访问器;直接的字段访问将不起作用。

Additional Configuration Options

默认的LocalValidatorFactoryBean配置在大多数情况下就足够了。从消息内插到遍历解析,各种Bean验证构造都有许多配置选项。有关这些选项的更多信息,请参阅LocalValidatorFactoryBeanjavadoc。

3.7.3. Configuring a DataBinder

从Spring3开始,您可以使用Validator配置DataBinder实例。配置完成后,您可以通过调用binder.valify()来调用验证器。任何验证错误都会自动添加到绑定器的BindingResult中。

下面的示例显示如何以编程方式使用DataBinder在绑定到目标对象后调用验证逻辑:

Java
Kotlin
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();

             
             

您还可以通过dataBinder.addValidatorsdataBinder.replaceValidators为一个DataBinder配置多个Validator实例。当将全局配置的Bean验证与DataBinder实例上本地配置的Spring验证器组合在一起时,这很有用。参见Spring MVC验证配置

3.7.4. Spring MVC 3 Validation

请参阅Spring MVC一章中的验证

4. Spring Expression Language (SpEL)

Spring表达式语言(简称“Spel”)是一种强大的表达式语言,支持在运行时查询和操作对象图。该语言语法类似于统一EL,但提供了其他功能,最显著的是方法调用和基本字符串模板功能。

虽然还有其他几种可用的Java表达式语言 -SpringOGNL、MVEL和JBoss EL,但仅举几个例子, 表达式语言的创建是为了向 社区提供单一的、得到良好支持的表达式语言,该语言可以用于SpringPortfolio中的所有产品。它的语言特性是由Spring项目组合中的项目需求驱动的,包括对Spring Tools for Eclipse中的代码完成支持的工具需求。这就是说,Spel基于一种与技术无关的API,如果需要,它允许集成其他表达式语言实现。

虽然Spel是在Spring包中进行表达式计算的基础,但它并不直接绑定到Spring,可以独立使用。为了自包含,本章中的许多示例使用Spel,就好像它是一种独立的表达式语言。这需要创建一些自举的基础设施类,例如解析器。大多数Spring用户不需要处理此基础设施,而只需编写用于计算的表达式字符串。这种典型用法的一个示例是将Spel集成到创建基于XML或注释的Bean定义中,如定义Bean定义的表达式支持中所示。

本章介绍了表达式语言的特点、API和语言语法。在多个位置,InventorSociety类用作表达式求值的目标对象。这些类声明和用于填充它们的数据列在本章的末尾。

表达式语言支持以下功能:

  • 文字表达式

  • 布尔运算符和关系运算符

  • 正则表达式

  • 类表达式

  • 访问属性、数组、列表和映射

  • 方法调用

  • 关系运算符

  • 赋值

  • 调用构造函数

  • Bean参考

  • 数组构造

  • 内联列表

  • 内联地图

  • 三元运算符

  • 变数

  • 用户定义的函数

  • 馆藏预测

  • 馆藏精选

  • 模板化的表达式

4.1. Evaluation

本节介绍Spel接口及其表达式语言的简单用法。完整的语言参考可在语言参考中找到。

下面的代码引入Spel API来计算文字字符串表达式Hello World

Java
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); (1)
String message = (String) exp.getValue();

            
            
1 The value of the message variable is 'Hello World'.

您最有可能使用的Spel类和接口位于org.springFrawork.Expression包及其子包中,例如spel.support

ExpressionParser接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串。表达式接口负责计算先前定义的表达式字符串。调用parser.parseExpressionexp.getValue时可以抛出的两个异常:ParseExceptionEvaluationException

Spel支持广泛的功能,如调用方法、访问属性和调用构造函数。

在下面的方法调用示例中,我们对字符串文本调用conat方法:

Java
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1)
String message = (String) exp.getValue();

            
            
1 The value of message is now 'Hello World!'.

下面的调用JavaBean属性的示例调用字符串属性Bytes

Java
Kotlin
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); (1)
byte[] bytes = (byte[]) exp.getValue();

            
            
1 This line converts the literal to a byte array.

Spel还通过使用标准点表示法(如pro1.pro2.pro3)以及相应的属性值设置来支持嵌套属性。还可以访问公共字段。

下面的示例说明如何使用点表示法来获取文字的长度:

Java
Kotlin
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 gives the length of the literal.

可以调用字符串的构造函数,而不是使用字符串文字,如下面的示例所示:

Java
Kotlin
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);

            
            
1 Construct a new String from the literal and make it be upper case.

注意泛型方法的使用:public<;T>;T getValue(Class<;T>;desiredResultType)。使用此方法不需要将表达式的值强制转换为所需的结果类型。如果值无法转换为T类型或无法使用注册的类型转换器进行转换,则引发EvaluationException

Spel更常见的用法是提供一个根据特定对象实例(称为根对象)计算的表达式字符串。下面的示例显示如何从Inventor类的实例中检索name属性或创建布尔条件:

Java
Kotlin
// 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

            
            

4.1.1. Understanding EvaluationContext

EvaluationContext接口在计算表达式以解析属性、方法或字段并帮助执行类型转换时使用。Spring提供了两种实现。

  • SimpleEvaluationContext:公开基本Spel语言功能和配置选项的子集,用于不需要Spel语言语法的全部范围并且应该受到有意义限制的表达式类别。示例包括但不限于数据绑定表达式和基于属性的筛选器。

  • StandardEvaluationContext:公开全套Spel语言功能和配置选项。您可以使用它来指定默认的根对象,并配置每个可用的评估相关策略。

SimpleEvaluationContext仅支持Spel语言语法的子集。它排除了Java类型引用、构造函数和Bean引用。它还要求您显式选择对表达式中的属性和方法的支持级别。默认情况下,create()静态工厂方法仅允许对属性进行读访问。您还可以获取构建器来配置所需的确切支持级别,目标是以下一项或某项组合:

  • 仅自定义PropertyAccessor(无反射)

  • 只读访问的数据绑定属性

  • 读写的数据绑定属性

Type Conversion

默认情况下,Spel使用Springcore(org.springframework.core.convert.ConversionService).中提供的转换服务此转换服务附带了许多用于常见转换的内置转换器,但也是完全可扩展的,因此您可以在类型之间添加自定义转换。此外,它是泛型感知的。这意味着,当您在表达式中使用泛型类型时,Spel会尝试转换以保持其遇到的任何对象的类型正确性。

这在实践中意味着什么?假设使用setValue()赋值来设置list属性。该属性的类型实际上是list<;boolean>;。Spel认识到,在将列表中的元素放入其中之前,需要将其转换为布尔。以下示例显示了如何执行此操作:

Java
Kotlin
class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

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);

              
              

4.1.2. Parser Configuration

可以通过使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration).来配置Spel表达式解析器配置对象控制某些表达式组件的行为。例如,如果您索引到一个数组或集合中,并且指定索引处的元素为,Spel可以自动创建该元素。当使用由属性引用链组成的表达式时,这很有用。如果为数组或列表编制索引,并且指定的索引超出了数组或列表的当前大小的末尾,Spel可以自动增加数组或列表以适应该索引。为了在指定索引处添加元素,Spel将在设置指定值之前尝试使用元素类型的默认构造函数创建元素。如果元素类型没有默认构造函数,Null将添加到数组或列表中。如果没有知道如何设置值的内置或自定义转换器,NULL将保留在数组或列表中的指定索引处。下面的示例演示如何自动增加列表:

Java
Kotlin
class Demo {
    public List<String> 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

             
             

4.1.3. SpEL Compilation

Spring Framework4.1包括一个基本的表达式编译器。表达式通常是解释的,这在计算期间提供了很大的动态灵活性,但不能提供最佳性能。对于偶尔使用的表达式,这是很好的,但是,当被其他组件使用时,比如Spring集成,性能可能非常重要,并且没有真正需要动态化。

Spel编译器旨在满足这一需求。在计算期间,编译器生成一个在运行时体现表达式行为的Java类,并使用该类来实现更快的表达式计算。由于缺少与表达式相关的类型,编译器在执行编译时使用在对表达式进行解释计算期间收集的信息。例如,它不能仅从表达式中知道属性引用的类型,但在第一次解释的求值期间,它会找出它是什么。当然,如果各种表达式元素的类型随着时间的推移而改变,那么基于这种派生信息的编译可能会在以后造成麻烦。因此,编译最适合其类型信息在重复求值时不会更改的表达式。

请考虑以下基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

由于前面的表达式涉及数组访问、一些属性取消引用和数值运算,因此性能提升非常明显。在一个50000次迭代的示例微基准测试中,使用解释器计算花费了75ms,而使用表达式的编译版本只花了3ms。

Compiler Configuration

默认情况下,编译器不会打开,但您可以通过两种不同的方式之一来打开它。您可以通过使用解析器配置过程(前面讨论的)或在Spel使用嵌入到另一个组件中时使用一个Spring属性来启用它。本节讨论这两个选项。

编译器可以在org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式中的一种模式下运行。模式如下:

  • off(默认):关闭编译器。

  • 立即:在立即模式下,尽快编译表达式。这通常是在第一次解释评估之后。如果编译后的表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用方会收到异常。

  • 混合:在混合模式下,随着时间的推移,表达式会在解释模式和编译模式之间静默切换。经过一定数量的解释运行后,它们将切换到编译形式,如果编译形式出现问题(如前面所述的类型更改),则表达式将再次自动切换回解释形式。稍后,它可能会生成另一个已编译的形式并切换到该形式。基本上,用户在立即模式中获得的异常是在内部处理的。

立即模式之所以存在,是因为混合模式可能会导致具有副作用的表达式出现问题。如果编译后的表达式在部分成功后崩溃,则它可能已经执行了一些影响系统状态的操作。如果发生这种情况,调用方可能不希望它在解释模式下以静默方式重新运行,因为部分表达式可能会运行两次。

选择模式后,使用SpelParserConfiguration配置解析器。以下示例显示了如何执行此操作:

Java
Kotlin
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);

              
              

当您指定编译器模式时,您还可以指定类加载器(允许传递NULL)。编译后的表达式在提供的任何下创建的子类加载器中定义。如果指定了类加载器,确保它可以看到表达式计算过程中涉及的所有类型,这一点很重要。如果不指定类加载器,则使用默认的类加载器(通常是在表达式求值期间运行的线程的上下文类加载器)。

第二种配置编译器的方法是在Spel嵌入到其他组件中并且可能无法通过配置对象进行配置时使用。在这些情况下,可以通过JVM系统属性(或通过SpringProperties机制)将spring.pression.compline属性设置为SpelCompilerMode枚举值之一(关闭立即混合)。

Compiler Limitations

从Spring Framework4.1开始,就有了基本的编译框架。然而,该框架还不支持编译所有类型的表达式。最初的重点一直是可能在性能关键型上下文中使用的常见表达式。目前无法编译以下几种表达式:

  • 涉及赋值的表达式

  • 依赖于转换服务的表达式

  • 使用自定义解析器或访问器的表达式

  • 使用选择或投影的表达式

将来会有更多类型的表达式可编译。

4.2. Expressions in Bean Definitions

您可以将Spel表达式与基于XML或基于注释的配置元数据一起使用来定义BeanDefinition实例。在这两种情况下,定义表达式的语法的形式都是#{<;表达式字符串>;}

4.2.1. XML Configuration

可以使用表达式设置属性或构造函数参数值,如下面的示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>
             
             

应用程序上下文中的所有Bean都可以作为带有公共Bean名称的预定义变量使用。这包括用于访问运行时环境的标准上下文Bean,如<代码>环境 (类型为org.springframework.core.env.Environment)以及<代码>系统属性 和<代码>系统环境 (类型为<代码>映射字符串,对象&> )。

下面的示例将system PropertiesBean作为Spel变量进行访问:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- other properties -->
</bean>
             
             

请注意,您不必在预定义变量前面加上#符号。

您还可以通过名称引用其他Bean属性,如下例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- other properties -->
</bean>
             
             

4.2.2. Annotation Configuration

要指定默认值,可以在字段、方法和方法或构造函数参数上放置@Value注释。

下面的示例设置字段的默认值:

Java
Kotlin
public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

             
             

下面的示例显示了等同的属性setter方法:

Java
Kotlin
public class PropertyValueTestBean {

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}

             
             

自动连接的方法和构造函数还可以使用@Value注释,如以下示例所示:

Java
Kotlin
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;
    }

    // ...
}

             
             
Java
Kotlin
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;
    }

    // ...
}

             
             

4.3. Language Reference

本节介绍了Spring表达式语言的工作原理。它涵盖了以下主题:

4.3.1. Literal Expressions

支持的文字表达式类型有字符串、数值(整型、实型、十六进制)、布尔型和空型。字符串由单引号分隔。要将单引号本身放入字符串中,请使用两个单引号字符。

下面的清单显示了文字的简单用法。通常,它们不会像这样单独使用,而是作为更复杂的表达式 - 的一部分使用,例如,在逻辑比较运算符一侧使用文字。

Java
Kotlin
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();

             
             

数字支持使用负号、指数记数法和小数点。默认情况下,使用Double.parseDouble()解析实数。

4.3.2. Properties, Arrays, Lists, Maps, and Indexers

使用属性引用导航很容易。为此,请使用句点来指示嵌套属性值。InventorPupinTesla的实例填充了示例部分中使用的类中列出的数据。为了向下导航对象图并获得特斯拉的出生年份和普平的出生城市,我们使用以下表达式:

Java
Kotlin
// evals to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);

             
             

允许属性名称的第一个字母不区分大小写。因此,上例中的表达式可以分别写为Birthate.Year+1900PlaceOfBirth.City。此外,可以选择通过方法调用 - 来访问属性,例如,getPlaceOfBirth().getCity()而不是placeOfBirth.City

数组和列表的内容是使用方括号表示法获得的,如下例所示:

Java
Kotlin
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);

             
             

映射的内容是通过在括号中指定文字键值来获得的。在以下示例中,因为Offers映射的键是字符串,所以我们可以指定字符串文字:

Java
Kotlin
// 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");

             
             

4.3.3. Inline Lists

您可以使用{}表示法直接表示表达式中的列表。

Java
Kotlin
// 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);

             
             

{}本身表示空列表。出于性能原因,如果列表本身完全由固定文字组成,则创建一个常量列表来表示表达式(而不是在每次求值时构建一个新列表)。

4.3.4. Inline Maps

您还可以通过使用{key:Value}表示法在表达式中直接表示映射。以下示例显示了如何执行此操作:

Java
Kotlin
// 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);

             
             

{:}本身表示空映射。出于性能原因,如果映射本身由固定文字或其他嵌套的常量结构(列表或映射)组成,则会创建一个常量映射来表示表达式(而不是在每次求值时构建新的映射)。映射键的引号是可选的(除非键包含句点(.))。上面的例子没有使用带引号的键。

4.3.5. Array Construction

您可以使用熟悉的Java语法构建数组,也可以选择提供一个初始值设定项来在构造时填充数组。以下示例显示了如何执行此操作:

Java
Kotlin
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);

             
             

构造多维数组时,当前不能提供初始值设定项。

4.3.6. Methods

您可以使用典型的Java编程语法来调用方法。您还可以调用文字上的方法。还支持变量参数。以下示例显示如何调用方法:

Java
Kotlin
// 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);

             
             

4.3.7. Operators

Spring表达式语言支持以下类型的运算符:

Relational Operators

使用标准运算符表示法支持关系运算符(等于、不等于、小于、小于或等于、大于、大于或等于)。这些运算符作用于数字类型以及实现可比的类型。下面的清单显示了几个操作符的示例:

Java
Kotlin
// 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);

// uses CustomValue:::compareTo
boolean trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean.class);

              
              

NULL的大于和小于比较遵循一条简单的规则:NULL被视为Nothing(即不是零)。因此,任何其他值始终大于NULL(X&>;NULL始终为TRUE),并且任何其他值都不小于零(X<;NULL始终为FALSE)。

如果您更喜欢数字比较,请避免基于数字的NULL比较,而倾向于与零进行比较(例如,X&>0X<;0)。

除了标准的关系运算符之外,Spel还支持instanceof和基于正则表达式的Matches运算符。下面的清单显示了这两个示例:

Java
Kotlin
// 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);

              
              
Be careful with primitive types, as they are immediately boxed up to their wrapper types. For example, 1 instanceof T(int) evaluates to false, while 1 instanceof T(Integer) evaluates to true, as expected.

还可以将每个符号运算符指定为纯字母等效项。这避免了使用的符号对于嵌入表达式的文档类型(如在XML文档中)具有特殊含义的问题。文本上的等价物是:

  • lt(<;)

  • gt(>;)

  • le(<;=)

  • ge(>;=)

  • eq(==)

  • ne(!=)

  • div(/)

  • mod(%)

  • 不是()。

所有文本操作符都不区分大小写。

Logical Operators

SPEL支持以下逻辑运算符:

  • (&;&;)

  • (||)

  • ()

以下示例显示如何使用逻辑运算符:

Java
Kotlin
// -- 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);

              
              
Mathematical Operators

您可以对数字和字符串使用加法运算符(+)。您只能对数字使用减法(-)、乘法(*)和除法(/)运算符。您还可以对数字使用模运算符(%)和指数幂(^)运算符。强制执行标准运算符优先级。以下示例显示了正在使用的数学运算符:

Java
Kotlin
// 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

              
              
The Assignment Operator

要设置属性,请使用赋值运算符(=)。这通常是在调用setValue时完成的,但也可以在调用getValue时完成。下面的清单显示了使用赋值运算符的两种方法:

Java
Kotlin
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);

              
              

4.3.8. Types

您可以使用特殊的T运算符来指定java.lang.Class(类型)的实例。静态方法也可以通过使用该操作符来调用。StandardEvaluationContext使用TypeLocator查找类型,而StandardTypeLocator(可以替换)是在理解java.lang包的基础上构建的。这意味着对java.lang包中类型的T()引用不需要完全限定,但所有其他类型引用必须完全限定。以下示例显示如何使用T运算符:

Java
Kotlin
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);

             
             

4.3.9. Constructors

您可以使用new运算符调用构造函数。除了位于java.lang包(IntegerFloat字符串等)中的类型之外,您应该对所有类型使用完全限定类名。下面的示例说明如何使用new运算符调用构造函数:

Java
Kotlin
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);

             
             

4.3.10. Variables

您可以使用#varableName语法在表达式中引用变量。变量是通过在EvaluationContext实现上使用setVariable方法设置的。

有效的变量名必须由以下一个或多个受支持的字符组成。

  • 字母:AZaz

  • 位数:09

  • 下划线:_

  • 美元符号:$

下面的示例显示如何使用变量。

Java
Kotlin
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"

             
             
The #this and #root Variables

始终定义#this变量,并引用当前求值对象(根据该对象解析不合格的引用)。#ROOT变量始终是定义的,并且引用根上下文对象。尽管#this可能会因计算表达式的组件而不同,但#ROOT始终引用根。以下示例显示如何使用#this#ROOT变量:

Java
Kotlin
// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
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<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

              
              

4.3.11. Functions

您可以通过注册可在表达式字符串中调用的用户定义函数来扩展Spel。该函数通过EvaluationContext注册。以下示例显示如何注册用户定义函数:

Java
Kotlin
Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

             
             

例如,考虑使用以下实用程序方法来反转字符串:

Java
Kotlin
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();
    }
}

             
             

然后可以注册并使用前面的方法,如下面的示例所示:

Java
Kotlin
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);

             
             

4.3.12. Bean References

如果使用Bean解析器配置了求值上下文,则可以使用@符号从表达式中查找Bean。以下示例显示了如何执行此操作:

Java
Kotlin
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);

             
             

要访问工厂Bean本身,您应该在Bean名称前面加上一个&;符号。以下示例显示了如何执行此操作:

Java
Kotlin
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);

             
             

4.3.13. Ternary Operator (If-Then-Else)

您可以使用三元运算符在表达式中执行IF-THEN-ELSE条件逻辑。下面的清单显示了一个最小的示例:

Java
Kotlin
String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

             
             

在本例中,布尔值FALSE导致返回字符串值‘FalseExp’。下面是一个更现实的例子:

Java
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'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

             
             

有关三元运算符的更短语法,请参阅下一节关于Elvis运算符的信息。

4.3.14. The Elvis Operator

Elvis操作符是三元操作符语法的缩写,用于Groovy语言中。使用三元运算符语法时,通常必须将一个变量重复两次,如下例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

             
             

相反,您可以使用Elvis操作符(因与Elvis的发型相似而命名)。下面的示例显示如何使用Elvis运算符:

Java
Kotlin
ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'

             
             

下面的清单显示了一个更复杂的示例:

Java
Kotlin
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

             
             

您可以使用Elvis运算符在表达式中应用默认值。下面的示例显示如何在@Value表达式中使用Elvis运算符:

@Value("#{systemProperties['pop3.port'] ?: 25}")

                  
                  

这将注入系统属性pop3.port(如果已定义)或25(如果未定义)。

4.3.15. Safe Navigation Operator

安全导航操作符用于避免NullPointerException,它来自Groovy语言。通常,当您具有对对象的引用时,在访问对象的方法或属性之前,可能需要验证它是否不为空。为了避免这种情况,安全导航操作符返回NULL,而不是引发异常。以下示例显示如何使用安全导航操作符:

Java
Kotlin
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!!!

             
             

4.3.16. Collection Selection

选择是一种强大的表达式语言功能,它允许您通过从其条目中进行选择来将源集合转换为另一个集合。

选择使用.?[seltionExpression]的语法。它筛选集合并返回包含原始元素子集的新集合。例如,选择使我们可以轻松获得塞尔维亚发明家的列表,如下例所示:

Java
Kotlin
List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "members.?[nationality == 'Serbian']").getValue(societyContext);

             
             

支持对数组和实现java.lang.Iterablejava.util.Map的任何内容进行选择。对于列表或数组,根据每个单独的元素评估选择条件。根据映射,根据每个映射条目(Java类型Map.Entry的对象)评估选择标准。每个映射条目都将其作为属性进行访问,以便在选择中使用。

以下表达式返回一个新映射,该映射由条目的值小于27的原始映射的元素组成:

Java
Kotlin
Map newMap = parser.parseExpression("map.?[value<27]").getValue();

             
             

除了返回所有选定的元素外,还可以仅检索第一个或最后一个元素。要获得与选择匹配的第一个元素,语法为.^[seltionExpression]。要获得最后一个匹配的选择,语法为.$[seltionExpression]

4.3.17. Collection Projection

投影允许集合驱动子表达式的计算,结果是一个新的集合。投影的语法为.![projectionExpression]。例如,假设我们有一个发明者列表,但想要他们出生的城市列表。实际上,我们想要为Inventor列表中的每个条目评估‘placeOfBirth.City’。下面的示例使用Projection来执行此操作:

Java
Kotlin
// returns ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]");

             
             

数组和实现java.lang.Iterablejava.util.Map的任何内容都支持投影。当使用地图驱动投影时,根据地图中的每个条目(表示为JavaMap.Entry)计算投影表达式。在地图上投影的结果是一个列表,该列表由针对每个地图条目的投影表达式的求值组成。

4.3.18. Expression templating

表达式模板允许将文字文本与一个或多个求值块混合。每个求值块都由您可以定义的前缀和后缀字符分隔。常见的选择是使用#{}作为分隔符,如下例所示:

Java
Kotlin
String randomPhrase = parser.parseExpression(
        "random number is #{T(java.lang.Math).random()}",
        new TemplateParserContext()).getValue(String.class);

// evaluates to "random number is 0.7038186818312008"

             
             

字符串的计算方法是将文字文本‘RANDOM NAME IS’#{}分隔符(在本例中是调用RANDOM()方法的结果)中的表达式计算结果连接在一起。parseExpression()方法的第二个参数的类型为ParserContextParserContext接口用于影响表达式的解析方式,以便支持表达式模板功能。TemplateParserContext定义如下:

Java
Kotlin
public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

             
             

4.4. Classes Used in the Examples

本节列出了本章所有示例中使用的类。

Inventor.Java
Inventor.kt
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; } } 
            
            
PlaceOfBirth.java
PlaceOfBirth.kt
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;
    }
}

            
            
Society.java
Society.kt
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<Inventor> members = new ArrayList<Inventor>(); 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; } } 
            
            

5. Aspect Oriented Programming with Spring

面向方面编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象编程(OOP)。在OOP中,模块化的关键单位是类,而在AOP中,模块化的单位是方面。方面支持跨多个类型和对象的关注点(如事务管理)的模块化。(在AOP文献中,这种关注点通常被称为“横切”关注点。)

Spring的关键组件之一是AOP框架。虽然Spring IoC容器不依赖于AOP(这意味着如果您不想使用AOP,则不需要使用AOP),但AOP是对Spring IoC的补充,以提供一个功能非常强大的中间件解决方案。

Spring AOP with AspectJ pointcuts

通过使用基于模式的方法@AspectJ注释样式,Spring提供了编写定制方面的简单而强大的方法。这两种风格都提供了完全类型化的建议和AspectJ切入点语言的使用,同时仍然使用Spring AOP进行编织。

本章讨论基于模式和@AspectJ的AOP支持。较低级别的AOP支持将在下一章中讨论。

AOP在Spring框架中用于:

  • 提供声明性企业服务。最重要的此类服务是声明性事务管理

  • 让用户实现定制方面,用AOP补充他们对OOP的使用。

If you are interested only in generic declarative services or other pre-packaged declarative middleware services such as pooling, you do not need to work directly with Spring AOP, and can skip most of this chapter.

5.1. AOP Concepts

让我们从定义一些核心的AOP概念和术语开始。这些术语不是特定于Spring的。不幸的是,AOP术语并不特别直观。然而,如果Spring使用自己的术语,那就更令人困惑了。

  • 方面:跨多个类的关注点的模块化。事务管理是企业Java应用程序中横切关注点的一个很好的例子。在Spring AOP中,方面是通过使用常规类(基于模式的方法)或使用@方面注释的常规类(@AspectJ样式)实现的。

  • 连接点:程序执行期间的一个点,如方法的执行或异常的处理。在Spring AOP中,连接点始终表示方法执行。

  • 建议:一个方面在特定的连接点采取的行动。不同类型的建议包括“周围”、“之前”和“之后”建议。(建议类型将在后面讨论。)包括Spring在内的许多AOP框架将通知建模为拦截器,并在连接点周围维护一系列拦截器。

  • 切入点:匹配连接点的谓词。通知与切入点表达式相关联,并在与切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。切入点表达式匹配的连接点的概念是AOP的核心,默认情况下,Spring使用AspectJ切入点表达式语言。

  • 简介:代表类型声明其他方法或字段。Spring AOP允许您向任何建议的对象引入新的接口(和相应的实现)。例如,您可以使用介绍来使一个Bean实现一个IsModified接口,以简化缓存。(在AspectJ社区中,介绍称为类型间声明。)

  • 目标对象:一个或多个方面建议的对象。也被称为“建议对象”。由于Spring AOP是通过使用运行时代理实现的,因此该对象始终是一个代理对象。

  • AOP代理:由AOP框架创建的对象,用于实现方面契约(通知方法执行等)。在Spring框架中,AOP代理是JDK动态代理或CGLIB代理。

  • 编织:将方面与其他应用程序类型或对象链接,以创建建议的对象。这可以在编译时(例如,使用AspectJ编译器)、加载时或运行时完成。Spring AOP与其他纯Java AOP框架一样,在运行时执行编织。

Spring AOP包括以下类型的建议:

  • 在建议之前:在连接点之前运行的建议,但不能阻止执行流继续进行到连接点(除非它抛出异常)。

  • 返回建议之后:在连接点正常完成后运行的建议(例如,如果方法返回时没有抛出异常)。

  • 抛出建议之后:方法通过抛出异常退出时要运行的建议。

  • 建议之后(最后):无论连接点以何种方式退出(正常或异常返回),都要运行建议。

  • 围绕建议:围绕连接点的建议,如方法调用。这是最有力的建议。环绕建议可以在方法调用之前和之后执行自定义行为。它还负责选择是继续到连接点,还是通过返回自己的返回值或引发异常来缩短建议的方法的执行。

围绕建议是最普遍的一种建议。由于Spring AOP与AspectJ一样,提供了全面的建议类型,因此我们建议您使用能够实现所需行为的功能最弱的建议类型。例如,如果只需要使用方法的返回值更新缓存,则实现返回后建议比实现环绕建议更好,尽管环绕建议也可以实现相同的功能。使用最具体的建议类型可以提供更简单的编程模型,并降低出错的可能性。例如,您不需要在用于环绕建议的JoinPoint上调用Continue()方法,因此,您不可能不调用它。

所有建议参数都是静态类型的,因此您可以使用适当类型的建议参数(例如,方法执行的返回值的类型),而不是对象数组。

与切入点匹配的连接点的概念是AOP的关键,这将其与仅提供拦截的旧技术区分开来。切入点允许独立于面向对象的层次结构来确定建议的目标。例如,您可以向跨越多个对象的一组方法(如服务层中的所有业务操作)应用提供声明性事务管理的CONTER建议。

5.2. Spring AOP Capabilities and Goals

Spring AOP是用纯Java实现的。不需要特殊的编译过程。Spring AOP不需要控制类加载器层次结构,因此适合在Servlet容器或应用服务器中使用。

Spring AOP目前只支持方法执行连接点(建议在SpringBean上执行方法)。虽然可以在不破坏核心的Spring AOPAPI的情况下添加对字段拦截的支持,但是没有实现字段拦截。如果您需要建议字段访问和更新连接点,可以考虑使用AspectJ这样的语言。

Spring AOP的AOP方法不同于大多数其他AOP框架。其目的不是提供最完整的AOP实现(尽管Spring AOP很有能力)。相反,其目标是在AOP实现和Spring IOC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。

因此,例如,Spring框架的AOP功能通常与Spring IOC容器结合使用。方面是通过使用标准的Bean定义语法配置的(尽管这允许强大的“自动代理”功能)。这是与其他AOP实现的一个重要区别。使用Spring AOP不能轻松高效地做一些事情,比如建议非常细粒度的对象(通常是域对象)。在这种情况下,AspectJ是最佳选择。然而,我们的经验是,Spring AOP为适用于AOP的企业Java应用程序中的大多数问题提供了一个极好的解决方案。

Spring AOP从未努力与AspectJ竞争,以提供全面的AOP解决方案。我们认为,基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都很有价值,它们是互补的,而不是竞争。Spring将Spring AOP和IoC与AspectJ无缝集成,以支持在一致的基于Spring的应用程序架构中使用AOP。这种集成不会影响Spring AOP API或AOP联盟API。Spring AOP保持向后兼容。有关Spring AOP API的讨论,请参阅下一章

Spring框架的核心原则之一是非侵入性。这就是不应该强迫您将特定于框架的类和接口引入到您的业务或域模型中的想法。然而,在某些地方,Spring框架确实允许您在代码库中引入特定于Spring框架的依赖项。为您提供这些选项的理由是,在某些情况下,以这种方式阅读或编写某些特定的功能可能会更容易。然而,Spring框架(几乎)总是为您提供选择:您可以自由地做出明智的决定,决定哪个选项最适合您的特定用例或场景。

与本章相关的一个这样的选择是选择哪种AOP框架(以及哪种AOP风格)。您可以选择AspectJ、Spring AOP或两者兼而有之。您还可以选择@AspectJ注解风格的方法或Spring XML配置风格的方法。本章选择首先介绍@AspectJ风格的方法这一事实不应被视为Spring团队更喜欢@AspectJ注释风格的方法而不是Spring XML配置风格的迹象。

请参阅选择使用哪种AOP声明样式,以更完整地讨论每种样式的“为什么”和“为什么”。

5.3. AOP Proxies

Spring AOP默认使用标准的JDK动态代理作为AOP代理。这使得任何接口(或接口集)都可以被代理。

Spring AOP还可以使用CGLIB代理。这对于代理类而不是接口是必要的。默认情况下,如果业务对象没有实现接口,则使用CGLIB。由于对接口而不是类进行编程是一种良好的实践,因此业务类通常实现一个或多个业务接口。可以强制使用CGLIB,在这些情况下(希望这种情况很少见),您需要建议未在接口上声明的方法,或者需要将代理对象作为具体类型传递给方法。

重要的是要了解Spring AOP是基于代理的这一事实。请参阅了解AOP代理,以全面了解此实现细节的实际含义。

5.4. @AspectJ support

@AspectJ指的是一种将方面声明为带有注释的常规Java类的样式。@AspectJ样式是由AspectJ项目作为AspectJ 5版本的一部分引入的。Spring解释与AspectJ 5相同的注释,使用AspectJ提供的库进行切入点解析和匹配。不过,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

Using the AspectJ compiler and weaver enables use of the full AspectJ language and is discussed in Using AspectJ with Spring Applications.

5.4.1. Enabling @AspectJ Support

要在Spring配置中使用@AspectJ方面,您需要启用对基于@AspectJ方面配置Spring AOP的Spring支持,并根据这些方面是否建议自动代理Bean。通过自动代理,我们的意思是,如果Spring确定一个Bean由一个或多个方面建议,它会自动为该Bean生成一个代理来拦截方法调用,并确保建议按需运行。

@AspectJ支持可以通过XML或Java风格的配置来启用。在这两种情况下,您还需要确保AspectJ的aspectjwever.jar库位于您的应用程序(1.8或更高版本)的类路径中。该库可以在AspectJ发行版的lib目录中找到,也可以从Maven Central存储库中获得。

Enabling @AspectJ Support with Java Configuration

要使用Java@configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注释,如下例所示:

Java
Kotlin
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

              
              
Enabling @AspectJ Support with XML Configuration

要使用基于XML的配置启用@AspectJ支持,请使用aop:AspectJ-AutoProxy元素,如下例所示:

<aop:aspectj-autoproxy/>
              
              

这里假设您按照基于XML架构的配置中所述使用架构支持。有关如何在AOP命名空间中导入标记的信息,请参阅AOP架构

5.4.2. Declaring an Aspect

启用@AspectJ支持后,Spring会自动检测在应用程序上下文中定义的任何Bean,其类是@AspectJ方面(具有@AspectJ注释),并用于配置Spring AOP。接下来的两个示例显示了一个不太有用的方面所需的最小定义。

两个示例中的第一个显示了应用程序上下文中的常规Bean定义,它指向具有@方面注释的Bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
    <!-- configure properties of the aspect here -->
</bean>
             
             

两个示例中的第二个显示了<代码>NotVeryUsefulAspect类定义,该类定义使用org.aspectj.lang.annotation.Aspect批注;

Java
Kotlin
package org.xyz; import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { } 
             
             

方面(用@方面标注的类)可以有方法和字段,与任何其他类一样。它们还可以包含切入点、建议和介绍(类型间)声明。

Autodetecting aspects through component scanning
You can register aspect classes as regular beans in your Spring XML configuration, via @Bean methods in @Configuration classes, or have Spring autodetect them through classpath scanning — the same as any other Spring-managed bean. However, note that the @Aspect annotation is not sufficient for autodetection in the classpath. For that purpose, you need to add a separate @Component annotation (or, alternatively, a custom stereotype annotation that qualifies, as per the rules of Spring’s component scanner).
Advising aspects with other aspects?
In Spring AOP, aspects themselves cannot be the targets of advice from other aspects. The @Aspect annotation on a class marks it as an aspect and, hence, excludes it from auto-proxying.

5.4.3. Declaring a Pointcut

切入点确定感兴趣的连接点,从而使我们能够控制建议何时运行。Spring AOP只支持SpringBean的方法执行连接点,因此您可以认为切入点与SpringBean上的方法执行相匹配。切入点声明包含两个部分:包含名称和任何参数的签名,以及准确确定我们感兴趣的方法执行的切入点表达式。在AOP的@AspectJ注释风格中,切入点签名由常规方法定义提供,切入点表达式使用@PointCut注释指示(充当切入点签名的方法必须具有void返回类型)。

举一个例子可能有助于明确切入点签名和切入点表达式之间的区别。下面的示例定义一个名为anyOldTransfer的切入点,该切入点与名为Transfer的任何方法的执行相匹配:

Java
Kotlin
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

             
             

构成@PointCut注释的值的切入点表达式是一个常规的AspectJ切入点表达式。有关AspectJ切入点语言的完整讨论,请参阅AspectJ编程指南(有关扩展,请参阅AspectJ 5开发人员笔记)或有关AspectJ的书籍(如Colyer等人的Eclipse AspectJ或Ramnivas Laddad的AspectJ in Action)。

Supported Pointcut Designators

Spring AOP支持在切入点表达式中使用以下AspectJ切入点指示符(PCD):

  • 执行:用于匹配方法执行连接点。这是使用Spring AOP时要使用的主要切入点指示符。

  • in:将匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法的执行)。

  • this:将匹配限制为连接点(使用Spring AOP时方法的执行),其中Bean引用(Spring AOP代理)是给定类型的实例。

  • 目标:将匹配限制到目标对象(被代理的应用程序对象)是给定类型的实例的连接点(使用Spring AOP时方法的执行)。

  • args:将匹配限制为连接点(使用Spring AOP时执行方法),其中参数是给定类型的实例。

  • @Target:将匹配限制为连接点(在使用Spring AOP时执行方法),其中执行对象的类具有给定类型的批注。

  • @args:将匹配限制到连接点(使用Spring AOP时执行方法),其中传递的实际参数的运行时类型具有给定类型的批注。

  • @in:将匹配限制为具有给定批注的类型内的连接点(使用Spring AOP时,执行带有给定批注的类型中声明的方法)。

  • @Annotation:将匹配限制为连接点的主题(在Spring AOP中运行的方法)具有给定注释的连接点。

Other pointcut types

完整的AspectJ切入点语言支持在Spring中不支持的其他切入点指示符:callgetset预初始化静态初始化初始化处理程序AdviceExecutionin incodecflowif@this@with incode。在由Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException

Spring AOP支持的切入点指示器集可能会在未来的版本中进行扩展,以支持更多的AspectJ切入点指示器。

由于Spring AOP仅将匹配限制为方法执行连接点,因此前面对切入点指示符的讨论给出了比您在AspectJ编程指南中找到的更窄的定义。此外,AspectJ本身具有基于类型的语义,并且在执行连接点,thisTarget都引用同一个对象:执行该方法的对象。Spring AOP是一个基于代理的系统,它区分代理对象本身(绑定到this)和代理背后的目标对象(绑定到目标)。

由于Spring的AOP框架是基于代理的,因此根据定义,目标对象内的调用不会被拦截。对于JDK代理,只能拦截代理上的公共接口方法调用。使用CGLIB,代理上的公共和受保护方法调用被拦截(如果需要,甚至还可以拦截包可见的方法)。但是,通过代理进行的常见交互应该始终通过公共签名来设计。

请注意,切入点定义通常与任何被拦截的方法相匹配。如果切入点严格意义上是仅公开的,即使在CGLIB代理场景中通过代理进行潜在的非公开交互,也需要相应地定义它。

如果您的拦截需要包括目标类中的方法调用甚至构造函数,请考虑使用Spring驱动的本机AspectJ编织,而不是Spring的基于代理的AOP框架。这构成了一种不同的AOP使用模式,具有不同的特点,因此在做出决定之前,请务必熟悉编织。

Spring AOP还支持名为Bean的附加PCD。此PCD允许您将连接点的匹配限制为特定命名的Spring Bean或一组命名的SpringBean(当使用通配符时)。BeanPCD的格式如下:

Java
Kotlin
bean(idOrNameOfBean)

              
              

idOrNameOfBean内标识可以是任何Spring Bean的名称。提供了使用*<字符的有限通配符支持,因此,如果您为您的SpringBean建立了一些命名约定,您可以编写一个BeanPCD表达式来选择它们。与其他切入点指示符一样,BeanPCD也可以与&;&;(And)、||(Or)和(求反)操作符一起使用。

BeanPCD仅在Spring AOP中受支持,在本机AspectJ编织中不受支持。它是AspectJ定义的标准PCD的特定于Spring的扩展,因此对于@Aspect模型中声明的方面不可用。

BeanPCD在实例级别(基于Spring Bean名称概念构建)而不是仅在类型级别(仅限于基于编织的AOP)进行操作。基于实例的切入点指示器是Spring的基于代理的AOP框架的一种特殊功能,它与Spring Bean工厂紧密集成,在该工厂中,通过名称来标识特定的Bean是自然和直接的。

Combining Pointcut Expressions

您可以使用&;&;,||组合切入点表达式。您还可以按名称引用切入点表达式。下面的示例显示了三个切入点表达式:

Java
Kotlin
@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 matches if a method execution join point represents the execution of any public method.
2 inTrading matches if a method execution is in the trading module.
3 tradingOperation matches if a method execution represents any public method in the trading module.

最佳实践是从较小的命名组件构建更复杂的切入点表达式,如前面所示。当按名称引用切入点时,应用正常的Java可见性规则(您可以看到相同类型的私有切入点、层次结构中的受保护切入点、任何地方的公共切入点等等)。可见性不影响切入点匹配。

Sharing Common Pointcut Definitions

在使用企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。为此,我们建议定义一个捕获公共切入点表达式的CommonPointCuts方面。这样的方面通常类似于以下示例:

Java
Kotlin
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() {} } 
              
              

您可以在需要切入点表达式的任何地方引用在这样一个方面中定义的切入点。例如,要使服务层成为事务性的,您可以编写以下代码:

<aop:config>
    <aop:advisor pointcut="com.xyz.myapp.CommonPointcuts.businessService()" advice-ref="tx-advice"/>
</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
              
              

<;aop:config>;<;aop:Advisor&>元素在基于架构的AOP支持中讨论。事务元素在事务管理中讨论。

Examples

Spring AOP用户可能最常使用Execution切入点指示符。执行表达式的格式如下:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)

除了返回类型模式(前面片段中的ret-type-Pattern)、名称模式和参数模式之外的所有部分都是可选的。返回类型模式确定方法的返回类型必须是什么才能匹配连接点。*最常用作返回类型模式。它匹配任何返回类型。仅当方法返回给定类型时,完全限定类型名才匹配。名称模式与方法名称匹配。您可以将*通配符用作名称模式的全部或部分。如果指定了声明类型模式,请在后面加上.以将其连接到名称模式组件。参数模式稍微复杂一些:()匹配不带参数的方法,而(..)匹配任意数量(零个或更多)的参数。(*)模式匹配接受任何类型的一个参数的方法。(*,字符串)匹配接受两个参数的方法。第一个参数可以是任何类型,而第二个参数必须是字符串。有关更多信息,请参考《AspectJ编程指南》的语言语义部分。

以下示例显示了一些常见的切入点表达式:

  • 执行任何公共方法:

        execution(public * *(..))
  • 执行名称以set开头的任何方法:

        execution(* set*(..))
  • 执行由AcCountService接口定义的任何方法:

        execution(* com.xyz.service.AccountService.*(..))
  • 执行服务包中定义的任何方法:

        execution(* com.xyz.service.*.*(..))
  • 执行服务包或其一个子包中定义的任何方法:

        execution(* com.xyz.service..*.*(..))
  • 服务包内的任何连接点(仅在Spring AOP中执行方法):

        within(com.xyz.service.*)
  • 服务包或其一个子包内的任何连接点(仅在Spring AOP中执行方法):

        within(com.xyz.service..*)
  • 代理实现Account Service接口的任何连接点(仅在Spring AOP中执行方法):

        this(com.xyz.service.AccountService)
    this is more commonly used in a binding form. See the section on Declaring Advice for how to make the proxy object available in the advice body.
  • 目标对象实现Account Service接口的任何连接点(仅在Spring AOP中执行方法):

        target(com.xyz.service.AccountService)
    target is more commonly used in a binding form. See the Declaring Advice section for how to make the target object available in the advice body.
  • 接受单个参数并且在运行时传递的参数是可序列化的的任何连接点(仅在Spring AOP中执行方法):

        args(java.io.Serializable)
    args is more commonly used in a binding form. See the Declaring Advice section for how to make the method arguments available in the advice body.

    请注意,本例中给出的切入点不同于执行(**(java.io.Serializable))。如果在运行时传递的参数是Serializable,则args版本匹配,如果方法签名声明了一个Serializable类型的参数,则执行版本匹配。

  • 目标对象具有@Transaction注释的任何连接点(仅在Spring AOP中执行方法):

        @target(org.springframework.transaction.annotation.Transactional)
    You can also use @target in a binding form. See the Declaring Advice section for how to make the annotation object available in the advice body.
  • 目标对象的声明类型具有@Transaction注释的任何连接点(仅在Spring AOP中执行方法):

        @within(org.springframework.transaction.annotation.Transactional)
    You can also use @within in a binding form. See the Declaring Advice section for how to make the annotation object available in the advice body.
  • 执行方法具有@Transaction注释的任何连接点(仅在Spring AOP中执行方法):

        @annotation(org.springframework.transaction.annotation.Transactional)
    You can also use @annotation in a binding form. See the Declaring Advice section for how to make the annotation object available in the advice body.
  • 任何只接受单个参数的连接点(仅在Spring AOP中执行方法),并且在其中传递的参数的运行时类型都有@Classed注释:

        @args(com.xyz.security.Classified)
    You can also use @args in a binding form. See the Declaring Advice section how to make the annotation object(s) available in the advice body.
  • 名为tradeService的Spring Bean上的任何连接点(仅在Spring AOP中执行方法):

        bean(tradeService)
  • 具有与通配符表达式*Service匹配的名称的Spring Bean上的任何连接点(仅在Spring AOP中执行方法):

        bean(*Service)
Writing Good Pointcuts

在编译期间,AspectJ处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态)与给定的切入点匹配是一个代价高昂的过程。(动态匹配意味着不能通过静态分析完全确定匹配,并且在代码中放置测试以确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ会将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点以DNF(析取范式)重写,并对切入点的组件进行排序,以便首先检查那些评估成本较低的组件。这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。

然而,AspectJ只能使用它被告知的内容。为了获得最佳匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。现有的指示符自然分为以下三组之一:KIND、SERCEING和CONTEXTIAL:

  • Kinded指示符选择特定类型的连接点:ExecutionGetSetCall处理程序

  • 作用域指示器选择一组感兴趣的连接点(可能有多种):

  • 上下文指示符基于上下文匹配(并可选地绑定):ThisTarget@Annotation

一个写得好的切入点应该至少包括前两种类型(KIND和作用域)。您可以根据连接点上下文包括要匹配的上下文指示符,也可以绑定该上下文以在建议中使用。只提供亲切的指示符或仅提供上下文指示符是可行的,但由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。作用域指示符的匹配速度非常快,使用它们意味着AspectJ可以非常迅速地消除不应该进一步处理的连接点组。如果可能的话,一个好的切入点应该总是包含一个切入点。

5.4.4. Declaring Advice

建议与切入点表达式相关联,并在与切入点匹配的方法执行之前、之后或周围运行。切入点表达式可以是对命名切入点的简单引用,也可以是原地声明的切入点表达式。

Before Advice

您可以在方面中使用@BEFORE注释在通知之前声明:

Java
Kotlin
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() { // ... } } 
              
              

如果使用就地切入点表达式,则可以将前面的示例重写为以下示例:

Java
Kotlin
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() { // ... } } 
              
              
After Returning Advice

返回建议后,当匹配的方法执行正常返回时运行。您可以使用@AfterReturning注释来声明它:

Java
Kotlin
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() { // ... } } 
              
              
You can have multiple advice declarations (and other members as well), all inside the same aspect. We show only a single advice declaration in these examples to focus the effect of each one.

有时,您需要在通知正文中访问返回的实际值。您可以使用@AfterReturning的形式绑定返回值以获得该访问权限,如下面的示例所示:

Java
Kotlin
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) { // ... } } 
              
              

返回属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行返回时,返回值作为相应的参数值传递给通知方法。返回子句还将匹配限制为仅那些返回指定类型的值的方法执行(在本例中,对象与任何返回值匹配)。

请注意,在返回建议后使用时,不可能返回完全不同的引用。

After Throwing Advice

抛出建议后,当匹配的方法执行通过抛出异常退出时运行。您可以使用@AfterThrowing注释来声明它,如下面的示例所示:

Java
Kotlin
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() { // ... } } 
              
              

通常,您希望通知仅在引发给定类型的异常时运行,并且还经常需要访问通知正文中引发的异常。您可以使用<代码>抛出 属性来限制匹配(如果需要,则使用< >Throwable 作为异常类型),并将抛出的异常绑定到建议参数。以下示例显示了如何执行此操作:

Java
Kotlin
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) { // ... } } 
              
              

抛出属性中使用的名称必须与通知方法中的参数名称相对应。当方法执行通过引发异常退出时,该异常将作为相应的参数值传递给通知方法。抛出子句还将匹配限制为仅那些抛出指定类型异常的方法执行(在本例中为DataAccessException)。

请注意,@AfterThrowing并不表示一般的异常处理回调。具体地说,@AfterThrowing通知方法应该只从连接点(用户声明的目标方法)本身接收异常,而不是从附带的@After/@AfterReturning方法接收异常。

After (Finally) Advice

After(最后)建议在匹配的方法执行退出时运行。它是使用@After注释声明的。After建议必须准备好处理正常和异常返回条件。它通常用于释放资源和类似目的。下面的示例显示如何使用After Finally建议:

Java
Kotlin
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() { // ... } } 
              
              

请注意,AspectJ中的@After通知被定义为“After Finally通知”,类似于try-Catch语句中的Finally块。对于从连接点抛出的任何结果、正常返回或异常(用户声明的目标方法),都将调用它,而@AfterReturning仅适用于成功的正常返回。

Around Advice

最后一种建议是围绕建议。环游建议“绕过”匹配方法的执行。它有机会在方法运行之前和之后做工作,并确定方法何时、如何运行,甚至是否真正开始运行。如果您需要以线程安全的方式共享方法执行前后的状态,例如,启动和停止计时器,则通常使用环回建议。

始终使用符合您要求的最不强大的建议形式。

例如,如果之前建议足以满足您的需要,请不要使用环绕建议。

AURBLE建议是通过使用@Around注解方法来声明的。该方法应将Object声明为其返回类型,并且该方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法的主体内,您必须在ProceedingJoinPoint上调用Continue(),才能运行底层方法。调用不带参数的Continue()将导致调用方的原始参数在被调用时被提供给底层方法。对于高级用例,有一个重载的Continue()方法,它接受参数数组(Object[])。在调用底层方法时,数组中的值将用作该方法的参数。

当使用对象[]调用时,Continue的行为与由AspectJ编译器编译的AspectJ编译器编译的变通建议的的行为略有不同。对于使用传统AspectJ语言编写的CORLE通知,传递给Process的参数数量必须与传递给CORLE通知的参数数量匹配(而不是底层连接点接受的参数数量),并且在给定参数位置传递来继续的值将替换该值绑定到的实体的连接点处的原始值(如果这目前没有意义,请不要担心)。

Spring采用的方法更简单,与其基于代理的、仅限执行的语义更匹配。只有当您编译为Spring编写的@AspectJ方面,并使用带有参数的与AspectJ编译器和weaver一起使用时,您才需要意识到这种差异。有一种方法可以编写在Spring AOP和AspectJ之间100%兼容的方面,这将在下面关于建议参数的一节中讨论。

Around通知返回的值是该方法的调用方看到的返回值。例如,一个简单的缓存方面可以从缓存中返回值(如果它有一个值),或者如果它没有,则调用Continue()(并返回该值)。请注意,Continue可以在Around通知的正文中调用一次、多次或根本不调用。所有这些都是合法的。

If you declare the return type of your around advice method as void, null will always be returned to the caller, effectively ignoring the result of any invocation of proceed(). It is therefore recommended that an around advice method declare a return type of Object. The advice method should typically return the value returned from an invocation of proceed(), even if the underlying method has a void return type. However, the advice may optionally return a cached value, a wrapped value, or some other value depending on the use case.

以下示例显示如何使用环绕建议:

Java
Kotlin
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; } } 
              
              
Advice Parameters

Spring提供了完全类型化的通知,这意味着您可以在通知签名中声明所需的参数(就像我们前面看到的返回和抛出示例一样),而不是一直使用Object[]数组。我们将在本节后面介绍如何使参数和其他上下文值可供建议正文使用。首先,我们来看一下如何编写通用的建议,以了解该建议当前建议的方法。

Access to the Current JoinPoint

任何通知方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数作为其第一个参数。请注意,声明ProceedingJoinPoint类型的第一个参数需要CORLE通知,该参数是JoinPoint的子类。

JoinPoint接口提供了许多有用的方法:

  • getArgs():返回方法参数。

  • getThis():返回代理对象。

  • getTarget():返回目标对象。

  • getSignature():返回被建议的方法的描述。

  • toString():打印所建议的方法的有用描述。

有关更多详细信息,请参阅javadoc

Passing Parameters to Advice

我们已经看到了如何绑定返回值或异常值(在返回和抛出建议之后使用)。要使参数值对通知正文可用,可以使用args的绑定形式。如果在args表达式中使用参数名而不是类型名,则在调用通知时会将相应参数的值作为参数值传递。举个例子应该会让这一点更清楚。假设您希望建议执行以Account对象作为第一个参数的DAO操作,并且您需要访问通知正文中的帐户。您可以编写以下代码:

Java
Kotlin
@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

               
               

切入点表达式的args(count,..)部分有两个用途。首先,它将匹配限制为该方法至少接受一个参数,并且传递给该参数的参数是Account的实例的那些方法执行。其次,它通过Account参数使通知可以使用实际的Account对象。

另一种编写方法是声明一个切入点,该切入点在Account对象值与连接点匹配时“提供”它,然后从通知中引用指定的切入点。这将如下所示:

Java
Kotlin
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

               
               

有关更多详细信息,请参阅AspectJ编程指南。

代理对象(this)、目标对象(目标)和注释(@in@目标@注解@args)都可以以类似的方式进行绑定。接下来的两个示例显示如何匹配使用@Audable注释的方法的执行,并提取审计代码:

两个示例中的第一个显示了@Audable注释的定义:

Java
Kotlin
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

               
               

两个示例中的第二个显示了与@Audable方法的执行相匹配的建议:

Java
Kotlin
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

               
               
Advice Parameters and Generics

Spring AOP可以处理类声明和方法参数中使用的泛型。假设您有如下所示的泛型类型:

Java
Kotlin
public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

               
               

通过将通知参数绑定到要截取方法的参数类型,可以将方法类型的截取限制为某些参数类型:

Java
Kotlin
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

               
               

此方法不适用于泛型集合。因此,您不能按如下方式定义切入点:

Java
Kotlin
@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

               
               

要做到这一点,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何处理一般的NULL值。要实现类似的操作,您必须在Collection<;?>;中输入参数,并手动检查元素的类型。

Determining Argument Names

通知调用中的参数绑定依赖于将切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称进行匹配。参数名称不能通过Java反射获得,因此Spring AOP使用以下策略来确定参数名称:

  • 如果用户已显式指定参数名称,则使用指定的参数名称。通知和切入点注释都有一个可选的argNames属性,您可以使用该属性指定带注释的方法的参数名称。这些参数名称在运行时可用。下面的示例显示如何使用argNames属性:

Java
Kotlin
@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
}

               
               

如果第一个参数是JoinPointProceedingJoinPointJoinPoint.StaticPart类型,则可以从argNames属性的值中省略参数的名称。例如,如果您修改前面的建议以接收连接点对象,则argNames属性不需要包括它:

Java
Kotlin
@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
}

               
               

JoinPointProceedingJoinPointJoinPoint.StaticPart类型的第一个参数进行的特殊处理对于不收集任何其他连接点上下文的通知实例特别方便。在这种情况下,您可以省略argNames属性。例如,以下建议不需要声明argNames属性:

Java
Kotlin
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

               
               
  • 使用argNames属性有点笨拙,所以如果没有指定argNames属性,Spring AOP会查看类的调试信息,并尝试从本地变量表中确定参数名称。只要类已经使用调试信息(至少是-g:vars)进行了编译,该信息就会存在。启用此标志进行编译的结果是:(1)代码更容易理解(反向工程),(2)类文件大小略大(通常无关紧要),(3)编译器不会应用删除未使用的局部变量的优化。换句话说,使用此标志进行构建应该不会遇到任何困难。

    If an @AspectJ aspect has been compiled by the AspectJ compiler (ajc) even without the debug information, you need not add the argNames attribute, as the compiler retain the needed information.
  • 如果代码是在没有必要的调试信息的情况下编译的,则Spring AOP会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中只绑定了一个变量,并且通知方法只接受一个参数,那么这种配对是显而易见的)。如果在给定可用信息的情况下变量绑定不明确,则抛出AmbiguousBindingException异常。

  • 如果以上所有策略都失败,则抛出IllegalArgumentException异常。

Proceeding with Arguments

我们在前面提到过,我们将描述如何使用在Spring、AOP和AspectJ中一致工作的参数编写Progress调用。解决方案是确保建议签名按顺序绑定每个方法参数。以下示例显示了如何执行此操作:

Java
Kotlin
@Around("execution(List<Account> 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});
}

               
               

在许多情况下,无论如何都要执行此绑定(如前面的示例所示)。

Advice Ordering

如果多条建议都希望在同一连接点上运行,会发生什么情况?Spring AOP遵循与AspectJ相同的优先规则来确定建议执行的顺序。优先级最高的建议排在“在进入的路上”(因此,给出两条之前的建议,优先级最高的建议排在第一位)。从连接点“在出去的路上”,优先级最高的建议最后运行(因此,给出两个After建议,优先级最高的建议将排在第二位)。

当在不同方面中定义的两条通知都需要在同一连接点上运行时,除非您另行指定,否则执行顺序是未定义的。您可以通过指定优先级来控制执行顺序。这是通过在方面类中实现org.springFrawork.core.Orded接口或使用@Order注释来以普通的Spring方式完成的。给定两个方面,从Ordered.getOrder()(或注释值)返回较低值的方面具有较高的优先级。

特定方面的每个不同的建议类型在概念上都意味着直接应用于连接点。因此,@AfterThrowing通知方法不应该从附带的@After/@AfterReturning方法接收异常。

从Spring Framework5.2.7开始,在同一个@Aspect类中定义的、需要在同一连接点运行的通知方法会根据其通知类型从高到低按以下顺序分配优先级:@Aspect@Aspect@AfterReturning@AfterThrowing。然而,请注意,在同一方面中的任何@AfterReturning@AfterThrowing建议方法之后,将有效地调用@AfterAfter建议方法,这遵循了AspectJ对@After的“After Finally Advisment”语义。

当同一个@Aspect类中定义的两个相同类型的通知(例如,两个@After通知方法)都需要在同一连接点上运行时,顺序是未定义的(因为没有办法通过反射来检索经过javac编译的类的源代码声明顺序)。考虑在每个@Aspect类中的每个连接点将这样的通知方法折叠为一个通知方法,或者将通知片段重构为单独的@Aspect类,您可以通过ordered@Order在方面级别对这些类进行排序。

5.4.5. Introductions

引入(在AspectJ中称为类型间声明)使方面能够声明建议的对象实现了给定的接口,并代表这些对象提供了该接口的实现。

您可以使用@DeclareParents注释进行介绍。该注释用于声明匹配的类型有一个新的父级(因此得名)。例如,给定名为UsageTracked的接口和名为DefaultUsageTracked的接口的实现,以下方面声明服务接口的所有实现者也实现UsageTracked接口(例如,通过JMX进行统计):

Java
Kotlin
@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();
    }

}

             
             

要实现的接口由带注释的字段的类型确定。@DeclareParents注释的属性是AspectJ类型模式。匹配类型的任何Bean都实现UsageTracked接口。请注意,在前面示例的前面建议中,服务Bean可以直接用作UsageTracked接口的实现。如果以编程方式访问Bean,您将编写以下代码:

Java
Kotlin
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

             
             

5.4.6. Aspect Instantiation Models

This is an advanced topic. If you are just starting out with AOP, you can safely skip it until later.

默认情况下,应用程序上下文中的每个方面都有一个实例。AspectJ将其称为单例实例化模型。可以定义具有不同生命周期的方面。Spring支持AspectJ的perthisperTarget实例化模型;目前不支持perflowperflowpertypein

您可以通过在@方面注释中指定perthis子句来声明perthis方面。请考虑以下示例:

Java
Kotlin
@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}

             
             

在前面的示例中,perthis子句的作用是为执行业务服务的每个唯一服务对象(每个唯一对象绑定到与切入点表达式匹配的连接点处的this)创建一个方面实例。方面实例是在第一次调用服务对象上的方法时创建的。当服务对象超出范围时,方面就会超出范围。在创建方面实例之前,其中的任何建议都不会运行。一旦创建了方面实例,在其中声明的通知就会在匹配的连接点上运行,但前提是服务对象是与此方面相关联的对象。有关per子句的更多信息,请参阅《AspectJ编程指南》。

perTarget实例化模型的工作方式与perthis完全相同,但它在匹配的连接点处为每个唯一的目标对象创建一个方面实例。

5.4.7. An AOP Example

现在您已经了解了所有组成部分是如何工作的,我们可以将它们组合在一起来做一些有用的事情。

业务服务的执行有时会因为并发问题而失败(例如,死锁失败者)。如果重试该操作,则很可能在下一次尝试时成功。对于适合在这种情况下重试的业务服务(不需要返回用户进行冲突解决的幂等操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException.这是一个明显跨越服务层中的多个服务的需求,因此非常适合通过方面实现。

因为我们要重试该操作,所以需要使用环绕建议,以便可以多次调用Continue。下面的清单显示了基本方面实现:

Java
Kotlin
@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;
    }
}

             
             

请注意,方面实现了有序的接口,因此我们可以将方面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。MaxRetriesorder属性都是由Spring配置的。主要操作发生在doConCurentOperation周围的建议中。请注意,目前,我们将重试逻辑应用于每个Commercial Service()。我们尝试继续,如果PessimisticLockingFailureException,失败,我们将再次尝试,除非我们已用尽所有重试尝试。

对应的Spring配置如下:

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>
             
             

要细化方面,使其仅重试幂等操作,我们可以定义以下幂等注释:

Java
Kotlin
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

             
             

然后,我们可以使用该注释来注释服务操作的实现。将方面更改为仅重试幂等操作涉及细化切入点表达式,以便只有@等幂操作匹配,如下所示:

Java
Kotlin
@Around("com.xyz.myapp.CommonPointcuts.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

             
             

5.5. Schema-based AOP Support

如果您更喜欢基于XML的格式,那么Spring还支持使用AOP命名空间标记来定义方面。支持与使用@AspectJ样式时完全相同的切入点表达式和建议类型。因此,在本节中,我们将重点介绍语法,并让读者参考上一节(@AspectJ Support)中讨论,以了解如何编写切入点表达式和绑定通知参数。

要使用本节中描述的AOP命名空间标记,您需要导入Spring-aop架构,如基于XML架构的配置中所述。有关如何在AOP命名空间中导入标记的信息,请参阅AOP架构

在您的Spring配置中,所有方面和Advisor元素都必须放在<;aop:config>;元素中(在应用程序上下文配置中可以有多个<;aop:config>;元素)。<;aop:config>;元素可以包含PointCut、Advisor和Aspect元素(请注意,这些元素必须按该顺序声明)。

The <aop:config> style of configuration makes heavy use of Spring’s auto-proxying mechanism. This can cause issues (such as advice not being woven) if you already use explicit auto-proxying through the use of BeanNameAutoProxyCreator or something similar. The recommended usage pattern is to use either only the <aop:config> style or only the AutoProxyCreator style and never mix them.

5.5.1. Declaring an Aspect

当您使用模式支持时,方面是在您的Spring应用程序上下文中定义为Bean的常规Java对象。状态和行为在对象的字段和方法中捕获,切入点和建议信息在XML中捕获。

您可以使用<;aop:方面>;元素声明方面,并使用ref属性引用支持Bean,如下面的示例所示:

<aop:config>
    <aop:aspect id="myAspect" ref="aBean">
        ...
    </aop:aspect>
</aop:config>

<bean id="aBean" class="...">
    ...
</bean>
             
             

当然,支持方面的Bean(本例中为aBean)可以像其他任何Spring Bean一样进行配置和注入依赖项。

5.5.2. Declaring a Pointcut

您可以在<;aop:config>;元素中声明一个命名的切入点,让切入点定义在几个方面和顾问之间共享。

表示服务层中任何业务服务的执行的切入点可以定义如下:

<aop:config>

    <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/>

</aop:config>
             
             

注意,切入点表达式本身使用的是@AspectJ Support中描述的相同的AspectJ切入点表达式语言。如果使用基于模式的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方式如下:

<aop:config>

    <aop:pointcut id="businessService" expression="com.xyz.myapp.CommonPointcuts.businessService()"/>

</aop:config>
             
             

假设您有一个CommonPointCuts方面,如共享公共切入点定义中所述。

然后,在方面内声明切入点与声明顶级切入点非常相似,如下面的示例所示:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        ...
    </aop:aspect>

</aop:config>
             
             

与@AspectJ方面非常类似,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,下面的切入点收集This对象作为连接点上下文,并将其传递给通知:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..)) &amp;&amp; this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>

</aop:config>
             
             

必须声明通知才能通过包括匹配名称的参数来接收收集的连接点上下文,如下所示:

Java
Kotlin
public void monitor(Object service) {
    // ...
}

             
             

在组合切入点的子表达式时,&;amp;&;amp;在XML文档中很不方便,因此可以分别使用andnot关键字来代替&;amp;&;amp;||。例如,前面的切入点可以更好地编写为:

<aop:config>

    <aop:aspect id="myAspect" ref="aBean">

        <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..)) and this(service)"/>

        <aop:before pointcut-ref="businessService" method="monitor"/>

        ...
    </aop:aspect>
</aop:config>
             
             

请注意,以这种方式定义的切入点由它们的XMLid引用,不能用作命名切入点来形成复合切入点。因此,基于模式的定义样式中的命名切入点支持比@AspectJ样式提供的更有限。

5.5.3. Declaring Advice

基于模式的AOP支持使用与@AspectJ样式相同的五种通知,并且它们具有完全相同的语义。

Before Advice

在匹配的方法执行之前运行通知。它是在<;aop:方面>;内使用<;aop:Being>;元素声明的,如下面的示例所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before pointcut-ref="dataAccessOperation" method="doAccessCheck"/>

    ...

</aop:aspect>
              
              

这里,dataAccessOperation是在顶层(<;aop:config>;)级别定义的切入点的id。要定义内联切入点,请使用切入点属性替换point-ref属性,如下所示:

<aop:aspect id="beforeExample" ref="aBean">

    <aop:before pointcut="execution(* com.xyz.myapp.dao.*.*(..))" method="doAccessCheck"/>

    ...
</aop:aspect>
              
              

正如我们在讨论@AspectJ样式时注意到的,使用命名切入点可以显著提高代码的可读性。

方法属性标识提供通知正文的方法(doAccessCheck)。必须为包含通知的方面元素引用的Bean定义此方法。在执行数据访问操作(与切入点表达式匹配的方法执行连接点)之前,将调用方面Bean上的doAccessCheck方法。

After Returning Advice

返回建议后,当匹配的方法执行正常完成时运行。它在<;aop:方面>;中声明,其方式与前面的通知相同。下面的示例显示如何声明它:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning pointcut-ref="dataAccessOperation" method="doAccessCheck"/>

    ...
</aop:aspect>
              
              

与@AspectJ样式一样,您可以在通知正文中获取返回值。为此,请使用返回属性指定应向其传递返回值的参数的名称,如下面的示例所示:

<aop:aspect id="afterReturningExample" ref="aBean">

    <aop:after-returning pointcut-ref="dataAccessOperation" returning="retVal" method="doAccessCheck"/>

    ...
</aop:aspect>
              
              

doAccessCheck方法必须声明名为retVal的参数。此参数的类型约束匹配的方式与@AfterReturning所述的方式相同。例如,您可以按如下方式声明方法签名:

Java
Kotlin
public void doAccessCheck(Object retVal) {...

              
              
After Throwing Advice

抛出建议后,当匹配的方法执行通过抛出异常退出时运行。它是使用抛出后的元素在<;aop:方面>;中声明的,如下面的示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing pointcut-ref="dataAccessOperation" method="doRecoveryActions"/>

    ...
</aop:aspect>
              
              

与@AspectJ样式一样,您可以在通知正文中获得抛出的异常。为此,请使用抛出属性指定应向其传递异常的参数的名称,如下面的示例所示:

<aop:aspect id="afterThrowingExample" ref="aBean">

    <aop:after-throwing pointcut-ref="dataAccessOperation" throwing="dataAccessEx" method="doRecoveryActions"/>

    ...
</aop:aspect>
              
              

doRecoveryActions方法必须声明名为dataAccessEx的参数。此参数的类型约束匹配的方式与@AfterThrowing描述的方式相同。例如,方法签名可以声明如下:

Java
Kotlin
public void doRecoveryActions(DataAccessException dataAccessEx) {...

              
              
After (Finally) Advice

在(最后)之后,无论匹配的方法执行如何退出,通知都会运行。您可以使用元素来声明它,如下面的示例所示:

<aop:aspect id="afterFinallyExample" ref="aBean">

    <aop:after pointcut-ref="dataAccessOperation" method="doReleaseLock"/>

    ...
</aop:aspect>
              
              
Around Advice

最后一种建议是围绕建议。环游建议“绕过”匹配方法的执行。它有机会在方法运行之前和之后做工作,并确定方法何时、如何运行,甚至是否真正开始运行。如果您需要以线程安全的方式共享方法执行前后的状态,例如,启动和停止计时器,则通常使用环回建议。

始终使用符合您要求的最不强大的建议形式。

例如,如果之前建议足以满足您的需要,请不要使用环绕建议。

您可以通过使用aop:our元素来声明建议。通知方法应将对象声明为其返回类型,并且该方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法的主体内,您必须在ProceedingJoinPoint上调用Continue(),才能运行底层方法。调用不带参数的Continue()将导致调用方的原始参数在被调用时被提供给底层方法。对于高级用例,有一个重载的Continue()方法,它接受参数数组(Object[])。在调用底层方法时,数组中的值将用作该方法的参数。有关使用对象[]调用继续的说明,请参阅周围的建议。

下面的示例显示如何在XML中声明通知:

<aop:aspect id="aroundExample" ref="aBean">

    <aop:around pointcut-ref="businessService" method="doBasicProfiling"/>

    ...
</aop:aspect>
              
              

doBasicProsing通知的实现可以与@AspectJ示例中的完全相同(当然,没有注释),如下面的示例所示:

Java
Kotlin
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
    // start stopwatch
    Object retVal = pjp.proceed();
    // stop stopwatch
    return retVal;
}

              
              
Advice Parameters

基于模式的声明样式通过将切入点参数按名称与通知方法参数进行匹配,以与@AspectJ支持 - 相同的方式支持完全类型化的通知。请参阅建议参数了解详细信息。如果您希望显式地为通知方法指定参数名(不依赖于前面描述的检测策略),则可以通过使用通知元素的arg-name属性来实现,该属性的处理方式与通知注释中的argNames属性相同(如确定参数名称中所述)。以下示例显示如何在XML中指定参数名称:

<aop:before pointcut="com.xyz.lib.Pointcuts.anyPublicMethod() and @annotation(auditable)" method="audit" arg-names="auditable"/>
              
              

arg-name属性接受逗号分隔的参数名列表。

下面这个稍微复杂一些的基于XSD的方法示例显示了一些与一些强类型参数结合使用的建议:

Java
Kotlin
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);
    }
}

              
              

接下来是这方面的问题。请注意,Profile(..)方法接受许多强类型参数,其中第一个恰好是用于继续方法调用的连接点。此参数的存在表明配置文件(..)将用作周围的建议,如下面的示例所示:

Java
Kotlin
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()); } } } 
              
              

最后,下面的示例XML配置会影响针对特定连接点的上述建议的执行:

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- this is the object that will be proxied by Spring's AOP infrastructure -->
    <bean id="personService" class="x.y.service.DefaultPersonService"/>

    <!-- this is the actual advice itself -->
    <bean id="profiler" class="x.y.SimpleProfiler"/>

    <aop:config>
        <aop:aspect ref="profiler">

            <aop:pointcut id="theExecutionOfSomePersonServiceMethod" expression="execution(* x.y.service.PersonService.getPerson(String,int)) and args(name, age)"/>

            <aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod" method="profile"/>

        </aop:aspect>
    </aop:config>

</beans>
              
              

请考虑以下驱动程序脚本:

Java
Kotlin
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); } } 
              
              

使用这样的Boot类,我们将在标准输出上获得类似于以下内容的输出:

StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0
-----------------------------------------
ms     %     Task name
-----------------------------------------
00000  ?  execution(getFoo)
Advice Ordering

当多条通知需要在同一连接点(执行方法)上运行时,排序规则如通知排序所述。方面之间的优先级通过<;aop:方面>;元素中的order属性确定,或者通过向支持方面的Bean添加@Order注释,或者通过让Bean实现Order接口来确定。

与在相同的@Aspect类中定义的通知方法的优先级规则不同,当在同一<;aop:Aop:方面>;元素中定义的两个通知都需要在相同的连接点运行时,优先级由封闭的<;aop:方面&>元素中声明的通知元素的顺序决定,从最高优先级到最低优先级。

例如,如果在同一个<;aop:方面>;元素中定义了Aop:方面>;元素中定义的Above建议,为了确保Above建议具有比建议更高的优先级,<;元素必须在<;元素之前声明<;元素。

根据一般经验,如果您发现在同一<;aop:方面&>元素中定义了多个通知,并且这些通知应用于相同的连接点,请考虑将这些通知方法折叠为每个<;aop:方面&>元素中的每个连接点一个通知方法,或者将这些通知重构为可以在方面级别排序的单独的<;aop:方面&>元素。

5.5.4. Introductions

引入(在AspectJ中称为类型间声明)允许方面声明建议的对象实现给定的接口,并代表这些对象提供该接口的实现。

您可以通过在aop:方面中使用aop:ECLARE-PARENTS元素进行介绍。您可以使用aop:ECLARE-PARENTS元素来声明匹配的类型有一个新的父级(由此得名)。例如,给定一个名为UsageTracked的接口和一个名为DefaultUsageTrack的接口的实现,下面的方面声明服务接口的所有实现者也实现UsageTracked接口。(例如,为了通过JMX公开统计数据。)

<aop:aspect id="usageTrackerAspect" ref="usageTracking">

    <aop:declare-parents types-matching="com.xzy.myapp.service.*+" implement-interface="com.xyz.myapp.service.tracking.UsageTracked" default-impl="com.xyz.myapp.service.tracking.DefaultUsageTracked"/>

    <aop:before pointcut="com.xyz.myapp.CommonPointcuts.businessService() and this(usageTracked)" method="recordUsage"/>

</aop:aspect>
             
             

然后,支持usageTrackingBean的类将包含以下方法:

Java
Kotlin
public void recordUsage(UsageTracked usageTracked) {
    usageTracked.incrementUseCount();
}

             
             

要实现的接口由IMPLEMENT-INFACE属性确定。类型匹配属性的值是一个AspectJ类型模式。匹配类型的任何Bean都实现UsageTracked接口。请注意,在前面示例的前面建议中,服务Bean可以直接用作UsageTracked接口的实现。要以编程方式访问Bean,您可以编写以下代码:

Java
Kotlin
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

             
             

5.5.5. Aspect Instantiation Models

模式定义的方面唯一支持的实例化模型是单例模型。在未来的版本中可能会支持其他实例化模型。

5.5.6. Advisors

“顾问”的概念来自于Spring中定义的AOP支持,在AspectJ中没有直接的等价物。顾问就像一个小而独立的方面,只有一条建议。通知本身由一个Bean表示,并且必须实现在Spring中的通知类型中描述的通知接口之一。顾问可以利用AspectJ切入点表达式。

Spring通过<;aop:Advisor>;元素支持Advisor概念。您最常看到的是它与事务性建议结合使用,后者在Spring中也有自己的命名空间支持。以下示例显示了一位顾问:

<aop:config>

    <aop:pointcut id="businessService" expression="execution(* com.xyz.myapp.service.*.*(..))"/>

    <aop:advisor pointcut-ref="businessService" advice-ref="tx-advice"/>

</aop:config>

<tx:advice id="tx-advice">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes>
</tx:advice>
             
             

除了前面示例中使用的PointCut-ref属性,您还可以使用PointCut属性内联定义一个切入点表达式。

若要定义指导的优先级,以便建议可以参与排序,请使用Order属性定义指导的ordered值。

5.5.7. An AOP Schema Example

本节展示AOP示例中的并发锁定失败重试示例在使用模式支持重写时的外观。

业务服务的执行有时会因为并发问题而失败(例如,死锁失败者)。如果重试该操作,则很可能在下一次尝试时成功。对于适合在这种情况下重试的业务服务(不需要返回用户进行冲突解决的幂等操作),我们希望透明地重试操作,以避免客户端看到PessimisticLockingFailureException.这是一个明显跨越服务层中的多个服务的需求,因此非常适合通过方面实现。

因为我们要重试该操作,所以需要使用环绕建议,以便可以多次调用Continue。下面的清单显示了基本方面实现(这是一个使用模式支持的常规Java类):

Java
Kotlin
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;
    }
}

             
             

请注意,方面实现了有序的接口,因此我们可以将方面的优先级设置为高于事务通知(我们希望每次重试时都有一个新的事务)。MaxRetriesorder属性都是由Spring配置的。主要操作发生在doConCurentOperationAnother通知方法中。我们试着继续下去。如果PessimisticLockingFailureException,失败,我们将再次尝试,除非我们已用尽所有重试尝试。

This class is identical to the one used in the @AspectJ example, but with the annotations removed.

对应的Spring配置如下:

<aop:config>

    <aop:aspect id="concurrentOperationRetry" ref="concurrentOperationExecutor">

        <aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..))"/>

        <aop:around pointcut-ref="idempotentOperation" method="doConcurrentOperation"/>

    </aop:aspect>

</aop:config>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
        <property name="maxRetries" value="3"/>
        <property name="order" value="100"/>
</bean>
             
             

请注意,目前,我们假设所有业务服务都是幂等的。如果不是这样,我们可以通过引入幂等注释并使用该注释来注释服务操作的实现,来改进方面,使其仅重试真正的幂等操作,如下面的示例所示:

Java
Kotlin
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

             
             

将方面更改为仅重试幂等操作涉及细化切入点表达式,以便只有@等幂操作匹配,如下所示:

<aop:pointcut id="idempotentOperation" expression="execution(* com.xyz.myapp.service.*.*(..)) and @annotation(com.xyz.myapp.service.Idempotent)"/>
             
             

5.6. Choosing which AOP Declaration Style to Use

一旦您确定方面是实现给定需求的最佳方法,您将如何决定是使用Spring AOP还是AspectJ,还是使用方面语言(代码)样式、@AspectJ注释样式或Spring XML样式?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对AOP的熟悉程度。

5.6.1. Spring AOP or Full AspectJ?

使用能起作用的最简单的东西。Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/weaver。如果您只需要建议如何执行SpringBean上的操作,则Spring AOP是正确的选择。如果需要建议不受Spring容器管理的对象(通常是域对象),则需要使用AspectJ。如果您希望通知除简单方法执行之外的连接点(例如,字段GET或设置连接点等),则还需要使用AspectJ。

当您使用AspectJ时,您可以选择AspectJ语言语法(也称为“代码样式”)或@AspectJ注释样式。显然,如果您不使用Java 5+,那么已经为您做出了选择:使用代码样式。如果方面在您的设计中扮演了重要角色,并且您能够使用AspectJ Development Tools(AJDT)插件,那么AspectJ语言语法是首选。它更干净、更简单,因为这种语言是专门为编写方面而设计的。如果您不使用Eclipse,或者只有几个方面没有在您的应用程序中扮演主要角色,那么您可能需要考虑使用@AspectJ样式,在您的IDE中坚持使用常规的Java编译,并向您的构建脚本添加一个方面编织阶段。

5.6.2. @AspectJ or XML for Spring AOP?

如果您选择使用Spring AOP,则可以选择@AspectJ或XML样式。需要考虑各种权衡。

现有的Spring用户可能对XML样式最为熟悉,而且它得到了真正的POJO的支持。当使用AOP作为配置企业服务的工具时,XML可能是一个很好的选择(一个很好的测试是您是否认为切入点表达式是您可能想要独立更改的配置的一部分)。使用XML样式,可以从您的配置中更清楚地了解系统中存在哪些方面。

XML样式有两个缺点。首先,它没有将它所处理的需求的实现完全封装在一个地方。干式原则说,一个系统内的任何知识都应该有一个单一的、明确的、权威的表示。当使用XML样式时,关于如何实现需求的知识在支持Bean类的声明和配置文件中的XML之间拆分。当您使用@AspectJ样式时,该信息被封装在单个模块中:方面。其次,与@AspectJ样式相比,XML样式在其能够表达的内容方面略有限制:只支持“Singleton”方面实例化模型,并且不可能组合在XML中声明的命名切入点。例如,在@AspectJ样式中,您可以编写如下内容:

Java
Kotlin
@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

             
             

在XML样式中,您可以声明前两个切入点:

<aop:pointcut id="propertyAccess" expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount" expression="execution(org.xyz.Account+ *(..))"/>
             
             

XML方法的缺点是您不能通过组合这些定义来定义acktPropertyAccess切入点。

@AspectJ样式支持额外的实例化模型和更丰富的切入点组合。它的优点是将方面保持为一个模块化单元。它还有一个优点,即Spring AOP和AspectJ都可以理解(并因此使用)@AspectJ方面。因此,如果您后来决定需要AspectJ的功能来实现其他需求,您可以很容易地迁移到传统的AspectJ设置。总的来说,相比简单的企业服务配置,Spring团队更喜欢@AspectJ风格的定制方面。

5.7. Mixing Aspect Types

通过使用自动代理支持、模式定义的<;aop:方面>;方面、<;aop:Advisor>;声明的顾问,甚至在同一配置中的其他样式中的代理和拦截器,混合@AspectJ风格的方面是完全可能的。所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。

5.8. Proxying Mechanisms

Spring AOP使用JDK动态代理或CGLIB为给定的目标对象创建代理。JDK动态代理内置于JDK中,而CGLIB是一个通用的开放源码类定义库(重新打包成Spring-core)。

如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。由目标类型实现的所有接口都被代理。如果目标对象没有实现任何接口,则会创建一个CGLIB代理。

如果您想强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅仅是由其接口实现的方法),您可以这样做。但是,您应该考虑以下问题:

  • 使用CGLIB时,不能建议最终方法,因为它们不能在运行时生成的子类中被重写。

  • 从Spring4.0开始,代理对象的构造函数不再被调用两次,因为CGLIB代理实例是通过Objenesis创建的。只有当您的JVM不允许绕过构造函数时,您才可能看到来自Spring的AOP支持的双重调用和相应的调试日志条目。

要强制使用CGLIB代理,请将<;aop:config>;元素的代理-目标-类属性的值设置为True,如下所示:

<aop:config proxy-target-class="true">
    <!-- other beans defined here... -->
</aop:config>
            
            

要在使用@AspectJ自动代理支持时强制CGLIB代理,请将<;aop:AspectJ-AutoProxy>;元素的Proxy-Target-class属性设置为true,如下所示:

<aop:aspectj-autoproxy proxy-target-class="true"/>
            
            

多个<;aop:config/&>节在运行时折叠成一个统一的自动代理创建器,它应用任何<;aop:config/&>节(通常来自不同的XML Bean定义文件)指定的最强的代理设置。这也适用于<;tx:Annotation-Driven/>;<;aop:AspectJ-AutoProxy/>;元素。

需要明确的是,在<;tx:Annotation-Driven/>;<;aop:AspectJ-AutoProxy/>;<;aop:config/>;元素上使用xy-Target-class=“true”元素会强制对这三个元素使用CGLIB代理。

5.8.1. Understanding AOP Proxies

Spring AOP是基于代理的。在编写自己的方面或使用Spring框架提供的任何基于Spring AOP的方面之前,掌握最后一条语句的实际含义是至关重要的。

首先考虑这样一个场景,其中您有一个普通的、未代理的、没有什么特别的、直接的对象引用,如以下代码片断所示:

Java
Kotlin
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...
    }
}

             
             

如果在对象引用上调用方法,则会直接在该对象引用上调用该方法,如下图和清单所示:

aop proxy plain pojo call
Java
Kotlin
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();
    }
}

             
             

当客户端代码拥有的引用是代理时,情况略有变化。请考虑以下图表和代码片段:

aop proxy call
Java
Kotlin
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();
    }
}

             
             

这里要理解的关键是main类的main(..)方法中的客户端代码引用了代理。这意味着对该对象引用的方法调用就是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(本例中的SimplePojo引用),它可能对自身进行的任何方法调用,如this.bar()this.foo(),都将针对this引用调用,而不是针对代理调用。这具有重要的影响。这意味着自调用不会导致与方法调用相关联的通知获得运行的机会。

好的,那么我们该怎么做呢?最好的方法(这里松散地使用术语“最佳”)是重构您的代码,使自我调用不会发生。这确实需要您做一些工作,但这是最好的、侵入性最小的方法。下一种方法绝对可怕,我们不愿指出这一点,正是因为它太可怕了。您可以将类中的逻辑完全绑定到Spring AOP,如下面的示例所示:

Java
Kotlin
public class SimplePojo implements Pojo {

    public void foo() {
        // this works, but... gah!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // some logic...
    }
}

             
             

这完全将您的代码耦合到Spring AOP,并且它使类本身意识到它是在AOP上下文中使用的,这与AOP相悖。创建代理时还需要一些额外的配置,如下例所示:

Java
Kotlin
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();
    }
}

             
             

最后,必须注意的是,AspectJ没有这种自调用问题,因为它不是一个基于代理的AOP框架。

5.9. Programmatic Creation of @AspectJ Proxies

除了通过使用<;aop:config>;<;aop:AspectJ-AutoProxy>;在配置中声明方面之外,还可以以编程方式创建通知目标对象的代理。有关Spring的AOP API的完整细节,请参阅下一章。在这里,我们希望关注通过使用@AspectJ方面自动创建代理的能力。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ方面建议的目标对象创建代理。此类的基本用法非常简单,如下面的示例所示:

Java
Kotlin
// 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();

            
            

有关详细信息,请参阅javadoc

5.10. Using AspectJ with Spring Applications

到目前为止,我们在本章中介绍的所有内容都是纯Spring AOP。在这一节中,如果您的需求超出了Spring AOP单独提供的功能,我们将研究如何使用AspectJ编译器或编织器来替代或补充Spring AOP。

Spring附带了一个小的AspectJ方面库,它可以在您的发行版中作为Spring-aspects.jar独立使用。为了使用类路径中的方面,您需要将其添加到类路径中。使用AspectJ依赖注入具有Spring的域对象AspectJ的其他Spring方面讨论这个库的内容以及如何使用它。使用Spring IOC配置AspectJ方面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。最后,在Spring框架中使用AspectJ加载时编织介绍了使用AspectJ的Spring应用程序的加载时编织。

5.10.1. Using AspectJ to Dependency Inject Domain Objects with Spring

Spring容器实例化和配置在您的应用程序上下文中定义的Bean。在给定包含要应用的配置的Bean定义的名称的情况下,也可以请求Bean工厂配置预先存在的对象。Spring-aspects.jar包含一个注释驱动的方面,该方面利用此功能来允许任何对象的依赖项注入。该支持旨在用于在任何容器控制之外创建的对象。域对象通常属于这一类,因为它们通常是使用new操作符以编程方式创建的,或者是由ORM工具作为数据库查询的结果创建的。

@Configable注释将一个类标记为符合Spring驱动的配置条件。在最简单的情况下,您可以将其纯粹用作标记批注,如下例所示:

Java
Kotlin
package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable public class Account { // ... } 
             
             

当以这种方式用作标记接口时,Spring通过使用与完全限定类型名称(com.xyz.myapp.domain.Account)相同名称的Bean定义(通常是原型作用域)来配置带注释的类型(在本例中为Account)的新实例。由于Bean的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略id属性,如下例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype">
    <property name="fundsTransferService" ref="fundsTransferService"/>
</bean>
             
             

如果要显式指定要使用的原型Bean定义的名称,可以直接在注释中执行此操作,如下例所示:

Java
Kotlin
package com.xyz.myapp.domain; import org.springframework.beans.factory.annotation.Configurable; @Configurable("account") public class Account { // ... } 
             
             

Spring现在查找名为Account的Bean定义,并将其用作配置新的Account实例的定义。

您还可以使用自动装配来完全避免指定专用的Bean定义。要让Spring应用自动装配,请使用@Configable注释的auTower属性。您可以分别按类型或名称指定用于自动装配的@Configurable(autowire=Autowire.BY_TYPE)@Configurable(autowire=Autowire.BY_NAME)。作为另一种选择,最好是通过@AuTower@Inject在字段或方法级别为@ConfigableBean指定显式的、注释驱动的依赖项注入(有关更多详细信息,请参阅基于注释的容器配置)。

最后,您可以通过使用<@Configurable(autowire=Autowire.BY_NAME,>DependencyCheck属性(例如,Spring DependencyCheck=TRUE)为新创建和配置的对象中的对象引用启用Spring依赖项检查。如果该属性设置为true,则Spring在配置后验证是否已设置所有属性(不是基元或集合)。

请注意,单独使用注释不会产生任何影响。正是Spring-aspects.jar中的AnnotationBeanConfigurerAspect作用于注释的存在。本质上,方面是这样说的:“在初始化一个用@Configable注释的类型的新对象后,根据注释的属性使用Spring配置新创建的对象”。在此上下文中,“初始化”是指新实例化的对象(例如,使用new操作符实例化的对象)以及正在进行反序列化(例如,通过readResolve())的可序列化对象。

上段中的关键短语之一是“实质上”。对于大多数情况,“从新对象的初始化返回后”的确切语义是可以接受的。在这种情况下,“初始化后”意味着依赖项是在对象构造之后注入的。这意味着依赖项不能在类的构造函数体中使用。如果您希望在构造函数体运行之前注入依赖项,从而可以在构造函数体中使用,则需要在@Configable声明中定义它,如下所示:

Java
Kotlin
@Configurable(preConstruction = true)

                  
                  

您可以在AspectJ编程指南的附录中找到有关AspectJ中各种切入点类型的语言语义的更多信息。

为此,必须使用AspectJ编织器来编织带注释的类型。您可以使用构建时Ant或Maven任务(例如,参见AspectJ开发环境指南)或加载时编织(参见在Spring框架中使用AspectJ加载时编织)。AnnotationBeanConfigurerAspect本身需要由Spring配置(以便获得对将用于配置新对象的Bean工厂的引用)。如果您使用的是基于Java的配置,您可以将@EnableSpringConfiguring添加到任何@Configuration类中,如下所示:

Java
Kotlin
@Configuration
@EnableSpringConfigured
public class AppConfig {
}

             
             

如果您更喜欢基于XML的配置,Spring上下文命名空间定义了一个方便的上下文:由Spring配置的元素,您可以使用如下方法:

<context:spring-configured/>
             
             

在配置方面之前创建的@Configurable对象的实例会导致向调试日志发出一条消息,并且不会发生对象配置。一个例子可能是Spring配置中的一个Bean,它在由Spring初始化时创建域对象。在这种情况下,您可以使用Dependers-onBean属性手动指定该Bean依赖于配置方面。下面的示例显示如何使用Dependers-on属性:

<bean id="myService" class="com.xzy.myapp.service.MyService" depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect">

    <!-- ... -->

</bean>
             
             
Do not activate @Configurable processing through the bean configurer aspect unless you really mean to rely on its semantics at runtime. In particular, make sure that you do not use @Configurable on bean classes that are registered as regular Spring beans with the container. Doing so results in double initialization, once through the container and once through the aspect.
Unit Testing @Configurable Objects

@可配置支持的目标之一是允许对域对象进行独立的单元测试,而不会遇到与硬编码查找相关的困难。如果@可配置类型尚未由AspectJ编织,则注释在单元测试期间不起作用。您可以在测试对象中设置模拟或存根属性引用,然后照常继续。如果AspectJ已经编织了@可配置类型,您仍然可以像往常一样在容器外进行单元测试,但每次构造@可配置对象时,您都会看到一条警告消息,表明它尚未由Spring配置。

Working with Multiple Application Contexts

用于实现@Configable支持的AnnotationBeanConfigurerAspect是一个AspectJ单例方面。单例方面的作用域与静态成员的作用域相同:每个类加载器都有一个定义类型的方面实例。这意味着,如果在同一类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfiguringBean,以及在类路径上的何处放置Spring-aspects.jar

考虑一个典型的Spring Web应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了公共业务服务、支持这些服务所需的一切,以及每个Servlet的一个子应用程序上下文(它包含特定于该Servlet的定义)。所有这些上下文都共存于相同的类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能包含对其中一个上下文的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfiguringBean。这定义了您可能想要注入到域对象中的服务。其结果是,您不能使用@Configable机制(这可能不是您想要做的事情)来配置域对象,该域对象引用在子(特定于Servlet)上下文中定义的Bean。

在同一容器中部署多个Web应用程序时,请确保每个Web应用程序通过使用其自己的类加载器(例如,将Spring-aspects.jar放在WEB-INF/lib中)来加载Spring-aspects.jar中的类型。如果Spring-aspects.jar仅添加到容器范围的类路径中(因此由共享的父类加载器加载),则所有Web应用程序共享相同的方面实例(这可能不是您想要的)。

5.10.2. Other Spring aspects for AspectJ

除了@可配置方面之外,Spring-aspects.jar还包含一个AspectJ方面,您可以使用该方面来驱动使用@Transaction注释的类型和方法的Spring事务管理。这主要面向希望在Spring容器之外使用Spring框架的事务支持的用户。

解释@Transaction注释的方面是AnnotationTransactionAspect。当您使用这个方面时,您必须注释实现类(或类中的方法或两者),而不是类实现的接口(如果有的话)。AspectJ遵循Java规则,即不继承接口上的注释。

类上的@Transaction注释为执行类中的任何公共操作指定默认事务语义。

类中方法上的@Transaction注释覆盖类注释(如果存在)给出的默认事务语义。可以注释任何可见性的方法,包括私有方法。直接注释非公共方法是为此类方法的执行获得事务分界的唯一方法。

Since Spring Framework 4.2, spring-aspects provides a similar aspect that offers the exact same features for the standard jakarta.transaction.Transactional annotation. Check JtaAnnotationTransactionAspect for more details.

对于希望使用Spring配置和事务管理支持但不想(或不能)使用批注的AspectJ程序员,Spring-aspects.jar还包含可以扩展的抽象方面,以提供您自己的切入点定义。有关更多信息,请参阅AbstractBeanConfigurerAspectAbstractTransactionAspect方面的源代码。作为示例,下面的摘录显示了如何编写一个方面,通过使用与完全限定类名匹配的原型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. Configuring AspectJ Aspects by Using Spring IoC

当您在Spring应用程序中使用AspectJ方面时,希望并期望能够使用Spring配置这些方面是很自然的。AspectJ运行时本身负责方面的创建,通过Spring配置AspectJ创建的方面的方法取决于方面使用的AspectJ实例化模型(per-xxx子句)。

大多数AspectJ方面都是单例方面。这些方面的配置很容易。您可以像Normal一样创建引用方面类型的Bean定义,并包括Factory-method=“aspectOf”Bean属性。这确保了Spring通过向AspectJ请求AspectJ来获得方面实例,而不是尝试自己创建实例。下面的示例显示如何使用Factory-method=“aspectOf”属性:

<bean id="profiler" class="com.xyz.profiler.Profiler" factory-method="aspectOf"> (1)

    <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>
             
             
1 Note the factory-method="aspectOf" attribute

非单例方面更难配置。然而,一旦AspectJ运行时创建了Bean,就可以通过创建原型Bean定义并使用来自Spring-aspects.jar@Configable支持来配置方面实例。

如果您有一些@AspectJ方面(例如,对域模型类型使用加载时编织)和其他@AspectJ方面希望与Spring AOP一起使用,并且这些方面都是在Spring中配置的,那么您需要告诉Spring AOP@AspectJ自动代理支持应该使用配置中定义的@AspectJ方面的哪个子集来进行自动代理。您可以通过在<;aop:AspectJ-AutoProxy/>;声明中使用一个或多个<;Include/<;元素来实现这一点。每个<;Include/>;元素都指定了一个名称模式,并且只有名称与至少一个模式匹配的Bean才用于Spring AOP自动代理配置。下面的示例显示如何使用<;Include/>;元素:

<aop:aspectj-autoproxy>
    <aop:include name="thisBean"/>
    <aop:include name="thatBean"/>
</aop:aspectj-autoproxy>
             
             
Do not be misled by the name of the <aop:aspectj-autoproxy/> element. Using it results in the creation of Spring AOP proxies. The @AspectJ style of aspect declaration is being used here, but the AspectJ runtime is not involved.

5.10.4. Load-time Weaving with AspectJ in the Spring Framework

加载时编织(LTW)指的是将AspectJ方面编织到加载到Java虚拟机(JVM)的应用程序类文件中的过程。本节的重点是在Spring框架的特定上下文中配置和使用LTW。本节不是对LTW的一般介绍。有关LTW和仅使用AspectJ配置LTW(根本不涉及Spring)的详细信息,请参阅AspectJ开发环境指南的LTW部分。

Spring框架为AspectJ LTW带来的价值在于能够对编织过程进行更细粒度的控制。‘Vanilla’AspectJ LTW通过使用Java(5+)代理来实现,该代理通过在启动JVM时指定一个VM参数来打开。因此,它是JVM范围的设置,在某些情况下可能很好,但通常有点太粗糙了。启用了Spring的LTW允许您在每个ClassLoader的基础上打开LTW,这更细粒度,在‘单JVM-多应用程序’环境中更有意义(比如在典型的应用程序服务器环境中)。

此外,在某些环境中,此支持支持加载时编织,而无需对添加-javaagent:path/to/aspectjweaver.jar或(如本节后面所述)-javaagent:path/to/spring-instrument.jar.所需的应用程序服务器的启动脚本进行任何修改开发人员配置应用程序上下文以启用加载时编织,而不是依赖通常负责部署配置的管理员,例如启动脚本。

既然推销已经结束,让我们首先快速浏览一个使用Spring的AspectJ LTW示例,然后详细介绍该示例中引入的元素。有关完整的示例,请参阅PetClinic示例应用程序

A First Example

假设您是一名应用程序开发人员,其任务是诊断系统中某些性能问题的原因。我们不是发布性能分析工具,而是打开一个简单的性能分析方面,让我们快速获得一些性能指标。然后,我们可以立即将更细粒度的分析工具应用于该特定区域。

The example presented here uses XML configuration. You can also configure and use @AspectJ with Java configuration. Specifically, you can use the @EnableLoadTimeWeaving annotation as an alternative to <context:load-time-weaver/> (see below for details).

下面的示例显示了分析方面,这并不复杂。它是一个基于时间的分析器,使用@AspectJ样式的方面声明:

Java
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 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(){} } 
              
              

我们还需要创建一个META-INF/aop.xml文件,以通知AspectJ编织器我们希望将ProfilingAspect编织到类中。这个文件约定,即Java类路径上存在一个名为META-INF/aop.xml的文件是标准的AspectJ。下面的示例显示了aop.xml文件:

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>

    <weaver>
        <!-- only weave classes in our application-specific packages -->
        <include within="foo.*"/>
    </weaver>

    <aspects>
        <!-- weave in just this aspect -->
        <aspect name="foo.ProfilingAspect"/>
    </aspects>

</aspectj>
              
              

现在,我们可以转到配置的特定于Spring的部分。我们需要配置一个LoadTimeWeaver(稍后解释)。这个加载时编织器是负责将一个或多个META-INF/aop.xml文件中的方面配置编织到应用程序的类中的基本组件。好的方面是它不需要很多配置(您可以指定更多选项,但这些将在后面详细介绍),如下例所示:

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

    <!-- a service object; we will be profiling its methods -->
    <bean id="entitlementCalculationService" class="foo.StubEntitlementCalculationService"/>

    <!-- this switches on the load-time weaving -->
    <context:load-time-weaver/>
</beans>
              
              

现在所有必需的构件(方面、META-INF/aop.xml文件和Spring配置)都已就绪,我们可以使用main(..)方法创建以下驱动程序类来演示LTW:

Java
Kotlin
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(); } } 
              
              

我们还有最后一件事要做。这一节的介绍确实说过,您可以使用Spring在每个ClassLoader的基础上有选择地打开LTW,这是真的。然而,在本例中,我们使用Java代理(随Spring提供)来打开LTW。我们使用以下命令运行前面所示的Main类:

java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main

-javaagent是一个标志,用于指定和启用代理来检测在JVM上运行的程序。Spring框架附带了这样一个代理,即InstrumentationSavingAgent,它被打包在SpringInstrument.jar中,该代理是作为前一个示例中的-javaagent参数的值提供的。

执行main程序的输出如下例所示。(我已经在culateEntiment()实现中引入了一个Thread..)语句,这样分析器实际上捕获的不是0毫秒(01234毫秒不是AOP引入的开销)。下面的清单显示了我们在运行分析器时获得的输出:

Calculating entitlement

StopWatch 'ProfilingAspect': running time (millis) = 1234
------ ----- ----------------------------
ms     %     Task name
------ ----- ----------------------------
01234  100%  calculateEntitlement

因为这个LTW是通过使用成熟的AspectJ来实现的,所以我们并不局限于建议SpringBean。对main程序的以下细微变化会产生相同的结果:

Java
Kotlin
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(); } } 
              
              

请注意,在前面的程序中,我们如何引导Spring容器,然后完全在Spring的上下文之外创建StubEntitlementCalculationService的新实例。侧写建议仍然牢牢地印在人们的脑海里。

诚然,这个例子过于简单化。然而,Spring中的LTW支持的基础都已经在前面的示例中介绍过了,本节的其余部分将详细解释每一点配置和用法背后的“原因”。

The ProfilingAspect used in this example may be basic, but it is quite useful. It is a nice example of a development-time aspect that developers can use during development and then easily exclude from builds of the application being deployed into UAT or production.
Aspects

您在LTW中使用的方面必须是AspectJ方面。您可以用AspectJ语言本身编写它们,也可以用@AspectJ风格编写方面。那么您的方面就是有效的AspectJ和Spring AOP方面。此外,编译后的方面类需要在类路径上可用。

'META-INF/aop.xml'

AspectJ LTW基础设施通过使用Java类路径上的一个或多个META-INF/aop.xml文件进行配置(直接或更典型地,在JAR文件中)。

AspectJ参考文档的LTW部分详细介绍了该文件的结构和内容。因为aop.xml文件是100%的AspectJ,所以我们不在这里进一步描述它。

Required libraries (JARS)

要使用Spring框架对AspectJ LTW的支持,您至少需要以下库:

  • Spring-aop.jar

  • aspectjwever.jar

如果您使用Spring提供的代理来启用插装,您还需要:

  • 弹性工具.jar

Spring Configuration

SpringLTW支持的关键组件是<org.springframework.instrument.classloading包中的<代码>LoadTimeWeaver 接口,以及Spring发行版附带的大量接口实现。LoadTimeWeaver负责在运行时向ClassLoader java.lang.instrument.ClassFileTransformers>添加一个或多个类,这为所有有趣的应用程序打开了大门,其中之一恰好是方面的LTW。

If you are unfamiliar with the idea of runtime class file transformation, see the javadoc API documentation for the java.lang.instrument package before continuing. While that documentation is not comprehensive, at least you can see the key interfaces and classes (for reference as you read through this section).

为特定的ApplicationContext配置LoadTimeWeaver就像添加一行一样简单。(注意,您几乎肯定需要使用一个< >ApplicationContext 作为您的Spring容器LTW。通常,一个BeanFactory是不够的,因为LTW支持使用BeanFactoryPostProcessors。)

要启用Spring框架的LTW支持,您需要配置一个LoadTimeWeaver,这通常通过使用@EnableLoadTimeWeving注释来完成,如下所示:

Java
Kotlin
@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

              
              

或者,如果您更喜欢基于xml的配置,可以使用<;context:load-time-weaver/>;元素。注意,该元素是在上下文命名空间中定义的。以下示例说明如何使用<;context:load-time-weaver/>;

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

    <context:load-time-weaver/>

</beans>
              
              

前面的配置自动为您定义和注册许多特定于LTW的基础设施Bean,例如LoadTimeWeaverAspectJWeavingEnabler。默认的LoadTimeWeaverDefaultContextLoadTimeWeaver类,它尝试修饰自动检测到的LoadTimeWeaver。“自动检测”的LoadTimeWeaver的确切类型取决于您的运行时环境。下表总结了各种LoadTimeWeaver实现:

Table 13. DefaultContextLoadTimeWeaver LoadTimeWeavers
Runtime Environment LoadTimeWeaver implementation

Apache Tomcat中运行

TomcatLoadTimeWeaver

GlassFish中运行(仅限于EAR部署)

GlassFish LoadTimeWeaver

在Red Hat的JBoss asWildFly中运行

JBossLoadTimeWeaver

在IBM的WebSphere中运行

WebSphereLoadTimeWeaver

在Oracle的WebLogic中运行

WebLogicLoadTimeWeaver

JVM以Spring<代码>工具 ) (<代码>Java SAVING

InstrumentationLoadTimeWeaver

回退,期望底层ClassLoader遵循通用约定(即addTransformer和可选的getThrowawayClassLoader方法)

ReflectiveLoadTimeWeaver

请注意,该表仅列出了使用DefaultContextLoadTimeWeaver时自动检测到的LoadTimeWever。您可以准确指定要使用的LoadTimeWeaver实现。

要使用Java配置指定特定的LoadTimeWeaver,请实现LoadTimeWeavingConfigurer接口并重写getLoadTimeWeaver()方法。下面的示例指定一个ReflectiveLoadTimeWeaver

Java
Kotlin
@Configuration
@EnableLoadTimeWeaving
public class AppConfig implements LoadTimeWeavingConfigurer {

    @Override
    public LoadTimeWeaver getLoadTimeWeaver() {
        return new ReflectiveLoadTimeWeaver();
    }
}

              
              

如果使用基于XML的配置,则可以将完全限定的类名指定为<;context:load-time-weaver/>;元素的weaver-class属性的值。同样,下面的示例指定一个ReflectiveLoadTimeWeaver

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

    <context:load-time-weaver weaver-class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/>

</beans>
              
              

由配置定义和注册的LoadTimeWeaver稍后可以使用众所周知的名称loadTimeWeaver从Spring容器中检索。请记住,LoadTimeWeaver仅作为Spring的LTW基础结构添加一个或多个ClassFileTransformers的机制而存在。执行LTW的实际ClassFileTransformerClassPreProcessorAgentAdapter(来自org.aspectj.wever.loadtime包)类。有关更多详细信息,请参见ClassPreProcessorAgentAdapter类的类级javadoc,因为实际如何实现编织的细节超出了本文的范围。

该配置还有最后一个属性需要讨论:aspectjWeving属性(如果您使用的是XML,则AspectJ-weving)。此属性控制是否启用LTW。它接受三个可能的值之一,如果该属性不存在,则默认为自动检测。下表总结了三个可能的值:

Table 14. AspectJ weaving attribute values
Annotation Value XML Value Explanation

已启用

AspectJ编织处于启用状态,并在加载时根据需要编织方面。

已禁用

关闭

LTW已关闭。在加载时不编织任何方面。

自动检测

自动检测

如果Spring LTW基础设施至少可以找到一个META-INF/aop.xml文件,那么AspectJ编织就开启了。否则,它将关闭。这是默认值。

Environment-specific Configuration

最后一节包含在应用程序服务器和Web容器等环境中使用Spring的LTW支持时需要的任何其他设置和配置。

Tomcat, JBoss, WebSphere, WebLogic

Tomcat、JBoss/WildFly、IBM WebSphere Application Server和Oracle WebLogic Server都提供了能够进行本地检测的通用应用程序ClassLoader。Spring的原生LTW可以利用这些ClassLoader实现来提供AspectJ编织。您可以简单地启用加载时编织,如前面所述。具体地说,您不需要修改JVM启动脚本来添加-javaagent:path/to/spring-instrument.jar.

请注意,在JBoss上,您可能需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一种快速的解决方法是将名为WEB-INF/Joss-scanning.xml的文件添加到您的构件中,该文件包含以下内容:

<scanning xmlns="urn:jboss:scanning:1.0"/>
               
               
Generic Java Applications

当在特定的LoadTimeWeaver实现不支持的环境中需要类检测时,通常使用JVM代理。对于这种情况,Spring提供了InstrumentationLoadTimeWeaver,它需要一个特定于Spring的(但非常通用的)JVM代理,SpringInstrument.jar,由公共的@EnableLoadTimeWeving<;context:load-time-weaver/>;设置自动检测。

要使用它,您必须通过提供以下JVM选项来启动带有Spring代理的虚拟机:

-javaagent:/path/to/spring-instrument.jar

请注意,这需要修改JVM启动脚本,这可能会阻止您在应用程序服务器环境中使用它(取决于您的服务器和您的操作策略)。也就是说,对于每个JVM一个应用程序的部署,比如独立的Spring Boot应用程序,您通常在任何情况下都可以控制整个JVM设置。

5.11. Further Resources

有关AspectJ的更多信息可以在AspectJ网站上找到。

月食AspectJ禤浩焯·科尔等人。艾尔(Addison-Wesley,2005)提供了对AspectJ语言的全面介绍和参考。

AspectJ in Action,Ramnivas Laddad(Manning,2009)第二版强烈推荐。本书的重点是AspectJ,但探索了许多一般的AOP主题(有一定深度)。

6. Spring AOP APIs

上一章使用@AspectJ和基于模式的方面定义描述了Spring对AOP的支持。在本章中,我们将讨论较低级别的Spring AOP API。对于常见的应用程序,我们建议将Spring AOP与AspectJ切入点结合使用,如上一章所述。

6.1. Pointcut API in Spring

本节描述了Spring如何处理关键的切入点概念。

6.1.1. Concepts

Spring的切入点模型支持独立于通知类型的切入点重用。您可以使用相同的切入点来针对不同的建议。

org.springFrawork.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(方法,类)方法用于测试此切入点是否与目标类上的给定方法匹配。这种评估可以在创建AOP代理时执行,以避免在每次方法调用时都需要测试。对于给定的方法,如果两参数匹配方法返回true,而方法匹配器的isRuntime()方法返回true,则在每次方法调用时都会调用三参数匹配方法。这允许切入点在目标通知开始之前立即查看传递给方法调用的参数。

大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()方法返回False。在本例中,永远不会调用带有三个参数的matches方法。

If possible, try to make pointcuts static, allowing the AOP framework to cache the results of pointcut evaluation when an AOP proxy is created.

6.1.2. Operations on Pointcuts

Spring支持切入点上的操作(特别是并集和交集)。

联合意味着两个切入点都匹配的方法。交集意味着两个切入点匹配的方法。联合通常更有用。您可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法或使用同一包中的ComposablePointCut类来组合切入点。然而,使用AspectJ切入点表达式通常是一种更简单的方法。

6.1.3. AspectJ Expression Pointcuts

从2.0开始,Spring使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut.这是一个切入点,它使用AspectJ提供的库来解析AspectJ切入点表达式字符串。

有关支持的AspectJ切入点原语的讨论,请参阅上一章

6.1.4. Convenience Pointcut Implementations

Spring提供了几个方便的切入点实现。您可以直接使用它们中的一些;其他的则打算在特定于应用程序的切入点中子类化。

Static Pointcuts

静态切入点基于方法和目标类,不能考虑方法的参数。静态切入点满足 - ,并且对于大多数用法来说是最好的 - 。在第一次调用方法时,Spring只能对静态切入点求值一次。在此之后,不需要在每次方法调用时再次计算切入点。

本节的其余部分描述了Spring中包含的一些静态切入点实现。

Regular Expression Pointcuts

指定静态切入点的一种明显方式是正则表达式。除了Spring之外,还有几个AOP框架可以实现这一点。org.springframework.aop.support.JdkRegexpMethodPointcut是一个通用的正则表达式切入点,它使用JDK中的正则表达式支持。

使用JdkRegexpMethodPointCut类,您可以提供模式字符串列表。如果其中任何一个匹配,则切入点的计算结果为True。(因此,得到的切入点实际上是指定模式的并集。)

下面的示例显示如何使用JdkRegexpMethodPointCut

<bean id="settersAndAbsquatulatePointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>
               
               

Spring提供了一个名为RegexpMethodPointutAdvisor的便利类,它还允许我们引用通知(请记住,通知可以是一个拦截器,在通知、抛出通知和其他通知之前)。在幕后,Spring使用JdkRegexpMethodPointCut。使用RegexpMethodPointutAdvisor可以简化连接,因为一个Bean同时封装了切入点和通知,如下面的示例所示:

<bean id="settersAndAbsquatulateAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
        <ref bean="beanNameOfAopAllianceInterceptor"/>
    </property>
    <property name="patterns">
        <list>
            <value>.*set.*</value>
            <value>.*absquatulate</value>
        </list>
    </property>
</bean>
               
               

您可以对任何建议类型使用RegexpMethodPointutAdvisor

Attribute-driven Pointcuts

静态切入点的一种重要类型是元数据驱动的切入点。这使用元数据属性的值(通常是源级元数据)。

Dynamic pointcuts

动态切入点的计算成本比静态切入点更高。它们考虑了方法参数和静态信息。这意味着它们必须在每次方法调用时求值,并且不能缓存结果,因为参数会有所不同。

主要的例子是控制流切入点。

Control Flow Pointcuts

Spring控制流切入点在概念上类似于AspectJcflow切入点,尽管功能不那么强大。(目前没有办法指定一个切入点运行在与另一个切入点匹配的连接点之下。)控制流切点与当前调用堆栈匹配。例如,如果com.mypanany.web包中的方法或SomeCaller类调用了连接点,它可能会触发。控制流切点是使用org.springframework.aop.support.ControlFlowPointcut类指定的。

Control flow pointcuts are significantly more expensive to evaluate at runtime than even other dynamic pointcuts. In Java 1.4, the cost is about five times that of other dynamic pointcuts.

6.1.5. Pointcut Superclasses

Spring提供了有用的切入点超类来帮助您实现自己的切入点。

因为静态切入点最有用,所以您可能应该子类StaticMethodMatcherPointCut。这只需要实现一个抽象方法(尽管您可以重写其他方法来定制行为)。下面的示例说明如何将StaticMethodMatcherPointCut子类化:

Java
Kotlin
class TestStaticPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method m, Class targetClass) {
        // return true if custom criteria match
    }
}

             
             

还有一些用于动态切入点的超类。您可以将定制切入点与任何通知类型一起使用。

6.1.6. Custom Pointcuts

因为Spring AOP中的切入点是Java类,而不是语言特性(就像在AspectJ中一样),所以您可以声明定制的切入点,无论是静态的还是动态的。Spring中的定制切入点可以任意复杂。但是,如果可以的话,我们建议使用AspectJ切入点表达式语言。

Later versions of Spring may offer support for “semantic pointcuts” as offered by JAC — for example, “all methods that change instance variables in the target object.”

6.2. Advice API in Spring

现在我们可以研究一下Spring AOP是如何处理通知的。

6.2.1. Advice Lifecycles

每条建议都是一颗春豆。建议实例可以在所有建议对象之间共享,也可以对每个建议对象是唯一的。这对应于每个类或每个实例的建议。

以班级为单位的建议是最常用的。它适用于一般建议,如事务顾问。这些不依赖于代理对象的状态或添加新状态。他们只是按照方法和论点行事。

每个实例的建议适用于介绍,以支持Mixin。在本例中,通知将状态添加到代理对象。

您可以在同一个AOP代理中混合使用共享和每个实例的建议。

6.2.2. Advice Types in Spring

Spring提供了几种通知类型,并且可以扩展以支持任意的通知类型。本节介绍基本概念和标准建议类型。

Interception Around Advice

Spring中最基本的建议类型是拦截建议。

Spring与使用方法拦截的AOPAlliance接口兼容。实现MethodInterceptor和围绕通知实现的类还应实现以下接口:

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}

              
              

Invoke()方法的方法调用参数公开被调用的方法、目标连接点、AOP代理和该方法的参数。Invoke()方法应该返回调用的结果:连接点的返回值。

下面的示例显示了一个简单的方法拦截器实现:

Java
Kotlin
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;
    }
}

              
              

注意对方法调用方法的调用。这将沿着拦截器链向连接点前进。大多数拦截器调用此方法并返回其返回值。然而,方法拦截器,就像任何周围的建议一样,可以返回不同的值或抛出异常,而不是调用Continue方法。然而,如果没有充分的理由,您不会想要这样做。

MethodInterceptor implementations offer interoperability with other AOP Alliance-compliant AOP implementations. The other advice types discussed in the remainder of this section implement common AOP concepts but in a Spring-specific way. While there is an advantage in using the most specific advice type, stick with MethodInterceptor around advice if you are likely to want to run the aspect in another AOP framework. Note that pointcuts are not currently interoperable between frameworks, and the AOP Alliance does not currently define pointcut interfaces.
Before Advice

更简单的建议类型是BEFORE建议。这不需要方法调用对象,因为它只在进入方法之前被调用。

BEFORE通知的主要优点是不需要调用Progress()方法,因此不可能无意中无法沿着拦截器链前进。

下面的清单显示了MethodBeForeAdacy接口:

public interface MethodBeforeAdvice extends BeforeAdvice {

    void before(Method m, Object[] args, Object target) throws Throwable;
}

              
              

(Spring的API设计将允许在通知之前使用field,尽管通常的对象适用于field拦截,Spring不太可能实现它。)

请注意,返回类型为void。在连接点运行之前,通知可以插入自定义行为,但不能更改返回值。如果BEFORE通知抛出异常,它将停止进一步执行拦截器链。异常沿拦截器链向上传播。如果未选中它或在被调用方法的签名上,它将被直接传递给客户端。否则,它被AOP代理包装在未检查的异常中。

下面的示例显示了Spring中的BEFORE通知,它对所有方法调用进行计数:

Java
Kotlin
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;
    }
}

              
              
Before advice can be used with any pointcut.
Throws Advice

如果连接点引发异常,则在连接点返回后调用抛出通知。Spring提供了打字的投掷建议。请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。它是一个标记接口,用于标识给定对象实现了一个或多个类型化的抛出通知方法。这些文件应采用以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

              
              

只需要最后一个参数。方法签名可以有一个或四个参数,具体取决于通知方法是否对方法和参数感兴趣。接下来的两个清单显示了抛出建议的示例类。

如果抛出RemoteException(包括从子类引发),则会调用以下建议:

Java
Kotlin
public class RemoteThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(RemoteException ex) throws Throwable {
        // Do something with remote exception
    }
}

              
              

与前面的建议不同,下一个示例声明了四个参数,以便它可以访问被调用的方法、方法参数和目标对象。如果抛出ServletException,则会调用以下建议:

Java
Kotlin
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {

    public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
        // Do something with all arguments
    }
}

              
              

最后一个示例说明如何在同时处理RemoteExceptionServletException的单个类中使用这两个方法。可以将任意数量的抛出通知方法组合到单个类中。下面的清单显示了最后一个示例:

Java
Kotlin
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
    }
}

              
              
If a throws-advice method throws an exception itself, it overrides the original exception (that is, it changes the exception thrown to the user). The overriding exception is typically a RuntimeException, which is compatible with any method signature. However, if a throws-advice method throws a checked exception, it must match the declared exceptions of the target method and is, hence, to some degree coupled to specific target method signatures. Do not throw an undeclared checked exception that is incompatible with the target method’s signature!
Throws advice can be used with any pointcut.
After Returning Advice

在Spring中返回建议后必须实现org.springframework.aop.AfterReturningAdvice接口,如下面的清单所示:

public interface AfterReturningAdvice extends Advice {

    void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable;
}

              
              

返回后的通知可以访问返回值(不能修改)、被调用的方法、方法的参数和目标。

下面的返回建议之后统计了所有未引发异常的成功方法调用:

Java
Kotlin
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;
    }
}

              
              

此建议不会更改执行路径。如果它抛出异常,它将在拦截器链上抛出,而不是返回值。

After returning advice can be used with any pointcut.
Introduction Advice

Spring将介绍建议视为一种特殊的拦截建议。

Introation需要实现以下接口的IntroductionAdvisorIntroductionInterceptor

public interface IntroductionInterceptor extends MethodInterceptor {

    boolean implementsInterface(Class intf);
}

              
              

继承自AOP联盟方法拦截器接口的Invoke()方法必须实现介绍。也就是说,如果被调用的方法位于引入的接口上,则引入拦截器负责处理方法调用 - ,它不能调用Procise()

介绍建议不能与任何切入点一起使用,因为它只适用于类级别,而不适用于方法级别。您只能将介绍建议与IntroductionAdvisor一起使用,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {

    ClassFilter getClassFilter();

    void validateInterfaces() throws IllegalArgumentException;
}

public interface IntroductionInfo {

    Class<?>[] getInterfaces();
}

              
              

没有方法匹配器,因此没有与介绍建议相关联的切入点。只有类过滤是合乎逻辑的。

getInterFaces()方法返回由该顾问引入的接口。

valiateInterFaces()方法在内部使用,用于查看已配置的IntroductionInterceptor是否可以实现引入的接口。

考虑来自Spring测试套件的一个示例,并假设我们想要将以下接口引入一个或多个对象:

Java
Kotlin
public interface Lockable {
    void lock();
    void unlock();
    boolean locked();
}

              
              

这说明了一种混合。我们希望能够将建议的对象强制转换为lockable,无论其类型如何,并调用lock和unlock方法。如果我们调用lock()方法,我们希望所有setter方法抛出LockedException。因此,我们可以添加一个方面,该方面能够在对象不知情的情况下使对象不可变:AOP的一个很好的例子。

首先,我们需要一个IntroductionInterceptor来完成繁重的任务。在本例中,我们扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor便利类。我们可以直接实现<代码>IntroductionInterceptor,但在大多数情况下使用DelegatingIntroductionInterceptor是最好的。

DelegatingIntroductionInterceptor旨在将介绍委托给引入的接口的实际实现,隐藏了使用侦听来执行此操作。可以使用构造函数参数将委托设置为任何对象。默认委托(当使用无参数构造函数时)是this。因此,在下一个示例中,委托是DelegatingIntroductionInterceptor.的LockMixin子类给定一个委托(默认情况下为其自身),DelegatingIntroductionInterceptor实例将查找该委托实现的所有接口(IntroductionInterceptor除外),并支持对其中任何接口的介绍。像LockMixin这样的子类可以调用suppressInterface(Class Intf)方法来隐藏不应该公开的接口。但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor都使用了实际公开的接口。引入的接口隐藏了目标对同一接口的任何实现。

因此,<代码>锁定混合 扩展了DelegatingIntroductionInterceptor并实现了<代码>可锁定 本身。超类自动获取可以支持引入的可锁定,所以我们不需要指定它。我们可以通过这种方式引入任意数量的接口。

注意锁定的实例变量的使用。这实际上为目标对象中保存的状态添加了额外的状态。

下面的示例显示示例LockMixin类:

Java
Kotlin
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);
    }

}

              
              

通常,您不需要覆盖Invoke()方法。DelegatingIntroductionInterceptor实现(如果引入<代码>委托 方法,则调用该方法,否则继续进行到连接点)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用setter方法。

所需的介绍只需要持有一个不同的LockMixin实例并指定引入的接口(在本例中,只需要Lockable)。一个更复杂的示例可能会引用引入拦截器(它将被定义为原型)。在本例中,没有与LockMixin相关的配置,所以我们使用new创建它。以下示例显示了我们的LockMixinAdvisor类:

Java
Kotlin
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {

    public LockMixinAdvisor() {
        super(new LockMixin(), Lockable.class);
    }
}

              
              

我们可以非常简单地应用这个Advisor,因为它不需要配置。(但是,如果没有IntroductionAdvisor,则无法使用IntroductionInterceptor。)与通常的介绍一样,顾问必须是每个实例的,因为它是有状态的。对于每个建议的对象,我们需要一个不同的LockMixinAdvisor实例,因此LockMixinAdvisor。顾问包括被建议对象的状态的一部分。

我们可以通过使用Advised.addAdvisor()方法或(推荐的方式)在XML配置中以编程方式应用此Advisor,就像使用任何其他Advisor一样。下面讨论的所有代理创建选项,包括“自动代理创建者”,都可以正确处理介绍和有状态混合。

6.3. The Advisor API in Spring

在Spring中,Advisor是一个方面,它只包含一个与切入点表达式相关联的通知对象。

除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。它可以与方法拦截器BeForeAdviceThrowsAdvice一起使用。

在同一个AOP代理中,可以在Spring中混合使用Advisor和Advisor类型。例如,您可以在一个代理配置中使用围绕建议、抛出建议和之前建议的拦截。Spring会自动创建必要的拦截器链。

6.4. Using the ProxyFactoryBean to Create AOP Proxies

如果您为业务对象使用了Spring IOC容器(ApplicationContextBeanFactory)(您应该这样做!),则需要使用Spring的AOPFactoryBean实现之一。(请记住,工厂Bean引入了一层间接层,使其可以创建不同类型的对象。)

The Spring AOP support also uses factory beans under the covers.

在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean.这使您可以完全控制切入点、适用的任何建议及其顺序。但是,如果您不需要这样的控制,则有一些更简单的选项是更可取的。

6.4.1. Basics

ProxyFactoryBean与其他SpringFactoryBean实现一样,引入了一定程度的间接性。如果定义名为fooProxyFactoryBean,则引用foo的对象不会看到ProxyFactoryBean实例本身,而是由ProxyFactoryBean中的getObject()方法的实现创建的对象。此方法创建包装目标对象的AOP代理。

使用ProxyFactoryBean或其他支持IOC的类来创建AOP代理的最重要的好处之一是,建议和切入点也可以由IOC管理。这是一个强大的功能,支持使用其他AOP框架难以实现的某些方法。例如,通知本身可能会引用应用程序对象(除了目标,它应该在任何AOP框架中都可用),从而受益于依赖项注入提供的所有可插入性。

6.4.2. JavaBean Properties

与大多数随Spring提供的FactoryBean实现一样,ProxyFactoryBean类本身就是一个JavaBean。其属性用于:

一些关键属性继承自org.springframework.aop.framework.ProxyConfig(Spring中所有AOP代理工厂的超类)。这些关键属性包括:

  • proxyTargetClass:如果要代理目标类而不是目标类的接口,则为True。如果该属性值设置为true,则会创建CGLIB代理(但也请参阅基于JDK和CGLIB的代理)。

  • 优化:控制是否对通过CGLIB创建的代理应用主动优化。除非您完全了解相关的AOP代理如何处理优化,否则不应随意使用此设置。该选项当前仅用于CGLIB代理。它对JDK动态代理没有影响。

  • 冻结:如果代理配置被冻结,则不再允许更改配置。这对于轻微的优化和在创建代理后不希望调用者能够操作代理(通过Advised接口)的情况都很有用。此属性的默认值为False,因此允许进行更改(如添加其他建议)。

  • exposeProxy:确定是否应该在ThreadLocal中公开当前代理,以便目标可以访问它。如果目标需要获取代理,并且exposeProxy属性设置为true,则目标可以使用AopConext.CurrentProxy()方法。

特定于ProxyFactoryBean的其他属性包括:

  • proxyInterages字符串接口名称的数组。如果没有提供,则使用目标类的CGLIB代理(但也请参阅基于JDK和CGLIB的代理)。

  • 拦截器名称:要应用的Advisor、拦截器或其他建议名称的字符串数组。根据先到先得的原则,订购是非常重要的。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。

    这些名称是当前工厂中的Bean名称,包括来自原始工厂的Bean名称。您不能在这里提到Bean引用,因为这样做会导致ProxyFactoryBean忽略通知的单例设置。

    您可以在拦截器名称后附加星号(*)。这样做会导致应用名称以要应用的星号之前的部分开头的所有Advisor Bean。您可以在Using“Global”Advisors中找到使用此功能的示例。

  • Singleton:工厂是否应该返回单个对象,无论调用getObject()方法的频率如何。有几个FactoryBean实现提供了这样的方法。默认值为true。如果您想要使用有状态建议--例如,对于有状态混合--使用原型建议以及FALSE的单值。

6.4.3. JDK- and CGLIB-based proxies

本节是关于ProxyFactoryBean如何为特定目标对象(要代理)选择创建基于JDK的代理或基于CGLIB的代理的权威文档。

The behavior of the ProxyFactoryBean with regard to creating JDK- or CGLIB-based proxies changed between versions 1.2.x and 2.0 of Spring. The ProxyFactoryBean now exhibits similar semantics with regard to auto-detecting interfaces as those of the TransactionProxyFactoryBean class.

如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则会创建一个基于CGLIB的代理。这是最简单的场景,因为JDK代理是基于接口的,而没有接口意味着JDK代理甚至不可能实现。您可以插入目标Bean并通过设置interceptorNames属性来指定拦截器列表。请注意,即使将ProxyFactoryBeanproxyTargetClass属性设置为FALSE,也会创建基于CGLIB的代理。(这样做没有任何意义,最好从Bean定义中删除,因为在最好的情况下,它是多余的,在最坏的情况下,它会令人困惑。)

如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。

如果ProxyFactoryBeanproxyTargetClass属性已设置为true,则会创建一个基于CGLIB的代理。这是有道理的,也符合最小意外的原则。即使ProxyFactoryBeanproxyInterFaces属性已设置为一个或多个完全限定的接口名称,proxyTargetClass属性设置为true也会导致基于CGLIB的代理生效。

如果已将ProxyFactoryBeanproxyInterFaces属性设置为一个或多个完全限定的接口名称,则会创建一个基于JDK的代理。创建的代理实现了proxyInterFaces属性中指定的所有接口。如果目标类恰好实现了比proxyInterFaces属性中指定的接口多得多的接口,这是很好的,但这些额外的接口不是由返回的代理实现的。

如果ProxyFactoryBeanproxyInterages属性尚未设置,但目标类实现了一个(或多个)接口,则ProxyFactoryBean会自动检测到目标类确实实现了至少一个接口,并创建了一个基于JDK的代理。实际代理的接口是目标类实现的所有接口。实际上,这等同于向proxyInterFaces属性提供目标类实现的每个接口的列表。然而,它的工作量要小得多,而且不太容易出现打印错误。

6.4.4. Proxying Interfaces

考虑一个运行中的ProxyFactoryBean的简单示例。此示例涉及:

  • 代理的目标Bean。这是示例中的PersTargetBean定义。

  • 用于提供建议的Advisor拦截器

  • 一个AOP代理Bean定义,用于指定目标对象(PersTargetBean)、代理的接口和要应用的建议。

下面的清单显示了该示例:

<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name" value="Tony"/>
    <property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>

    <property name="target" ref="personTarget"/>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>
             
             

请注意,interceptorNames属性接受字符串的列表,该列表保存当前工厂中拦截器或顾问的Bean名称。您可以使用Advisors、拦截器、返回前、返回后和抛出通知对象。顾问的排序意义重大。

You might be wondering why the list does not hold bean references. The reason for this is that, if the singleton property of the ProxyFactoryBean is set to false, it must be able to return independent proxy instances. If any of the advisors is itself a prototype, an independent instance would need to be returned, so it is necessary to be able to obtain an instance of the prototype from the factory. Holding a reference is not sufficient.

可以使用前面显示的PersonBean定义来代替Person实现,如下所示:

Java
Kotlin
Person person = (Person) factory.getBean("person");

             
             

与普通Java对象一样,同一IoC上下文中的其他Bean可以表达对它的强类型依赖关系。以下示例显示了如何执行此操作:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>
             
             

本例中的PersonUser类公开了Person类型的属性。就它而言,可以透明地使用AOP代理来代替“真实的”人实现。但是,它的类将是动态代理类。可以将其强制转换为建议接口(稍后讨论)。

您可以通过使用匿名内部Bean来隐藏目标和代理之间的区别。只有ProxyFactoryBean定义不同。仅出于完整性考虑才包含该建议。以下示例显示如何使用匿名内部Bean:

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <!-- Use inner bean, not local reference to target -->
    <property name="target">
        <bean class="com.mycompany.PersonImpl">
            <property name="name" value="Tony"/>
            <property name="age" value="51"/>
        </bean>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>
             
             

使用匿名内部Bean的好处是只有一个Person类型的对象。如果我们想要防止应用程序上下文的用户获得对未被建议的对象的引用,或者需要避免使用Spring IOC自动装配来避免任何歧义,这是很有用的。可以说,还有一个优势在于ProxyFactoryBean定义是自包含的。然而,在某些情况下,能够从工厂获得未经建议的目标实际上可能是一种优势(例如,在某些测试场景中)。

6.4.5. Proxying Classes

如果需要代理一个类,而不是一个或多个接口,该怎么办?

想象一下,在我们前面的示例中,没有Person接口。我们需要通知一个名为Person的类,该类没有实现任何业务接口。在这种情况下,您可以将Spring配置为使用CGLIB代理,而不是动态代理。为此,请将前面显示的ProxyFactoryBean上的proxyTargetClass属性设置为true。虽然最好是对接口而不是类进行编程,但在使用遗留代码时,向未实现接口的类提供建议的功能可能会很有用。(一般来说,Spring不是说明性的。虽然它使应用好的实践变得容易,但它避免了强制使用特定的方法。)

如果您愿意,您可以在任何情况下强制使用CGLIB,即使您确实有接口。

CGLIB代理的工作原理是在运行时生成目标类的子类。Spring将这个生成的子类配置为将方法调用委托给原始目标。子类用于实现Decorator模式,编织在通知中。

CGLIB代理通常应该对用户透明。但是,有一些问题需要考虑:

  • 无法建议最终方法,因为它们不能被重写。

  • 不需要将CGLIB添加到类路径中。从Spring3.2开始,CGLIB被重新打包并包含在Spring-core JAR中。换句话说,基于CGLIB的AOP“开箱即用”,JDK动态代理也是如此。

CGLIB代理和动态代理的性能差别不大。在这种情况下,业绩不应该是决定性的考虑因素。

6.4.6. Using “Global” Advisors

通过在拦截器名称后附加星号,所有具有与星号前部分匹配的Bean名称的Advisor都将添加到Advisor链中。如果您需要添加一组标准的“全局”顾问,这会派上用场。以下示例定义了两个全局顾问:

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="service"/>
    <property name="interceptorNames">
        <list>
            <value>global*</value>
        </list>
    </property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
             
             

6.5. Concise Proxy Definitions

特别是在定义事务代理时,您可能会得到许多类似的代理定义。父Bean和子Bean定义以及内部Bean定义的使用可以产生更清晰、更简洁的代理定义。

首先,我们为代理创建一个父模板Bean定义,如下所示:

<bean id="txProxyTemplate" abstract="true" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>
            
            

它本身从未被实例化,因此它实际上可能是不完整的。然后,需要创建的每个代理都是一个子Bean定义,它将代理的目标包装为内部Bean定义,因为无论如何目标都不会单独使用。下面的示例显示了这样一个子Bean:

<bean id="myService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MyServiceImpl">
        </bean>
    </property>
</bean>
            
            

您可以覆盖父模板中的属性。在下面的示例中,我们覆盖事务传播设置:

<bean id="mySpecialService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MySpecialServiceImpl">
        </bean>
    </property>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="store*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>
            
            

注意,在父Bean示例中,我们显式地将父Bean定义标记为抽象的,方法是将抽象属性设置为true,如前面所述,因此它可能永远不会被实例化。默认情况下,应用程序上下文(而不是简单的Bean工厂)预实例化所有单例。因此,重要的是(至少对于单例Bean),如果您有一个只打算用作模板的(父)Bean定义,并且该定义指定了一个类,则必须确保将抽象属性设置为true。否则,应用程序上下文实际上会尝试预实例化它。

6.6. Creating AOP Proxies Programmatically with the ProxyFactory

使用Spring以编程方式创建AOP代理很容易。这使您可以使用Spring AOP,而不依赖于Spring IOC。

由目标对象实现的接口被自动代理。下面的清单显示了为目标对象创建代理的过程,其中包含一个拦截器和一个顾问:

Java
Kotlin
ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

            
            

第一步是构造org.springframework.aop.framework.ProxyFactory.类型的对象您可以使用目标对象创建它,如上例所示,或者指定要在替代构造函数中代理的接口。

您可以添加建议(拦截器作为一种特殊的建议)、建议或两者,并在ProxyFactory的生命周期中处理它们。如果添加IntroductionInterceptionAroundAdvisor,,则可以使代理实现其他接口。

ProxyFactory(继承自AdvisedSupport)上也有一些方便的方法,使您可以添加其他建议类型,如之前和抛出建议。AdvisedSupportProxyFactoryProxyFactoryBean的超类。

Integrating AOP proxy creation with the IoC framework is best practice in most applications. We recommend that you externalize configuration from Java code with AOP, as you should in general.

6.7. Manipulating Advised Objects

无论您如何创建AOP代理,都可以通过使用org.springframework.aop.framework.Advised接口来操作它们。任何AOP代理都可以强制转换到此接口,无论它实现了哪些其他接口。该接口包括以下方法:

Java
Kotlin
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();

            
            

getAdvisors()方法为添加到工厂的每个顾问、拦截器或其他建议类型返回一个Advisor。如果您添加了Advisor,则在此索引处返回的Advisor就是您添加的对象。如果添加了拦截器或其他通知类型,则Spring会将其包装在带有切入点的Advisor中,该切入点始终返回true。因此,如果添加了方法拦截器,则为此索引返回的顾问是返回方法拦截器DefaultPointutAdvisor以及匹配所有类和方法的切入点。

addAdvisor()方法可用于添加任何Advisor。通常,保存切入点和建议的Advisor是通用的DefaultPointutAdvisor,您可以将其与任何建议或切入点一起使用(但不能用于介绍)。

默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。唯一的限制是不可能添加或删除引入顾问,因为工厂中的现有代理不会显示接口更改。(您可以从工厂获取新的代理以避免此问题。)

以下示例显示将AOP代理转换为Advised接口并检查和操作其通知:

Java
Kotlin
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);

            
            
It is questionable whether it is advisable (no pun intended) to modify advice on a business object in production, although there are, no doubt, legitimate usage cases. However, it can be very useful in development (for example, in tests). We have sometimes found it very useful to be able to add test code in the form of an interceptor or other advice, getting inside a method invocation that we want to test. (For example, the advice can get inside a transaction created for that method, perhaps to run SQL to check that a database was correctly updated, before marking the transaction for roll back.)

根据您创建代理的方式,您通常可以设置冻结标志。在这种情况下,AdvisedisFrozen()方法返回true,任何通过添加或删除来修改通知的尝试都会导致AopConfigException。冻结建议对象的状态的功能在某些情况下很有用(例如,防止调用代码移除安全拦截器)。

6.8. Using the "auto-proxy" facility

到目前为止,我们已经考虑使用ProxyFactoryBean或类似的工厂Bean显式创建AOP代理。

Spring还允许我们使用“自动代理”Bean定义,它可以自动代理选定的Bean定义。这是建立在Spring的“Bean后处理器”基础设施之上的,该基础设施允许在加载容器时修改任何Bean定义。

在此模型中,您将在XML Bean定义文件中设置一些特殊的Bean定义,以配置自动代理基础设施。这使您可以声明符合自动代理条件的目标。您不需要使用ProxyFactoryBean

有两种方法可以做到这一点:

  • 通过使用引用当前上下文中的特定Bean的自动代理创建器。

  • 自动代理创建的一个特殊情况值得单独考虑:由源级元数据属性驱动的自动代理创建。

6.8.1. Auto-proxy Bean Definitions

本节介绍org.springframework.aop.framework.autoproxy包提供的自动代理创建器。

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator类是一个BeanPostProcessor,它自动为名称与文字值或通配符匹配的Bean创建AOP代理。以下示例说明如何创建BeanNameAutoProxyCreatorBean:

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>
              
              

ProxyFactoryBean一样,有一个interceptorNames属性,而不是拦截器列表,以允许原型顾问的正确行为。命名为“拦截器”的可以是Advisor或任何通知类型。

与通常的自动代理一样,使用BeanNameAutoProxyCreator的主要目的是将相同的配置一致地应用于多个对象,而配置量最小。它是将声明性事务应用于多个对象的流行选择。

名称匹配的Bean定义,如前面示例中的jdkMyBeanonlyJdk,都是带有目标类的普通老式Bean定义。AOP代理由BeanNameAutoProxyCreator自动创建。同样的建议也适用于所有匹配的Bean。请注意,如果使用Advisor(而不是前面示例中的拦截器),切入点可能会以不同的方式应用于不同的Bean。

DefaultAdvisorAutoProxyCreator

一个更通用、功能极其强大的自动代理创建器是DefaultAdvisorAutoProxyCreator。这将在当前上下文中自动应用符合条件的顾问,而不需要在自动代理顾问的Bean定义中包括特定的Bean名称。它提供了与BeanNameAutoProxyCreator相同的配置一致性和避免重复的优点。

使用此机制包括:

  • 指定DefaultAdvisorAutoProxyCreatorBean定义。

  • 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是建议,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点进行评估,以检查对候选Bean定义的每个建议的合格性。

DefaultAdvisorAutoProxyCreator自动评估每个Advisor中包含的切入点,以查看它应该对每个业务对象应用什么建议(如果有)(如示例中的Commercial Object1Commercial Object2)。

这意味着可以将任意数量的顾问自动应用于每个业务对象。如果任何顾问中没有与业务对象中的任何方法匹配的切入点,则不代理该对象。当为新业务对象添加Bean定义时,如果需要,会自动代理它们。

通常,自动代理的优点是使调用方或依赖项不可能获得未通知的对象。在此ApplicationContext上调用getBean(“Commercial Obt1”)将返回AOP代理,而不是目标业务对象。(前面显示的“内部Bean”习惯用法也提供了这个好处。)

下面的示例创建一个DefaultAdvisorAutoProxyCreatorBean和本节中讨论的其他元素:

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
              
              

如果要将相同的建议一致地应用于多个业务对象,DefaultAdvisorAutoProxyCreator非常有用。一旦基础结构定义就绪,您就可以添加新的业务对象,而无需包括特定的代理配置。您还可以轻松地加入其他方面(例如,跟踪或性能监视方面),只需对配置进行最少的更改。

DefaultAdvisorAutoProxyCreator支持筛选(通过使用命名约定,以便只评估某些顾问,从而允许在同一工厂中使用多个配置不同的AdvisorAutoProxyCreator)和排序。顾问可以实现org.springfrawork.core.Ordered接口,以便在出现问题时确保正确的排序。上例中使用的TransactionAttributeSourceAdvisor具有可配置的顺序值。默认设置为无序。

6.9. Using TargetSource Implementations

Spring提供了用org.springframework.aop.TargetSource接口表示的<代码>目标源代码 的概念。该接口负责返回实现连接点的“目标对象”。AOP代理每次处理方法调用时,都会向TargetSource实现请求一个目标实例。

使用Spring AOP的开发人员通常不需要直接使用TargetSource实现,但这提供了一种支持池化、热插拔和其他复杂目标的强大方法。例如,通过使用池来管理实例,池化TargetSource可以为每次调用返回不同的目标实例。

如果不指定TargetSource,则使用默认实现来包装本地对象。每次调用都返回相同的目标(正如您所预期的那样)。

本节的其余部分描述了随Spring提供的标准目标源代码以及如何使用它们。

When using a custom target source, your target will usually need to be a prototype rather than a singleton bean definition. This allows Spring to create a new target instance when required.

6.9.1. Hot-swappable Target Sources

org.springframework.aop.target.HotSwappableTargetSource的存在是为了允许切换AOP代理的目标,同时允许调用者保留对它的引用。

更改目标源的目标将立即生效。HotSwappableTargetSource是线程安全的。

您可以在HotSwappableTargetSource上使用swap()方法更改目标,如下例所示:

Java
Kotlin
HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);

             
             

以下示例显示了所需的XML定义:

<bean id="initialTarget" class="mycompany.OldTarget"/>

<bean id="swapper" class="org.springframework.aop.target.HotSwappableTargetSource">
    <constructor-arg ref="initialTarget"/>
</bean>

<bean id="swappable" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="swapper"/>
</bean>
             
             

前面的swap()调用更改了可交换Bean的目标。持有对该Bean的引用的客户端不会意识到更改,但会立即开始达到新目标。

虽然本例没有添加任何通知(使用TargetSource不需要添加通知),但任何TargetSource都可以与任意通知一起使用。

6.9.2. Pooling Target Sources

使用池化目标源提供了类似于无状态会话EJB的编程模型,其中维护了一个相同实例的池,方法调用将释放池中的对象。

Spring池化和SLSB池化之间的一个关键区别是,Spring池化可以应用于任何POJO。与通常的Spring一样,该服务可以以非侵入性的方式应用。

Spring支持Commons Pool 2.2,后者提供了相当高效的池化实现。您需要应用程序类路径上的Commons-poolJar才能使用此功能。您还可以将org.springframework.aop.target.AbstractPoolingTargetSource子类化以支持任何其他池化API。

Commons Pool 1.5+ is also supported but is deprecated as of Spring Framework 4.2.

下面的清单显示了一个配置示例:

<bean id="businessObjectTarget" class="com.mycompany.MyBusinessObject" scope="prototype">
    ... properties omitted
</bean>

<bean id="poolTargetSource" class="org.springframework.aop.target.CommonsPool2TargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
    <property name="maxSize" value="25"/>
</bean>

<bean id="businessObject" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="targetSource" ref="poolTargetSource"/>
    <property name="interceptorNames" value="myInterceptor"/>
</bean>
             
             

请注意,目标对象(上一个示例中的Commercial ObjectTarget)必须是原型。这允许PoolingTargetSource实现创建目标的新实例,以根据需要扩大池。有关其属性的信息,请参阅AbstractPoolingTargetSourcejavadoc和您希望使用的具体子类。MaxSize是最基本的,并且始终保证存在。

在本例中,myInterceptor是需要在同一IOC上下文中定义的拦截器的名称。但是,您不需要指定拦截器来使用池。如果您只需要池化,而不需要其他建议,则根本不要设置interceptorNames属性。

您可以将Spring配置为能够将任何池化对象强制转换为org.springframework.aop.target.PoolingConfig接口,该接口通过介绍公开了有关池的配置和当前大小的信息。您需要定义一个类似于以下内容的指导:

<bean id="poolConfigAdvisor" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="poolTargetSource"/>
    <property name="targetMethod" value="getPoolingConfigMixin"/>
</bean>
             
             

该顾问是通过在AbstractPoolingTargetSource类上调用一个方便的方法获得的,因此使用了MethodInvokingFactoryBean。此顾问的名称(此处为poolConfigAdvisor)必须在公开池对象的ProxyFactoryBean中的拦截器名称列表中。

演员阵容的定义如下:

Java
Kotlin
PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

             
             
Pooling stateless service objects is not usually necessary. We do not believe it should be the default choice, as most stateless objects are naturally thread safe, and instance pooling is problematic if resources are cached.

通过使用自动代理,可以实现更简单的池化。您可以设置任何自动代理创建者使用的TargetSource实现。

6.9.3. Prototype Target Sources

设置“原型”目标源类似于设置池化TargetSource。在这种情况下,在每次方法调用时都会创建一个新的目标实例。尽管在现代JVM中创建新对象的成本并不高,但连接新对象(满足其IOC依赖项)的成本可能会更高。因此,在没有非常充分的理由的情况下,您不应该使用这种方法。

为此,您可以修改前面所示的poolTargetSource定义(为清楚起见,我们还更改了名称):

<bean id="prototypeTargetSource" class="org.springframework.aop.target.PrototypeTargetSource">
    <property name="targetBeanName" ref="businessObjectTarget"/>
</bean>
             
             

唯一的属性是目标Bean的名称。在TargetSource实现中使用继承以确保命名一致。与池化目标源一样,目标Bean必须是原型Bean定义。

6.9.4. ThreadLocal Target Sources

ThreadLocal如果您需要为每个传入请求(即每个线程)创建一个对象,则目标源非常有用。ThreadLocal的概念提供了一个JDK范围的工具,用于透明地将资源存储在线程旁边。设置ThreadLocalTargetSource与为其他类型的目标源所做的说明基本相同,如下面的示例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource">
    <property name="targetBeanName" value="businessObjectTarget"/>
</bean>
             
             
ThreadLocal instances come with serious issues (potentially resulting in memory leaks) when incorrectly using them in multi-threaded and multi-classloader environments. You should always consider wrapping a threadlocal in some other class and never directly use the ThreadLocal itself (except in the wrapper class). Also, you should always remember to correctly set and unset (where the latter simply involves a call to ThreadLocal.set(null)) the resource local to the thread. Unsetting should be done in any case, since not unsetting it might result in problematic behavior. Spring’s ThreadLocal support does this for you and should always be considered in favor of using ThreadLocal instances without other proper handling code.

6.10. Defining New Advice Types

Spring AOP被设计为可扩展的。虽然拦截实现策略目前在内部使用,但除了拦截建议周围、之前、抛出建议和返回建议之后,还可以支持任意建议类型。

org.springframework.aop.framework.adapter包是一个SPI包,它允许在不更改核心框架的情况下添加对新的定制建议类型的支持。对自定义建议类型的唯一约束是它必须实现org.aopalliance.aop.Adacy标记接口。

有关更多信息,请参阅org.springframework.aop.framework.adapterjavadoc。

7. Null-safety

尽管Java不允许您使用其类型系统来表示空安全,但Spring框架现在在org.springfrawork.lang包中提供了以下注释,让您可以声明API和字段的空性:

  • @Nullable:用于指示特定参数、返回值或字段可以为的注释。

  • @NonNull:用于指示特定参数、返回值或字段不能为的批注(参数/返回值和@NonNullApi@NonNullFields分别适用的参数/返回值和字段不需要)。

  • @NonNullApi:包级别的批注,将非空声明为参数和返回值的默认语义。

  • @NonNullFields:包级别的批注,该批注将非空声明为字段的默认语义。

Spring框架本身利用了这些注释,但也可以在任何基于Spring的Java项目中使用它们来声明空安全的API和可选的空安全字段。泛型类型参数、变量和数组元素的空性尚不受支持,但应该会在即将发布的版本中提供,有关最新信息,请参阅spr-15942。可空性声明预计将在不同的Spring框架版本之间进行微调,包括次要版本。方法体内使用的类型的可空性不在此功能的范围内。

Other common libraries such as Reactor and Spring Data provide null-safe APIs that use a similar nullability arrangement, delivering a consistent overall experience for Spring application developers.

7.1. Use cases

除了为Spring框架API的空性提供显式声明之外,这些注释还可以被IDE(如IDEA或Eclipse)用来提供与空安全性相关的有用警告,以便在运行时避免NullPointerException

它们还用于在Kotlin项目中使Spring API为空安全,因为Kotlin本身就支持空安全。有关更多详细信息,请参阅Kotlin支持文档

7.2. JSR-305 meta-annotations

Spring批注使用JSR305批注(一种休眠但分布广泛的JSR)进行元批注。JSR-305元批注允许IDEA或Kotlin等工具供应商以通用方式提供空安全支持,而不必硬编码支持Spring批注。

不需要也不推荐将JSR-305依赖项添加到项目类路径以利用Spring空安全API。只有那些在代码库中使用空安全注释的项目,比如基于Spring库的项目,才应该添加带有<com.google.code.findbugs:jsr305:3.0.2>只编译Gradle配置或Maven提供的作用域的代码,以避免编译警告。

8. Data Buffers and Codecs

Java NIO提供了ByteBuffer,但许多库都在上面构建了自己的字节缓冲区API,特别是对于重用缓冲区和/或使用直接缓冲区有利于提高性能的网络操作。例如,Netty有ByteBuf层次结构,Undertow使用XNIO,Jetty使用带有要释放的回调的池字节缓冲区,等等。Spring-core模块提供了一组抽象来使用各种字节缓冲区API,如下所示:

8.1. DataBufferFactory

DataBufferFactory用于创建数据缓冲区的方式有两种:

  1. 分配一个新的数据缓冲区,可以选择预先指定容量(如果已知),这会更有效,即使DataBuffer的实现可以根据需要扩展和缩小。

  2. 包装现有的byte[]java.nio.ByteBuffer,它用DataBuffer实现修饰给定的数据,并且不涉及分配。

请注意,WebFlux应用程序不会直接创建DataBufferFactory,而是通过客户端的ServerHttpResponseClientHttpRequest访问它。工厂的类型取决于底层的客户端或服务器,例如,NettyDataBufferFactory表示反应器Netty,DefaultDataBufferFactory表示其他类型。

8.2. DataBuffer

DataBuffer接口提供了与java.nio.ByteBuffer类似的操作,但还带来了一些额外的好处,其中一些好处是受到NettyByteBuf的启发。以下是部分好处清单:

  • 读写位置独立,即不需要调用flip()来交替读写。

  • 容量按需扩展,与java.lang.StringBuilder一样。

  • 通过PooledDataBuffer共享缓冲区和引用计数。

  • 将缓冲区查看为java.nio.ByteBufferInputStreamOutputStream

  • 确定给定字节的索引或最后一个索引。

8.3. PooledDataBuffer

正如ByteBuffer的Javadoc中所解释的,字节缓冲区可以是直接的,也可以是非直接的。直接缓冲区可以驻留在Java堆之外,这样就不需要复制本机I/O操作。这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但它们的创建和释放成本也更高,这导致了将缓冲区池化的想法。

PooledDataBufferDataBuffer的扩展,它帮助进行引用计数,而引用计数对于字节缓冲池是必不可少的。它怎麽工作?当分配PooledDataBuffer时,引用计数为1。调用retain()递增计数,而调用Release()递减计数。只要计数大于0,就保证缓冲区不会被释放。当计数减少到0时,可以释放池化缓冲区,这实际上可能意味着为缓冲区保留的内存返回到内存池。

请注意,与其直接操作PooledDataBuffer,在大多数情况下,最好使用DataBufferUtils中的方便方法,这些方法仅在PooledDataBuffer的实例时才对DataBuffer应用释放或保留。

8.4. DataBufferUtils

DataBufferUtils提供了许多实用程序方法来操作数据缓冲区:

  • 如果底层字节缓冲区API支持的话,可以通过复合缓冲区将数据缓冲区流加入到单个缓冲区中,可能是零拷贝。

  • InputStream或NIOChannel转换为Flux<;DataBuffer;,反之亦然。

  • 如果缓冲区是PooledDataBuffer的实例,则释放或保留DataBuffer的方法。

  • 从字节流中跳过或获取,直到特定的字节计数。

8.5. Codecs

org.springfrawork.core.codec包提供了以下策略接口:

  • 编码器Publisher<;T&>编码到数据缓冲区流中。

  • DecoderPublisher<;DataBuffer;解码为更高级别的对象流。

Spring-core模块提供byte[]ByteBufferDataBuffer资源字符串编码器和解码器实现。Spring-web模块添加了Jackson JSON、Jackson SMILE、JAXB2、协议缓冲区等编解码器。请参阅WebFlux部分中的编解码器

8.6. Using DataBuffer

在使用数据缓冲区时,必须特别注意确保释放缓冲区,因为它们可能被池化。我们将使用编解码器来说明它是如何工作的,但这些概念更适用于一般情况。让我们看看编解码器必须在内部执行哪些操作来管理数据缓冲区。

解码器是在创建更高级别对象之前最后读取输入数据缓冲区的,因此它必须按如下方式释放它们:

  1. 如果解码器只是读取每个输入缓冲区并准备立即释放它,则它可以通过DataBufferUtils.release(dataBuffer).执行此操作

  2. 如果解码器正在使用FluxMono运算符,如Flat MapReduce等在内部预取和缓存数据项的运算符,或者正在使用FilterSkip等遗漏项的运算符,则必须将doOnDisCard(DataBuffer.class,DataBufferUtils::Release)添加到合成链中,以确保在丢弃此类缓冲区之前释放这些缓冲区,这也可能是错误或取消信号的结果。

  3. 如果解码器以任何其他方式持有一个或多个数据缓冲区,则它必须确保在完全读取时释放它们,或者在读取和释放缓存的数据缓冲区之前发生错误或取消信号的情况下释放它们。

请注意,DataBufferUtils#Join提供了一种将数据缓冲区流聚合到单个数据缓冲区中的安全有效的方法。同样,skipUntilByteCounttaken UntilByteCount是解码器可以使用的额外安全方法。

编码器分配其他人必须读取(并释放)的数据缓冲区。因此,编码器没有太多事情可做。但是,如果在使用数据填充缓冲区时发生序列化错误,编码器必须注意释放数据缓冲区。例如:

Java
Kotlin
DataBuffer buffer = factory.allocateBuffer();
boolean release = true;
try {
    // serialize and populate buffer..
    release = false;
}
finally {
    if (release) {
        DataBufferUtils.release(buffer);
    }
}
return buffer;

            
            

编码器的使用者负责释放它接收到的数据缓冲区。在WebFlux应用程序中,编码器的输出用于写入HTTP服务器响应或客户端HTTP请求,在这种情况下,释放数据缓冲区是写入服务器响应或客户端请求的代码的责任。

请注意,在Netty上运行时,有用于排除缓冲区泄漏故障的调试选项

9. Logging

从Spring Framework5.0开始,Spring在Spring-JCL模块中实现了自己的Commons日志桥。该实现检查类路径中是否存在Log4j 2.x API和SLF4J 1.7 API,并使用其中的第一个作为日志记录实现,如果Log4j 2.x和SLF4J都不可用,则回退到Java平台的核心日志记录工具(也称为Juljava.util.Logging)。

将Log4j 2.x或Logback(或其他SLF4J提供程序)放入您的类路径中,不需要任何额外的桥,并让框架自动适应您的选择。有关更多信息,请参阅Spring Boot Logging参考文档

Spring的Commons日志变体仅用于核心框架和扩展中的基础设施日志目的。

对于应用程序代码中的日志记录需求,最好直接使用Log4j 2.x、SLF4J或JUL。

日志实现可以通过org.apache.commons.logging.LogFactory检索,如下例所示。

Java
Kotlin
public class MyBean {
    private final Log log = LogFactory.getLog(getClass());
    // ...
}

           
           

10. Ahead of Time Optimizations

本章介绍了Spring的提前(AOT)优化。

有关特定于集成测试的AOT支持,请参阅提前支持测试

10.1. Introduction to Ahead of Time Optimizations

Spring对AOT优化的支持是为了在构建时检查ApplicationContext,并应用通常在运行时发生的决策和发现逻辑。这样做可以构建更直接的应用程序启动安排,并且主要基于类路径和Environment集中于一组固定的功能。

早期应用此类优化意味着存在以下限制:

  • 类路径是固定的,并且在构建时完全定义。

  • 应用程序中定义的Bean不能在运行时更改,这意味着:

    • @Profile,特别是需要在构建时选择特定于配置文件的配置。

    • 影响Bean存在的环境属性(@Conditional)只在构建时被考虑。

当这些限制就位时,就可以在构建时执行提前处理并生成额外的资产。经过Spring AOT处理的应用程序通常会生成:

  • Java源代码

  • 字节码(通常用于动态代理)

  • 运行提示,用于使用反射、资源加载、序列化和JDK代理。

At the moment, AOT is focused on allowing Spring applications to be deployed as native images using GraalVM. We intend to offer more JVM-based use cases in future generations.

10.2. AOT engine overview

处理ApplicationContext排列的AOT引擎的入口点是ApplicationConextAotGenerator。它基于表示要优化的应用程序的GenericApplicationContextGenerationContext负责以下步骤:

  • 刷新ApplicationContext以进行AOT处理。与传统的刷新相反,此版本仅创建Bean定义,而不创建Bean实例。

  • 调用可用的BeanFactoryInitializationAotProcessor实现,并将它们的贡献应用于生成上下文。例如,核心实现迭代所有候选Bean定义并生成必要的代码来恢复BeanFactory的状态。

此过程完成后,已使用应用程序运行所需的生成代码、资源和类更新了GenerationContextRounmeHints实例也可用于生成相关的GraalVM本机镜像配置文件。

ApplicationContextAotGenerator#processAheadOfTime返回ApplicationContextInitializer入口点的类名,该入口点允许使用AOT优化启动上下文。

以下各节将更详细地介绍这些步骤。

10.3. Refresh for AOT Processing

所有GenericApplicationContext实现都支持AOT处理的刷新。应用程序上下文是使用任意数量的入口点创建的,通常采用@Configuration注释类的形式。

让我们来看一个基本的例子:

Java
@Configuration(proxyBeanMethods=false)
@ComponentScan
@Import({DataSourceConfiguration.class, ContainerConfiguration.class})
public class MyApplication {
}

            
            

使用常规运行时启动该应用程序涉及许多步骤,包括类路径扫描、配置类解析、Bean实例化和生命周期回调处理。REFRESH FOR AOT处理只应用常规刷新所发生的事情的子集。AOT处理可按如下方式触发:

Java
GenericApplicationContext applicationContext = new AnnotatedConfigApplicationContext();
context.register(MyApplication.class);
context.refreshForAotProcessing();

            
            

在此模式下,将照常调用BeanFactoryPostProcessor实现。这包括配置类解析、导入选择器、类路径扫描等。这些步骤确保BeanRegistry包含应用程序的相关Bean定义。如果Bean定义受条件(如@Profile)保护,则在此阶段将丢弃这些条件。

因为该模式并不实际创建Bean实例,所以不会调用BeanPostProcessor实现,除了与AOT处理相关的特定变量。它们是:

  • MergedBeanDefinitionPostProcessor实现对Bean定义进行后处理,以提取其他设置,如<代码>初始化 和<代码>销毁 方法。

  • 如果需要,SmartInstantiationAwareBeanPostProcessor实现将确定更精确的Bean类型。这确保在运行时创建所需的任何代理。

此部分完成后,BeanFactory包含应用程序运行所需的Bean定义。它不触发Bean实例化,但允许AOT引擎检查将在运行时创建的Bean。

10.4. Bean Factory Initialization AOT Contributions

希望参与此步骤的组件可以实现BeanFactoryInitializationAotProcessor接口。每个实现都可以根据Bean工厂的状态返回一个AOT贡献。

AOT贡献是一个组件,它贡献重现特定行为的生成代码。它还可以贡献RounmeHints来指示是否需要反射、资源加载、序列化或JDK代理。

BeanFactoryInitializationAotProcessor实现可以在META-INF/Spring/aot.Factory中注册,键等于接口的完全限定名。

BeanFactoryInitializationAotProcessor也可以由Bean直接实现。在此模式下,Bean提供的AOT贡献等同于它使用常规运行时提供的功能。因此,这样的Bean将自动从AOT优化的上下文中排除。

如果某个Bean实现了BeanFactoryInitializationAotProcessor接口,则该Bean及其所有依赖项将在AOT处理过程中被初始化。我们通常建议此接口仅由基础架构Bean实现,例如BeanFactoryPostProcessor,它们具有有限的依赖关系,并且在Bean工厂生命周期的早期就已经被初始化。如果这样的Bean是使用@Bean工厂方法注册的,请确保该方法是静态,这样就不必初始化其封闭的@Configuration类。

10.4.1. Bean Registration AOT Contributions

核心代码实现负责为每个候选<BeanFactoryInitializationAotProcessor>BeanDefinition收集必要的贡献。它使用专用的BeanRegistrationAotProcessor完成此操作。

该接口的用法如下:

  • BeanPostProcessorBean实现,以替换其运行时行为。例如,AutowiredAnnotationBeanPostProcessor实现此接口以生成代码,该代码注入用@Autwire注释的成员。

  • META-INF/Spring/aot.Factory中注册的类型实现,该类型的键等于接口的完全限定名称。通常在需要针对核心框架的特定功能调整Bean定义时使用。

如果一个Bean实现了BeanRegistrationAotProcessor接口,则该Bean及其所有依赖项将在AOT处理期间被初始化。我们通常建议此接口仅由基础架构Bean实现,例如BeanFactoryPostProcessor,它们具有有限的依赖关系,并且在Bean工厂生命周期的早期就已经被初始化。如果这样的Bean是使用@Bean工厂方法注册的,请确保该方法是静态,这样就不必初始化其封闭的@Configuration类。

如果没有BeanRegistrationAotProcessor处理特定的注册Bean,则由默认实现处理它。这是默认行为,因为调优为Bean定义生成的代码应该仅限于最基本的情况。

以前面的示例为例,我们假设DataSourceConfiguration如下所示:

Java
@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

    @Bean
    public SimpleDataSource dataSource() {
        return new SimpleDataSource();
    }

}

             
             

由于此类上没有任何特定条件,因此dataSourceConfigurationDataSource被标识为候选对象。AOT引擎将上述配置类转换为类似于以下内容的代码:

Java
/** * Bean definitions for {@link DataSourceConfiguration} */
public class DataSourceConfiguration__BeanDefinitions {
    /** * Get the bean definition for 'dataSourceConfiguration' */
    public static BeanDefinition getDataSourceConfigurationBeanDefinition() {
        Class<?> beanType = DataSourceConfiguration.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(DataSourceConfiguration::new);
        return beanDefinition;
    }

    /** * Get the bean instance supplier for 'dataSource'. */
    private static BeanInstanceSupplier<SimpleDataSource> getDataSourceInstanceSupplier() {
        return BeanInstanceSupplier.<SimpleDataSource>forFactoryMethod(DataSourceConfiguration.class, "dataSource")
                .withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(DataSourceConfiguration.class).dataSource());
    }

    /** * Get the bean definition for 'dataSource' */
    public static BeanDefinition getDataSourceBeanDefinition() {
        Class<?> beanType = SimpleDataSource.class;
        RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
        beanDefinition.setInstanceSupplier(getDataSourceInstanceSupplier());
        return beanDefinition;
    }
}

             
             
The exact code generated may differ depending on the exact nature of your bean definitions.

上面生成的代码创建了等同于@Configuration类的Bean定义,但是如果可能的话,它会以一种直接的方式创建,并且不使用反射。dataSourceConfiguration有一个Bean定义,dataSourceBean有一个。当需要数据源实例时,将调用BeanInstanceSupplier。此供应商对dataSourceConfigurationBean调用DataSource()方法。

10.5. Runtime Hints

与常规的JVM运行时相比,将应用程序作为本机映像运行需要额外的信息。例如,GraalVM需要提前知道组件是否使用反射。类似地,除非显式指定,否则类路径资源不会在本机映像中提供。因此,如果应用程序需要加载资源,则必须从相应的GraalVM本机映像配置文件中引用该资源。

RounmeHintsAPI在运行时收集对反射、资源加载、序列化和JDK代理的需求。下面的示例确保可以在运行时从本机映像中的类路径加载config/app.properties

Java
runtimeHints.resources().registerPattern("config/app.properties");

            
            

在AOT处理过程中会自动处理多个合同。例如,检查@Controller方法的返回类型,如果Spring检测到该类型应该序列化(通常为JSON),则添加相关的反射提示。

对于核心容器无法推断的情况,您可以通过编程注册此类提示。还为常见用例提供了许多方便的注释。

10.5.1. @ImportRuntimeHints

RounmeHintsRegister实现允许您回调由AOT引擎管理的RunmeHints实例。可以在任何Spring Bean上使用@ImportRunimeHints@Bean工厂方法注册该接口的实现。运行提示注册器实现在生成时被检测和调用。

Java
@Component
@ImportRuntimeHints(MyComponentRuntimeHints.class)
public class MyComponent {

    // ...

    private static class MyComponentRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            // ...
        }
    }
}

             
             

如果可能,@ImportRounmeHints应该尽可能靠近需要提示的组件。这样,如果组件没有提供给BeanFactory,提示也不会提供。

还可以通过在META-INF/Spring/aot.Factory中添加一个条目来静态注册一个实现,该条目的关键字等于RunmeHintsRegister接口的完全限定名称。

10.5.2. @Reflective

@Reflective提供了一种惯用的方法来标记需要在带注释的元素上进行反射。例如,@EventListener使用@Reflective进行元注释,因为底层实现使用反射调用带注释的方法。

默认情况下,只考虑SpringBean,并为带注释的元素注册调用提示。这可以通过@Reflective注释指定一个定制的ReflectiveProcessor实现来进行调优。

库作者可以将该注释用于自己的目的。如果需要处理SpringBean以外的组件,代码可以检测相关类型并使用<BeanFactoryInitializationAotProcessor>ReflectiveRunmeHintsRegister来处理它们。

10.5.3. @RegisterReflectionForBinding

@RegisterReflectionForBinding是<代码>@Reflective 的专门化,它注册了序列化任意类型的需要。一个典型的用例是使用容器无法推断的DTO,例如使用方法体中的Web客户端。

@RegisterReflectionForBinding可以在类级别应用于任何Spring Bean,但也可以直接应用于方法、字段或构造函数,以更好地指示实际需要提示的位置。下面的示例注册帐户以进行序列化。

Java
@Component
public class OrderService {

    @RegisterReflectionForBinding(Account.class)
    public void process(Order order) {
        // ...
    }

}

             
             

11. Appendix

11.1. XML Schemas

附录的这一部分列出了与核心容器相关的XML模式。

11.1.1. The util Schema

顾名思义,util标记处理常见的实用程序配置问题,如配置集合、引用常量等。要使用util架构中的标记,您需要在您的Spring XML配置文件的顶部具有以下前言(代码片段中的文本引用正确的架构,以便您可以使用util命名空间中的标记):

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

        <!-- bean definitions here -->

</beans>
             
             
Using <util:constant/>

考虑以下Bean定义:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>
              
              

前面的配置使用了一个SpringFactoryBean实现(FieldRetrievingFactoryBean),将Bean上的隔离属性的值设置为java.sql.Connection.TRANSACTION_SERIALIZABLE常量的值。这一切都很好,但它很冗长,并且(不必要地)向最终用户公开了Spring的内部管道。

下面的基于XML模式的版本更简洁,清楚地表达了开发人员的意图(“注入这个常量值”),读起来也更好:

<bean id="..." class="...">
    <property name="isolation">
        <util:constant static-field="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
    </property>
</bean>
              
              
Setting a Bean Property or Constructor Argument from a Field Value

FieldRetrievingFactoryBean是一个 FactoryBean,它检索 静态或非静态字段值。它通常用于检索 公共 静态 最终常量,然后可以使用这些常量为另一个Bean设置属性值或构造函数参数。

下面的示例显示如何使用staticField属性公开静态字段:

<bean id="myField" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
    <property name="staticField" value="java.sql.Connection.TRANSACTION_SERIALIZABLE"/>
</bean>
               
               

还有一个方便的用法表单,其中静态字段被指定为Bean名称,如下面的示例所示:

<bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/>
               
               

这确实意味着,对于Beanid是什么不再有任何选择(因此引用它的任何其他Bean也必须使用这个更长的名称),但此形式定义起来非常简洁,并且用作内部Bean非常方便,因为不必为Bean引用指定id,如下面的示例所示:

<bean id="..." class="...">
    <property name="isolation">
        <bean id="java.sql.Connection.TRANSACTION_SERIALIZABLE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean" />
    </property>
</bean>
               
               

您还可以访问另一个Bean的非静态(实例)字段,如FieldRetrievingFactoryBean类的API文档中所述。

在Spring中,将枚举值作为属性或构造函数参数注入Bean很容易。您实际上不需要做任何事情,也不需要了解任何关于Spring内部的东西(甚至不需要了解像FieldRetrievingFactoryBean这样的类)。下面的示例枚举显示注入枚举值是多么容易:

Java
Kotlin
package jakarta.persistence;

public enum PersistenceContextType {

    TRANSACTION,
    EXTENDED
}

               
               

现在考虑以下PersistenceConextType类型的setter和相应的Bean定义:

Java
Kotlin
package example;

public class Client {

    private PersistenceContextType persistenceContextType;

    public void setPersistenceContextType(PersistenceContextType type) {
        this.persistenceContextType = type;
    }
}

               
               
<bean class="example.Client">
    <property name="persistenceContextType" value="TRANSACTION"/>
</bean>
               
               
Using <util:property-path/>

请考虑以下示例:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<bean id="testBean.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
              
              

前面的配置使用了一个SpringFactoryBean实现(PropertyPathFactoryBean)来创建一个名为testBean.age的Bean(类型为int),其值等于TestBeanBean的age属性。

现在考虑以下示例,它添加了一个<;util:Property-Path/>;元素:

<!-- target bean to be referenced by name -->
<bean id="testBean" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 10, which is the value of property 'age' of bean 'testBean' -->
<util:property-path id="name" path="testBean.age"/>
              
              

<;Property-Path/>;元素的路径属性的值遵循beanName.beanProperty的形式。在本例中,它获取名为testBean的Bean的age属性。age属性值为10。

Using <util:property-path/> to Set a Bean Property or Constructor Argument

PropertyPathFactoryBean是计算给定目标对象上的属性路径的FactoryBean。目标对象可以直接指定,也可以通过Bean名称指定。然后,您可以在另一个Bean定义中将该值用作属性值或构造函数参数。

以下示例按名称显示了对另一个Bean使用的路径:

<!-- target bean to be referenced by name -->
<bean id="person" class="org.springframework.beans.TestBean" scope="prototype">
    <property name="age" value="10"/>
    <property name="spouse">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="11"/>
        </bean>
    </property>
</bean>

<!-- results in 11, which is the value of property 'spouse.age' of bean 'person' -->
<bean id="theAge" class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetBeanName" value="person"/>
    <property name="propertyPath" value="spouse.age"/>
</bean>
               
               

在下面的示例中,根据内部Bean计算路径:

<!-- results in 12, which is the value of property 'age' of the inner bean -->
<bean id="theAge" class="org.springframework.beans.factory.config.PropertyPathFactoryBean">
    <property name="targetObject">
        <bean class="org.springframework.beans.TestBean">
            <property name="age" value="12"/>
        </bean>
    </property>
    <property name="propertyPath" value="age"/>
</bean>
               
               

还有一种快捷形式,其中的Bean名称是属性路径。下面的示例显示了快捷表单:

<!-- results in 10, which is the value of property 'age' of bean 'person' -->
<bean id="person.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
               
               

这种形式确实意味着在Bean的名称中没有选择。任何对它的引用也必须使用相同的id,即路径。如果用作内部Bean,则根本不需要引用它,如下面的示例所示:

<bean id="..." class="...">
    <property name="age">
        <bean id="person.age" class="org.springframework.beans.factory.config.PropertyPathFactoryBean"/>
    </property>
</bean>
               
               

您可以在实际定义中专门设置结果类型。对于大多数用例来说,这并不是必需的,但有时它可能很有用。有关此功能的更多信息,请参阅javadoc。

Using <util:properties/>

请考虑以下示例:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<bean id="jdbcConfiguration" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="location" value="classpath:com/foo/jdbc-production.properties"/>
</bean>
              
              

前面的配置使用了一个SpringFactoryBean实现(PropertiesFactoryBean)来实例化一个java.util.Properties实例,该实例的值是从所提供的资源位置加载的。

下面的示例使用util:Properties元素来进行更简洁的表示:

<!-- creates a java.util.Properties instance with values loaded from the supplied location -->
<util:properties id="jdbcConfiguration" location="classpath:com/foo/jdbc-production.properties"/>
              
              
Using <util:list/>

请考虑以下示例:

<!-- creates a java.util.List instance with values loaded from the supplied 'sourceList' -->
<bean id="emails" class="org.springframework.beans.factory.config.ListFactoryBean">
    <property name="sourceList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>
              
              

前面的配置使用了一个SpringFactoryBean实现(ListFactoryBean)来创建一个java.util.List实例,并使用来自所提供的SourceList的值对其进行初始化。

下面的示例使用<;util:list/>;元素来进行更简洁的表示:

<!-- creates a java.util.List instance with the supplied values -->
<util:list id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:list>
              
              

您还可以使用<;util:list/>;元素上的list-class属性显式控制实例化和填充的list的确切类型。例如,如果我们确实需要实例化一个java.util.LinkedList,我们可以使用以下配置:

<util:list id="emails" list-class="java.util.LinkedList">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>d'[email protected]</value>
</util:list>
              
              

如果没有提供list-class属性,则容器选择list实现。

Using <util:map/>

请考虑以下示例:

<!-- creates a java.util.Map instance with values loaded from the supplied 'sourceMap' -->
<bean id="emails" class="org.springframework.beans.factory.config.MapFactoryBean">
    <property name="sourceMap">
        <map>
            <entry key="pechorin" value="[email protected]"/>
            <entry key="raskolnikov" value="[email protected]"/>
            <entry key="stavrogin" value="[email protected]"/>
            <entry key="porfiry" value="[email protected]"/>
        </map>
    </property>
</bean>
              
              

前面的配置使用了一个SpringFactoryBean实现(MapFactoryBean)来创建一个java.util.Map实例,该实例使用来自所提供的‘SourceMap’的键-值对进行初始化。

下面的示例使用<;util:map/>;元素来进行更简洁的表示:

<!-- creates a java.util.Map instance with the supplied key-value pairs -->
<util:map id="emails">
    <entry key="pechorin" value="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>
              
              

您还可以使用<;util:map/>;元素上的‘map-class’属性显式控制实例化和填充的Map的确切类型。例如,如果我们确实需要实例化一个java.util.TreeMap,我们可以使用以下配置:

<util:map id="emails" map-class="java.util.TreeMap">
    <entry key="pechorin" value="[email protected]"/>
    <entry key="raskolnikov" value="[email protected]"/>
    <entry key="stavrogin" value="[email protected]"/>
    <entry key="porfiry" value="[email protected]"/>
</util:map>
              
              

如果没有提供‘map-class’属性,则容器选择Map实现。

Using <util:set/>

请考虑以下示例:

<!-- creates a java.util.Set instance with values loaded from the supplied 'sourceSet' -->
<bean id="emails" class="org.springframework.beans.factory.config.SetFactoryBean">
    <property name="sourceSet">
        <set>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </set>
    </property>
</bean>
              
              

前面的配置使用了一个SpringFactoryBean实现(SetFactoryBean)来创建一个java.util.Set实例,该实例使用来自所提供的SourceSet的值进行初始化。

下面的示例使用<;util:set/>;元素来进行更简洁的表示:

<!-- creates a java.util.Set instance with the supplied values -->
<util:set id="emails">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>
              
              

您还可以使用<;util:set/>;元素上的set-class属性显式控制实例化和填充的set的确切类型。例如,如果我们确实需要实例化一个java.util.TreeSet,我们可以使用以下配置:

<util:set id="emails" set-class="java.util.TreeSet">
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
    <value>[email protected]</value>
</util:set>
              
              

如果没有提供set-class属性,则容器选择一个set实现。

11.1.2. The aop Schema

AOP标记处理在Spring中配置所有AOP,包括Spring自己的基于代理的AOP框架以及Spring与AspectJ AOP框架的集成。这些标记在题为使用Spring进行面向方面编程的章节中有全面介绍。

为了完整起见,要使用AOP模式中的标记,您需要在您的Spring XML配置文件的顶部有以下前言(代码片段中的文本引用正确的模式,以便AOP命名空间中的标记可供您使用):

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

    <!-- bean definitions here -->

</beans>
             
             

11.1.3. The context Schema

上下文标记处理与管道 - 相关的ApplicationContext配置,也就是说,通常不是对最终用户重要的Bean,而是在Spring中执行大量“繁琐”工作的Bean,例如BeanfactoryPostProcessors。以下代码片段引用了正确的架构,以便您可以使用上下文命名空间中的元素:

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

    <!-- bean definitions here -->

</beans>
             
             
Using <property-placeholder/>

此元素激活${…>的替换​} 占位符,根据指定的属性文件(作为Spring资源位置)进行解析。该元素是一种为您设置PropertySourcesPlaceholderConfigurer的便利机制。如果您需要更多地控制特定的PropertySourcesPlaceholderConfigurer设置,您可以自己显式地将其定义为Bean。

Using <annotation-config/>

该元素激活了Spring基础设施,以检测Bean类中的注释:

  • Spring的@配置模型

  • @Autowired/@Inject,<代码>@值 和<代码>@查找

  • JSR-250的@Resource@PostConstruct@PreDestroy(如果可用)

  • JAX-WS的@WebServiceRef和EJB3的@ejb(如果可用)

  • JPA的@PersistenceContext@PersistenceUnit(如果可用)

  • Spring的@EventListener

或者,您可以选择为这些批注显式激活单个BeanPostProcessors

This element does not activate processing of Spring’s @Transactional annotation; you can use the <tx:annotation-driven/> element for that purpose. Similarly, Spring’s caching annotations need to be explicitly enabled as well.
Using <component-scan/>

该元素在基于注释的容器配置一节中有详细介绍。

Using <load-time-weaver/>

这个元素在在Spring框架中使用AspectJ加载时编织一节中有详细介绍。

Using <spring-configured/>

该元素在使用AspectJ通过Spring依赖注入域对象一节中有详细介绍。

Using <mbean-export/>

该元素在配置基于注释的MBean导出一节中有详细介绍。

11.1.4. The Beans Schema

最后但并非最不重要的一点是,我们有Bean模式中的元素。这些元素从框架诞生之日起就一直存在于Spring中。这里没有显示Bean架构中各种元素的示例,因为依赖关系和配置中详细介绍了它们(实际上,在整个一章中)。

请注意,您可以向<;Bean/>;XML定义添加零个或多个键-值对。如何处理这些额外的元数据完全取决于您自己的定制逻辑(因此,通常只有当您编写自己的定制元素时才有用,如标题为XML模式创作的附录中所述)。

下面的示例显示了<;meta/>;上下文中的<;meta/>;元素(请注意,如果没有任何逻辑来解释它,元数据实际上毫无用处)。

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

    <bean id="foo" class="x.y.Foo">
        <meta key="cacheName" value="foo"/> (1)
        <property name="name" value="Rick"/>
    </bean>

</beans>
             
             
1 This is the example meta element

在前面的示例中,您可以假设有一些使用Bean定义的逻辑,并设置了一些使用所提供的元数据的缓存基础设施。

11.2. XML Schema Authoring

从2.0版开始,Spring就提供了一种机制,用于向基本的Spring XML格式添加基于模式的扩展,以定义和配置Bean。本节介绍如何编写您自己的定制XML Bean定义解析器,并将这些解析器集成到Spring IOC容器中。

为了便于编写使用支持模式的XML编辑器的配置文件,Spring的可扩展XML配置机制基于XML模式。如果您不熟悉标准Spring发行版附带的Spring当前的XML配置扩展,您应该首先阅读关于XML模式的上一节。

要创建新的XML配置扩展:

  1. 作者描述您的自定义元素的XML架构。

  2. Code自定义NamespaceHandler实现。

  3. 代码一个或多个BeanDefinitionParser实现(这是完成实际工作的地方)。

  4. 向Spring注册您的新构件。

作为一个统一的示例,我们创建了一个XML扩展(一个定制的XML元素),它允许我们配置SimpleDateFormat类型的对象(来自java.text包)。完成后,我们将能够定义SimpleDateFormat类型的Bean定义,如下所示:

<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
            
            

(我们将在本附录后面包括更详细的示例。(第一个简单示例的目的是引导您完成创建自定义扩展的基本步骤。)

11.2.1. Authoring the Schema

创建与Spring的IoC容器一起使用的XML配置扩展首先要编写一个XML模式来描述扩展。对于我们的示例,我们使用以下架构来配置SimpleDateFormat对象:

<!-- myns.xsd (inside package org/springframework/samples/xml) -->

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.mycompany.example/schema/myns" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:beans="http://www.springframework.org/schema/beans" targetNamespace="http://www.mycompany.example/schema/myns" elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:element name="dateformat">
        <xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="beans:identifiedType"> (1)
                    <xsd:attribute name="lenient" type="xsd:boolean"/>
                    <xsd:attribute name="pattern" type="xsd:string" use="required"/>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>
             
             
1 The indicated line contains an extension base for all identifiable tags (meaning they have an id attribute that we can use as the bean identifier in the container). We can use this attribute because we imported the Spring-provided beans namespace.

前面的架构允许我们使用<;myns:DateFormat/>;元素在XML应用程序上下文文件中直接配置SimpleDateFormat对象,如下面的示例所示:

<myns:dateformat id="dateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/>
             
             

请注意,在我们创建了基础结构类之后,前面的XML片段基本上与下面的XML片段相同:

<bean id="dateFormat" class="java.text.SimpleDateFormat">
    <constructor-arg value="yyyy-MM-dd HH:mm"/>
    <property name="lenient" value="true"/>
</bean>
             
             

前面两个片段中的第二个在容器(由SimpleDateFormat类型的名称DateFormat标识)中创建一个具有两个属性集的Bean。

The schema-based approach to creating configuration format allows for tight integration with an IDE that has a schema-aware XML editor. By using a properly authored schema, you can use autocompletion to let a user choose between several configuration options defined in the enumeration.

11.2.2. Coding a NamespaceHandler

除了模式之外,我们还需要NamespaceHandler来解析Spring在解析配置文件时遇到的这个特定名称空间的所有元素。对于本例,NamespaceHandler应该负责myns:日期格式元素的解析。

NamespaceHandler接口有三个方法:

  • init():允许初始化NamespaceHandler,在使用处理程序之前由Spring调用。

  • BeanDefinition parse(Element,ParserContext):当Spring遇到顶级元素(不嵌套在Bean定义或其他命名空间中)时调用。此方法本身可以注册Bean定义、返回Bean定义,或者两者兼而有之。

  • BeanDefinitionHolder Decorate(Node,BeanDefinitionHolder,ParserContext):当Spring遇到不同命名空间的属性或嵌套元素时调用。一个或多个Bean定义的修饰(例如)与Spring支持的作用域一起使用。我们从突出一个简单的例子开始,不使用装饰,然后我们用一个更高级的例子展示装饰。

尽管您可以为整个名称空间编写自己的NamespaceHandler(因此提供解析名称空间中的每个元素的代码),但通常情况下,Spring XML配置文件中的每个顶级XML元素都会产生一个Bean定义(就像我们的例子一样,一个<;myns:DateFormat元素会产生一个SimpleDateFormatBean定义)。Spring提供了许多支持此场景的便利类。在下面的示例中,我们使用NamespaceHandlerSupport类:

Java
Kotlin
package org.springframework.samples.xml; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class MyNamespaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser()); } } 
             
             

您可能会注意到,这个类中实际上并没有很多解析逻辑。事实上,NamespaceHandlerSupport类有一个内置的委托概念。它支持注册任意数量的BeanDefinitionParser实例,在需要解析其名称空间中的元素时委托给这些实例。这种清晰的关注点分离允许NamespaceHandler处理其名称空间中所有定制元素的解析的编排,同时委托BeanDefinitionParser执行繁琐的XML解析工作。这意味着每个BeanDefinitionParser只包含解析单个定制元素的逻辑,如我们在下一步中所见。

11.2.3. Using BeanDefinitionParser

如果NamespaceHandler遇到映射到特定Bean定义解析器的类型的XML元素(本例中为DateFormat),则使用BeanDefinitionParser。换句话说,BeanDefinitionParser负责解析模式中定义的一个不同的顶级XML元素。在解析器中,我们可以访问XML元素(因此也可以访问它的子元素),这样我们就可以解析我们的定制XML内容,如下例所示:

Java
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; 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 We use the Spring-provided AbstractSingleBeanDefinitionParser to handle a lot of the basic grunt work of creating a single BeanDefinition.
2 We supply the AbstractSingleBeanDefinitionParser superclass with the type that our single BeanDefinition represents.

在这个简单的例子中,这就是我们需要做的全部工作。我们的单个<AbstractSingleBeanDefinitionParser>BeanDefinition的创建由Bean超类处理,提取和设置Bean定义的唯一标识符也是如此。

11.2.4. Registering the Handler and the Schema

编码完成了。剩下要做的就是让Spring XML解析基础设施意识到我们的定制元素。为此,我们在两个特殊用途的属性文件中注册了定制的名称空间处理程序和定制的XSD文件。这些属性文件都放在应用程序中的META-INF目录中,例如,可以与JAR文件中的二进制类一起分发。通过使用这些特殊的属性文件,Spring XML解析基础设施会自动获取您的新扩展,其格式将在接下来的两节中详细介绍。

Writing META-INF/spring.handlers

名为spring.handters的属性文件包含从XML模式URI到名称空间处理程序类的映射。对于我们的示例,我们需要编写以下代码:

http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler

(字符是Java属性格式的有效分隔符,因此URI中的字符需要用反斜杠转义。)

键-值对的第一部分(键)是与定制名称空间扩展相关联的URI,并且需要与定制XSD架构中指定的Target Namesspace属性的值完全匹配。

Writing 'META-INF/spring.schemas'

名为spring.schemas的属性文件包含将模式用作xsi:schemaLocation属性的一部分的XML文件中的XML模式位置(连同模式声明一起引用)到类路径资源的映射。这个文件是为了防止Spring绝对必须使用默认的EntityResolver来检索架构文件,该默认EntityResolver需要访问Internet。如果您在此属性文件中指定映射,则Spring将搜索类路径上的模式(在本例中为org.springFramework.samples.xml包中的myns.xsd)。以下代码片段显示了我们需要为自定义架构添加的代码行:

http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd

(请记住,字符必须转义。)

建议您将XSD文件(或多个文件)与类路径上的NamespaceHandlerBeanDefinitionParser类一起部署。

11.2.5. Using a Custom Extension in Your Spring XML Configuration

使用您自己实现的自定义扩展与使用Spring提供的一个“自定义”扩展没有什么不同。下面的示例使用在Spring XML配置文件中的上述步骤中开发的自定义<;日期格式/&>元素:

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

    <!-- as a top-level bean -->
    <myns:dateformat id="defaultDateFormat" pattern="yyyy-MM-dd HH:mm" lenient="true"/> (1)

    <bean id="jobDetailTemplate" abstract="true">
        <property name="dateFormat">
            <!-- as an inner bean -->
            <myns:dateformat pattern="HH:mm MM-dd-yyyy"/>
        </property>
    </bean>

</beans>
             
             
1 Our custom bean.

11.2.6. More Detailed Examples

本节提供一些更详细的自定义XML扩展示例。

Nesting Custom Elements within Custom Elements

本节中提供的示例显示了如何编写满足以下配置的目标所需的各种构件:

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

    <foo:component id="bionic-family" name="Bionic-1">
        <foo:component name="Mother-1">
            <foo:component name="Karate-1"/>
            <foo:component name="Sport-1"/>
        </foo:component>
        <foo:component name="Rock-1"/>
    </foo:component>

</beans>
              
              

前面的配置相互嵌套自定义扩展。<;foo:Component/>;元素实际配置的类是Component类(如下例所示)。注意组件类如何不公开组件属性的setter方法。这使得通过使用setter注入为组件类配置Bean定义变得困难(或者更确切地说,不可能)。下面的清单显示了组件类:

Java
Kotlin
package com.foo; import java.util.ArrayList; import java.util.List; public class Component { private String name; private List<Component> components = new ArrayList<Component> (); // mmm, there is no setter method for the 'components' public void addComponent(Component component) { this.components.add(component); } public List<Component> getComponents() { return components; } public String getName() { return name; } public void setName(String name) { this.name = name; } } 
              
              

此问题的典型解决方案是创建自定义FactoryBean,它公开组件属性的setter属性。下面的清单显示了这样一个定制的FactoryBean

Java
Kotlin
package com.foo; import org.springframework.beans.factory.FactoryBean; import java.util.List; public class ComponentFactoryBean implements FactoryBean<Component> { private Component parent; private List<Component> children; public void setParent(Component parent) { this.parent = parent; } public void setChildren(List<Component> 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<Component> getObjectType() { return Component.class; } public boolean isSingleton() { return true; } } 
              
              

这可以很好地工作,但它向最终用户公开了大量的Spring管道。我们要做的是编写一个自定义扩展来隐藏所有的Spring管道。如果我们坚持前面描述的步骤,我们首先创建XSD模式来定义定制标记的结构,如下面的清单所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.example/schema/component" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.foo.example/schema/component" elementFormDefault="qualified" attributeFormDefault="unqualified">

    <xsd:element name="component">
        <xsd:complexType>
            <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element ref="component"/>
            </xsd:choice>
            <xsd:attribute name="id" type="xsd:ID"/>
            <xsd:attribute name="name" use="required" type="xsd:string"/>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>
              
              

同样在前面描述的过程之后,我们创建一个定制的NamespaceHandler

Java
Kotlin
package com.foo; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class ComponentNamespaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser()); } } 
              
              

接下来是定制的BeanDefinitionParser。请记住,我们正在创建描述ComponentFactoryBeanBeanDefinition。下面的清单显示了我们的定制BeanDefinitionParser实现:

Java
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; 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<Element> 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<Element> childElements, BeanDefinitionBuilder factory) { ManagedList<BeanDefinition> children = new ManagedList<BeanDefinition>(childElements.size()); for (Element element : childElements) { children.add(parseComponentElement(element)); } factory.addPropertyValue("children", children); } } 
              
              

最后,需要向Spring XML基础设施注册各种构件,方法是修改META-INF/spring.handtersMETA-INF/spring.schemas文件,如下所示:

# 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
Custom Attributes on “Normal” Elements

编写您自己的定制解析器和相关的构件并不难。然而,这有时并不是正确的做法。考虑这样一个场景,您需要向现有的Bean定义中添加元数据。在这种情况下,您当然不希望编写自己的整个自定义扩展。相反,您只需要向现有的Bean定义元素添加一个额外的属性。

举另一个例子,假设您为一个服务对象定义了一个Bean定义,该服务对象(对于它来说是未知的)访问集群JCache,并且您想要确保命名的JCache实例在周围的集群中急切地启动。下面的清单显示了这样的定义:

<bean id="checkingAccountService" class="com.foo.DefaultCheckingAccountService" jcache:cache-name="checking.account">
    <!-- other dependencies here... -->
</bean>
              
              

然后,我们可以在解析‘jcache:cache-name’属性时创建另一个BeanDefinition。这个BeanDefinition然后为我们初始化命名的JCache。我们还可以为‘check ingAccount tService’修改现有的BeanDefinition,以使其依赖于这个新的JCache-初始化BeanDefinition。以下清单显示了我们的JCacheInitializer

Java
Kotlin
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...
    }
}

              
              

现在我们可以转到定制扩展了。首先,我们需要创作描述定制属性的XSD架构,如下所示:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://www.foo.example/schema/jcache" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.foo.example/schema/jcache" elementFormDefault="qualified">

    <xsd:attribute name="cache-name" type="xsd:string"/>

</xsd:schema>
              
              

接下来,我们需要创建关联的NamespaceHandler,如下所示:

Java
Kotlin
package com.foo; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; public class JCacheNamespaceHandler extends NamespaceHandlerSupport { public void init() { super.registerBeanDefinitionDecoratorForAttribute("cache-name", new JCacheInitializingBeanDefinitionDecorator()); } } 
              
              

接下来,我们需要创建解析器。请注意,在本例中,因为我们要解析一个XML属性,所以我们编写了BeanDefinitionDecorator而不是BeanDefinitionParser。下面的清单显示了我们的BeanDefinitionDecorator实现:

Java
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; 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; } } 
              
              

最后,我们需要通过修改META-INF/spring.handtersMETA-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

11.3. Application Startup Steps

附录的这一部分列出了用于检测核心容器的现有StartupSteps

The name and detailed information about each startup step is not part of the public contract and is subject to change; this is considered as an implementation detail of the core container and will follow its behavior changes.
Table 15. Application startup steps defined in the core container
Name Description Tags

spring.beans.instantiate

Bean及其依赖项的实例化。

beanNameBean的名称,beanType注入点所需的类型。

spring.beans.Smart-初始化

初始化SmartInitializingSingletonBean。

beanNameBean的名称。

spring.context.annotated-bean-reader.create

创建AnnotatedBeanDefinitionReader

spring.context.base-packages.scan

扫描基本包裹。

用于扫描的基本包的数组。

spring.context.beans.post-process

Bean后处理阶段。

spring.context.bean-factory.post-process

调用BeanFactoryPostProcessorBean。

后处理器当前后处理器。

spring.context.beandef-registry.post-process

调用BeanDefinitionRegistryPostProcessorBean。

后处理器当前后处理器。

spring.context.component-classes.register

通过AnnotationConfigApplicationContext#register.注册组件类

Class用于注册的给定类的数组。

spring.context.config-classes.enhance

使用CGLIB代理增强配置类。

classCount增强类的计数。

spring.context.config-classes.parse

使用ConfigurationClassPostProcessor的配置类分析阶段。

classCount已处理类的计数。

spring.context.renh

应用程序上下文刷新阶段。