Spring - Core

接着上文

3.验证,数据绑定和类型转换

考虑将验证作为业务逻辑是有利有弊,Spring提供了一种验证(和数据绑定)设计,但并不排除其中任何一个。 具体来说,验证不应与Web层绑定,并且应该易于本地化,并且应该可以插入任何可用的验证器。 考虑到这些问题,Spring提供了一个Validator合同,该合同既基本又可以在应用程序的每个层中使用。

数据绑定对于使用户输入动态绑定到应用程序的域模型(或用于处理用户输入的任何对象)非常有用。 Spring提供了恰当地命名为DataBinder的功能。 Validator和DataBinder组成了验证包,该验证包主要用于但不限于Web层

BeanWrapper是Spring框架中的基本概念,并在很多地方使用。 但是,您可能不需要直接使用BeanWrapper。 但是,因为这是参考文档,所以我们认为可能需要进行一些解释。 我们将在本章中解释BeanWrapper,因为如果您要使用它,那么在尝试将数据绑定到对象时最有可能使用它。

Spring的DataBinder和较低级别的BeanWrapper都使用PropertyEditorSupport实现来解析和格式化属性值。 PropertyEditor和PropertyEditorSupport类型是JavaBeans规范的一部分,本章还将对此进行说明。 Spring 3引入了core.convert包,该包提供了常规的类型转换工具,以及用于格式化UI字段值的高级“ format”包。 您可以将这些包用作PropertyEditorSupport实现的更简单替代方案。 本章还将对它们进行讨论。

Spring通过设置基础结构和Spring自己的Validator合同的适配器来支持Java Bean验证。 应用程序可以全局启用一次Bean验证,如Java Bean验证中所述,并将其专用于所有验证需求。 在Web层中,应用程序可以每个DataBinder进一步注册控制器本地的Spring Validator实例,如配置DataBinder中所述,这对于插入自定义验证逻辑很有用。

3.1 使用Spring的Validator界面进行验证

Spring具有Validator接口,可用于验证对象。 Validator接口通过使用Errors对象来工作,以便验证器在验证时可以将验证失败报告给Errors对象。

public class Person {private String name;private int age;// the usual getters and setters...
}

下一个示例通过实现org.springframework.validation.Validator接口的以下两个方法来提供Person类的验证行为:

support(Class):此验证程序可以验证提供的Class的实例吗?

validate(Object,org.springframework.validation.Errors):验证给定的对象,并在发生验证错误的情况下,向给定的Errors对象注册这些对象。

实施Validator非常简单,尤其是当您知道Spring Framework也提供的ValidationUtils帮助器类时。 以下示例实现了用于Person实例的Validator:

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或为空字符串)。 看看ValidationUtils javadoc,看看它除了提供前面显示的示例外还提供什么功能。

尽管可以实现单个Validator类来验证丰富对象中的每个嵌套对象,但最好将每个嵌套对象类的验证逻辑封装在其自己的Validator实现中。 一个“丰富”对象的简单示例是一个Customer,它由两个String属性(第一个和第二个名称)和一个复杂的Address对象组成。 地址对象可以独立于客户对象使用,因此已实现了不同的AddressValidator。 如果希望CustomerValidator重用AddressValidator类中包含的逻辑而不求助于复制粘贴,则可以在CustomerValidator中依赖注入或实例化一个AddressValidator,如以下示例所示:

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

验证错误将报告给传递给验证器的Errors对象。 对于Spring Web MVC,可以使用<spring:bind />标记检查错误消息,但是也可以自己检查Errors对象。 关于它提供的方法的更多信息可以在javadoc中找到。

3.2 将代码解析为错误消息

我们介绍了数据绑定和验证。 本节介绍与验证错误相对应的输出消息。 在上一节显示的示例中,我们拒绝了名称和年龄字段。 如果要使用MessageSource输出错误消息,可以使用拒绝字段时提供的错误代码(在这种情况下为“名称”和“年龄”)来进行输出。 当您从Errors接口调用(直接或间接地,例如通过使用ValidationUtils类)rejectValue或其他拒绝方法之一时,基础实现不仅注册您传入的代码,还注册许多 其他错误代码。 MessageCodesResolver确定Errors接口寄存器中的哪个错误代码。 默认情况下,使用DefaultMessageCodesResolver,它(例如)不仅使用您提供的代码注册消息,而且还注册包含传递给拒绝方法的字段名称的消息。 因此,如果您通过使用rejectValue(“ age”,“ too.darn.old”)拒绝字段,除了too.darn.old代码外,Spring还会注册too.darn.old.age和too.darn.old .age.int(第一个包含字段名称,第二个包含字段类型)。 这样做是为了方便开发人员在定位错误消息时提供帮助。

有关MessageCodesResolver和默认策略的更多信息,可以分别在MessageCodesResolver和DefaultMessageCodesResolver的javadoc中找到。

3.3 Bean操作和BeanWrapper

org.springframework.beans包遵循JavaBeans标准.avaBean是具有默认无参数构造函数的类,并且遵循命名约定,在该命名约定下,例如,名为bingoMadness的属性将具有setter方法setBingoMadness(…)和getter方法getBingoMadness()。 有关JavaBean和规范的更多信息,请参见javabeans。

Bean包中的一个非常重要的类是BeanWrapper接口及其相应的实现(BeanWrapperImpl)。 正如从Javadoc引用的那样,BeanWrapper提供了以下功能:设置和获取(单独或批量)属性值,获取属性描述符以及查询属性以确定它们是否可读或可写.此外,BeanWrapper还支持嵌套属性,从而可以将子属性上的属性设置为无限深度。 BeanWrapper还支持添加标准JavaBeans PropertyChangeListeners和VetoableChangeListeners的功能,而无需在目标类中支持代码。 最后但并非最不重要的一点是,BeanWrapper支持设置索引属性.BeanWrapper通常不直接由应用程序代码使用,而是由DataBinder和BeanFactory使用。

BeanWrapper的工作方式部分由其名称表示:它包装一个Bean,以对该Bean执行操作,例如设置和检索属性。

3.3.1 设置和获取基本和嵌套属性

设置和获取属性是通过BeanWrapper的setPropertyValue和getPropertyValue重载方法变体完成的。 有关详细信息,请参见其Javadoc。 下表显示了这些约定的一些示例:

Expression Explanation
name 指示与getName()或isName()和setName(…)方法相对应的属性名称。
account.name 指示与(例如)getAccount()。setName()或getAccount()。getName()方法相对应的属性帐户的嵌套属性名称。
account[2] 指示索引属性帐户的第三个元素。 索引属性的类型可以是数组,列表或其他自然排序的集合。
account[COMPANYNAME] 表示由帐户Map属性的COMPANYNAME键索引的MAP条目的值

(如果您不打算直接使用BeanWrapper,那么下一部分对您而言并不是至关重要的。如果仅使用DataBinder和BeanFactory及其默认实现,则应该跳到PropertyEditors的这一部分。)

以下两个示例类使用BeanWrapper来获取和设置属性

ublic 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;}
}
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;}
}

以下代码段显示了一些有关如何检索和操纵实例化的公司和雇员的某些属性的示例:

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 内置的PropertyEditor实现

Spring使用PropertyEditor的概念来实现对象和字符串之间的转换。 以不同于对象本身的方式表示属性可能很方便。 例如,日期可以用人可读的方式表示(如字符串:‘2007-14-09’),而我们仍然可以将人类可读的形式转换回原始日期(或者更好的是,转换任何日期 以人类可读的形式输入到Date对象)。 通过注册类型为java.beans.PropertyEditor的自定义编辑器,可以实现此行为。 在BeanWrapper上或在特定的IoC容器中注册自定义编辑器(如上一章所述),使它具有如何将属性转换为所需类型的知识。 有关PropertyEditor的更多信息,请参见Oracle的java.beans包的javadoc

在Spring中使用属性编辑的两个示例:

  • 通过使用PropertyEditor实现在bean上设置属性。 当使用String作为在XML文件中声明的某些bean的属性的值时,Spring(如果相应属性的设置器具有Class参数)将使用ClassEditor尝试将参数解析为Class对象。

  • 在Spring的MVC框架中,通过使用各种PropertyEditor实现来解析HTTP请求参数,您可以在CommandController的所有子类中手动绑定这些实现。

Spring具有许多内置的PropertyEditor实现,以简化周期。 它们都位于org.springframework.beans.propertyeditors包中。 默认情况下,大多数(但不是全部,如下表所示)由BeanWrapperImpl注册。 如果可以通过某种方式配置属性编辑器,则仍可以注册自己的变体以覆盖默认变体。 下表描述了Spring提供的各种PropertyEditor实现:

表12.内置的PropertyEditor实现

Class Explanation
ByteArrayPropertyEditor 字节数组的编辑器。 将字符串转换为其相应的字节表示形式。 默认情况下由BeanWrapperImpl注册。
ClassEditor 将代表类的字符串解析为实际类,反之亦然。 当找不到类时,将抛出IllegalArgumentException。 默认情况下,由BeanWrapperImpl注册
CustomBooleanEditor 布尔属性的可定制属性编辑器。 默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor 布尔属性的可定制属性编辑器。 默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomDateEditor java.util.Date的可自定义属性编辑器,支持自定义DateFormat。 默认未注册。 必须根据需要以适当的格式进行用户注册。
CustomNumberEditor 任何Number子类(例如Integer,Long,Float或Double)的可自定义属性编辑器。 默认情况下,由BeanWrapperImpl注册,但是可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor 将字符串解析为java.io.File对象。 默认情况下,由BeanWrapperImpl注册。
InputStreamEditor 单向属性编辑器,它可以采用字符串并生成(通过中间的ResourceEditor和Resource)一个InputStream,以便可以将InputStream属性直接设置为字符串。 请注意,默认用法不会为您关闭InputStream。 默认情况下,由BeanWrapperImpl注册。
LocaleEditor 可以将字符串解析为Locale对象,反之亦然(字符串格式为[country] [variant],与Locale的toString()方法相同)。 默认情况下,由BeanWrapperImpl注册。
PatternEditor 可以将字符串解析为java.util.regex.Pattern对象,反之亦然。
PropertiesEditor 可以将字符串(以java.util.Properties类的javadoc中定义的格式格式化)转换为Properties对象。 默认情况下,由BeanWrapperImpl注册。
StringTrimmerEditor 修剪字符串的属性编辑器。 (可选)允许将空字符串转换为空值。 默认情况下未注册—必须是用户注册的。
URLEditor 可以将URL的字符串表示形式解析为实际的URL对象。 默认情况下,由BeanWrapperImpl注册。

Spring使用java.beans.PropertyEditorManager设置可能需要的属性编辑器的搜索路径。 搜索路径还包括sun.bean.editors,其中包括针对诸如Font,Color和大多数基本类型的类型的PropertyEditor实现。 还要注意,如果标准JavaBeans基础结构与它们处理的类在同一包中,并且与该类具有相同的名称,并附加了Editor,则标准JavaBeans基础结构将自动发现PropertyEditor类(无需显式注册它们)。 例如,可能具有以下类和包结构,足以使SomethingEditor类被识别并用作Something类型的属性的PropertyEditor。

comchankpopSomethingSomethingEditor // the PropertyEditor for the Something class

注意,您也可以在此处使用标准的BeanInfo JavaBeans机制(在某种程度上进行了描述)。 以下示例使用BeanInfo机制使用关联类的属性显式注册一个或多个PropertyEditor实例:

 comchankpopSomethingSomethingBeanInfo // the BeanInfo for the Something class

所引用的SomethingBeanInfo类的以下Java源代码将CustomNumberEditor与Something类的age属性相关联:

public class SomethingBeanInfo extends SimpleBeanInfo {public PropertyDescriptor[] getPropertyDescriptors() {try {final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true);PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) {public PropertyEditor createPropertyEditor(Object bean) {return numberPE;};};return new PropertyDescriptor[] { ageDescriptor };}catch (IntrospectionException ex) {throw new Error(ex.toString());}}
}

注册其他自定义PropertyEditor实现

当将bean属性设置为字符串值时,Spring IoC容器最终使用标准JavaBeans PropertyEditor实现将这些字符串转换为属性的复杂类型。 Spring预注册了许多自定义的PropertyEditor实现(例如,将表示为字符串的类名称转换为Class对象)。 此外,Java的标准JavaBeans PropertyEditor查找机制允许适当地命名类的PropertyEditor,并将其与提供支持的类放在同一包中,以便可以自动找到它。

如果需要注册其他自定义PropertyEditor,则可以使用几种机制。 最手动的方法(通常不方便或不建议使用)是使用ConfigurableBeanFactory接口的registerCustomEditor()方法,假设您有BeanFactory引用。 另一种(稍微方便些)的机制是使用一种称为CustomEditorConfigurer的特殊bean工厂后处理器。 尽管您可以将Bean工厂后处理器与BeanFactory实现一起使用,但CustomEditorConfigurer具有嵌套的属性设置,因此我们强烈建议您将其与ApplicationContext一起使用,在这里可以将其以与其他任何Bean相似的方式进行部署,并且可以在任何位置进行部署。 自动检测并应用。

请注意,所有的bean工厂和应用程序上下文通过使用BeanWrapper来处理属性转换,都会自动使用许多内置的属性编辑器。 上一节列出了BeanWrapper注册的标准属性编辑器。 此外,ApplicationContext还以适合特定应用程序上下文类型的方式重写或添加其他编辑器,以处理资源查找。

标准JavaBeans PropertyEditor实例用于将以字符串表示的属性值转换为该属性的实际复杂类型。 您可以使用bean工厂的后处理器CustomEditorConfigurer来方便地将对其他PropertyEditor实例的支持添加到ApplicationContext。

考虑以下示例,该示例定义了一个名为ExoticType的用户类和另一个名为DependsOnExoticType的类,该类需要将ExoticType设置为属性:

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实现可能类似于以下内容:

// converts string representation to ExoticType object
package example;public class ExoticTypeEditor extends PropertyEditorSupport {public void setAsText(String text) {setValue(new ExoticType(text.toUpperCase()));}
}

最后,下面的示例演示如何使用CustomEditorConfigurer向ApplicationContext注册新的PropertyEditor,然后可以根据需要使用它:

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer"><property name="customEditors"><map><entry key="example.ExoticType" value="example.ExoticTypeEditor"/></map></property>
</bean>

使用PropertyEditorRegistrar

使用Spring容器注册属性编辑器的另一种机制是创建和使用PropertyEditorRegistrar。 当需要在几种不同情况下使用同一组属性编辑器时,此界面特别有用。 您可以编写相应的注册商,并在每种情况下重复使用它。 PropertyEditorRegistrar实例与一个名为PropertyEditorRegistry的接口一起工作,该接口由Spring BeanWrapper(和DataBinder)实现。 当与CustomEditorConfigurer(在此描述)结合使用时,PropertyEditorRegistrar实例特别方便,后者公开了一个名为setPropertyEditorRegistrars(…)的属性。 以这种方式添加到CustomEditorConfigurer的PropertyEditorRegistrar实例可以轻松地与DataBinder和Spring MVC控制器共享。 此外,它避免了在自定义编辑器上进行同步的需求:希望PropertyEditorRegistrar为每次创建bean的尝试创建新的PropertyEditor实例。

以下示例说明如何创建自己的PropertyEditorRegistrar实现:

package com.foo.editors.spring;public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {public void registerCustomEditors(PropertyEditorRegistry registry) {// it is expected that new PropertyEditor instances are createdregistry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor());// you could register as many custom property editors as are required here...}
}

另请参阅org.springframework.beans.support.ResourceEditorRegistrar以获取示例PropertyEditorRegistrar实现。 注意,在实现registerCustomEditors(…)方法时,它如何创建每个属性编辑器的新实例。

下一个示例显示了如何配置CustomEditorConfigurer并将其注入我们的CustomPropertyEditorRegistrar的实例:

<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框架的读者来说,与本章的重点有所偏离),将PropertyEditorRegistrars与数据绑定控制器(例如SimpleFormController)结合使用会非常方便。 下面的示例在initBinder(…)方法的实现中使用PropertyEditorRegistrar:

public final class RegisterUserController extends SimpleFormController {private final PropertyEditorRegistrar customPropertyEditorRegistrar;public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) {this.customPropertyEditorRegistrar = propertyEditorRegistrar;}protected void initBinder(HttpServletRequest request,ServletRequestDataBinder binder) throws Exception {this.customPropertyEditorRegistrar.registerCustomEditors(binder);}// other methods to do with registering a User
}

这种PropertyEditor注册样式可以导致代码简洁(initBinder(…)的实现只有一行长),并且可以将通用的PropertyEditor注册代码封装在一个类中,然后根据需要在许多Controller之间共享。

3.4 Spring类型转换

Spring 3引入了core.convert包,该包提供了通用的类型转换系统。 系统定义了一个用于实现类型转换逻辑的SPI和一个用于在运行时执行类型转换的API。 在Spring容器中,可以使用此系统作为PropertyEditor实现的替代方法,以将外部化的bean属性值字符串转换为所需的属性类型。 您还可以在应用程序中需要类型转换的任何地方使用公共API。

3.4.1 转换器SPI

package org.springframework.core.convert.converter;public interface Converter<S, T> {T convert(S source);
}

要创建自己的转换器,请实现Converter接口并将S设置为要转换的类型,并将T设置为要转换的类型。 如果还需要注册一个委托数组或集合转换器(默认情况下DefaultConversionService会这样做),则也可以透明地应用此类转换器,如果需要将S的集合或数组转换为T的数组或集合。

对于每次对convert(S)的调用,保证源参数不为null。 如果转换失败,您的转换器可能会引发任何未经检查的异常。 具体来说,它应该抛出IllegalArgumentException以报告无效的源值。 注意确保您的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 使用ConverterFactory

当需要集中整个类层次结构的转换逻辑时(例如,从String转换为Enum对象时),可以实现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是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 使用GenericConverter

当您需要复杂的Converter实现时,请考虑使用GenericConverter接口。 与Converter相比,GenericConverter具有比Converter更灵活但强度不高的签名,支持在多种源类型和目标类型之间进行转换。 另外,当实现转换逻辑时,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()返回支持的源→目标类型对。 然后实现convert(Object,TypeDescriptor,TypeDescriptor)包含您的转换逻辑。 源TypeDescriptor提供对包含正在转换的值的源字段的访问。 通过目标TypeDescriptor,可以访问要设置转换值的目标字段。

GenericConverter的一个很好的例子是在Java数组和集合之间进行转换的转换器。 这样的ArrayToCollectionConverter会对声明目标集合类型的字段进行内省,以解析集合的元素类型。 这样就可以在将集合设置到目标字段上之前,将源数组中的每个元素转换为集合元素类型。

由于GenericConverter是一个更复杂的SPI接口,因此仅应在需要时使用它。 支持Converter或ConverterFactory以满足基本的类型转换需求。

使用ConditionalGenericConverter

有时,您希望仅在满足特定条件时才运行Converter。 例如,您可能只想在目标字段上存在特定注释时才运行Converter,或者可能仅在目标类上定义了特定方法(例如静态valueOf方法)时才运行Converter。 ConditionalGenericConverter是GenericConverter和ConditionalConverter接口的联合,可让您定义以下自定义匹配条件:

public interface ConditionalConverter {boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {}

ConditionalGenericConverter的一个很好的例子是EntityConverter,它在持久实体标识符和实体引用之间进行转换。 仅当目标实体类型声明静态查找器方法(例如findAccount(Long))时,此类EntityConverter才可能匹配。 您可以在matchs(TypeDescriptor,TypeDescriptor)的实现中执行这种finder方法检查。

3.4.4 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 配置ConversionService

ConversionService是无状态对象,旨在在应用程序启动时实例化,然后在多个线程之间共享。 在Spring应用程序中,通常为每个Spring容器(或ApplicationContext)配置一个ConversionService实例。 当框架需要执行类型转换时,Spring会选择该ConversionService并使用它。 您还可以将此ConversionService注入到任何bean中,然后直接调用它。

如果未向Spring注册任何ConversionService,则使用原始的基于PropertyEditor的系统。

<bean id="conversionService"class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的ConversionService可以在字符串,数字,枚举,集合,映射和其他常见类型之间进行转换。 要用您自己的自定义转换器补充或覆盖默认转换器,请设置converters属性。 属性值可以实现Converter,ConverterFactory或GenericConverter接口中的任何一个。

<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的详细信息,请参见FormatterRegistry SPI。

3.4.6 以编程方式使用ConversionService

要以编程方式使用ConversionService实例,可以像对待其他任何bean一样注入对该实例的引用。 以下示例显示了如何执行此操作:

@Service
public class MyService {public MyService(ConversionService conversionService) {this.conversionService = conversionService;}public void doIt() {this.conversionService.convert(...)}
}

对于大多数使用情况,可以使用指定targetType的convert方法,但不适用于更复杂的类型,例如参数化元素的集合。 例如,如果要以编程方式将整数列表转换为字符串列表,则需要提供源类型和目标类型的正式定义。

幸运的是,如下面的示例所示,TypeDescriptor提供了各种选项来使操作变得简单明了:

DefaultConversionService cs = new DefaultConversionService();List<Integer> input = ...
cs.convert(input,TypeDescriptor.forObject(input), // List<Integer> type descriptorTypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

请注意,DefaultConversionService自动注册适用于大多数环境的转换器。 这包括集合转换器,标量转换器和基本的对象到字符串转换器。 您可以使用DefaultConversionService类上的静态addDefaultConverters方法向任何ConverterRegistry注册相同的转换器。

值类型的转换器可重用于数组和集合,因此,假设标准集合处理适当,则无需创建特定的转换器即可将S的集合转换为T的集合。

3.5 Spring字段格式

如前一节所述,core.convert是一种通用类型转换系统。 它提供了统一的ConversionService API和强类型的Converter SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring容器使用此系统绑定bean属性值。 此外,Spring Expression Language(SpEL)和DataBinder都使用此系统绑定字段值。 例如,当SpEL需要强制将Short到Long强制完成一个expression.setValue(Object bean,Object value)尝试时,core.convert系统将执行强制。

现在考虑典型客户端环境(例如Web或桌面应用程序)的类型转换要求。 在这样的环境中,您通常会从String转换为支持客户端回发过程,然后又转换为String以支持视图渲染过程。 另外,您通常需要本地化String值。 更通用的core.convert Converter SPI不能直接满足此类格式化要求。 为了直接解决这些问题,Spring 3引入了便利的Formatter SPI,它为客户端环境提供了PropertyEditor实现的简单而强大的替代方案。

通常,当您需要实现通用类型转换逻辑时,可以使用Converter SPI,例如在java.util.Date和Long之间进行转换。 在客户端环境(例如Web应用程序)中工作并且需要解析和打印本地化的字段值时,可以使用Formatter SPI。 ConversionService为两个SPI提供统一的类型转换API。

3.5.1 格式化器SPI

用于实现字段格式化逻辑的Formatter SPI非常简单且类型严格。 以下清单显示了Formatter接口定义:

package org.springframework.format;public interface Formatter<T> extends Printer<T>, Parser<T> {}

Formatter从Printer和Parser构建块接口扩展。 以下清单显示了这两个接口的定义:

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的实例。 如果解析尝试失败,则Formatter应该抛出ParseException或IllegalArgumentException。 注意确保您的Formatter实现是线程安全的。

为方便起见,format子包提供了几种Formatter实现。 数字包提供NumberStyleFormatter,CurrencyStyleFormatter和PercentStyleFormatter来格式化使用java.text.NumberFormat的Number对象。 datetime包提供了一个DateFormatter,用于使用java.text.DateFormat格式化java.util.Date对象。 datetime.joda软件包基于Joda-Time库提供了全面的日期时间格式支持。

以下DateFormatter是Formatter实现的示例:

package org.springframework.format.datetime;
//T参数化为您希望格式化的对象的类型
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;}
}

3.5.2 注释驱动的格式

可以通过字段类型或注释配置字段格式。 要将注释绑定到Formatter,请实现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);
}

要创建实现类:。 将A参数化为要与格式逻辑关联的字段注解类型,例如org.springframework.format.annotation.DateTimeFormat。让getFieldTypes()返回可以在其上使用注释的字段类型. 让getPrinter()返回打印机以打印带注释的字段的值。 让getParser()返回解析器以解析带注释字段的clientValue。

以下示例AnnotationFormatterFactory实现将@NumberFormat注解绑定到格式化程序,以指定数字样式或模式:

public final class NumberFormatAnnotationFormatterFactoryimplements 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注释字段,如以下示例所示:

public class MyModel {@NumberFormat(style=Style.CURRENCY)private BigDecimal decimal;
}

格式注释API

org.springframework.format.annotation包中存在一个可移植的格式注释API。 您可以使用@NumberFormat格式化数字字段(例如Double和Long),并使用@DateTimeFormat格式化java.util.Date,java.util.Calendar,Long(用于毫秒时间戳)以及JSR-310 java.time和Joda- 时间值类型。

以下示例使用@DateTimeFormat将java.util.Date格式化为ISO日期(yyyy-MM-dd):

3.5.3 FormatterRegistry SPI

FormatterRegistry是用于注册格式器和转换器的SPI。 FormattingConversionService是适用于大多数环境的FormatterRegistry的实现。 您可以通过编程方式或声明方式将此变体配置为Spring Bean,例如 通过使用FormattingConversionServiceFactoryBean。 由于此实现还实现了ConversionService,因此您可以直接将其配置为与Spring的DataBinder和Spring表达式语言(SpEL)一起使用。

以下清单显示了FormatterRegistry SPI:

package org.springframework.format;public interface FormatterRegistry extends ConverterRegistry {void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);void addFormatterForFieldType(Formatter<?> formatter);void addFormatterForAnnotation(AnnotationFormatterFactory<?> factory);
}

如前面的清单所示,您可以按字段类型或注解注册格式化程序。

FormatterRegistry SPI使您可以集中配置格式设置规则,而不必在控制器之间复制此类配置。 例如,您可能需要强制所有日期字段以

某种方式设置格式或具有特定注释的字段以某种方式设置格式。 使用共享的FormatterRegistry,您可以一次定义这些规则,并在需要格式化时应用它们。

3.5.4 FormatterRegistrar SPI

FormatterRegistrar是一个SPI,用于通过FormatterRegistry注册格式器和转换器。 以下清单显示了其接口定义:

package org.springframework.format;public interface FormatterRegistrar {void registerFormatters(FormatterRegistry registry);
}

为给定的格式类别(例如日期格式)注册多个相关的转换器和格式器时,FormatterRegistrar很有用。 在声明式注册不足的情况下,它也很有用。例如,当格式化程序需要在不同于其自身的特定字段类型下建立索引时,或者在注册“打印机/解析器”对时。 下一节将提供有关转换器和格式化程序注册的更多信息。

3.5.5 在Spring MVC中配置格式

参见Spring MVC一章中的转换和格式化。

3.6 配置全局日期和时间格式

默认情况下,未使用@DateTimeFormat注释的日期和时间字段是使用DateFormat.SHORT样式从字符串转换的。 如果愿意,可以通过定义自己的全局格式来更改此设置。

为此,请确保Spring不注册默认格式器。 相反,可以借助以下方法手动注册格式化程序:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar

  • org.springframework.format.datetime.DateFormatterRegistrar 或者

``org.springframework.format.datetime.joda.JodaTimeFormatterRegistrarfor Joda-Time.

例如,以下Java配置注册全局yyyyMMdd格式:

@Configuration
public class AppConfig {@Beanpublic FormattingConversionService conversionService() {// Use the DefaultFormattingConversionService but do not register defaultsDefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);// Ensure @NumberFormat is still supportedconversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());// Register JSR-310 date conversion with a specific global formatDateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));registrar.registerFormatters(conversionService);// Register date conversion with a specific global formatDateFormatterRegistrar registrar = new DateFormatterRegistrar();registrar.setFormatter(new DateFormatter("yyyyMMdd"));registrar.registerFormatters(conversionService);return conversionService;}
}

如果您更喜欢基于XML的配置,则可以使用FormattingConversionServiceFactoryBean。 以下示例显示了如何执行此操作(这次使用Joda Time)

<?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/beanshttps://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.joda.JodaTimeFormatterRegistrar"><property name="dateFormatter"><bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean"><property name="pattern" value="yyyyMMdd"/></bean></property></bean></set></property></bean>
</beans>

请注意,在Web应用程序中配置日期和时间格式时,还有其他注意事项。 请参阅WebMVC转换和格式或WebFlux转换和格式。

3.7 Java Bean验证

Spring框架提供了对Java Bean验证API的支持。

3.7.1 Bean验证概述

Bean验证通过约束声明和Java应用程序的元数据提供了一种通用的验证方法。 要使用它,您需要使用声明性验证约束对域模型属性进行注释,然后由运行时强制实施。 有内置的约束,您也可以定义自己的自定义约束。

考虑以下示例,该示例显示了具有两个属性的简单PersonForm模型:

public class PersonForm {private String name;private int age;
}

Bean验证使您可以声明约束,如以下示例所示:

public class PersonForm {@NotNull@Size(max=64)private String name;@Min(0)private int age;
}

然后,Bean验证验证器根据声明的约束来验证此类的实例。 有关该API的一般信息,请参见Bean验证。 有关特定限制,请参见Hibernate Validator文档。 要学习如何将bean验证提供程序设置为Spring bean,请继续阅读。

3.7.2 配置Bean验证提供程序

Spring提供了对Bean验证API的全面支持,包括将Bean验证提供程序作为Spring Bean进行引导。 这使您可以在应用程序中需要验证的任何地方注入javax.validation.ValidatorFactory或javax.validation.Validator。

您可以使用LocalValidatorFactoryBean将默认的Validator配置为Spring Bean,如以下示例所示:

import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;@Configuration
public class AppConfig {@Beanpublic LocalValidatorFactoryBean validator() {return new LocalValidatorFactoryBean;}
}

前面示例中的基本配置触发Bean验证,以使用其默认引导机制进行初始化。 Bean验证提供程序(例如Hibernate Validator)应该存在于类路径中并被自动检测到。

注入验证器

LocalValidatorFactoryBean同时实现javax.validation.ValidatorFactory和javax.validation.Validator以及Spring的org.springframework.validation.Validator。 您可以将对这些接口之一的引用注入需要调用验证逻辑的bean中。

如果您希望直接使用Bean Validation API,则可以注入对javax.validation.Validator的引用,如以下示例所示:

import javax.validation.Validator;@Service
public class MyService {@Autowiredprivate Validator validator;
}

如果您的bean需要使用Spring Validation API,则可以注入对org.springframework.validation.Validator的引用,如以下示例所示:

配置自定义约束

每个bean验证约束都包括两个部分:

  • @Constraint注解,用于声明约束及其可配置属性。

  • javax.validation.ConstraintValidator接口的实现,用于实现约束的行为。

要将声明与实现相关联,每个@Constraint注解都引用一个对应的ConstraintValidator实现类。 在运行时,当在域模型中遇到约束注释时,ConstraintValidatorFactory实例化引用的实现。

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {}
import javax.validation.ConstraintValidator;public class MyConstraintValidator implements ConstraintValidator {@Autowired;private Foo aDependency;// ...
}

如前面的示例所示,ConstraintValidator实现可以像其他任何Spring bean一样具有@Autowired的依赖项。

Spring 驱动方法验证

您可以通过MethodValidationPostProcessor bean定义将Bean验证1.1(以及作为自定义扩展,还包括Hibernate Validator 4.3)支持的方法验证功能集成到Spring上下文中:

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;@Configuration
public class AppConfig {@Beanpublic MethodValidationPostProcessor validationPostProcessor() {return new MethodValidationPostProcessor;}
}

为了有资格进行Spring驱动的方法验证,所有目标类都必须使用Spring的@Validated注释进行注释,该注释也可以选择声明要使用的验证组。 有关使用Hibernate Validator和Bean Validation 1.1提供程序的设置详细信息,请参见MethodValidationPostProcessor。

注意:

方法验证依赖于目标类周围的AOP代理,即接口上方法的JDK动态代理或CGLIB代理。 代理的使用存在某些限制,“了解AOP代理”中介绍了其中的一些限制。 此外,请记住始终在代理类上使用方法和访问器; 直接现场访问将不起作用。

其他配置选项

在大多数情况下,默认的LocalValidatorFactoryBean配置就足够了。 从消息插值到遍历解析,有许多用于各种Bean验证构造的配置选项。 有关这些选项的更多信息,请参见LocalValidatorFactoryBean javadoc。

3.7.3 配置一个DataBinder

从Spring 3开始,您可以使用Validator配置DataBinder实例。 配置完成后,您可以通过调用binder.validate()来调用Validator。 任何验证错误都会自动添加到活页夹的BindingResult中。

下面的示例演示如何在绑定到目标对象后,以编程方式使用DataBinder来调用验证逻辑:

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());// bind to the target object
binder.bind(propertyValues);// validate the target object
binder.validate();// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

您还可以通过dataBinder.addValidators和dataBinder.replaceValidators配置具有多个Validator实例的DataBinder。 当将全局配置的bean验证与在DataBinder实例上本地配置的Spring Validator结合使用时,这很有用。 请参阅Spring MVC验证配置。

4.Spring表达语言(SpEL)

Spring表达式语言(简称“ SpEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。 语言语法类似于Unified EL,但提供了其他功能,最著名的是方法调用和基本的字符串模板功能。

尽管还有其他几种Java表达式语言可用-OGNL,MVEL和JBoss EL,仅举几例-Spring表达式语言的创建是为了向Spring社区提供一种受良好支持的表达式语言,该语言可用于以下版本中的所有产品 Spring投资组合。 其语言功能受Spring产品组合中项目的要求驱动,包括Spring Tools for Eclipse中代码完成支持的工具要求。 也就是说,SpEL基于与技术无关的API,如有需要,该API可以集成其他表达语言实现。

虽然SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,而是可以独立使用。 为了自成一体,本章中的许多示例都将SpEL用作独立的表达语言。 这需要创建一些自举基础结构类,例如解析器。 大多数Spring用户不需要处理此基础结构,而只能编写表达式字符串进行评估。 这种典型用法的一个示例是将SpEL集成到创建XML或基于注释的Bean定义中,如Expression对定义Bean定义的支持所示。

本章介绍了表达式语言,其API和语言语法的功能。 在许多地方,Inventor和Society类都用作表达评估的目标对象。 这些类声明和用于填充它们的数据在本章末尾列出。

表达式语言支持以下功能:

  • Literal expressions 文字表达
  • Boolean and relational operators 布尔运算符和关系运算符
  • Regular expressions 常用表达
  • Class expressions 类表达式
  • Accessing properties, arrays, lists, and maps 访问属性,数组,列表和映射
  • Method invocation 方法调用
  • Relational operators 关系运算符
  • Assignment 分配
  • Calling constructors 调用构造函数
  • Bean references bean 依赖
  • Array construction 数组构造
  • Inline lists 内联列表
  • Inline maps 内联map
  • Ternary operator 三元运算符
  • Variables 变数
  • User-defined functions 用户定义的功能
  • Collection projection 集合投影
  • Collection selection 选择集合
  • Templated expressions 模板表达式

4.1 Evaluation

本节介绍SpEL接口的简单用法及其表达语言。 完整的语言参考可以在“语言参考”中找到。

以下代码介绍了SpEL API,用于评估文字字符串表达式Hello World。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();//message变量的值为“ Hello World”。

您最可能使用的SpEL类和接口位于org.springframework.expression包及其子包中,例如spel.support。

ExpressionParser接口负责解析表达式字符串。 在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。 Expression接口负责评估先前定义的表达式字符串。 分别调用parser.parseExpression和exp.getValue时,可以引发两个异常ParseException和EvaluationException。

SpEL支持广泛的功能,例如调用方法,访问属性和调用构造函数。

在以下方法调用示例中,我们在字符串文字上调用concat方法:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();//message的值现在是“ Hello World!”。

以下调用JavaBean属性的示例将调用String属性Bytes:

ExpressionParser parser = new SpelExpressionParser();// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();//这行将文字转换为字节数组。

SpEL还通过使用标准点符号(例如prop1.prop2.prop3)以及属性值的相应设置来支持嵌套属性。 也可以访问公共字段。

下面的示例演示如何使用点表示法来获取文字的长度:

ExpressionParser parser = new SpelExpressionParser();// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();//“ Hello World” .bytes.length给出文字的长度

可以调用String的构造函数,而不使用字符串文字,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);//从文字构造一个新的String并将其变为大写。

注意使用通用方法:public T getValue(Class requiredResultType)。 使用此方法无需将表达式的值强制转换为所需的结果类型。 如果无法将值转换为T类型或无法使用已注册的类型转换器进行转换,则将引发EvaluationException。

SpEL的更常见用法是提供一个表达式字符串,该字符串针对特定对象实例(称为根对象)进行评估。 以下示例显示如何从Inventor类的实例检索name属性或如何创建布尔条件:

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");ExpressionParser parser = new SpelExpressionParser();Expression exp = parser.parseExpression("name"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true

4.1.1 理解 EvaluationContext

在评估表达式以解析属性,方法或字段并帮助执行类型转换时,使用EvaluationContext接口。 Spring提供了两种实现

  • SimpleEvaluationContext:对于不需要全部SpEL语言语法范围且应受到有意义限制的表达式类别,公开了SpEL基本语言功能和配置选项的子集。 示例包括但不限于数据绑定表达式和基于属性的过滤器。

  • StandardEvaluationContext:公开了全套SpEL语言功能和配置选项。 您可以使用它来指定默认的根对象,并配置每个可用的评估相关策略。

SimpleEvaluationContext设计为仅支持SpEL语言语法的子集。 它不包括Java类型引用,构造函数和Bean引用。 它还要求您明确选择对表达式中的属性和方法的支持级别。 默认情况下,create()静态工厂方法仅启用对属性的读取访问。 您还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某些组合:

  • 仅自定义PropertyAccessor(无反射)

  • 只读访问的数据绑定属性

  • 读写的数据绑定属性

类型转换

默认情况下,SpEL使用Spring核心(org.springframework.core.convert.ConversionService)中可用的转换服务。 此转换服务随附有许多用于常见转换的内置转换器,但它也是完全可扩展的,因此您可以在类型之间添加自定义转换。 此外,它是泛型感知的。 这意味着,当您在表达式中使用泛型类型时,SpEL会尝试进行转换以维护遇到的任何对象的类型正确性。

实际上这是什么意思? 假设使用setValue()进行赋值来设置List属性。 该属性的类型实际上是List 。 SpEL认识到列表中的元素在放入列表之前需要转换为布尔值。 以下示例显示了如何执行此操作:

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 解析器配置

可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置SpEL表达式解析器。 配置对象控制某些表达式组件的行为。 例如,如果您索引到数组或集合中并且指定索引处的元素为null,则可以自动创建该元素。 当使用由属性引用链组成的表达式时,这很有用。 如果您索引到数组或列表中并指定了超出数组或列表当前大小末尾的索引,则可以自动增长数组或列表以容纳该索引。 下面的示例演示如何自动增加列表:

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 编译

Spring Framework 4.1包含一个基本的表达式编译器。 通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。 对于偶尔使用表达式,这很好,但是当与其他组件(例如Spring Integration)一起使用时,性能可能非常重要,并且不需要动态性。

SpEL编译器旨在满足这一需求。 在评估过程中,编译器生成一个Java类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。 由于缺少在表达式周围输入内容的信息,因此编译器在执行编译时会使用在表达式的解释求值过程中收集的信息。 例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释求值时就知道它是什么。 当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。 因此,编译最适合于类型信息在重复求值时不会改变的表达式。

考虑以下基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

因为前面的表达式涉及数组访问,一些属性取消引用和数字运算,所以性能提升非常明显。 在一个示例中,进行了50000次迭代的微基准测试,使用解释器评估需要75毫秒,而使用表达式的编译版本仅需要3毫秒

编译器配置

默认情况下不打开编译器,但是您可以通过两种不同的方式之一来打开它。 当SpEL用法嵌入到另一个组件中时,可以使用解析器配置过程(前面讨论过)或使用系统属性来打开它。 本节讨论这两个选项。

编译器可以在org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式之一进行操作。 模式如下:

  • OFF(关闭)(默认):编译器已关闭。

  • 立即:在立即模式下,将尽快编译表达式。 通常是在第一次解释评估之后。 如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式求值的调用者将收到异常。

  • 混合:在混合模式下,表达式会随着时间静默在解释模式和编译模式之间切换。 经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(例如,如前面所述的类型更改),则表达式会自动再次切换回解释形式。 稍后,它可能会生成另一个已编译的表单并切换到该表单。 基本上,用户进入即时模式的异常是在内部处理的。

存在IMMEDIATE模式是因为MIXED模式可能会导致具有副作用的表达式出现问题。 如果已编译的表达式在部分成功后就崩溃了,则它可能已经完成了影响系统状态的操作。 如果发生这种情况,调用者可能不希望它在解释模式下静默地重新运行,因为表达式的一部分可能运行了两次。

选择模式后,使用SpelParserConfiguration配置解析器。 以下示例显示了如何执行此操作:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,this.getClass().getClassLoader());SpelExpressionParser parser = new SpelExpressionParser(config);Expression expr = parser.parseExpression("payload");MyMessage message = new MyMessage();Object payload = expr.getValue(message);

当指定编译器模式时,还可以指定一个类加载器(允许传递null)。 编译的表达式在提供的任何子类下创建的子类加载器中定义。 重要的是要确保,如果指定了类加载器,则它可以查看表达式评估过程中涉及的所有类型。 如果未指定类加载器,则使用默认的类加载器(通常是在表达式求值期间运行的线程的上下文类加载器)。

第二种配置编译器的方法是将SpEL嵌入到其他组件中,并且可能无法通过配置对象进行配置。 在这些情况下,可以使用系统属性。 您可以将spring.expression.compiler.mode属性设置为SpelCompilerMode枚举值之一(关闭,立即或混合)。

编译器限制

从Spring Framework 4.1开始,已经有了基本的编译框架。 但是,该框架尚不支持编译每种表达式。 最初的重点是可能在性能关键型上下文中使用的通用表达式。 目前无法编译以下类型的表达式:

  • 涉及赋值的表达式

  • 表达式依赖转换服务

  • 使用自定义解析器或访问器的表达式

  • 使用选择或投影的表达式

将来将可以编译更多类型的表达。

4.2 Bean定义中的表达式

您可以将SpEL表达式与基于XML或基于注释的配置元数据一起使用,以定义BeanDefinition实例。 在这两种情况下,用于定义表达式的语法都采用#{<表达式字符串>}的形式。

4.2.1 XML配置

可以使用表达式来设置属性或构造函数的参数值,如以下示例所示:

<bean id="numberGuess" class="org.spring.samples.NumberGuess"><property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/><!-- other properties -->
</bean>

systemProperties变量是预定义的,因此您可以在表达式中使用它,如以下示例所示:

<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 注解配置

若要指定默认值,可以将@Value注解放置在字段,方法以及方法或构造函数参数上。

以下示例设置字段变量的默认值:

public class FieldValueTestBean {@Value("#{ systemProperties['user.region'] }")private String defaultLocale;public void setDefaultLocale(String defaultLocale) {this.defaultLocale = defaultLocale;}public String getDefaultLocale() {return this.defaultLocale;}
}

下面的示例显示等效的但使用属性设置器方法的示例:

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注解,如以下示例所示

public class SimpleMovieLister {private MovieFinder movieFinder;private String defaultLocale;@Autowiredpublic void configure(MovieFinder movieFinder,@Value("#{ systemProperties['user.region'] }") String defaultLocale) {this.movieFinder = movieFinder;this.defaultLocale = defaultLocale;}// ...
}

4.3 语法依赖

本节描述了Spring Expression Language的工作方式。 它涵盖以下主题:

文字表达

属性,数组,列表,映射和索引器

内联列表

内联地图

阵列构造

方法

经营者

种类

建设者

变数

功能

Bean参考

三元运算符(If-Then-Else)

Elvis操作符

安全导航操作

4.3.1 文字表达

支持的文字表达式的类型为字符串,数值(int,实数,十六进制),布尔值和null。 字符串由单引号引起来。 要将单引号本身放在字符串中,请使用两个单引号字符。

以下清单显示了文字的简单用法。 通常,它们不是像这样孤立地使用,而是作为更复杂的表达式的一部分使用-例如,在逻辑比较运算符的一侧使用文字。

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 属性,数组,列表,映射和索引器

使用属性引用进行导航很容易。 为此,请使用句点来指示嵌套的属性值。 Inventor类的实例pupin和tesla填充有示例部分使用的类中列出的数据。 要向下导航并获取特斯拉的出生年份和普平的出生城市,我们使用以下表达式:

// evals to 1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

属性名称的首字母允许不区分大小写。 数组和列表的内容通过使用方括号表示法获得,如以下示例所示:

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);

4.3.3 内联列表

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 内联MAP

您也可以使用{key:value}表示法在表达式中直接表达MAP。 以下示例显示了如何执行此操作:

// 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);

{:}本身就是一个空的MAP。 出于性能原因,如果映射图本身由固定的文字或其他嵌套的常量结构(列表或映射图)组成,则会创建一个常量映射图来表示该表达式(而不是在每次求值时都构建一个新的映射图)。 映射键的引用是可选的。 上面的示例不使用带引号的键。

4.3.5. Array 构造

您可以使用熟悉的Java语法来构建数组,可以选择提供一个初始化程序以在构造时填充该数组。 以下示例显示了如何执行此操作:

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 方法

您可以使用典型的Java编程语法来调用方法。 您还可以在文字上调用方法。 还支持变量参数。 下面的示例演示如何调用方法

// 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 操作符

Spring表达式语言支持以下几种运算符:

  • 关系运算符

  • 逻辑运算符

  • 数学运算符

  • 赋值运算符

关系运算符

使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于和大于或等于)。 以下清单显示了一些运算符示例:

// 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);

注意:

对null的大于和小于比较遵循一个简单的规则:null被视为无(不是零)。 结果,任何其他值始终大于null(X> null始终为true),并且其他任何值都不小于零(X <null始终为false)。

如果您更喜欢数字比较,请避免使用基于数字的空比较,而建议使用零进行比较(例如,X> 0或X <0)。

除了标准的关系运算符外,SpEL还支持instanceof和基于正则表达式的匹配运算符。 以下清单显示了两个示例

// 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);

注意:

请注意原始类型,因为它们会立即被包装为包装类型,因此,按预期方式,1个instanceof T(int)的计算结果为false,而1个instanceof T(Integer)的计算结果为true。

每个符号运算符也可以指定为纯字母等效项。 这样可以避免使用的符号对于嵌入表达式的文档类型(例如在XML文档中)具有特殊含义的问题。 等效的文字是:

  • lt (<)
  • gt (>)
  • le (<=)
  • ge (>=)
  • eq (==)
  • ne (!=)
  • div (/)
  • mod (%)
  • not (!).

所有的文本运算符都不区分大小写

逻辑运算符

SpEL支持以下逻辑运算符:

  • and (&&)
  • or (||)
  • not (!)

下面的示例显示如何使用逻辑运算符

// -- AND --// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);// evaluates to true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);// -- OR --// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);// evaluates to true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);// -- NOT --// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);// -- AND and NOT --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

数学运算符

您可以在数字和字符串上使用加法运算符。 您只能对数字使用减,乘和除运算符。 您还可以使用模数(%)和指数幂(^)运算符。 强制执行标准运算符优先级。 以下示例显示了正在使用的数学运算符:

// Addition
int two = parser.parseExpression("1 + 1").getValue(Integer.class);  // 2String testString = parser.parseExpression("'test' + ' ' + 'string'").getValue(String.class);  // 'test string'// Subtraction
int four = parser.parseExpression("1 - -3").getValue(Integer.class);  // 4double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class);  // -9000// Multiplication
int six = parser.parseExpression("-2 * -3").getValue(Integer.class);  // 6double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class);  // 24.0// Division
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class);  // -2double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class);  // 1.0// Modulus
int three = parser.parseExpression("7 % 4").getValue(Integer.class);  // 3int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class);  // 1// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21

赋值运算符

要设置属性,请使用赋值运算符(=)。 这通常在调用setValue的过程中完成,但也可以在调用getValue的过程中完成。 下面的清单显示了使用赋值运算符的两种方法:

ventor 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 类型

您可以使用特殊的T运算符来指定java.lang.Class(类型)的实例。 静态方法也可以通过使用此运算符来调用。 StandardEvaluationContext使用TypeLocator查找类型,而StandardTypeLocator(可以替换)是在了解java.lang包的情况下构建的。 这意味着对Java.lang中的类型的T()引用不需要完全限定,但是所有其他类型引用都必须是完全限定的。 下面的示例演示如何使用T运算符:

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);boolean trueValue = parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR").getValue(Boolean.class);

4.3.9 构造器

您可以使用new运算符来调用构造函数。 除基本类型(int,float等)和String以外的所有其他类都应使用完全限定的类名。 下面的示例演示如何使用new运算符调用构造函数:

Inventor einstein = p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')").getValue(Inventor.class);//create new inventor instance within add method of List
p.parseExpression("Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))").getValue(societyContext);

4.3.10 变量

您可以使用#variableName语法在表达式中引用变量。 通过在EvaluationContext实现上使用setVariable方法设置变量。

注意:

有效的变量名称必须由以下一个或多个受支持的字符组成。

字母:A到Z和a到z

位数:0到9

下划线:_

美元符号:$

以下示例显示了如何使用变量

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"

#this和#root变量

始终定义#this变量,并引用当前评估对象(针对不合格的引用,将对其进行解析)。 始终定义#root变量,并引用根上下文对象。 尽管#this可能随表达式的组成部分的求值而变化,但#root始终引用根。 以下示例说明如何使用#this和#root变量:

// 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 功能

您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展SpEL。 该函数通过EvaluationContext注册。 下面的示例演示如何注册用户定义的函数:

Method method = ...;EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,考虑以下用于反转字符串的实用程序方法:

public abstract class StringUtils {public static String reverseString(String input) {StringBuilder backwards = new StringBuilder(input.length());for (int i = 0; i < input.length(); i++) {backwards.append(input.charAt(input.length() - 1 - i));}return backwards.toString();}
}

然后,您可以注册并使用前面的方法,如以下示例所示:

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的引用

如果评估上下文已使用bean解析器配置,则可以使用@符号从表达式中查找bean。 以下示例显示了如何执行此操作

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要访问工厂bean本身,您应该在bean名称前加上&符号。 以下示例显示了如何执行此操作:

4.3.13 三元运算符(If-Then-Else)

您可以使用三元运算符在表达式内部执行if-then-else条件逻辑。 以下清单显示了一个最小的示例:

String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class);

在这种情况下,布尔值false导致返回字符串值’falseExp’。 一个更现实的示例如下:

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 操作符

Elvis运算符是三元运算符语法的简化,并且在Groovy语言中使用。 使用三元运算符语法,通常必须将变量重复两次,如以下示例所示:

String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");

而是可以使用Elvis运算符(以与Elvis的发型相似的方式命名)。 以下示例显示了如何使用Elvis运算符:

ExpressionParser parser = new SpelExpressionParser();String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);
System.out.println(name);  // 'Unknown'

以下清单显示了一个更复杂的示例

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 Teslatesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

4.3.15 安全导航操作

安全导航运算符用于避免NullPointerException,它来自Groovy语言。 通常,当您引用一个对象时,可能需要在访问该对象的方法或属性之前验证其是否为null。 为避免这种情况,安全导航运算符返回null而不是引发异常。 以下示例显示了如何使用安全导航操作符:

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);  // Smiljantesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

4.3.16 集合选择

选择是一种强大的表达语言功能,可让您通过从源集合中进行选择来将其转换为另一个集合。

选择使用。?[selectionExpression]的语法。 它过滤集合并返回一个包含原始元素子集的新集合。 例如,通过选择,我们可以轻松地获得塞尔维亚发明者的列表,如以下示例所示:

List<Inventor> list = (List<Inventor>) parser.parseExpression("Members.?[Nationality == 'Serbian']").getValue(societyContext);

在list和map上都可以选择。 对于列表,将针对每个单独的列表元素评估选择标准。 针对地图,针对每个地图条目(Java类型Map.Entry的对象)评估选择标准。 每个地图条目都有其键和值,可作为属性访问以供选择。

以下表达式返回一个新映射,该映射包含条目值小于27的原始映射的那些元素:

4.3.17。 集合投影

投影使集合可以驱动子表达式的求值,结果是一个新的集合。 投影的语法是。![projectionExpression]。 例如,假设我们有一个发明家列表,但是想要他们出生的城市列表。 实际上,我们希望为发明人列表中的每个条目评估“ placeOfBirth.city”。 下面的示例使用投影来做到这一点:

4.3.18 表达式模板

表达式模板允许将文字文本与一个或多个评估块混合。 每个评估块都由您可以定义的前缀和后缀字符分隔。 常见的选择是使用#{}作为分隔符,如以下示例所示:

String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}",new TemplateParserContext()).getValue(String.class);

通过将文字文本“随机数为”与计算#{}分隔符内的表达式的结果(在本例中为调用那个random()方法的结果)相连接来评估字符串。 parseExpression()方法的第二个参数的类型为ParserContext。 ParserContext接口用于影响表达式的解析方式,以支持表达式模板功能。 TemplateParserContext的定义如下:

public class TemplateParserContext implements ParserContext {public String getExpressionPrefix() {return "#{";}public String getExpressionSuffix() {return "}";}public boolean isTemplate() {return true;}
}

4.4 示例中使用的类

本节列出了本章示例中使用的类。

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;}
}
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;}
}
ackage 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.使用Spring进行面向切面的编程

面向切面的编程(AOP)通过提供另一种思考程序结构的方式来补充面向对象的编程(OOP)。 OOP中模块化的关键单元是类,而在AOP中模块化是切面。 切面使关注点(例如事务管理)的模块化跨越了多种类型和对象。 (这种关注在AOP文献中通常被称为“跨领域”关注。)

Spring的关键组件之一是AOP框架。 虽然Spring IoC容器不依赖于AOP(这意味着您不需要的话就不需要使用AOP),但AOP是对Spring IoC的补充,以提供功能强大的中间件解决方案。

注意:

具有AspectJ切入点的Spring AOP

Spring通过使用基于模式的方法或@AspectJ注解样式,提供了编写自定义方面的简单而强大的方法。 这两种样式都提供了完全类型化的建议,并使用了AspectJ切入点语言,同时仍使用Spring AOP进行编织。

本章讨论基于架构和基于@AspectJ的AOP支持。 下一章将讨论较低级别的AOP支持。

AOP在Spring框架中用于:

  • 提供声明式企业服务。 此类服务中最重要的是声明式事务管理

  • 让用户实现自定义方面,以AOP补充其对OOP的使用

5.1 AOP概念

让我们首先定义一些重要的AOP概念和术语。 这些术语不是特定于Spring的。 不幸的是,AOP术语并不是特别直观。 但是,如果使用Spring自己的术语,将会更加令人困惑。

  • 切面:涉及多个类别的关注点的模块化,在企业级java 引用里面事务是一个好的例子.在Spring AOP中,方面是通过使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的。

  • 连接点:切入点在方法的执行期间的一点,比如在方法执行期间或者是处理异常的时候

  • 通知: 动作通过切面发生在一个指定的切点,不同的通知包括 “around”, “before” and “after” advice.(后面会讨论),许多AOP框架,包括Spring 将建议建模为拦截器,并在连接点周围维护一系列拦截器

  • 切入点:和连接点匹配的,通知和切点表达式关联和切点匹配上了连接点(例如,执行具有特定名称的方法);切入点表达式匹配的连接点的概念是AOP的核心,默认情况下,Spring使用AspectJ切入点表达语言。

 @Pointcut("@annotation(com.sy.context.UserAccess)")public void userAccess() {}@Around("userAccess()")public Object around(ProceedingJoinPoint jp) throws Throwable{//do somethingObject =  jp.proceed();//do somethingreturn Object;       }

就是 连接点: 就是一个个方法执行点

​ 切入点 : 就是 这个表达式 可以匹配连接点 ,有多个连接点组成

​ 通知 :就是对匹配上的所有连接点的其他处理方法 前置 啊 后置 啊 环绕等等

​ 切面: 就是上面的所有概念的一个相当于融合吧 代码层面上就是都在切面里面实现,

  • 简介:代表类型声明其他方法或字段。 Spring AOP允许您向任何建议的对象引入新的接口(和相应的实现)。 例如,您可以使用简介使Bean实现IsModified接口,以简化缓存。 (在AspectJ社区中,介绍被称为类型间声明。)

  • 目标对象:一个或多个切面通知的对象。 也称为“通知对象”。 由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。

  • AOP代理:由AOP框架创建的一个对象,用于实现方面合同(建议方法执行等)。 在Spring Framework中,AOP代理是JDK动态代理或CGLIB代理。

  • 编织:将方面与其他应用程序类型或对象链接在一起以创建建议的对象。 这可以在编译时(例如,使用AspectJ编译器),加载时或在运行时完成。 像其他纯Java AOP框架一样,Spring AOP在运行时执行编织。

Spring AOP包括以下类型的通知:

  • 前置通知:在连接点之前运行的通知,但是它不能阻止执行流程前进到连接点(除非它引发异常)

  • 返回之后的通知:通知运行在方法正常放回结果后 (例如 如果方法没有爆出异常)

  • 抛出异常后通知:如果方法因抛出异常而退出,则执行这个通知。

  • finally 之后运行的通知:无论连接点退出的方式如何(正常或特殊返回),均应执行建议。

  • 环绕通知: 围绕着连接点的通知,这个时最强力的通知,通知可以在方法调用之前和之后执行一些自定义的行为,它还可以选择是返回连接点还是返回自己处理后的结果或者引发异常的结果

所有通知的参数都是静态类型的,因此您可以使用适当类型的通知参数(例如,方法执行的返回值的类型),而不是对象数组。

通过切入点匹配的连接点的概念是AOP的关键,这与仅提供拦截功能的旧技术有所不同。切入点使建议的目标独立于面向对象的层次结构。 例如,您可以将提供声明式事务管理的环绕建议应用于跨越多个对象(例如服务层中的所有业务操作)的一组方法。

5.2 Spring AOP能力和目标

Spring AOP是用纯Java实现的。 不需要特殊的编译过程。 Spring AOP不需要控制类加载器的层次结构,因此适合在Servlet容器或应用程序服务器中使用。

Spring AOP当前仅支持方法执行连接点(建议在Spring Bean上执行方法)。 尽管可以在不破坏核心Spring AOP API的情况下添加对字段拦截的支持,但并未实现字段拦截。 如果需要通知字段访问和更新连接点,请考虑使用诸如AspectJ之类的语言。

Spring AOP的AOP方法不同于大多数其他AOP框架。 目的不是提供最完整的AOP实现(尽管Spring AOP相当强大)。 相反,其目的是在AOP实现和Spring IoC之间提供紧密的集成,以帮助解决企业应用程序中的常见问题。

因此,例如,通常将Spring Framework的AOP功能与Spring IoC容器结合使用。 通过使用常规bean定义语法来配置方面(尽管这允许强大的“自动代理”功能)。 这是与其他AOP实现的关键区别。 使用Spring AOP不能轻松或高效地完成某些事情,例如建议非常细粒度的对象(通常是域对象)。 在这种情况下,AspectJ是最佳选择。 但是,我们的经验是,Spring AOP为企业Java应用程序中适合AOP的大多数问题提供了出色的解决方案。

Spring AOP从未努力与AspectJ竞争以提供全面的AOP解决方案。 我们认为,基于代理的框架(如Spring AOP)和成熟的框架(如AspectJ)都是有价值的,它们是互补的,而不是竞争。 Spring无缝地将Spring AOP和IoC与AspectJ集成在一起,以在基于Spring的一致应用程序架构中支持AOP的所有使用。 这种集成不会影响Spring AOP API或AOP Alliance API。 Spring AOP仍然向后兼容。 有关Spring AOP API的讨论,请参见下一章

Spring框架的中心宗旨之一是非侵入性。 这是一个想法,不应强迫您将特定于框架的类和接口引入业务或域模型。 但是,在某些地方,Spring Framework确实为您提供了将特定于Spring Framework的依赖项引入代码库的选项。 提供此类选项的理由是,在某些情况下,以这种方式阅读或编码某些特定功能可能会变得更加容易。 但是,Spring框架(几乎)总是为您提供选择:您可以自由地就哪个选项最适合您的特定用例或场景做出明智的决定。

与本章相关的一种选择是选择哪种AOP框架(以及哪种AOP样式)。 您可以选择AspectJ和/或Spring AOP。 您也可以选择@AspectJ注释样式方法或Spring XML配置样式方法。 本章选择首先介绍@AspectJ样式方法的事实不应被视为表明Spring团队相对于XML XML配置样式更喜欢@AspectJ注释样式方法。

有关每种样式的“为什么和为什么”的更完整讨论,请参见选择要使用的AOP声明样式。

5.3 AOP代理

Spring AOP默认将标准JDK动态代理用于AOP代理。 这使得可以代理任何接口(或一组接口)。

Spring AOP也可以使用CGLIB代理。 这对于代理类而不是接口是必需的。 默认情况下,如果业务对象未实现接口,则使用CGLIB。 由于对接口而不是对类进行编程是一种好习惯,因此业务类通常实现一个或多个业务接口。 在那些需要建议在接口上未声明的方法或需要将代理对象作为具体类型传递给方法的情况下(在极少数情况下),可以强制使用CGLIB。

掌握Spring AOP是基于代理的这一事实很重要。 有关完全了解此实现细节实际含义的详细信息,请参阅了解AOP代理

5.4 @AspectJ支持

@AspectJ是一种将切面声明为带有注释的常规Java类的样式。 @AspectJ样式是AspectJ项目在AspectJ 5版本中引入的。 Spring使用AspectJ提供的用于切入点解析和匹配的库来解释与AspectJ 5相同的注释。 但是,AOP运行时仍然是纯Spring AOP,并且不依赖于AspectJ编译器或编织器。

注意:

使用AspectJ编译器和weaver可以使用完整的AspectJ语言,有关在Spring Applications中使用AspectJ进行了讨论。

5.4.1 启用@AspectJ支持

要在Spring配置中使用@AspectJ切面,您需要启用Spring支持以基于@AspectJ方面配置Spring AOP,并根据这些方面是否建议对它们进行自动代理。 通过自动代理,我们的意思是,如果Spring确定一个或多个方面建议一个bean,它会自动为该bean生成一个代理来拦截方法调用并确保按需执行建议。

可以使用XML或Java样式的配置来启用@AspectJ支持。 无论哪种情况,您都需要确保AspectJ的Aspectjweaver.jar库位于应用程序的类路径(版本1.8或更高版本)上。 该库在AspectJ发行版的lib目录中或从Maven Central存储库中可用

通过Java配置启用@AspectJ支持

要通过Java @Configuration启用@AspectJ支持,请添加@EnableAspectJAutoProxy注解,如以下示例所示

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}

通过XML配置启用@AspectJ支持

<aop:aspectj-autoproxy/>

假定您使用基于XML Schema的配置中所述的架构支持。 有关如何在aop名称空间中导入标签的信息,请参见AOP模式。

5.4.2 声明一个切面

启用@AspectJ支持后,Spring会自动检测在应用程序上下文中使用@AspectJ方面(具有@Aspect注解)的类定义的任何bean,并用于配置Spring AOP。 接下来的两个示例显示了一个不太有用的方面所需的最小定义。

这两个示例中的第一个示例显示了应用程序上下文中的常规bean定义,该定义指向具有@Aspect注解的bean类:

<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect"><!-- configure properties of the aspect here -->
</bean>

这两个示例中的第二个示例显示了NotVeryUsefulAspect类定义,该类定义使用org.aspectj.lang.annotation.Aspect注释进行了注释;

package org.xyz;
import org.aspectj.lang.annotation.Aspect;@Aspect
public class NotVeryUsefulAspect {}

切面(使用@Aspect注释的类)可以具有方法和字段,与任何其他类相同。 它们还可以包含切入点,通知1和介绍(类型间)声明。

注意:

通过组件扫描自动检测切面

您可以将方面类注册为Spring XML配置中的常规bean,也可以通过类路径扫描来自动检测它们-与其他任何Spring管理的bean一样。 但是,请注意,@ Aspect注释不足以在类路径中进行自动检测。 为此,您需要添加一个单独的@Component注解(或者,或者,按照Spring的组件扫描程序的规则,有条件的自定义构造型注解)。

向其他切面提供通知:

在Spring AOP中,切面本身不能成为其他切面的通知目标。 类上的@Aspect注释将其标记为一个切面,因此将其从自动代理中排除。

5.4.3 声明切入点

切入点确定了感兴趣的连接点,从而使我们能够控制执行建议的时间。 Spring AOP仅支持Spring Bean的方法执行连接点,因此您可以将切入点视为与Spring Bean上的方法执行相匹配。 切入点声明由两部分组成:一个包含名称和任何参数的签名,以及一个切入点表达式,该切入点表达式精确确定我们感兴趣的方法执行。在AOP的@AspectJ注解样式中,常规方法定义提供了切入点签名。 ,并使用@Pointcut注解指示切入点表达式(用作切入点签名的方法必须具有void返回类型)。

一个示例可能有助于使切入点签名和切入点表达式之间的区别变得清晰。 下面的示例定义一个名为anyOldTransfer的切入点,该切入点与任何名为transfer的方法的执行相匹配:

@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature

形成@Pointcut注解的值的切入点表达式是常规的AspectJ 5切入点表达式。 有关AspectJ的切入点语言的完整讨论,请参见AspectJ编程指南(以及扩展,包括AspectJ 5开发人员手册)或有关AspectJ的书籍之一(如Colyer等人的Eclipse AspectJ,或《 AspectJ in Action》 ,由Ramnivas Laddad撰写)。

支持的切入点指示符

  • execution:用于匹配方法执行的连接点。 这是使用Spring AOP时要使用的主要切入点指示符。
  • within: 将匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法的执行)
  • this : 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中bean引用(Spring AOP代理)是给定类型的实例。
  • target : 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中目标对象(正在代理的应用程序对象)是给定类型的实例。
  • args : 限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中参数是给定类型的实例。
  • @target :限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中执行对象的类具有给定类型的注释。
  • @args :限制匹配的连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。
  • @within : 限制匹配到具有给定注解的类型内的连接点(使用Spring AOP时,使用给定注解在类型中声明的方法的执行)
  • @annotation :将匹配点限制为连接点的主题(在Spring AOP中正在执行的方法)具有给定注释的连接点。

其他切入点类型

完整的AspectJ切入点语言支持Spring不支持的其他切入点指示符:调用,获取,设置,预初始化,静态初始化,初始化,处理程序,建议执行,内部代码,cflow,cflowbelow(如果,@ this和@withincode)。 在Spring AOP解释的切入点表达式中使用这些切入点指示符会导致抛出IllegalArgumentException。

Spring AOP支持的切入点指示符集合可能会在将来的版本中扩展,以支持更多的AspectJ切入点指示符。

由于Spring AOP将匹配限制为仅方法执行连接点,因此前面对切入点指示符的讨论所提供的定义比AspectJ编程指南中的定义要窄。另外,AspectJ本身具有基于类型的语义,并且在执行连接点处,此对象和目标都引用相同的对象:执行该方法的对象。 Spring AOP是基于代理的系统,可区分代理对象本身(绑定到此对象)和代理后面的目标对象(绑定到目标)。

注意:

由于Spring的AOP框架基于代理的性质,因此根据定义,不会拦截目标对象内的调用。 对于JDK代理,只能拦截代理上的公共接口方法调用。 使用CGLIB时,将拦截代理上的公共方法和受保护方法(甚至必要时包括程序包可见的方法)。 但是,应始终通过公共签名设计通过代理进行的常见交互。

请注意,切入点定义通常与任何拦截方法匹配。 如果严格地将切入点设置为仅公开,即使在CGLIB代理方案中可能通过代理进行非公开交互,也需要相应地定义切入点。

如果您的拦截需要在目标类中包括方法调用甚至构造函数,请考虑使用Spring驱动的本机AspectJ编织,而不是Spring的基于代理的AOP框架。 这构成了具有不同特征的AOP使用模式,因此请确保在做出决定之前先熟悉编织。

Spring AOP还支持其他名为bean的PCD。 使用PCD,可以将联接点的匹配限制为特定的命名Spring Bean或一组命名Spring Bean(使用通配符时)。 Bean PCD具有以下形式:

总结:

就是切入点所在的那个对象 也就是目标对象,不会拦截对象内的调用 ,就是说会拦截 对象.方法() 这种格式的拦截 ,还有就是构造器也是不会被SpringAOP 拦截的 简单的分析一下就是 拦截发生在对象创建之后 在对象被创建之前 无法拦截;

bean(idOrNameOfBean)

idOrNameOfBean令牌可以是任何Spring bean的名称。 提供了使用*字符的有限通配符支持,因此,如果为Spring bean建立了一些命名约定,则可以编写bean PCD表达式来选择它们。 与其他切入点指定符一样,bean PCD可以与&&(和),||一起使用。 (或)和! (否定)运算符。

注意:

Bean PCD仅在Spring AOP中受支持,而在本机AspectJ编织中不受支持。 它是AspectJ定义的标准PCD的特定于Spring的扩展,因此不适用于@Aspect模型中声明的方面。

Bean PCD在实例级别(基于Spring bean名称概念构建)上运行,而不是仅在类型级别(基于编织的AOP受其限制)上运行。 基于实例的切入点指示符是Spring基于代理的AOP框架的特殊功能,并且与Spring bean工厂紧密集成,因此可以自然而直接地通过名称识别特定bean。

组合切入点表达式

您可以使用&&,||组合切入点表达式 和! 您也可以按名称引用切入点表达式。 以下示例显示了三个切入点表达式:

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
//anyPublicOperation是否匹配,如果方法执行连接点代表执行 任何公共方法。@Pointcut("within(com.xyz.someapp.trading..*)")//如果交易模块中有方法执行,则匹配。
private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()")
//如果一个方法执行代表该方法中的任何公共方法,则匹配交易模块
private void tradingOperation() {}

最佳实践是从较小的命名组件中构建更复杂的切入点表达式,如先前所示。 按名称引用切入点时,将应用常规的Java可见性规则(您可以看到相同类型的私有切入点,层次结构中受保护的切入点,任何位置的公共切入点,等等)。 可见性不影响切入点匹配。

共享通用切入点定义

在使用企业应用程序时,开发人员通常希望从多个方面引用应用程序的模块和特定的操作集。 我们建议为此定义一个“ SystemArchitecture”方面,以捕获常见的切入点表达式。 这样的方面通常类似于以下示例:

package com.xyz.someapp;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;@Aspect
public class SystemArchitecture {/*** A join point is in the web layer if the method is defined* in a type in the com.xyz.someapp.web package or any sub-package* under that.*/@Pointcut("within(com.xyz.someapp.web..*)")public void inWebLayer() {}/*** A join point is in the service layer if the method is defined* in a type in the com.xyz.someapp.service package or any sub-package* under that.*/@Pointcut("within(com.xyz.someapp.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.someapp.dao package or any sub-package* under that.*/@Pointcut("within(com.xyz.someapp.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.someapp.abc.service and com.xyz.someapp.def.service) then* the pointcut expression "execution(* com.xyz.someapp..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.someapp..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.someapp.dao.*.*(..))")public void dataAccessOperation() {}}

您可以在需要切入点表达式的任何地方引用在这样的方面中定义的切入点。 例如,要使服务层具有事务性,您可以编写以下内容:

<aop:config><aop:advisorpointcut="com.xyz.someapp.SystemArchitecture.businessService()"advice-ref="tx-advice"/>
</aop:config><tx:advice id="tx-advice"><tx:attributes><tx:method name="*" propagation="REQUIRED"/></tx:attributes>
</tx:advice>

在基于模式的AOP支持中讨论了<aop:config>和<aop:advisor>元素。 事务管理中讨论了事务元素

例子

Spring AOP用户可能最常使用执行切入点指示符。 执行表达式的格式如下:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)

除了返回类型模式(前面的代码片段中的ret-type-pattern),名称模式和参数模式以外的所有部分都是可选的。 返回类型模式确定该方法的返回类型必须是什么才能使连接点匹配。 最常用作返回类型模式。 它匹配任何返回类型。 仅当方法返回给定类型时,标准类型名称才匹配。 名称模式与方法名称匹配。 您可以将通配符用作名称模式的全部或一部分。 如果指定声明类型模式,请在其后加上。 将其加入名称模式组件。 参数模式稍微复杂一些:()匹配不带参数的方法,而(…)匹配任意数量(零个或多个)的参数。 (*)模式与采用任何类型的一个参数的方法匹配。 (,String)与采用两个参数的方法匹配。 第一个可以是任何类型,而第二个必须是字符串。 有关更多信息,请查阅AspectJ编程指南的“语言语义”部分。

以下示例显示了一些常用的切入点表达式:

总结:execution :细粒度到方法

  • 任何公共方法的执行:
execution(public * *(..))
  • 名称以set开头的任何方法的执行:
 execution(* set*(..))
  • AccountService接口定义的任何方法的执行:
execution(* com.xyz.service.AccountService.*(..))
  • service包中定义的任何方法的执行:
execution(* com.xyz.service.*.*(..))
  • service或其子包之一中定义的任何方法的执行:
execution(* com.xyz.service..*.*(..))

总结:within:细粒度到类型

  • 服务包中的任何连接点(仅在Spring AOP中执行方法)
 within(com.xyz.service.*)
  • 服务包或其子包之一中的任何连接点(仅在Spring AOP中执行方法):
 within(com.xyz.service..*)

总结:this(类型全限定名)

  • 代理实现AccountService接口的任何连接点(仅在Spring AOP中是方法执行):
 this(com.xyz.service.AccountService)

总结: target(类型全限定名)

  • 目标对象实现AccountService接口的任何连接点(仅在Spring AOP中执行方法):
 target(com.xyz.service.AccountService)

总结:args(参数类型列表)

  • 任何采用单个参数并且在运行时传递的参数为Serializable的连接点(仅在Spring AOP中是方法执行)
args(java.io.Serializable)
  • 目标对象具有@Transactional注解的任何连接点(仅在Spring AOP中是方法执行):
 @target(org.springframework.transaction.annotation.Transactional)
  • 目标对象的声明类型具有@Transactional注解的任何连接点(仅在Spring AOP中是方法执行):
 @within(org.springframework.transaction.annotation.Transactional)
  • 任何执行方法带有@Transactional注解的联接点(仅在Spring AOP中是方法执行):
 @annotation(org.springframework.transaction.annotation.Transactional)
  • 任何采用单个参数的联接点(仅在Spring AOP中是方法执行),并且传递的参数的运行时类型具有@Classified注解:
@args(com.xyz.security.Classified)
  • 名为tradeService的Spring bean上的任何连接点(仅在Spring AOP中执行方法)
 bean(tradeService)
  • Spring Bean上具有与通配符表达式* Service匹配的名称的任何连接点(仅在Spring AOP中执行方法):
 bean(*Service)

编写好的切入点

在编译期间,AspectJ处理切入点以优化匹配性能。 检查代码并确定每个连接点是否(静态或动态)匹配给定的切入点是一个昂贵的过程。 (动态匹配意味着无法从静态分析中完全确定匹配,并且在代码中进行测试以确定在运行代码时是否存在实际匹配)。 首次遇到切入点声明时,AspectJ将其重写为匹配过程的最佳形式。 这是什么意思? 基本上,切入点以DNF(析取范式)重写,并且对切入点的组件进行排序,以便首先检查那些较便宜的组件。 这意味着您不必担心理解各种切入点指示符的性能,并且可以在切入点声明中以任何顺序提供它们。

但是,AspectJ只能使用所告诉的内容。 为了获得最佳的匹配性能,您应该考虑他们试图实现的目标,并在定义中尽可能缩小匹配的搜索空间。 现有的指示符自然属于以下三类之一:同类,作用域和上下文:

  • 亲切的指定者选择一种特殊的连接点:执行,获取,设置,调用和处理程序。

  • 作用域指定者选择一组感兴趣的连接点(可能是多种):代码内和代码内

  • 上下文指示符根据上下文匹配(并可选地绑定):this,target和@annotation

写得好的切入点至少应包括前两种类型(种类和作用域)。 您可以包括上下文指示符以根据连接点上下文进行匹配,也可以绑定该上下文以在建议中使用。 仅提供同类的标识符或仅提供上下文的标识符是可行的,但是由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。 范围指示符的匹配非常快,使用它们的使用意味着AspectJ可以非常迅速地消除不应进一步处理的连接点组。 一个好的切入点应始终包括一个切入点。

5.4.4 通知Advice

Advice 与切入点表达式关联,并且在切入点匹配的方法执行 before, after, around运行。 切入点表达式可以是对命名切入点的简单引用,也可以是就地声明的切入点表达式。

前置Advice

您可以通过使用@Before注释在方面中声明Advice

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;@Aspect
public class BeforeExample {@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doAccessCheck() {// ...}
}

如果使用就地切入点表达式,则可以将前面的示例重写为以下示例

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;@Aspect
public class BeforeExample {@Before("execution(* com.xyz.myapp.dao.*.*(..))")public void doAccessCheck() {// ...}}

注意:

您可以在同一aspect内拥有多个advice 声明(以及其他成员)。 在这些示例中,我们仅显示单个advice 声明,以集中每个advice 的效果。

返回后Advice

返回Advice后,当匹配的方法执行正常返回时,运行Advice。 您可以使用@AfterReturning注解进行声明:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;@Aspect
public class AfterReturningExample {@AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doAccessCheck() {// ...}}

有时,您需要在建议正文中访问返回的实际值。 您可以使用@AfterReturning的形式绑定返回值以获取该访问权限,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;@Aspect
public class AfterReturningExample {@AfterReturning(pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",returning="retVal")//===等于下面通知方法里面的参数public void doAccessCheck(Object retVal) {// ...}}

返回属性中使用的名称必须与advice方法中的参数名称相对应。 当方法执行返回时,返回值将作为相应的参数值传递到通知方法。 返回子句还将匹配仅限制为返回指定类型值的方法执行(在这种情况下为Object,它匹配任何返回值)。

请注意,返回建议后使用时,不可能返回完全不同的参考。

抛出异常后通知Advice

抛出异常后通知,当匹配的方法执行通过抛出异常退出时运行通知。 您可以使用@AfterThrowing注解进行声明,如以下示例所示:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;@Aspect
public class AfterThrowingExample {@AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doRecoveryActions() {// ...}}

通常,您希望通知仅在引发给定类型的异常时才运行,并且您通常还需要访问通知正文中的引发异常。 您可以使用throwing属性来限制匹配(如果需要)(如果需要,请使用Throwable作为异常类型),并将抛出的异常绑定到advice参数。 以下示例显示了如何执行此操作:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;@Aspect
public class AfterThrowingExample {@AfterThrowing(pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",throwing="ex")public void doRecoveryActions(DataAccessException ex) {// ...}}

throwing属性中使用的名称必须与advice方法中的参数名称相对应。 当通过抛出异常退出方法执行时,该异常将作为相应的参数值传递给通知方法。 throwing子句还将匹配仅限制为引发指定类型的异常(在这种情况下为DataAccessException)的那些方法执行

After (Finally)Advice

当匹配的方法执行退出时,After (Finally)Advice 方法运行。 通过使用@After注释声明它。 之后必须准备处理正常和异常返回条件的建议。 它通常用于释放资源和类似目的。 以下示例显示了最终建议后的用法:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;@Aspect
public class AfterFinallyExample {@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")public void doReleaseLock() {// ...}}

环绕通知

最后一种通知是围绕通知。 围绕通知在匹配方法的执行过程中“围绕”运行。 它有机会在方法执行之前和之后进行工作,并确定何时,如何以及什至根本不执行该方法。 如果需要以线程安全的方式(例如,启动和停止计时器)在方法执行之前和之后共享状态,则通常使用环绕通知。 始终使用最不可行的通知形式来满足您的要求(也就是说,在通知可以使用之前,不要在通知周围使用)。

通过使用@Around注解来声明周围通知。 咨询方法的第一个参数必须是ProceedingJoinPoint类型。 在建议的正文中,在ProceedingJoinPoint上调用proce()会使基础方法执行。 前进方法也可以传入Object []。 数组中的值用作方法执行时的参数。

注意:

当用Object []调用时,procedes的行为与AspectJ编译器编译的aroundadvice的procedure行为略有不同。 对于使用传统的AspectJ语言编写的环绕通知,传递给proc的参数数量必须与传递给环绕通知的参数数量(而不是基础连接点采用的参数数量)相匹配,并且传递给 给定的参数位置会取代该值绑定到的实体的连接点处的原始值(不要担心,如果这现在没有意义)。 Spring采取的方法更简单,并且更适合其基于代理的,仅执行的语义。 仅当编译为Spring编写的@AspectJ方面并使用AspectJ编译器和weaver的参数进行处理时,才需要意识到这种区别。 有一种方法可以在Spring AOP和AspectJ之间100%兼容,并且在下面有关建议参数的部分中对此进行了讨论。

以下示例显示了如何使用环绕通知:

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.SystemArchitecture.businessService()")public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {// start stopwatchObject retVal = pjp.proceed();// stop stopwatchreturn retVal;}}

环绕通知返回的值是该方法的调用者看到的返回值。 例如,如果一个简单的缓存方面有一个值,则它可以从缓存中返回一个值,如果没有,则调用proce()。 请注意,在around通知的正文中,proc可能被调用一次,多次或完全不被调用。 所有这些都是合法的。

总结: 就是 在环绕通知里面pjp.proceed(); 方法可以条用一次或者多次 或者一次都不调用,就是比如说一个应用场景就是方法是在缓存里面那数据 可以使用环绕通知来说明缓存里面是不是有数据,如果没有数据的话就是调用方法就可以了,没有数据的时候就可以不调用 方法直接返回缓存里面方法的返回值就可以了;

通知参数

Spring提供了完全类型化的通知,这意味着您可以在通知签名中声明所需的参数(如我们先前在返回和抛出示例中所见),而不是一直使用Object []数组。 我们将在本节的后面部分介绍如何使参数和其他上下文值可用于通知主体。 首先,我们看一下如何编写通用通知,以了解通知当前通知的方法。

访问当前的JoinPoint

任何通知方法都可以将org.aspectj.lang.JoinPoint类型的参数声明为它的第一个参数(请注意,需要周围建议以声明ProceedingJoinPoint类型的第一个参数,它是JoinPoint的子类。JoinPoint接口提供了一个多种有用的方法:

  • getArgs():返回方法参数。

  • getThis():返回代理对象。

  • getTarget(): Returns the target object.

  • getSignature():返回所建议方法的描述。

  • toString():打印有关所建议方法的有用描述。

将参数传递给通知

我们已经看到了如何绑定返回的值或异常值(在返回和抛出建议后使用)。 要使参数值可用于通知正文,可以使用args的绑定形式。 如果在args表达式中使用参数名称代替类型名称,则在调用通知时会将相应参数的值作为参数值传递。 一个例子应该使这一点更加清楚。 假设您要建议以Account对象为第一个参数的DAO操作的执行,并且您需要在通知正文中访问该帐户。 您可以编写以下内容:

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {// ...
}

切入点表达式的args(account,…)部分有两个用途。 首先,它将匹配限制为仅方法采用至少一个参数并且传递给该参数的参数是Account实例的方法执行。 第二,它通过account参数使建议的实际Account对象可用。

编写此内容的另一种方法是声明一个切入点,当切入点与匹配点匹配时“提供” Account对象值,然后从通知中引用命名切入点。 如下所示:

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {// ...
}

有关更多详细信息,请参见AspectJ编程指南。

代理对象(this),目标对象(target)和注释(@ within,@ target,@ annotation和@args)都可以以类似的方式绑定。 接下来的两个示例显示如何匹配使用@Auditable注解进行注解的方法的执行并提取审核代码:

这两个示例中的第一个显示了@Auditable注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {AuditCode value();
}

这两个示例中的第二个示例显示了与@Auditable方法的执行相匹配的通知:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {AuditCode code = auditable.value();// ...
}

@annotation(auditable)就是标识方法上面带这个注解

通知参数和泛型

Spring AOP可以处理类声明和方法参数中使用的泛型。 假设您具有如下通用类型

public interface Sample<T> {void sampleGenericMethod(T param);void sampleGenericCollectionMethod(Collection<T> param);
}

您可以通过在要截获该方法的参数类型中键入advice参数,将截获方法类型限制为某些参数类型

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {// Advice implementation
}

方法不适用于通用集合。 因此,您不能按以下方式定义切入点:

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {// Advice implementation
}

为了使这项工作有效,我们将不得不检查集合的每个元素,这是不合理的,因为我们也无法决定通常如何处理空值。 要实现类似的目的,您必须将参数键入Collection <?>并手动检查元素的类型

确定参数名称

通知调用中的参数绑定依赖于切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称的匹配。 通过Java反射无法获得参数名称,因此Spring AOP使用以下策略来确定参数名称:

  • 如果用户已明确指定参数名称,则使用指定的参数名称。通知和切入点注释都具有可选的argNames属性,可用于指定带注释的方法的参数名称。 这些参数名称在运行时可用。 以下示例显示如何使用argNames属性:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {AuditCode code = auditable.value();// ... use code and bean
}

如果第一个参数是JoinPoint,ProceedingJoinPoint或JoinPoint.StaticPart类型,则可以从argNames属性的值中忽略该参数的名称。 例如,如果您修改前面的建议以接收连接点对象,则argNames属性不需要包括它:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {AuditCode code = auditable.value();// ... use code, bean, and jp
}

对JoinPoint,ProceedingJoinPoint和JoinPoint.StaticPart类型的第一个参数给予的特殊处理对于不收集任何其他联接点上下文的建议实例特别方便。 在这种情况下,您可以省略argNames属性。 例如,以下建议无需声明argNames属性:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {// ... use jp
}
  • 使用’argNames’属性有点笨拙,因此,如果未指定’argNames’属性,Spring AOP将查看该类的调试信息,并尝试从局部变量表中确定参数名称。 只要已使用调试信息(至少是“ -g:vars”)编译了类,此信息就会存在。 启用此标志时进行编译的结果是:(1)您的代码稍微易于理解(逆向工程),(2)类文件的大小略大(通常无关紧要),(3)删除未使用的本地代码的优化 变量不适用于您的编译器。 换句话说,通过启用该标志,您应该不会遇到任何困难。

如果即使没有调试信息,AspectJ编译器(ajc)都已编译@AspectJ方面,则无需添加argNames属性,因为编译器会保留所需的信息。

  • 如果在没有必要调试信息的情况下编译了代码,Spring AOP会尝试推断绑定变量与参数的配对(例如,如果切入点表达式中仅绑定了一个变量,并且advice方法仅接受一个参数,则配对 很明显)。 如果在给定可用信息的情况下变量的绑定是不明确的,则抛出AmbiguousBindingException。
  • 如果以上所有策略均失败,则抛出IllegalArgumentException。

通过参数调用

前面我们提到过,我们将描述如何编写一个在Spring AOP和AspectJ中始终有效的参数的proceed调用。 解决方案是确保建议签名按顺序绑定每个方法参数。 以下示例显示了如何执行此操作:

@Around("execution(List<Account> find*(..)) && " +"com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +"args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,String accountHolderNamePattern) throws Throwable {String newPattern = preProcess(accountHolderNamePattern);return pjp.proceed(new Object[] {newPattern});
}

在许多情况下,无论如何都要进行这种绑定(如上例所示)。

通知顺序

当多条通知都希望在同一连接点上运行时,会发生什么情况? Spring AOP遵循与AspectJ相同的优先级规则来确定建议执行的顺序。 优先级最高的建议首先在“途中”运行(因此,给定两条前置通知,则优先级最高的通知首先运行)。 从连接点“出路”时,优先级最高的通知将最后运行(因此,给定两条后置通知,优先级最高的建议将排名第二)。

当在不同方面定义的两条通知都需要在同一连接点上运行时,除非另行指定,否则执行顺序是不确定的。 您可以通过指定优先级来控制执行顺序。 通过在方面类中实现org.springframework.core.Ordered接口或使用Order注解对其进行注释,可以通过普通的Spring方法来完成。 给定两个方面,从Ordered.getValue()(或注释值)返回较低值的方面具有较高的优先级。

当在同一切面定义的两条通知都需要在同一连接点上运行时,其顺序是未定义的(因为无法通过反射来获取javac编译类的声明顺序)。 考虑将这些建议方法折叠为每个方面类中每个连接点的一个建议方法,或将建议重构为可在方面级别订购的单独方面类

总结 就是在不相同的切面类里面 可以使用order注解来决定优先级 前置通知优先级高的先 后置通知优先级高的后执行 ,在相同的切面方法里面的时候 顺序是未定义的,可以考虑拆分为两个切面类来实现顺序调用

5.4.5 引言

简介(在AspectJ中称为类型间声明)使切面可以声明通知对象实现给定的接口,并代表那些对象提供该接口的实现。

您可以使用@DeclareParents注解进行介绍。 此注解用于声明匹配类型具有新的父代(因此具有名称)。 例如,给定一个名为UsageTracked的接口和该接口名为DefaultUsageTracked的实现,以下方面声明服务接口的所有实现者也都实现了UsageTracked接口(例如,通过JMX公开统计信息):

@Aspect
public class UsageTracking {@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)public static UsageTracked mixin;@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")public void recordUsage(UsageTracked usageTracked) {usageTracked.incrementUseCount();}}

要实现的接口由带注释的字段的类型确定。 @DeclareParents注解的value属性是AspectJ类型的模式。 匹配类型的任何Bean均实现UsageTracked接口。 请注意,在前面示例的之前通知中,服务Bean可以直接用作UsageTracked接口的实现。 如果以编程方式访问bean,则应编写以下内容:

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

5.4.6 切面实例化模型

这是一个高级主题。 如果您刚开始使用AOP,则可以放心地跳过它,直到以后。

默认情况下,应用程序上下文中每个切面都有一个实例。 AspectJ将此称为单例实例化模型。 可以使用备用生命周期来定义方面。 Spring支持AspectJ的perthis和pertarget实例化模型(当前不支持percflow,percflowbelow和pertypewithin)。

您可以通过在@Aspect注解中指定perthis子句来声明perthis方面。 考虑以下示例:

@Aspect("perthis(com.xyz.myapp.SystemArchitecture.businessService())")
public class MyAspect {private int someState;@Before(com.xyz.myapp.SystemArchitecture.businessService())public void recordServiceUsage() {// ...}}

在前面的示例中,“ perthis”子句的作用是为每个执行业务服务的唯一服务对象(每个与切入点表达式匹配的联接点绑定到**“ this”的唯一对象**)创建一个方面实例。 方面实例是在服务对象上首次调用方法时创建的。 当服务对象超出范围时,方面将超出范围。 在创建切面实例之前,其中的任何通知都不会执行。 创建切面实例后,在其中声明的通知将在匹配的连接点处执行,但仅当服务对象是与此方面相关联的对象时才执行。 有关每个子句的更多信息,请参见AspectJ编程指南。

perthis表示如果某个类的代理类符合其指定的切面表达式,那么就会为每个符合条件的目标类都声明一个切面实例 参数指定方法调用的时候实例化这个切面 在实例化这个切面之前 切面里面的通知都是不会执行的

5.4.7 AOP示例

既然您已经了解了所有组成部分的工作方式,那么我们可以将它们组合在一起以做一些有用的事情。

业务服务的执行有时可能由于并发问题而失败(例如,死锁失败者)。 如果重试该操作,则很可能在下一次尝试中成功。 对于适合在这种情况下重试的业务(不需要为解决冲突而需要返回给用户的幂等操作),我们希望透明地重试该操作,以避免客户端看到PessimisticLockingFailureException。 这项要求清楚地跨越了服务层中的多个服务,因此非常适合通过一个方面实施。

因为我们想重试该操作,所以我们需要使用周围建议,以便可以多次调用proceed。 以下清单显示了基本方面的实现:

@Aspect
public class ConcurrentOperationExecutor implements Ordered {private static final int DEFAULT_MAX_RETRIES = 2;private int maxRetries = DEFAULT_MAX_RETRIES;private int order = 1;public void setMaxRetries(int maxRetries) {this.maxRetries = maxRetries;}public int getOrder() {return this.order;}public void setOrder(int order) {this.order = order;}@Around("com.xyz.myapp.SystemArchitecture.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;}}

请注意,切面实现了Ordered接口,因此我们可以将方面的优先级设置为高于事务建议(每次重试时都希望有新的事务)。 maxRetries和order属性均由Spring配置。 建议的主要动作发生在doConcurrentOperation中。 请注意,目前,我们将重试逻辑应用于每个businessService()。 我们尝试继续,如果失败并出现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>

为了改进切面,使其仅重试幂等运算,我们可以定义以下幂等注解:

round("com.xyz.myapp.SystemArchitecture.businessService() && " +"@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {// ...
}

5.6 选择要使用的AOP声明样式

一旦确定方面是实现给定需求的最佳方法,您如何在使用Spring AOP或AspectJ以及在Aspect语言(代码)样式,@ AspectJ注解样式或Spring XML样式之间做出决定? 这些决定受许多因素影响,包括应用程序需求,开发工具和团队对AOP的熟悉程度

5.6.1 Spring AOP还是全部的AspectJ?

使用最简单的方法即可。 Spring AOP比使用完整的AspectJ更简单,因为不需要在开发和构建过程中引入AspectJ编译器/编织器.如果您只需要通知在Spring bean上执行操作,则Spring AOP是正确的选择。 如果您需要通知不受Spring容器管理的对象(通常是域对象),则需要使用AspectJ。 如果您希望建议除简单方法执行之外的连接点(例如,字段get或设置连接点等),则还需要使用AspectJ。

使用AspectJ时,可以选择AspectJ语言语法(也称为“代码样式”)或@AspectJ注释样式。 显然,如果您不使用Java 5+,则已经为您做出了选择:使用代码样式。 如果方面在您的设计中起着重要作用,并且您能够将AspectJ开发工具(AJDT)插件用于Eclipse,则AspectJ语言语法是首选。 它更干净,更简单,因为该语言是专为编写方面而设计的。 如果您不使用Eclipse或只有少数几个方面在您的应用程序中不起作用,那么您可能需要考虑使用@AspectJ样式,在IDE中坚持常规Java编译,并向其中添加方面编织阶段 您的构建脚本。

5.6.2 @AspectJ或Spring AOP的XML?

如果选择使用Spring AOP,则可以选择@AspectJ或XML样式。 有各种折衷考虑。

XML样式可能是现有Spring用户最熟悉的,并且得到了真正的POJO的支持。 当使用AOP作为配置企业服务的工具时,XML是一个不错的选择(一个很好的测试是您是否将切入点表达式视为配置的一部分,而您可能希望独立更改)。 使用XML样式,可以说从您的配置中可以更清楚地了解系统中存在哪些方面。

XML样式有两个缺点。 首先,它没有完全将要解决的需求的实现封装在一个地方。 DRY原则说,系统中的任何知识都应该有单一,明确,权威的表示。 在使用XML样式时,关于如何实现需求的知识会在配置文件中的后备bean类的声明和XML中分散。 当您使用@AspectJ样式时,此信息将封装在一个模块中:方面。 其次,与@AspectJ样式相比,XML样式在表达能力上有更多限制:仅支持“单例”方面实例化模型,并且无法组合以XML声明的命名切入点。 例如,使用@AspectJ样式,您可以编写如下内容:

@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方法的缺点是您无法通过组合这些定义来定义

@AspectJ样式支持其他实例化模型和更丰富的切入点组合。 它具有将方面保持为模块化单元的优势。 它还具有Spring AOP和AspectJ都可以理解@AspectJ方面的优点。 因此,如果您以后决定需要AspectJ的功能来实现其他要求,则可以轻松地迁移到经典的AspectJ设置。 总而言之,Spring团队在自定义方面更喜欢@AspectJ样式,而不是简单地配置企业服务。

5.7 混合切面类型

通过使用自动代理支持,模式定义的<aop:aspect>方面,<aop:advisor>声明的顾问程序,甚至是同一配置中其他样式的代理和拦截器,完全可以混合@AspectJ样式的方面。 所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。

5.8 代理机制

通过使用自动代理支持,模式定义的<aop:aspect>方面,<aop:advisor>声明的顾问程序,甚至是同一配置中其他样式的代理和拦截器,完全可以混合@AspectJ样式的方面。 所有这些都是通过使用相同的基础支持机制实现的,并且可以毫无困难地共存。

Spring AOP使用JDK动态代理或CGLIB创建给定目标对象的代理。 JDK内置了JDK动态代理,而CGLIB是常见的开源类定义库(重新包装到spring-core中)。

如果要代理的目标对象实现至少一个接口,则使用JDK动态代理。 代理了由目标类型实现的所有接口。 如果目标对象未实现任何接口,则将创建CGLIB代理。

如果要强制使用CGLIB代理(例如,代理为目标对象定义的每个方法,而不仅是由其接口实现的方法),都可以这样做。 但是,您应该考虑以下问题:

  • 使用CGLIB,不能通知最终方法,因为它们不能在运行时生成的子类中被覆盖。

  • 从Spring 4.0开始,由于CGLIB代理实例是通过Objenesis创建的,因此不再调用代理对象的构造函数两次。 仅当您的JVM不允许绕过构造函数时,您才可能从Spring的AOP支持中看到两次调用和相应的调试日志条目

要强制使用CGLIB代理,请将<aop:config>元素的proxy-target-class属性的值设置为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 />元素上使用proxy-target-class =“ true”会强制对所有三个元素使用CGLIB代理 其中。

5.8.1 理解AOP代理

Spring AOP是基于代理的。 在编写自己的方面或使用Spring框架提供的任何基于Spring AOP的方面之前,掌握最后一条语句实际含义的语义至关重要。

首先考虑以下情形:您有一个普通的,未经代理的,没有特殊要求的,直接的对象引用,如以下代码片段所示:

public class SimplePojo implements Pojo {public void foo() {// this next method invocation is a direct call on the 'this' referencethis.bar();}public void bar() {// some logic...}
}

如果在对象引用上调用方法,则直接在该对象引用上调用该方法,如下图和清单所示:

public class Main {public static void main(String[] args) {Pojo pojo = new SimplePojo();// this is a direct method call on the 'pojo' referencepojo.foo();}
}

当客户端代码具有的引用是代理时,情况会稍有变化。 考虑以下图表和代码片段:

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())都会被调用。 反对这个参考,而不是代理.这具有重要的意义。 这意味着自调用不会导致与方法调用相关的通知得到执行的机会;

好吧,那该怎么办?最佳方法(在这里宽松地使用术语“最佳”)是重构代码,以免发生自调用。 这确实需要您做一些工作,但这是最好的,侵入性最小的方法。 下一种方法绝对可怕,我们正要指出这一点,恰恰是因为它是如此可怕。 您可以(对我们来说是痛苦的)完全将类中的逻辑绑定到Spring AOP,如以下示例所示:

public class SimplePojo implements Pojo {public void foo() {// this works, but... gah!((Pojo) AopContext.currentProxy()).bar();???????//bar()  如果是这样  不会触发bar()的通知方法  因为这个执行这个方法以及是代理对象内部的this  也就是被代理的对象本生了 在执行bar()  就是相当于原生的调用不会触发通知方法 //通过使用上述的方法  获取当前的代理对象 AopContext.currentProxy())使用的是ThreadLocal模式 会获取到当前的代理对象 再次触发才会执行bar() 方法的通知方法;}public void bar() {// some logic...}
}

这将您的代码完全耦合到Spring AOP,并且使类本身意识到在AOP上下文中使用它的事实,而AOP上下文却是这样。 创建代理时,它还需要一些其他配置,如以下示例所示:

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 以编程方式创建@AspectJ代理

除了使用<aop:config>或<aop:aspectj-autoproxy>声明配置中的各个方面外,还可以通过编程方式创建通知目标对象的代理。 有关Spring的AOP API的完整详细信息,请参阅下一章。 在这里,我们要重点介绍通过使用@AspectJ方面自动创建代理的功能。

您可以使用org.springframework.aop.aspectj.annotation.AspectJProxyFactory类为一个或多个@AspectJ方面建议的目标对象创建代理。 此类的基本用法非常简单,如以下示例所示:

// 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();

5.10 在Spring应用程序中使用AspectJ

到目前为止,本章介绍的所有内容都是纯Spring AOP。 在本节中,我们将研究如果您的需求超出了Spring AOP本身提供的功能,那么如何使用AspectJ编译器或weaver代替Spring AOP或除Spring AOP之外使用。

Spring附带了一个小的AspectJ方面库,该库在您的发行版中可以作为spring-aspects.jar独立使用。 您需要将其添加到类路径中才能使用其中的方面。 使用AspectJ依赖于Spring和AspectJ的其他Spring方面来注入域对象以及AspectJ讨论了该库的内容以及如何使用它。 使用Spring IoC配置AspectJ方面讨论了如何依赖注入使用AspectJ编译器编织的AspectJ方面。 最后,Spring Framework中使用AspectJ进行的加载时编织为使用AspectJ的Spring应用程序提供了加载时编织的介绍。

5.10.1 使用AspectJ通过Spring依赖注入域对象

Spring容器实例化并配置在您的应用程序上下文中定义的bean。 给定包含要应用的配置的Bean定义的名称,也可以要求Bean工厂配置预先存在的对象。 spring-aspects.jar包含注释驱动的方面,该方面利用此功能允许依赖项注入任何对象。 该支架旨在用于在任何容器的控制范围之外创建的对象。 域对象通常属于此类,因为它们通常是通过数据库查询的结果由新操作员或ORM工具以编程方式创建的。

@Configurable注释将一个类标记为符合Spring驱动的配置。 在最简单的情况下,您可以将其纯粹用作标记注释,如以下示例所示:


package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurable
public class Account {// ...
}

当以这种方式用作标记接口时,Spring通过使用具有与完全限定类型名称(com)相同名称的bean定义(通常为原型作用域)来配置带注释类型的新实例(在这种情况下为Account)。 xyz.myapp.domain.Account)。 由于bean的默认名称是其类型的全限定名,因此声明原型定义的简便方法是省略id属性,如以下示例所示:

<bean class="com.xyz.myapp.domain.Account" scope="prototype"><property name="fundsTransferService" ref="fundsTransferService"/>
</bean>

如果要显式指定要使用的原型bean定义的名称,则可以直接在注解中这样做,如以下示例所示:

package com.xyz.myapp.domain;import org.springframework.beans.factory.annotation.Configurable;@Configurable("account")
public class Account {// ...
}

Spring现在查找名为account的bean定义,并将其用作配置新Account实例的定义。

您也可以使用自动装配来避免完全指定专用的bean定义。 要让Spring应用自动装配,请使用@Configurable批注的autowire属性。 您可以指定@Configurable(autowire = Autowire.BY_TYPE)或@Configurable(autowire = Autowire.BY_NAME)分别按类型或名称进行自动装配。作为替代方案,最好为您的对象指定显式的,注释驱动的依赖项注入。 通过@Autowired或@Inject在字段或方法级别进行@Configurable Bean(有关更多详细信息,请参见基于注释的容器配置)

最后,您可以使用dependencyCheck属性(例如,@Configurable(autowire = Autowire.BY_NAME,dependencyCheck = true))为新创建和配置的对象中的对象引用启用Spring依赖检查。 如果此属性设置为true,则Spring在配置后验证是否已设置了所有属性(不是基元或集合)。

请注意,单独使用注释不会执行任何操作。 spring-aspects.jar中的AnnotationBeanConfigurerAspect对注释的存在起作用。 从本质上讲,方面说,“从初始化带有@Configurable注释的类型的新对象返回之后,使用Spring根据注释的属性配置新创建的对象”。 在这种情况下,“初始化”是指新实例化的对象(例如,使用new运算符实例化的对象)以及正在进行反序列化(例如,通过readResolve())的Serializable对象。

上段中的关键短语之一是“本质上”。 在大多数情况下,“从新对象的初始化返回后”的确切语义是可以的。 在这种情况下,“初始化后”是指在构造对象之后注入依赖项。 这意味着该依赖项不可在类的构造函数体中使用。 如果您希望在构造函数主体执行之前注入依赖项,从而可以在构造函数主体中使用这些依赖项,则需要在@Configurable声明中对此进行定义,如下所示:

@Configurable(preConstruction = true)///在构造函数之前注入依赖项

为此,必须将带注释的类型与AspectJ编织器编织在一起。 您可以使用构建时的Ant或Maven任务来执行此操作(例如,参见《 AspectJ开发环境指南》),也可以使用加载时编织(请参阅Spring Framework中的使用AspectJ进行加载时编织)。 Spring需要配置AnnotationBeanConfigurerAspect本身(以便获得对将用于配置新对象的bean工厂的引用)。 如果使用基于Java的配置,则可以将@EnableSpringConfigured添加到任何@Configuration类中,如下所示:

@Configuration
@EnableSpringConfigured
public class AppConfig {}

如果您更喜欢基于XML的配置,则Spring上下文名称空间定义了一个方便的context:spring-configured元素,您可以按以下方式使用它:

<context:spring-configured/>

在配置方面之前创建的@Configurable对象实例会导致向调试日志发出消息,并且未进行对象配置。 一个示例可能是Spring配置中的bean,当它由Spring初始化时会创建域对象。 在这种情况下,您可以使用depends-on bean属性来手动指定bean取决于配置方面。 下面的示例显示如何使用depends-on属性:

<bean id="myService"class="com.xzy.myapp.service.MyService"depends-on="org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect"><!-- ... --></bean>

除非您真的想在运行时依赖它的语义,否则不要通过bean配置器方面激活@Configurable处理。 特别是,请确保不要在通过容器注册为常规Spring bean的bean类上使用@Configurable。 这样做将导致两次初始化,一次是通过容器,一次是通过切面。

单元测试@Configurable对象

@Configurable支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。 如果AspectJ尚未编织@Configurable类型,则注释在单元测试期间不起作用。 您可以在被测对象中设置模拟或存根属性引用,然后照常进行。 如果AspectJ编织了@Configurable类型,您仍然可以像往常一样在容器外部进行单元测试,但是每次构造@Configurable对象时,您都会看到一条警告消息,指示该对象尚未由Spring配置。

使用多个应用程序上下文

用于实现@Configurable支持的AnnotationBeanConfigurerAspect是AspectJ单例切面。 单例切面的范围与静态成员的范围相同:每个类加载器都有一个切面实例来定义类型。 这意味着,如果您在同一个类加载器层次结构中定义多个应用程序上下文,则需要考虑在何处定义@EnableSpringConfigured bean,以及在何处将spring-aspects.jar放在类路径上。

考虑一个典型的Spring Web应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了通用的业务服务,支持那些服务所需的一切,以及每个Servlet的一个子应用程序上下文(其中包含该Servlet的特定定义)。 所有这些上下文共存于同一类加载器层次结构中,因此AnnotationBeanConfigurerAspect只能保存对其中一个的引用。 在这种情况下,我们建议在共享(父)应用程序上下文中定义@EnableSpringConfigured bean。 这定义了您可能想注入域对象的服务。 结果是,您无法使用@Configurable机制来配置域对象,该域对象引用的是在子(特定于servlet的)上下文中定义的bean的引用(无论如何,这可能不是您想做的事情)。

在同一容器中部署多个Web应用程序时,请确保每个Web应用程序通过使用其自己的类加载器(例如,将spring-aspects.jar放置在“ WEB-INF / lib”中)在spring-aspects.jar中加载类型。 如果将spring-aspects.jar仅添加到容器级的类路径中(并因此由共享的父类加载器加载),则所有Web应用程序共享相同的切面实例(这可能不是您想要的)。容器级别的切面方法

5.10.2 AspectJ的其他Spring方面

除了@Configurable方面,spring-aspects.jar还包含一个AspectJ方面,您可以使用该方面来驱动Spring的事务管理,以使用@Transactional批注来批注类型和方法。 这主要是针对希望在Spring容器之外使用Spring Framework的事务支持的用户。

解释@Transactional批注的方面是AnnotationTransactionAspect。 使用此方面时,必须注释实现类(或该类中的方法,或两者),而不是注释该类所实现的接口(如果有)。 AspectJ遵循Java的规则,即不继承接口上的注释。

类上的@Transactional批注指定用于执行该类中任何public 操作的默认事务语义。

类中方法上的@Transactional注释会覆盖类注释(如果存在)给出的默认事务语义。 可以注释任何可见性的方法,包括私有方法。 直接注释非公共方法是执行此类方法而获得事务划分的唯一方法。

从Spring Framework 4.2开始,spring-aspects提供了类似的方面,为标准javax.transaction.Transactional注释提供了完全相同的功能。 检查JtaAnnotationTransactionAspect了解更多详细信息。

对于希望使用Spring配置和事务管理支持但又不想(或不能)使用注释的AspectJ程序员,spring-aspects.jar还包含抽象方面,您可以扩展它们以提供自己的切入点定义。 有关更多信息,请参见AbstractBeanConfigurerAspect和AbstractTransactionAspect的来源。 例如,以下摘录显示了如何编写方面来使用与完全限定的类名匹配的原型bean定义来配置域模型中定义的对象的所有实例:

public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect {public DomainObjectConfiguration() {setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver());}// the creation of a new bean (any object in the domain model)protected pointcut beanCreation(Object beanInstance) :initialization(new(..)) &&SystemArchitecture.inDomainModel() &&this(beanInstance);
}

5.10.3 使用Spring IoC配置AspectJ Aspects

当您将AspectJ切面与Spring应用程序一起使用时,既自然又希望能够使用Spring配置这些切面。 AspectJ运行时本身负责切面的创建,并且通过Spring配置AspectJ创建的切面的方式取决于切面所使用的AspectJ实例化模型(per-xxx子句)。

AspectJ的大多数方面都是单例切面。 这些切面的配置很容易。 您可以创建一个bean定义,该bean定义按常规引用方面类型,并包括factory-method =“ aspectOf” bean属性。 这样可以确保Spring通过向AspectJ索要长宽比实例,而不是尝试自己创建实例来获得长宽比实例。 以下示例显示如何使用factory-method =“ aspectOf”属性

<bean id="profiler" class="com.xyz.profiler.Profiler"factory-method="aspectOf"> <property name="profilingStrategy" ref="jamonProfilingStrategy"/>
</bean>

非单例切面更难配置。 但是,可以通过创建原型Bean定义并使用spring-aspects.jar中的@Configurable支持来实现,一旦它们由AspectJ运行时创建了Bean,就可以配置切面实例。

如果您有一些要与AspectJ编织的@AspectJ方面(例如,对域模型类型使用加载时编织)以及要与Spring AOP一起使用的其他@AspectJ方面,那么这些方面都已在Spring中配置 ,您需要告诉Spring AOP @AspectJ自动代理支持,应使用配置中定义的@AspectJ方面的确切子集进行自动代理。 您可以通过在<aop:aspectj-autoproxy />声明中使用一个或多个元素来做到这一点。 每个元素都指定一个名称模式,并且只有名称与至少一个模式匹配的bean才可用于Spring AOP自动代理配置。 以下示例显示了如何使用元素:

<aop:aspectj-autoproxy><aop:include name="thisBean"/><aop:include name="thatBean"/>
</aop:aspectj-autoproxy>

不要被<aop:aspectj-autoproxy />元素的名称所迷惑。 使用它可以创建Spring AOP代理。 这里使用了@AspectJ样式的声明,但是没有涉及AspectJ运行时。

5.10.4 在Spring Framework中使用AspectJ进行加载时编织

6.Spring AOP API

上一章通过@AspectJ和基于架构的方面定义描述了Spring对AOP的支持。 在本章中,我们讨论了较低级别的Spring AOP API。 对于常见的应用程序,我们建议将Spring AOP与AspectJ切入点一起使用,如上一章所述。

6.1 Spring中的Pointcut API

本节描述了Spring如何处理关键切入点概念。

6.1.1 概念

Spring的切入点模型使切入点重用不受建议类型的影响。 您可以使用相同的切入点来定位不同的通知.org.springframework.aop.Pointcut接口是中央接口,用于将建议定向到特定的类和方法。 完整的界面如下:

public interface Pointcut {ClassFilter getClassFilter();MethodMatcher getMethodMatcher();}

将Pointcut接口分为两部分,可以重用类和方法匹配的部分以及细粒度的合成操作(例如与另一个方法匹配器执行“联合”)。

ClassFilter接口用于将切入点限制为给定的一组目标类。 如果matches()方法始终返回true,则将匹配所有目标类。 以下清单显示了ClassFilter接口定义:

public interface ClassFilter {boolean matches(Class clazz);
}

MethodMatcher接口通常更重要。 完整的界面如下:

public interface MethodMatcher {boolean matches(Method m, Class targetClass);boolean isRuntime();boolean matches(Method m, Class targetClass, Object[] args);

matchs(Method,Class)方法用于测试此切入点是否与目标类上的给定方法匹配。 创建AOP代理时可以执行此评估,以避免需要对每个方法调用进行测试。 如果两个参数的match方法对于给定的方法返回true,并且MethodMatcher的isRuntime()方法返回true,则在每次方法调用时都将调用三个参数的match方法。 这样,切入点就可以在执行目标建议之前立即查看传递给方法调用的参数。

大多数MethodMatcher实现都是静态的,这意味着它们的isRuntime()方法返回false。 在这种情况下,永远不会调用三参数匹配方法。

6.1.2 切入点的操作

Spring支持切入点上的操作(特别是联合和相交)。

联合表示两个切入点都匹配的方法。 交集是指两个切入点都匹配的方法。 联合通常更有用。 您可以通过使用org.springframework.aop.support.Pointcuts类中的静态方法或在同一包中使用ComposablePointcut类来编写切入点。 但是,使用AspectJ切入点表达式通常是一种更简单的方法。

6.1.3 AspectJ表达切入点

从2.0开始,Spring使用的最重要的切入点类型是org.springframework.aop.aspectj.AspectJExpressionPointcut。 这是一个切入点,该切入点使用AspectJ提供的库来解析AspectJ切入点表达式字符串。

有关支持的AspectJ切入点原语的讨论,请参见上一章。

6.1.4 便捷切入点实现

Spring提供了几种方便的切入点实现。 您可以直接使用其中一些。 其他的则应归入特定于应用程序的切入点中。

静态切入点

静态切入点基于方法和目标类,不能考虑方法的参数。 静态切入点足以满足大多数用途,并且最好。 首次调用方法时,Spring只能评估一次静态切入点。 之后,无需在每次方法调用时再次评估切入点。

本节的其余部分描述了Spring附带的一些静态切入点实现。

正则表达式切入点

指定静态切入点的一种明显方法是正则表达式。 除了Spring之外,还有几个AOP框架使之成为可能。 org.springframework.aop.support.JdkRegexpMethodPointcut是一个通用的正则表达式切入点,它使用JDK中的正则表达式支持。

使用JdkRegexpMethodPointcut类,可以提供模式字符串的列表。 如果其中任何一个匹配,则切入点的评估结果为true。 (因此,结果实际上是这些切入点的并集。)

<bean id="settersAndAbsquatulatePointcut"class="org.springframework.aop.support.JdkRegexpMethodPointcut"><property name="patterns"><list><value>.*set.*</value><value>.*absquatulate</value></list></property>
</bean>

Spring提供了一个名为RegexpMethodPointcutAdvisor的便捷类,该类使我们还可以引用一个Advice(请记住,Advice可以是拦截器,在,引发通知等之前)。 在幕后,Spring使用了JdkRegexpMethodPointcut。 使用RegexpMethodPointcutAdvisor可以简化接线,因为一个bean封装了切入点和通知,如以下示例所示:

6.1.5 切入点超类

Spring提供了有用的切入点超类,以帮助您实现自己的切入点。

因为静态切入点最有用,所以您可能应该子类化StaticMethodMatcherPointcut。 这仅需要实现一个抽象方法(尽管您可以覆盖其他方法以自定义行为)。 下面的示例演示如何对StaticMethodMatcherPointcut进行子类化:

class TestStaticPointcut extends StaticMethodMatcherPointcut {public boolean matches(Method m, Class targetClass) {// return true if custom criteria match}
}

动态切入点也有超类。 您可以将自定义切入点与任何通知类型一起使用。

Spring的更高版本可能会提供JAC提供的“语义切入点”的支持-例如,“更改目标对象中实例变量的所有方法”

6.2 Spring 通知 API

现在,我们可以检查Spring AOP如何处理建议。

6.2.1 Spring 生命周期

每个通知都是一个Spring bean。 建议实例可以在所有建议对象之间共享,或者对于每个建议对象都是唯一的。 这对应于每个类或每个实例的通知。

pre 通知最常用。 适用于一般通知,例如事务。 这些不依赖于代理对象的状态或添加新状态。 它们仅作用于方法和参数。

每个实例的通知都适合引入,以支持混合。 在这种情况下,通知将状态添加到代理对象。

您可以在同一AOP代理中混合使用共享通知和基于实例的通知。

6.2.2 spring的通知类型

Spring提供了几种通知类型,并且可以扩展以支持任意通知类型。 本节介绍基本概念和标准通知类型。

拦截环绕通知

Spring中最基本的建议类型是围绕建议的拦截。

对于使用方法拦截的通知,Spring符合AOP Alliance接口。 实现MethodInterceptor和围绕通知的类也应该实现以下接口:

public interface MethodInterceptor extends Interceptor {Object invoke(MethodInvocation invocation) throws Throwable;
}

invoke()方法的MethodInvocation参数公开了正在调用的方法,目标连接点,AOP代理以及该方法的参数。 invoke()方法应返回调用的结果:连接点的返回值。

以下示例显示了一个简单的MethodInterceptor实现:

public class DebugInterceptor implements MethodInterceptor {public Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("Before: invocation=[" + invocation + "]");Object rval = invocation.proceed();System.out.println("Invocation returned");return rval;}
}

请注意对MethodInvocation的proceed()方法的调用。 这沿着拦截器链向下到达连接点。 大多数拦截器都调用此方法并返回其返回值。 但是,MethodInterceptor像任何周围的通知一样,可以返回不同的值或引发异常,而不是调用proceed方法。 但是,您不希望在没有充分理由的情况下执行此操作。

前置通知

一种更简单的通知类型是前置通知。 这不需要MethodInvocation对象,因为它仅在进入方法之前被调用。

前置通知的主要优点在于,无需调用proce()方法,因此,不会因疏忽而未能沿拦截器链继续前进。

以下清单显示了MethodBeforeAdvice接口:

public interface MethodBeforeAdvice extends BeforeAdvice {void before(Method m, Object[] args, Object target) throws Throwable;
}

(尽管通常的对象适用于字段拦截,并且Spring不太可能实现,Spring的API设计允许先于前置通知 。)

请注意,返回类型为void。 通知可以在联接点执行之前插入自定义行为,但不能更改返回值。 如果之前的通知引发异常,它将中止拦截器链的进一步执行。 异常会传播回拦截链。 如果未选中它或在调用的方法的签名上,则将其直接传递给客户端。 否则,它将被AOP代理包装在未经检查的异常中。

以下示例显示了Spring中的before通知,该通知计算所有方法调用:

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

在通知可以与任何切入点一起使用之前。

抛出异常通知

如果联接点引发异常,则在联接点返回之后调用引发通知。 Spring提供类型化的抛出建议。 请注意,这意味着org.springframework.aop.ThrowsAdvice接口不包含任何方法。 它是一个标签接口,用于标识给定的对象实现了一个或多个类型的throws建议方法。 这些应采用以下形式:

afterThrowing([Method, args, target], subclassOfThrowable)

仅最后一个参数是必需的。 方法签名可以具有一个或四个自变量,具体取决于建议方法是否对该方法和自变量感兴趣。 接下来的两个清单显示了作为引发建议示例的类。

如果引发RemoteException(包括从子类),则调用以下通知:

public class RemoteThrowsAdvice implements ThrowsAdvice {public void afterThrowing(RemoteException ex) throws Throwable {// Do something with remote exception}
}

与前面的通知不同,下一个示例声明了四个参数,以便可以访问被调用的方法,方法参数和目标对象。 如果抛出ServletException,则调用以下通知:

public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {// Do something with all arguments}
}

最后一个示例说明如何在处理RemoteException和ServletException的单个类中使用这两种方法。 可以在单个类中组合任意数量的引发通知方法。 以下清单显示了最后一个示例:

Throws advice can be used with any pointcut.!!!

介绍通知(通知的一种)

Spring将介绍通知视为一种特殊的拦截通知。

介绍通知需要实现以下接口的IntroductionAdvisorIntroductionInterceptor

public interface IntroductionInterceptor extends MethodInterceptor {boolean implementsInterface(Class intf);
}

从AOP Alliance MethodInterceptor接口继承的invoke()方法必须实现引入。 也就是说,如果被调用的方法在引入的接口上,则引入拦截器负责处理方法调用,不能调用proceed()。

介绍通知不能与任何切入点一起使用,因为它仅适用于类,而不适用于方法级别。 您只能通过IntroductionAdvisor使用引入通知,它具有以下方法:

public interface IntroductionAdvisor extends Advisor, IntroductionInfo {ClassFilter getClassFilter();void validateInterfaces() throws IllegalArgumentException;
}public interface IntroductionInfo {Class<?>[] getInterfaces();
}

没有MethodMatcher,因此没有与介绍通知相关的Pointcut。 只有类过滤是合乎逻辑的

getInterfaces()方法返回此Advisor 引入的接口。

在内部使用validateInterfaces()方法来查看引入的接口是否可以由配置的IntroductionInterceptor实现。

考虑一下Spring测试套件中的一个示例,并假设我们想为一个或多个对象引入以下接口:

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旨在将引入的接口的实际实现委派给委派,从而隐藏使用侦听的方式。 您可以使用构造函数参数将委托设置为任何对象。 默认委托(使用无参数构造函数时)是这个。 因此,在下一个示例中,委托是DelegatingIntroductionInterceptor的LockMixin子类。 给定一个委托(默认情况下为本身),DelegatingIntroductionInterceptor实例将查找由委托实现的所有接口(IntroductionInterceptor除外),并支持针对其中任何一个的介绍。 诸如LockMixin的子类可以调用preventInterface(Class intf)方法来禁止不应公开的接口。 但是,无论IntroductionInterceptor准备支持多少个接口,IntroductionAdvisor都会使用控件来实际公开哪些接口。 引入的接口隐藏了目标对同一接口的任何实现。

因此,LockMixin扩展了DelegatingIntroductionInterceptor并实现了Lockable本身。 超类自动选择可支持Lockable的引入,因此我们不需要指定它。 我们可以通过这种方式引入任意数量的接口。

注意锁定实例变量的使用。 这有效地将附加状态添加到目标对象中保存的状态。

下面的示例显示示例LockMixin类:

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类:

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {public LockMixinAdvisor() {super(new LockMixin(), Lockable.class);}
}

我们可以非常简单地应用此顾问程序,因为它不需要配置。 (但是,如果没有IntroductionAdvisor,则无法使用IntroductionInterceptor。)与简介一样,顾问必须是按实例的,因为它是有状态的。 对于每个建议对象,我们需要一个LockMixinAdvisor实例,因此需要一个LockMixin实例。 顾问程序包含建议对象状态的一部分。

我们可以像其他任何顾问一样,通过使用Advised.addAdvisor()方法或XML配置中的(推荐方式)以编程方式应用此顾问。 下面讨论的所有代理创建选择(包括“自动代理创建器”)都可以正确处理介绍和有状态的混合。

6.3 Spring的Advisor API

在Spring中,顾问程序是一个方面,仅包含与切入点表达式关联的单个建议对象。

除了介绍的特殊情况外,任何顾问都可以与任何建议一起使用。 org.springframework.aop.support.DefaultPointcutAdvisor是最常用的顾问类。 它可以与MethodInterceptor,BeforeAdvice或ThrowsAdvice一起使用。

可以在同一AOP代理中的Spring中混合使用顾问和建议类型。 例如,您可以在一个代理配置中使用对建议的拦截,抛出建议和建议之前。 Spring自动创建必要的拦截器链。

总结 :

通知Advice是Spring提供的一种切面(Aspect)。但其功能过于简单,只能
将切面织入到目标类的所有目标方法中,无法完成将切面织入到指定目标方法中。

顾问Advisor是Spring提供的另一种切面。其可以完成更为复杂的切面织入功能。PointcutAdvisor是顾问的一种,可以指定具体
的切入点。顾问将通知进行了包装,会根据不同的通知类型,在不同的时间点,将切面织入到不同的切入点。
PointcutAdvisor接口有两个较为常用的实现类:
*:NameMatchMethodPointcutAdvisor 名称匹配方法切入点顾问
*:RegexpMethodPointcutAdvisor 正则表达式匹配方法切入点顾问

6.4 使用ProxyFactoryBean创建AOP代理

如果您将Spring IoC容器(ApplicationContext或BeanFactory)用于您的业务对象(应该如此!),则要使用Spring的AOP FactoryBean实现之一。 (请记住,工厂bean引入了一个间接层,允许它创建其他类型的对象。)

Spring AOP支持还在后台使用了工厂bean。

在Spring中创建AOP代理的基本方法是使用org.springframework.aop.framework.ProxyFactoryBean。 这样可以完全控制切入点,任何适用的建议及其顺序。 但是,如果不需要这样的控制,则有一些更简单的选项是可取的。

6.4.1 基本

像其他Spring FactoryBean实现一样,ProxyFactoryBean引入了一个间接级别。 如果定义一个名为foo的ProxyFactoryBean,则引用foo的对象将看不到ProxyFactoryBean实例本身,而是看到由ProxyFactoryBean中的getObject()方法的实现创建的对象。 此方法创建一个包装目标对象的AOP代理

使用ProxyFactoryBean或另一个支持IoC的类创建AOP代理的最重要好处之一是,建议和切入点也可以由IoC管理。 这是一项强大的功能,可以实现某些其他AOP框架难以实现的方法。 例如,受益于依赖注入提供的所有可插入性,建议本身可以引用应用程序对象(目标之外,目标应该在任何AOP框架中可用)。

6.4.2 JavaBean属性

与Spring随附的大多数FactoryBean实现一样,ProxyFactoryBean类本身就是JavaBean。 其属性用于:

指定要代理的目标。

指定是否使用CGLIB(稍后介绍,另请参见基于JDK和CGLIB的代理)。

一些关键属性是从org.springframework.aop.framework.ProxyConfig(Spring中所有AOP代理工厂的超类)继承的。 这些关键属性包括:

  • proxyTargetClass:如果要替代目标类而不是目标类的接口,则为true。 如果将此属性值设置为true,则将创建CGLIB代理(另请参见基于JDK和CGLIB的代理)。

  • 优化:控制是否对通过CGLIB创建的代理应用激进的优化。 除非您完全了解相关的AOP代理如何处理优化,否则不要随意使用此设置。 当前仅用于CGLIB代理。 它对JDK动态代理无效。

  • Frozen:如果代理配置被冻结,则不再允许对配置进行更改。 这对于进行轻微的优化以及在不希望调用者在创建代理后希望调用者能够(通过Advised接口)操纵代理的情况下都是有用的。 此属性的默认值为false,因此允许进行更改(例如添加其他建议)。

  • excludeProxy:确定是否应在ThreadLocal中公开当前代理(前面有讲过 在拦截的方法里面调用被代理的方法通过this调用),以便目标可以访问它。 如果目标需要获取代理,并且暴露代理属性设置为true,则目标可以使用AopContext.currentProxy()方法。

ProxyFactoryBean特有的其他属性包括:

  • proxyInterfaces:字符串接口名称的数组。 如果未提供,则使用目标类的CGLIB代理(另请参见基于JDK和CGLIB的代理)。

  • InterceptorNames:顾问,拦截器或其他要应用的建议名称的String数组。 顺序很重要,先到先得。 也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。名称是当前工厂中的bean名称,包括祖先工厂中的bean名称。 您不能在此提及bean引用,因为这样做会导致ProxyFactoryBean忽略建议的单例设置。您可以在拦截器名称后加上星号(*)。 这样做会导致应用所有顾问Bean,其名称以要应用星号的部分开头。 您可以在使用“全局”顾问程序中找到使用此功能的示例。

  • 单例:无论调用getObject()方法的频率如何,工厂是否应返回单个对象。 一些FactoryBean实现提供了这种方法。 默认值是true。 如果要使用有状态的建议(例如,对于有状态的混合),请使用原型建议以及单例值false。

6.4.3 基于JDK和CGLIB的代理

本部分是有关ProxyFactoryBean如何选择为特定目标对象(将被代理)创建基于JDK的代理或基于CGLIB的代理的权威性文档

在Spring的1.2.x版和2.0版之间,ProxyFactoryBean的行为基于创建基于JDK或CGLIB的代理而改变。 在自动检测接口方面,ProxyFactoryBean现在表现出与TransactionProxyFactoryBean类类似的语义。

如果要代理的目标对象的类(以下简称为目标类)没有实现任何接口,则将创建基于CGLIB的代理。这是最简单的情况,因为JDK代理是基于接口的,并且没有接口意味着甚至无法进行JDK代理。您可以插入目标bean并通过设置interceptorNames属性来指定拦截器列表。 请注意,即使ProxyFactoryBean的proxyTargetClass属性设置为false,也会创建基于CGLIB的代理。(这样做没有任何意义,最好将其从bean定义中删除,因为它充其量是多余的,并且在最糟的情况下会造成混淆。)

如果目标类实现一个(或多个)接口,则创建的代理类型取决于ProxyFactoryBean的配置。

如果ProxyFactoryBean的proxyTargetClass属性已设置为true,则将创建基于CGLIB的代理。 这是有道理的,并且符合最小惊讶原则。 即使将ProxyFactoryBean的proxyInterfaces属性设置为一个或多个完全限定的接口名称,proxyTargetClass属性设置为true的事实也会导致基于CGLIB的代理生效

如果ProxyFactoryBean的proxyInterfaces属性已设置为一个或多个完全限定的接口名称,则将创建一个基于JDK的代理。 创建的代理实现了proxyInterfaces属性中指定的所有接口。 如果目标类恰好实现了比proxyInterfaces属性中指定的接口更多的接口,那很好,但是返回的代理不实现那些其他接口。

如果尚未设置ProxyFactoryBean的proxyInterfaces属性,但是目标类确实实现了一个(或多个)接口,则ProxyFactoryBean会自动检测到目标类实际上至少实现了一个接口以及基于JDK的代理 被建造。 实际代理的接口是目标类实现的所有接口。 实际上,这与将目标类实现的每个接口的列表提供给proxyInterfaces属性相同。 但是,它的工作量大大减少,而且不容易出现印刷错误。

6.4.4 代理接口

考虑一个简单的ProxyFactoryBean操作示例。 此示例涉及:

代理的目标bean。 这是示例中的personTarget bean定义。

  • 用于提供建议的顾问和拦截器。

  • 一个AOP代理bean定义,用于指定目标对象(personTarget bean),代理接口以及要应用的建议。

以下清单显示了示例:

<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属性采用String列表,其中包含当前工厂中的拦截器或顾问程序的bean名称。 您可以在返回之前,之后使用顾问程序,拦截器并引发通知对象。 顾问的顺序很重要。

您可能想知道为什么列表中不包含bean引用。 这样做的原因是,如果ProxyFactoryBean的singleton属性设置为false,则它必须能够返回独立的代理实例。 如果任何顾问本身就是原型,则需要返回一个独立的实例,因此必须能够从工厂获得原型的实例。 保持引用是不够的。可以使用前面显示的person Bean定义代替Person实现,如下所示

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代理代替“真实”人的实现。 但是,其类将是动态代理类。 可以将其强制转换为Advised接口(稍后讨论)。

您可以使用匿名内部bean隐藏目标和代理之间的区别。 仅ProxyFactoryBean定义不同。 该建议仅出于完整性考虑。

6.4.5 代理类

如果您需要代理一类,而不是一个或多个接口,该怎么办?

想象一下,在我们之前的示例中,没有Person接口。 我们需要建议一个名为Person的类,该类没有实现任何业务接口。 在这种情况下,可以将Spring配置为使用CGLIB代理而不是动态代理。 为此,请将前面显示的ProxyFactoryBean的proxyTargetClass属性设置为true。 尽管最好对接口而不是对类进行编程,但是在处理遗留代码时,建议未实现接口的类的功能可能会很有用。 (通常,Spring并不是规定性的。虽然可以轻松地应用良好实践,但可以避免强制采用特定方法。)

如果需要,即使您确实有接口,也可以在任何情况下强制使用CGLIB。

  • CGLIB代理通过在运行时生成目标类的子类来工作。 Spring配置此生成的子类以将方法调用委托给原始目标。 子类用于实现Decorator模式,并编织在建议中。

CGLIB代理通常应对用户透明。 但是,有一些问题要考虑:

  • 不能建议最终方法,因为它们不能被覆盖。

  • 无需将CGLIB添加到您的类路径中。 从Spring 3.2开始,CGLIB被重新打包并包含在spring-core JAR中。 换句话说,基于CGLIB的AOP就像JDK动态代理一样“开箱即用”。

CGLIB代理和动态代理之间几乎没有性能差异。 在这种情况下,性能不应作为决定性的考虑因素。

6.4.6 使用“全局”顾问

通过在拦截器名称后附加星号,所有具有与该星号之前的部分匹配的Bean名称的顾问程序都将添加到顾问程序链中。 如果您需要添加标准的“全局”顾问程序集,这可能会派上用场。 以下示例定义了两个全局顾问程序:

<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 简洁的代理定义

特别是在定义事务代理时,您可能会得到许多类似的代理定义。 使用父子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的示例中,我们通过将abstract属性设置为true来将父bean定义显式标记为抽象,如前所述,因此实际上可能不会实例化它。 默认情况下,应用程序上下文(但不是简单的bean工厂)会预先实例化所有单例。 因此,重要的是(至少对于单例bean),如果您有一个(父)bean定义仅打算用作模板,并且此定义指定了一个类,则必须确保将abstract属性设置为 真正。 否则,应用程序上下文实际上会尝试对其进行实例化。

6.6 使用ProxyFactory以编程方式创建AOP代理

使用Spring以编程方式创建AOP代理很容易。 这使您可以在不依赖Spring IoC的情况下使用Spring AOP。

由目标对象实现的接口将被自动代理。 以下清单显示了使用一个拦截器和一个顾问程序为目标对象创建代理的过程:

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继承)上还有便捷的方法,可让您添加其他建议类型,例如before并引发建议。 AdvisedSupport是ProxyFactory和ProxyFactoryBean的超类。

在大多数应用程序中,将AOP代理创建与IoC框架集成在一起是最佳实践。 通常,我们建议您使用AOP从Java代码外部化配置。

6.7 操作通知对象

6.8 使用“自动代理”功能

6.9 使用TargetSource实现

Spring提供了TargetSource的概念,以org.springframework.aop.TargetSource接口表示。 该接口负责返回实现连接点的“目标对象”。 每当AOP代理处理方法调用时,都会向TargetSource实现请求目标实例。

使用Spring AOP的开发人员通常不需要直接与TargetSource实现一起工作,但这提供了一种强大的手段来支持池化,可热插拔和其他复杂的目标。 例如,通过使用池来管理实例,池TargetSource可以为每个调用返回不同的目标实例。

如果未指定TargetSource,则使用默认实现包装本地对象。 每次调用都返回相同的目标(与您期望的一样)。

本节的其余部分描述了Spring随附的标准目标源以及如何使用它们

使用自定义目标源时,目标通常需要是原型而不是单例bean定义。 这样,Spring可以在需要时创建一个新的目标实例。

6.9.1 可热交换的目标源

org.springframework.aop.target.HotSwappableTargetSource的存在是为了允许AOP代理的目标切换,同时允许调用者保留对其的引用。

更改目标来源的目标会立即生效。 HotSwappableTargetSource是线程安全的。

您可以通过在HotSwappableTargetSource上使用swap()方法来更改目标,如以下示例所示:

HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper");
Object oldTarget = swapper.swap(newTarget);
<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 汇集目标源

使用池目标源提供了与无状态会话EJB相似的编程模型,在无状态会话EJB中,维护了相同实例的池,并且方法调用将释放池中的对象。

Spring池和SLSB池之间的关键区别在于,Spring池可以应用于任何POJO。 通常,与Spring一样,可以以非侵入方式应用此服务。

Spring提供对Commons Pool 2.2的支持,该池提供了相当有效的池实现。 您需要在应用程序的类路径上使用公共池Jar才能使用此功能。 您也可以将org.springframework.aop.target.AbstractPoolingTargetSource子类化以支持任何其他池化API。

<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>

请注意,目标对象(前面示例中的businessObjectTarget)必须是原型。 这使PoolingTargetSource实现可以创建目标的新实例,以根据需要扩展池。 有关其属性的信息,请参见AbstractPoolingTargetSource的javadoc和您希望使用的具体子类。 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中的拦截器名称列表中。

PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject");
System.out.println("Max pool size is " + conf.getMaxSize());

通过使用自动代理,可以实现更简单的池化。 您可以设置任何自动代理创建者使用的TargetSource实现

6.9.4 ThreadLocal目标源

如果您需要为每个传入请求(每个线程)创建一个对象,则ThreadLocal目标源非常有用。 ThreadLocal的概念提供了JDK范围的功能,可以透明地将资源与线程一起存储。 设置ThreadLocalTargetSource几乎与针对其他类型的目标源所说明的相同,如以下示例所示:

<bean id="threadlocalTargetSource" class="org.springframework.aop.target.ThreadLocalTargetSource"><property name="targetBeanName" value="businessObjectTarget"/>
</bean>

在多线程和多类加载器环境中错误使用ThreadLocal实例时,会遇到严重的问题(可能导致内存泄漏)。 您应该始终考虑在其他一些类中包装threadlocal,并且永远不要直接使用ThreadLocal本身(包装类中除外)。 另外,您应该始终记住正确设置取消设置线程本地资源的设置和取消设置(后者仅涉及对ThreadLocal.set(null)的调用)。 在任何情况下都应进行取消设置,因为不取消设置可能会导致出现问题。 Spring的ThreadLocal支持为您做到了这一点,应该始终考虑使用ThreadLocal实例,而无需其他适当的处理代码。

7. 空安全

尽管Java不允许您使用其类型系统来表示空安全性,但Spring Framework现在在org.springframework.lang包中提供了以下注释,以使您声明API和字段的空性:

  • @Nullable:表示特定参数,返回值或字段可以为null的注释。

  • @NonNull:指示特定参数,返回值或字段不能为null的注释(分别适用于@NonNullApi和@NonNullFields的参数/返回值和字段不需要)。

  • @NonNullApi:程序包级别的注释,它声明非null为参数和返回值的默认语义。

  • @NonNullFields:程序包级别的注释,它声明非null为字段的默认语义。

Spring框架本身利用了这些注释,但是它们也可以在任何基于Spring的Java项目中使用,以声明null安全的API和可选的null安全的字段。 尚不支持泛型类型参数,varargs(可变参数)和数组元素的可空性,但应在即将发布的版本中使用它们,有关最新信息,请参见SPR-15942。 可空性声明有望在Spring Framework版本之间进行微调,包括次要版本。 在方法主体内部使用的类型的可空性超出了此功能的范围。

7.1 用例

除了为Spring Framework API可空性提供显式声明之外,IDE(例如IDEA或Eclipse)还可以使用这些注释来提供与空安全性相关的有用警告,从而避免在运行时出现NullPointerException。

由于Kotlin原生支持null安全,因此它们还用于在Kotlin项目中使Spring API为null安全。 Kotlin支持文档中提供了更多详细信息。

7.2 JSR-305元注释

Spring注释使用JSR 305注释(休眠但广泛使用的JSR)进行元注释。 JSR-305元注释使工具供应商(如IDEA或Kotlin)能够以通用方式提供null安全支持,而无需对Spring注释进行硬编码支持。

既不需要也不建议向项目类路径添加JSR-305依赖项以利用Spring空安全API。 只有诸如在其代码库中使用空安全注释的基于Spring的库之类的项目才应添加com.google.code.findbugs:jsr305:3.0.2的compileOnly Gradle配置或Maven提供的范围,以避免编译警告。

8. 数据缓冲区和编解码器

Java NIO提供了ByteBuffer,但是许多库在顶部构建了自己的字节缓冲区API,特别是对于网络操作,其中重用缓冲区和/或使用直接缓冲区对性能有利。 例如,Netty具有ByteBuf层次结构,Undertow使用XNIO,Jetty使用池字节缓冲区以及要释放的回调,依此类推。 spring-core模块提供了一组抽象,可与各种字节缓冲区API配合使用,如下所示:

  • DataBufferFactory抽象数据缓冲区的创建。

  • DataBuffer表示一个字节缓冲区,可以将其合并。

  • DataBufferUtils提供了用于数据缓冲区的实用程序方法。

  • 编解码器将流数据缓冲区流解码或编码为更高级别的对象。

8.1 数据缓冲区工厂

DataBufferFactory用于通过以下两种方式之一创建数据缓冲区:

分配一个新的数据缓冲区,可以选择预先指定容量(如果已知),即使DataBuffer的实现可以按需增长和缩小,该容量也会更有效。

包装一个现有的byte []或java.nio.ByteBuffer,它们用DataBuffer实现来修饰给定的数据,并且不涉及分配。

请注意,WebFlux应用程序不会直接创建DataBufferFactory,而是通过客户端的ServerHttpResponse或ClientHttpRequest访问它。 工厂的类型取决于基础客户端或服务器,例如 NettyDataBufferFactory用于Reactor Netty,DefaultDataBufferFactory用于其他。

8.2 数据缓冲区

DataBuffer接口提供与java.nio.ByteBuffer类似的操作,但还带来了一些其他好处,其中一些是受Netty ByteBuf启发的。 以下是部分好处清单:

  • 具有独立位置的读取和写入,即不需要调用flip()在读取和写入之间交替。

  • 与java.lang.StringBuilder一样,容量可以按需扩展。

  • 通过PooledDataBuffer进行缓冲池和引用计数。

  • 将缓冲区查看为java.nio.ByteBuffer,InputStream或OutputStream。

  • 确定给定字节的索引或最后一个索引。

8.3 PooledDataBuffer

如Javadoc中针对ByteBuffer所述,字节缓冲区可以是直接的也可以是非直接的。 直接缓冲区可以驻留在Java堆之外,从而无需复制本机I / O操作。 这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是它们的创建和释放也更昂贵,这导致了缓冲池的想法。(zero copy)

PooledDataBuffer是DataBuffer的扩展,可帮助进行引用计数,这对于字节缓冲区池至关重要。 它是如何工作的? 分配PooledDataBuffer时,引用计数为1。调用keep()会增加计数,而调用release()会减少计数。 只要计数大于0,就保证不会释放缓冲区。 当计数减少到0时,可以释放池中的缓冲区,这实际上意味着将为缓冲区保留的内存返回到内存池。

请注意,与其直接在PooledDataBuffer上进行操作,不如在大多数情况下,最好使用DataBufferUtils中的便捷方法,仅当它是PooledDataBuffer的实例时,才将释放或保留应用于DataBuffer。

8.4 DataBufferUtils

  • DataBufferUtils提供了许多实用程序方法来对数据缓冲区进行操作:

  • 将数据缓冲区流合并到单个缓冲区中,可能具有零个副本,例如 通过复合缓冲区(如果基础字节缓冲区API支持的话)。

  • 将InputStream或NIO通道转换为Flux ,反之亦然,将Publisher 转换为OutputStream或NIO通道。

  • 如果缓冲区是PooledDataBuffer的实例,则释放或保留DataBuffer的方法。

  • 从字节流中跳过或获取,直到特定的字节数为止。

8.5 编解码器

org.springframework.core.codec包提供以下策略接口:

  • 编码器,用于将Publisher 编码为数据缓冲区流。

  • 解码器,用于将Publisher 解码为更高级别的对象流。

spring-core模块提供byte [],ByteBuffer,DataBuffer,Resource和String编码器和解码器实现。 spring-web模块添加了Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers和其他编码器和解码器。 请参阅WebFlux部分中的编解码器。

8.6 使用DataBuffer

使用数据缓冲区时,必须特别小心以确保释放缓冲区,因为它们可能会被合并。 我们将使用编解码器来说明其工作原理,但这些概念会更普遍地应用。 让我们看看编解码器必须在内部执行哪些操作来管理数据缓冲区。

在创建更高级别的对象之前,解码器是最后一个读取输入数据缓冲区的对象,因此,它必须按以下方式释放它们:

  1. 如果解码器只是读取每个输入缓冲区并准备立即释放它,则可以通过DataBufferUtils.release(dataBuffer)这样做。

  2. 如果Decoder使用的是Flux或Mono运算符,例如flatMap,reduce和其他在内部预取和缓存数据项的运算符,或者正在使用诸如filter,skip的运算符以及其他遗漏项的运算符,则doOnDiscard(PooledDataBuffer.class,DataBufferUtils 必须将::: release)添加到组合链中,以确保在丢弃此类缓冲区之前将其释放,这也可能是错误或取消信号的结果。

  3. 如果解码器以任何其他方式保留一个或多个数据缓冲区,则它必须确保在完全读取时释放它们,或者在读取和释放缓存的数据缓冲区之前发生错误或取消信号的情况下。

请注意,DataBufferUtils#join提供了一种安全有效的方法来将数据缓冲区流聚合到单个数据缓冲区中。 同样,skipUntilByteCount和takeUntilByteCount是供解码器使用的其他安全方法。

编码器分配其他人必须读取(释放)的数据缓冲区。 因此,编码器无事可做。 但是,如果在用数据填充缓冲区时发生序列化错误,则编码器必须小心释放数据缓冲区。 例如:

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上运行时,有用于调试缓冲区泄漏的调试选项。

,它声明非null为参数和返回值的默认语义。

  • @NonNullFields:程序包级别的注释,它声明非null为字段的默认语义。

Spring框架本身利用了这些注释,但是它们也可以在任何基于Spring的Java项目中使用,以声明null安全的API和可选的null安全的字段。 尚不支持泛型类型参数,varargs(可变参数)和数组元素的可空性,但应在即将发布的版本中使用它们,有关最新信息,请参见SPR-15942。 可空性声明有望在Spring Framework版本之间进行微调,包括次要版本。 在方法主体内部使用的类型的可空性超出了此功能的范围。

7.1 用例

除了为Spring Framework API可空性提供显式声明之外,IDE(例如IDEA或Eclipse)还可以使用这些注释来提供与空安全性相关的有用警告,从而避免在运行时出现NullPointerException。

由于Kotlin原生支持null安全,因此它们还用于在Kotlin项目中使Spring API为null安全。 Kotlin支持文档中提供了更多详细信息。

7.2 JSR-305元注释

Spring注释使用JSR 305注释(休眠但广泛使用的JSR)进行元注释。 JSR-305元注释使工具供应商(如IDEA或Kotlin)能够以通用方式提供null安全支持,而无需对Spring注释进行硬编码支持。

既不需要也不建议向项目类路径添加JSR-305依赖项以利用Spring空安全API。 只有诸如在其代码库中使用空安全注释的基于Spring的库之类的项目才应添加com.google.code.findbugs:jsr305:3.0.2的compileOnly Gradle配置或Maven提供的范围,以避免编译警告。

8. 数据缓冲区和编解码器

Java NIO提供了ByteBuffer,但是许多库在顶部构建了自己的字节缓冲区API,特别是对于网络操作,其中重用缓冲区和/或使用直接缓冲区对性能有利。 例如,Netty具有ByteBuf层次结构,Undertow使用XNIO,Jetty使用池字节缓冲区以及要释放的回调,依此类推。 spring-core模块提供了一组抽象,可与各种字节缓冲区API配合使用,如下所示:

  • DataBufferFactory抽象数据缓冲区的创建。

  • DataBuffer表示一个字节缓冲区,可以将其合并。

  • DataBufferUtils提供了用于数据缓冲区的实用程序方法。

  • 编解码器将流数据缓冲区流解码或编码为更高级别的对象。

8.1 数据缓冲区工厂

DataBufferFactory用于通过以下两种方式之一创建数据缓冲区:

分配一个新的数据缓冲区,可以选择预先指定容量(如果已知),即使DataBuffer的实现可以按需增长和缩小,该容量也会更有效。

包装一个现有的byte []或java.nio.ByteBuffer,它们用DataBuffer实现来修饰给定的数据,并且不涉及分配。

请注意,WebFlux应用程序不会直接创建DataBufferFactory,而是通过客户端的ServerHttpResponse或ClientHttpRequest访问它。 工厂的类型取决于基础客户端或服务器,例如 NettyDataBufferFactory用于Reactor Netty,DefaultDataBufferFactory用于其他。

8.2 数据缓冲区

DataBuffer接口提供与java.nio.ByteBuffer类似的操作,但还带来了一些其他好处,其中一些是受Netty ByteBuf启发的。 以下是部分好处清单:

  • 具有独立位置的读取和写入,即不需要调用flip()在读取和写入之间交替。

  • 与java.lang.StringBuilder一样,容量可以按需扩展。

  • 通过PooledDataBuffer进行缓冲池和引用计数。

  • 将缓冲区查看为java.nio.ByteBuffer,InputStream或OutputStream。

  • 确定给定字节的索引或最后一个索引。

8.3 PooledDataBuffer

如Javadoc中针对ByteBuffer所述,字节缓冲区可以是直接的也可以是非直接的。 直接缓冲区可以驻留在Java堆之外,从而无需复制本机I / O操作。 这使得直接缓冲区对于通过套接字接收和发送数据特别有用,但是它们的创建和释放也更昂贵,这导致了缓冲池的想法。(zero copy)

PooledDataBuffer是DataBuffer的扩展,可帮助进行引用计数,这对于字节缓冲区池至关重要。 它是如何工作的? 分配PooledDataBuffer时,引用计数为1。调用keep()会增加计数,而调用release()会减少计数。 只要计数大于0,就保证不会释放缓冲区。 当计数减少到0时,可以释放池中的缓冲区,这实际上意味着将为缓冲区保留的内存返回到内存池。

请注意,与其直接在PooledDataBuffer上进行操作,不如在大多数情况下,最好使用DataBufferUtils中的便捷方法,仅当它是PooledDataBuffer的实例时,才将释放或保留应用于DataBuffer。

8.4 DataBufferUtils

  • DataBufferUtils提供了许多实用程序方法来对数据缓冲区进行操作:

  • 将数据缓冲区流合并到单个缓冲区中,可能具有零个副本,例如 通过复合缓冲区(如果基础字节缓冲区API支持的话)。

  • 将InputStream或NIO通道转换为Flux ,反之亦然,将Publisher 转换为OutputStream或NIO通道。

  • 如果缓冲区是PooledDataBuffer的实例,则释放或保留DataBuffer的方法。

  • 从字节流中跳过或获取,直到特定的字节数为止。

8.5 编解码器

org.springframework.core.codec包提供以下策略接口:

  • 编码器,用于将Publisher 编码为数据缓冲区流。

  • 解码器,用于将Publisher 解码为更高级别的对象流。

spring-core模块提供byte [],ByteBuffer,DataBuffer,Resource和String编码器和解码器实现。 spring-web模块添加了Jackson JSON,Jackson Smile,JAXB2,Protocol Buffers和其他编码器和解码器。 请参阅WebFlux部分中的编解码器。

8.6 使用DataBuffer

使用数据缓冲区时,必须特别小心以确保释放缓冲区,因为它们可能会被合并。 我们将使用编解码器来说明其工作原理,但这些概念会更普遍地应用。 让我们看看编解码器必须在内部执行哪些操作来管理数据缓冲区。

在创建更高级别的对象之前,解码器是最后一个读取输入数据缓冲区的对象,因此,它必须按以下方式释放它们:

  1. 如果解码器只是读取每个输入缓冲区并准备立即释放它,则可以通过DataBufferUtils.release(dataBuffer)这样做。

  2. 如果Decoder使用的是Flux或Mono运算符,例如flatMap,reduce和其他在内部预取和缓存数据项的运算符,或者正在使用诸如filter,skip的运算符以及其他遗漏项的运算符,则doOnDiscard(PooledDataBuffer.class,DataBufferUtils 必须将::: release)添加到组合链中,以确保在丢弃此类缓冲区之前将其释放,这也可能是错误或取消信号的结果。

  3. 如果解码器以任何其他方式保留一个或多个数据缓冲区,则它必须确保在完全读取时释放它们,或者在读取和释放缓存的数据缓冲区之前发生错误或取消信号的情况下。

请注意,DataBufferUtils#join提供了一种安全有效的方法来将数据缓冲区流聚合到单个数据缓冲区中。 同样,skipUntilByteCount和takeUntilByteCount是供解码器使用的其他安全方法。

编码器分配其他人必须读取(释放)的数据缓冲区。 因此,编码器无事可做。 但是,如果在用数据填充缓冲区时发生序列化错误,则编码器必须小心释放数据缓冲区。 例如:

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上运行时,有用于调试缓冲区泄漏的调试选项。

Spring-Core 中文翻译+总结文档(下)相关推荐

  1. Spring-Core 中文翻译+总结文档(上)

    Spring - Core 引言 使用版本 5.2.5RELEASE 这一部分的文档覆盖了几乎所有的Spring 框架的技术,Spring框架最主要的控制反转(IOC容器),在对Spring框架的IO ...

  2. 太赞了:《Spring Framework 4.x 参考文档》最新中文版开放下载!

    前言 <Spring Framework Reference Documentation>是Spring官方出的文档,但是是英文版导致很多国内的程序员阅读起来有一定的难度. 好在,有国内的 ...

  3. 怎么翻译Word文档?这里有Word文档翻译小妙招

    Word文档你会翻译嘛?文字翻译对大家来说很简单,直接进行释义就好了,但是怎么翻译Word文档呢?今天小编就要带大家一起来了解下Word文档翻译的小妙招,感兴趣的不妨来看看,说不定哪天你真的会用到哦! ...

  4. (转载)中文Appium API 文档

    该文档是Testerhome官方翻译的源地址:https://github.com/appium/appium/tree/master/docs/cn官方网站上的:http://appium.io/s ...

  5. 中文翻译韩文软件有哪些?

    关于中文翻译韩文的软件对我们日常生活中可能不会起到什么作用,但是在办公中往往会起到很大的作用特别是对于一些外贸公司而言,翻译软件是他们经常会使用到的办公工具,那么中文翻译韩文的软件有哪些呢?下面的俩种 ...

  6. 中文 Appium API 文档

    该文档是Testerhome官方翻译的 源地址:https://github.com/appium/appium/tree/master/docs/cn 官方网站上的:http://appium.io ...

  7. Qt中文翻译(官方文档,界面,工具等)集锦

    Qt中文翻译(官方文档,界面,工具等)集锦 GitHub - jiangcaiyang/QtCreator-Translation-CN: This repository is trying to f ...

  8. .NET操作RabbitMQ组件EasyNetQ使用中文简版文档。

    本文出自EasyNetQ官方文档,内容为自己理解加翻译.文档地址:https://github.com/EasyNetQ/EasyNetQ/wiki/Quick-Start EasyNetQ简介 Ea ...

  9. iText创建一个含有中文的pdf文档

    有朋友问我pdfbox支不支持向pdf文档中写入中文.然后试了好多遍都是有乱码,也找了好多资料没有找到解决办法. 但是在查找资料的过程中发现了另一个处理pdf的开源库iText.官方介绍 http:/ ...

最新文章

  1. FFmpeg实现获取USB摄像头视频流测试代码
  2. 【JavaScript】Ubuntu16.04安装vscode+npm+yarn
  3. UVA122 树的层次遍历 Trees on the level(两种方法详解)
  4. python制作图形化小游戏_创意编程|Python的GUI简易界面设计测测你的反应力
  5. BCG、阿里、百度联合发布中国互联网经济白皮书2.0,解读“中国互联网新篇章:迈向产业融合”...
  6. ubuntu c++检测usb口事件变化_从MacBook支持USB-C口充电看电脑标配充电器发展史
  7. Ubuntu 16.04 LTS, 64bit,cuda 8, Caffe环境配置编译和安装
  8. struts2自定义拦截器一——模拟登陆权限验证
  9. 从数组随机抽取5个不重复_Power Query 如何保证随机抽取元素不重复
  10. php-fpm哪里下载_centos – Nginx PHP-FPM提供.php文件作为下载
  11. win10英文系统一键装机教程
  12. Atitit 编程范式总结 目录 1.1.1. IP(Imperative Programming)指令式编程 1 1.1.2. SP(Structured Programming)结构化编程 2 1
  13. 关于CUDA,cuDNN,TF,CUDA驱动版本兼容问题
  14. c#图片反色,取底色,照片底色效果
  15. Linux内核ncsi驱动源码分析(一)
  16. 15k的php会什么,【后端开辟】15k的php须要控制什么手艺
  17. 盘点波卡生态潜力项目 | 跨链特性促进多赛道繁荣
  18. 爱莫科技线下营销智能“四部曲”,推进快消品行业数智化创新
  19. 机器学习---之量纲与无量纲
  20. arm linux vector_swi分析

热门文章

  1. 【小技巧】word文档编辑技巧(一)
  2. MySQL5.7解压版配置
  3. 推送加密的PDF报表
  4. Docker,系统影分身之术
  5. 解决服装老手瓶颈——服装店铺软件全方面调控
  6. Leetcode-寻找两个有序数组的中位数
  7. Java中final类是存放在哪_详解Java中的final关键字
  8. java中final常量用法
  9. 【Linux】查看硬盘(fdisk | lsblk)
  10. 扇形微带偏置线的分析与作用