IoC 容器
Bean 的作用域
作用域 | 使用 | 描述 |
---|---|---|
singleton | @Scope("singleton") | IoC 容器(ApplicationContext)范围单例 |
prototype | @Scope("prototype") | 每次获取该 Bean 都会 new 一个新的对象返回 |
request | @Scope("request") 或 @RequestScope | 单次 HTTP 请求范围单例 |
session | @Scope("session") 或 @SessionScope | 单个 HTTP 会话范围单例 |
application | @Scope("application") 或 @ApplicationScope | Web 应用(ServletContext)范围单例 |
websocket | @Scope("websocket") | 单个 WebSocket 会话范围单例 |
-
自定义作用域
实现
org.springframework.beans.factory.config.Scope
接口,并将其对象注册到 BeanFactory 中,然后即可通过@Scope("...")
方式使用。
Spring 内置了一个线程范围单例作用域org.springframework.context.support.SimpleThreadScope
,但默认没有注册,可通过如下方式注册:@Bean public static CustomScopeConfigurer customScopeConfigurer() { CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer(); customScopeConfigurer.addScope("thread", new SimpleThreadScope()); return customScopeConfigurer; }
-
当在 singleton Bean 中注入短作用域 Bean 时,需要通过 AOP 为短作用域 Bean 生成代理 Bean,才能确保在 singleton Bean 中每次获取到最新的短作用域 Bean。配置
@Scope
注解的proxyMode
属性即可。比如:@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
proxyMode 属性可配置值如下:
代理模式 描述 ScopedProxyMode.DEFAULT 默认值,等同于 NO ScopedProxyMode.NO 不创建代理 ScopedProxyMode.INTERFACES 通过实现接口的方式生成代理(JDK),注入到接口时适用 ScopedProxyMode.TARGET_CLASS 通过继承的方式生成代理(CGLIB),非 final 类适用
Bean 的生命周期
-
在 Spring 管理的 Bean 中,可以使用
@PostConstruct
和@PreDestroy
注解标注需要在 Bean 初始化之后和销毁之前需要执行的方法。比如:@Service public class Foo { @PostConstruct public void init() { // 该方法会在 Bean 初始化完成、装载所有依赖之后、AOP 拦截器应用到该 Bean 之前执行 } @PreDestroy public void destroy() { // 该方法会在 Bean 销毁之前执行 } }
-
通过
@Bean
注解的initMethod
和destroyMethod
属性指定初始化之后和销毁之前需要执行的方法。比如:public class Foo { public void init() { // initialization logic } } public class Bar { public void cleanup() { // destruction logic } } @Configuration public class AppConfig { @Bean(initMethod = "init") public Foo foo() { return new Foo(); } @Bean(destroyMethod = "cleanup") public Bar bar() { return new Bar(); } }
注意: 如果不指定
@Bean
注解的destroyMethod
属性,默认以public
类型的close
方法或shutdown
方法作为销毁方法。如果开发者的类中有这些方法且不希望作为销毁方法,需指定destroyMethod = ""
。 -
让 Bean 实现 InitializingBean 和 DisposableBean 接口,afterPropertiesSet() 方法和 destroy() 方法会在该 Bean 初始化之后和销毁之前执行。比如:
@Compenent public class AnotherExampleBean implements InitializingBean, DisposableBean { @Override public void afterPropertiesSet() { // do some initialization work } @Override public void destroy() { // do some destruction work (like releasing pooled connections) } }
@Autowired
@Autowired
注解可用在 Bean 的有参构造函数上。如果只有一个有参构造函数,该注解可省略;如果有多个有参构造函数(且无无参构造函数),必须在其中一个构造函数上声明。-
@Autowired
注解也可用在数组、Set 等数据结构上。比如:@Component public class MovieRecommender { @Autowired private MovieCatalog[] movieCatalogs; @Autowired private Set<MovieCatalog> movieCatalogs; }
目标 Bean 即 MovieCatalog 可通过实现
org.springframework.core.Ordered
接口、使用@Order
注解指定其在数组、List 中的顺序,默认为注册顺序。 @Autowired
注解也可用在 Map 上(Map 的 Key 必须为 String 类型)。-
@Autowired
注解标注的属性或方法如果没有找到可装载的 Bean,Spring 会抛出异常,与@Required
注解作用相同。如下三种方式可避免异常抛出:@Component public class SimpleMovieLister { @Autowired(required = false) public void setMovieFinder(MovieFinder movieFinder) { ... } @Autowired public void setMovieFinder(Optional<MovieFinder> movieFinder) { ... // Java 8 支持 } @Autowired public void setMovieFinder(@Nullable MovieFinder movieFinder) { ... // Spring Framework 5.0 支持 } }
@Primary
@Autowired
注解基于类型自动装载,当有多个类型匹配的 Bean 可供装载但只需要装载一个时,Spring 会优先装载 @Primary
注解标注的目标 Bean。
@Qualifier
@Autowired
注解基于类型自动装载,当有多个类型匹配的 Bean 可供装载但只需要装载一个时,可配合使用@Qualifier
注解(的 value 属性)指定目标 Bean 的名称。当需要装载到数组、List、Set、Map 等数据结构时,也可配合使用
@Qualifier
注解缩小装载范围。
@Component、@Repository、@Service、@Controller
@Component
注解标注当前类为受 Spring 管理的通用组件,@Repository
、@Service
、@Controller
是 @Component
的特殊形式,分别对应持久层、服务层、表现层,方便对每个层进行针对性的拓展。
@Profile
@Profile
注解表示当前组件在何种 Environment 下适用。只有满足当前 Environment 的组件才会被注册到 Spring 的上下文中。比如为开发和生产环境指定不同的数据源:
@Configuration
@Profile("dev")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
@Profile
注解也可以在方法级别使用:
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
通过配置 Environment 属性 spring.profiles.active
来激活 Profile。比如在 Spring Boot 中配置 application.properties:
spring.profiles.active=dev
如果没有指定需要激活的 Profile,Spring 会激活名为 default
的默认 Profile,如下配置会生效:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
@PropertySource
@PropertySource
注解提供了一种方便的机制来将 PropertySource 增加到 Spring 的 Environment 之中:
# /com/myco/app.properties
testbean.name=myTestBean
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
@Value
@Value
注解可以将 Environment 中的属性注入到对象属性中:
@Value("${testbean.name}")
private String testBeanName;
@ConfigurationProperties(Spring Boot 特性)
Spring Boot 的 @ConfigurationProperties
注解可以很方便的将属性批量注入到一个配置对象中:
# /com/myco/app.properties
my.name=example
my.port=8080
my.servers[0]=dev.bar.com
my.servers[1]=foo.bar.com
@Compenent
@PropertySource("classpath:/com/myco/app.properties")
@ConfigurationProperties(prefix="my")
public class Config {
private String name;
private Integer port;
private List<String> servers = new ArrayList<String>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public List<String> getServers() {
return servers;
}
public void setServers(List<String> servers) {
this.servers = servers;
}
}
事件
ApplicationContext
中的事件处理是通过 ApplicationEvent
类和 ApplicationListener
接口提供的。 如果一个实现 ApplicationListener
接口的 Bean 被部署到上下文中,则每当 ApplicationEvent
发布到 ApplicationContext
时,都会通知该 Bean。本质上,这是标准的观察者模式。
Spring 提供了以下几种标准事件:
事件 | 描述 |
---|---|
ContextRefreshedEvent | 在 ApplicationContext 初始化或刷新时发布。例如,使用 ConfigurableApplicationContext 接口上的 refresh() 方法。 这里的“初始化”意味着所有的 Bean 都被加载,检测并激活后置处理器 Bean,单例被预先实例化,并且 ApplicationContext 对象已经可以使用了。 只要上下文没有关闭,只要所选的 ApplicationContext 实际上支持这种“热”刷新,刷新可以被触发多次。例如,XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。 |
ContextStartedEvent | 在 ApplicationContext 启动时发布,使用 ConfigurableApplicationContext 接口上的 start() 方法时。这里的“开始”意味着所有的生命周期 Bean 都会收到明确的启动信号。通常,这个信号用于在显式停止后重新启动 Bean,但也可以用于启动尚未配置为自动启动的组件,例如尚未启动的组件。 |
ContextStoppedEvent | 在 ApplicationContext 停止时发布,使用 ConfigurableApplicationContext 接口上的 stop() 方法时。 这里“停止”意味着所有生命周期的 Bean 都会收到明确的停止信号。 停止的上下文可以通过 start() 调用重新启动。 |
ContextClosedEvent | 在 ApplicationContext 关闭时发布,在 ConfigurableApplicationContext 接口上使用 close() 方法时。 这里的“关闭”意味着所有的单例 Bean 被销毁。 一个关闭的上下文到达其生命的尽头; 它不能刷新或重新启动。 |
RequestHandledEvent | 一个 Web 特定的事件,告诉所有的 Bean 一个 HTTP 请求已被处理。 此事件在请求完成后发布。 此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。 |
可通过继承 ApplicationEvent
自定义事件:
public class BlackListEvent extends ApplicationEvent {
private final String address;
private final String test;
public BlackListEvent(Object source, String address, String test) {
super(source);
this.address = address;
this.test = test;
}
// accessor and other methods...
}
可通过调用 ApplicationEventPublisher
的 publishEvent()
方法发布事件:
@Service
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String text) {
if (blackList.contains(address)) {
BlackListEvent event = new BlackListEvent(this, address, text);
publisher.publishEvent(event);
return;
}
// send email...
}
}
可通过 ApplicationListener
的 onApplicationEvent(event)
回调方法处理事件:
@Component
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
}
也可通过 @EventListener
注解标注的方法处理事件:
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
修改方法返回事件类型即可支持处理完当前事件后发布另一个事件:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
使用 @Async
注解异步处理事件:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
注意:
- 异步处理事件的方法抛出的异常不会被发布者接收到
- 异步处理事件的方法不能通过返回事件类型的对象发布新的事件
使用 @Order
注解指定事件处理方法调用的优先级:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
类型转换、字段格式化与验证
使用 BeanWrapper 操作 JavaBeans
JavaBean 是指遵循统一标准的简单类,拥有无参构造器,所有属性都有遵循命名约定的
getter/setter
方法。举例来说:名为bingoMadness
的属性将具有getBingoMadness()
和setBingoMadness(...)
方法。
Spring 提供了 BeanWrapper
接口和它的实现类 BeanWrapperImpl
,可以以一种通用便捷的方式操作 JavaBean。
-
使用
getPropertyValue
和setPropertyValues
方法对 JavaBean 的属性进行读写操作。支持如下表达式:
表达式 描述 name 标示名为 name
的属性,对应的方法getName()/isName()
和setName(...)
account.name 标示名为 account
的属性的嵌套属性name
,对应的方法如getAccount().getName()
和getAccount().setName()
account[2] 标示名为 account
的属性的第 3 个元素,该属性可以是数组、列表或者其它有自然顺序的集合account[COMPANYNAME] 标示名为 account
的属性的key
为COMPANYNAME
键值对对应的value
值,该属性可以是 Map假设有如下两个类:
public class Company { private String name; private Employee managingDirector; public String getName() { return this.name; } public void setName(String name) { this.name = name; } public Employee getManagingDirector() { return this.managingDirector; } public void setManagingDirector(Employee managingDirector) { this.managingDirector = managingDirector; } } 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; } }
下列代码展示了如何检索和操作
Companies
和Employees
属性: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");
类型转换
-
类型转换器 Converter
package org.springframework.core.convert.converter; public interface Converter<S, T> { T convert(S source); }
通过实现
Converter
接口可以定义自己的类型转换器。范型S
表示需要转换的类型,范型T
表示转换成的类型。
下面是一个简单的例子,定义了一个可以将 String 对象转成 Integer 对象的转换器:final class StringToInteger implements Converter<String, Integer> { public Integer convert(String source) { return Integer.valueOf(source); } }
注意:
convert
方法的入参不能为null
,如果入参不符合要求,应该抛出IllegalArgumentException
异常。如果转换失败,可以抛出非检查型异常。 -
类型转换器工厂
ConverterFactory
package org.springframework.core.convert.converter; public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); }
当开发者需要集中整个类层次结构的转换逻辑时,可以实现
ConverterFactory
接口。范型S
表示需要转换的类型,范型R
表示转换成的类型的范围,范型T
表示转换成的具体类型,T
是R
的子类。
比如,将String
对象转换成java.lang.Enum
对象的转换器工厂: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()); } } }
-
通用类型转换器
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
方法时,需要提供源类型和目标类型的类型描述TypeDescriptor
对象。
ArrayToCollectionConverter
是一个很好的例子,用于把数组转换成集合:package org.springframework.core.convert.support; final class ArrayToCollectionConverter implements ConditionalGenericConverter { // ... @Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object[].class, Collection.class)); } @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return ConversionUtils.canConvertElements( sourceType.getElementTypeDescriptor(), targetType.getElementTypeDescriptor(), this.conversionService); } @Override @Nullable public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } int length = Array.getLength(source); TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), (elementDesc != null ? elementDesc.getType() : null), length); if (elementDesc == null) { for (int i = 0; i < length; i++) { Object sourceElement = Array.get(source, i); target.add(sourceElement); } } else { for (int i = 0; i < length; i++) { Object sourceElement = Array.get(source, i); Object targetElement = this.conversionService.convert(sourceElement, sourceType.elementTypeDescriptor(sourceElement), elementDesc); target.add(targetElement); } } return target; } }
-
带条件的通用类型转换器
ConditionalGenericConverter
有时候,你需要一个
Converter
在某个条件为真时去执行。比如,你可能只有在目标字段有某个特殊的注释时,才会去执行Converter
。或者你可能在目标类型中定义了某个特殊的方法,比如static valueOf
方法时才执行。
ConditionalGenericConverter
联合了GenericConverter
和ConditionalConverter
接口:public interface ConditionalConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { }
-
类型转换服务
ConversionService
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
接口,用于注册转换器:package org.springframework.core.convert.converter; public interface ConverterRegistry { void addConverter(Converter<?, ?> converter); <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter); void addConverter(GenericConverter converter); void addConverterFactory(ConverterFactory<?, ?> factory); void removeConvertible(Class<?> sourceType, Class<?> targetType); }
ConversionService
的实现类将类型转换的逻辑妥托给其注册的转换器。core.convert.support
包提供了一个健壮的ConversionService
实现类GenericConversionService
,是适用于大多数环境的通用实现。 -
配置和使用
ConversionService
ConversionSerive
是一个无状态对象,被设计在应用程序启动时初始化,然后在多个线程间共享。
在一个 Spring 应用中,你可以为每个 Spring 容器(或是一个应用程序上下文)配置一个ConversionService
实例。
ConversionService
将会被 Spring 检索然后在任何需要类型转化的时候被框架执行。你也可以直接将ConversionService
注入到你的 Bean 中然后调用。@Bean public FactoryBean<ConversionService> conversionServiceFactoryBean() { return new ConversionServiceFactoryBean(); } @Service public class MyService { @Autowired private ConversionService conversionService; public void doIt() { conversionService.convert(...); } }
大多数情况,可以使用特定
targetType
的convert
方法,但是不适合像元素集合这种更复杂的类型。比如,如果你想要将一个Integer
的List
转换成String
的List
,那需要你提供一个更正式的关于源和目标类型的定义。
幸运的是,TypeDescriptor
提供了几个选项让这变得直接:DefaultConversionService cs = new DefaultConversionService(); List<Integer> input = .... cs.convert(input, TypeDescriptor.forObject(input), // List<Integer> type descriptor TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
字段格式化
一般来说,当你需要实现通用的类型转换逻辑时请使用 Converter SPI,例如,在 java.util.Date
和 java.lang.Long
之间进行转换。当你在一个客户端环境(比如 web 应用程序)工作并且需要解析和打印本地化的字段值时,请使用 Formatter SPI。
-
格式化器
Formatter
package org.springframework.format; public interface Formatter<T> extends Printer<T>, Parser<T> { } public interface Printer<T> { String print(T fieldValue, Locale locale); } public interface Parser<T> { T parse(String clientValue, Locale locale) throws ParseException; }
要创建你自己的格式化器,只需要实现上面的
Formatter
接口。泛型参数T
代表你想要格式化的对象的类型,例如,java.util.Date
。实现print()
操作可以将类型T
的实例按客户端区域设置的显示方式打印出来。实现parse()
操作可以从依据客户端区域设置返回的格式化表示中解析出类型T
的实例。如果解析尝试失败,你的格式化器应该抛出一个ParseException
或者IllegalArgumentException
异常。请注意确保你的格式化器实现是线程安全的。如下自定义了
LocalDate
类的格式化器:import org.springframework.format.Formatter; public final class LocalDateFormatter implements Formatter<LocalDate> { private DateTimeFormatter formatter; public LocalDateFormatter(String pattern) { formatter = DateTimeFormatter.ofPattern(pattern); } @Override public LocalDate parse(String text, Locale locale) throws ParseException { try { return LocalDate.parse(text, formatter); } catch (DateTimeParseException e) { ParseException parseException = new ParseException(e.getMessage(), e.getErrorIndex()); parseException.initCause(e); throw parseException; } } @Override public String print(LocalDate localDate, Locale locale) { return localDate.format(formatter); } }
-
注解驱动的格式化
字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现
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
代表你想要关联格式化逻辑的字段注解类型,getFieldTypes()
方法返回支持的字段类型,getPrinter()
方法返回可以打印被注解字段的值的打印机,getParser()
方法返回可以解析被注解字段的客户端值的解析器。如下所示,把注解
@LocalDateFormat
绑定到对应的格式化器LocalDateFormatter
上:@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface LocalDateFormat { String value() default ""; } public final class LocalDateFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<LocalDateFormat> { @Override public Set<Class<?>> getFieldTypes() { return new HashSet<>(Collections.singletonList(LocalDate.class)); } @Override public Printer<LocalDate> getPrinter(LocalDateFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } @Override public Parser<LocalDate> getParser(LocalDateFormat annotation, Class<?> fieldType) { return configureFormatterFrom(annotation, fieldType); } private Formatter<LocalDate> configureFormatterFrom(LocalDateFormat annotation, Class<?> fieldType) { if (!annotation.value().isEmpty()) { return new LocalDateFormatter(annotation.value()); } else { return new LocalDateFormatter(); } } }
-
格式化注解 API
org.springframework.format.annotation
包中存在一套可移植(portable)的格式化注解 API。请使用@NumberFormat
格式化java.lang.Number
字段,使用@DateTimeFormat
格式化java.util.Date
、java.util.Calendar
、java.lang.Long
或者 Joda Time 字段。下面这个例子使用
@DateTimeFormat
将java.util.Date
格式化为 ISO 时间(yyyy-MM-dd
)public class MyModel { @DateTimeFormat(iso=ISO.DATE) private Date date; }
-
配置一个全局的日期、时间格式
默认情况下,未被
@DateTimeFormat
注解的日期和时间字段会使用DateFormat.SHORT
风格从字符串转换。如果你愿意,你可以定义你自己的全局格式来改变这种默认行为。你将需要确保 Spring 不会注册默认的格式化器,取而代之的是你应该手动注册所有的格式化器。请根据你是否依赖 Joda Time 库来确定是使用
org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar
类还是org.springframework.format.datetime.DateFormatterRegistrar
类。例如,下面的 Java 配置会注册一个全局的
yyyyMMdd
格式,这个例子不依赖于 Joda Time 库:@Configuration public class AppConfig { @Bean public FormattingConversionService conversionService() { // Use the DefaultFormattingConversionService but do not register defaults DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); // Ensure @NumberFormat is still supported conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // Register date conversion with a specific global format DateFormatterRegistrar registrar = new DateFormatterRegistrar(); registrar.setFormatter(new DateFormatter("yyyyMMdd")); registrar.registerFormatters(conversionService); return conversionService; } }
Spring 字段验证
-
JSR-303 Bean Validation API
JSR-303 对 Java 平台的验证约束声明和元数据进行了标准化定义。使用此 API,你可以用声明性的验证约束对领域模型的属性进行注解,并在运行时强制执行它们。现在已经有一些内置的约束供你使用,当然你也可以定义你自己的自定义约束。
为了说明这一点,考虑一个拥有两个属性的简单的
PersonForm
模型:public class PersonForm { private String name; private int age; }
JSR-303 允许你针对这些属性定义声明性的验证约束:
public class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; }
当此类的一个实例被实现 JSR-303 规范的验证器进行校验的时候,这些约束就会被强制执行。
-
配置 Bean 验证器提供程序
Spring 提供了对 Bean Validation API 的全面支持,这包括将实现 JSR-303/JSR-349 规范的 Bean 验证提供程序引导为 Spring Bean 的方便支持。这样就允许在应用程序任何需要验证的地方注入
javax.validation.ValidatorFactory
或者javax.validation.Validator
。把
LocalValidatorFactoryBean
当作 Spring Bean 来配置成默认的验证器:@Bean public LocalValidatorFactoryBean localValidatorFactoryBean() { return new LocalValidatorFactoryBean(); }
以上的基本配置会触发 Bean Validation 使用它默认的引导机制来进行初始化。作为实现 JSR-303/JSR-349 规范的提供程序,如 Hibernate Validator,可以存在于类路径以使它能被自动检测到。
LocalValidatorFactoryBean
实现了javax.validation.ValidatorFactory
和javax.validation.Validator
这两个接口,以及 Spring 的org.springframework.validation.Validator
接口,你可以将这些接口当中的任意一个注入到需要调用验证逻辑的 Bean 里。如果你喜欢直接使用 Bean Validtion API,那么就注入
javax.validation.Validator
的引用:import javax.validation.Validator; @Service public class MyService { @Autowired private Validator validator; }
如果你的 Bean 需要 Spring Validation API,那么就注入
org.springframework.validation.Validator
的引用:import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; }
-
自定义约束
每一个 Bean 验证约束由两部分组成,第一部分是声明了约束和其可配置属性的
@Constraint
注解,第二部分是实现约束行为的javax.validation.ConstraintValidator
接口实现。为了将声明与实现关联起来,每个@Constraint
注解会引用一个相应的验证约束的实现类。在运行期间,ConstraintValidatorFactory
会在你的领域模型遇到约束注解的情况下实例化被引用到的实现。默认情况下,
LocalValidatorFactoryBean
会配置一个SpringConstraintValidatorFactory
,其使用 Spring 来创建约束验证器实例。这允许你的自定义约束验证器可以像其他 Spring Bean 一样从依赖注入中受益。下面显示了一个自定义的
@Constraint
声明的例子,紧跟着是一个关联的ConstraintValidator
实现,其使用 Spring 进行依赖注入:@Target({ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy=MyConstraintValidator.class) public @interface MyConstraint { } import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> { @Autowired; private Foo aDependency; @Override public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) { // ... } }
如你所见,一个约束验证器实现可以像其他 Spring Bean 一样使用
@Autowired
注解来自动装配它的依赖。被 Bean Validation 1.1 以及作为 Hibernate Validator 4.3 中的自定义扩展所支持的方法验证功能可以通过配置
MethodValidationPostProcessor
的 Bean 定义集成到 Spring 的上下文中:@Bean public static MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }
为了符合 Spring 驱动的方法验证,需要对所有目标类用 Spring 的
@Validated
注解进行注解,且有选择地对其声明验证组,这样才可以使用。 -
绑定 DataBinder
从 Spring 3 开始,
DataBinder
的实例可以配置一个验证器。一旦配置完成,那么可以通过调用binder.validate()
来调用验证器,任何的验证错误都会自动添加到DataBinder
的绑定结果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
,一个DataBinder
也可以配置多个Validator
实例。当需要将全局配置的 Bean 验证与一个DataBinder
实例上局部配置的 Spring Validator 结合时,这一点是非常有用的。
Spring 表达式语言 SpEL
-
语法
// 字符串 'Hello World' // 数字 6.0221415E+23 0x7FFFFFFF // 布尔值 true // null null // 方法调用 'Hello World'.concat('!') // 属性调用(实际调用 getBytes() 方法) 'Hello World'.bytes // 级联调用 'Hello World'.bytes.length // 数组、列表元素 inventions[3] // Map 元素 Officers['president'] // 内联列表 {1,2,3,4} {{'a','b'},{'x','y'}} // 内联Map {name:'Nikola',dob:'10-July-1856'} {name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}} // 关系运算符,包括 ==,!=,<,<=,>,>=,简写形式:lt (<), gt (>), le (?), ge (>=), eq (==), ne (!=), div (/), mod (%), not (!) age == 18 // instanceof 关键字 'xyz' instanceof T(Integer) // 正则表达式 '5.00' matches '\^-?\\d+(\\.\\d{2})?$' // 逻辑运算符,包括 and,or,not isMember('Nikola Tesla') and isMember('Mihajlo Pupin') // 算术运算符,包括 +,-,*,/,%,^ 1 + 1 'hello' + ' ' + 'world' 1000.00 - 1e4 // 赋值 Name = 'Alexandar Seovic' // 类型,默认对 java.lang 包可见,其它包的类都要使用全类名 T(java.util.Date) T(String) T(java.math.RoundingMode).FLOOR // 构造器,除了基本类型的包装类和 String,都要使用全类名 new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German') // 变量,使用 # + 变量名引用 Name = #newName // #this 变量永远指向当前表达式正在求值的对象,#root 总是指向根上下文对象 primes = {2,3,5,7,11,13,17} #primes.?[#this>10] // 结果为 [11, 13, 17] // Bean 使用 @ 引用,工厂 Bean 使用 & 引用 @foo &foo // 三元运算符 false ? 'trueExp' : 'falseExp' // Elvis 运算符 name ?: 'Unknown' // 等价于 name ? name : 'Unknown' systemProperties['pop3.port'] ?: 25 // 安全引用运算符 PlaceOfBirth?.City // 当 PlaceOfBirth 为 null 时,不会抛出空指针异常而是返回 null // 集合筛选,?[...] 返回满足条件的元素构成的子集,^[...] 返回满足条件的第一个元素,$[...] 返回满足条件的最后一个元素 Members.?[Nationality == 'Serbian'] // 国籍为塞尔维亚的成员集合 map.?[value<27] // 值小于 27 元素组成的子集(key 表示键,value 表示值) // 集合投影 Members.![placeOfBirth.city] // 成员的出生城市集合
-
使用
无论 XML 还是注解类型的 Bean 定义都可以使用 SpEL 表达式。在两种方式下定义的表达式语法都是一样的,即:
#{ <expression string> }
XML 配置文件中使用:
<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>
@Value
注解中使用:@Value("#{ systemProperties['user.region'] }") private String defaultLocale; @Value("#{ systemProperties['user.region'] }") public void setDefaultLocale(String defaultLocale) { this.defaultLocale = defaultLocale; } @Autowired public void configure(MovieFinder movieFinder, @Value("#{ systemProperties['user.region'] }") String defaultLocale) { this.movieFinder = movieFinder; this.defaultLocale = defaultLocale; } @Autowired public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, @Value("#{systemProperties['user.country']}") String defaultLocale) { this.customerPreferenceDao = customerPreferenceDao; this.defaultLocale = defaultLocale; }
AOP 面向切面编程
AOP(Aspect Oriented Programming),即面向切面编程,可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP 引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过 OOP 允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系。对于其他类型的代码,如权限校验、异常处理和事务也都是如此,这种散布在各处的无关的代码被称为横切(crosscutting),在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP 技术恰恰相反,它剖解开封装的对象内部,将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为切面(Aspect)。所谓“切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用"切面"技术,AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似。AOP 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
概念和术语
-
切面(Aspect):横切关注点,可重用模块。事务管理就是一个典型的例子。可以使用
@Aspect
注解来定义。 - 连接点(Join point):程序执行过程中的一个点,比如一个方法的执行或一个异常的处理。在 Spring AOP 中,连接点总是一个方法的执行。
- 通知、加强(Advice):切面在特定连接点发生的动作。通知类型包括环绕(around),执行前(before),执行后(after)。很多 AOP 框架,包括 Spring,将通知定义为拦截器(interceptor),环绕连接点维护一个拦截器链。
- 切入点(Pointcut):定义匹配连接点的规则。通知会关联切入点表达式匹配的连接点,并在连接点前后执行。
-
引入(Introduction):向现有的类添加新方法或属性。Spring AOP 允许你引入新的接口(和相应的实现)给任何被加强的对象。比如,你可以使用引入让一个 Bean 实现
IsModified
接口,简化缓存实现。 - 目标对象(Target object):被一个或多个切面加强的对象,也称为加强对象(Advised object)。自从 Spring AOP 使用动态代理技术,该对象总是一个被代理的对象。
- AOP 代理(AOP proxy):AOP 框架创建的对象用来实现切面的逻辑。在 Spring 框架中,AOP 代理可以是 JDK 动态代理或者 CGLIB 代理。
- 织入(Weaving):将切面应用到目标对象并导致代理对象创建的过程,可以在编译时期(比如使用 AspectJ 编译器)、装载时期或运行时期完成。Spring AOP 在运行时间完成织入。
通知类型:
- 执行前通知(Before advice):在连接点执行之前执行,但是不能阻止连接点执行(除非它抛出异常)。
- 返回后通知(After returning advice):在连接点正常执行完成之后执行。
- 抛出异常后通知(After throwing advice):在连接点因异常中断后执行。
- 执行后通知(After (finally) advice):在连接点执行后执行,无论连接点是否正常执行完成。
- 环绕通知(Around advice):最强大的通知类型,可以在连接点之前或者之后执行,可以选择是否执行连接点,也可以代替连接点直接返回或抛出异常。
AOP 代理类型
Spring AOP 默认使用标准的 JDK 动态代理技术实现 AOP 代理。所有的接口(或者接口集)都可以被代理。
Spring AOP 也可以使用 CGLIB 代理。代理类而不是接口也是有必要的。当一个类没有实现任何接口时会使用 CGLIB 代理。当你需要使用未在接口定义的方法时,或者当你需要把代理对象作为一个具体的类型传递到方法的情况下,你可以强制使用 CGLIB 代理。
@AspectJ
注解支持
使用 @AspectJ
注解可以在普通 Java 类上声明切面,这种风格在 AspectJ 5 中被引入。Spring 复用了 AspectJ 5 的注解,使用 AspectJ 提供的库解析和匹配切入点。但是 AOP 在运行时仍是纯粹的 Spring AOP,并不依赖 AspectJ 的编译器和织入器。
-
启用
@AspectJ
注解支持:@Configuration @EnableAspectJAutoProxy public class AppConfig { }
-
声明一个切面:
import org.aspectj.lang.annotation.Aspect; @Compenent @Aspect public class NotVeryUsefulAspect { }
-
声明一个切入点:
// 切入点 anyOldTransfer 匹配任意名为 transfer 的方法执行 @Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
注意:这个方法的返回类型必须为 void
-
支持的切入点指示器:
- execution:匹配方法执行连接点
- within:匹配指定包(或类)内所有方法执行连接点
- this:匹配指定类的代理对象(instanceof)内所有方法执行连接点
- target:匹配指定类的目标对象(instanceof)内所有方法执行连接点
- args:匹配入参为指定类型的方法执行连接点
- @target:匹配标注有指定注解的类的目标对象内所有方法执行连接点
- @args:匹配入参标注有指定注解的方法执行连接点
- @within:匹配标注有指定注解的类内所有方法执行连接点
- @annotation:匹配标注有指定注解的方法执行连接点
- bean:匹配指定名称的 Bean 的所有方法执行连接点
AspectJ 还有很多切入点指示器是 Spring 不支持的:
call, get, set, preinitialization, staticinitialization, initialization, handler, adviceexecution, withincode, cflow, cflowbelow, if, @this, and @withincode
。使用这些不支持的切入点指示器 Spring AOP 会抛出IllegalArgumentException
异常。
Spring AOP 会在未来的版本中进行的拓展,支持更多的 AspectJ 切入点指示器。 -
合并切入点表达式
切入点表达式可以使用
&&
、||
、!
来合并。还可以通过名字复用其它的切入点表达式。如:// 匹配所有 public 方法 @Pointcut("execution(public * *(..))") private void anyPublicOperation() {} // 匹配 com.xyz.someapp.trading 包内的所有类的所有方法 @Pointcut("within(com.xyz.someapp.trading..*)") private void inTrading() {} // 匹配 com.xyz.someapp.trading 包内的所有类的 public 方法 @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {}
-
共享通用的切入点定义
当开发企业级应用的时候,你通常会想要从几个切面来引用系统的模块和特定的操作集。 我们推荐定义一个
SystemArchitecture
切面来定义通用的切入点表达式。一个典型的切面可能看起来像下面这样:@Compenent @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() {} }
-
切入点表达式语法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
除了返回类型模式(ret-type-pattern),方法名模式(name-pattern)和方法参数模式(param-pattern),其它模式都是可选的。
- 修饰符模式(modifiers-pattern):可以省略。
- 返回类型模式(ret-type-pattern):可以使用
*
匹配所有返回类型。 - 包名模式(declaring-type-pattern):可以省略。
- 方法名模式(name-pattern):可以使用
*
匹配所有或部分方法名。 - 方法参数模式(param-pattern):
()
匹配无参方法;(..)
匹配任意数量参数方法(无参或多参);(*)
匹配只有一个任意类型参数的方法;(*,String)
匹配两个参数的方法,且第一个参数可以是任意类型,但第二个参数只能是String
类型。 - 抛出异常模式(throws-pattern):可以省略。
下面是一些常见切入点表达式的例子:
// 任意 public 方法 execution(public * *(..)) // 任意方法名为“set”开头的方法 execution(* set*(..)) // AccountService 接口定义的任意方法 execution(* com.xyz.service.AccountService.*(..)) // service 包内定义的任意方法 execution(* com.xyz.service.*.*(..)) // service 包和子包内定义的任意方法 execution(* com.xyz.service..*.*(..)) // service 包内定义的任意方法 within(com.xyz.service.*) // service 包和子包内定义的任意方法 within(com.xyz.service..*) // AccountService 接口代理对象内的任意方法 this(com.xyz.service.AccountService) // AccountService 接口目标对象内的任意方法 target(com.xyz.service.AccountService) // 只有一个参数且运行时入参对象为 Serializable 类型的任意方法 args(java.io.Serializable) // 只有一个参数且为 Serializable 类型的任意方法 execution(* *(java.io.Serializable)) // 标注 @Transactional 注解的目标对象的任意方法 @target(org.springframework.transaction.annotation.Transactional) // 标注 @Transactional 注解的目标对象的任意方法 @within(org.springframework.transaction.annotation.Transactional) // 标注 @Transactional 注解的任意方法 @annotation(org.springframework.transaction.annotation.Transactional) // 只有一个参数且标注 @Classified 注解的任意方法 @args(com.xyz.security.Classified) // 名为 tradeService 的 Bean 的任意方法 bean(tradeService) // 名字后缀为 Service 的 Bean 的任意方法 bean(*Service)
-
声明通知(加强)
执行前通知(Before advice)使用
@Before
注解声明:@Compenent @Aspect public class BeforeExample { @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") // @Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } }
返回后通知(After returning advice)使用
@AfterReturning
注解声明:@Compenent @Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doAccessCheck() { // ... } @AfterReturning( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } }
抛出异常后通知(After throwing advice)使用
@AfterThrowing
注解声明:@Compenent @Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doRecoveryActions() { // ... } @AfterThrowing( pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } }
执行后通知(After (finally) advice)使用
@After
注解声明:@Compenent @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // ... } }
环绕通知(Around advice)使用
@Around
注解声明,通知方法的第一个参数必须是ProceedingJoinPoint
类型。方法体内调用ProceedingJoinPoint
的proceed()
方法执行连接点方法。proceed
方法在调用可以传入一个Object[]
数组,数组中的值会在连接点方法执行时作为参数:@Compenent @Aspect public class AroundExample { @Around("com.xyz.myapp.SystemArchitecture.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { // start stopwatch Object retVal = pjp.proceed(); // stop stopwatch return retVal; } }
通知方法的返回值会被调用连接点方法的人接收。可以用环绕通知实现一个简单的缓存切面,如果有缓存结果直接返回;如果无缓存结果,就调用
proceed
方法。
注意:proceed
方法调用一次、多次或者不调用就是允许的。 -
通知参数:
任何通知方法可以在第一个参数位置声明一个
org.aspectj.lang.JointPoint
类型的参数。注意:环绕通知(Around advice)必须要求声明第一个参数为
ProceedingJointPoint
类型,它是JointPoint
的子类。JointPoint
接口提供许多的方法,比如getArgs()
返回方法的参数,getThis()
返回代理对象,getTarget()
返回目标对象,getSignature()
返回被通知的方法的描述,toString()
打印被通知的方法的有用的描述。 -
传递参数给通知:
如果在参数表达式中使用参数名字代替参数类型,那么当通知执行的时候,对应的参数值会被传入。假设你希望在一个第一个参数为
Account
对象的 DAO 方法添加通知,并需要在通知体内访问account
。你可以像下面这样写:@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) { // ... }
代理对象(
this
),目标对象(target
)和注解(@within
,@target
,@annotation
,@args
)可以用同样的方式绑定。下面的例子展示了你可以如何去匹配标注@Auditable
注解的方法,并且提取出审核的编码:@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Auditable { AuditCode value(); } @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") public void audit(Auditable auditable) { AuditCode code = auditable.value(); // ... }
支持范型参数:
public interface Sample<T> { void sampleGenericMethod(T param); void sampleGenericCollectionMethod(Collection<T> param); } @Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") public void beforeSampleMethod(MyType param) { // Advice implementation } @Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") public void beforeSampleMethod(Collection<?> param) { // Advice implementation }
注意:不支持
Collection<MyType> param
这种指定范型的容器使用
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 } @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 } @Before("com.xyz.lib.Pointcuts.anyPublicMethod()") public void audit(JoinPoint jp) { // ... use jp }
带参执行连接点方法:
@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 相同的优先规则来决定通知执行的顺序:
- “进入”时,优先级高的通知会先执行(因此如果有两个执行前通知,高优先级的通知先执行)。
- “退出”时,优先级高的通知会后执行(因此如果有两个执行后通知,高优先级的通知后执行)。
当定义在不同切面的两个通知要在相同的连接点运行时,除非你指定了,否则数序是未定义的。你可以通过指定优先级来控制执行顺序。这可以用常用的 Spring 做到,让切面类实现
org.springframework.core.Ordered
接口或是标注@Order
注解。对于给定的两个切面,Ordered.getValue()
返回值(或是注解的值)低的,优先级更高。当定义在一个切面中的两个通知要在相同的连接点上运行时,顺序是未定的(因为没有办法为 javac 编译过的类声明顺序)。可以考虑将这些通知方法合到一个通知里,或者是重构每个通知,放在不同的切面类里——然后以切面的层次进行排序。
引入
引入使得切面能够声明被通知的对象实现给定的接口及其实现。
引入可以用 @DeclareParents
注解声明,这个注解被用来声明匹配的类型拥有一个新的父级。比如,给定一个接口 UsageTracked
,和这个接口的实现 DefaultUsageTracked
,下面的切面声明了所有 service 接口的实现同样实现了 UsageTracked
接口(比如说为了通过 JMX 公开统计消息):
@Compenent
@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();
}
}
上述例子执行前通知中,service Bean 可以直接被作为 UsageTracked
接口的实现使用。当你用编程的方式访问一个 Bean,你可以像下面这么写:
UsageTracked usageTracked = (UsageTracked) context.getBean("myService");
-
一个实用的例子
业务服务有时候会因为并发的问题导致执行失败(比如,死锁)。如果一个操作再次尝试,那么很有可能在下一次成功。对于适合在这些情况下重试的业务服务(幂等操作,不需要用户来解决冲突),我们希望重试操作变透明避免客户端看到
PessimisticLockingFailureException
。这是一个清晰地跨越服务层中多个服务的需求,因此通过切面来实现是个好主意。因为我们希望重试操作,我们需要使用环绕通知来多次调用
procced
。一个基础的切面方案:@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; } }
注意切面实现了
Orderd
接口,因此我们可以设置切面的优先级高于事务通知(我们希望每次尝试时都是新的事务)。maxRetries
和order
属性都可以通过 Spring 配置。为了改进切面,只重试幂等操作,我们需要定义一个
@Idempotent
注解:@Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // marker annotation }
并且使用这个注解去标注业务服务的实现方法。让切面只重试幂等操作只需要简单改进切点表达式,让它只匹配
@Idempotent
注解:@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ... }
理解 AOP 代理
考虑对如下类进行代理:
public class SimplePojo implements Pojo {
public void foo() {
// this next method invocation is a direct call on the 'this' reference
this.bar();
}
public void bar() {
// some logic...
}
}
一个简单的实现:
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();
}
}
如上代码意味着对对象引用的方法调用将是代理上的调用,同样地代理将能够将所有的拦截器(通知)委托到相关的特定的方法调用上。
但是,一旦调用到达最终的目标对象,在本例中是 SimplePojo
引用时,任何方法调用都在最终目标对象上调用(也就是 SimplePojo
上) 。这意味着自调用不会通知相关的切面,切面上的业务方法也不会被执行。
所以,为了避免此类情况的发生,最好的方法是重构你的代码,确保被加强的方法不会出现自调用的情况。
Null 安全性
尽管 Java 没有在它的类型系统提供 Null 安全性的表示,Spring 框架在 org.springframework.lang
包中提供了以下注解声明 API 和字段的为空性:
-
@NonNull:注释特定的参数,返回值或者字段不能为
null
(当使用@NonNullApi
和@NonNullFields
时,不需要在参数和返回值上使用)。 -
@Nullable:注释特定的参数,返回值或者字段可以为
null
。 -
@NonNullApi:在包级别注释参数和返回值默认不能为
null
。 - @NonNullFields:在包级别注释字段默认不能为 null。
目前还不支持范型、可变参数和数组元素的为空性,但是会在未来的版本更新。
用例
这些注解可以被 IDE 使用,为 Java 开发者 Null 安全性警告,以避免在运行时抛出 NullPointerException
。