由MissingServletRequestParameterException产生原因的分析

一种MissingServletRequestParameterException发生的业务场景

问题描述

最近小弟封装了一个新的方法,大概是把请求访问controller层的信息,包括正常的响应和异常信息,通过http请求的方式发送给另外一个接口,之前是通过发送消息给kafka来完成的,改造过程中他除了把请求方式改变了,还把同步的方式改成了启用线程池的异步调用。

public void sendData(HttpServletRequest request, ResponseMessage response) {
        executorService.submit(() -> {
                Map<String, Object> map = new HashMap<>();
                map.put("request", JSON.toJSONString(request.getParameterMap()));
                ... ...
        });
}

这样修改之后,问题随之产生了:
在这里插入图片描述

由于这个方法调用频率还是比较高的,类似这样的错误日志每隔几分钟就会出现,于是决定好好分析一下错误原因。

产生原因分析

仔细分析日志,发现请求进来的时候,先进入了项目中的拦截器,而拦截器中是可以拿到参数的,那就很奇怪了,为什么在拦截器里能拿到的参数,访问到controller层的时候却因为参数上有@RequestParam注解而导致报了miss parameter的异常?

分析发现,最近一次上线之后才有了以上的错误日志,于是分析最近提交的代码,怀疑是这个异步读取request的parameter的问题,于是开始追根溯源查源码。
首先查到了Request的源码,因为request是直接调用getParameter的对象:

@Override
    public String getParameter(String name) {

        if (!parametersParsed) {
            parseParameters();
        }

        return coyoteRequest.getParameters().getParameter(name);

    }

紧接着,继续追溯coyoteRequest.getParameters().getParameter(name)方法:

public String getParameter(String name ) {
        handleQueryParameters();
        ArrayList<String> values = paramHashValues.get(name);
        if (values != null) {
            if(values.size() == 0) {
                return "";
            }
            return values.get(0);
        } else {
            return null;
        }
    }

这里看到这个handleQueryParameters()方法引起警觉,继续查看源码:

 // -------------------- Processing --------------------
    /** Process the query string into parameters
     */
    public void handleQueryParameters() {
        if (didQueryParameters) {
            return;
        }

        didQueryParameters = true;

        if (queryMB == null || queryMB.isNull()) {
            return;
        }

        if(log.isDebugEnabled()) {
            log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
        }

        try {
            decodedQuery.duplicate(queryMB);
        } catch (IOException e) {
            // Can't happen, as decodedQuery can't overflow
            e.printStackTrace();
        }
        processParameters(decodedQuery, queryStringCharset);
    }

这个方法的主要作用是将查询串中的参数放到Parameters对象中去,这样request才能通过getParameter()方法拿到参数,这里的didQueryParameters变量就是问题所在了,通过测试发现报异常的时候,这个变量是为true的,而默认值是false,设想完成一次请求后,Parameters对象如果不去reset这些标志位,肯定会影响下次请求的,果然我们在Parameters对象里发现了这个方法:

public void recycle() {
        parameterCount = 0;
        paramHashValues.clear();
        didQueryParameters = false;
        charset = DEFAULT_BODY_CHARSET;
        decodedQuery.recycle();
        parseFailedReason = null;
    }

这里会把didQueryParameters重置为false,以便下一个request调用,那么问题又来了,这个方法是谁在调用的呢?继续查源码:

public void recycle() {
        bytesRead=0;

        contentLength = -1;
        contentTypeMB = null;
        charset = null;
        characterEncoding = null;
        expectation = false;
        headers.recycle();
        serverNameMB.recycle();
        serverPort=-1;
        localAddrMB.recycle();
        localNameMB.recycle();
        localPort = -1;
        remoteAddrMB.recycle();
        remoteHostMB.recycle();
        remotePort = -1;
        available = 0;
        sendfile = true;

        serverCookies.recycle();
        parameters.recycle();
        pathParameters.clear();

        uriMB.recycle();
        decodedUriMB.recycle();
        queryMB.recycle();
        methodMB.recycle();
        protoMB.recycle();

        schemeMB.recycle();

        remoteUser.recycle();
        remoteUserNeedsAuthorization = false;
        authType.recycle();
        attributes.clear();

        listener = null;
        allDataReadEventSent.set(false);

        startTime = -1;
    }

是由request的recycle()方法调用的,我们这里也可以看到,request完成一次请求调用后,需要重置很多变量和标志位的,然而这还不是最终答案,继续追溯:

/**
     * Recycle the input buffer. This should be called when closing the
     * connection.
     */
    void recycle() {
        wrapper = null;
        request.recycle();

        for (int i = 0; i <= lastActiveFilter; i++) {
            activeFilters[i].recycle();
        }

        byteBuffer.limit(0).position(0);
        lastActiveFilter = -1;
        parsingHeader = true;
        swallowInput = true;

        headerParsePos = HeaderParsePosition.HEADER_START;
        parsingRequestLine = true;
        parsingRequestLinePhase = 0;
        parsingRequestLineEol = false;
        parsingRequestLineStart = 0;
        parsingRequestLineQPos = -1;
        headerData.recycle();
    }


    /**
     * End processing of current HTTP request.
     * Note: All bytes of the current request should have been already
     * consumed. This method only resets all the pointers so that we are ready
     * to parse the next HTTP request.
     */
    void nextRequest() {
        request.recycle();

        if (byteBuffer.position() > 0) {
            if (byteBuffer.remaining() > 0) {
                // Copy leftover bytes to the beginning of the buffer
                byteBuffer.compact();
                byteBuffer.flip();
            } else {
                // Reset position and limit to 0
                byteBuffer.position(0).limit(0);
            }
        }

        // Recycle filters
        for (int i = 0; i <= lastActiveFilter; i++) {
            activeFilters[i].recycle();
        }

        // Reset pointers
        lastActiveFilter = -1;
        parsingHeader = true;
        swallowInput = true;

        headerParsePos = HeaderParsePosition.HEADER_START;
        parsingRequestLine = true;
        parsingRequestLinePhase = 0;
        parsingRequestLineEol = false;
        parsingRequestLineStart = 0;
        parsingRequestLineQPos = -1;
        headerData.recycle();
    }

这个对象,Http11InputBuffer,给出了我们终极答案,当关闭连接的时候会调用recycle()方法,而处理完当前的http 请求后,还会调用nextRequest()方法,这里的提示让我们豁然开朗,当前的请求必须被已经消费,如果request请求走异步没有结束的话,nextRequest方法是不会被调用的,相关标志位也就不会被重置,而采用线程池的方式处理,有可能由同一线程处理两个request,而第一个request还没有做recycle()处理,导致第二个request的参数无法解析了。
感兴趣的童鞋可以继续追溯源码,看看这个nextRequest()方法到底是由哪里调用的,同步和异步处理到底有什么不同呢?如果同步异步处理方式不同,那应该由两个地方调用nextRequest()方法分别处理才对,下边的代码验证了我们的猜测:
同步调用代码:

if (!isAsync() || getErrorState().isError()) {
       request.updateCounters();
        if (getErrorState().isIoAllowed()) {
            inputBuffer.nextRequest();
            outputBuffer.nextRequest();
        }
}


异步方法调用:

@Override
    protected SocketState dispatchEndRequest() {
        if (!keepAlive) {
            return SocketState.CLOSED;
        } else {
            endRequest();
            inputBuffer.nextRequest();
            outputBuffer.nextRequest();
            if (socketWrapper.isReadPending()) {
                return SocketState.LONG;
            } else {
                return SocketState.OPEN;
            }
        }
    }
   
 @Override
    public boolean isAsync() {
        return asyncStateMachine.isAsync();
    }

if (dispatches != null) {
        DispatchType nextDispatch = dispatches.next();
        state = dispatch(nextDispatch.getSocketStatus());
    } else if (status == SocketEvent.DISCONNECT) {
        // Do nothing here, just wait for it to get recycled
    } else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
        state = dispatch(status);
        if (state == SocketState.OPEN) {
            // There may be pipe-lined data to read. If the data isn't
            // processed now, execution will exit this loop and call
            // release() which will recycle the processor (and input
            // buffer) deleting any pipe-lined data. To avoid this,
            // process it now.
            state = service(socketWrapper);
        }
} 

这里我们很清楚的看到,同步方法的调用需要判断isAsync()是false才行,如果这个方法是true,说明是异步调用,需要根据异步状态机的状态定义,判断异步调用完成的状态,才可以最终执行dispatchEndRequest()方法,进而调用inputBuffer.nextRequest()方法。我们这里看下异步状态机的定义,作为本文的结尾:

private static enum AsyncState {
        DISPATCHED      (false, false, false, false),
        STARTING        (true,  true,  false, false),
        STARTED         (true,  true,  false, false),
        MUST_COMPLETE   (true,  true,  true,  false),
        COMPLETE_PENDING(true,  true,  false, false),
        COMPLETING      (true,  false, true,  false),
        TIMING_OUT      (true,  true,  false, false),
        MUST_DISPATCH   (true,  true,  false, true),
        DISPATCH_PENDING(true,  true,  false, false),
        DISPATCHING     (true,  false, false, true),
        READ_WRITE_OP   (true,  true,  false, false),
        MUST_ERROR      (true,  true,  false, false),
        ERROR           (true,  true,  false, false);

        private final boolean isAsync;
        private final boolean isStarted;
        private final boolean isCompleting;
        private final boolean isDispatching;

        private AsyncState(boolean isAsync, boolean isStarted, boolean isCompleting,
                boolean isDispatching) {
            this.isAsync = isAsync;
            this.isStarted = isStarted;
            this.isCompleting = isCompleting;
            this.isDispatching = isDispatching;
        }

        public boolean isAsync() {
            return isAsync;
        }

        public boolean isStarted() {
            return isStarted;
        }

        public boolean isDispatching() {
            return isDispatching;
        }

        public boolean isCompleting() {
            return isCompleting;
        }
    }

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值