Spring 笔记——核心-数据规则篇
前言
官网地址: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区
很晚了,睡觉,有空补上
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律