zoukankan      html  css  js  c++  java
  • 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




  • 相关阅读:
    CodeForcesGym 100517B Bubble Sort
    CodeForcesGym 100517H Hentium Scheduling
    BZOJ 1208: [HNOI2004]宠物收养所
    BZOJ 1503: [NOI2004]郁闷的出纳员
    BZOJ 1588: [HNOI2002]营业额统计
    sublime 3 user Settings
    sublime 3 注册码
    Why does this json4s code work in the scala repl but fail to compile?
    cat 显示指定行
    Spark Kill Application
  • 原文地址:https://www.cnblogs.com/jiangdewen/p/15573907.html
Copyright © 2011-2022 走看看