Dunwu Blog

大道至简,知易行难

Spring 访问 Redis

简介

Redis 是一个被数百万开发人员用作数据库、缓存、流引擎和消息代理的开源内存数据库。

在 Spring 中,spring-data-redis 项目对访问 Redis 进行了 API 封装,提供了便捷的访问方式。 spring-data-redis

spring-boot 项目中的子模块 spring-boot-starter-data-redis 基于 spring-data-redis 项目,做了二次封装,大大简化了 Redis 的相关配置。

Spring Boot 快速入门

引入依赖

在 pom.xml 中引入依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

数据源配置

1
2
3
4
spring.redis.database = 0
spring.redis.host = localhost
spring.redis.port = 6379
spring.redis.password =

定义实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {

private static final long serialVersionUID = 4142994984277644695L;

private Long id;
private String name;
private Integer age;
private String address;
private String email;

}

定义 CRUD 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Map;

public interface UserService {

void batchSetUsers(Map<String, User> users);

long count();

User getUser(Long id);

void setUser(User user);

}

创建 CRUD 接口实现

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

import cn.hutool.core.bean.BeanUtil;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class UserServiceImpl implements UserService {

public static final String DEFAULT_KEY = "spring:tutorial:user";

private final RedisTemplate<String, Object> redisTemplate;

public UserServiceImpl(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}

@Override
public void batchSetUsers(Map<String, User> users) {
redisTemplate.opsForHash().putAll(DEFAULT_KEY, users);
}

@Override
public long count() {
return redisTemplate.opsForHash().size(DEFAULT_KEY);
}

@Override
public User getUser(Long id) {
Object obj = redisTemplate.opsForHash().get(DEFAULT_KEY, id.toString());
return BeanUtil.toBean(obj, User.class);
}

@Override
public void setUser(User user) {
redisTemplate.opsForHash().put(DEFAULT_KEY, user.getId().toString(), user);
}

}

创建 Application

创建 Application,实例化一个 RedisTemplate 对象。

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
55
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Slf4j
@SpringBootApplication
public class RedisQuickstartApplication {

@Autowired
private ObjectMapper objectMapper;

@Bean
@Primary
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
// objectMapper.activateDefaultTyping(new DefaultBaseTypeLimitingValidator(),
// ObjectMapper.DefaultTyping.NON_FINAL);

// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
serializer.setObjectMapper(objectMapper);

RedisTemplate<String, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(factory);
// 值采用json序列化
template.setValueSerializer(serializer);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
// 设置hash key 和value序列化模式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();

return template;
}

public static void main(String[] args) {
SpringApplication.run(RedisQuickstartApplication.class, args);
}

}

测试

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
@Slf4j
@SpringBootTest(classes = { RedisQuickstartApplication.class })
public class RedisQuickstartTests {

@Autowired
private UserService userService;

@Test
public void test() {
final long SIZE = 1000L;
Map<String, User> map = new HashMap<>();
for (long i = 0; i < SIZE; i++) {
User user = new User(i, RandomUtil.randomChineseName(),
RandomUtil.randomInt(1, 100),
RandomUtil.randomEnum(Location.class).name(),
RandomUtil.randomEmail());
map.put(String.valueOf(i), user);
}
userService.batchSetUsers(map);
long count = userService.count();
Assertions.assertThat(count).isEqualTo(SIZE);

for (int i = 0; i < 100; i++) {
long id = RandomUtil.randomLong(0, 1000);
User user = userService.getUser(id);
log.info("user-{}: {}", id, user.toString());
}
}

}

示例源码

更多 Spring 访问 Redis 示例请参考:Redis 示例源码

参考资料

Spring 应用上下文生命周期

Spring 应用上下文启动准备阶段

AbstractApplicationContext#prepareRefresh() 方法

  • 启动时间 - startupDate
  • 状态标识 - closed(false)、active(true)
  • 初始化 PropertySources - initPropertySources()
  • 检验 Environment 中必须属性
  • 初始化事件监听器集合
  • 初始化早期 Spring 事件集合

BeanFactory 创建阶段

AbstractApplicationContext#obtainFreshBeanFactory() 方法

  • 刷新 Spring 应用上下文底层 BeanFactory - refreshBeanFactory()
    • 销毁或关闭 BeanFactory,如果已存在的话
    • 创建 BeanFactory - createBeanFactory()
    • 设置 BeanFactory Id
    • 设置“是否允许 BeanDefinition 重复定义” - customizeBeanFactory(DefaultListableBeanFactory)
    • 设置“是否允许循环引用(依赖)” - customizeBeanFactory(DefaultListableBeanFactory)
    • 加载 BeanDefinition - loadBeanDefinitions(DefaultListableBeanFactory) 方法
    • 关联新建 BeanFactory 到 Spring 应用上下文
  • 返回 Spring 应用上下文底层 BeanFactory - getBeanFactory()

BeanFactory 准备阶段

AbstractApplicationContext#prepareBeanFactory(ConfigurableListableBeanFactory) 方法

  • 关联 ClassLoader
  • 设置 Bean 表达式处理器
  • 添加 PropertyEditorRegistrar 实现 - ResourceEditorRegistrar
  • 添加 Aware 回调接口 BeanPostProcessor 实现 - ApplicationContextAwareProcessor
  • 忽略 Aware 回调接口作为依赖注入接口
  • 注册 ResolvableDependency 对象 - BeanFactory、ResourceLoader、ApplicationEventPublisher 以及 ApplicationContext
  • 注册 ApplicationListenerDetector 对象
  • 注册 LoadTimeWeaverAwareProcessor 对象
  • 注册单例对象 - Environment、Java System Properties 以及 OS 环境变量

BeanFactory 后置处理阶段

  • AbstractApplicationContext#postProcessBeanFactory(ConfigurableListableBeanFactory) 方法
    • 由子类覆盖该方法
  • AbstractApplicationContext#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory 方法
    • 调用 BeanFactoryPostProcessor 或 BeanDefinitionRegistry 后置处理方法
    • 注册 LoadTimeWeaverAwareProcessor 对象

BeanFactory 注册 BeanPostProcessor 阶段

AbstractApplicationContext#registerBeanPostProcessors(ConfigurableListableBeanFactory) 方法

  • 注册 PriorityOrdered 类型的 BeanPostProcessor Beans
  • 注册 Ordered 类型的 BeanPostProcessor Beans
  • 注册普通 BeanPostProcessor Beans
  • 注册 MergedBeanDefinitionPostProcessor Beans
  • 注册 ApplicationListenerDetector 对象

初始化內建 Bean:MessageSource

AbstractApplicationContext#initMessageSource() 方法

初始化內建 Bean:Spring 事件广播器

AbstractApplicationContext#initApplicationEventMulticaster() 方法

Spring 应用上下文刷新阶段

AbstractApplicationContext#onRefresh() 方法

子类覆盖该方法

  • org.springframework.web.context.support.AbstractRefreshableWebApplicationContext#onRefresh()
  • org.springframework.web.context.support.GenericWebApplicationContext#onRefresh()
  • org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh()
  • org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh()
  • org.springframework.web.context.support.StaticWebApplicationContext#onRefresh()

Spring 事件监听器注册阶段

AbstractApplicationContext#registerListeners() 方法

  • 添加当前应用上下文所关联的 ApplicationListener 对象(集合)
  • 添加 BeanFactory 所注册 ApplicationListener Beans
  • 广播早期 Spring 事件

BeanFactory 初始化完成阶段

AbstractApplicationContext#finishBeanFactoryInitialization(ConfigurableListableBeanFactory) 方法

  • BeanFactory 关联 ConversionService Bean,如果存在
  • 添加 StringValueResolver 对象
  • 依赖查找 LoadTimeWeaverAware Bean
  • BeanFactory 临时 ClassLoader 置为 null
  • BeanFactory 冻结配置
  • BeanFactory 初始化非延迟单例 Beans

Spring 应用上下刷新完成阶段

AbstractApplicationContext#finishRefresh() 方法

  • 清除 ResourceLoader 缓存 - clearResourceCaches() @since 5.0
  • 初始化 LifecycleProcessor 对象 - initLifecycleProcessor()
  • 调用 LifecycleProcessor#onRefresh() 方法
  • 发布 Spring 应用上下文已刷新事件 - ContextRefreshedEvent
  • 向 MBeanServer 托管 Live Beans

Spring 应用上下文启动阶段

AbstractApplicationContext#start() 方法

  • 启动 LifecycleProcessor
    • 依赖查找 Lifecycle Beans
    • 启动 Lifecycle Beans
  • 发布 Spring 应用上下文已启动事件 - ContextStartedEvent

Spring 应用上下文停止阶段

AbstractApplicationContext#stop() 方法

  • 停止 LifecycleProcessor
    • 依赖查找 Lifecycle Beans
    • 停止 Lifecycle Beans
  • 发布 Spring 应用上下文已停止事件 - ContextStoppedEvent

Spring 应用上下文关闭阶段

AbstractApplicationContext#close() 方法

  • 状态标识:active(false)、closed(true)
  • Live Beans JMX 撤销托管
    • LiveBeansView.unregisterApplicationContext(ConfigurableApplicationContext)
  • 发布 Spring 应用上下文已关闭事件 - ContextClosedEvent
  • 关闭 LifecycleProcessor
    • 依赖查找 Lifecycle Beans
    • 停止 Lifecycle Beans
  • 销毁 Spring Beans
  • 关闭 BeanFactory
  • 回调 onClose()
  • 注册 Shutdown Hook 线程(如果曾注册)

问题

Spring 应用上下文生命周期有哪些阶段

  • 刷新阶段 - ConfigurableApplicationContext#refresh()
  • 启动阶段 - ConfigurableApplicationContext#start()
  • 停止阶段 - ConfigurableApplicationContext#stop()
  • 关闭阶段 - ConfigurableApplicationContext#close()

参考资料

Spring Environment 抽象

理解 Spring Environment 抽象

统一的 Spring 配置属性管理

Spring Framework 3.1 开始引入 Environment 抽象,它统一 Spring 配置属性的存储,包括占位符处理和类型转换,不仅完整地替换 PropertyPlaceholderConfigurer,而且还支持更丰富的配置属性源(PropertySource)

条件化 Spring Bean 装配管理

通过 Environment Profiles 信息,帮助 Spring 容器提供条件化地装配 Bean

Spring Environment 接口使用场景

  • ⽤于属性占位符处理
  • 用于转换 Spring 配置属性类型
  • 用于存储 Spring 配置属性源(PropertySource)
  • 用于 Profiles 状态的维护

Environment 占位符处理

Spring 3.1 前占位符处理

  • 组件:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer
  • 接口:org.springframework.util.StringValueResolver

Spring 3.1 + 占位符处理

  • 组件:org.springframework.context.support.PropertySourcesPlaceholderConfigurer
  • 接口:org.springframework.beans.factory.config.EmbeddedValueResolver

理解条件配置 Spring Profiles

Spring 3.1 条件配置

  • API:org.springframework.core.env.ConfigurableEnvironment
  • 修改:addActiveProfile(String)、setActiveProfiles(String…) 和 setDefaultProfiles(String…)
  • 获取:getActiveProfiles() 和 getDefaultProfiles()
  • 匹配:#acceptsProfiles(String…) 和 acceptsProfiles(Profiles)
  • 注解:@org.springframework.context.annotation.Profile

Spring 4 重构 @Profile

基于 Spring 4 org.springframework.context.annotation.Condition 接口实现

org.springframework.context.annotation.ProfileCondition

依赖注入 Environment

直接依赖注入

  • 通过 EnvironmentAware 接口回调
  • 通过 @Autowired 注入 Environment

间接依赖注入

  • 通过 ApplicationContextAware 接口回调
  • 通过 @Autowired 注入 ApplicationContext

依赖查找 Environment

直接依赖查找

  • 通过 org.springframework.context.ConfigurableApplicationContext#ENVIRONMENT_BEAN_NAME

间接依赖查找

  • 通过 org.springframework.context.ConfigurableApplicationContext#getEnvironment

依赖注入 @Value

通过注入 @Value

实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor

Spring 类型转换在 Environment 中的运用

Environment 底层实现

  • 底层实现 - org.springframework.core.env.PropertySourcesPropertyResolver
  • 核心方法 - convertValueIfNecessary(Object,Class)
  • 底层服务 - org.springframework.core.convert.ConversionService
  • 默认实现 - org.springframework.core.convert.support.DefaultConversionService

Spring 类型转换在 @Value 中的运用

@Value 底层实现

  • 底层实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
    • org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency
  • 底层服务 - org.springframework.beans.TypeConverter
    • 默认实现 - org.springframework.beans.TypeConverterDelegate
      • java.beans.PropertyEditor
      • org.springframework.core.convert.ConversionService

Spring 配置属性源 PropertySource

  • API
    • 单配置属性源 - org.springframework.core.env.PropertySource
    • 多配置属性源 - org.springframework.core.env.PropertySources
  • 注解
    • 单配置属性源 - @org.springframework.context.annotation.PropertySource
    • 多配置属性源 - @org.springframework.context.annotation.PropertySources
  • 关联
    • 存储对象 - org.springframework.core.env.MutablePropertySources
    • 关联方法 - org.springframework.core.env.ConfigurableEnvironment#getPropertySources()

Spring 內建的配置属性源

內建 PropertySource

PropertySource 类型 说明
org.springframework.core.env.CommandLinePropertySource 命令行配置属性源
org.springframework.jndi.JndiPropertySource JDNI 配置属性源
org.springframework.core.env.PropertiesPropertySource Properties 配置属性源
org.springframework.web.context.support.ServletConfigPropertySource Servlet 配置属性源
org.springframework.web.context.support.ServletContextPropertySource ServletContext 配置属性源
org.springframework.core.env.SystemEnvironmentPropertySource 环境变量配置属性源

基于注解扩展 Spring 配置属性源

@org.springframework.context.annotation.PropertySource 实现原理

  • 入口 - org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass
    • org.springframework.context.annotation.ConfigurationClassParser#processPropertySource
  • 4.3 新增语义
    • 配置属性字符编码 - encoding
    • org.springframework.core.io.support.PropertySourceFactory
  • 适配对象 - org.springframework.core.env.CompositePropertySource

基于 API 扩展 Spring 配置属性源

  • Spring 应用上下文启动前装配 PropertySource
  • Spring 应用上下文启动后装配 PropertySource

问题

简单介绍 Spring Environment 接口?

  • 核心接口 - org.springframework.core.env.Environment
  • 父接口 - org.springframework.core.env.PropertyResolver
  • 可配置接口 - org.springframework.core.env.ConfigurableEnvironment
  • 职责:
    • 管理 Spring 配置属性源
    • 管理 Profiles

参考资料

Spring 注解

Spring 注解驱动编程发展历程

  • 注解驱动启蒙时代:Spring Framework 1.x
  • 注解驱动过渡时代:Spring Framework 2.x
  • 注解驱动黄金时代:Spring Framework 3.x
  • 注解驱动完善时代:Spring Framework 4.x
  • 注解驱动当下时代:Spring Framework 5.x

Spring 核心注解场景分类

Spring 模式注解

Spring 注解 场景说明 起始版本
@Repository 数据仓储模式注解 2.0
@Component 通用组件模式注解 2.5
@Service 服务模式注解 2.5
@Controller Web 控制器模式注解 2.5
@Configuration 配置类模式注解 3.0

装配注解

Spring 注解 场景说明 起始版本
@ImportResource 替换 XML 元素 <import> 2.5
@Import 导入 Configuration 类 2.5
@ComponentScan 扫描指定 package 下标注 Spring 模式注解的类 3.1

依赖注入注解

Spring 注解 场景说明 起始版本
@Autowired Bean 依赖注入,支持多种依赖查找方式 2.5
@Qualifier 细粒度的 @Autowired 依赖查找 2.5

Spring 注解编程模型

  • 元注解(Meta-Annotations)
  • Spring 模式注解(Stereotype Annotations)
  • Spring 组合注解(Composed Annotations)
  • Spring 注解属性别名和覆盖(Attribute Aliases and Overrides)

Spring 元注解(Meta-Annotations)

  • java.lang.annotation.Documented
  • java.lang.annotation.Inherited
  • java.lang.annotation.Repeatable

Spring 模式注解(Stereotype Annotations)

理解 @Component “派⽣性”:元标注 @Component 的注解在 XML 元素 context:component-scan 或注解 @ComponentScan 扫描中“派生”了 @Component 的特性,并且从 Spring Framework 4.0 开始支持多层次“派⽣性”。

举例说明:

  • @Repository
  • @Service
  • @Controller
  • @Configuration
  • @SpringBootConfiguration(Spring Boot)

@Component “派⽣性”原理

  • 核心组件 - org.springframework.context.annotation.ClassPathBeanDefinitionScanner
  • org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
  • 资源处理 - org.springframework.core.io.support.ResourcePatternResolver
  • 资源-类元信息
  • org.springframework.core.type.classreading.MetadataReaderFactory
  • 类元信息 - org.springframework.core.type.ClassMetadata
  • ASM 实现 - org.springframework.core.type.classreading.ClassMetadataReadingVisitor
  • 反射实现 - org.springframework.core.type.StandardAnnotationMetadata
  • 注解元信息 - org.springframework.core.type.AnnotationMetadata
  • ASM 实现 - org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor
  • 反射实现 - org.springframework.core.type.StandardAnnotationMetadata

Spring 组合注解(Composed Annotations)

Spring 组合注解(Composed Annotations)中的元注允许是 Spring 模式注解(Stereotype Annotation)与其他 Spring 功能性注解的任意组合。

Spring 注解属性别名(Attribute Aliases)

Spring 注解属性覆盖(Attribute Overrides)

Spring @Enable 模块驱动

@Enable 模块驱动

@Enable 模块驱动是以 @Enable 为前缀的注解驱动编程模型。所谓“模块”是指具备相同领域的功能组件集合,组合所形成⼀个独⽴的单元。⽐如 Web MVC 模块、AspectJ 代理模块、Caching(缓存)模块、JMX(Java 管理扩展)模块、Async(异步处理)模块等。

举例说明

  • @EnableWebMvc
  • @EnableTransactionManagement
  • @EnableCaching
  • @EnableMBeanExport
  • @EnableAsync

@Enable 模块驱动编程模式

  • 驱动注解:@EnableXXX
  • 导入注解:@Import 具体实现
  • 具体实现
  • 基于 Configuration Class
  • 基于 ImportSelector 接口实现
  • 基于 ImportBeanDefinitionRegistrar 接口实现

Spring 条件注解

基于配置条件注解 - @org.springframework.context.annotation.Profile

  • 关联对象 - org.springframework.core.env.Environment 中的 Profiles
  • 实现变化:从 Spring 4.0 开始,@Profile 基于 @Conditional 实现

基于编程条件注解 - @org.springframework.context.annotation.Conditional

  • 关联对象 - org.springframework.context.annotation.Condition 具体实现

@Conditional 实现原理

  • 上下文对象 - org.springframework.context.annotation.ConditionContext
  • 条件判断 - org.springframework.context.annotation.ConditionEvaluator
  • 配置阶段 - org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase
  • 判断入口
    • org.springframework.context.annotation.ConfigurationClassPostProcessor
    • org.springframework.context.annotation.ConfigurationClassParser

参考资料

Spring 事件

Java 事件/监听器编程模型

设计模式 - 观察者模式扩展

  • 可观者对象(消息发送者) - java.util.Observable
  • 观察者 - java.util.Observer

标准化接口

  • 事件对象 - java.util.EventObject
  • 事件监听器 - java.util.EventListener

面向接口的事件/监听器设计模式

事件/监听器场景举例

Java 技术规范 事件接口 监听器接口
JavaBeans java.beans.PropertyChangeEvent java.beans.PropertyChangeListener
Java AWT java.awt.event.MouseEvent java.awt.event.MouseListener
Java Swing javax.swing.event.MenuEvent javax.swing.event.MenuListener
Java Preference java.util.prefs.PreferenceChangeEvent java.util.prefs.PreferenceChangeListener

面向注解的事件/监听器设计模式

事件/监听器注解场景举例

Java 技术规范 事件注解 监听器注解
Servlet 3.0+ @javax.servlet.annotation.WebListener
JPA 1.0+ @javax.persistence.PostPersist
Java Common @PostConstruct
EJB 3.0+ @javax.ejb.PrePassivate
JSF 2.0+ @javax.faces.event.ListenerFor

Spring 标准事件 - ApplicationEvent

Java 标准事件 java.util.EventObject 扩展

  • 扩展特性:事件发生事件戳
  • Spring 应用上下文 ApplicationEvent 扩展 - ApplicationContextEvent
  • Spring 应用上下文(ApplicationContext)作为事件源

具体实现:

  • org.springframework.context.event.ContextClosedEvent
  • org.springframework.context.event.ContextRefreshedEvent
  • org.springframework.context.event.ContextStartedEvent
  • org.springframework.context.event.ContextStoppedEvent

基于接口的 Spring 事件监听器

Java 标准事件监听器 java.util.EventListener 扩展

  • 扩展接口 - org.springframework.context.ApplicationListener
  • 设计特点:单一类型事件处理
  • 处理方法:onApplicationEvent(ApplicationEvent)
  • 事件类型:org.springframework.context.ApplicationEvent

基于注解的 Spring 事件监听器

Spring 注解 - @org.springframework.context.event.EventListener

特性 说明
设计特点 支持多 ApplicationEvent 类型,无需接口约束
注解目标 方法
是否支持异步执行 支持
是否支持泛型类型事件 支持
是指支持顺序控制 支持,配合 @Order 注解控制

注册 Spring ApplicationListener

  • 方法一:ApplicationListener 作为 Spring Bean 注册
  • 方法二:通过 ConfigurableApplicationContext API 注册

Spring 事件发布器

  • 方法一:通过 ApplicationEventPublisher 发布 Spring 事件
    • 获取 ApplicationEventPublisher
      • 依赖注入
  • 方法二:通过 ApplicationEventMulticaster 发布 Spring 事件
    • 获取 ApplicationEventMulticaster
      • 依赖注入
      • 依赖查找

Spring 层次性上下文事件传播

  • 发生说明
  • 当 Spring 应用出现多层次 Spring 应用上下文(ApplicationContext)时,如 Spring WebMVC、Spring Boot 或 Spring Cloud 场景下,由子 ApplicationContext 发起 Spring 事件可能会传递到其 Parent ApplicationContext(直到 Root)的过程
  • 如何避免
  • 定位 Spring 事件源(ApplicationContext)进行过滤处理

Spring 内建事件

ApplicationContextEvent 派生事件

  • ContextRefreshedEvent :Spring 应用上下文就绪事件
  • ContextStartedEvent :Spring 应用上下文启动事件
  • ContextStoppedEvent :Spring 应用上下文停止事件
  • ContextClosedEvent :Spring 应用上下文关闭事件

Spring 4.2 Payload 事件

Spring Payload 事件 - org.springframework.context.PayloadApplicationEvent

  • 使用场景:简化 Spring 事件发送,关注事件源主体
  • 发送方法:ApplicationEventPublisher#publishEvent(java.lang.Object)

自定义 Spring 事件

  • 扩展 org.springframework.context.ApplicationEvent
  • 实现 org.springframework.context.ApplicationListener
  • 注册 org.springframework.context.ApplicationListener

依赖注入 ApplicationEventPublisher

  • 通过 ApplicationEventPublisherAware 回调接口
  • 通过 @Autowired ApplicationEventPublisher

依赖查找 ApplicationEventMulticaster

查找条件

  • Bean 名称:”applicationEventMulticaster”
  • Bean 类型:org.springframework.context.event.ApplicationEventMulticaster

ApplicationEventPublisher 底层实现

  • 接口:org.springframework.context.event.ApplicationEventMulticaster
  • 抽象类:org.springframework.context.event.AbstractApplicationEventMulticaster
  • 实现类:org.springframework.context.event.SimpleApplicationEventMulticaster

同步和异步 Spring 事件广播

基于实现类 - org.springframework.context.event.SimpleApplicationEventMulticaster

  • 模式切换:setTaskExecutor(java.util.concurrent.Executor) 方法
    • 默认模式:同步
    • 异步模式:如 java.util.concurrent.ThreadPoolExecutor
  • 设计缺陷:非基于接口契约编程

基于注解 - @org.springframework.context.event.EventListener

  • 模式切换
    • 默认模式:同步
    • 异步模式:标注 @org.springframework.scheduling.annotation.Async
  • 实现限制:无法直接实现同步/异步动态切换

Spring 4.1 事件异常处理

Spring 3.0 错误处理接口 - org.springframework.util.ErrorHandler

使用场景

  • Spring 事件(Events)
    • SimpleApplicationEventMulticaster Spring 4.1 开始支持
  • Spring 本地调度(Scheduling)
    • org.springframework.scheduling.concurrent.ConcurrentTaskScheduler
    • org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler

Spring 事件/监听器实现原理

核心类 - org.springframework.context.event.SimpleApplicationEventMulticaster

  • 设计模式:观察者模式扩展
    • 被观察者 - org.springframework.context.ApplicationListener
      • API 添加
      • 依赖查找
    • 通知对象 - org.springframework.context.ApplicationEvent
  • 执行模式:同步/异步
  • 异常处理:org.springframework.util.ErrorHandler
  • 泛型处理:org.springframework.core.ResolvableType

问题

Spring Boot 事件

事件类型 发生时机
ApplicationStartingEvent 当 Spring Boot 应用已启动时
ApplicationStartedEvent 当 Spring Boot 应用已启动时
ApplicationEnvironmentPreparedEvent 当 Spring Boot Environment 实例已准备时
ApplicationPreparedEvent 当 Spring Boot 应用预备时
ApplicationReadyEvent 当 Spring Boot 应用完全可用时
ApplicationFailedEvent 当 Spring Boot 应用启动失败时

Spring Cloud 事件

事件类型 发生时机
EnvironmentChangeEvent 当 Environment 示例配置属性发生变化时
HeartbeatEvent 当 DiscoveryClient 客户端发送心跳时
InstancePreRegisteredEvent 当服务实例注册前
InstanceRegisteredEvent 当服务实例注册后
RefreshEvent 当 RefreshEndpoint 被调用时
RefreshScopeRefreshedEvent 当 Refresh Scope Bean 刷新后

Spring 事件核心接口/组件

  • Spring 事件 - org.springframework.context.ApplicationEvent
  • Spring 事件监听器 - org.springframework.context.ApplicationListener
  • Spring 事件发布器 - org.springframework.context.ApplicationEventPublisher
  • Spring 事件广播器 - org.springframework.context.event.ApplicationEventMulticaster

Spring 同步和异步事件处理的使用场景

  • Spring 同步事件 - 绝大多数 Spring 使用场景,如 ContextRefreshedEvent
  • Spring 异步事件 - 主要 @EventListener 与 @Async 配合,实现异步处理,不阻塞主线程,比如长时间的数据计算任务等。不要轻易调整 SimpleApplicationEventMulticaster 中关联的 taskExecutor 对象,除非使用者非常了解 Spring 事件机制,否则容易出现异常行为。

参考资料

Spring 泛型处理

Java 泛型基础

泛型类型

  • 泛型类型是在类型上参数化的泛型类或接口

泛型使用场景

  • 编译时强类型检查
  • 避免类型强转
  • 实现通用算法

泛型类型擦写

  • 泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。类型擦除确保不会
    为参数化类型创建新类;因此,泛型不会产生运行时开销。为了实现泛型,编译器将类型擦除应用于:
    • 将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则将其替换为
      “Object”。因此,生成的字节码只包含普通类、接口和方法
    • 必要时插入类型转换以保持类型安全
    • 生成桥方法以保留扩展泛型类型中的多态性

Java 5 类型接口

Java 5 类型接口 - java.lang.reflect.Type

派生类或接口 说明
java.lang.Class Java 类 API,如 java.lang.String
java.lang.reflect.GenericArrayType 泛型数组类型
java.lang.reflect.ParameterizedType 泛型参数类型
java.lang.reflect.TypeVariable 泛型类型变量,如 Collection<E> 中的 E
java.lang.reflect.WildcardType 泛型通配类型

Java 泛型反射 API

类型 API
泛型信息(Generics Info) java.lang.Class#getGenericInfo()
泛型参数(Parameters) java.lang.reflect.ParameterizedType
泛型父类(Super Classes) java.lang.Class#getGenericSuperclass()
泛型接口(Interfaces) java.lang.Class#getGenericInterfaces()
泛型声明(Generics Declaration) java.lang.reflect.GenericDeclaration

Spring 泛型类型辅助类

核心 API - org.springframework.core.GenericTypeResolver

  • 版本支持:[2.5.2 , )
  • 处理类型相关(Type)相关方法
    • resolveReturnType
    • resolveType
  • 处理泛型参数类型(ParameterizedType)相关方法
    • resolveReturnTypeArgument
    • resolveTypeArgument
    • resolveTypeArguments
  • 处理泛型类型变量(TypeVariable)相关方法
    • getTypeVariableMap

Spring 泛型集合类型辅助类

核心 API - org.springframework.core.GenericCollectionTypeResolver

  • 版本支持:[2.0 , 4.3]
  • 替换实现:org.springframework.core.ResolvableType
  • 处理 Collection 相关
    • getCollection*Type
  • 处理 Map 相关
    • getMapKey*Type
    • getMapValue*Type

Spring 方法参数封装 - MethodParameter

核心 API - org.springframework.core.MethodParameter

  • 起始版本:[2.0 , )
  • 元信息
    • 关联的方法 - Method
    • 关联的构造器 - Constructor
    • 构造器或方法参数索引 - parameterIndex
    • 构造器或方法参数类型 - parameterType
    • 构造器或方法参数泛型类型 - genericParameterType
    • 构造器或方法参数参数名称 - parameterName
    • 所在的类 - containingClass

Spring 4.0 泛型优化实现 - ResolvableType

核心 API - org.springframework.core.ResolvableType

  • 起始版本:[4.0 , )
  • 扮演角色:GenericTypeResolverGenericCollectionTypeResolver 替代者
  • 工厂方法:for* 方法
  • 转换方法:as* 方法
  • 处理方法:resolve* 方法

ResolvableType 的局限性

  • 局限一:ResolvableType 无法处理泛型擦写
  • 局限二:ResolvableType 无法处理非具体化的 ParameterizedType

问题

Java 泛型擦写发生在编译时还是运行时

运行时

请介绍 Java 5 Type 类型的派生类或接口

  • java.lang.Class
  • java.lang.reflect.GenericArrayType
  • java.lang.reflect.ParameterizedType
  • java.lang.reflect.TypeVariable
  • java.lang.reflect.WildcardType

请说明 ResolvableType 的设计优势

  • 简化 Java 5 Type API 开发,屏蔽复杂 API 的运用,如 ParameterizedType
  • 不变性设计(Immutability)
  • Fluent API 设计(Builder 模式),链式(流式)编程

参考资料

Spring 类型转换

Spring 类型转换的实现

  • 基于 JavaBeans 接口的类型转换实现
    • 基于 java.beans.PropertyEditor 接口扩展
  • Spring 3.0+ 通用类型转换实现

使用场景

场景 基于 JavaBeans 接口的类型转换实现 Spring 3.0+ 通用类型转换实现
数据绑定 YES YES
BeanWrapper YES YES
Bean 属性类型转换 YES YES
外部化属性类型转换 NO YES

基于 JavaBeans 接口的类型转换

核心职责

  • 将 String 类型的内容转化为目标类型的对象

扩展原理

  • Spring 框架将文本内容传递到 PropertyEditor 实现的 setAsText(String) 方法
  • PropertyEditor#setAsText(String) 方法实现将 String 类型转化为目标类型的对象
  • 将目标类型的对象传入 PropertyEditor#setValue(Object) 方法
  • PropertyEditor#setValue(Object) 方法实现需要临时存储传入对象
  • Spring 框架将通过 PropertyEditor#getValue() 获取类型转换后的对象

Spring 內建 PropertyEditor 扩展

內建扩展(org.springframework.beans.propertyeditors 包下)

转换场景 实现类
String -> Byte 数组 org.springframework.beans.propertyeditors.ByteArrayPropertyEditor
String -> Char org.springframework.beans.propertyeditors.CharacterEditor
String -> Char 数组 org.springframework.beans.propertyeditors.CharArrayPropertyEditor
String -> Charset org.springframework.beans.propertyeditors.CharsetEditor
String -> Class org.springframework.beans.propertyeditors.ClassEditor
String -> Currency org.springframework.beans.propertyeditors.CurrencyEditor

自定义 PropertyEditor 扩展

扩展模式

  • 扩展 java.beans.PropertyEditorSupport

实现 org.springframework.beans.PropertyEditorRegistrar

  • 实现 registerCustomEditors(org.springframework.beans.PropertyEditorRegistry) 方法
  • PropertyEditorRegistrar 实现注册为 Spring Bean

org.springframework.beans.PropertyEditorRegistry 注册自定义 PropertyEditor 实现

  • 通用类型实现 registerCustomEditor(Class<?>, PropertyEditor)
  • Java Bean 属性类型实现:registerCustomEditor(Class<?>, String, PropertyEditor)

Spring PropertyEditor 的设计缺陷

违反职责单一原则

  • java.beans.PropertyEditor 接口职责太多,除了类型转换,还包括 Java Beans 事件和 Java GUI 交

java.beans.PropertyEditor 实现类型局限

  • 来源类型只能为 java.lang.String 类型

java.beans.PropertyEditor 实现缺少类型安全

  • 除了实现类命名可以表达语义,实现类无法感知目标转换类型

Spring 3 通用类型转换接口

类型转换接口 - org.springframework.core.convert.converter.Converter<S,T>

  • 泛型参数 S:来源类型,参数 T:目标类型
  • 核心方法:T convert(S)

通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter

  • 核心方法:convert(Object,TypeDescriptor,TypeDescriptor)
  • 配对类型:org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
  • 类型描述:org.springframework.core.convert.TypeDescriptor

Spring 內建类型转换器

內建扩展

转换场景 实现类所在包名(package)
日期/时间相关 org.springframework.format.datetime
Java 8 日期/时间相关 org.springframework.format.datetime.standard
通用实现 org.springframework.core.convert.support

Converter 接口的局限性

局限一:缺少 Source Type 和 Target Type 前置判断

  • 应对:增加 org.springframework.core.convert.converter.ConditionalConverter 实现

局限二:仅能转换单一的 Source Type 和 Target Type

  • 应对:使用 org.springframework.core.convert.converter.GenericConverter 代替

GenericConverter 接口

org.springframework.core.convert.converter.GenericConverter

核心要素 说明
使用场景 用于“复合”类型转换场景,比如 Collection、Map、数组等
转换范围 Set<ConvertiblePair> getConvertibleTypes()
配对类型 org.springframework.core.convert.converter.GenericConverter.ConvertiblePair
转换方法 convert(Object,TypeDescriptor,TypeDescriptor)
类型描述 org.springframework.core.convert.TypeDescriptor

优化 GenericConverter 接口

GenericConverter 局限性

  • 缺少 Source Type 和 Target Type 前置判断
  • 单一类型转换实现复杂

GenericConverter 优化接口 - ConditionalGenericConverter

  • 复合类型转换:org.springframework.core.convert.converter.GenericConverter
  • 类型条件判断:org.springframework.core.convert.converter.ConditionalConverter

扩展 Spring 类型转换器

实现转换器接口

  • org.springframework.core.convert.converter.Converter
  • org.springframework.core.convert.converter.ConverterFactory
  • org.springframework.core.convert.converter.GenericConverter

注册转换器实现

  • 通过 ConversionServiceFactoryBean Spring Bean
  • 通过 org.springframework.core.convert.ConversionService API

统一类型转换服务

org.springframework.core.convert.ConversionService

实现类型 说明
GenericConversionService 通用 ConversionService 模板实现,不内置转化器实现
DefaultConversionService 基础 ConversionService 实现,内置常用转化器实现
FormattingConversionService 通用 Formatter + GenericConversionService 实现,不内置转化器和 Formatter 实现
DefaultFormattingConversionService DefaultConversionService + 格式化 实现(如:JSR-354 Money & Currency, JSR-310 Date-Time)

ConversionService 作为依赖

类型转换器底层接口 - org.springframework.beans.TypeConverter

  • 起始版本:Spring 2.0
  • 核心方法 - convertIfNecessary 重载方法
  • 抽象实现 - org.springframework.beans.TypeConverterSupport
  • 简单实现 - org.springframework.beans.SimpleTypeConverter

类型转换器底层抽象实现 - org.springframework.beans.TypeConverterSupport

  • 实现接口 - org.springframework.beans.TypeConverter
  • 扩展实现 - org.springframework.beans.PropertyEditorRegistrySupport
  • 委派实现 - org.springframework.beans.TypeConverterDelegate

类型转换器底层委派实现 - org.springframework.beans.TypeConverterDelegate

  • 构造来源 - org.springframework.beans.AbstractNestablePropertyAccessor 实现
    • org.springframework.beans.BeanWrapperImpl
  • 依赖 - java.beans.PropertyEditor 实现
    • 默认內建实现 - PropertyEditorRegistrySupport#registerDefaultEditors
  • 可选依赖 - org.springframework.core.convert.ConversionService 实现

问题

Spring 类型转换实现有哪些

  • 基于 JavaBeans PropertyEditor 接口实现
  • Spring 3.0+ 通用类型转换实现

Spring 类型转换器接口有哪些

  • 类型转换接口 - org.springframework.core.convert.converter.Converter
  • 通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter
  • 类型条件接口 - org.springframework.core.convert.converter.ConditionalConverter
  • 综合类型转换接口 - org.springframework.core.convert.converter.ConditionalGenericConverter

参考资料

Spring 数据绑定

Spring 数据绑定(Data Binding)的作用是将用户的输入动态绑定到 JavaBean。换句话说,Spring 数据绑定机制是将属性值设置到目标对象中。

在 Spring 中,数据绑定功能主要由 DataBinder 类实现。此外,BeanWrapper 也具有类似的功能,但 DataBinder 额外支持字段验证、字段格式化和绑定结果分析。

快速入门

定义一个用于测试的 JavaBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestBean {

private int num;

public int getNum() {
return num;
}

public void setNum(int num) {
this.num = num;
}

@Override
public String toString() {
return "TestBean{" + "num=" + num + '}';
}

}

数据绑定示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DataBindingDemo {

public static void main(String[] args) {

MutablePropertyValues mpv = new MutablePropertyValues();
mpv.add("num", "10");

TestBean testBean = new TestBean();
DataBinder db = new DataBinder(testBean);

db.bind(mpv);
System.out.println(testBean);
}

}

Spring 数据绑定使用场景

  • Spring BeanDefinition 到 Bean 实例创建
  • Spring 数据绑定(DataBinder
  • Spring Web 参数绑定(WebDataBinder

DataBinder

在 Spring 中,DataBinder 类是数据绑定功能的基类。WebDataBinderDataBinder 的子类,主要用于 Spring Web 数据绑定,此外,还有一些 WebDataBinder 的扩展子类,其类族如下图所示:

DataBinder 核心属性:

属性 说明
target 关联目标 Bean
objectName 目标 Bean 名称
bindingResult 属性绑定结果
typeConverter 类型转换器
conversionService 类型转换服务
messageCodesResolver 校验错误文案 Code 处理器
validators 关联的 Bean Validator 实例集合

DataBinder 类的核心方法是 bind(PropertyValues):将 PropertyValues Key-Value 内容映射到关联 Bean(target)中的属性上

  • 假设 PropertyValues 中包含 name=dunwu 的键值对时, 同时 Bean 对象 User 中存在 name 属性, 当 bind 方法执行时, User 对象中的 name 属性值将被绑定为 dunwu

Spring 数据绑定元数据

DataBinder 元数据 - PropertyValues

特征 说明
数据来源 BeanDefinition,主要来源 XML 资源配置 BeanDefinition
数据结构 由一个或多个 PropertyValue 组成
成员结构 PropertyValue 包含属性名称,以及属性值(包括原始值、类型转换后的值)
常见实现 MutablePropertyValues
Web 扩展实现 ServletConfigPropertyValues、ServletRequestParameterPropertyValues
相关生命周期 InstantiationAwareBeanPostProcessor#postProcessProperties

Spring 数据绑定控制参数

DataBinder 绑定特殊场景分析

  • 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 不存在 x 属性,当 bind 方法执
    行时,会发生什么?
  • 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 中存在 x 属性,当 bind 方法执
    行时,如何避免 B 属性 x 不被绑定?
  • 当 PropertyValues 中包含名称 x.y 的 PropertyValue,目标对象 B 中存在 x 属性(嵌套 y 属性)
    ,当 bind 方法执行时,会发生什么?

DataBinder 绑定控制参数

参数名称 说明
ignoreUnknownFields 是否忽略未知字段,默认值:true
ignoreInvalidFields 是否忽略非法字段,默认值:false
autoGrowNestedPaths 是否自动增加嵌套路径,默认值:true
allowedFields 绑定字段白名单
disallowedFields 绑定字段黑名单
requiredFields 必须绑定字段

BeanWrapper 的使用场景

  • Spring 底层 JavaBeans 基础设施的中心化接口
  • 通常不会直接使用,间接用于 BeanFactory 和 DataBinder
  • 提供标准 JavaBeans 分析和操作,能够单独或批量存储 Java Bean 的属性(properties)
  • 支持嵌套属性路径(nested path)
  • 实现类 org.springframework.beans.BeanWrapperImpl

Spring 底层 Java Beans 替换实现

JavaBeans 核心实现 - java.beans.BeanInfo

  • 属性(Property)
    • java.beans.PropertyEditor
  • 方法(Method)
  • 事件(Event)
  • 表达式(Expression)

Spring 替代实现 - org.springframework.beans.BeanWrapper

  • 属性(Property)
    • java.beans.PropertyEditor
  • 嵌套属性路径(nested path)

DataBinder 数据校验

DataBinder 与 BeanWrapper

  • bind 方法生成 BeanPropertyBindingResult
  • BeanPropertyBindingResult 关联 BeanWrapper

问题

标准 JavaBeans 是如何操作属性的?

API 说明
java.beans.Introspector Java Beans 内省 API
java.beans.BeanInfo Java Bean 元信息 API
java.beans.BeanDescriptor Java Bean 信息描述符
java.beans.PropertyDescriptor Java Bean 属性描述符
java.beans.MethodDescriptor Java Bean 方法描述符
java.beans.EventSetDescriptor Java Bean 事件集合描述符

参考资料

Spring 校验

Java API 规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email@Length等。Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

快速入门

引入依赖

如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator-parent</artifactId>
<version>6.2.5.Final</version>
</dependency>

对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  • POST、PUT 请求,使用 requestBody 传递参数;
  • GET 请求,使用 requestParam/PathVariable 传递参数。

实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。

校验示例

(1)在实体上标记校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {

@NotNull
private Long id;

@NotBlank
@Size(min = 2, max = 10)
private String name;

@Min(value = 1)
@Max(value = 100)
private Integer age;

}

(2)在方法参数上声明校验注解

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
@Slf4j
@Validated
@RestController
@RequestMapping("validate1")
public class ValidatorController {

/**
* {@link RequestBody} 参数校验
*/
@PostMapping(value = "save")
public DataResult<Boolean> save(@Valid @RequestBody User entity) {
log.info("保存一条记录:{}", JSONUtil.toJsonStr(entity));
return DataResult.ok(true);
}

/**
* {@link RequestParam} 参数校验
*/
@GetMapping(value = "queryByName")
public DataResult<User> queryByName(
@RequestParam("username")
@NotBlank
@Size(min = 2, max = 10)
String name
) {
User user = new User(1L, name, 18);
return DataResult.ok(user);
}

/**
* {@link PathVariable} 参数校验
*/
@GetMapping(value = "detail/{id}")
public DataResult<User> detail(@PathVariable("id") @Min(1L) Long id) {
User user = new User(id, "李四", 18);
return DataResult.ok(user);
}

}

(3)如果请求参数不满足校验规则,则会抛出 ConstraintViolationExceptionMethodArgumentNotValidException 异常。

统一异常处理

在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。

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
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

/**
* 处理所有不可知的异常
*/
@ResponseBody
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(Throwable.class)
public Result handleException(Throwable e) {
log.error("未知异常", e);
return new Result(ResultStatus.HTTP_SERVER_ERROR.getCode(), e.getMessage());
}

/**
* 统一处理请求参数校验异常(普通传参)
*
* @param e ConstraintViolationException
* @return {@link DataResult}
*/
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ ConstraintViolationException.class })
public Result handleConstraintViolationException(final ConstraintViolationException e) {
log.error("ConstraintViolationException", e);
List<String> errors = new ArrayList<>();
for (ConstraintViolation<?> violation : e.getConstraintViolations()) {
Path path = violation.getPropertyPath();
List<String> pathArr = StrUtil.split(path.toString(), ',');
errors.add(pathArr.get(0) + " " + violation.getMessage());
}
return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ","));
}

/**
* 处理参数校验异常
*
* @param e MethodArgumentNotValidException
* @return {@link DataResult}
*/
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler({ MethodArgumentNotValidException.class })
private Result handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException", e);
List<String> errors = new ArrayList<>();
for (ObjectError error : e.getBindingResult().getAllErrors()) {
errors.add(((FieldError) error).getField() + " " + error.getDefaultMessage());
}
return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ","));
}

}

进阶使用

分组校验

在实际项目中,可能多个方法需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。

(1)定义分组

1
2
3
4
5
6
7
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface AddCheck { }

@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface EditCheck { }

(2)在实体上标记校验注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class User2 {

@NotNull(groups = EditCheck.class)
private Long id;

@NotNull(groups = { AddCheck.class, EditCheck.class })
@Size(min = 2, max = 10, groups = { AddCheck.class, EditCheck.class })
private String name;

@IsMobile(message = "不是有效手机号", groups = { AddCheck.class, EditCheck.class })
private String mobile;

}

(3)在方法上根据不同场景进行校验分组

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
@Slf4j
@Validated
@RestController
@RequestMapping("validate2")
public class ValidatorController2 {

/**
* {@link RequestBody} 参数校验
*/
@PostMapping(value = "add")
public DataResult<Boolean> add(@Validated(AddCheck.class) @RequestBody User2 entity) {
log.info("添加一条记录:{}", JSONUtil.toJsonStr(entity));
return DataResult.ok(true);
}

/**
* {@link RequestBody} 参数校验
*/
@PostMapping(value = "edit")
public DataResult<Boolean> edit(@Validated(EditCheck.class) @RequestBody User2 entity) {
log.info("编辑一条记录:{}", JSONUtil.toJsonStr(entity));
return DataResult.ok(true);
}

}

嵌套校验

前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。
post
比如,上面保存 User 信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记@Valid 注解。

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
@Data
public class UserDTO {

@Min(value = 10000000000000000L, groups = Update.class)
private Long userId;

@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String userName;

@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String account;

@NotNull(groups = {Save.class, Update.class})
@Length(min = 6, max = 20, groups = {Save.class, Update.class})
private String password;

@NotNull(groups = {Save.class, Update.class})
@Valid
private Job job;

@Data
public static class Job {

@Min(value = 1, groups = Update.class)
private Long jobId;

@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String jobName;

@NotNull(groups = {Save.class, Update.class})
@Length(min = 2, max = 10, groups = {Save.class, Update.class})
private String position;
}

/**
* 保存的时候校验分组
*/
public interface Save {
}

/**
* 更新的时候校验分组
*/
public interface Update {
}
}
复制代码

嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List<Job>字段会对这个 list 里面的每一个 Job 对象都进行校验

自定义校验注解

(1)自定义校验注解 @IsMobile

1
2
3
4
5
6
7
8
9
10
11
12
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface IsMobile {

String message();

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}

(2)实现 ConstraintValidator 接口,编写 @IsMobile 校验注解的解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import cn.hutool.core.util.StrUtil;
import io.github.dunwu.spring.core.validation.annotation.IsMobile;
import io.github.dunwu.tool.util.ValidatorUtil;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class MobileValidator implements ConstraintValidator<IsMobile, String> {

@Override
public void initialize(IsMobile isMobile) { }

@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (StrUtil.isBlank(s)) {
return false;
} else {
return ValidatorUtil.isMobile(s);
}
}

}

自定义校验

可以通过实现 org.springframework.validation.Validator 接口来自定义校验。

有以下要点

  • 实现 supports 方法
  • 实现 validate 方法
    • 通过 Errors 对象收集错误
      • ObjectError:对象(Bean)错误:
      • FieldError:对象(Bean)属性(Property)错误
    • 通过 ObjectErrorFieldError 关联 MessageSource 实现获取最终的错误文案
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package io.github.dunwu.spring.core.validation;

import io.github.dunwu.spring.core.validation.annotation.Valid;
import io.github.dunwu.spring.core.validation.config.CustomValidatorConfig;
import io.github.dunwu.spring.core.validation.entity.Person;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Component
public class CustomValidator implements Validator {

private final CustomValidatorConfig validatorConfig;

public CustomValidator(CustomValidatorConfig validatorConfig) {
this.validatorConfig = validatorConfig;
}

/**
* 本校验器只针对 Person 对象进行校验
*/
@Override
public boolean supports(Class<?> clazz) {
return Person.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty");

List<Field> fields = getFields(target.getClass());
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().getAnnotation(Valid.class) != null) {
try {
ValidatorRule validatorRule = validatorConfig.findRule(annotation);
if (validatorRule != null) {
validatorRule.valid(annotation, target, field, errors);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

private List<Field> getFields(Class<?> clazz) {
// 声明Field数组
List<Field> fields = new ArrayList<>();
// 如果class类型不为空
while (clazz != null) {
// 添加属性到属性数组
Collections.addAll(fields, clazz.getDeclaredFields());
clazz = clazz.getSuperclass();
}
return fields;
}

}

快速失败(Fail Fast)

Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。

1
2
3
4
5
6
7
8
9
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
// 快速失败模式
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}

Spring 校验原理

Spring 校验使用场景

  • Spring 常规校验(Validator)
  • Spring 数据绑定(DataBinder)
  • Spring Web 参数绑定(WebDataBinder)
  • Spring WebMVC/WebFlux 处理方法参数校验

Validator 接口设计

  • 接口职责
    • Spring 内部校验器接口,通过编程的方式校验目标对象
  • 核心方法
    • supports(Class):校验目标类能否校验
    • validate(Object,Errors):校验目标对象,并将校验失败的内容输出至 Errors 对象
  • 配套组件
    • 错误收集器:org.springframework.validation.Errors
    • Validator 工具类:org.springframework.validation.ValidationUtils

Errors 接口设计

  • 接口职责
    • 数据绑定和校验错误收集接口,与 Java Bean 和其属性有强关联性
  • 核心方法
    • reject 方法(重载):收集错误文案
    • rejectValue 方法(重载):收集对象字段中的错误文案
  • 配套组件
    • Java Bean 错误描述:org.springframework.validation.ObjectError
    • Java Bean 属性错误描述:org.springframework.validation.FieldError

Errors 文案来源

Errors 文案生成步骤

  • 选择 Errors 实现(如:org.springframework.validation.BeanPropertyBindingResult
  • 调用 reject 或 rejectValue 方法
  • 获取 Errors 对象中 ObjectError 或 FieldError
  • 将 ObjectError 或 FieldError 中的 code 和 args,关联 MessageSource 实现(如:ResourceBundleMessageSource

spring web 校验原理

RequestBody 参数校验实现原理

在 spring-mvc 中,RequestResponseBodyMethodProcessor 是用于解析 @RequestBody 标注的参数以及处理@ResponseBody 标注方法的返回值的。其中,执行参数校验的逻辑肯定就在解析参数的方法 resolveArgument() 中:

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
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

parameter = parameter.nestedIfOptional();
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 尝试进行参数校验
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
// 如果存在校验错误,则抛出 MethodArgumentNotValidException
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}

return adaptArgumentIfNecessary(arg, parameter);
}

可以看到,resolveArgument()调用了 validateIfApplicable()进行参数校验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 获取参数注解,如 @RequestBody、@Valid、@Validated
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 先尝试获取 @Validated 注解
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 如果标注了 @Validated,直接开始校验。
// 如果没有,那么判断参数前是否有 Valid 开头的注解。
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
// 执行校验
binder.validate(validationHints);
break;
}
}
}

以上代码,就解释了 Spring 为什么能同时支持 @Validated@Valid 两个注解。

接下来,看一下 WebDataBinder.validate() 的实现:

1
2
3
4
5
6
7
8
@Override
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(
// 此处调用 Hibernate Validator 执行真正的校验
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}

通过上面代码,可以看出 Spring 校验实际上是基于 Hibernate Validator 的封装。

方法级别的参数校验实现原理

Spring 支持根据方法去进行拦截、校验,原理就在于应用了 AOP 技术。具体来说,是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法织入增强。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
@Override
public void afterPropertiesSet() {
// 为所有 @Validated 标注的 Bean 创建切面
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
// 创建 Advisor 进行增强
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}

// 创建 Advice,本质就是一个方法拦截器
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}

接着看一下 MethodValidationInterceptor

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
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 无需增强的方法,直接跳过
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
// 获取分组信息
Class<?>[] groups = determineValidationGroups(invocation);
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
// 方法入参校验,最终还是委托给 Hibernate Validator 来校验
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
...
}
// 有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// 真正的方法调用
Object returnValue = invocation.proceed();
// 对返回值做校验,最终还是委托给Hibernate Validator来校验
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
// 有异常直接抛出
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
}

实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。

问题

Spring 有哪些校验核心组件

  • 检验器:org.springframework.validation.Validator
  • 错误收集器:org.springframework.validation.Errors
  • Java Bean 错误描述:org.springframework.validation.ObjectError
  • Java Bean 属性错误描述:org.springframework.validation.FieldError
  • Bean Validation 适配:org.springframework.validation.beanvalidation.LocalValidatorFactoryBean

参考资料