# 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; }
1
2
3
4
5
6当在 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 销毁之前执行
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 通过
@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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
> **注意:** 如果不指定 `@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)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# @Autowired
@Autowired
注解可用在 Bean 的有参构造函数上。如果只有一个有参构造函数,该注解可省略;如果有多个有参构造函数(且无无参构造函数),必须在其中一个构造函数上声明。@Autowired
注解也可用在数组、Set 等数据结构上。比如:
@Component
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
@Autowired
private Set<MovieCatalog> movieCatalogs;
}
2
3
4
5
6
7
8
9
目标 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 支持
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# @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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
@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");
}
}
2
3
4
5
6
7
8
9
10
@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");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过配置 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();
}
}
2
3
4
5
6
7
8
9
10
11
12
# @PropertySource
@PropertySource
注解提供了一种方便的机制来将 PropertySource 增加到 Spring 的 Environment 之中:
# /com/myco/app.properties
testbean.name=myTestBean
2
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# @Value
@Value
注解可以将 Environment 中的属性注入到对象属性中:
@Value("${testbean.name}")
private String testBeanName;
2
# @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
2
3
4
5
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 事件
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...
}
2
3
4
5
6
7
8
9
10
11
12
13
可通过调用 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...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
可通过 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...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
也可通过 @EventListener
注解标注的方法处理事件:
@EventListener
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
2
3
4
修改方法返回事件类型即可支持处理完当前事件后发布另一个事件:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
2
3
4
5
使用 @Async
注解异步处理事件:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
2
3
4
5
注意:
- 异步处理事件的方法抛出的异常不会被发布者接收到
- 异步处理事件的方法不能通过返回事件类型的对象发布新的事件
使用 @Order
注解指定事件处理方法调用的优先级:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
2
3
4
5
# 类型转换、字段格式化与验证
# 使用 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; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44下列代码展示了如何检索和操作
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");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 类型转换
- 类型转换器 Converter
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
2
3
4
5
通过实现 Converter
接口可以定义自己的类型转换器。范型 S
表示需要转换的类型,范型 T
表示转换成的类型。
下面是一个简单的例子,定义了一个可以将 String 对象转成 Integer 对象的转换器:
final class StringToInteger implements Converter<String, Integer> {
public Integer convert(String source) {
return Integer.valueOf(source);
}
}
2
3
4
5
> **注意:**`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);
}
2
3
4
5
当开发者需要集中整个类层次结构的转换逻辑时,可以实现 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());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 通用类型转换器
GenericConverter
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
2
3
4
5
6
当开发者需要设计一个复杂的通用转换器,可以转换多种类型到多种类型时,可以实现 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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
带条件的通用类型转换器
ConditionalGenericConverter
有时候,你需要一个
Converter
在某个条件为真时去执行。比如,你可能只有在目标字段有某个特殊的注释时,才会去执行Converter
。或者你可能在目标类型中定义了某个特殊的方法,比如static valueOf
方法时才执行。
ConditionalGenericConverter
联合了GenericConverter
和ConditionalConverter
接口:public interface ConditionalConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); } public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { }
1
2
3
4
5
6
7类型转换服务
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); }
1
2
3
4
5
6
7
8
9
10
11大部分
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); }
1
2
3
4
5
6
7
8
9
10
11
12
13ConversionService
的实现类将类型转换的逻辑妥托给其注册的转换器。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(...); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14大多数情况,可以使用特定
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)));
1
2
3
4
5
6
# 字段格式化
一般来说,当你需要实现通用的类型转换逻辑时请使用 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;
}
2
3
4
5
6
7
8
9
10
11
12
要创建你自己的格式化器,只需要实现上面的 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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
注解驱动的格式化
字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现
AnnotationFormatterFactory
接口:package org.springframework.format; public interface AnnotationFormatterFactory<A extends Annotation> { Set<Class<?>> getFieldTypes(); Printer<?> getPrinter(A annotation, Class<?> fieldType); Parser<?> getParser(A annotation, Class<?> fieldType); }
1
2
3
4
5
6
7
8
9
10
11泛型参数
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(); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32格式化注解 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; }
1
2
3
4配置一个全局的日期、时间格式
默认情况下,未被
@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; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Spring 字段验证
JSR-303 Bean Validation API
JSR-303 对 Java 平台的验证约束声明和元数据进行了标准化定义。使用此 API,你可以用声明性的验证约束对领域模型的属性进行注解,并在运行时强制执行它们。现在已经有一些内置的约束供你使用,当然你也可以定义你自己的自定义约束。
为了说明这一点,考虑一个拥有两个属性的简单的
PersonForm
模型:public class PersonForm { private String name; private int age; }
1
2
3
4JSR-303 允许你针对这些属性定义声明性的验证约束:
public class PersonForm { @NotNull @Size(max=64) private String name; @Min(0) private int age; }
1
2
3
4
5
6
7
8当此类的一个实例被实现 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(); }
1
2
3
4以上的基本配置会触发 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; }
1
2
3
4
5
6
7如果你的 Bean 需要 Spring Validation API,那么就注入
org.springframework.validation.Validator
的引用:import org.springframework.validation.Validator; @Service public class MyService { @Autowired private Validator validator; }
1
2
3
4
5
6
7自定义约束
每一个 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 { }
1
2
3
4
5import javax.validation.ConstraintValidator; public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> { @Autowired; private Foo aDependency; @Override public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) { // ... } }
1
2
3
4
5
6
7
8
9
10如你所见,一个约束验证器实现可以像其他 Spring Bean 一样使用
@Autowired
注解来自动装配它的依赖。被 Bean Validation 1.1 以及作为 Hibernate Validator 4.3 中的自定义扩展所支持的方法验证功能可以通过配置
MethodValidationPostProcessor
的 Bean 定义集成到 Spring 的上下文中:@Bean public static MethodValidationPostProcessor methodValidationPostProcessor() { return new MethodValidationPostProcessor(); }
1
2
3
4为了符合 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();
1
2
3
4
5
6
7
8
9
10
11
12通过
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] // 成员的出生城市集合
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
使用
无论 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>
1
2
3
4
5
6
7
8
9
10
11@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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 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 {
}
2
3
4
- 声明一个切面:
import org.aspectj.lang.annotation.Aspect;
@Compenent
@Aspect
public class NotVeryUsefulAspect {
}
2
3
4
5
6
- 声明一个切入点:
// 切入点 anyOldTransfer 匹配任意名为 transfer 的方法执行
@Pointcut("execution(* transfer(..))") // the pointcut expression
private void anyOldTransfer() {} // the pointcut signature
2
3
> **注意:**这个方法的返回类型必须为 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() {}
1
2
3
4
5
6
7
8
9
10
11共享通用的切入点定义
当开发企业级应用的时候,你通常会想要从几个切面来引用系统的模块和特定的操作集。 我们推荐定义一个
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() {} }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54切入点表达式语法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)
throws-pattern?)
2
除了返回类型模式(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)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
声明通知(加强)
执行前通知(Before advice)使用
@Before
注解声明:@Compenent @Aspect public class BeforeExample { @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") // @Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } }
1
2
3
4
5
6
7
8
9返回后通知(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) { // ... } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15抛出异常后通知(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) { // ... } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15执行后通知(After (finally) advice)使用
@After
注解声明:@Compenent @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()") public void doReleaseLock() { // ... } }
1
2
3
4
5
6
7
8环绕通知(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; } }
1
2
3
4
5
6
7
8
9
10
11通知方法的返回值会被调用连接点方法的人接收。可以用环绕通知实现一个简单的缓存切面,如果有缓存结果直接返回;如果无缓存结果,就调用
proceed
方法。
注意:proceed
方法调用一次、多次或者不调用就是允许的。通知参数:
任何通知方法可以在第一个参数位置声明一个
org.aspectj.lang.JointPoint
类型的参数。注意:环绕通知(Around advice)必须要求声明第一个参数为
ProceedingJointPoint
类型,它是JointPoint
的子类。JointPoint
接口提供许多的方法,比如getArgs()
返回方法的参数,getThis()
返回代理对象,getTarget()
返回目标对象,getSignature()
返回被通知的方法的描述,toString()
打印被通知的方法的有用的描述。传递参数给通知:
如果在参数表达式中使用参数名字代替参数类型,那么当通知执行的时候,对应的参数值会被传入。假设你希望在一个第一个参数为 `Account` 对象的 DAO 方法添加通知,并需要在通知体内访问 `account`。你可以像下面这样写:
```java
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
```
切入点表达式中 `args(account,..)` 有两个作用:第一,它限制了连接点至少有一个 `Account` 类型的参数;第二,它让通知方法可以通过 `account` 参数访问连接点的 `Account` 对象。
另一种写法是声明一个可以“提供” `Account` 实例的切点,然后在其他通知上通过名字直接引用。像下面这样:
```java
@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` 注解的方法,并且提取出审核的编码:
```java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
```
```java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
```
支持范型参数:
```java
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
```
```java
@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` 属性确定参数名称:
```java
@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
}
```
```java
@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
}
```
```java
@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
// ... use jp
}
```
带参执行连接点方法:
```java
@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();
}
}
2
3
4
5
6
7
8
9
10
11
上述例子执行前通知中,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; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37注意切面实现了
Orderd
接口,因此我们可以设置切面的优先级高于事务通知(我们希望每次尝试时都是新的事务)。maxRetries
和order
属性都可以通过 Spring 配置。为了改进切面,只重试幂等操作,我们需要定义一个
@Idempotent
注解:@Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { // marker annotation }
1
2
3
4并且使用这个注解去标注业务服务的实现方法。让切面只重试幂等操作只需要简单改进切点表达式,让它只匹配
@Idempotent
注解:@Around("com.xyz.myapp.SystemArchitecture.businessService() && " + "@annotation(com.xyz.myapp.service.Idempotent)") public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { ... }
1
2
3
4
5
# 理解 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...
}
}
2
3
4
5
6
7
8
9
10
11
一个简单的实现:
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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如上代码意味着对对象引用的方法调用将是代理上的调用,同样地代理将能够将所有的拦截器(通知)委托到相关的特定的方法调用上。
但是,一旦调用到达最终的目标对象,在本例中是 SimplePojo
引用时,任何方法调用都在最终目标对象上调用(也就是 SimplePojo
上) 。这意味着自调用不会通知相关的切面,切面上的业务方法也不会被执行。
所以,为了避免此类情况的发生,最好的方法是重构你的代码,确保被加强的方法不会出现自调用的情况。
# Null 安全性
尽管 Java 没有在它的类型系统提供 Null 安全性的表示,Spring 框架在 org.springframework.lang
包中提供了以下注解声明 API 和字段的为空性:
- **@NonNull:**注释特定的参数,返回值或者字段不能为
null
(当使用@NonNullApi
和@NonNullFields
时,不需要在参数和返回值上使用)。 - **@Nullable:**注释特定的参数,返回值或者字段可以为
null
。 - **@NonNullApi:**在包级别注释参数和返回值默认不能为
null
。 - **@NonNullFields:**在包级别注释字段默认不能为 null。
目前还不支持范型、可变参数和数组元素的为空性,但是会在未来的版本更新。
# 用例
这些注解可以被 IDE 使用,为 Java 开发者 Null 安全性警告,以避免在运行时抛出 NullPointerException
。
作者:Hsinwong
链接:https://www.jianshu.com/p/f2a8c63a4927
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。