Retrofit原理_Retrofit

Retrofit原理_Retrofit一:Retrofit是什么?准确来说,Retrofit是一个RESTful的HTTP网络请求框架的封装。原因:网络请求的工作本质上是OkHttp完成,而Retrofit仅负责网络请求接口的封装我们先来看看下面这个图:上图说明了如下几点:1.App应用程序通过Retrofit请求网络,实际上是使用Retrofit接口层封装请求参数、Header、Url等信息,之后由OkHttp完成后续的请求操作。2.在服务端返回数据之后,OkHttp将原始的结果交给

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

一:Retrofit是什么?
准确来说,Retrofit 是一个 RESTful 的 HTTP 网络请求框架的封装。
原因:网络请求的工作本质上是 OkHttp 完成,而 Retrofit 仅负责 网络请求接口的封装
我们先来看看下面这个图:

Retrofit原理_Retrofit

上图说明了如下几点:
1. App应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之
后由 OkHttp 完成后续的请求操作。
2. 在服务端返回数据之后,OkHttp 将原始的结果交给 Retrofit,Retrofit根据用户的需求对结果进行解析。
所以,网络请求的本质仍旧是OkHttp完成的,retrofit只是帮使用者来进行工作简化的,比如配置网络,处理数据等
工作,提高这一系列操作的复用性。这也就是网上流行的一个不太准确的总结:okhttp是瑞士军刀,retrofit则是讲这
边瑞士军刀包装从了一个非常好用的指甲钳。

二: Retrofit 对Okhttp做了什么
Retrofit并没有改变网络请求的本质,也无需改变,因为Okhttp已经足够强大,Retrofit的封装可以说是很强大,里
面涉及到一堆的设计模式,可以通过注解直接配置请求,可以使用不同的http客户端,虽然默认是用http ,可以使用
不同Json Converter 来序列化数据,同时提供对RxJava的支持,使用Retrofit + OkHttp + RxJava 可以说是目前比较
潮的一套框架,但是需要有比较高的门槛。
下面我们来对比一下OKhttp网络请求和 retrofit网络请求的区别。

2.1. Okhttp请求总结
[大家先看下面okhttp请求的样例代码]

private void testOkHttp() throws IOException {
    //Step1
    final OkHttpClient client = new OkHttpClient();
    //Step2
    final Request request = new Request.Builder()
     .url("https://www.google.com.hk").build();
    //Step3
    Call call = client.newCall(request);
    //step4 发送网络请求,获取数据,进行后续处理
    call.enqueue(new Callback() {
      @Override
      public void onFailure(Call call, IOException e) {
     }
      @Override
     public void onResponse(Call call, Response response) throws IOException {
          Log.i(TAG,response.toString());
          Log.i(TAG,response.body().string());
     }
   });
 }

Jetbrains全家桶1年46,售后保障稳定

「解析一下上面的代码」
Step1:创建HttpClient对象,也就是构建一个网络类型的实例,一般会将所有的网络请求使用同一个单例对象。
Step2:构建Request,也就是构建一个具体的网络请求对象,具体的请求url,请求头,请求体等等。
Step3:构建请求Call,也就是将具体的网络请求与执行请求的实体进行绑定,形成一个具体的正式的可执行实体。
Step4: 后面就进行网络请求了,然后处理网络请求的数据了。
「总结一下」
OKhttp的意义:OkHttp 是基于Http协议封装的一套请求客户端,虽然它也可以开线程,但根本上它更偏向真正的
请求,跟HttpClient, HttpUrlConnection的职责是一样的。
Okhttp的职责:OkHttp主要负责socket部分的优化,比如多路复用,buffer缓存,数据压缩等等。
Okhttp给用户留下的问题:
1)用户网络请求的接口配置繁琐,尤其是需要配置请求body,请求头,参数的时候;
2)数据解析过程需要用户手动拿到responsbody进行解析,不能复用;
3)无法适配自动进行线程的切换。
那么这几个问题谁来解决? 对,retrofit!

2.2 Retrofit请求总结
【Retrofit 进行网络请求的流程样板代码】

//step1
Retrofit retrofit = new Retrofit.Builder()
       .baseUrl("https://www.wanandroid.com/")
       .addConverterFactory(GsonConverterFactory.create(new Gson()))
       .build();
//step2
ISharedListService sharedListService =  retrofit.create(ISharedListService.class);
//step3
Call<SharedListBean> sharedListCall = sharedListService.getSharedList(2,1);
//step4
sharedListCall.enqueue(new Callback<SharedListBean>() {
  @Override
  public void onResponse(Call<SharedListBean> call, Response<SharedListBean> response{
    if (response.isSuccessful()) {
        System.out.println(response.body().toString());
      }
    }
    @Override
    public void onFailure(Call<SharedListBean> call, Throwable t) {
     t.printStackTrace();
   }
});

Step1: 创建retrofit对象, 构建一个网络请求的载体对象,和okhttp构建OkhttpClient对象有一样的意义,只不过
retrofit在build的时候有非常多的初始化内容,这些内容可以为后面网络请求提供准备,如准备 现成转换Executor,
Gson convert,RxJavaCallAdapter。
Step2:Retrofit的精髓,为统一配置网络请求完成动态代理的设置。
Step3:构建具体网络请求对象Request(service),在这个阶段要完成的任务:1)将接口中的注解翻译成对应的
参数;2)确定网络请求接口的返回值response类型以及对应的转换器;3)讲Okhttp的Request封装成为Retrofit的
OKhttpCall。总结来说,就是根据请求service 的Interface来封装Okhttp请求Request。
Step4:后面就进行网络请求了,然后处理网络请求的数据了
2.3「总结一下」
Retrofit主要负责应用层面的封装,就是说主要面向开发者,方便使用,比如请求参数,响应数据的处理,错误处理
等等。
Retrofit封装了具体的请求,线程切换以及数据转换。
网上一般都推荐RxJava+Retrofit+OkHttp框架,Retrofit负责请求的数据和请求的结果,使用接口的方式呈现,
OkHttp负责请求的过程,RxJava负责异步,各种线程之间的切换,用起来非常便利。
小结:
通过下图,让我们来总结一下,retrofit是如何来封装okhttp请求的。

Retrofit原理_Retrofit

大体的网络流程是一致的,毕竟都是通过okhttp进行网络请求。主要的步骤都是:创建网络请求实体client->构建真
正的网络请求-> 将网络请求方案与真正的网络请求实体结合构成一个请求Call->执行网络请求->处理返回数据->处理
Android 平台的线程问题。
在上图中,我们看到的对比最大的区别是什么?
0)okhttp创建的是OkhttpClient,然而retrofit创建的是 Retrofit实例
1)构建蓝色的Requet的方案,retrofit是通过注解来进行的适配
2)配置Call的过程中,retrofit是利用Adapter适配的Okhttp 的Call
3)相对okhttp,retrofit会对responseBody进行 自动的Gson解析
4)相对okhttp,retrofit会自动的完成线程的切换。
那么retrofit是如何完成这几点的封装的呢?请看下面的文章

三: Retrofit的构建过程
Retrofit通过build模式来生成一个Retrofit对象,通过代码我们知道,Retrofit默认会使用OkHttp来发送网络请求,当
然,我们也可以自己定制。
「看下面的代码」

public Retrofit build() {
   if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }
  // 代码1
  //默认只支持okhttp请求,不支持 httpurlconnection 和 httpclient
   okhttp3.Call.Factory callFactory = this.callFactory;
   if (callFactory == null) { 
    callFactory = new OkHttpClient();
  }
   // 代码2
  // 添加一个线程管理 Executor,okhttp 切换线程需要手动操作,但是retrofit
   // 不需要,就是因为这个Executor 的存在,其实他是handler
   Executor callbackExecutor = this.callbackExecutor;
   if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }
   //代码3    
  // Make a defensive copy of the adapters and add the default Call adapter.
   List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
   adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
   // Make a defensive copy of the converters.
   List<Converter.Factory> converterFactories = new ArrayList<>
(this.converterFactories);
   return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
 unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}

1:在代码 1 处
初始化 构建call 的工厂,但是这个地方直接就是使用了 okhttp的call,没有使用到工厂设计模式去添加构建
httpclient 或者 httpurlconnection的方法来创建 call,说明retrofit 已经铁下心只支持okhttp创建call请求了。
那么call 是什么的抽象呢?看下面的代码,okhttp请求的代码

OkHttpClient client = new OkHttpClient.Builder().
        readTimeout(5, TimeUnit.SECONDS).build();
Request request = new Request.Builder().
        url("http://www.baidu.com").get().build();
okhttp3.Call call = client.newCall(request);
call.enqueue(new okhttp3.Callback() )...

OkHttpClient是 http 请求的载体包含socket等可以复用的对象,协议配置等等一切。
Request 创建的是一个具体的有url,header,等请求信息的一个网络请求,表示这个具体的请求。
Call 通往请求的,去执行请求的整个过程的一个抽象。也是进行网络请求的最终接口。
所以,此次调用,目的就是创建了一个OkHttpClient,换句话说,这里的调用就是生产 Okhttp网络请求需要的请
求Call的,以备后面进行真正的网络请求。
2:在代码2处
网络请求需要在子线程中执行,那么就需要线程管理,所以就有了代码2的存在,深入源码后发现,这个地方就是运
用handler进行线程切换,当网络请求回来了进行线程切换,可以看下面的源码

static final class Android extends Platform {
  Android() {
   super(Build.VERSION.SDK_INT >= 24);
 }
  @Override public Executor defaultCallbackExecutor() {
   return new MainThreadExecutor();
 }
  static class MainThreadExecutor implements Executor {
   private final Handler handler = new Handler(Looper.getMainLooper());
   @Override public void execute(Runnable r) {
    handler.post(r);
  }
 }
}

所以,此次调用,目的是构建一个用handler封装的Executor,以备后面进行网络请求成功后的线程切换用
3:在代码3处
设置默认CallAdapterFactory

在此添加的CallAdapterFactory属于系统默认的,当然,我们可以添加RxJavaCallAdapterFactory。默认的
CallAdapterFactory是 ExecutorCallAdapterFactory 类的对象,在Platform.java Class里面可以梳理出来

defaultCallAdapterFactory(Executor callbackExecutor) {
   return new ExecutorCallAdapterFactory(callbackExecutor);
}

所以构建的Retrofit都是用于进行后面请求的需要的内容的一个准备工作。也就是封装Okhttp需要的准备工作。

四: Retrofit构建 IxxxService 对象的过程
(Retrofit.create())
看下面的代码:

ISharedListService sharedListService =  retrofit.create(ISharedListService.class);
Call<SharedListBean> sharedListCall = sharedListService.getSharedList(2,1);

上面两行代码需要连起来才能正确的被阅读,因为,在create里面是使用了动态代理的技术方案,而动态代理是运行
时生效的,当我们看到看到create的时候只
create的代码如下:

public <T> T create(final Class<T> service) {
  Utils.validateServiceInterface(service);
  if (validateEagerly) {
   eagerlyValidateMethods(service);
 }
  return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
// 通过动态代理的方式生成具体的网络请求实体对象
    new InvocationHandler() { // 统一处理所有的请求方法
     private final Platform platform = Platform.get();
     @Override
     public Object invoke(Object proxy, Method method, @Nullable Object[] args)
       throws Throwable {
      // If the method is a method from Object then defer to normal invocation.
      if (method.getDeclaringClass() == Object.class) {
       return method.invoke(this, args);
     }
      if (platform.isDefaultMethod(method)) {
       return platform.invokeDefaultMethod(method, service, proxy, args);
     }
      // 根据方法生成一个ServiceMethod对象(内部会将生成的ServiceMethod放入在缓存中,
      //如果已经生成过则直接从缓存中获取)
      ServiceMethod<Object, Object> serviceMethod =
       (ServiceMethod<Object, Object>) loadServiceMethod(method); 
      // 根据ServiceMethod对象和请求参数生成一个OkHttpCall对象,这个OkHttpCall能够
      //调用OkHttp的接口发起网络请求
     OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
      // 调用serviceMethod的callAdapter的adapt方法,并传入okHttpCall,返回一个对象,
      //这个的目的主要是为了适配返回类型,其内部会对OkhttpCall对象进行包装
      return serviceMethod.callAdapter.adapt(okHttpCall);
    }
   });
}

1)Retrofit的create方法通过动态代理的模式,生成了实现了具体的网络请求接口的对象,并在InvocationHandler
的invoke方法中统一处理网络请求接口实体对象的方法;2)invoke方法会通过方法构造一个ServiceMethod对象,
并将其放入缓存中;3)然后根据ServiceMethod对象和网络请求的参数args去构造一个OkHttpCall对象;4)最后
调用serviceMethod的callAdapter的adapt方法,传入将OkHttpCall对象,callAdapter的目的主要是为了适配
OkHttpCall对象,其内部会对OkHttpCall对象进行包装,生成对应返回类型的对象。

4.1 动态代理
动态代理的原理主要是在运行时动态生成代理类,然后根据代理类生成一个代理对象,在这个代理对象的方法中中又
会调用InvocationHandler的invoke来转发对方法的处理。
那么大家一定要关注一个细节,我们在使用retrofit的时候,对每一个网络请求的产生都必须要先调用create函数,也
就是意味着,我们的请求都是通过代理类来进行处理的。但是代理类具体的代理行为是发生在哪里呢?很显然,他并
不是在create函数执行的时候,而是在使用具体的接口创建具体网络请求Call的时候,当调用具体网络请求Call的代
码示例如下:

Call<SharedListBean> sharedListCall = sharedListService.getSharedList(2,1);

在执行上面的代码的时候,它会走代理设计模式的InvocationHandler里面的invoke()函数,也就是所有的网络请求
在创建具体网络请求Call的时候,都会走Invoke,从而我们可以在invoke里面进行各种行为的统一处理,比如:接口
的统一配置,也就是注解的解读和网络请求参数的拼接。

4.2 ServiceMethod
大家先看看loadServiceMethod方法

ServiceMethod loadServiceMethod(Method method) {
 ServiceMethod result;
 synchronized (serviceMethodCache) {
// 为什么会缓存?为了效率
  result = serviceMethodCache.get(method);
  if (result == null) {
   result = new ServiceMethod.Builder(this, method).build();
   serviceMethodCache.put(method, result);
  }
 }
 return result;
}

loadServiceMethod首先会从缓存中获取ServiceMethod对象,如果没有,则通过Method和Retrofit对象构造一个
ServiceMethod对象,并将其放入缓存中。
[一个细节]

每一个method 都有一个自己的ServiceMethod,这就意味着ServiceMethod是属于函数的,而不是类的。也就是我
们定义的网络访问接口类,在接口类里面的每一个函数都会在反射阶段形成自己的serviceMethod。那么
ServiceMethod是什么呢?
ServiceMethod其实是用来存储一次网络请求的基本信息的,比如Host、URL、请求方法等,同时ServiceMethod还
会存储用来适配OkHttpCall对象的CallAdpater。ServiceMethod的build方法会解读传入的Method,首先
ServiceMethod会在CallAdpaterFactory列表中寻找合适的CallAdapter来包装OkHttpCall对象,这一步主要是根据
Method的返回参数来匹配的,比如如果方法的返回参数是Call对象,那么ServiceMethod就会使用默认的
CallAdpaterFactory来生成CallAdpater,而如果返回对象是RxJava的Obserable对象,则会使用
RxJavaCallAdapterFactory提供的CallAdpater。然后build方法会解读Method的注解,来获得注解上配置的网络请
求信息,比如请求方法、URL、Header等

public ServiceMethod build() {
   callAdapter = createCallAdapter(); // 查找能够适配返回类型的CallAdpater
   responseType = callAdapter.responseType();
   if (responseType == Response.class || responseType == okhttp3.Response.class) {
    throw methodError("'"
      + Utils.getRawType(responseType).getName()
      + "' is not a valid response body type. Did you mean ResponseBody?");
  }
   //设置请求的数据适配器converter
   responseConverter = createResponseConverter();
   // 解读方法的注解
   for (Annotation annotation : methodAnnotations) {
    parseMethodAnnotation(annotation);
  }
   if (httpMethod == null) {
    throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
  }
   if (!hasBody) {
    if (isMultipart) {
     throw methodError(
       "Multipart can only be specified on HTTP methods with request body (e.g.,
@POST).");
   }
    if (isFormEncoded) {
     throw methodError("FormUrlEncoded can only be specified on HTTP methods with "
       + "request body (e.g., @POST).");
   }
  }
   int parameterCount = parameterAnnotationsArray.length;
   parameterHandlers = new ParameterHandler<?>[parameterCount];
   for (int p = 0; p < parameterCount; p++) {
    Type parameterType = parameterTypes[p];
    if (Utils.hasUnresolvableType(parameterType)) {
     throw parameterError(p, "Parameter type must not include a type variable or
wildcard: %s",
       parameterType);
  }
    Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
    if (parameterAnnotations == null) {
     throw parameterError(p, "No Retrofit annotation found.");
   }
    parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
  }
   if (relativeUrl == null && !gotUrl) {
    throw methodError("Missing either @%s URL or @Url parameter.", httpMethod);
  }
   if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
    throw methodError("Non-body HTTP method cannot contain @Body.");
  }
   if (isFormEncoded && !gotField) {
    throw methodError("Form-encoded method must contain at least one @Field.");
  }
   if (isMultipart && !gotPart) {
    throw methodError("Multipart method must contain at least one @Part.");
  }
   return new ServiceMethod<>(this);
 }

4.3 okHttpCall

OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);

我们知道,ServiceMethod封装了网络请求的基本信息,比如Host、URL等,我们根据ServiceMethod和请求参数
args就可以确定本次网络请求的所有信息了,OkHttpCall主要是将这些信息封装起来,并调用OkHttp的接口去发送
网络请求,这里,我们就将OkHttpCall看成是一个处理网络请求的类即可。
4.4 callAdapter
在retrofit中,invoke() 里面的最后一行代码,

return serviceMethod.callAdapter.adapt(okHttpCall);

那么我们可以设想一下为什么Retrofit还要设计一个CallAdapter接口呢?
先来说一个客观事实,Retrofit真正使用Okhttp进行网络请求的就是OkHttpCall这个类
曾提到了Call对象的创建是通过是通过ServiceMethod.adapt()完成的,这里在看看该方法的源码:
ServiceMethod.adapt()方法:

T adapt(Call<R> call) {
 return callAdapter.adapt(call);
}123

通过上述源码可知,最终Call对象是调用CallAdapter.adapt(Call)方法创建的,那么CallAdapter及具体的Call对象又
是如何生成的呢?
如果没有这个适配器模式,会出现什么情况?
很明显,没有适配器的时候此时我们网络请求的返回接口只能直接返回OkHttpCall,那么所有的网络请求都是用
okhttpCall进行,这样的话就失去了retrofit 封装网络请求call的意义了,譬如:rxjavaCallAdapterFactory 就没有办
法支持。
如果我们想要返回的不是Call呢?比如RxJava的Observable,这种情况下该 怎么办呢?
适配器模式在此发挥了其应用的作用!!!
将网络请求的核心类OkHttpCall进行适配,你需要什么类型的数据就通过适配器适配,返回适配后的对象就是了。
正是这种CallApdate接口的设计,使得我们在使用Retrofit的时候可以自定义我们想要的返回类型。此接口的设计也
为RxJava的扩展使用做了很好的基础!!!
更多关于CallAdapter在retrofit中的实现细节,大家可以看我的其他的文章。

五: Retrofit网络请求操作小结
一般的Retrofit网络请求的操作是指 Call.excute() & Call.enqueue()的过程,这个过程才是真正的网络请求,因为,
网络配置,请求地址配置,Call适配,网络请求 requestBody &返回值responseBody转化适配准备工作都已经完成。

sharedListCall.enqueue(new Callback()...);
sharedListCall.excute();

在进行网络请求的执行的时候,基本上就是调用,ServiceMethod中设置的各个内容如 :
1)OkHttpCall进行网络请求,实则是进行okhttp的网络请求;
2)利用 converter进行网络请求数据的转换,一般是Gson();
3)利用 rxjava observable构建 rxjava类型的责任链访问方案,并进行线程切换;
4) 如果没有rxjava的添加,那么就使用默认的callAdapter里面的callbackExecutor进行线程的切换
, 进行网络请求.
整体网络请求的流程图请看下图:

Retrofit原理_Retrofit

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/226922.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 抽象工厂设计模式例题_什么是抽象工厂模式

    抽象工厂设计模式例题_什么是抽象工厂模式定义:为创建一组相关或相互依赖的对象提供一个接口,而且无需指定他们的具体类。类型:创建类模式类图:抽象工厂模式与工厂方法模式的区别       抽象工厂模式是工厂方法模式的升级版本,他用来创建一组相关或者相互依赖的对象。他与工厂方法模式的区别就在于,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式则是针对的多个产品等级结构。在编程中,通常一个产品结构,表现为一个接口或者抽

    2022年10月29日
  • load average 计算「建议收藏」

    load average 计算「建议收藏」平均负载是指单位时间内,系统处于可运行状态和不可中断状态的平均活跃进程数。对于Ubuntu获取cpu数和cpu核数more/proc/cpuinfo|grep”physicalid”|uniq|wc-l#=>1more/proc/cpuinfo|grep”physicalid”|grep”0″|wc-l#=>16则1*…

  • LeetCode1两数之和

    LeetCode1两数之和题目:给定一个整数数列,找出其中和为特定值的那两个数。你可以假设每个输入都只会有一种答案,同样的元素不能被重用。示例:给定nums=[2,7,11,15],target=9因为nums[0]+nums[1]=2+7=9所以返回[0,1]分析:可以直接遍历两遍数组,第一遍用target-nums[i],第二遍找nums数组中是否存在target-num…

  • dirsearch使用_elasticsearch fielddata

    dirsearch使用_elasticsearch fielddatadirsearch配置1.下载地址:https://github.com/maurosoria/dirsearch/archive/master.zip2.安装3.7以上版本python或者gitclonehttps://github.com/maurosoria/dirsearch.gitcddirsearchpip3install-rrequirements.txtpython3dirsearch.py-u<URL>-e<EXTENSI

  • matlab之simulink仿真入门

    matlab之simulink仿真入门MatlabSimulink仿真工具的应用****Simulink是一个用来对动态系统进行建模、仿真和分析的软件包。使用Simulink来建模、分析和仿真各种动态系统(包括连续系统、离散系统和混合系统),将是一件非常轻松的事情。它提供了一种图形化的交互环境,只需用鼠标拖动的方法便能迅速地建立起系统框图模型,甚至不需要编写一行代码。由于Simulink具有强大的功能与友好的用户界面,因此它已

  • 图片打水印 缩放 和一个输入流的转换

    图片打水印 缩放 和一个输入流的转换

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号