Spring多次读取HttpServletRequest
1.介绍
如何使用Spring多次从HttpServletRequest读取正文。
HttpServletRequest是一个接口,该接口公开getInputStream()方法以读取正文。 默认情况下,此InputStream中的数据只能读取一次。
2. Maven 依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
另外,由于我们使用的是application/json内容类型,因此需要jackson-databind依赖项:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
3.Spring的ContentCachingRequestWrapper
Spring提供了一个ContentCachingRequestWrapper类。 此类提供一种方法getContentAsByteArray()来多次读取正文。
但是,此类有一个局限性:无法使用getInputStream()和getReader()方法多次读取正文。
此类通过使用InputStream来缓存请求正文。 如果在其中一个过滤器中读取InputStream,则过滤器链中的其他后续过滤器将无法再读取它。 由于存在此限制,因此此类不适用于所有情况。
为了克服此限制,现在让我们看一个更通用的解决方案。
4.扩展HttpServletRequest
让创建一个新类CachedBodyHttpServletRequest,它扩展了HttpServletRequestWrapper。 这样,我们不需要重写HttpServletRequest接口的所有抽象方法。
HttpServletRequestWrapper类具有两个抽象方法getInputStream()和getReader()。 将覆盖这两个方法,并创建一个新的构造函数。
4.1构造函数
首先,创建一个构造函数。 在其中,将从实际的InputStream中读取正文并将其存储在byte []对象中:
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
}
结果,将能够多次读取正文。
4.2getInputStream()
接下来,重写getInputStream()方法。 将使用此方法读取原始主体并将其转换为对象。
在此方法中,将创建并返回CachedBodyServletInputStream类的新对象(ServletInputStream的实现):
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
4.3. getReader()
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
5.ServletInputStream的实现
创建一个类-CachedBodyServletInputStream-该类将实现ServletInputStream。 在此类中,将创建一个新的构造函数,并覆盖isFinished(),isReady()和read()方法。
5.1构造函数
在其中,将使用该字节数组创建一个新的ByteArrayInputStream实例。 之后,将其分配给全局变量cachedBodyInputStream:
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
}
5.2. read()
覆盖read()方法。 在此方法中,调用ByteArrayInputStream#read:
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
5.3. isFinished()
覆盖isFinished()方法。 此方法指示InputStream是否有更多数据要读取。 当零字节可读取时,它返回true:
@Override
public boolean isFinished() {
return cachedBody.available() == 0;
}
5.4. isReady()
覆盖isReady()方法。 此方法指示InputStream是否已准备好读取。
由于已经将InputStream复制到字节数组中,因此将返回true指示其始终可用:
@Override
public boolean isReady() {
return true;
}
6.完整代码
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.util.StreamUtils;
/**
* @author Created by niugang on 2021-03-26 14:02
*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
@Override
public BufferedReader getReader() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
}
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
/**
* @author Created by niugang on 2021-03-26 14:02
*/
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished() {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read() throws IOException {
return cachedBodyInputStream.read();
}
}
7.过滤器
最后,创建一个新的过滤器以使用CachedBodyHttpServletRequest类。扩展Spring的OncePerRequestFilter类。 此类具有抽象方法doFilterInternal()。
在此方法中,将从实际的请求对象创建CachedBodyHttpServletRequest类的对象:
然后,将这个新的请求包装对象传递给过滤器链。 因此,对getInputStream()方法的所有后续调用都将调用重写的方法:
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.example.demo.cacherequest.CachedBodyHttpServletRequest;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* @author Created by niugang on 2021-03-26 14:03
*/
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "ContentCachingFilter", urlPatterns = "/*")
public class ContentCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("IN ContentCachingFilter ");
//请求包装
CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);
}
在新建一个过滤器尝试从请求中,获取请求内容
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* @author Created by niugang on 2021-03-26 14:06
*/
@Order(Ordered.LOWEST_PRECEDENCE)
@Component
@WebFilter(filterName = "printRequestContentFilter", urlPatterns = "/*")
public class PrintRequestContentFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println("IN PrintRequestContentFilter ");
InputStream inputStream = httpServletRequest.getInputStream();
byte[] body = StreamUtils.copyToByteArray(inputStream);
System.out.println("In PrintRequestContentFilter. Request body is: " + new String(body));
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
8.测试
@Data
public class Person {
private String firstName;
private String lastName;
private int age;
}
@RestController
public class PersonController {
@PostMapping(value = "/person")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void printPerson(@RequestBody Person person) {
System.out.println("In Demo Controller. Person " + "is : " + person);
}
@GetMapping(value = "/person")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void getPerson() {
System.out.println("In Demo Controller get method.");
}
}
请求post person 输出如下内容
IN ContentCachingFilter
IN PrintRequestContentFilter
In PrintRequestContentFilter. Request body is: {
“firstName”:“xxxx”,
“lastName”:“aaaa”,
“age”:15
}
In Demo Controller. Person is : Person(firstName=xxxx, lastName=aaaa, age=15)