自定义SpringMVC的 GET/POST请求(简单请求)实体类参数名
背景
想必大家在使用spring的时候,使用fastjson、jackson、gson等都知道如何通过相应注解将json反序列化为对象时数据的属性绑定方式,这里我们就先不谈了,这里主要想处理当入参很多,想自定义实体类来接收,又无法通过使用
@RequestParam
来指定参数名的问题
@Data
public class GetEnvReq {
private Long id;
private String projectId;
....
}
在Spring MVC
的GET
请求使用它
@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)的方式肯定可以解决这种问题,不过这里很多时候我们不能轻易修改接口的请求方式,尤其是要兼容旧接口服务的时候。
解决方案
参考stackoverflow: https://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_id
、project-id
还是projectId
都可以绑定到 GetEnvReq
的projectId
字段上
Q.E.D.