前言
官网地址:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation
本篇的内容,spring官方说明是数据校验,绑定,类型转换。
将验证视为业务逻辑有利有弊,Spring 提供了一种验证(和数据绑定)设计,不排除其中任何一个。具体来说,验证不应绑定到 Web 层,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题,Spring 提供了一个Validator既基本又非常适用于应用程序每一层的契约。
个人浅薄经验:数据校验功能是对我们的API调用进行数据校验,防止非法参数;数据绑定功能是各个bean的属性注入,经常看到的场景就是配置文件;
数据绑定与校验
DataBinder 数据绑定器
Validator 校验器
BeanWrapper bean包装
ValidationUtils 数据校验工具类
数据类型转换
Converter 转换器定义,是个函数式接口,定义了一个转换动作 api
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
ConverterFactory 转换器工厂类定义,案例是 StringToEnumConverterFactory
package org.springframework.core.convert.support;
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnumConverter(targetType);
}
private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {
private Class<T> enumType;
public StringToEnumConverter(Class<T> enumType) {
this.enumType = enumType;
}
public T convert(String source) {
return (T) Enum.valueOf(this.enumType, source.trim());
}
}
}
GenericConverter 通用转换器定义,相比 Converter
他更方便写支持多种类型转换的转换器
package org.springframework.core.convert.converter;
public interface GenericConverter {
public Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
ConversionService 转换服务定义
package org.springframework.core.convert;
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
<T> T convert(Object source, Class<T> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
关于集合类型的处理,我们怎么告诉转换器集合内元素的数据类型。直接通过 class 对象是肯定不行的。所以这里 spring
设计了个 TypeDescriptor
来辅助。
DefaultConversionService cs = new DefaultConversionService();
List<Integer> input = ...
cs.convert(input,
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
使用方法无非就是 xml配置声明 与代码编程注入。没什么好说的
这里类型转换值得一提的是 spring
在这个功能上的设计思路,我们写自己的业务代码的时候可以借鉴。设计好了的话,具体的这几个顶层接口,看一样就明白有哪些方法是干什么的。
String与对象的互相转换
我们最常用的json传输,我们发送的是一个遵循json规范的字符串,而这个字符串在spring中是怎么转换成我们具体使用的对象的?
Printer 定义一个对象转字符串 api
Parser 定义一个字符串转对象 api
Formatter 继承这 Printer 与 Parser
// Local参数传入的是当前地区
// Printer
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
// Parser
import java.text.ParseException;
public interface Parser<T> {
T parse(String clientValue, Locale locale) throws ParseException;
}
// Formatter
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
DateFormatter 案例
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
private String pattern;
public DateFormatter(String pattern) {
this.pattern = pattern;
}
public String print(Date date, Locale locale) {
if (date == null) {
return "";
}
return getDateFormat(locale).format(date);
}
public Date parse(String formatted, Locale locale) throws ParseException {
if (formatted.length() == 0) {
return null;
}
return getDateFormat(locale).parse(formatted);
}
protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
dateFormat.setLenient(false);
return dateFormat;
}
}
通过注释的方式转换数据
spring 本身实现了一套处理 基于注释的方式声明格式转换器 的流程,我们只需按照这个流程提供的接入口接入即可使用。本来想着 Spring 自身的 @NumberFormat
注解已经够用了,然后整了半天一直失效,最后无奈,专门重写序列号与反序列化类提供序列化与反序列化处理。主要是用到 @JsonSerialize
与 @JsonDeserialize
注解,只要配置了这两个注解,Spring 序列化前端传过来的 json 的时候就会调用相应的配置类处理
效果图:
MoneySerializer
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class MoneySerializer extends JsonSerializer<BigDecimal> {
@Override
public void serialize(BigDecimal o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString("¥ "+ o.setScale(2, RoundingMode.HALF_UP));
}
}
MoneyDeSerializer
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.io.BigDecimalParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers;
import java.io.IOException;
import java.math.BigDecimal;
public class MoneyDeSerializer extends NumberDeserializers.BigDecimalDeserializer {
@Override
public BigDecimal deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
String moneyStr = deserializationContext.readValue(jsonParser, String.class);
moneyStr = moneyStr.trim().replace("¥ ", "");
return BigDecimalParser.parse(moneyStr);
}
}
配置全局日期与字符串转换
以 Converter 为入口研究了半天,没弄出来,还是直接使用过去的以 Jackson2 为入口的配置吧
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* @author ListJiang
* @class LocalDateTime序列化配置
* @remark 用于解决json转换时的格式问题
* @date 2022/01/03
*/
@Configuration
public class LocalDateTimeSerializerConfig {
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DATE_PATTERN = "yyyy-MM-dd";
private static final String TIME_PATTERN = "HH:mm:ss";
/**
* 统一配置 LocalDate、LocalDateTime、LocalTime 与 String 之间的互相转换
* <p>
* 最终效果:
* {
* "localDate": "2022-01-03",
* "localDateTime": "2022-01-03 18:36:53",
* "localTime": "18:36:53",
* "date": "2022-01-03 18:36:53",
* "calendar": "2022-01-03 18:36:53"
* }
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
JavaTimeModule module = new JavaTimeModule();
module.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
module.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
return builder -> {
builder.simpleDateFormat(DATE_TIME_PATTERN);
builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_PATTERN)));
builder.modules(module);
};
}
}
bean校验
应该叫API(内部的与外部的)交互时的数据校验。系统内部的验证其实意义不大(比如 controller 调用 service ,serviceA 调用 serviceB),大部分的时候我们调用之前就会处理好。主要需要处理的是前端调用的校验与外部系统调用API的数据校验。
而 Spring 基本上把通用场景都考虑实现了,只需要使用即可。在 javax.validation.constraints
包下面,各个注解的含义基本上看一眼就能理解,实在不理解,点进去看下注释就行。使用的话,实体属性上加上注解,实体传参的时候标注 @Valid
或者 @Validated
就行,@Valid
是 spring-boot-starter-validation 引入的 jakarta.validation-api-2.0.2.jar 里面的,@Validated
是 spring-context-5.3.14.jar 里面的。随便确定一个,项目整体保持一致就行。
此处主要说下自定义的数据校验。比如有个需求叫校验前端传入的地址全称必须是"xxx省xxx市xxx区",这个 省、市不定,即必须可以通过不同的配置校验一下案例
xxx省xxx市xxx区
xxx省xxx市xxx县
xxx省xxx市
市xxx区
很晚了,睡觉,有空补上