前言
在SpringMVC web應(yīng)用中爸吮,對(duì)于一個(gè)rest接口所宰,獲取請(qǐng)求參數(shù)我們一般使用@requestParam
、@requestBody
等注解 够傍。對(duì)于表單類型的請(qǐng)求參數(shù)甫菠,有一下幾種獲取方式
- @requestParam注解方式
- request.getParameter(String name)
- request.getInputStream()
前兩種方式其實(shí)是一種方式,@requestParam底層就是利用request.getParameter的原理冕屯。這兩種方式有一個(gè)弊端就是只能一個(gè)個(gè)獲取寂诱,而且必須知道對(duì)方傳過(guò)來(lái)的參數(shù)的key值,如果想要一次性獲取安聘,可以使用request.getInputStream方法獲取一個(gè)inputStream對(duì)象痰洒,然后讀取流里面的數(shù)據(jù)。
//獲取到的數(shù)據(jù)格式key=value以‘&’分隔的形式
age=20&name=faderw
問(wèn)題
但在實(shí)際過(guò)程中浴韭,我們會(huì)發(fā)現(xiàn)通過(guò)request.getInputStream()方式獲取的數(shù)據(jù)為空丘喻。
根據(jù)Servlet規(guī)范,如果同時(shí)滿足下列條件念颈,則請(qǐng)求體(Entity)中的表單數(shù)據(jù)泉粉,將被填充到request的parameter集合中(request.getParameter系列方法可以讀取相關(guān)數(shù)據(jù))
- 這是一個(gè)HTTP/HTTPS請(qǐng)求
- 請(qǐng)求方法是POST(querystring無(wú)論是否POST都將被設(shè)置到parameter中)
- 請(qǐng)求的類型(Content-Type頭)是application/x-www-form-urlencoded
- Servlet調(diào)用了getParameter系列方法
這里的表單數(shù)據(jù)已經(jīng)被填充到parameterMap中,不能再通過(guò)getInputStream獲取。
如何解決這個(gè)問(wèn)題呢嗡靡。
實(shí)現(xiàn)
在javax.servlet.http包下面有一個(gè)裝飾器類HttpServletRequestWrapper
跺撼,利用這個(gè)裝飾器類,我們可以重新包裝一個(gè)HttpServletRequest對(duì)象讨彼。
public class HttpServletRequestWrapper extends ServletRequestWrapper implements
HttpServletRequest {
定義一個(gè)裝飾器繼承HttpServletRequestWrapper
,streamBody
字節(jié)變量用來(lái)保存讀取的數(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()));
}
}
聲明一個(gè)帶有HttpServletRequest入?yún)⒌臉?gòu)造器哩至,從該參數(shù)對(duì)象的流中解析數(shù)據(jù),如果沒(méi)有則繼續(xù)從parameterMap中獲取黑滴,然后以key=value&key=value形式拼接憨募。用streamBody接收紧索。然后我們重寫getInputStream方法袁辈,以后每次調(diào)用getInputStream方法,其實(shí)是重新利用streamBody重新new一個(gè)流珠漂,所以可以多次讀取晚缩。
有了裝飾器后,我們就要裝飾目標(biāo)對(duì)象媳危。我們都知道SpringMVC的一次請(qǐng)求會(huì)被一個(gè)個(gè)過(guò)濾器層層調(diào)用荞彼,也就是我們常說(shuō)的責(zé)任鏈模式。利用Filter
我們就可以在某個(gè)特定的位置裝飾HttpServletRequest對(duì)象待笑。
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
這個(gè)過(guò)濾器能夠保證一次請(qǐng)求只經(jīng)過(guò)一次過(guò)濾器鸣皂,所以我們直接繼承該類就行了。
@Bean
@Order(1)
public FilterRegistrationBean inputStreamWrapperFilterRegistration() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new InputStreamWrapperFilter());
registrationBean.setName("inputStreamWrapperFilter");
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
然后注冊(cè)該過(guò)濾器暮蹂,設(shè)置優(yōu)先級(jí)為1寞缝。Spring Boot 會(huì)按照order值的大小,從小到大的順序來(lái)依次過(guò)濾仰泻。
測(cè)試
我們寫一個(gè)簡(jiǎn)單的rest接口測(cè)試下
@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"
}