统一解析web请求参数新姿势

一.前言

​ hello,everyone。

​ 相信大家日常工作中或者学习过程中肯定多多少少接触过日期参数的处理。通常springboot中会接收日期会用**@DateTimeFormat或者@JsonFormat进行序列化与反序列化操作。有的开发也会使用在VO层使用string进行日期接收,然后使用序列化策略转换到LocalDateTime**这种类型。字段少的情况下几行代码也就结束了,但是如果提交一些大表单,并且表单中需要接收前端传递的日志相关参数比较多的情况,就很麻烦了。

​ 还有一种参数情况,get请求,前端传递的数组类型的参数,后端习惯使用List参数,无奈还要转一层List.就显得很呆。

​ 本文将会针对以上两个日常工作中的代码优化点提出相应的解决方案,并附上源码。如果觉得帮助到了大家,记得给个赞,么么哒~

二.时间格式化

2.1.问题描述

​ 假如现在有一个新建用户的需求,需要填写用户的出生日期。前端传递过来的值为1999-01-01,后端使用LocalDate进行数据接收与处理。在前言中说了,有两种方案,一种直接使用string类型字段接收,第二种标注**@DateTimeFormat或者@JsonFormat**注解进行接收。

​ 第二个需求,查询用户的详细信息,生日字段后端存储的为LocalDate,需要序列化成string格式给前端。也是两种方案,第一种:增加一层VO层处理日期字段,第二种:在字段上标注**@JsonFormat**进行序列化处理。

​ 咋的一看,上面的这种处理方案也没有什么问题,并且我相信很多同学在日常开发过程中也都是这么做的。但是仔细想想看会有什么问题吗?第一种单独string的形式,只要是时间日期相关的字段,我都要弄一个VO层去进行转化。第二种,加注解的形式,加入我现在时间日期相关的字段特别多,一个表单里面有10几个相关字段,那你的代码上标注的注解就会叠起来,特别多。更何况还有一些类似于**@NotEmpty**这种的校验类型注解。实体类看这就比较臃肿且不优雅。

2.2.解决思路

​ spring之所以能成为一个里程碑意义的框架,就是在AOP与IOC上的领先思想。那独立的对象统一进行管理与操作。那么我们现在的核心需求也就是对时间日期格式的数据进行统一的序列化与反序列化操作。

​ 这里就要引入一个知识点,spring默认使用jackson左右序列化框架。如果我们需要对字段值进行绑定处理是不是可以使用对于默认的序列化策略做一下修改呢?

​ 当然**@JsonFormat**其实本身就是被jackson序列化所解析的。这不是本文的重点,提一嘴,不做解析。

2.3.代码

@Configuration
public class CommonJacksonConfig {

    public static final String timeFormat = "HH:mm:ss";
    public static final String dateFormat = "yyyy-MM-dd";
    public static final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";

    /**
     * 全局时间格式化
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            builder.simpleDateFormat(dateTimeFormat);
            //日期序列化
            builder.serializers(new LocalTimeSerializer(DateTimeFormatter.ofPattern(timeFormat)));
            builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(dateFormat)));
            builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
            //日期反序列化
            builder.deserializers(new LocalTimeDeserializer(DateTimeFormatter.ofPattern(timeFormat)));
            builder.deserializers(new LocalDateDeserializer(DateTimeFormatter.ofPattern(dateFormat)));
            builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(dateTimeFormat)));
        };
    }
}
复制代码

只要在你当前的工程内增加Jackson序列化的初始化策略即可。

三.数组参数解析成List

3.1.问题描述

​ 比如我现在要进行一个用户查询的操作,支持查询多用户,那么前端可以针对userNames这个参数进行发起后端的请求。针对Get请求,参数放在请求链接里面,前端只能传递数组,后端接收也只能使用数组。但是日常处理字段解析的时候,更多使用的是List类型的。又要做一层转化,才能将对应的参数传递到ORM框架进行查询。非常不方便。

3.2.解决思路

​ 跟时间日期格式化统一处理的思路一致。前端传递过来的数组参数我解析成List进行接收。但是不能任何数组都转化成List,会影响到历史代码逻辑。

​ 这里spring已经提供了参数解析与绑定即可。

​ 参数解析类:ServletModelAttributeMethodProcessor

逐层往上看类接口关系,发现实现了HandlerMethodArgumentResolver,该结构的实现

image-20210713161215374.png

均为spring默认支持的参数解析方式,例如PathVariableMethodArgumentResolver类,将解析**@PathVariable**注解表述的字段。

​ 参数绑定类:ExtendedServletRequestDataBinder

3.3.代码

3.3.1.自定义注解

​ 被该注解锁标注的类,属性,方法参数如果前端传递的参数为数组,将被解析为list

/**
 * 数组转换List
 *
 * @author baiyan
 * @date 2021/07/13
 */
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParamName {

    /**
     * 入参名称
     *
     * @return
     */
    String value() default "";
}
复制代码

3.3.2.自定义参数绑定解析器

/**
 * @author baiyan
 * @date 2021/07/13
 */
public class RenamingDataBinder extends ExtendedServletRequestDataBinder {

    private static final Map<Class, Map<String, String>> renamingMapCache = Maps.newHashMap();

    public RenamingDataBinder(Object target) {
        super(target);
        parsingTarget(target);
    }

    public RenamingDataBinder(Object target, String objectName) {
        super(target, objectName);
        parsingTarget(target);
    }

    public RenamingDataBinder(DataBinder dataBinder) {
        super(dataBinder.getTarget(), dataBinder.getObjectName());
        parsingTarget(dataBinder.getTarget());
    }

    private void parsingTarget(Object target) {
        if (target == null || renamingMapCache.containsKey(target.getClass())) {
            return;
        }

        Field[] fields = target.getClass().getDeclaredFields();
        Map<String, String> renamingMap = new HashMap<>();
        for (Field field : fields) {
            RequestParamName requestParamName = field.getAnnotation(RequestParamName.class);
            Optional.ofNullable(requestParamName)
                    .filter(rp -> StrUtil.isNotBlank(rp.value())).ifPresent(rp -> {
                renamingMap.put(rp.value(), field.getName());
            });
        }
        renamingMapCache.put(target.getClass(), renamingMap);
    }

    @Override
    protected void doBind(MutablePropertyValues mpvs) {
        // 参数重命名
        renaming(mpvs);
        // 去除前端数组传的[]
        changeArrayParams(mpvs);
        super.doBind(mpvs);
    }

    private void changeArrayParams(MutablePropertyValues mpvs) {
        PropertyValue[] pvArray = mpvs.getPropertyValues();
        for (PropertyValue pv : pvArray) {
            String fieldName = pv.getName();
            if (!fieldName.endsWith("[]")) {
                continue;
            }
            String newFieldName = fieldName.substring(0, fieldName.length() - 2);
            if (getPropertyAccessor().isWritableProperty(newFieldName) && !mpvs.contains(newFieldName)) {
                mpvs.add(newFieldName, pv.getValue());
            }
            mpvs.removePropertyValue(pv);
        }
    }

    protected void renaming(MutablePropertyValues mpvs) {
        Map<String, String> renamingMap = Optional.ofNullable(this.getTarget())
                .map(Object::getClass)
                .map(renamingMapCache::get)
                .orElse(Maps.newHashMap());

        PropertyValue[] pvArray = mpvs.getPropertyValues();
        for (PropertyValue pv : pvArray) {
            if (renamingMap.containsKey(pv.getName())) {
                String fieldName = renamingMap.get(pv.getName());
                if (getPropertyAccessor().isWritableProperty(fieldName) && !mpvs.contains(fieldName)) {
                    mpvs.add(fieldName, pv.getValue());
                }
                mpvs.removePropertyValue(pv);
            }
        }
    }
}
复制代码

3.3.3.自定义参数解析

/**
 * @author baiyan
 * @date 2021/07/13
 */
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {

    @Autowired
    private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

    public RenamingProcessor(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return super.supportsParameter(parameter) &&
                (parameter.getParameterType().isAnnotationPresent(RequestParamName.class) ||
                        Lists.newArrayList(parameter.getParameterType().getDeclaredFields()).stream().anyMatch(field -> field.isAnnotationPresent(RequestParamName.class)));
    }

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        RenamingDataBinder renamingDataBinder = new RenamingDataBinder(binder);
        WebBindingInitializer webBindingInitializer = requestMappingHandlerAdapter.getWebBindingInitializer();
        Assert.state(webBindingInitializer != null, "WebBindingInitializer is null");
        webBindingInitializer.initBinder(renamingDataBinder);
        super.bindRequestParameters(renamingDataBinder, request);
    }
}
复制代码

3.3.4.webmvc配置

/**
 * @author baiyan
 * @date 2021/07/13
 */
@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(renamingProcessor());
    }

    @Bean
    public RenamingProcessor renamingProcessor() {
        return new RenamingProcessor(true);
    }
}
复制代码

3.4.效果展示

postman发起请求

image-20210713161816898.png

定义个查询类

@Data
@RequestParamName
public class UserQuery {
    /**
     * 用户id
     */
//    @RequestParamName("userIds[]")
    private List<Long> userIds;

    /**
     * 关键字
     */
    private String keyword;
}
复制代码

效果

image-20210713162418560.png

四.总结

​ 本文给出两种统一进行参数处理的解决方案跟思路,类似的对于参数处理有定制需求的也可以参照以上的解决思路进行处理,简化代码。

五.联系我

文中如有不正确之处,欢迎指正,写文不易,点个赞吧,么么哒~

钉钉:louyanfeng25

微信:baiyan_lou

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享