关于 HttpServletRequest 生命周期导致 NPE

54 阅读1分钟

记得之前有次发布后线上莫名偶发性出现 NPE,出现 NPE 还好,问题在于 NPE 的地方特别奇怪: image.png

当时看到这个 Exception 一脸懵,这算啥问题?先来尝试直接来看报错点:

// MimeHeaders
private void findNext() {
    next=null;
    for(; pos< size; pos++ ) {
        next=headers.getName( pos ).toString(); // NPE 
        for( int j=0; j<pos ; j++ ) {
            if( headers.getName( j ).equalsIgnoreCase( next )) {
                // duplicate.
                next=null;
                break;
            }
        }
        if( next!=null ) {
            // it's not a duplicate
            break;
        }
    }
    // next time findNext is called it will try the
    // next element
    pos++;
}

public MessageBytes getName(int n) {
    return n >= 0 && n < count ? headers[n].getName() : null;
}

结合上述两段代码来看,最可疑的是 getName 返回 null 从而导致 toString 调用触发 NPE!那什么时候回返回 null?考虑到 n >= 0 基本上为 true ( 不然就是代码 bug ),那么自然剩下的 n < count,观察到在另外一个方法中有这么一个逻辑:

public void clear() {
    for (int i = 0; i < count; i++) {
        headers[i].recycle();
    }
    count = 0;
}

通过断点发现该方法会在一次请求后调用,那么看起来问题就很清晰了!想到问题是在上一次发版之后才出现,直接看修改记录,发现有如下代码 ( 为方便理解移除无关代码 ):

public long exportItemInformation() {
    // ······
    return baseTaskService.asyncExportItemInformation(RequestContextHolder.getRequestAttributes());
}

@Async("asyncTaskExecutor")
public void asyncExportItemInformation(RequestAttributes requestAttributes) {
    RequestContextHolder.setRequestAttributes(requestAttributes);
    // ······
}

看到这两段代码段,顿时也就明白问题所在 -- 在主方法请求完成后,其中的 MimeHeaders 会被 clear,而异步方法则在使用。至此,问题的根因浮现!( 自然解决办法也相当简单,相信聪明如你立马能想到 )

并发问题相当难定位,在设计上能不用就不用,这也是为什么 Redis 坚持单线程执行 command 的原因。