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区
很晚了,睡觉,有空补上

自定义参数校验注解实现,涉及两个注解相关配置类,全局异常处理类,请求实体类,请求 Controller 类

源码地址:https://gitee.com/J-dw/springboot-study.git




posted @ 2021-11-22 12:07  临渊不羡渔  阅读(149)  评论(0编辑  收藏  举报