大家好,我是三友~~

在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章

但是说实话,从我现在的角度来看,这两篇文章的结构和内容其实还可以更加完善

刚好我最近打算整个SpringCloud各个组件架构原理的小册子

所以趁着这个机会,我就来重新写一下这两篇文章,弥补之前文章的不足

这一篇文章就先来讲一讲OpenFeign的核心架构原理

整篇文章大致分为以下四个部分的内容:

第一部分,脱离于SpringCloud,原始的Feign是什么样的?

第二部分,Feign的核心组件有哪些,整个执行链路是什么样的?

第三部分,SpringCloud是如何把Feign融入到自己的生态的?

第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?

好了,话不多说,接下来就直接进入主题,来探秘OpenFeign核心架构原理

原始Feign是什么样的?

在日常开发中,使用Feign很简单,就三步

第一步:引入依赖

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>2.2.5.RELEASE</version>
</dependency>

第二步:在启动引导类加上@EnableFeignClients注解

@SpringBootApplication
@EnableFeignClients
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

第三步:写个FeignClient接口

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderApiClient {

    @GetMapping
    Order queryOrder(@RequestParam("orderId") Long orderId);

}

之后当我们要使用时,只需要注入OrderApiClient对象就可以了

虽然使用方便,但这并不是Feign最原始的使用方式,而是SpringCloud整合Feign之后的使用方式

Feign最开始是由Netflix开源的

后来SpringCloud就将Feign进行了一层封装,整合到自己的生态,让Feign使用起来更加简单

并同时也给它起了一个更高级的名字,OpenFeign

接下来文章表述有时可能并没有严格区分Feign和OpenFeign的含义,你知道是这么个意思就行了。

Feign本身有自己的使用方式,也有类似Spring MVC相关的注解,如下所示:

public interface OrderApiClient {

    @RequestLine("GET /order/{orderId}")
    Order queryOrder(@Param("orderId") Long orderId);

}

OrderApiClient对象需要手动通过Feign.builder()来创建

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class, "http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

Feign的本质:动态代理 + 七大核心组件

相信稍微了解Feign的小伙伴都知道,Feign底层其实是基于JDK动态代理来的

所以Feign.builder()最终构造的是一个代理对象

Feign在构建动态代理的时候,会去解析方法上的注解和参数

获取Http请求需要用到基本参数以及和这些参数和方法参数的对应关系

比如Http请求的url、请求体是方法中的第几个参数、请求头是方法中的第几个参数等等

之后在构建Http请求时,就知道请求路径以及方法的第几个参数对应是Http请求的哪部分数据

当调用动态代理方法的时候,Feign就会将上述解析出来的Http请求基本参数和方法入参组装成一个Http请求

然后发送Http请求,获取响应,再根据响应的内容的类型将响应体的内容转换成对应的类型

这就是Feign的大致原理

在整个Feign动态代理生成和调用过程中,需要依靠Feign的一些核心组件来协调完成

如下图所示是Feign的一些核心组件

这些核心组件可以通过Feign.builder()进行替换

由于组件很多,这里我挑几个重要的跟大家讲一讲

1、Contract

前面在说Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数

而这个Contract接口的作用就是用来干解析这件事的

Contract的默认实现是解析Feign自己原生注解的

解析时,会为每个方法生成一个MethodMetadata对象

MethodMetadata就封装了Http请求需要用到基本参数以及这些参数和方法参数的对应关系

SpringCloud在整合Feign的时候,为了让Feign能够识别Spring MVC的注解,所以就自己实现了Contract接口

2、Encoder

通过名字也可以看出来,这个其实用来编码的

具体的作用就是将请求体对应的方法参数序列化成字节数组

Feign默认的Encoder实现只支持请求体对应的方法参数类型为String和字节数组

如果是其它类型,比如说请求体对应的方法参数类型为AddOrderRequest.class类型,此时就无法对AddOrderRequest对象进行序列化

这就导致默认情况下,这个Encoder的实现很难用

于是乎,Spring就实现了Encoder接口

可以将任意请求体对应的方法参数类型对象序列化成字节数组

3、Decoder

Decoder的作用恰恰是跟Encoder相反

Encoder是将请求体对应的方法参数序列化成字节数组

而Decoder其实就是将响应体由字节流反序列化成方法返回值类型的对象

Decoder默认情况下跟Encoder的默认情况是一样的,只支持反序列化成字节数组或者是String

所以,Spring也同样实现了Decoder,扩展它的功能

可以将响应体对应的字节流反序列化成任意返回值类型对象

4、Client

从接口方法的参数和返回值其实可以看出,这其实就是动态代理对象最终用来执行Http请求的组件

默认实现就是通过JDK提供的HttpURLConnection来的

除了这个默认的,Feign还提供了基于HttpClient和OkHttp实现的

在项目中,要想替换默认的实现,只需要引入相应的依赖,在构建Feign.builder()时设置一下就行了

SpringCloud环境底下会根据引入的依赖自动进行设置

除了上述的三个实现,最最重要的当然是属于它基于负载均衡的实现

如下是OpenFeign用来整合Ribbon的核心实现

这个Client会根据服务名,从Ribbon中获取一个服务实例的信息,也就是ip和端口

之后会通过ip和端口向服务实例发送Http请求

5、InvocationHandlerFactory

InvocationHandler我相信大家应该都不陌生

对于JDK动态代理来说,必须得实现InvocationHandler才能创建动态代理

InvocationHandler的invoke方法实现就是动态代理走的核心逻辑

而InvocationHandlerFactory其实就是创建InvocationHandler的工厂

所以,这里就可以猜到,通过InvocationHandlerFactory创建的InvocationHandler应该就是Feign动态代理执行的核心逻辑

InvocationHandlerFactory默认实现是下面这个

SpringCloud环境下默认也是使用它的这个默认实现

所以,我们直接去看看InvocationHandler的实现类FeignInvocationHandler

从实现可以看出,除了Object类的一些方法,最终会调用方法对应的MethodHandler的invoke方法

所以注意注意,这个MethodHandler就封装了Feign执行Http调用的核心逻辑,很重要,后面还会提到

虽然说默认情况下SpringCloud使用是默认实现,最终使用FeignInvocationHandler

但是当其它框架整合SpringCloud生态的时候,为了适配OpenFeign,有时会自己实现InvocationHandler

比如常见的限流熔断框架Hystrix和Sentinel都实现了自己的InvocationHandler

这样就可以对MethodHandler执行前后,也就是Http接口调用前后进行限流降级等操作。

6、RequestInterceptor

RequestInterceptor它其实是一个在发送请求前的一个拦截接口

通过这个接口,在发送Http请求之前再对Http请求的内容进行修改

比如我们可以设置一些接口需要的公共参数,如鉴权token之类的

@Component
public class TokenRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        template.header("token", "token值");
    }

}

7、Retryer

这是一个重试的组件,默认实现如下

默认情况下,最大重试5次

在SpringCloud下,并没有使用上面那个实现,而使用的是下面这个实现

所以,SpringCloud下默认是不会进行重试

小总结

这一节主要是介绍了7个Feign的核心组件以及Spring对应的扩展实现

为了方便你查看,我整理了如下表格

接口 作用 Feign默认实现 Spring实现
Contract 解析方法注解和参数,将Http请求参数和方法参数对应 Contract.Default SpringMvcContract
Encoder 将请求体对应的方法参数序列化成字节数组 Encoder.Default SpringEncoder
Decoder 将响应体的字节流反序列化成方法返回值类型对象 Decoder.Default SpringDecoder
Client 发送Http请求 Client.Default LoadBalancerFeignClient
InvocationHandlerFactory InvocationHandler工厂,动态代理核心逻辑 InvocationHandlerFactory.Default
RequestInterceptor 在发送Http请求之前,再对Http请求的内容进行拦截修改
Retryer 重试组件 Retryer.Default

除了这些之外,还有一些其它组件这里就没有说了

比如日志级别Logger.Level,日志输出Logger,有兴趣的可以自己查看

Feign核心运行原理分析

上一节说了Feign核心组件,这一节我们来讲一讲Feign核心运行原理,主要分为两部分内容:

  • 动态代理生成原理
  • 一次Feign的Http调用执行过程

1、动态代理生成原理

这里我先把上面的Feign原始使用方式的Demo代码再拿过来

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class, "http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

通过Demo可以看出,最后是通过Feign.builder().target(xx)获取到动态代理的

而上述代码执行逻辑如下所示:

最终会调用ReflectiveFeign的newInstance方法来创建动态代理对象

而ReflectiveFeign内部设置了前面提到的一些核心组件

接下我们来看看newInstance方法

这个方法主要就干两件事:

第一件事首先解析接口,构建每个方法对应的MethodHandler

MethodHandler在前面讲InvocationHandlerFactory特地提醒过

动态代理(FeignInvocationHandler)最终会调用MethodHandler来处理Feign的一次Http调用

在解析接口的时候,就会用到前面提到的Contract来解析方法参数和注解,生成MethodMetadata,这里我代码我就不贴了

第二件事通过InvocationHandlerFactory创建InvocationHandler

然后再构建出接口的动态代理对象

ok,到这其实就走完了动态代理的生成过程

所以动态代理生成逻辑很简单,总共也没几行代码,画个图来总结一下

2、一次Feign的Http调用执行过程

前面说了,调用接口动态代理的方式时,通过InvocationHandler(FeignInvocationHandler),最终交给MethodHandler的invoke方法来执行

MethodHandler是一个接口,最终会走到它的实现类SynchronousMethodHandler的invoke方法实现

SynchronousMethodHandler中的属性就是我们前面提到的一些组件

由于整个代码调用执行链路比较长,这里我就不截代码了,有兴趣的可以自己翻翻

不过这里我画了一张图,可以通过这张图来大致分析整个Feign一次Http调用的过程

  • 首先就是前面说的,进入FeignInvocationHandler,找到方法对应的SynchronousMethodHandler,调用invoke方法实现
  • 之后根据MethodMetadata和方法的入参,构造出一个RequestTemplate,RequestTemplate封装了Http请求的参数,在这个过程中,如果有请求体,那么会通过Encoder序列化
  • 然后调用RequestInterceptor,通过RequestInterceptor对RequestTemplate进行拦截扩展,可以对请求数据再进行修改
  • 再然后将RequestTemplate转换成Request,Request其实跟RequestTemplate差不多,也是封装了Http请求的参数
  • 接下来通过Client去根据Request中封装的Http请求参数,发送Http请求,得到响应Response
  • 最后根据Decoder,将响应体反序列化成方法返回值类型对象,返回

这就是Feign一次Http调用的执行过程

如果有设置重试,那么也是在这个阶段生效的

SpringCloud是如何整合Feign的?

SpringCloud在整合Feign的时候,主要是分为两部分

  • 核心组件重新实现,支持更多SpringCloud生态相关的功能
  • 将接口动态代理对象注入到Spring容器中

第一部分核心组件重新实现前面已经都说过了,这里就不再重复了

至于第二部分我们就来好好讲一讲,Spring是如何将接口动态代理对象注入到Spring容器中的

1、将FeignClient接口注册到Spring中

使用OpenFeign时,必须加上@EnableFeignClients

这个注解就是OpenFeign的发动机

@EnableFeignClients最后通过@Import注解导入了一个FeignClientsRegistrar

FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar

所以最终Spring在启动的时候会调用registerBeanDefinitions方法实现

之所以会调用registerBeanDefinitions方法,是@Import注解的作用,不清楚的同学可以看一下扒一扒Bean注入到Spring的那些姿势,你会几种?

最终会走到registerFeignClients这个方法

这个方法虽然比较长,主要是干了下面这个2件事:

第一件事,扫描@EnableFeignClients所在类的包及其子包(如果有指定包就扫指定包),找出所有加了@FeignClient注解的接口,生成一堆BeanDefinition

这个BeanDefinition包含了这个接口的信息等信息

第二件事,将扫描到的这些接口注册到Spring容器中

在注册的时候,并非直接注册接口类型,而是FeignClientFactoryBean类型

好了,到这整个@EnableFeignClients启动过程就结束了

虽然上面写的很长,但是整个@EnableFeignClients其实也就只干了一件核心的事

扫描到所有的加了@FeignClient注解的接口

然后为每个接口生成一个Bean类型为FeignClientFactoryBean的BeanDefinition

最终注册到Spring容器中

2、FeignClientFactoryBean的秘密

上一节说到,每个接口都对应一个class类型为FeignClientFactoryBean的BeanDefinition

如上所示,FeignClientFactoryBean是一个FactoryBean

并且FeignClientFactoryBean的这些属性,是在生成BeanDefinition的时候设置的

并且这个type属性就是代表的接口类型

由于实现了FactoryBean,所以Spring启动过程中,一定为会调用getObject方法获取真正的Bean对象

FactoryBean的作用就不说了,不清楚的小伙伴还是可以看看扒一扒Bean注入到Spring的那些姿势,你会几种?这篇文章

getObject最终会走到getTarget()方法

从如上代码其实可以看出来,最终还是会通过Feign.builder()来创建动态代理对象

只不过不同的是,SpringCloud会替换Feign默认的组件,改成自己实现的

总的来说,Spring是通过FactoryBean的这种方式,将Feign动态代理对象添加到Spring容器中

OpenFeign的各种配置方式以及对应优先级

既然Feign核心组件可以替换,那么在SpringCloud环境下,我们该如何去配置自己的组件呢?

不过在说配置之前,先说一下FeignClient配置隔离操作

在SpringCloud环境下,为了让每个不同的FeignClient接口配置相互隔离

在应用启动的时候,会为每个FeignClient接口创建一个Spring容器,接下来我就把这个容器称为FeignClient容器

这些FeignClient的Spring容器有一个相同的父容器,那就是项目启动时创建的容器

SpringCloud会给每个FeignClient容器添加一个默认的配置类FeignClientsConfiguration配置类

这个配置类就声明了各种Feign的组件

所以,默认情况下,OpenFeign就使用这些配置的组件构建代理对象

知道配置隔离之后,接下来看看具体的几种方式配置以及它们之间的优先级关系

1、通过@EnableFeignClients注解的defaultConfiguration属性配置

举个例子,比如我自己手动声明一个Contract对象,类型为MyContract

public class FeignConfiguration {
    
    @Bean
    public Contract contract(){
        return new MyContract();
    }
    
}

注意注意,这里FeignConfiguration我没加@Configuration注解,原因后面再说

此时配置如下所示:

@EnableFeignClients(defaultConfiguration = FeignConfiguration.class)

之后这个配置类会被加到每个FeignClient容器中,所以这个配置是对所有的FeignClient生效

并且优先级大于默认配置的优先级

比如这个例子就会使得FeignClient使用我声明的MyContract,而不是FeignClientsConfiguration中声明的SpringMvcContract

2、通过@FeignClient注解的configuration属性配置

还以上面的FeignConfiguration配置类举例,可以通过@FeignClient注解配置

@FeignClient(name = "order", configuration = FeignConfiguration.class)

此时这个配置类会被加到自己FeignClient容器中,注意是自己FeignClient容器

所以这种配置的作用范围是自己的这个FeignClient

并且这种配置的优先级是大于@EnableFeignClients注解配置的优先级

3、在项目启动的容器中配置

前面提到,由于所有的FeignClient容器的父容器都是项目启动的容器

所以可以将配置放在这个项目启动的容器中

还以FeignConfiguration为例,加上@Configuration注解,让项目启动的容器的扫描到就成功配置了

这种配置的优先级大于前面提到的所有配置优先级

并且是对所有的FeignClient生效

所以,这就是为什么使用注解配置时为什么配置类不能加@Configuration注解的原因,因为一旦被项目启动的容器扫描到,这个配置就会作用于所有的FeignClient,并且优先级是最高的,就会导致你其它的配置失效,当然你也可以加@Configuration注解,但是一定不能被项目启动的容器扫到

4、配置文件

除了上面3种编码方式配置,OpenFeign也是支持通过配置文件的方式进行配置

并且也同时支持对所有FeignClient生效和对单独某个FeignClient生效

对所有FeignClient生效配置:

feign:
  client:
    config:
      default: # default 代表对全局生效
        contract: com.sanyou.feign.MyContract

对单独某个FeignClient生效配置:

feign:
  client:
    config:
      order: # 具体的服务名
        contract: com.sanyou.feign.MyContract

在默认情况下,这种配置文件方式优先级最高

但是如果你在配置文件中将配置项feign.client.default-to-properties设置成false的话,配置文件的方式优先级就是最低了

feign:
  client:
    default-to-properties: false

小总结

这一节,总共总结了4种配置OpenFeign的方式以及它们优先级和作用范围

画张图来总结一下

如果你在具体使用的时候,还是遇到了一些优先级的问题,可以debug这部分源码,看看到底生效的是哪个配置

总结

到这,总算讲完了OpenFeign的核心架构原理了

这又是一篇洋洋洒洒的万字长文

由于OpenFeign它只是一个框架,并没有什么复杂的机制

所以整篇文章还是更多偏向源码方面

不知道你看起来感觉如何

如果你感觉还不错,欢迎点赞、在看、收藏、转发分享给其他需要的人

你的支持就是我更新的最大动力,感谢感谢!

更多SpringCloud系列的文章,可以在公众号后台菜单栏中查看。

好了,本文就讲到这里,让我们下期再见,拜拜!

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

两万字盘点那些被玩烂了的设计模式

万字+20张图探秘Nacos注册中心核心实现原理

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

扫码或者搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

新来个架构师,用48张图把OpenFeign原理讲的炉火纯青~~的更多相关文章

  1. 架构师成长之路2.1-PXE+Kickstart原理

    点击返回架构师成长之路 架构师成长之路2.1-PXE+Kickstart原理 PXE+Kickstart 主要用于在公司内网批量安装新服务器系统,这极大地简化了用光盘重复安装Linux操作系统的过程, ...

  2. Java架构师技能发展脑图

    图中还有好多东西不会,先把图保存好,逐项击破

  3. 阿里P7架构师详解微服务链路追踪原理

    背景介绍 在微服务横行的时代,服务化思维逐渐成为了程序员的基本思维模式,但是,由于绝大部分项目只是一味地增加服务,并没有对其妥善管理,当接口出现问题时,很难从错综复杂的服务调用网络中找到问题根源,从而 ...

  4. 一张图解释NIO原理

  5. 专访 | 新浪架构师:0-5年Java工程师的职业规划如何做?

    经历了2018年末的阵痛,大家都积攒着一股暗劲蠢蠢欲动. 3月初即将迎来2019年互联网行业换工作的大潮,技术工程师的升级换位对于一家互联网公司来说无疑是命脉般的存在——技术强则公司强! 如何做一个抢 ...

  6. 《架构师杂志》评述:Scott Guthrie

    发布日期: 2007-03-29 | 更新日期: 2007-03-29   Scott Guthrie 是 Microsoft 开发事业部的总经理.他领导着负责构建 CLR(公共语言运行库).ASP. ...

  7. 阿里Java架构师分享自己的成长经历,教你如何快速成长为架构师

    架构师是公司的“金领”,很少需要考虑生存的问题,从而有更多的精力思考关键技术,形成“强者愈强”的良性循环.当然,冰冻三尺非一日之寒,成为一名合格的架构师是一个漫长的积累过程.对于大部分的软件开发人员来 ...

  8. 个人总结的一个中高级Java开发工程师或架构师需要掌握哪几点!

    今天,我来唠叨几句~~ 知识改变命运,对于Java程序员来说,技术不断更新,只有及时充电,才能不被市场淘汰.今天为大家分享Java程序员学习的6个小技巧. 1.一定要看书 现在学习Java变得比以前容 ...

  9. 架构师-盛大许式伟VS金山张宴

    许式伟:作为系统架构师,您一般会从哪些方面来保证网站的高可用性(降低故障时间)? 张宴:很多因素都会导致网站发生故障,从而影响网站的高可用性,比如服务器硬件故障.软件系统故障.IDC机房故障.程序上线 ...

  10. 微软架构师解读Windows Server 2008 R2新特性

    目前众多企业都开始为自己寻找一个更加适合自身发展的服务器操作平台.微软的Windows Server 2008 R2就是可以为大家解决服务器平台问题.微软最新的服务器平台Windows Server ...

随机推荐

  1. 使用rpm打包nacos然后部署为systemd服务开机自动启动的方法

    背景 Nacos是阿里开源的服务注册组件,能够简单的实现微服务的注册与发现机制. 但是官方并没有提供 sytemd的服务脚本, 也没有提供rpm包的方式. 公司里面使用 nacos的场景越来越多, 部 ...

  2. 取消ts校验的注释

    常用的有以下注释 单行忽略 // @ts-ignore 忽略全文:如果你使用这样,需要放在ts的最顶部哈. // @ts-nocheck 如下 <script lang="ts&quo ...

  3. echarts的初始化和销毁dispose

    容器节点被销毁以及被重建时 假设页面中存在多个标签页, 每个标签页都包含一些图表. 当选中一个标签页的时候,其他标签页的内容在 DOM 中被移除了. 这样,当用户再选中这些标签页的时候,就会发现图表& ...

  4. Gin 框架介绍与快速入门

    Gin 框架介绍与快速入门 目录 Gin 框架介绍与快速入门 一.Gin框架介绍 1. 快速和轻量级 2. 路由和中间件 3. JSON解析 4. 支持插件 5. Gin相关文档 二.基本使用 1.安 ...

  5. P7900 [COCI2006-2007#2] SJECIŠTA_题解

    [COCI2006-2007#2] SJECIŠTA_题解 rt 我们来看一下题目描述 考虑一个有 \(n\) 个顶点的凸多边形,且这个多边形没有任何三个(或以上) 的对角线交于一点. 这句话什么意思 ...

  6. 从零开始配置vim(30)——DAP的其他配置

    很抱歉这么久才来更新这一系列,主要是来新公司还在试用期,我希望在试用期干出点事来,所以摸鱼的时间就少了.加上前面自己阳了休息了一段时间.在想起来更新就过去一个多月了.废话不多说了,让我们开始进入正题. ...

  7. 书写自动智慧文本分类器的开发与应用:支持多分类、多标签分类、多层级分类和Kmeans聚类

    书写自动智慧文本分类器的开发与应用:支持多分类.多标签分类.多层级分类和Kmeans聚类 文本分类器,提供多种文本分类和聚类算法,支持句子和文档级的文本分类任务,支持二分类.多分类.多标签分类.多层级 ...

  8. 从零搭建Vue3 + Typescript + Pinia + Vite + Tailwind CSS + Element Plus开发脚手架

    项目代码以上传至码云,项目地址:https://gitee.com/breezefaith/vue-ts-scaffold 目录 前言 脚手架技术栈简介 vue3 TypeScript Pinia T ...

  9. 制作包含最新更新的Windows 10 LTSC 2021 ISO

    介绍 在制作桌面云windows 模板的时候,一般需要安装最新的更新.更新安装过程非常耗时,并且安装更新会导致桌面模板的磁盘空间膨胀.制作出的模板会占用很大的磁盘空间.如果不安装更新,模板大小约5G. ...

  10. ESP8266的AT指令模块程序

    最新代码可点击下载:ESP8266 模块代码 和以下代码实现方式不一致,更加自由可控 本段代码只是测试了esp8266作为服务器端使用,没有测试作为客户端使用. 没有超长延时等待或死循环等待AT指令反 ...