Spring Framework 5 Core 学习笔记

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 会话范围单例
  1. 自定义作用域

    实现 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;
     }
    
  2. 当在 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 的生命周期

  1. 在 Spring 管理的 Bean 中,可以使用 @PostConstruct@PreDestroy 注解标注需要在 Bean 初始化之后和销毁之前需要执行的方法。比如:

     @Service
     public class Foo {
     
         @PostConstruct
         public void init() {
             // 该方法会在 Bean 初始化完成、装载所有依赖之后、AOP 拦截器应用到该 Bean 之前执行
         }
         
         @PreDestroy
         public void destroy() {
             // 该方法会在 Bean 销毁之前执行
         }
     }
    
  2. 通过 @Bean 注解的 initMethoddestroyMethod 属性指定初始化之后和销毁之前需要执行的方法。比如:

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

  3. 让 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

  1. @Autowired 注解可用在 Bean 的有参构造函数上。如果只有一个有参构造函数,该注解可省略;如果有多个有参构造函数(且无无参构造函数),必须在其中一个构造函数上声明。

  2. @Autowired 注解也可用在数组、Set 等数据结构上。比如:

     @Component
     public class MovieRecommender {
     
         @Autowired
         private MovieCatalog[] movieCatalogs;
     
         @Autowired
         private Set<MovieCatalog> movieCatalogs;
     }
    

    目标 Bean 即 MovieCatalog 可通过实现 org.springframework.core.Ordered 接口、使用 @Order 注解指定其在数组、List 中的顺序,默认为注册顺序。

  3. @Autowired 注解也可用在 Map 上(Map 的 Key 必须为 String 类型)。

  4. @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

  1. @Autowired 注解基于类型自动装载,当有多个类型匹配的 Bean 可供装载但只需要装载一个时,可配合使用 @Qualifier 注解(的 value 属性)指定目标 Bean 的名称。

  2. 当需要装载到数组、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...
}

可通过调用 ApplicationEventPublisherpublishEvent() 方法发布事件:

@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...
    }
}

可通过 ApplicationListeneronApplicationEvent(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
}

注意:

  1. 异步处理事件的方法抛出的异常不会被发布者接收到
  2. 异步处理事件的方法不能通过返回事件类型的对象发布新的事件

使用 @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。

  1. 使用 getPropertyValuesetPropertyValues 方法对 JavaBean 的属性进行读写操作。

    支持如下表达式:

    表达式 描述
    name 标示名为 name 的属性,对应的方法 getName()/isName()setName(...)
    account.name 标示名为 account 的属性的嵌套属性 name,对应的方法如 getAccount().getName()getAccount().setName()
    account[2] 标示名为 account 的属性的第 3 个元素,该属性可以是数组、列表或者其它有自然顺序的集合
    account[COMPANYNAME] 标示名为 account 的属性的 keyCOMPANYNAME 键值对对应的 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;
         }
     }
    

    下列代码展示了如何检索和操作 CompaniesEmployees 属性:

     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. 类型转换器 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 异常。如果转换失败,可以抛出非检查型异常。

  2. 类型转换器工厂 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 表示转换成的具体类型,TR 的子类。
    比如,将 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());
             }
         }
     }
    
  3. 通用类型转换器 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;
         }
    
     }
    
  4. 带条件的通用类型转换器 ConditionalGenericConverter

    有时候,你需要一个 Converter 在某个条件为真时去执行。比如,你可能只有在目标字段有某个特殊的注释时,才会去执行 Converter。或者你可能在目标类型中定义了某个特殊的方法,比如 static valueOf 方法时才执行。
    ConditionalGenericConverter 联合了 GenericConverterConditionalConverter 接口:

     public interface ConditionalConverter {
         boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
     }
    
     public interface ConditionalGenericConverter
         extends GenericConverter, ConditionalConverter {
     }
    
  5. 类型转换服务 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,是适用于大多数环境的通用实现。

  6. 配置和使用 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(...);
         }
     }
    

    大多数情况,可以使用特定 targetTypeconvert 方法,但是不适合像元素集合这种更复杂的类型。比如,如果你想要将一个 IntegerList 转换成 StringList,那需要你提供一个更正式的关于源和目标类型的定义。
    幸运的是,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.Datejava.lang.Long 之间进行转换。当你在一个客户端环境(比如 web 应用程序)工作并且需要解析和打印本地化的字段值时,请使用 Formatter SPI。

  1. 格式化器 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);
         }
     }
    
  2. 注解驱动的格式化

    字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现 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();
             }
         }
     }
    
  3. 格式化注解 API

    org.springframework.format.annotation 包中存在一套可移植(portable)的格式化注解 API。请使用 @NumberFormat 格式化 java.lang.Number 字段,使用 @DateTimeFormat 格式化 java.util.Datejava.util.Calendarjava.lang.Long 或者 Joda Time 字段。

    下面这个例子使用 @DateTimeFormatjava.util.Date 格式化为 ISO 时间(yyyy-MM-dd

     public class MyModel {
         @DateTimeFormat(iso=ISO.DATE)
         private Date date;
     }
    
  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;
         }
     }
    

Spring 字段验证

  1. 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 规范的验证器进行校验的时候,这些约束就会被强制执行。

  2. 配置 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.ValidatorFactoryjavax.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;
     }
    
  3. 自定义约束

    每一个 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 注解进行注解,且有选择地对其声明验证组,这样才可以使用。

  4. 绑定 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.addValidatorsdataBinder.replaceValidators,一个 DataBinder 也可以配置多个 Validator 实例。当需要将全局配置的 Bean 验证与一个 DataBinder 实例上局部配置的 Spring Validator 结合时,这一点是非常有用的。

Spring 表达式语言 SpEL

  1. 语法

     // 字符串
     '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. 使用

    无论 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 的编译器和织入器。

  1. 启用 @AspectJ 注解支持:

     @Configuration
     @EnableAspectJAutoProxy
     public class AppConfig {
     }
    
  2. 声明一个切面:

     import org.aspectj.lang.annotation.Aspect;
    
     @Compenent
     @Aspect
     public class NotVeryUsefulAspect {
     }
    
  3. 声明一个切入点:

     // 切入点 anyOldTransfer 匹配任意名为 transfer 的方法执行
     @Pointcut("execution(* transfer(..))")// the pointcut expression
     private void anyOldTransfer() {}// the pointcut signature
    

    注意:这个方法的返回类型必须为 void

  4. 支持的切入点指示器:

    • 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 切入点指示器。

  5. 合并切入点表达式

    切入点表达式可以使用 &&||! 来合并。还可以通过名字复用其它的切入点表达式。如:

     // 匹配所有 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() {}
    
  6. 共享通用的切入点定义

    当开发企业级应用的时候,你通常会想要从几个切面来引用系统的模块和特定的操作集。 我们推荐定义一个 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() {}
    
     }
    
  7. 切入点表达式语法

     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)
    
  8. 声明通知(加强)

    执行前通知(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 类型。方法体内调用 ProceedingJoinPointproceed() 方法执行连接点方法。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 方法调用一次、多次或者不调用就是允许的。

  9. 通知参数:

    任何通知方法可以在第一个参数位置声明一个 org.aspectj.lang.JointPoint 类型的参数。

    注意:环绕通知(Around advice)必须要求声明第一个参数为 ProceedingJointPoint 类型,它是 JointPoint 的子类。

    JointPoint 接口提供许多的方法,比如 getArgs() 返回方法的参数,getThis() 返回代理对象,getTarget() 返回目标对象,getSignature() 返回被通知的方法的描述,toString() 打印被通知的方法的有用的描述。

  10. 传递参数给通知:

    如果在参数表达式中使用参数名字代替参数类型,那么当通知执行的时候,对应的参数值会被传入。假设你希望在一个第一个参数为 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});
    }
    
  11. 通知的执行顺序

    当许多通知想要运行在一个相同的连接点上时会发生什么?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");
  1. 一个实用的例子

    业务服务有时候会因为并发的问题导致执行失败(比如,死锁)。如果一个操作再次尝试,那么很有可能在下一次成功。对于适合在这些情况下重试的业务服务(幂等操作,不需要用户来解决冲突),我们希望重试操作变透明避免客户端看到 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 接口,因此我们可以设置切面的优先级高于事务通知(我们希望每次尝试时都是新的事务)。maxRetriesorder 属性都可以通过 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,087评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,290评论 6 344
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,292评论 18 399
  • 刚刚过去的这个周末是一个难忘的周末,因为我来到了昆明参加的国际演讲会(头马)Toastmasters 89大区最后...
    将军府上阅读 1,065评论 0 10
  • 一 人生大部分旅途,无论你是处于何种阶段,你与自己在一起的时刻是最多的,所以取悦自己是一个特别重要的事情。 小唯给...
    凌笑阅读 615评论 6 14