Dunwu Blog

大道至简,知易行难

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

参考资料

Spring 国际化

Spring 国际化使用场景

  • 普通国际化文案
  • Bean Validation 校验国际化文案
  • Web 站点页面渲染
  • Web MVC 错误消息提示

Spring 国际化接口

  • 核心接口:org.springframework.context.MessageSource
  • 主要概念
    • 文案模板编码(code)
    • 文案模板参数(args)
    • 区域(Locale)

层次性 MessageSource

  • Spring 层次性接口回顾
    • org.springframework.beans.factory.HierarchicalBeanFactory
    • org.springframework.context.ApplicationContext
    • org.springframework.beans.factory.config.BeanDefinition
  • Spring 层次性国际化接口
    • org.springframework.context.HierarchicalMessageSource

Java 国际化标准实现

核心接口:

  • 抽象实现 - java.util.ResourceBundle
  • Properties 资源实现 - java.util.PropertyResourceBundle
  • 例举实现 - java.util.ListResourceBundle

ResourceBundle 核心特性

  • Key-Value 设计
  • 层次性设计
  • 缓存设计
  • 字符编码控制 - java.util.ResourceBundle.Control(@since 1.6)
  • Control SPI 扩展 - java.util.spi.ResourceBundleControlProvider(@since 1.8)

Java 文本格式化

  • 核心接口
    • java.text.MessageFormat
  • 基本用法
    • 设置消息格式模式- new MessageFormat(…)
    • 格式化 - format(new Object[]{…})
  • 消息格式模式
    • 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))}
    • FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一
    • FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、
      percent
  • 高级特性
    • 重置消息格式模式
    • 重置 java.util.Locale
    • 重置 java.text.Format

MessageSource 开箱即用实现

  • 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现
  • org.springframework.context.support.ResourceBundleMessageSource
  • 可重载 Properties + MessageFormat 组合 MessageSource 实现
  • org.springframework.context.support.ReloadableResourceBundleMessageSource

MessageSource 內建依赖

  • MessageSource 內建 Bean 可能来源
  • 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean
  • 默认內建实现 - DelegatingMessageSource
  • 层次性查找 MessageSource 对象

问题

Spring Boot 为什么要新建 MessageSource Bean

  • AbstractApplicationContext 的实现决定了 MessageSource 內建实现
  • Spring Boot 通过外部化配置简化 MessageSource Bean 构建
  • Spring Boot 基于 Bean Validation 校验非常普遍

Spring 国际化接口有哪些

  • 核心接口 - MessageSource
  • 层次性接口 - org.springframework.context.HierarchicalMessageSource

Spring 有哪些 MessageSource 內建实现

  • org.springframework.context.support.ResourceBundleMessageSource
  • org.springframework.context.support.ReloadableResourceBundleMessageSource
  • org.springframework.context.support.StaticMessageSource
  • org.springframework.context.support.DelegatingMessageSource

如何实现配置自动更新 MessageSource

主要技术

  • Java NIO 2:java.nio.file.WatchService
  • Java Concurrency : java.util.concurrent.ExecutorService
  • Spring:org.springframework.context.support.AbstractMessageSource

参考资料

Spring 配置元数据

Spring 配置元信息

  • Spring Bean 配置元信息 - BeanDefinition
  • Spring Bean 属性元信息 - PropertyValues
  • Spring 容器配置元信息
  • Spring 外部化配置元信息 - PropertySource
  • Spring Profile 元信息 - @Profile

Spring Bean 配置元信息

Bean 配置元信息 - BeanDefinition

  • GenericBeanDefinition:通用型 BeanDefinition
  • RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后 BeanDefinition
  • AnnotatedBeanDefinition:注解标注的 BeanDefinition

Spring Bean 属性元信息

  • Bean 属性元信息 - PropertyValues
    • 可修改实现 - MutablePropertyValues
    • 元素成员 - PropertyValue
  • Bean 属性上下文存储 - AttributeAccessor
  • Bean 元信息元素 - BeanMetadataElement

Spring 容器配置元信息

Spring XML 配置元信息 - beans 元素相关

beans 元素属性 默认值 使用场景
profile null(留空) Spring Profiles 配置值
default-lazy-init default 当 outter beans “default-lazy-init” 属性存在时,继承该值,否则为“false”
default-merge default 当 outter beans “default-merge” 属性存在时,继承该值,否则为“false”
default-autowire default 当 outter beans “default-autowire” 属性存在时,继承该值,否则为“no”
default-autowire-candidates null(留空) 默认 Spring Beans 名称 pattern
default-init-method null(留空) 默认 Spring Beans 自定义初始化方法
default-destroy-method null(留空) 默认 Spring Beans 自定义销毁方法

Spring XML 配置元信息 - 应用上下文相关

XML 元素 使用场景
<context:annotation-config /> 激活 Spring 注解驱动
<context:component-scan /> Spring @Component 以及自定义注解扫描
<context:load-time-weaver /> 激活 Spring LoadTimeWeaver
<context:mbean-export /> 暴露 Spring Beans 作为 JMX Beans
<context:mbean-server /> 将当前平台作为 MBeanServer
<context:property-placeholder /> 加载外部化配置资源作为 Spring 属性配
<context:property-override /> 利用外部化配置资源覆盖 Spring 属

基于 XML 文件装载 Spring Bean 配置元信息

底层实现 - XmlBeanDefinitionReader

XML 元素 使用场景
<beans:beans /> 单 XML 资源下的多个 Spring Beans 配置
<beans:bean /> 单个 Spring Bean 定义(BeanDefinition)配置
<beans:alias /> 为 Spring Bean 定义(BeanDefinition)映射别名
<beans:import /> 加载外部 Spring XML 配置资源

基于 Properties 文件装载 Spring Bean 配置元信息

底层实现 - PropertiesBeanDefinitionReader

Properties 属性名 使用场景
class Bean 类全称限定名
abstract 是否为抽象的 BeanDefinition
parent 指定 parent BeanDefinition 名称
lazy-init 是否为延迟初始化
ref 引用其他 Bean 的名称
scope 设置 Bean 的 scope 属性
${n} n 表示第 n+1 个构造器参数

基于 Java 注解装载 Spring Bean 配置元信息

Spring 模式注解

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

Spring Bean 定义注解

| Spring 注解 | 场景说明 | 起始版本 |
| ———— | —————————————— | ———– | — |
| @Bean | 替换 XML 元素 <bean> | 3.0 |
| @DependsOn | 替代 XML 属性 <bean depends-on="..."/> | 3.0 |
| @Lazy | 替代 XML 属性 <bean lazy-init="true | falses" /> | 3.0 |
| @Primary | 替换 XML 元素 <bean primary="true | false" /> | 3.0 |
| @Role | 替换 XML 元素 <bean role="..." /> | 3.1 |
| @Lookup | 替代 XML 属性 <bean lookup-method="..."> | 4.1 |

Spring Bean 依赖注入注解

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

Spring Bean 条件装配注解

Spring 注解 场景说明 起始版本
@Profile 配置化条件装配 3.1
@Conditional 编程条件装配 4.0

Spring Bean 生命周期回调注解

Spring 注解 场景说明 起始版本
@PostConstruct 替换 XML 元素 <bean init-method="..." /> 或 InitializingBean 2.5
@PreDestroy 替换 XML 元素 <bean destroy-method="..." /> 或 DisposableBean 2.5

Spring BeanDefinition 解析与注册

Spring 注解 场景说明 起始版本
XML 资源 XmlBeanDefinitionReader 1.0
Properties 资源 PropertiesBeanDefinitionReader 1.0
Java 注解 AnnotatedBeanDefinitionReader 3.0

Spring Bean 配置元信息底层实现

Spring XML 资源 BeanDefinition 解析与注册

核心 API - XmlBeanDefinitionReader

  • 资源 - Resource
  • 底层 - BeanDefinitionDocumentReader
    • XML 解析 - Java DOM Level 3 API
    • BeanDefinition 解析 - BeanDefinitionParserDelegate
    • BeanDefinition 注册 - BeanDefinitionRegistry

Spring Properties 资源 BeanDefinition 解析与注册

核心 API - PropertiesBeanDefinitionReader

  • 资源
    • 字节流 - Resource
    • 字符流 - EncodedResouce
  • 底层
    • 存储 - java.util.Properties
    • BeanDefinition 解析 - API 内部实现
    • BeanDefinition 注册 - BeanDefinitionRegistry

Spring Java 注册 BeanDefinition 解析与注册

核心 API - AnnotatedBeanDefinitionReader

  • 资源
    • 类对象 - java.lang.Class
  • 底层
    • 条件评估 - ConditionEvaluator
    • Bean 范围解析 - ScopeMetadataResolver
    • BeanDefinition 解析 - 内部 API 实现
    • BeanDefinition 处理 - AnnotationConfigUtils.processCommonDefinitionAnnotations
    • BeanDefinition 注册 - BeanDefinitionRegistry

基于 XML 文件装载 Spring IoC 容器配置元信息

Spring IoC 容器相关 XML 配置

命名空间 所属模块 Schema 资源 URL
beans spring-beans https://www.springframework.org/schema/beans/spring-beans.xsd
context spring-context https://www.springframework.org/schema/context/spring-context.xsd
aop spring-aop https://www.springframework.org/schema/aop/spring-aop.xsd
tx spring-tx https://www.springframework.org/schema/tx/spring-tx.xsd
util spring-beans beans https://www.springframework.org/schema/util/spring-util.xsd
tool spring-beans https://www.springframework.org/schema/tool/spring-tool.xsd

基于 Java 注解装载 Spring IoC 容器配置元信息

Spring IoC 容器装配注解

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

Spring IoC 配属属性注解

Spring 注解 场景说明 起始版本
@PropertySource 配置属性抽象 PropertySource 注解 3.1
@PropertySources @PropertySource 集合注解 4.0

基于 Extensible XML authoring 扩展 SpringXML 元素

Spring XML 扩展

  • 编写 XML Schema 文件:定义 XML 结构
  • 自定义 NamespaceHandler 实现:命名空间绑定
  • 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析
  • 注册 XML 扩展:命名空间与 XML Schema 映射

Extensible XML authoring 扩展原理

触发时机

  • AbstractApplicationContext#obtainFreshBeanFactory
    • AbstractRefreshableApplicationContext#refreshBeanFactory
      • AbstractXmlApplicationContext#loadBeanDefinitions
          • XmlBeanDefinitionReader#doLoadBeanDefinitions
              • BeanDefinitionParserDelegate#parseCustomElement

核心流程

BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, BeanDefinition)

  • 获取 namespace
  • 通过 namespace 解析 NamespaceHandler
  • 构造 ParserContext
  • 解析元素,获取 BeanDefinintion

基于 Properties 文件装载外部化配置

注解驱动

  • @org.springframework.context.annotation.PropertySource
  • @org.springframework.context.annotation.PropertySources

API 编程

  • org.springframework.core.env.PropertySource
  • org.springframework.core.env.PropertySources

基于 YAML 文件装载外部化配置

API 编程

  • org.springframework.beans.factory.config.YamlProcessor
    • org.springframework.beans.factory.config.YamlMapFactoryBean
    • org.springframework.beans.factory.config.YamlPropertiesFactoryBean

问题

Spring 內建 XML Schema 常见有哪些

命名空间 所属模块 Schema 资源 URL
beans spring-beans https://www.springframework.org/schema/beans/spring-beans.xsd
context spring-context https://www.springframework.org/schema/context/spring-context.xsd
aop spring-aop https://www.springframework.org/schema/aop/spring-aop.xsd
tx spring-tx https://www.springframework.org/schema/tx/spring-tx.xsd
util spring-beans beans https://www.springframework.org/schema/util/spring-util.xsd
tool spring-beans https://www.springframework.org/schema/tool/spring-tool.xsd

Spring 配置元信息具体有哪些

  • Bean 配置元信息:通过媒介(如 XML、Proeprties 等),解析 BeanDefinition
  • IoC 容器配置元信息:通过媒介(如 XML、Proeprties 等),控制 IoC 容器行为,比如注解驱动、AOP 等
  • 外部化配置:通过资源抽象(如 Proeprties、YAML 等),控制 PropertySource
  • Spring Profile:通过外部化配置,提供条件分支流程

Extensible XML authoring 的缺点

  • 高复杂度:开发人员需要熟悉 XML Schema,spring.handlers,spring.schemas 以及 Spring API
  • 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套(子)元素
  • XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差
  • XML 框架移植性差:很难适配高性能和便利性的 XML 框架,如 JAXB

参考资料

Spring Bean 生命周期

Spring Bean 元信息配置阶段

BeanDefinition 配置

  • 面向资源
    • XML 配置
    • Properties 资源配置
  • 面向注解
  • 面向 API

Spring Bean 元信息解析阶段

  • 面向资源 BeanDefinition 解析
    • BeanDefinitionReader
    • XML 解析器 - BeanDefinitionParser
  • 面向注解 BeanDefinition 解析
    • AnnotatedBeanDefinitionReader

Spring Bean 注册阶段

BeanDefinition 注册接口:BeanDefinitionRegistry

Spring BeanDefinition 合并阶段

BeanDefinition 合并

父子 BeanDefinition 合并

  • 当前 BeanFactory 查找
  • 层次性 BeanFactory 查找

Spring Bean Class 加载阶段

  • ClassLoader 类加载
  • Java Security 安全控制
  • ConfigurableBeanFactory 临时 ClassLoader

Spring Bean 实例化前阶段

实例化方式

  • 传统实例化方式:实例化策略(InstantiationStrategy)
  • 构造器依赖注入

Spring Bean 实例化阶段

非主流生命周期 - Bean 实例化前阶段

InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation

Spring Bean 实例化后阶段

Bean 属性赋值(Populate)判断

InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation

Spring Bean 属性赋值前阶段

  • Bean 属性值元信息
    • PropertyValues
  • Bean 属性赋值前回调
    • Spring 1.2 - 5.0:InstantiationAwareBeanPostProcessor#postProcessPropertyValues
    • Spring 5.1:InstantiationAwareBeanPostProcessor#postProcessProperties

Spring Bean Aware 接口回调阶段

Spring Aware 接口:

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • EnvironmentAware
  • EmbeddedValueResolverAware
  • ResourceLoaderAware
  • ApplicationEventPublisherAware
  • MessageSourceAware
  • ApplicationContextAware

Spring Bean 初始化前阶段

已完成:

  • Bean 实例化

  • Bean 属性赋值

  • Bean Aware 接口回调

方法回调:

  • BeanPostProcessor#postProcessBeforeInitialization

Spring Bean 初始化阶段

Bean 初始化(Initialization)

  • @PostConstruct 标注方法
  • 实现 InitializingBean 接口的 afterPropertiesSet() 方法
  • 自定义初始化方法

Spring Bean 初始化后阶段

方法回调:BeanPostProcessor#postProcessAfterInitialization

Spring Bean 初始化完成阶段

方法回调:Spring 4.1 +:SmartInitializingSingleton#afterSingletonsInstantiated

Spring Bean 销毁前阶段

方法回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction

Spring Bean 销毁阶段

Bean 销毁(Destroy)

  • @PreDestroy 标注方法
  • 实现 DisposableBean 接口的 destroy() 方法
  • 自定义销毁方法

Spring Bean 垃圾收集

Bean 垃圾回收(GC)

  • 关闭 Spring 容器(应用上下文)
  • 执行 GC
  • Spring Bean 覆盖的 finalize() 方法被回调

问题

BeanPostProcessor 的使用场景有哪些

BeanPostProcessor 提供 Spring Bean 初始化前和初始化后的生命周期回调,分别对应 postProcessBeforeInitialization 以及 postProcessAfterInitialization 方法,允许对关心的 Bean 进行扩展,甚至是替换。

加分项:其中,ApplicationContext 相关的 Aware 回调也是基于 BeanPostProcessor 实现,即 ApplicationContextAwareProcessor。

BeanFactoryPostProcessor 与 BeanPostProcessor 的区别

BeanFactoryPostProcessor 是 Spring BeanFactory(实际为 ConfigurableListableBeanFactory) 的后置处理器,用于扩展 BeanFactory,或通过 BeanFactory 进行依赖查找和依赖注入。

BeanFactoryPostProcessor 必须有 Spring ApplicationContext 执行,BeanFactory 无法与其直接交互。

而 BeanPostProcessor 则直接与 BeanFactory 关联,属于 N 对 1 的关系。

BeanFactory 是怎样处理 Bean 生命周期

BeanFactory 的默认实现为 DefaultListableBeanFactory,其中 Bean生命周期与方法映射如下:

  • BeanDefinition 注册阶段 - registerBeanDefinition
  • BeanDefinition 合并阶段 - getMergedBeanDefinition
  • Bean 实例化前阶段 - resolveBeforeInstantiation
  • Bean 实例化阶段 - createBeanInstance
  • Bean 初始化后阶段 - populateBean
  • Bean 属性赋值前阶段 - populateBean
  • Bean 属性赋值阶段 - populateBean
  • Bean Aware 接口回调阶段 - initializeBean
  • Bean 初始化前阶段 - initializeBean
  • Bean 初始化阶段 - initializeBean
  • Bean 初始化后阶段 - initializeBean
  • Bean 初始化完成阶段 - preInstantiateSingletons
  • Bean 销毁前阶段 - destroyBean
  • Bean 销毁阶段 - destroyBean

参考资料

Spring Bean 作用域

Spring Bean 作用域

来源 说明
singleton 默认 Spring Bean 作用域,一个 BeanFactory 有且仅有一个实例
prototype 原型作用域,每次依赖查找和依赖注入生成新 Bean 对象
request 将 Spring Bean 存储在 ServletRequest 上下文中
session 将 Spring Bean 存储在 HttpSession 中
application 将 Spring Bean 存储在 ServletContext 中

“singleton” Bean 作用域

“prototype” Bean 作用域

Spring 容器没有办法管理 prototype Bean 的完整生命周期,也没有办法记录实例的存在。销毁回调方法将不会执行,可以利用 BeanPostProcessor 进行清扫工作。

“request” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “request" />
    • Java 注解 - @RequestScope@Scope(WebApplicationContext.SCOPE_REQUEST)
  • 实现
    • API - RequestScope

“session” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “session" />
    • Java 注解 - @SessionScope@Scope(WebApplicationContext.SCOPE_SESSION)
  • 实现
    • API - SessionScope

“application” Bean 作用域

  • 配置
    • XML - <bean class="..." scope = “application" />
    • Java 注解 - @ApplicationScope@Scope(WebApplicationContext.SCOPE_APPLICATION)
  • 实现
    • API - ServletContextScope

自定义 Bean 作用域

  • 实现 Scope

    • org.springframework.beans.factory.config.Scope
  • 注册 Scope

    • API - org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope
  • 配置

    1
    2
    3
    4
    5
    6
    7
    8
    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
    <property name="scopes">
    <map>
    <entry key="...">
    </entry>
    </map>
    </property>
    </bean>

问题

Spring 內建的 Bean 作用域有几种?

singleton、prototype、request、session、application 以及 websocket

singleton Bean 是否在一个应用是唯一的?

否。singleton bean 仅在当前 Spring IoC 容器(BeanFactory)中是单例对象。

application Bean 是否可以被其他方案替代?

可以的,实际上,“application” Bean 与“singleton” Bean 没有本质区别

参考资料

Spring IoC 依赖来源

依赖查找的来源

查找来源

来源 配置元数据
Spring BeanDefinition <bean id ="user" class="xxx.xxx.User">
@Bean public User user() {...}
BeanDefinitionBuilder
单例对象 API 实现

Spring 內建 BeanDefintion

Bean 名称 Bean 实例 使用场景
org.springframework.context.annotation.internalConfigurationAnnotationProcessor ConfigurationClassPostProcessor 对象 处理 Spring 配置类
org.springframework.context.annotation.internalAutowiredAnnotationProcessor AutowiredAnnotationBeanPostProcessor 对象 处理 @Autowired 以及 @Value 注解
org.springframework.context.annotation.internalCommonAnnotationProcessor CommonAnnotationBeanPostProcessor 对象 (条件激活)处理 JSR-250 注解,如 @PostConstruct 等
org.springframework.context.event.internalEventListenerProcessor EventListenerMethodProcessor 对象 处理标注 @EventListener 的 Spring 事件监听方法

Spring 內建单例对象

Bean 名称 Bean 实例 使用场景
environment Environment 对象 外部化配置以及 Profiles
systemProperties java.util.Properties 对象 Java 系统属性
systemEnvironment java.util.Map 对象 操作系统环境变量
messageSource MessageSource 对象 国际化文案
lifecycleProcessor LifecycleProcessor 对象 Lifecycle Bean 处理器
applicationEventMulticaster ApplicationEventMulticaster 对象 Spring 事件广播器

依赖注入的来源

来源 配置元数据
Spring BeanDefinition <bean id ="user" class="xxx.xxx.User">
@Bean public User user() {...}
BeanDefinitionBuilder
单例对象 API 实现
非 Spring 容器管理对象

Spring 容器管理和游离对象

来源 Spring Bean 对象 生命周期管理 配置元信息 使用场景
Spring BeanDefinition 依赖查找、依赖注入
单体对象 依赖查找、依赖注入
Resolvable Dependency 依赖注入

Spring BeanDefinition 作为依赖来源

  • 元数据:BeanDefinition
  • 注册:BeanDefinitionRegistry#registerBeanDefinition
  • 类型:延迟和非延迟
  • 顺序:Bean 生命周期顺序按照注册顺序

单例对象作为依赖来源

  • 要素
    • 来源:外部普通 Java 对象(不一定是 POJO)
    • 注册:SingletonBeanRegistry#registerSingleton
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean

非 Spring 对象容器管理对象作为依赖来源

  • 要素
    • 注册:ConfigurableListableBeanFactory#registerResolvableDependency
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean
    • 无法通过依赖查找

外部化配置作为依赖来源

  • 要素
    • 类型:非常规 Spring 对象依赖来源
  • 限制
    • 无生命周期管理
    • 无法实现延迟初始化 Bean
    • 无法通过依赖查找

问题

注入和查找的依赖来源是否相同?

否,依赖查找的来源仅限于 Spring BeanDefinition 以及单例对象,而依赖注入的来源还包括 Resolvable Dependency 以及 @Value 所标注的外部化配置

单例对象能在 IoC 容器启动后注册吗?

可以的,单例对象的注册与 BeanDefinition 不同,BeanDefinition 会被 ConfigurableListableBeanFactory#freezeConfiguration() 方法影响,从而冻结注册,单例对象则没有这个限制。

Spring 依赖注入的来源有哪些?

  • Spring BeanDefinition
  • 单例对象
  • Resolvable Dependency
  • @Value 外部化配置

参考资料

安全漏洞防护

XSS

概念

跨站脚本(Cross-site scripting,通常简称为XSS) 是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了 HTML 以及用户端脚本语言。

XSS 攻击示例:

假如有下面一个 textbox

1
<input type="text" name="address1" value="value1from" />

value1from 是来自用户的输入,如果用户不是输入 value1from,而是输入 "/><script>alert(document.cookie)</script><!- 那么就会变成:

1
2
3
4
5
<input type="text" name="address1" value="" />
<script>
alert(document.cookie)
</script>
<!- ">

嵌入的 JavaScript 代码将会被执行。攻击的威力,取决于用户输入了什么样的脚本。

攻击手段和目的

常用的 XSS 攻击手段和目的有:

  • 盗用 cookie,获取敏感信息。
  • 利用植入 Flash,通过 crossdomain 权限设置进一步获取更高权限;或者利用 Java 等得到类似的操作。
  • 利用 iframeframeXMLHttpRequest 或上述 Flash 等方式,以(被攻击)用户的身份执行一些管理动作,或执行一些一般的如发微博、加好友、发私信等操作。
  • 利用可被攻击的域受到其他域信任的特点,以受信任来源的身份请求一些平时不允许的操作,如进行不当的投票活动。
  • 在访问量极大的一些页面上的 XSS 可以攻击一些小型网站,实现 DDoS 攻击的效果。

应对手段

  • 过滤特殊字符 - 将用户所提供的内容进行过滤,从而避免 HTML 和 Jascript 代码的运行。如 > 转义为 &gt< 转义为 &lt 等,就可以防止大部分攻击。为了避免对不必要的内容错误转移,如 3<5 中的 < 需要进行文本匹配后再转移,如:<img src= 这样的上下文中的 < 才转义。
  • 设置 Cookie 为 HttpOnly - 设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。

:point_right: 参考阅读:

CSRF

概念

**跨站请求伪造(Cross-site request forgery,CSRF)**,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF。它是一种挟持用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。和跨站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

攻击手段和目的

可以如此理解 CSRF:攻击者盗用了你的身份,以你的名义发送恶意请求。

CSRF 能做的事太多:

  • 以你名义发送邮件,发消息
  • 用你的账号购买商品
  • 用你的名义完成虚拟货币转账
  • 泄露个人隐私

应对手段

  • 表单 Token - CSRF 是一个伪造用户请求的操作,所以需要构造用户请求的所有参数才可以。表单 Token 通过在请求参数中添加随机数的办法来阻止攻击者获得所有请求参数。
  • 验证码 - 请求提交时,需要用户输入验证码,以避免用户在不知情的情况下被攻击者伪造请求。
  • Referer check - HTTP 请求头的 Referer 域中记录着请求资源,可通过检查请求来源,验证其是否合法。

:point_right: 参考阅读:

SQL 注入

概念

**SQL 注入攻击(SQL injection)**,是发生于应用程序之数据层的安全漏洞。简而言之,是在输入的字符串之中注入 SQL 指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的 SQL 指令而运行,因此遭到破坏或是入侵。

攻击示例:

考虑以下简单的登录表单:

1
2
3
4
5
<form action="/login" method="POST">
<p>Username: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
<p><input type="submit" value="登陆" /></p>
</form>

我们的处理里面的 SQL 可能是这样的:

1
2
3
username:=r.Form.Get("username")
password:=r.Form.Get("password")
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"

如果用户的输入的用户名如下,密码任意

1
myuser' or 'foo' = 'foo' --

那么我们的 SQL 变成了如下所示:

1
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'

在 SQL 里面 -- 是注释标记,所以查询语句会在此中断。这就让攻击者在不知道任何合法用户名和密码的情况下成功登录了。

对于 MSSQL 还有更加危险的一种 SQL 注入,就是控制系统,下面这个可怕的例子将演示如何在某些版本的 MSSQL 数据库上执行系统命令。

1
2
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
Db.Exec(sql)

如果攻击提交 a%' exec master..xp_cmdshell 'net user test testpass /ADD' -- 作为变量 prod 的值,那么 sql 将会变成

1
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"

MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统添加新用户的命令。如果这个程序是以 sa 运行而 MSSQLSERVER 服务又有足够的权限的话,攻击者就可以获得一个系统帐号来访问主机了。

虽然以上的例子是针对某一特定的数据库系统的,但是这并不代表不能对其它数据库系统实施类似的攻击。针对这种安全漏洞,只要使用不同方法,各种数据库都有可能遭殃。

攻击手段和目的

  • 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。
  • 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。
  • 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。
  • 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。
  • 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell “net stop iisadmin”可停止服务器的 IIS 服务)。
  • 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell “FORMAT C:”)。

应对手段

  • 使用参数化查询 - 建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如使用 database/sql 里面的查询函数 PrepareQuery ,或者 Exec(query string, args ...interface{})
  • 单引号转换 - 在组合 SQL 字符串时,先针对所传入的参数进行字符替换(将单引号字符替换为连续 2 个单引号字符)。

:point_right: 参考阅读:

DoS

**拒绝服务攻击(denial-of-service attack, DoS)亦称洪水攻击**,是一种网络攻击手法,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。

当黑客使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击时,称为分布式拒绝服务攻击(distributed denial-of-service attack,缩写:DDoS attack、DDoS)。

攻击方式

  • 带宽消耗型攻击
  • 资源消耗型攻击

应对手段

  • 防火墙 - 允许或拒绝特定通讯协议,端口或 IP 地址。当攻击从少数不正常的 IP 地址发出时,可以简单的使用拒绝规则阻止一切从攻击源 IP 发出的通信。
  • 路由器、交换机 - 具有速度限制和访问控制能力。
  • 流量清洗 - 通过采用抗 DoS 软件处理,将正常流量和恶意流量区分开。

:point_right: 参考阅读: