Spring cloud zuul为什么需要FormBodyWrapperFilter

源码调试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());

buildContentData方法:

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());
        }
    }

MultiValueMapform从哪里来呢,它通过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

(长按或扫码识别)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值