自定义SpringMVC的 GET/POST请求(简单请求)实体类参数名

背景

想必大家在使用spring的时候,使用fastjson、jackson、gson等都知道如何通过相应注解将json反序列化为对象时数据的属性绑定方式,这里我们就先不谈了,这里主要想处理当入参很多,想自定义实体类来接收,又无法通过使用@RequestParam来指定参数名的问题

@Data
public class GetEnvReq {
    private Long id;
    private String projectId;
	  ....
}

Spring MVCGET请求使用它

    @GetMapping("get_env")
    @SystemLog(description = "获取项目的环境变量值")
    public ApiResult getEnv(@Valid GetEnvReq req){
        Long projectId = req.getProjectId();
			... 
       
    }

请求http://xxx.xxx/get_env?id=111&projectId=222会得到正确显示,
请求http://xxx.xxx/get_env?_id=111&project_id=222则不会得到正确显示
这时候就需要自定义GET请求的字段名了,Spring对这样的支持可能不太完美。

PS:如果有这样的需求,使用POST JSON方式,然后使用 @JSONField(fastjson)或JsonAlias(jackson)或使用@SerializedName(gson)的方式肯定可以解决这种问题,不过这里很多时候我们不能轻易修改接口的请求方式,尤其是要兼容旧接口服务的时候。

解决方案

参考stackoverflowhttps://stackoverflow.com/questions/8986593/how-to-customize-parameter-names-when-binding-spring-mvc-command-objects/16520399

但是该解决方法不太完美,不支持类继承情况下的别名映射以及不支持多对一的字段映射。本人修改后完整代码如下:

自定义一个注解


/**
 * <ol>
 *  字段别名映射注解
 * </ol>
 *
 * @author www.alianga.com
 * @version 1.0
 * @date 2021/2/23 19:12
 * @email mpro@vip.qq.com
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParamName {
    /**
     * The name of the request parameter to bind to.
     */
    String name();

    /**
     * the alternative names of the field when it is deserialized
     * @return
     */
    String[] alternateNames() default {};
}

使用ServletRequestDataBinder给真实的Filed赋值


/**
 * <ol>
 *  参数名绑定器,
 *  将 {@link com.dtsz.yapi.common.annotation.ParamName} 中的别名映射关系添加到 MutablePropertyValues中,
 *  使Spring可以通过该关系给相应字段注入数据
 *
 * </ol>
 *
 * @author www.alianga.com
 * @version 1.0
 * @date 2021/2/24 11:30
 * @email mpro@vip.qq.com
 */
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder {

    private final Map<String, String> renameMapping;

    public ParamNameDataBinder(Object target, String objectName, Map<String, String> renameMapping) {
        super(target, objectName);
        this.renameMapping = renameMapping;
    }

    @Override
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        super.addBindValues(mpvs, request);
        renameMapping.forEach((name,realName) ->{
            if (mpvs.contains(name)) {
                mpvs.add(realName,mpvs.getPropertyValue(name).getValue());
            }
        });

    }
}

处理Field和别名的映射关系

/**
 * <ol>
 *  字段别名处理器,用来做字段多对一映射
 * </ol>
 *
 * @author www.alianga.com
 * @version 1.0
 * @date 2021/2/24 11:38
 * @email mpro@vip.qq.com
 */
public class RenamingProcessor extends ServletModelAttributeMethodProcessor {


    /**
     * Class constructor.
     *
     * @param annotationNotRequired if "true", non-simple method arguments and
     *                              return values are considered model attributes with or without a
     *                              {@code @ModelAttribute} annotation
     */
    public RenamingProcessor(boolean annotationNotRequired) {
        super(annotationNotRequired);
    }

    /**
     * A map caching annotation definitions of command objects (@ParamName-to-fieldname mappings)
     */
    private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<>();

    @Override
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
        Object target = binder.getTarget();
        Map<String, String> fieldMapping = getFieldMapping(target.getClass());
        ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), fieldMapping);

        super.bindRequestParameters(paramNameDataBinder, request);
    }

    /**
     * 获取包含映射字段的数据集合
     * @param targetClass
     * @return
     */
    private Map<String, String> getFieldMapping(Class<?> targetClass) {
        if (targetClass == Object.class) {
            return Collections.emptyMap();
        }

        if (replaceMap.containsKey(targetClass)) {
            return replaceMap.get(targetClass);
        }

        Map<String, String> renameMap = new HashMap<>();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            ParamName paramNameAnnotation = field.getAnnotation(ParamName.class);
            if (paramNameAnnotation != null) {
                // 字段名称
                String fieldName = field.getName();
                // 别名
                if(!paramNameAnnotation.name().isEmpty()){
                    renameMap.put(paramNameAnnotation.name(), fieldName);
                }
                // 备用别名
                String[] alternateNames = paramNameAnnotation.alternateNames();
                if(alternateNames != null && alternateNames.length > 0){
                    for (String alternateName : alternateNames) {
                        renameMap.put(alternateName, fieldName);
                    }
                }

            }
        }

        // 递归获取全部Field
        renameMap.putAll(getFieldMapping(targetClass.getSuperclass()));

        if (renameMap.isEmpty()) {
            renameMap = Collections.emptyMap();
        }
        replaceMap.put(targetClass, renameMap);
        return renameMap;
    }
}

配置ServletModelAttributeMethodProcessor生效

@Configuration
public class WebContextConfiguration extends WebMvcConfigurerAdapter {
   @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
		  // 添加自定义字段别名映射解析器
        resolvers.add(renamingProcessor());
    }
    
    @Bean
    public RenamingProcessor renamingProcessor(){
        return new RenamingProcessor(true);
    }
}

使用示例

@Data
public class GetEnvReq implements Serializable {


    private Long id;

    @NotNull(message = "项目id不能为空")
    @ParamName(name = "project_id",alternateNames = {"project-id"})
    private Long projectId;
}
    @GetMapping("get_env")
    @SystemLog(description = "获取项目的环境变量值")
    public ApiResult getEnv(@Valid GetEnvReq req){
        Long projectId = req.getProjectId();
			... 
       
    }

这个时候,请求参数无论是传project_idproject-id还是projectId都可以绑定到 GetEnvReqprojectId字段上

Q.E.D.


寻门而入,破门而出