大家好,我最近在工作中,我遇到了一个关于HTTP请求的问题,我想在这里和大家分享一下。
问题是这样的:在处理HTTP请求时,我需要在拦截器中获取请求体,用于提取其中的一个token进行登录校验。然而,当我在控制器层再次尝试获取请求体时,我发现我无法获取到数据。
这个问题的原因是HTTP请求的输入流只能被读取一次。在Java Servlet API中,我们通过ServletRequest
的getInputStream()
或getReader()
方法获取请求体。这两个方法返回的都是一次性的资源,也就是说,一旦我们读取了请求体,就无法再次读取它。这是因为这两个方法返回的输入流或读取器是从HTTP请求的输入流创建的,而HTTP请求的输入流是一个只能向前移动的流,不支持回退或重置。这就是为什么我们在拦截器中读取了请求体后,就无法在控制器层再次读取请求体的原因。
为了解决这个问题,我使用了HttpServletRequestWrapper来重置输入流。HttpServletRequestWrapper是ServletRequest的一个子类,它提供了一种方法来修改请求的属性。我创建了一个HttpServletRequestWrapper的子类,这个子类允许我在读取请求体后重置输入流。这是通过在子类的构造函数中读取请求体并将其存储在一个字节数组中实现的。然后,我重写了getInputStream()和getReader()方法,使它们返回一个新的输入流或读取器,这些输入流或读取器是从存储请求体的字节数组中创建的。这样,我就可以在后续的处理中多次读取请求体。
以下是我用于解决这个问题的代码:
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = HttpHelper.getBodyString(request).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
public boolean isFinished() {
return false;
}
public boolean isReady() {
return false;
}
public void setReadListener(ReadListener readListener) {}
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
public byte[] getBody() {
return body;
}
}
public class HttpHelper {
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
在拦截器中,我这样使用BodyReaderHttpServletRequestWrapper
:
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(request);
JSONObject requestParamterMap = HttpHelper.getRequestParamterMap((HttpServletRequest) requestWrapper);
//在拦截器中获取了做了一些业务处理,并将相关值放入Attribute
if(!CollectionUtils.isEmpty(requestParamterMap)){
requestWrapper.setAttribute(AppTokenEnum.SOMETHIN.key, requestParamterMap);
}
// 验证登陆
if (loginManager.isLogin()) {
chain.doFilter(requestWrapper, response);
} else {
// 跳转到登陆页面
toLogin((HttpServletRequest) requestWrapper, response);
}
return true;
}
// ...
}
然后,在你的控制器中,你可以这样获取requestWrapper
并从中读取请求体:
@RestController
public class MyController {
@PostMapping("/myEndpoint")
public String myEndpoint(@RequestBody String body) {
// 进行你的处理
// ...
return "success";
}
// ...
}
这个解决方案的关键在于创建了一个新的资源来存储请求体,并从这个新的资源中创建输入流或读取器。在BodyReaderHttpServletRequestWrapper的构造函数中,我首先调用了父类的构造函数,然后读取了请求体并将其存储在一个字节数组中。这个字节数组是一个新的资源,它独立于原始的HTTP请求输入流,所以我们可以多次读取它。
然后,我重写了getInputStream()和getReader()方法,使它们返回一个新的输入流或读取器。这些输入流或读取器是从存储请求体的字节数组中创建的,而不是从原始的HTTP请求输入流中创建的。这样,我们就可以在后续的处理中多次读取请求体,而不会受到HTTP请求输入流只能被读取一次的限制。
通过这种方式,我成功地解决了HTTP请求输入流只能读取一次的问题。希望这个解决方案也能帮助到遇到类似问题的你。