在 参数签名实现 中大致的屡清楚了参数签名实现逻辑,但是在实现过程中,有些细节没有注意,就会出现问题:
这里就是 在调用 request.getInputStream()的时候获取body失败了,比如 spring mvc中 在controller层调用方法需要注入 RequestBody属性的值就需要 通过调用 request.getInputStream()获取body进行注入
其实spring mvc调用底层也是使用web容器request对象调用,我项目中用到的web 容器是 jetty,现在来看下jetty的 Request 类中的getInputStream(),getReader()方法的实现
public class Request implements HttpServletRequest
{
public static final String MULTIPART_CONFIG_ELEMENT = "org.eclipse.jetty.multipartConfig";
public static final String MULTIPARTS = "org.eclipse.jetty.multiParts";
private static final Logger LOG = Log.getLogger(Request.class);
private static final Collection<Locale> __defaultLocale = Collections.singleton(Locale.getDefault());
private static final int INPUT_NONE = 0;
private static final int INPUT_STREAM = 1;
private static final int INPUT_READER = 2;
private static final MultiMap<String> NO_PARAMS = new MultiMap<>();
.............................................................................
@Override
public ServletInputStream getInputStream() throws IOException
{
if (_inputState != INPUT_NONE && _inputState != INPUT_STREAM)
throw new IllegalStateException("READER");
_inputState = INPUT_STREAM;
if (_channel.isExpecting100Continue())
_channel.continue100(_input.available());
return _input;
}
@Override
public BufferedReader getReader() throws IOException
{
if (_inputState != INPUT_NONE && _inputState != INPUT_READER)
throw new IllegalStateException("STREAMED");
if (_inputState == INPUT_READER)
return _reader;
String encoding = getCharacterEncoding();
if (encoding == null)
encoding = StringUtil.__ISO_8859_1;
if (_reader == null || !encoding.equalsIgnoreCase(_readerEncoding))
{
final ServletInputStream in = getInputStream();
_readerEncoding = encoding;
_reader = new BufferedReader(new InputStreamReader(in, encoding))
{
@Override
public void close() throws IOException
{
in.close();
}
};
}
_inputState = INPUT_READER;
return _reader;
}
...............................................................
}
其中有个重要的状态判断:_inputState
如果 if (_inputState != INPUT_NONE && _inputState != INPUT_STREAM) 为true的时候,就会报出上述 那个异常信息
_inputState != INPUT_NONE && _inputState != INPUT_STREAM 也就是 _inputState == INPUT_READER
因为一个请求只会生成一个request对象,当第一次调用的时候request.getInputStream(),request.getReader() ,其都会 _inputState = INPUT_READER
所以第二次调用的 时候,就会throw new IllegalStateException("READER");
那现在明白为什么报错了?因为出现了多次调用 request.getInputStream()/request.getReader() 的情况
前面讲到在参数签名逻辑中,我们需要通过 request.getReader() 去获取 post请求中 body的值
public static String getPostData(HttpServletRequest request) {
StringBuffer data = new StringBuffer();
String line = null;
BufferedReader reader = null;
try {
reader = request.getReader();
while (null != (line = reader.readLine()))
data.append(line);
} catch (IOException e) {
} finally {
}
System.out.println("-------request.getReader() 获取值---------"+data.toString());
return data.toString();
}
可见第一次调用可以正常获取值,第二次调用获取不到,有人会有疑问,不是第二次调用会throw new IllegalStateException("READER");抛异常么?仔细看下上面getPostData方法实现,我已经把异常给catch了
所有不会抛出异常
现在问题来了,一个服务请求过程中要是出现多次调用的情况,怎么解决呢?
可以使用HttpServletRequestWrapper对request对象进行装饰,这里使用到装饰器模式,我们不直接使用 web容器的request对象去调用getReader(),而是通过HttpServletRequestWrapper去调用
第一次调用的时候,就把 获取的 body给缓存起来,当存在多次调用的时候,通过HttpServletRequestWrapper去调用,直接获取缓存的body值,而不是再次去request.getReader()
import org.apache.commons.lang3.StringUtils;
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.io.InputStreamReader;
import java.nio.charset.Charset;
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = readBytes(request.getReader(), "utf-8");
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@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 listener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
/**
* 通过BufferedReader和字符编码集转换成byte数组
* @param br
* @param encoding
* @return
* @throws IOException
*/
private byte[] readBytes(BufferedReader br,String encoding) throws IOException{
String str = null,retStr="";
while ((str = br.readLine()) != null) {
retStr += str;
}
if (StringUtils.isNotBlank(retStr)) {
return retStr.getBytes(Charset.forName(encoding));
}
return null;
}
}
看用BodyReaderHttpServletRequestWrapper 把request对象包装了一下,两次调用都 打印出值,第二次掉用,其实就是第一次调用缓存起来的值
这里使用BodyReaderHttpServletRequestWrapper bodyReaderHttpServletRequestWrapper = new BodyReaderHttpServletRequestWrapper(httpRequest);只是局部会有效
如要一个请求全局有效的话,需要加一个过滤器,整个过程 BodyReaderHttpServletRequestWrapper 都会对 httpRequest 进行包装
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean wmhFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new WmhFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(1);
return registration;
}
private static class WmhFilter implements Filter {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if(HttpMethod.POST.name().equals(httpRequest.getMethod())){
chain.doFilter(new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request), response);
}else{
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
}