Retrofit源碼閱讀

動態代理在 Android 中的應用:Retrofit 源碼解析

在之前的文章 《Andriod 網絡框架 OkHttp 源碼解析》 中我們分析了 OkHttp 的源代碼。現在我們就來分析一下 OkHttp 的兄弟框架 Retrofit。關于 Retrofit 的注解的使用,可以參考其官方文檔:https://square.github.io/retrofit/。

Retrofit 也是 Square 發布的一個開源的庫,它是一個類型安全的 Http 客戶端,適用于 Android 和 Java。本質上,Retrofit 使用了 Java 的動態代理,內部使用 OkHttp 來進行網絡訪問,并且可以通過指定 “請求適配器” 和 “類型轉換器” 來完成:請求的適配,方法參數到 OkHttp 請求的轉換,以及響應到 Java 類型的轉換。

1、基本使用

Retrofit 設計的一個好的地方就是它把我們上面提到的 “請求適配器” 和 “類型轉換器” 使用策略模式解耦出來。用戶可以根據自己的需求通過實現指定的接口來自定義自己的類型轉換器。所以,當我們使用 Gson 進行轉換和 RxJava2 進行適配的時候,就需要指定下面三個依賴:

    api 'com.squareup.retrofit2:retrofit:2.4.0'api 'com.squareup.retrofit2:converter-gson:2.4.0'api 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

然后,我們需要根據自己的 API 接口的信息,在代碼里用一個接口來對該 API 進行聲明:

public interface WXInfoService {@GET("/sns/userinfo")Observable<WXUserInfo> getWXUserInfo(@Query("access_token") String accessToken, @Query("openid") String openId);
}

這里的 WXUserInfo 是由該 API 接口返回的 Json 生成的 Java 對象。然后,我們可以像下面這樣獲取一個該接口的代理對象:

	WXInfoService wXInfoService = new Retrofit.Builder().baseUrl("https://api.weixin.qq.com/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(okHttpClient).build().create(WXInfoService.class);

然后,我們就可以使用該對象并調用其方法來獲取接口返回的信息了:

    Disposable disposable = wxInfoService.getWXUserInfo(accessToken, openId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(wxUserInfo -> { /*...拿到結果之后進行處理...*/ });

上面我們只使用了 Retrofit 最基礎的 GET 接口。當然,Retrofit 本身的功能遠比這要豐富得多,關于其更多的使用,可以參考其官方的文檔。

2、動態代理:魔力發生的地方

上面我們使用 Retrofit 進行網絡請求,實際其內部使用 OkHttp 來完成網絡請求的。僅定義了一個接口并調用了該接口的方法,就拿到了請求的結果,這看上去非常簡潔,而這其中的功不可沒的就是動態代理

當我們使用 Retrofit.Buildercreate() 方法獲取一個 WXInfoService 實例的時候,實際返回的是經過代理之后的對象。該方法內部會調用 Proxy 的靜態方法 newProxyInstance() 來得到一個代理之后的實例。為了說明這個方法的作用,我們寫了一個例子:

    public static void main(String...args) {Service service = getProxy(Service.class);String aJson = service.getAInfo();System.out.println(aJson);String bJson = service.getBInfo();System.out.println(bJson);}private static <T> T getProxy(final Class<T> service) {InvocationHandler h = (proxy, method, args) -> {String json = "{}";if (method.getName().equals("getAInfo")) {json = "{A請求的結果}";} else if (method.getName().equals("getBInfo")) {json = "{B請求的結果}";}return json;};return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service}, h);}
該程序的輸出結果:{A請求的結果}{B請求的結果}

在上面的這個例子中,我們先使用 getProxy() 獲取一個代理之后的實例,然后依次調用它的 getAInfo()getBInfo() 方法,來模擬調用 A 接口和 B 接口的情形,并依次得到了 A 請求的結果和 B 請求的結果。

上面的效果近似于我們使用 Retrofit 訪問接口的過程。為了說明這個過程中發生了什么,我們需要先了解一下這里的 newProxyInstance() 方法:

    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {// ...}

該方法接收三個參數:

  1. 第一個是類加載器;
  2. 第二個是接口的 Class 類型;
  3. 第三個是一個處理器,你可以將其看作一個用于回調的接口。當我們的代理實例觸發了某個方法的時候,就會觸發該回調接口的方法。

InvocationHandler 是一個接口,它內部定義了一個方法如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

該方法也接收三個參數,第一個是觸發該方法的代理實例;第二個是觸發的方法;第三個是觸發的方法的參數。invoke() 方法的返回結果會作為代理類的方法執行的結果。

所以,當了解了 newProxyInstance() 方法的定義之后,我們可以做如下總結:當我們使用 newProxyInstance() 方法獲取了一個代理實例 service 并調用其 getAInfo() 方法之后,該方法的信息和參數信息會分別通過 method 和 args 傳入到 h 的 invoke() 中。所以,最終的效果就是,當我們調用 service 的 getAInfo() 時候會觸發 h 的 invoke()。在 invoke() 方法中我們根據 method 得知觸發的方法是 getAInfo。于是,我們把它對應的請求從 invoke() 方法中返回,并作為 service.getAInfo() 的返回結果。

所以,我們可以總結 Retrofit 的大致工作流程:當我們獲取了接口的代理實例,并調用它的 getWXUserInfo() 方法之后,該方法的請求參數會傳遞到代理類的 InvocationHandler.invoke() 方法中。然后在該方法中,我們將這些信息轉換成 OkHttp 的 Request 并使用 OkHttp 進行訪問。從網絡中拿到結果之后,我們使用 “轉換器” 將響應轉換成接口指定的 Java 類型。

上面是 Retrofit 請求處理的基本流程,下面我們看一下 Retrofit 的代理方法內部究竟發生了什么。

3、Retrofit 的源碼解析

3.1 創建 Retrofit

根據上面的例子,當使用 Retrofit 的時候,首先我們需要使用 Retrofit 的構建者來創建 Retrofit 的實例。這里的構建者有幾個重要的方法需要提及一下:

3.1.1 addConverterFactory 方法

該方法用來向 Retrofit 中添加一個 Converter.FactoryConverter.Factory,顧名思義是一種工廠模式。它是一個接口需要,實現兩個重要的方法。每個方法需要返回一個轉換器:某種數據類型到請求體的轉換器,響應體到我們需要的數據類型的轉換器。當我們使用 Gson 來完成這個轉換,那么我們就需要使用 GsonConverterFactory.create() 來得到一個適用于 Gson 的 Converter.Factory

public Builder addConverterFactory(Converter.Factory factory) {converterFactories.add(checkNotNull(factory, "factory == null"));return this;
}
3.1.2 addCallAdapterFactory 方法

CallAdapter.Factory 用于獲取 CallAdapter 對象, CallAdapter 對象用于把原生的 OkHttp 的 Call 轉換成我們指定的請求類型。比如,上面的例子中,我們用來將其轉換成 Observable<WXUserInfo>。下面是該方法的定義:

public Builder addCallAdapterFactory(CallAdapter.Factory factory) {callAdapterFactories.add(checkNotNull(factory, "factory == null"));return this;
}
3.1.3 build 方法

當根據用戶的自定義設置完了參數之后,就可以調用 build() 方法,來獲取一個 Retrofit 的實例。在該方法中會將上述方法傳入的 “適配器” 和 “轉換器” 添加到各自的列表中,然后 new 出一個 Retrofit 的實例并返回。

3.1.4 小結

為了說明適配器 CallAdapter 和轉換器 Converter 的作用,我們繪制了下圖:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

從上面我們看出,CallAdapter 主要用來將某個請求轉換成我們指定的類型。比如,在我們最開始的例子中,要將請求轉換成 Observable<WXUserInfo>。如果轉換之后的請求是 Observable 類型的,那么當我們對轉換后的請求進行訂閱的時候,就啟動了 OkHttp 的網絡請求過程。

在進行網絡請求之前會先使用 Converter 將請求的參數轉換成一個 RequestBody。這里將其作為一個接口的好處是便于解耦。比如,上面我們用 Gson 來完成轉換過程,你也可以通過自定義轉換器來使用其他的框架,比如 Moshi 等。當拿到了響應之后,我們又會再次使用 Converter 來將響應體 ResponseBody 轉換成我們要求的類型。比如 WXUserInfo

從上面我們看出,Retrofit 設計的非常妙的地方就在于上面的兩個過程的解耦(策略模式+工廠模式+適配器模式)。一次是將請求轉換成 Observable 的過程,一次是將請求體和響應體轉換成 OkHttp 要求的 RequestBodyResponseBody 的過程。對于前者,不論我們使用的是 RxJava 1 還是 RxJava 2,只要傳入一個 CallAdapter 即可。對于后者,不論我們使用哪種 Json 轉換框架,只要實現了 Converter 接口皆可。

3.2 獲取代理實例

3.2.1 劃分平臺:Platform

創建了 Retrofit 的實例之后,我們就可以使用它的 create() 方法來獲取代理之后的服務實例。下面是這個方法的定義。在這里,我們會先根據 validateEagerly 變量來判斷是否立即對傳入的服務接口的方法進行解析。然后,我們使用 Proxy 的靜態方法獲取一個代理實例。

public <T> T create(final Class<T> service) {Utils.validateServiceInterface(service);// 這里的 validateEagerly 在 Retrofit 構建的時候設置if (validateEagerly) {// 是否立即對 Service 方法的內容進行解析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 {// 該方法是 Object 的方法,直接觸發該方法if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args);}// 如果是 default 方法,那么使用該 Java8 平臺的方法執行if (platform.isDefaultMethod(method)) {return platform.invokeDefaultMethod(method, service, proxy, args);}// 獲取服務方法的信息,并將其包裝成 ServiceMethodServiceMethod<Object, Object> serviceMethod =(ServiceMethod<Object, Object>) loadServiceMethod(method);OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);return serviceMethod.adapt(okHttpCall);}});
}

這里的 eagerlyValidateMethods() 方法定義如下:

private void eagerlyValidateMethods(Class<?> service) {// 獲取程序當前運行的平臺Platform platform = Platform.get();for (Method method : service.getDeclaredMethods()) {// 判斷該方法是否是 default 方法if (!platform.isDefaultMethod(method)) {loadServiceMethod(method);}}
}

它的作用是立即對服務接口的方法進行解析,并將解析之后的結果放進一個緩存中。這樣,當這個服務方法被觸發的時候,直接從緩存當中獲取解析之后的 ServiceMethod 來使用即可。該方法會先會根據當前程序運行的平臺來決定是否應該加載服務的方法。因為,Java 8 之后,我們可以為接口增加 default 類型的方法,所以,如果是 default 類型的話,我們不會調用 loadServiceMethod() 進行解析,而是調用 Java8 平臺的 invokeDefaultMethod() 來處理。在 invokeDefaultMethod() 中,會根據傳入的信息創建一個實例并使用反射觸發它的方法。此時,就間接地觸發了該 default 方法。

判斷平臺的時候,使用了如下這段代碼:

platform.isDefaultMethod(Method)

這里的 platform 是調用 Platform.get() 的時候得到的。它會在 get() 方法中嘗試使用反射去獲取一個只有 Java8 平臺才具有的類,以此來判斷是否是 Java8 的環境。在 Retrofit 中,提供了 Java8Android 兩個類來區分所在的平臺,并會根據運行環境來決定返回哪個實例。

所以,Platform 應用了策略模式,以對不同的平臺做不同的處理。在當前的版本中,它的主要作用是對 default 類型的方法進行處理。

3.2.2 解析服務方法:ServiceMethod

上面我們提到過 loadServiceMethod() 方法,它的主要作用:首先會嘗試從緩存當中獲取該方法對應的 ServiceMethod 實例,如果取到的話,就將其返回;否則,就使用構建者模式創建一個并放進緩存中,然后將其返回。

ServiceMethod<?, ?> loadServiceMethod(Method method) {// 從緩存中進行獲取ServiceMethod<?, ?> result = serviceMethodCache.get(method);if (result != null) return result;synchronized (serviceMethodCache) {result = serviceMethodCache.get(method);if (result == null) {// 創建ServiceMethod實例result = new ServiceMethod.Builder<>(this, method).build();serviceMethodCache.put(method, result);}}return result;
}

ServiceMethod 的構建過程比較簡單,只需要把當前的 Retrofit 實例和服務方法 method 傳入進去,然后調用它的 build() 方法就完成了整個創建過程。在 build() 方法中,會完成對 method 的解析,比如根據注解判斷是什么類型的請求,根據方法的參數來解析請求的請求體等等。

所以,ServiceMethod 的作用是緩存服務方法對應的請求信息,這樣下次我們就不需要再次解析了。同時,它提供了以下幾個方法。它們的主要作用是用來從 ServiceMethod 中獲取請求相關的信息:

toCall() 用來獲取用于 OkHttp 請求的 Call 對象:

okhttp3.Call toCall(@Nullable Object... args) throws IOException {RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers,contentType, hasBody, isFormEncoded, isMultipart);// ...return callFactory.newCall(requestBuilder.build());
}

toResponse(ResponseBody) 用來把 OkHttp 得到的響應體轉換成 Java 對象等(在示例中是WXUserInfo):

R toResponse(ResponseBody body) throws IOException {return responseConverter.convert(body);
}

adapt(Call<R>) 用來將 OkHttp 的請求轉換成我們的服務方法的返回類型(在示例中是Observable<WXUserInfo>):

T adapt(Call<R> call) {return callAdapter.adapt(call);
}
3.2.3 請求封裝:OkHttpCall

解析完畢服務方法之后,我們得到了 ServiceMethod 實例。然后,我們使用它來創建 OkHttpCall 實例。這里的 OkHttpCall 實現了 Retrofit 中定義的 Call 接口,會在方法內調用 ServiceMethodtoCall() 方法來獲取 OkHttp 中的 Call 對象,然后使用它進行網絡訪問。當拿到了請求的結果之后又使用 ServiceMethodtoResponse() 把響應轉換成我們指定的類型。下面是該類中的幾個比較重要的方法:

  1. execute() 方法,用來同步執行網絡請求:
    @Overridepublic Response<T> execute() throws IOException {okhttp3.Call call;synchronized (this) {// ...call = rawCall;if (call == null) {try {// 創建 OkHttp 的 Call 實例call = rawCall = createRawCall();} catch (IOException | RuntimeException | Error e) {throwIfFatal(e);creationFailure = e;throw e;}}}if (canceled) {call.cancel();}// 同步執行請求,并解析結果return parseResponse(call.execute());}
  1. createRawCall() 用來創建 OkHttp 的 Call 實例:
    // 使用 serviceMethod 的 toCall 方法獲取 OkHttp 的 Call 實例private okhttp3.Call createRawCall() throws IOException {okhttp3.Call call = serviceMethod.toCall(args);if (call == null) {throw new NullPointerException("Call.Factory returned null.");}return call;}
  1. parseResponse() 用來將 OkHttp 的響應轉換成我們接口中定義的類型。比如,在我們的例子中,返回的是 Observable<WXUserInfo>:
    // 使用 serviceMethod 的 toResponse 方法獲取 OkHttp 的 Response 實例Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {ResponseBody rawBody = rawResponse.body();rawResponse = rawResponse.newBuilder().body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength())).build();// ...ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);try {// 使用 serviceMethod 的 toResponse 方法獲取 OkHttp 的 Response 實例T body = serviceMethod.toResponse(catchingBody);return Response.success(body, rawResponse);} catch (RuntimeException e) {catchingBody.throwIfCaught();throw e;}}

3.3 Retrofit 的工作過程

上面是 Retrofit 框架設計中幾個關鍵的部分的功能的解析。下面,我們再來具體看一下,從觸發代理類的方法到拿到響應的結果,這一整個過程中,都有哪些類的哪些方法參與,以及它們在什么時候,扮演什么樣的角色。這里我們仍然使用最初的示例:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

上圖中,我們將 Retrofit 的請求的過程分成三個過程來進行說明:

  1. 創建代理實例的過程:在這個過程中主要是調用 Proxy.newProxyInstance() 來獲取一個代理實例。相關的主要參數是 validateEagerly,我們會使用它來決定是否立即對傳入的接口的方法進行解析。不論我們什么時候進行解析,都會把解析的結果緩存起來。
  2. 觸發代理方法的過程:觸發代理方法是整個請求的第二過程。這個時候,我們調用了 WXInfoService 代理實例的 getWXUserInfo() 方法。此時,會觸發 InvocationHandler.invoke() 方法。在該方法內部會調用 ServiceMethod 的構建者模式來創建 serviceMethod 實例。當調用構建者模式的 build() 方法的時候,會對方法 getWXUserInfo() 的信息進行解析。然后,使用 serviceMethod 創建 okHttpCall。最后,調用 serviceMethod.adapt() 方法將 okHttpCall 實例轉換成 Observable<WXUserInfo>。在轉換的過程中會使用 CallAdapteradapt() 方法來完成適配。
  3. 執行網絡請求的過程:拿到了 Observable<WXUserInfo> 之后,需要對其進行訂閱才能觸發網絡請求。相關的邏輯在 CallAdapter 中完成。首先,它會根據你使用同步還是異步的來決定使用哪個執行器。這里存在兩個執行器,它們的區別是一個會在內部調用 OkHttpCallenqueue(),另一個會在執行器中調用 OkHttpCallexecute() 方法。不論調用 enqueue() 還是 execute(),都會先使用 OkHttpCalltoCall() 方法獲取一個 Call 請求。獲取請求的過程中會使用 Converter 來將某個實例轉換成請求體。拿到了請求之后,使用該請求來進行網絡訪問。當從網絡中拿到了響應之后,會使用 Converter 來將響應體轉換成對象。這樣,拿到了實際的結果之后,就會調用 ObserveronNext() 方法把結果通知給觀察者。

4、總結

在這篇文章中,我們先簡單介紹了 Retrofit 的使用,然后,因為 Retrofit 內部使用動態代理來實現的,所以,我們對動態代理相關內容進行了介紹。最后,我們對 Retrofit 的源碼進行了分析,先從設計思路,后從各個環節的執行過程進行了說明。最后的最后,我們將兩者結合起來用一個時序圖做了說明。

從上文中可以看出來,Retrofit 設計的幾個值得我們借鑒的地方:

  1. 使用運行時注解和反射簡化請求描述,但是考慮到反射的效率比較低,所以將一次反射之后的結果緩存起來,以便于下次使用。
  2. 動態代理:使用接口描述請求的好處是它簡潔,而且 “描述” 本來就是它的責任。但是,一般我們需要去實現接口才能使用。而這里告訴我們,使用動態代理一樣可以使用接。
  3. 解耦:從我們上面的圖中也可以看出來,Retrofit 的設計的思路是比較清晰的。它將一個請求的幾個過程解耦出來。首先是我們 Observable 到請求的轉換,這里使用適配器來完成;然后是請求體和響應體的轉換,基本就是 Json 的轉換,使用轉換器來完成。這樣,不論你使用 RxJava 1 還是 RxJava 2,不論是 Gson 還是 FastXml 都可以和 Retrifut 配合使用。

以上就是我們對 Retrofit 的源碼的分析。

另外
有什么技術問題歡迎加我交流 qilebeaf
本人10多年大廠軟件開發經驗,精通Android,Java,Python,前端等開發,空余時間承接軟件開發設計、課程設計指導、解決疑難bug、AI大模型搭建,AI繪圖應用等。
歡迎砸單

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/39291.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/39291.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/39291.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

控制臺廠商配額查詢

概述 廠商推送限制 每個廠商通道都有對應的廠商配額和 QPS 限制&#xff0c;當請求超過限制且已配置廠商回執時&#xff0c;MobPush會采取以下措施&#xff1a; 當開發者推送請求超過廠商配額時&#xff0c;MobPush將通過自有通道進行消息下發。當開發者推送請求超過廠商 QPS…

java default注解怎么設置數組默認值

在Java中&#xff0c;為注解的數組類型屬性設置默認值時&#xff0c;需要使用大括號{}包圍數組元素。如果數組默認值只有一個元素&#xff0c;也可以直接使用該元素而不需要大括號。下面是一個示例&#xff0c;展示了如何為注解的數組類型屬性設置默認值&#xff1a; import j…

Spark on k8s 源碼解析執行流程

Spark on k8s 源碼解析執行流程 1.通過spark-submit腳本提交spark程序 在spark-submit腳本里面執行了SparkSubmit類的main方法 2.運行SparkSubmit類的main方法&#xff0c;解析spark參數&#xff0c;調用submit方法 3.在submit方法里調用doRunMain方法&#xff0c;最終調用r…

Activity top resumed state loss timeout for ActivityRecord 報錯原因是?

Activity top resumed state loss timeout for ActivityRecord 這個錯誤通常不是直接顯示給用戶看的&#xff0c;而是Android系統內部日志&#xff08;如Logcat&#xff09;中的一個錯誤信息&#xff0c;它指示了系統在嘗試恢復或管理某個Activity的狀態時遇到了問題。這個錯誤…

算法-位圖與底層運算邏輯

文章目錄 1. 位圖的理論基礎2. 完整版位圖實現3. 底層的運算邏輯-位運算 1. 位圖的理論基礎 首先我們要理解什么是位圖, 位圖的一些作用是什么 位圖法就是bitmap的縮寫。所謂bitmap&#xff0c;就是用每一位來存放某種狀態&#xff0c;適用于大規模數據&#xff0c;但數據狀態又…

Python+Pytest+Allure+Yaml+Pymysql+Jenkins+GitLab接口自動化測試框架詳解

PythonPytestAllureYaml接口自動化測試框架詳解 編撰人&#xff1a;CesareCheung 更新時間&#xff1a;2024.06.20 一、技術棧 PythonPytestAllureYamlJenkinsGitLab 版本要求&#xff1a;Python3.7.0,Pytest7.4.4,Allure2.18.1,PyYaml6.0 二、環境配置 安裝python3.7&…

Python 類與對象:深入理解與應用

在 Python 中&#xff0c;類是一種抽象數據類型&#xff0c;用于描述具有相同屬性和方法的對象集合。類通過屬性&#xff08;變量&#xff09;和方法&#xff08;函數&#xff09;來定義對象的行為。對象是類的實例化結果&#xff0c;它可以具備類定義的所有特性。Python 中的類…

ROS2 RQT

1. RQT是什么 RQT是一個GUI框架&#xff0c;通過插件的方式實現了各種各樣的界面工具。 強行解讀下&#xff1a;RQT就像插座&#xff0c;任何電器只要符合插座的型號就可以插上去工作。 2.選擇插件 這里我們可以選擇現有的幾個RQT插件來試一試&#xff0c;可以看到和話題、參…

金蝶云星空字段之間連續觸發值更新

文章目錄 金蝶云星空字段之間連續觸發值更新場景說明具體需求&#xff1a;解決方案 金蝶云星空字段之間連續觸發值更新 場景說明 字段A配置了字段B的計算公式&#xff0c;字段B配置了自動C的計算公式&#xff0c;修改A的時候&#xff0c;觸發了B的重算&#xff0c;但是C觸發不…

【云原生】Kubernetes----ETCD數據的備份與恢復

目錄 引言 一、ETCD數據備份 &#xff08;一&#xff09;確定備份策略 &#xff08;二&#xff09;使用etcdctl工具進行備份 1.安裝etcdctl命令 2.設置ETCDCTL_API環境變量 &#xff08;三&#xff09;執行備份 二、數據還原 &#xff08;一&#xff09;創建新資源 &…

XMind2TestCase:高效測試用例設計工具

XMind2TestCase&#xff1a;高效測試用例設計工具 引言傳統測試用例設計的問題1. Excel表格的局限性2. 傳統測試管理工具的不足3. 自研測試管理工具的挑戰 思維導圖在測試用例設計中的應用思維導圖的優勢思維導圖的挑戰 簡介安裝使用方式命令行調用使用Web界面 使用示例XMind文…

廣州自閉癥機構哪家好

在廣州&#xff0c;眾多的自閉癥康復機構中&#xff0c;星貝育園自閉癥兒童康復學校以其獨特的優勢脫穎而出。 一、專業的師資團隊 我們擁有一支經驗豐富、專業素養極高的師資隊伍。每位老師都經過嚴格的專業培訓&#xff0c;深入了解自閉癥兒童的特點和需求。他們不僅…

蒼穹外賣項目 常用注解 + 動態sql

常用注解 常見的注解解析方法有兩種&#xff1a; 編譯期直接掃描&#xff1a;編譯器在編譯 Java 代碼的時候掃描對應的注解并處理&#xff0c;比如某個方法使用Override 注解&#xff0c;編譯器在編譯的時候就會檢測當前的方法是否重寫了父類對應的方法。運行期通過反射處理&…

SAP_ABAP相關日語單詞

基本概念 1. プログラミング言語 (プログラミングげんご, Puroguramingu gengo) - 編程語言 2. 開発 (かいはつ, Kaihatsu) - 開發 3. システム (システム, Shisutemu) - 系統 4. モジュール (モジュール, Mojūru) - 模塊 5. トランザクションコード (トランザクションコード,…

探索旅游卡項目的八大黃金賽道,你離月入十幾萬僅一步之遙!

作為旅游卡項目的推廣精英&#xff0c;我深知在這個充滿機遇與挑戰的時代&#xff0c;選擇正確的賽道至關重要。今天&#xff0c;我將從定位、內容、產品、流量、變現這五個核心維度出發&#xff0c;為你揭秘旅游卡項目的八大熱門方向。如果你正對旅游充滿熱情&#xff0c;或擁…

【基于R語言群體遺傳學】-3-計算等位基因頻率

書接上文&#xff0c;我們講完了哈代溫伯格基因型頻率&#xff0c;也使用數據進行了擬合&#xff0c;那么接下來就是考慮一些計算的問題&#xff1a; 【基于R語言群體遺傳學】-1-哈代溫伯格基因型比例-CSDN博客 【基于R語言群體遺傳學】-2-模擬基因型&#xff08;simulating …

【leetcode--最小棧】

設計一個支持 push &#xff0c;pop &#xff0c;top 操作&#xff0c;并能在常數時間內檢索到最小元素的棧。 實現 MinStack 類: MinStack() 初始化堆棧對象。void push(int val) 將元素val推入堆棧。void pop() 刪除堆棧頂部的元素。int top() 獲取堆棧頂部的元素。int get…

TextInput是用于在用戶界面中輸入文本的控件,通常應用于表單、搜索框等需要用戶輸入文字的場景

TextInput是用于在用戶界面中輸入文本的控件&#xff0c;通常應用于表單、搜索框等需要用戶輸入文字的場景。以下是對TextInput的詳細解釋&#xff0c;涵蓋其各個方面的功能和屬性。 基本屬性 text 描述&#xff1a;TextInput中當前顯示的文本。用法&#xff1a;text: "示…

WebKey備受矚目的Web3.0新敘事,硬件與加密生態完美融合特性成為數字世界的新入口

在當今迅速發展的科技領域&#xff0c;Web3.0正在引領一場顛覆性的變革。而作為這一變革的先鋒&#xff0c;WebKey無疑是備受矚目的創新項目。它不僅代表了一種全新的技術趨勢&#xff0c;更是數字世界中硬件與加密生態完美融合的典范。 硬件與加密生態的完美融合 WebKey的核心…

Java基礎面試題(簡單版):

1.java的8個基本數據類型? 整型: byte(占用1個字節) short(占用2個字節) int(占用4個字節) long(占用8個字節) 浮點型: float(占用4個字節)、double(占用8個字節) 字符型: char 布爾型: boolean 2.ArrayList和LinkedList的區別? 可以說ArrayList和LinkedList除了是同屬于集合…