千万不要把Request传递到异步线程里面,有坑

news2024/12/27 11:57:58

前几天在网上冲浪的时候看到一篇技术文章,讲的是他把一个 request 请求传递到了线程池里面,然后遇到了一个匪夷所思的情况。

他写了这篇文章,把自己针对这个问题的探索过程分享了出来:

《springboot 中如何正确的在异步线程中使用request》
https://www.cnblogs.com/mysgk/p/16470336.html

文章还是挺不错的,把发现问题和解决问题都写的很明白了。

但是,我觉得把探索问题的部分写的太省略了,导致我看完之后都不知道这个问题的根本原因是什么。

而为什么我会对这篇文章特别感兴趣呢?

因为这个“坑”我记得我刚刚入行没两年的也遇到过,我已经不记得自己当时是怎么解决的了,但是我肯定也没有深入的去研究。

因为那个时候遇到问题,就去网上费尽心思的查,粘一个方案过来看能不能用。

如果不能用的话,心里暗骂一句:小可(S)爱(B),然后接着找。

直到找到一个可以用的。

至于为什么能用?

管它呢,研究这玩意干啥。

主要是当时觉得探索这个玩意到进入到源码里面去,一涉及到源码心里就犯怵,所以就敬而远之。

现在不一样了,现在我看到源码我就觉得兴奋,心里想着:多好的素材啊。

既然这次又让我遇到了,所以我决定把几年前的坑填上,盘一盘它。

搞个 Demo

由于这个现象太过匪夷所思,所以写文章的那个老哥认为这个是一个 BUG,还在 Spring 的 github 上提了一个 issues:

https://github.com/spring-projects/spring-framework/issues/28741

这里面他附上了一个可以复现的 Demo,所以我就直接拿来用了。

确实是可以复现,但是其实他提供的这个 Demo 还是有点臃肿,具有一点点的迷惑性,直接给我迷晕了,让我在这上面稍微花了时间。

先给你看一下他的 Demo 是怎么样的。

主要是两个 Controller 接口。

第一个接口是  get 请求类型的 getParams,代码很简单,先放在这里,等下用:

第二个接口是 post 请求类型的 postTest,就这么几行代码:

@PostMapping("/postTest")
public String postTest(HttpServletRequest request) {
    String age1 = request.getParameter("age");
    String name1 = request.getParameter("name");
    System.out.println("age1=" + age1 + ",name1=" + name1);
    new Thread(new Runnable() {
        @Override
        public void run() {
            String age2 = request.getParameter("age");
            String name2 = request.getParameter("name");
            System.out.println("age2=" + age2 + ",name2=" + name2);
            //模拟业务请求
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            age2 = request.getParameter("age");
            name2 = request.getParameter("name");
        }
    }).start();
    return "post success";
}

主要是里面启动了一个线程,在线程里面有从 request 里面获取参数的动作。

这个方法访问起来是这样的一个情况:

从 age2、name2 输出上看,虽然 request 传入到异步线程里面了,但是还是能从里面获取到对应的参数,没有看出来有什么毛病。

但是接下来,匪夷所思的事情就要出现了。

还记得我们前面的 getParams 接口吗?

我再把它拿过来给你看一眼:

你说,就这个接口,我用下面这个链接去访问,在我的认知里面是完全不可能有任何问题的,对吧?

http://127.0.0.1:8080/getParams?a=1&b=2

但是,这玩意还真的就打破了我的认知:

在访问 postTest 方法之后,再次访问 getParams 方法,getParams 方法居然抛出异常了?

抛出的异常是说我调用的时候没有传递 b 这个参数。

但是我的链接里面明明就是有 b=2 的啊?

这玩意上哪里说理去?

上面就是那位老哥提供的可复现的 Demo 的主要部分。

但是我前面说了,这个 Demo 有点臃肿,具有一点点迷惑性。

首先如果我再加一个输出语句,那么在一个短暂的 sleep 之后, age2 和 name2 就没了:

虽然还是感觉有点神奇吧,但是也没有刚刚那个操作让我感到震惊。

因为从输出 null 这个结果,我至少可以知道程序在这个地方就出现问题了,把问题的范围限定在了一次请求中。

刚刚那个操作,好家伙,表现出来到情况是这样的:

  • 先发起一个 post 请求,看起来是正常的。

  • 然后再发起一个 get 请求,这个 get 请求挂了。

  • 但是这个 get 请求从发起的角度来看找不到任何毛病。

你要基于上面这个情况去分析问题的话,就不好找问题了,毕竟要发起两个毫不相干的请求才能触发问题。

加入一行输出日志,相当于把问题简化了一点。

但是你看到的是我就加了一行输出日志,实际上等我加这行日志的时候,我拿到这个 Demo 已经过去了好几个小时了。

在这期间我也一直以为必须要按照这个流程来操作,才能复现问题。

所以我才说具有一点点迷惑性。

好,现在不管怎么说吧。

我先把 Demo 简化一点,便于继续分析。我的 Demo 可以简化到这个程度:

@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";
}

get 和 post 请求都可以,只是我为了方便选择发起 get 请求。

然后只需要传递一个参数就行,核心步骤是要把 request 传递到异步线程里面去,调用 getParameter 再次获取对应入参。

你可以把上面的代码粘到你本地,把项目跑起来,然后调一次下面这个链接:

http://127.0.0.1:8080/getTest?age=18

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

到这里就复现了前面说的问题。

但是你別着急,你再次发起调用,你会看到控制台的输出是这样的:

怎么样,是不是很神奇,很懵逼?

为了让你更加直观的懵逼,我给你上个动图,发起两次调用,主要关注控制台的输出:

好,现在,你就去泡杯茶,点根烟,慢慢去琢磨,这玩意是不是属于超自然现象。

探索

其实我看到这个现象的时候并不是特别的震惊,毕竟写文章这几年,什么稀奇古怪的现象都遇到过。

所以我只是轻蔑一笑,看向了我排查问题的武器库,很快就看到了一个比较趁手的东西:开启 Debug 日志。

如果是以前,对于这种没有抛出异常的问题跟着,由于没有异常堆栈,我肯定是迫不及待的正向的 Debug 跟了一下源码,扎到源码里面去一顿狂翻,左看右看。

但是结果常常是一头扎进去之后,很快就迷失了,搞了好几个小时才从源码里面爬出来,出来的时候基本上一无所获。

但是我现在不会这么猴急了,现在就成熟了很多。遇到这类问题不会先急着去卷源码会先多从日志里面挖掘一点东西出来。

所以我遇到这个问题的第一反应就是调整日志级别到 Debug:

logging.level.root=debug

观察日志这个小技巧我在之前的文章里面也分享过。

当日志调整到 Debug 级别之后,再次发起两次调用,问题复现,同时把日志拿出来做对比。

两次请求的 Debug 日志整体情况是这样的,左边是第一次请求,右边是第二次请求:

可以看到第一次请求比第二次请求的日志多。

多说明什么问题?

是不是说明第一次请求调用的方法更多一点?

为什么多一点,到底是哪些方法只调用了一次?

我也不知道,但是我能从 Debug 日志里面梳理出来。

比如下面这个图就是梳理出来的第一次请求多打印的日志:

很快我就从 Debug 日志里面看到了一个我觉得很可疑的地方:

Start processing with input [age=18]

这一行日志,只有第一次请求的时候打印了,从日志表达的意思来看,是处理请求里面的 age=18。

为什么第二次不打印呢?

我也不知道,但是我知道了第一个关键断点打在什么位置了。

全局搜索关键字 “Start processing with input” 可以找到配置文件里面的 “parameters.bytes”。

然后全局搜索 “parameters.bytes”,就能找到是在 Parameters.java 文件里面输出的:

也就是这个地方:

org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)

找到第一个断点,就找到了突破口,只要好好的拿捏住,之后的事情就基本上就顺风顺水了。

首先,重启项目,发起调用,在断点处看调用堆栈:

接下来的思路是什么?

就是我要从堆栈里面找到一个东西。

你想啊,第一次请求走这个地方,第二次请求就不走这个地方了,所以一定有个类似于这样的逻辑:

if(满足某个条件){
    走processParameters方法
}

所以,只需要往回找五个调用栈,我就找到了这一个方法:

org.apache.catalina.connector.Request#getParameter

这个时候你看旁边的 parametersParsed 参数是 true,按理来说 true 不应该走进 if 分支呀?

因为这个地方我们是从断点处的堆栈信息往回找,在从 parseParameters 方法到 processParameters 方法之间,肯定有地方修改了 parametersParsed 参数的值为 true。

这一点,从 parametersParsed 的初始值是 false 也能看出来:

因此,我决定把第二个断点打在 getParameter 方法中:

再次重启服务,发起调用,parametersParsed 为 false,开始执行 parseParameters() 方法解析参数:

而解析参数的目的之一就是把我的 age=18 放到 paramHashValues 这个 Map 容器里面:

org.apache.tomcat.util.http.Parameters#addParameter

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

但是,朋友们,注意上面的图片中有个标号为 ① 的地方:

这个方法,在 parseParameters 方法里面也会被调用:

org.apache.tomcat.util.http.Parameters#handleQueryParameters

好,现在打起精神来听我说。

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

didQueryParameters 初始为 false,随后被设置为 true。

这个很好理解,入参解析一次就行了,解析的产物一个 Map,后续要拿参数对应的值,从 Map 里面获取即可。

比如我把入参修改为这样:

http://127.0.0.1:8080/getTest?a=1&b=2&c=3&d=4

那么经过解析之后,这个 Map 就变成了这样:

经过了前面的这一顿折腾之后,现在找到了解析入参的方法。

那么全文的关键点就在 didQueryParameters 这个参数的变化了。

只有是 false 的时候才会去解析入参。

那么我接下来的排查思路就是观察 didQueryParameters 参数的变化,所以在字段上打上断点,重启项目,继续调试:

第一次进入这个方法的时候 didQueryParameters 为 false,入参是 age=18:

而第一次进入这个方法的原因我前面也说了,是因为触发了 parseParameters 的逻辑:

第二次进入这个方法 didQueryParameters 变为 true 了,不用再次解析:

那么第二次进入这个方法的原因是什么?

前面也说了,getParameter 方法的第一行就是触发解析的逻辑:

接下来,断点停在了这个地方:

org.apache.tomcat.util.http.Parameters#recycle

方法叫做 recycle,表明是循环再利用,在这里面会把存放参数的 Map 清空,把 didQueryParameters 再次设置为了 false。

而当你用同样的手段去观察 parametersParsed 参数,也就是这个参数的时候:

会发现它也有一个 recycle 方法:

org.apache.catalina.connector.Request#recycle

这个方法上的注释,也有一个特别扎眼的词:reuse。

注释过来是这样的:释放所有的对象引用,并初始化实例变量,为重新使用这个对象做准备。

种种迹象表明 request 在 tomcat 里面是循环使用的。

虽然在这之前我也知道是循环使用的,但是百闻不如一见嘛。这次是我 Debug 的时候亲眼看到了。

又拿捏一个小细节。

由于我们在异步线程里面还触发了一次 getParameter 方法:

但是 getTest 方法已经完成了响应,这个时候 Request 可能已经完成了回收。

注意我说的是“可能”,因为这个时候 Request 的回收动作和异步线程谁先谁后还不一定。

这也解释了这个现象:

虽然 request 传入到异步线程里面了,但是还是能从里面获取到对应的参数。

因为此时 request 的回收动作还没做完,还可以继续获取参数。

为了避免这个“可能”,我把 sleep 的时间调整为 5s,保证 request 完成回收。

然后这异步线程里面继续 Debug,接下来神奇的事情就要开始了。

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

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

但是,我们可以看到,此时查询的内容却没有了,是个 null:

这个也好理解,肯定是随着调用结束,被 recycle 了嘛:

所以,到这里我能解答为什么异步线程里面的输出是 null 了。

queryMB 就是我调用的时候传入的 age=18。

通过 Debug 发现异步线程里面调用 getParameter 的时候没有 queryMB ,所以就不会解析出 Map。

没有 Map ,异步线程里面的输出肯定是 null。

为什么没有 queryMB 呢?

因为当前这个请求已经被返回了,执行了 recycle 相关操作,queryMB 就是在这个时候没有的。

那么为什么再次发起调用,会出现这个神奇的现象呢?

很简单,因为在异步线程里面调用 getParameter 的时候,把 didQueryParameters 设置为 true 了。

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

所以,从 Debug 来看,虽然 queryMB 是有值的,但是没用啊,didQueryParameters 是 true,程序直接 return 了,不会去解析你的入参:

问题得到解答。

此时,我们再回到最开始的这个方法中:

你想想为什么这个方法调用的时候出现异常了?

还是一样的道理呀,由于 request 是复用的,虽然你传入了参数 b,但是由于前一个请求在异步线程里面调用了 getParameter 方法,将 didQueryParameters 设置为了 true,导致程序不会去解析我传入的 a=1&b=2。

从调用链接的角度来说,虽然我们调用的是这个链接:

http://127.0.0.1:8080/getParams?a=1&b=2

但是对于程序来说,它等效于这个链接:

http://127.0.0.1:8080/getParams

由于入参 b 是 int 类型的,那可不就是会抛出这个异常吗:

这个异常是说:哥们,你要么把 b 搞成 Integer 类型的,不传值我就给你赋为 null。要么给我传一个值。

你现在用 int 来接受,又不给我值,我这没法处理啊?

我能给你默认赋值一个 0 吗?

肯定不能啊,0 和 null 可不是一个含义,万一你程序出异常了,把锅甩给我怎么办?

算了,我还是抛异常吧,最稳妥了。

所以你看,要是你从这个抛异常的地方去找答案,也许能找到,但是路就走远了一点。

因为这个地方并不是问题的根因。

到这里,你应该清楚这个 BUG 到底是怎么回事了。

request 的生命周期

在探索这个问题的过程中,我也想到了另外一个问题:

一个 request 请求的生命周期是怎么样的?

这题我记得几年前我背过,现在我确实有点想不起来了,但是我知道去哪里找答案。

Java Servlet Specification,这是一份规范,答案就藏在这个规范里面:

https://javaee.github.io/servlet-spec/downloads/servlet-4.0/servlet-4_0_FINAL.pdf

在 3.13 小节里面,关于 request 这个 Object 的生命周期,规范是这样说的:

这寥寥数语,非常关键,所以我一句句的拆解给你看。

Each request object is valid only within the scope of a servlet’s service method, or within the scope of a filter’s doFilter method,unless the asynchronous processing is enabled for the component and the startAsync method is invoked on the request object.

一上来就是一个长句,但是根本不要慌。

你知道的,我英语八级半,水平一向是可以的。

先把长句拆短一点,我可以先只翻译 unless 之前的部分。

前面这部分说:每个 request 对象只在 servlet 的服务方法的范围内有效,或者在过滤器的 doFilter 方法的范围内有效。

接着它来了一个 unless,表示转折,和 but 差不多。

我们主要关注 unless 后面这句:

the asynchronous processing is enabled for the component and the startAsync method is invoked on the request object.

组件的异步处理功能被启用,并且在 request 上调用了 startAsync 方法。

也就是说,request 的生命周期在遇到异步的时候有点特殊,但是这个异步又不是我前面演示的那种异步。

关于异步,规范中提到了 request 里面有个方法:startAsync。

我去看了一眼,果然是有:

返回值是一个叫做 AsyncContext 的东西。

但是我先按下不表,接着往下翻译。

In the case where asynchronous processing occurs, the request object remains valid until complete is invoked on the AsyncContext.

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

这里又提到了一个 complete 方法,这个 complete 方法 invoked on the AsyncContext。

AsyncContext 是什么玩意?

不就是 request.startAsync() 方法的返回值吗?

果然在 AsyncContext 里面有个 complete 方法:

不慌,继续按下不表,一会就回收,接着往下看。

Containers commonly recycle request objects in order to avoid the performance overhead of request object creation.

容器通常会 recycle 请求对象,以避免创建请求对象的性能开销。

看到这个 recycle 我们就很眼熟了,原来规范里面是建议了容器里面实现 request 的时候尽量复用,而不是回收,目的是节约性能。

这玩意,属于意外收获呀。

最后一句话是这样的:

The developer must be aware that maintaining references to request objects  for which startAsync has not been called outside the scope described above is not recommended as it may have indeterminate results.

这句话是说:程序员朋友们必须要意识到,我不建议在上述范围之外维护 request 的引用,因为它可能会产生不确定的结果。

看到这个“不确定的结果”时我很开心,因为我前面已经演示过了,确实会产生莫名其妙的结果。

但是规范里面在“scope”之前还加了一个限定词:startAsync has not been called。

反过来说,意思就是如果你有一个调用了 startAsync 方法的 request,那么在上述范围之外,你还可以操作这个 request,也不会有问题。

这一整段话中,我们提炼到了两个关键的方法:

  • request 的 startAsync 方法

  • AsyncContext 的 complete 方法

根据规范来说,这两个方法才是 request 异步编程的正确打开方式。

正确打开方式

在这之前,假设你完全不知道 startAsync 和 complete 方法。

但是看了规范上的描述,猜也能猜出来代码应该这样写,然后发起多次调用,没有任何毛病:

这就是正确的打开方式。

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

为什么这样写就能实现 request 的异步化呢?

用脚指头想也能想到,一定有一个这样的判断逻辑存在:

if(调用过request的startAsync方法){
    先不回收
}

所以,用之前的方法,在 recycle 方法上打断点,并往回找,很快就能找到这个方法:

然后,关于 AsyncContext 的 complete 方法我还注意到它有这样的一个描述:

也就是说在调用 complete 方法之后 response 流才会关闭,那么有意思的就来了:

我不仅在异步线程里面可以操作 request 还可以操作 response。

但是转念一想,既然都是异步编程了,操作 response 的意义肯定比操作 request 的意义更大。

关于 Tomcat 对于异步请求的支持还有很多可以探索的地方,自己慢慢去玩吧。

写到这里的时候我发现标题说的也不对,标题是:千万不要把 Request 传递到异步线程里面!有坑!

而正确的说法应该是:

千万不要随便把 Request 传递到异步线程里面!有坑!你拿捏不住,得用 startAsync 方法才行。

好了,就这样吧,本文写到这里就差不多了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/108493.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

艾美捷脂质过氧化检测试剂盒参数说明和文献参考

脂质过氧化的定量对于评估氧化损伤在病理生理学疾病中的作用至关重要。脂质过氧化导致饱和和不饱和脂质的高反应性和不稳定的氢过氧化物的形成。 艾美捷脂质过氧化检测试剂盒直接利用与亚铁离子的氧化还原反应测量过氧化氢,将脂质过氧化氢提取到氯仿中,提…

【python绘制地图——folium的方法和类的介绍(思维导图)】

Python使用folium制作地图并生成png图片 第一章 folium的方法和类的介绍(思维导图) 第二章 使用folium制作地图 第三章 folium实用功能进阶 第三章 使用Html2Image生成png图片 第四章 使用reportlab制作pdf报告 文章目录Python使用folium制作地图并生成…

轮廓图编程-自定义QChartView

目录 一、功能需求 二、实现效果 三、实现方法 一、功能需求 3D测量软件中,需要在轮廓上进行二次编程,需要显示轮廓线,然后可以调节矩形框的范围的获取参数,如华汉的HyperShape3D软件,对轮廓的编程界面如下。 二、实…

【Java基础知识复盘】String、StringBuffer、StringBuilder篇——持续更新中

本人知识复盘系列的博客并非全部原创,大部分摘自网络,只是为了记录在自己的博客方便查阅,往后也会陆续在本篇博客更新本人查阅到的新的知识点,望悉知! String类 在 Java 中字符串属于对象,Java 提供了 Str…

《融合视觉显著性和局部熵的红外弱小目标检测》论文复现

1.复现论文概要 复现的论文为《融合视觉显著性和局部熵的红外弱小目标检测》(赵鹏鹏,李庶中等,中国光学2022,http://www.chineseoptics.net.cn/cn/article/doi/10.37188/CO.2021-0170,以下简称论文)。论文…

【库存控制】基于象鼻虫损害优化算法求解库存控制问题(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

交换机的结构特点及主要功能

什么是交换机? Switch 意为“交换机”,是一种用于转发电 ( 光 ) 信号的网络设备。它可以为访问交换机的任意两个网络节点提供专用的电信号路径。非常常见的 是以太网交换机。其他常见的有电话语音交换机、光纤交换机等。 它的工作原理是什么&#xff1f…

美国藤校Top30大学对IB成绩的要求

众所周知,IB体系是目前全球认可度比较高的国际课程。那么,用IBDP成绩申请美国TOP30大学需要什么样的成绩? 小智今天来带大家好好研究下美国TOP30大学对IB成绩的要求。普林斯顿大学 Princeton University 普林斯顿大学对于IB成绩没有具体要求&…

nginx架构解析:朴实中见真知

目录前言为什么高并发很重要Apache可以做到吗使用nginx会更有优势吗?nginx架构概览代码结构Workers模型nginx进程规则nginx缓存概览nginx配置nginx内部典型的HTTP请求处理循环课程总结前言 nginx(发音“engine x”)是俄国的软件工程师Igor S…

Godzilla(哥斯拉)安装与使用

Godzilla安装与使用1.Godzilla介绍2.Godzliia下载与安装2.1.Godzilla下载2.2.运行环境2.3.Godzilla安装2.3.1.执行文件2.3.2.安装完成3.Godzliia3.1.Godzliia使用3.1.1.生成木马3.1.2.存储木马3.1.3.放入木马3.1.4.访问木马3.1.5.连接木马3.1.6.测试连接3.1.7.连接进入3.2.Godz…

麒麟水乡,IU酒店进驻云南旅游咽喉之地曲靖

曲靖位于云南省东北部,是云南连接内地的重要陆路通道,素有“滇黔锁钥”、“入滇门户”、“云南咽喉”之称,是仅次于昆明的云南第二大城市。曾入选“中国十佳宜居城市”榜单10次的城市,拥有3000多年的文明史,早在三国魏…

Python解题 - CSDN周赛第17期 - 拯救公主

本期又出现了题目测试数据的问题,而且题目和算法关系也不太大,基本就属于用代码代替手工解答算术题的感觉。不禁让人怀疑官方题库是否已经没有高质量的题了,同时也怀疑长期满分却又不更新博客拿奖品的某位选手是不是托。。。 第一题&#xff…

VS系列多通道振弦传感器无线采发仪与参数配置工具连接

VS101~VS432 设备配备了专门的参数配置工具 SETP 来完成设备工作参数的查看和修改工作。 连接前的准备工作 (1)数据接口与计算机连接 使用标配的通讯线与计算机 RS232 接口连接。若需基于手机网络发送数据,请在开机前安装 SIM 卡。 若…

D. Lucky Chains(GCD+素数筛)

input: 4 5 15 13 37 8 9 10009 20000output: 0 1 -1 79题目大意: 如果一个数对(x,y)是幸运的,当且仅当gcd(x,y)1,一条链可以由以下规律的数对组成,(x,y),(x1,y1),(x2,y2)……(xk,yk),如果说一条链是幸运的,当且仅当…

非零基础自学Golang 第16章 正则表达式 16.3 regexp包 16.4 小结 16.5 知识拓展

非零基础自学Golang 文章目录非零基础自学Golang第16章 正则表达式16.3 regexp包16.3.1 MatchString函数16.3.2 FindStringIndex函数16.3.3 ReplaceAllString函数16.4 小结16.5 知识拓展16.5.1 常用正则表达式参考第16章 正则表达式 16.3 regexp包 Go在处理正则表达式时主要使…

拆箱phper最适合入门的go框架beego

beego beego 是一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性&#xff…

路由信息协议RIP(计算机网络)

目录 路由选择协议 路由信息协议 RIP中的距离 RIP协议的具体算法 RIP协议路由表的更新 例题 距离向量算法 RIP 协议的要点 路由选择协议 路由器之间要互相不断交换网络拓扑和状态信息,根据信息求出到所有目的网络的最佳路由 用于交换路由信息,进…

软件测试实战教程系列—接口测试用例和报告模板|收藏版

文章目录 接口测试为什么会如此重要呢? 接口测试的必要性 获取接口相关信息 接口测试的流程 接口文档 是接口测试的参照,至少包括: 接口测试用例设计 接口测试用例模板 (可根据项目实际情况设计增减) 接口测试…

前端基础_组合多个图形

组合多个图形 在前面的实例中,我们看到使用Canvas API可以将一个图形重叠绘制在另一个图形上面,但图形中能够被看到的部分完全取决于以哪种方式进行组合,这时需要使用到Canvas API的图形组合技术。在HTML5中,只要用图形上下文对象…

模数转换器(ADC)

目录 一、简介: 二、主要特征 三、校准(CLB) 四、ADC时钟 五、ADCON开关 六、规则组和注入组 七、转换模式 八、注入通道管理 九、可编程的采样时间 十、外部触发 十一、温度传感器和内部参考电压 一、简介: 12位ADC是…