当前位置: 首页 > article >正文

Request 跨线程访问问题

优质博文:IT-BLOG-CN

此篇文章是基于 Tomcat Request Cookie 丢失问题 文章的一个延续

一、Request 跨线程访问问题

问题代码摘要

为了方便选择发起get请求,然后只需要传递一个参数就行,核心步骤是要把request传递到异步线程里面去,调用getParameter再次获取对应入参。

@GetMapping("/getTest")
public String getTest(HttpServletRequest request) {
    String age = request.getParameter("age");
    System.out.println("age=" + age);
    new Thread(() -> {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        String age1 = request.getParameter("age");
        System.out.println("age1=" + age1);
    }).start();
    return "success";
}

获取请求:http://127.0.0.1:8080/getTest?age=18

从控制台你可以看到这样的输出:

age=18
age1=null

当再次发起调用,会看到控制台的输出是这样的:

age=18
age1=null
age=null
age1=null

和上面的问题类似,这里也有一个类似的方法:getParameter

@Override
public String getParameter(String name) {
    if (!parametersParsed) {
        parseParameters();
    }
    return coyoteRequest.getParameters().getParameter(name);
}

parametersParsed参数初始化是false进入parseParameters()方法解析参数,将age=18放到paramHashValues这个Map容器中。 后续的重复请求就会省略解析参数的操作。

parseParameters()方法执行完成之后,接着从前面的 paramHashValues容器里面把age对应的18返回回去:

public String getParameter(String name) {
    handleQueryParameters(); // 这里也需要注意,存在一个类似的逻辑
    ArrayList<String> values = paramHashValues.get(name);
    if (values != null) {
        if (values.size() == 0) {
            return "";
        }
        return values.get(0); // 返回的是 age 的值 18
    } else {
        return null;
    }
}

这里重点看下handleQueryParameters方法的实现:

public void handleQueryParameters() {
    if (didQueryParameters) {
        return;
    }

    didQueryParameters = true;

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

    try {
        decodedQuery.duplicate(queryMB);
    } catch (IOException e) {
        e.printStackTrace();
    }

    processParameters(decodedQuery, queryStringCharset);
}

这个方法在parseParamters中也会调用:

protected void parseParamters() {
    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());

        //...
        Charset charset = getCharset();

        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
        parameters.setCharset(charset);
        
        //...
        
        // 这里也调用了 handleQueryParameters 方法。
        parameters.handleQueryParameters();

    }
}

handleQueryParameters方法才是真正解析参数的方法,为了防止重复解析它加入了这样的逻辑:

if (didQueryParameters) {
    return;
}

didQueryParameters = true;

didQueryParameters初始为false,随后被设置为true。这个和之前的业务逻辑一致,入参解析一次并存放至Map中。

方法叫做recycle,表明是循环再利用,在这里面会把存放参数的Map清空,把didQueryParameters再次设置为了falseorg.apache.tomcat.util.http.Parameters#recycle方法如下:

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

而当你用同样的手段去观察parametersParsed参数,也就是这个参数的时候,会发现它也有一个recycle方法:org.apache.catalina.connector.Request#recycle

public void recycle() {
    internalDispatcherType = null;
    requestDispatcherPath = null;

    authType = null;
    inputBuffer.recycle();
    usingInputStream = false;
    usingReader = false;
    userPrincipal = null;
    parametersParsed = false;
}

由于我们在异步线程里面还触发了一次getParameter方法:但是getTest方法已经完成了响应,这个时候Request可能已经完成了回收。为了避免这个“可能”,我添加了sleep,保证request完成回收。

@GetMapping("/getTest")
public String getTest(HttpServletRequest request) {
    String age = request.getParameter("age");
    System.out.println("age=" + age);
    new Thread(() -> {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        String age1 = request.getParameter("age");
        System.out.println("age1=" + age1);
    }).start();
    return "success";
}

再次触发handleQueryParameters的时候,didQueryParameters由于被recycle了,所以变成了false

然后执行解析的逻辑,把didQueryParameters设置为true

但是,我们可以看到,此时查询的内容却没有了,是个null:这里的null也很好理解,肯定是随着调用结束,被recycle了。

为什么再次发起请求的时候,都返回null。

因为TomcatRquest使用的是池化思想,如果你拿到的是上一次的Request请求,那么因为在异步线程里面调用getParameter的时候,把didQueryParameters设置为true了。

但是异步线程里面的调用,超出了request的生命周期,所以并不会再次触发requestrecycle相关操作,因此这个request拿来复用的时候didQueryParameters还是true

所以,第二次请求的入参有值的,但是没用啊,didQueryParameterstrue,程序直接return了,不会去解析你的入参:

二、Request 的生命周期

每个request对象只在servlet的服务方法的范围内有效,或者在过滤器的doFilter方法的范围内有效。

但是组件的异步处理功能被启用后,并且在request上调用了startAsync方法后比较特殊。我们先看下startAsync方法:

public AsyncContext startAsync() throws IllegalStateException

在发生异步处理的情况下,request对象的生命周期一直会延续到在AsyncContext上调用complete方法之前。

/**
 * Completes the async request processing and closes the response stream
 */
void complete();

也就是说如果需要在上述范围之外,也就是多线程中使用request对象,需要使用到如下两个方法:
【1】requeststartAsync方法;
【2】AsyncContextcomplete方法;

我们将之前的代码进行改造:

@GetMapping("/getTest")
public String getTest(HttpServletRequest request, HttpServletResponse response) {
    AsycnContext asycnContext = request.startAsync(request, response);
    String age = request.getParameter("age");
    System.out.println("age=" + age);
    new Thread(() -> {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        String age1 = request.getParameter("age");
        System.out.println("age1=" + age1);
        asycnContext.complete();
    }).start();
    return "success";
}

此时在进行调用的时候,就算调用两次,都会正常输出:

age=18
age1=18
age=18
age1=18

从现象上来说,就是getTest请求返回之后,request线程并没有被调用recycle方法进行回收。

recycle方法的调用链上很快就能找到这个方法:

@Override
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status) throws IOException {
    // ......
    if (dispatches != null) {
        // ......
    } else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
        state = dispatch(status);
        state = checkForPipelinedData(state, socketWrapper);
    }
}

complete()方法上面的注解closes the response stream也不难发现,只有调用complete()方法之后,response流才会关闭。


http://www.kler.cn/a/315997.html

相关文章:

  • Javascript高级—常见算法
  • 神仙级网络安全入门教程(非常详细),从零基础入门到精通,从看这篇开始!
  • 工作和学习遇到的技术问题
  • 【月之暗面kimi-注册/登录安全分析报告】
  • 深度学习--正则化
  • 传奇996_21——龙岭事件
  • 屋顶气膜网球馆:智慧城市资源利用之道—轻空间
  • STM32 的 SDIO 接口(基于STM32F429HAL库)
  • 考题抄错会做也白搭——模板方法模式
  • h5dump用法详解
  • 乐观锁、悲观锁及死锁
  • 【机器学习】---神经架构搜索(NAS)
  • 【tomcat】tomcat学习笔记
  • 垃圾邮件检测_TF-IDF分析,聚类分析与朴素贝叶斯
  • spring springboot 日志框架
  • 光伏行业的酸洗与深度除氟
  • 零信任安全架构--分段网络
  • 实战OpenCV之直方图
  • ESP32-WROOM-32 [ESP连接路由器+TCP Client 透传 + TCP Server数据发送]
  • 网络安全:构建数字世界的坚实防线
  • ps学习。
  • 经典大语言模型解读(3):参数量更大、泛化性能更强的生成式模型GPT-2
  • 低代码开发平台系统架构概述
  • js进阶——函数作用域和块作用域
  • 卷积神经网络(CNN):深度学习中的视觉奇迹
  • 【论文阅读】Benchmarking Retrieval-Augmented Generation for Medicine