前言
在SpringMVC web應(yīng)用中,對于一個rest接口,獲取請求參數(shù)我們一般使用@requestParam、@requestBody等注解 。對于表單類型的請求參數(shù),有一下幾種獲取方式
- @requestParam注解方式
- request.getParameter(String name)
- request.getInputStream()
前兩種方式其實是一種方式,@requestParam底層就是利用request.getParameter的原理。這兩種方式有一個弊端就是只能一個個獲取,而且必須知道對方傳過來的參數(shù)的key值,如果想要一次性獲取,可以使用request.getInputStream方法獲取一個inputStream對象,然后讀取流里面的數(shù)據(jù)。
//獲取到的數(shù)據(jù)格式key=value以‘&’分隔的形式
age=20&name=faderw
問題
但在實際過程中,我們會發(fā)現(xiàn)通過request.getInputStream()方式獲取的數(shù)據(jù)為空。
根據(jù)Servlet規(guī)范,如果同時滿足下列條件,則請求體(Entity)中的表單數(shù)據(jù),將被填充到request的parameter集合中(request.getParameter系列方法可以讀取相關(guān)數(shù)據(jù))
- 這是一個HTTP/HTTPS請求
- 請求方法是POST(querystring無論是否POST都將被設(shè)置到parameter中)
- 請求的類型(Content-Type頭)是application/x-www-form-urlencoded
- Servlet調(diào)用了getParameter系列方法
這里的表單數(shù)據(jù)已經(jīng)被填充到parameterMap中,不能再通過getInputStream獲取。
如何解決這個問題呢。
實現(xiàn)
在javax.servlet.http包下面有一個裝飾器類HttpServletRequestWrapper,利用這個裝飾器類,我們可以重新包裝一個HttpServletRequest對象。
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
HttpServletRequest {
定義一個裝飾器繼承HttpServletRequestWrapper,streamBody字節(jié)變量用來保存讀取的數(shù)據(jù),以便于多次讀取。
public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper{
private final byte[] streamBody;
private static final int BUFFER_SIZE = 4096;
public InputStreamHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
byte[] bytes = inputStream2Byte(request.getInputStream());
if (bytes.length == 0 && RequestMethod.POST.name().equals(request.getMethod())) {
//從ParameterMap獲取參數(shù),并保存以便多次獲取
bytes = request.getParameterMap().entrySet().stream()
.map(entry -> {
String result;
String[] value = entry.getValue();
if (value != null && value.length > 1) {
result = Arrays.stream(value).map(s -> entry.getKey() + "=" + s)
.collect(Collectors.joining("&"));
} else {
result = entry.getKey() + "=" + value[0];
}
return result;
}).collect(Collectors.joining("&")).getBytes();
}
streamBody = bytes;
}
private byte[] inputStream2Byte(InputStream inputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {
outputStream.write(bytes, 0, length);
}
return outputStream.toByteArray();
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream inputStream = new ByteArrayInputStream(streamBody);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
聲明一個帶有HttpServletRequest入?yún)⒌臉?gòu)造器,從該參數(shù)對象的流中解析數(shù)據(jù),如果沒有則繼續(xù)從parameterMap中獲取,然后以key=value&key=value形式拼接。用streamBody接收。然后我們重寫getInputStream方法,以后每次調(diào)用getInputStream方法,其實是重新利用streamBody重新new一個流,所以可以多次讀取。
有了裝飾器后,我們就要裝飾目標(biāo)對象。我們都知道SpringMVC的一次請求會被一個個過濾器層層調(diào)用,也就是我們常說的責(zé)任鏈模式。利用Filter我們就可以在某個特定的位置裝飾HttpServletRequest對象。
public class InputStreamWrapperFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
ServletRequest servletRequest = new InputStreamHttpServletRequestWrapper(httpServletRequest);
filterChain.doFilter(servletRequest, httpServletResponse);
}
}
OncePerRequestFilter這個過濾器能夠保證一次請求只經(jīng)過一次過濾器,所以我們直接繼承該類就行了。
@Bean
@Order(1)
public FilterRegistrationBean inputStreamWrapperFilterRegistration() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new InputStreamWrapperFilter());
registrationBean.setName("inputStreamWrapperFilter");
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
然后注冊該過濾器,設(shè)置優(yōu)先級為1。Spring Boot 會按照order值的大小,從小到大的順序來依次過濾。
測試
我們寫一個簡單的rest接口測試下
@PostMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object inputStreamTest(HttpServletRequest request) throws Exception {
String bs = IOUtils.toString(request.getInputStream(), "UTF-8");
Map<String, String> map = Maps.newHashMapWithExpectedSize(1);
map.put("data", bs);
return map;
}
curl命令
curl -X POST \
http://127.0.0.1:9003/home \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Postman-Token: bb6e680c-5142-4d27-b930-6efb118a505a' \
-d 'age=20&name=wangyuxin'
結(jié)果
{
"data": "age=20&name=wangyuxin"
}