在日常的开发中遇到了一种问题 - 没办法对Controller注入多个参数,每个Controller的RequestMap方法参数只能注入一个的RequestBody。但是又经常使用多个参数,通常我们的做法是将那几个DTO重新组合成一个新的DTO,最常见的例子就是我们在请求分页数据的时候,每次要嘛将请求参数和分页参数组合成一个新的DTO,要嘛请求参数的DTO继承分页参数的DTO,这样使我们没办法从方法上直观的看到这个请求接口的参数性质。我再网上找到了一种多参数注入的方式,是一个大神写的,他自己写了一个注解(@MultiRequestBody),应该会有人看过这篇文章,我顺便也把链接附上SpringBoot Controller 中使用多个@RequestBody的正确姿势 ,我在这边再次膜拜大神,这篇文章初期给了我很大的帮助,感谢。
人总是不满足的,因为在业务当中要加很多参数的判空处理,所以我就想着有没有什么工具可以自动帮我们做参数的校验,所以就找到了BindingRequest,配合@Valid使用十分好用。但是大神的@MultiRequestBody没办法配合@Valid、BindingRequest使用,所以就很蛋疼。抱着我一定要用上可以自动参数校验的多个RequestBody注入的想法,便开始趟雷了。
趟雷的时候发现报的异常我很熟悉,异常叫什么我忘记了,但是记得内容是request.getInputStream()里面没有值了,所以RequestBody拿不到值(异常引起的原因是request.getInputStream()里面的值只能取一次,顺便说一下request.getReader()里面的值也只能取一次,可能是request.getReader()使用了request.getInputStream())。也很幸运,因为之前遇到过这个异常,感觉会不会是第二个RequestBody从request里面去取值的时候因为request.getInputStream()的值被第一个拿走了,所以第二个RequestBody拿不到值,所以就网上找了一个工具,先将request.getInputStream()拿出来,然后再把它放到一个新的request里面,我们去使用这个新的request,不就解决了这个问题了。
现在就开始把这些骚操作串起来。
1、加入依赖
javax.validation
validation-api
2.0.1.Final
2、加入重置request里面的值的工具
附上大神的文章链接解决在Filter中读取Request中的流后, 然后在Controller中@RequestBody的参数无法注入而导致 400 错误
大神的代码中我们只需要里面将request.getInputStream()重置这部分。
(1)写好request重置的工具
import org.apache.commons.io.IOUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
/** 将request中的getinputstream取出来再放到一个新的request中
* @author Chen KaiYang
* @date 2019/8/12 9:01
*/
public class ReaderReuseHttpServletRequestWrapper extends HttpServletRequestWrapper {
//取出来的值就放在这
private final byte[] body;
public ReaderReuseHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = IOUtils.toString(request.getReader()).getBytes(Charset.forName("UTF-8"));
}
@Override
public BufferedReader getReader() throws IOException {
return super.getReader();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
(2)使用新的request顶替旧的request
import cn.fancybull.framework.config.ReaderReuseHttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 权限认证过滤器
* 执行接口访问权限控制。需与控制层的AOP进行联动
* 跨越处理
*/
@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "authorFilter")
public class AuthorFilter implements Filter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = ((HttpServletResponse) servletResponse);
String uri = request.getRequestURI().replaceFirst(request.getContextPath(), "");
ServletContext context = request.getSession().getServletContext();
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest){
requestWrapper = new ReaderReuseHttpServletRequestWrapper(request);
}
//设置跨域处理
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "0");
response.setHeader("Access-Control-Allow-Headers",
"Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token,Access-Control-Allow-Headers");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("XDomainRequestAllowed", "1");
/// 过滤器的控制实现代码
/// 过滤结果可以写入到request中,在控制层的AOP进行处理
//使用新的request顶替旧的request
filterChain.doFilter(requestWrapper, response);
}
@Override
public void destroy() {
}
}
我里面的代码可以自己增删,最重要的代码为
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest){
requestWrapper = new ReaderReuseHttpServletRequestWrapper(request);
}
//使用新的request顶替旧的request
filterChain.doFilter(requestWrapper, response);
3、操作演示
public class BaseDTO implements Serializable {
private Integer code;
private String msg;
private T result;
//.....setget自己补全
}
public class PageDTO extends BaseDTO {
@Min(value = 1,message = "请求页数不能小于1")
/**
* 第几页
*/
private int pageno;
@Min(value = 0,message = "每页请求不能小于0")
@Max(value = 100,message = "每页请求不能超过100")
/**
* 每页个数
*/
private int pagesize;
/**
* 总数
*/
private Long total;
//.....setget自己补全
}
public class SysuserDTO {
/**
* 身份证号码
*/
@NotBlank(message = "身份证号码不能为空")
private String idnumber;
/**
* 联系电话
*/
@NotBlank(message = "手机号码不能为空")
private String tel;
//。。。。。set get 自己补全
}
public class SysuserDTO2 extends PageDTO {
/**
* 身份证号码
*/
private String idnumber;
/**
* 联系电话
*/
private String tel;
//。。。。。set get 自己补全
}
(1)旧的操作
@RestController
@RequestMapping("/user")
public class SysuserController {
//这边services我就不写了
@AutoWired
UserServices userServices;
/**
* 假设是根据用户的身份证和电话号码来搜索满足条件的用户列表
*/
@PostMapping("/userList")
public BaseDTO userList(@RequestBody SysUserDTO2 dto){
//StringUntil.isEmpty()自己封装的一个判空方法
if(StringUntil.isEmpty(dto.getIdnumber())){
//ReturnUtil 自己封装的一个统一返回信息格式
//ResponseEnum.RESPONSE_CODE_300.getCode()这是返回的状态码 自己定义的
ReturnUtil.error(ResponseEnum.RESPONSE_CODE_300.getCode(),"身份证号码不能为空");
}
if(StringUntil.isEmpty(dto.getTel())){
return ReturnUtil.error(ResponseEnum.RESPONSE_CODE_300.getCode(),"手机号码不能为空");
}
//现在看起来好像不怎么繁琐 但是如果你有十几个字段需要判断呢 岂不是累死而且代码可读性极差
return userServices.userList(dto);
}
}
(2)骚操作
单个参数的我就不贴了,只贴多个参数。
@RestController
@RequestMapping("/user")
public class SysuserController {
//这边services我就不写了
@AutoWired
UserServices userServices;
/**
* 假设是根据用户的身份证和电话号码来搜索满足条件的用户列表
*/
@PostMapping("/userList")
public BaseDTO userList(@Valid @RequestBody SysUserDTO dto,
BindingResult result1, @Valid @RequestBody PageDTO pageDTO,
BindingResult result2){
if (result1.hasErrors()||result2.hasErrors()) {
String strMsg = result1.hasErrors()?result1.getFieldError().getDefaultMessage():
result2.getFieldError().getDefaultMessage();
//ReturnUtil这边我是封装了一个统一的返回信息格式
//ResponseEnum.RESPONSE_CODE_300.getCode()这是返回的状态码 自己定义的
ReturnUtil.error(ResponseEnum.RESPONSE_CODE_300.getCode(),strMsg);
}
return userServices.userList(dto,pageDTO);
}
}
(3)@MultiRequestBody 的操作
大神的操作下次有空再贴出来(其实这样说的话基本上没空),很晚了 睡觉。
后面的演示代码手打,有什么错误请谅解,然后顺便提醒我一下,Java小萌新致上。