Spring cloud zuul为什么要有FormBodyWr
源码调试web容器:tomcat
Spring cloud zuul里面有一些核心过滤器,以前文章大致介绍了下各个过滤器的作用,
武三通的zuul之惑
这次重点讲解下FormBodyWrapperFilter,先贴出完整源码:
/**
* Pre {@link ZuulFilter} that parses form data and reencodes it for downstream services
*
* @author Dave Syer
*/
public class FormBodyWrapperFilter extends ZuulFilter {
private FormHttpMessageConverter formHttpMessageConverter;
private Field requestField;
private Field servletRequestField;
public FormBodyWrapperFilter() {
this(new AllEncompassingFormHttpMessageConverter());
}
public FormBodyWrapperFilter(FormHttpMessageConverter formHttpMessageConverter) {
this.formHttpMessageConverter = formHttpMessageConverter;
this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class,
"req", HttpServletRequest.class);
this.servletRequestField = ReflectionUtils.findField(ServletRequestWrapper.class,
"request", ServletRequest.class);
Assert.notNull(this.requestField,
"HttpServletRequestWrapper.req field not found");
Assert.notNull(this.servletRequestField,
"ServletRequestWrapper.request field not found");
this.requestField.setAccessible(true);
this.servletRequestField.setAccessible(true);
}
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return FORM_BODY_WRAPPER_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String contentType = request.getContentType();
// Don't use this filter on GET method
if (contentType == null) {
return false;
}
try {
MediaType mediaType = MediaType.valueOf(contentType);
return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
|| (isDispatcherServletRequest(request)
&& MediaType.MULTIPART_FORM_DATA.includes(mediaType));
}
catch (InvalidMediaTypeException ex) {
return false;
}
}
private boolean isDispatcherServletRequest(HttpServletRequest request) {
return request.getAttribute(
DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
FormBodyRequestWrapper wrapper = null;
if (request instanceof HttpServletRequestWrapper) {
HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
.getField(this.requestField, request);
wrapper = new FormBodyRequestWrapper(wrapped);
ReflectionUtils.setField(this.requestField, request, wrapper);
if (request instanceof ServletRequestWrapper) {
ReflectionUtils.setField(this.servletRequestField, request, wrapper);
}
}
else {
wrapper = new FormBodyRequestWrapper(request);
ctx.setRequest(wrapper);
}
if (wrapper != null) {
ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
}
return null;
}
private class FormBodyRequestWrapper extends Servlet30RequestWrapper {
private HttpServletRequest request;
private byte[] contentData;
private MediaType contentType;
private int contentLength;
public FormBodyRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
}
@Override
public String getContentType() {
if (this.contentData == null) {
buildContentData();
}
return this.contentType.toString();
}
@Override
public int getContentLength() {
if (super.getContentLength() <= 0) {
return super.getContentLength();
}
if (this.contentData == null) {
buildContentData();
}
return this.contentLength;
}
public long getContentLengthLong() {
return getContentLength();
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.contentData == null) {
buildContentData();
}
return new ServletInputStreamWrapper(this.contentData);
}
private synchronized void buildContentData() {
try {
MultiValueMap<String, Object> builder = RequestContentDataExtractor.extract(this.request);
FormHttpOutputMessage data = new FormHttpOutputMessage();
this.contentType = MediaType.valueOf(this.request.getContentType());
data.getHeaders().setContentType(this.contentType);
FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder, this.contentType, data);
// copy new content type including multipart boundary
this.contentType = data.getHeaders().getContentType();
this.contentData = data.getInput();
this.contentLength = this.contentData.length;
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert form data", e);
}
}
private class FormHttpOutputMessage implements HttpOutputMessage {
private HttpHeaders headers = new HttpHeaders();
private ByteArrayOutputStream output = new ByteArrayOutputStream();
@Override
public HttpHeaders getHeaders() {
return this.headers;
}
@Override
public OutputStream getBody() throws IOException {
return this.output;
}
public byte[] getInput() throws IOException {
this.output.flush();
return this.output.toByteArray();
}
}
}
}
正如前面注释中说的,FormBodyWrapperFilter主要是解析表单数据并重新编码,供后续服务使用。
filterType:
pre,可以在请求被路由之前调用
filterOrder:
为-1,越小优先级越高,它是在ServletDetectionFilter和Servlet30WrapperFilter之后执行的。
shouldFilter:
该过滤器仅对两种类请求生效,第一类是Content-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求。为什么是这两类呢,如果研究前面的请求流程,我们会发现,这两类请求在前面的流程中已经被读取处理了,流是不可重复读取的,这意味着zuul在转发这个Request的时候,已经丢失了原本的内容,因此需要把放回去。这也是开发spring cloud gateway的原因之一,因为它没有这些问题(尚未看源码验证,有相关经验者欢迎留言)。以下我们仅以Content-Type=application/x-www-form-urlencoded的请求为例进行讲解。通过断点调试,发现在请求达到ZuulServlet前,具体执行request流读取操作的是
org.apache.catalina.connector.RequestFacade类,这是一个包装类,下面是与之关联的关键代码片段:
/**
* Parse request parameters.
*/
protected void parseParameters() {
parametersParsed = true;
Parameters parameters = coyoteRequest.getParameters();
boolean success = false;
try {
// Set this every time in case limit has been changed via JMX
parameters.setLimit(getConnector().getMaxParameterCount());
// getCharacterEncoding() may have been overridden to search for
// hidden form field containing request encoding
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If !useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoyeAdapter.service()
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
success = true;
return;
}
String contentType = getContentType();
if (contentType == null) {
contentType = "";
}
int semicolon = contentType.indexOf(';');
if (semicolon >= 0) {
contentType = contentType.substring(0, semicolon).trim();
} else {
contentType = contentType.trim();
}
if ("multipart/form-data".equals(contentType)) {
parseParts(false);
success = true;
return;
}
if( !getConnector().isParseBodyMethod(getMethod()) ) {
success = true;
return;
}
if (!("application/x-www-form-urlencoded".equals(contentType))) {
success = true;
return;
}
int len = getContentLength();
if (len > 0) {
int maxPostSize = connector.getMaxPostSize();
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}
byte[] formData = null;
if (len < CACHED_POST_LEN) {
if (postData == null) {
postData = new byte[CACHED_POST_LEN];
}
formData = postData;
} else {
formData = new byte[len];
}
try {
if (readPostBody(formData, len) != len) {
parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);
return;
}
} catch (IOException e) {
// Client disconnect
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
} else if ("chunked".equalsIgnoreCase(
coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
e);
}
return;
}
if (formData != null) {
parameters.processParameters(formData, 0, formData.length);
}
}
success = true;
} finally {
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}
}
}
/**
* Read post body in an array.
*
* @param body The bytes array in which the body will be read
* @param len The body length
* @return the bytes count that has been read
* @throws IOException if an IO exception occurred
*/
protected int readPostBody(byte[] body, int len)
throws IOException {
int offset = 0;
do {
int inputLen = getStream().read(body, offset, len - offset);
if (inputLen <= 0) {
return offset;
}
offset += inputLen;
} while ((len - offset) > 0);
return len;
}
上面仅对application/x-www-form-urlencodedreadPostBody的情况执行readPostBody,请求体中的参数已经被读取解析为map,流已经不可重复读取,因此在转发之前,FormBodyWrapperFilter需要把map中的参数放回请求体中。
run方法
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
FormBodyRequestWrapper wrapper = null;
if (request instanceof HttpServletRequestWrapper) {
HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
.getField(this.requestField, request);
wrapper = new FormBodyRequestWrapper(wrapped);
ReflectionUtils.setField(this.requestField, request, wrapper);
if (request instanceof ServletRequestWrapper) {
ReflectionUtils.setField(this.servletRequestField, request, wrapper);
}
}
else {
wrapper = new FormBodyRequestWrapper(request);
ctx.setRequest(wrapper);
}
if (wrapper != null) {
ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
}
return null;
}
最后执行时,wrapper.getContentType()内部执行buildContentData方法,将参数放回请求体中。
ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
private synchronized void buildContentData() {
try {
MultiValueMap<String, Object> builder = RequestContentDataExtractor.extract(this.request);
FormHttpOutputMessage data = new FormHttpOutputMessage();
this.contentType = MediaType.valueOf(this.request.getContentType());
data.getHeaders().setContentType(this.contentType);
FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder, this.contentType, data);
// copy new content type including multipart boundary
this.contentType = data.getHeaders().getContentType();
this.contentData = data.getInput();
this.contentLength = this.contentData.length;
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert form data", e);
}
}
formHttpMessageConverter的write如下:
@Override
@SuppressWarnings("unchecked")
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (!isMultipart(map, contentType)) {
writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
}
else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
}
}
writeForm如下所示
private void writeForm(MultiValueMap<String, String> form, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
Charset charset;
if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType);
charset = (contentType.getCharset() != null ? contentType.getCharset() : this.charset);
}
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
charset = this.charset;
}
StringBuilder builder = new StringBuilder();
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
String name = nameIterator.next();
for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
String value = valueIterator.next();
builder.append(URLEncoder.encode(name, charset.name()));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(value, charset.name()));
if (valueIterator.hasNext()) {
builder.append('&');
}
}
}
if (nameIterator.hasNext()) {
builder.append('&');
}
}
final byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
StreamUtils.copy(bytes, outputStream);
}
});
}
else {
StreamUtils.copy(bytes, outputMessage.getBody());
}
}
MultiValueMap<String, String> form从哪里来呢,它通过extractFromRequest方法获取,参数request即是前面的RequestFacade。方法将获取Query Params和从请求体中解析出来的的参数。
private static MultiValueMap<String, Object> extractFromRequest(HttpServletRequest request) throws IOException {
MultiValueMap<String, Object> builder = new LinkedMultiValueMap<>();
Set<String> queryParams = findQueryParams(request);
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
String key = entry.getKey();
if (!queryParams.contains(key)) {
for (String value : entry.getValue()) {
builder.add(key, value);
}
}
}
return builder;
}
那spring zuul 怎么保证后续zuul filter处理参数的时候,输入流和参数可以重复读取呢,答案是它重新构造了一个FormBodyRequestWrapper,并把它作为实例变量req放入包装类com.netflix.zuul.http.HttpServletRequestWrapper中,HttpServletRequestWrapper实现了装饰器模式,除了读取request内容(参数、流或reader),其他方法默认通过被包装的request对象调用。这个类提供了缓冲的内容供读取,允许getReader()、getInputStream()和getParameterXXX这些方法安全、重复地被调用,并且返回的结果相同。
以下是HttpServletRequestWrapper的几个典型方法,其中成员变量req即是传入的FormBodyRequestWrapper。
/**
* This method is safe to execute multiple times.
*
* @see javax.servlet.ServletRequest#getParameter(java.lang.String)
*/
@Override
public String getParameter(String name) {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
if (parameters == null) return null;
String[] values = parameters.get(name);
if (values == null || values.length == 0)
return null;
return values[0];
}
/**
* This method is safe.
*
* @see {@link #getParameters()}
* @see javax.servlet.ServletRequest#getParameterMap()
*/
@SuppressWarnings("unchecked")
@Override
public Map getParameterMap() {
try {
parseRequest();
} catch (IOException e) {
throw new IllegalStateException("Cannot parse the request!", e);
}
return getParameters();
}
parseRequest处理如下:
private void parseRequest() throws IOException {
if (parameters != null)
return; //already parsed
HashMap<String, List<String>> mapA = new HashMap<String, List<String>>();
List<String> list;
Map<String, List<String>> query = HTTPRequestUtils.getInstance().getQueryParams();
if (query != null) {
for (String key : query.keySet()) {
list = query.get(key);
mapA.put(key, list);
}
}
if (shouldBufferBody()) {
// Read the request body inputstream into a byte array.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// Copy all bytes from inputstream to byte array, and record time taken.
long bufferStartTime = System.nanoTime();
//这里的req是FormBodyRequestWrapper,inputstream可以重复被读取。
IOUtils.copy(req.getInputStream(), baos);
bodyBufferingTimeNs = System.nanoTime() - bufferStartTime;
contentData = baos.toByteArray();
} catch (SocketTimeoutException e) {
// This can happen if the request body is smaller than the size specified in the
// Content-Length header, and using tomcat APR connector.
LOG.error("SocketTimeoutException reading request body from inputstream. error=" + String.valueOf(e.getMessage()));
if (contentData == null) {
contentData = new byte[0];
}
}
try {
LOG.debug("Length of contentData byte array = " + contentData.length);
if (req.getContentLength() != contentData.length) {
LOG.warn("Content-length different from byte array length! cl=" + req.getContentLength() + ", array=" + contentData.length);
}
} catch(Exception e) {
LOG.error("Error checking if request body gzipped!", e);
}
final boolean isPost = req.getMethod().equals("POST");
String contentType = req.getContentType();
final boolean isFormBody = contentType != null && contentType.contains("application/x-www-form-urlencoded");
// only does magic body param parsing for POST form bodies
if (isPost && isFormBody) {
String enc = req.getCharacterEncoding();
if (enc == null) enc = "UTF-8";
String s = new String(contentData, enc), name, value;
StringTokenizer st = new StringTokenizer(s, "&");
int i;
boolean decode = req.getContentType() != null;
while (st.hasMoreTokens()) {
s = st.nextToken();
i = s.indexOf("=");
if (i > 0 && s.length() > i + 1) {
name = s.substring(0, i);
value = s.substring(i + 1);
if (decode) {
try {
name = URLDecoder.decode(name, "UTF-8");
} catch (Exception e) {
}
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (Exception e) {
}
}
list = mapA.get(name);
if (list == null) {
list = new LinkedList<String>();
mapA.put(name, list);
}
list.add(value);
}
}
}
}
HashMap<String, String[]> map = new HashMap<String, String[]>(mapA.size() * 2);
for (String key : mapA.keySet()) {
list = mapA.get(key);
map.put(key, list.toArray(new String[list.size()]));
}
parameters = map;
}
parseRequest中的req.getInputStream()可以被重复读取。FormBodyRequestWrapper典型方法如下:
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.contentData == null) {
buildContentData();
}
return new ServletInputStreamWrapper(this.contentData);
}
buildContentData实现代码前面已经贴出,可以知道,contentData是一个byte数组,在buildContentData时已经被赋值,可以重复使用。
this.contentData = data.getInput();
在后续route filter转发过程中,当需要获取流数据重新构造请求时,以上的过程就派上用场,比如SimpleHostRoutingFilter中的代码片段。
private InputStream getRequestBody(HttpServletRequest request) {
InputStream requestEntity = null;
try {
requestEntity = request.getInputStream();
}
catch (IOException ex) {
// no requestBody is ok.
}
return requestEntity;
}
总结及后续:
request 流是不可重复读取的,在请求达到ZuulServlet之前,也许Content-Type = application/x-www-form- urlencoding类型的请求,其请求体已经被解析并将参数缓存了,因此转发的时候其请求体中的内容已经丢失,需要重新放回去。放回去后又需要在后续zuul filter中能够重复读取使用。FormBodyWrapperFilter就是出于这个目标诞生的。而当Content-Type = multipart/form-data时,目的也类似,因为如果请求先达到DispatcherServlet,其中的数据也是要被解析处理的,有兴趣可以阅读DispatcherServlet 相关源码:
/**
* Convert the request into a multipart request, and make multipart resolver available.
* <p>If no multipart resolver is set, simply use the existing request.
* @param request current HTTP request
* @return the processed request (multipart wrapper if necessary)
* @see MultipartResolver#resolveMultipart
*/
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
else if (hasMultipartException(request) ) {
logger.debug("Multipart resolution failed for current request before - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
以上是个人的一些发现,如有不当,欢迎交流指正。
java达人
ID:drjava
(长按或扫码识别)
image