在springboot
中,可以很简单的通过@RequestBody
注解获取到请求参数并映射为实体,但是仅限于请求方式为application/json
,而@RequestParam
注解则对实体映射不友好,在一些场景(尤其是对接了古老遗留系统)需要同时支持表单以及json
的请求,我们可以实现自己的一个类似@RequstBody
的参数解析器,可以同时支持表单或json
到实体的转换。
要实现自己的参数解析器,得益于springboot
的封装,只需三部操作:
自定义注解
自己的一个注解,用于替代
@RequestBody
,比如叫@AssginParams
实现参数解析器
只需要实现
HandlerMethodArgumentResolver
接口即可,对应用了我们自定义注解的参数进行处理,将请求参数取出来并转换为实体对象,同时支持进行校验。配置参数解析器
将参数解析器添加到
spring
的解析器列表里。
需要用到的依赖如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
自定义注解
定义一个注解,替代@RequestBody
,将注解声明为可作用于方法参数上。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface AssignParams {
}
实现参数解析器
只需要实现HandlerMethodArgumentResolver
接口即可,实现具体的解析逻辑,其中接口中要实现的解析方法参数如下:
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mContainer, NativeWebRequest webRequest, WebDataBinderFactory factory) {}
解析器会把此方法返回的值赋给方法参数,methodParameter
即是要解析的参数类型对象,我们要做的也就是在此方法中将网络请求参数取出来,根据methodParameter
利用反射生成实体对象返回。
解析时存在两种情况,一是请求类型是表单请求(form-data
、x-www-form-urlencoded
),这时直接通过webRequest
即可得到,先获取到所有请求参数名:
Iterator<String> parameterNames = webRequest.getParameterNames();
获取某一参数:
webRequest.getParameter(parameterName);
迭代即可获取所有表单值,然后依据methodParameter
的反射,实例化实体对象,并将相应的字段赋值即可。
二是请求类型是json
,此时就不能通过webRequest
直接获取了,需要通过webRequest
获取到HttpServletRequest
,主动去读:
HttpServletRequest servletRequest = ((ServletWebRequest) webRequest).getRequest();
String jsonStr = servletRequest.getReader().lines().reduce("", String::concat); // 得到json字符串
然后直接将jsonStr
装换为实体对象即可。
最后判断参数是否同时应用了@Valid
或@Validate
注解,如果有,则进行一下校验。
完整实现如下:
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.validation.*;
import com.alibaba.fastjson.JSON;
import org.apache.commons.beanutils.ConvertUtils;
import org.hibernate.validator.HibernateValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* 自定义参数解析器,以便同时支持 json/x-www-form-urlencoded/form-data 到对象实体的的转换,同时进行校验
*/
@Component
public class AssignParamsResolver implements HandlerMethodArgumentResolver {
private final Logger logger = LoggerFactory.getLogger(AssignParamsResolver.class);
private final Validator validator = Validation
.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory()
.getValidator();
/**
* 判断是否是application/json方式的请求
* @param request 请求对象
* @return boolean
*/
private boolean hasJsonBody(HttpServletRequest request) {
String contentType = request.getContentType();
return contentType != null && contentType.contains("application/json");
}
// 解析实现
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mContainer, NativeWebRequest webRequest, WebDataBinderFactory factory) throws Exception {
Class<?> clazz = methodParameter.getParameterType(); // 获取到参数的class对象
HttpServletRequest servletRequest = ((ServletWebRequest) webRequest).getRequest(); // 获取到HttpServletRequest对象
Object instance = null;
if (hasJsonBody(servletRequest)) {
// 解析JSON
String jsonStr = servletRequest.getReader().lines().reduce("", String::concat);
try {
if (jsonStr.length() > 0) {
instance = JSON.parseObject(jsonStr, clazz); // 直接通过fastjson转换
}
} catch (Exception e) {
logger.warn(String.format("Parse body to object failed, body: %s ...", jsonStr));
throw new HttpServerErrorException(HttpStatus.FORBIDDEN, "Body is invalid");
}
} else {
// 解析x-www-form-urlencoded/form-data
Iterator<String> parameterNames = webRequest.getParameterNames(); // 取出所有参数名
instance = clazz.newInstance(); // 创建实体对象
while (parameterNames.hasNext()) {
String name = parameterNames.next(); // 请求参数名
String value = webRequest.getParameter(name); // 请求参数值
try {
Field field = clazz.getDeclaredField(name); // 取出实体对象中与请求参数名一致的字段
field.setAccessible(true);
// 将请求参数值赋值给实体对象对于的字段,这里用到了ConvertUtils.convert先做了装换,因为form表单中的值均是字符串类型存储的,而实体中的参数可能是int或boolean,因此先要把字符串类型的表单值转换为字段对应的类型再进行赋值
field.set(instance, ConvertUtils.convert(value, field.getType()));
} catch (NoSuchFieldException e) {
// 实体中没有与参数名称一致的字段
logger.warn(String.format("resolver parameter %s failed, no such field in dto ...", name));
}
}
}
if (methodParameter.hasParameterAnnotation(Valid.class) || methodParameter.hasParameterAnnotation(Validated.class)) {
// 如果应用了Valid或Validated注解,则进行校验抛出第一个错误
Set<ConstraintViolation<Object>> constraintValidatorSet = validator.validate(instance);
if (!constraintValidatorSet.isEmpty()) {
ConstraintViolation<Object> violation = constraintValidatorSet.iterator().next();
// 校验错误,直接抛出异常
throw new HttpServerErrorException(HttpStatus.FORBIDDEN, violation.getMessage());
}
}
return instance; // 返回值会被赋给参数
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 解析的条件,只对用了 AssignParams 自定义注解的参数执行解析逻辑
return parameter.hasParameterAnnotation(AssignParams.class);
}
}
配置参数解析器
将参数解析器添加到spring
的解析器列表里:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Autowired private AssignParamsResolver paramsResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(paramsResolver); // 添加到解析器列表
}
}
使用
至此参数解析器便实现完成,使用与@RequestBody
一致,只需定义相应的实体对象并直接应用于controller
的方法参数上即可,如下:
import lombok.Data;
import lombok.ToString;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
@Data
@ToString
public static class UserDto {
@NotNull(message = "用户id不能为空")
@Positive(message = "用户id必须为正整数")
private int userId;
@NotEmpty(message = "用户名不能为空")
private String username;
}
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("user")
public class UserController {
// 无论是json还是form表单,都能映射成实体对象
@PostMapping()
public String getUser(@Valid @AssignParams UserDto user) {
// TODO
return "ok ...";
}
}