目录
一、创建参数解析器
创建参数注解
参数解析器加入校验
二、将自定义的解析器加入到参数解析器中
三、测试效果
创建用于接收GET请求的参数dto
编写控制器GET请求方法
通过postman模拟GET请求
测试校验注解
四、结语
一般的接口GET请求方式,我们是怎么接收参数的呢?
@GetMapping("/get_data_demo")
public FastjsonDemoResponse getDataDemo(@RequestParam("user_name")String username, @RequestParam("age")Integer age) {
FastjsonDemoResponse response = new FastjsonDemoResponse();
response.setUserName(username);
response.setAge(age);
return response;
}
我们项目中,一般是通过@RequestParam注解获取query参数值。那么如果参数有好多个的时候,这里就需要写很多个参数。这还不是最麻烦的,如果我们还需要对每个字段进行一些校验,那么代码里面就是一大串的if判断。
那么有没有一些优雅的写法呢?答案是肯定有的。
思路是将get请求参数,即"?"后面的参数值注入到控制器的dto,就好像post请求一样。
一、创建参数解析器
因为需要将query参数注入到控制器的dto,所以需要使用到参数解析器进行处理。
创建参数注解
我们使用注解的方式,来启用对应的参数解析器。
/**
* fastjson解析注解
*
* 一般使用于非json格式请求的参数进行拦截注入
*
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FastJsonParser {
boolean require() default true;
}
参数解析器加入校验
通过HttpServletRequest.getQueryString方法,我们可以获取到请求参数,然后通过hutool工具将其转换成Map,然后再转换为JSON对象,最后将其序列化为方法参数dto。
@Component
public class FastJsonParserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//适配FastJsonParser注解
return methodParameter.hasParameterAnnotation(FastJsonParser.class);
}
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
//只适配get请求方式
if (request.getMethod().equals("GET")) {
//获取get请求参数
String queryParams = request.getQueryString();
//使用hutool工具 将query参数转换为map
Map<String, String> paramMap = HttpUtil.decodeParamMap(queryParams, StandardCharsets.UTF_8);
//转json
JSONObject paramJson = JSON.parseObject(JSON.toJSONString(paramMap));
//转换为对应的方法参数bean
Object obj = this.mapToBean(paramJson, methodParameter, nativeWebRequest, webDataBinderFactory);
return obj;
}
return null;
}
/**
* 请求参数转化为对应的bean
*
* @param paramObject
* @param methodParameter
* @param nativeWebRequest
* @param webDataBinderFactory
* @return
* @throws Exception
*/
private Object mapToBean(JSONObject paramObject, MethodParameter methodParameter, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
//将拦截转换好的JSONObject参数,转换为注解FastJsonParser上的请求参数bean
Object obj = JSON.to(methodParameter.getParameterType(), paramObject);
//存在映射字段
if (ObjectUtil.isNotEmpty(obj)) {
//获取到入参的名称
String name = methodParameter.getParameterName();
//只有存在binderFactory才会去完成自动的绑定、校验
if (webDataBinderFactory != null) {
WebDataBinder binder = webDataBinderFactory.createBinder(nativeWebRequest, obj, name);
if (binder.getTarget() != null) {
// 如果使用了validation校验, 则进行相应校验
if (methodParameter.hasParameterAnnotation(Valid.class)) {
// 如果有校验报错,会将结果放在binder.bindingResult属性中
binder.validate();
}
// 如果参数中不包含BindingResult参数,直接抛出异常
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, methodParameter)) {
throw new BindException(binder.getBindingResult());
}
}
}
}
return obj;
}
/**
* 检查参数中是否包含BindingResult参数
*
* @param binder
* @param methodParam
* @return
*/
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
int i = methodParam.getParameterIndex();
Class[] paramTypes = methodParam.getMethod().getParameterTypes();
boolean hasBindingResult = paramTypes.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]);
return !hasBindingResult;
}
}
在mapToBean方法中,实现了@Valid校验注解,这样才会对@FastJsonParser注解的参数的校验生效。
校验部分是参考了@RequestBody注解对@Valid校验注解是如何能生效的,笔者翻阅了一些资料和源码后发现,原来@RequestBody注解内部是自己去实现了@Valid校验,所以这两个注解同时加在请求参数dto上,为何就能生效,自定义的参数注解对@Valid校验就不生效。
二、将自定义的解析器加入到参数解析器中
/**
* web配置类
*/
@Configuration
public class BackendWebMvcConfig extends WebMvcConfigurationSupport {
//加入自定义的参数解析器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new FastJsonParserArgumentResolver());
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//定义一个convert转换消息的对象
FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter();
//添加fastjson的配置信息,比如是否要格式化返回的json数据;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setWriterFeatures(
//是否输出值为null的字段,默认为false
JSONWriter.Feature.WriteMapNullValue,
//将Collection类型字段的字段空值输出为[]
JSONWriter.Feature.WriteNullListAsEmpty,
//将字符串类型字段的空值输出为空字符串
JSONWriter.Feature.WriteNullStringAsEmpty
);
//在convert中添加配置信息
fastConverter.setFastJsonConfig(fastJsonConfig);
//设置支持的媒体类型
fastConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
//设置默认字符集
fastConverter.setDefaultCharset(StandardCharsets.UTF_8);
//将convert添加到converters
converters.add(0, fastConverter);
//解决返回字符串带双引号问题
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.TEXT_PLAIN));
stringHttpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(0, stringHttpMessageConverter);
super.addDefaultHttpMessageConverters(converters);
}
/**
* 添加静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.addResourceLocations("classpath:/templates/")
.addResourceLocations("classpath:/META-INF/resources/");
}
/**
* 跨域支持
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
//对哪些目录可以跨域访问
registry.addMapping("/**")
//允许哪些网站可以跨域访问
.allowedOrigins("*")
//允许哪些方法
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
}
三、测试效果
接下来,我们测试一下运行的效果。
创建用于接收GET请求的参数dto
我们使用fastjson的@JSONField注解对字段进行命名,同时加入校验注解@NotBlank对用户名字段进行判空。
@Data
public class FastjsonGetDataRequest {
@NotBlank(message = "用户名称不能为空")
@JSONField(name = "user_name")
private String userName;
@JSONField(name = "age")
private Integer age;
}
编写控制器GET请求方法
@GetMapping("/get_data")
public FastjsonDemoResponse getData(@FastJsonParser @Valid FastjsonGetDataRequest fastjsonGetDataRequest) {
FastjsonDemoResponse response = new FastjsonDemoResponse();
response.setUserName(fastjsonGetDataRequest.getUserName());
response.setAge(fastjsonGetDataRequest.getAge());
return response;
}
通过postman模拟GET请求
填写请求的参数
下图可看出请求到达了参数解析器中,并能够成功获取到query的参数值。
通过解析器后,来到控制器中的调试窗口,可以看到get方式的请求参数已经成功注入到请求的dto中。
至此,我们的参数解析器就能获取到GET方法的参数并注入到dto中。
测试校验注解
接下来,我们测试一下请求参数的user_name值为空,看看校验注解是否生效。
在参数解析器中的调试窗口看到,已校验中有问题的字段了。
从控制台抛出的异常可以看到我们自定义的异常消息,前面我们是对用户名称作为@NotBlank非空的校验。
因为我们没有做统一的异常处理,所以可以看到接口的响应是400。
如果我们要做统一的异常处理,对应校验注解的异常,我们要这样子来接收。
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result methodArgumentNotValidExceptionHandler(HttpServletRequest httpServletRequest, MethodArgumentNotValidException e) {
//打印第一条错误信息
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
String exceptionText = "MethodArgumentNotValidException-请求参数格式异常";
this.logger(httpServletRequest, exceptionText, message, e);
return ResultUtil.fail(ResultEnum.PARAMS_ERROR.getCode(), message);
}
@ResponseBody
@ExceptionHandler(BindException.class)
public Result bindExceptionHandler(HttpServletRequest httpServletRequest, BindException e) {
//打印第一条错误信息
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
String exceptionText = "BindException-请求参数格式异常";
this.logger(httpServletRequest, exceptionText, message, e);
return ResultUtil.fail(ResultEnum.PARAMS_ERROR.getCode(), message);
}
四、结语
其实细心的同学可能会发现,在参数解析器中,我们是用了一个if判断来只处理GET请求方式,那么我们是不是也能够对POST请求方式,或者是对某种content-type来独立处理呢?
答案是肯定的,但是笔者通过一系列踩坑后很负责任的告诉同学们,千万别这么做,除非真的情非得已,因为后面要做的事情就相当于要做一套spring对于参数解析器的处理,麻烦得很。