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.mzph.cn/diannao/39291.shtml

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

相关文章

控制台厂商配额查询

概述 厂商推送限制 每个厂商通道都有对应的厂商配额和 QPS 限制&#xff0c;当请求超过限制且已配置厂商回执时&#xff0c;MobPush会采取以下措施&#xff1a; 当开发者推送请求超过厂商配额时&#xff0c;MobPush将通过自有通道进行消息下发。当开发者推送请求超过厂商 QPS…

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…

算法-位图与底层运算逻辑

文章目录 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&…

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触发不…

XMind2TestCase:高效测试用例设计工具

XMind2TestCase&#xff1a;高效测试用例设计工具 引言传统测试用例设计的问题1. Excel表格的局限性2. 传统测试管理工具的不足3. 自研测试管理工具的挑战 思维导图在测试用例设计中的应用思维导图的优势思维导图的挑战 简介安装使用方式命令行调用使用Web界面 使用示例XMind文…

广州自闭症机构哪家好

在广州&#xff0c;众多的自闭症康复机构中&#xff0c;星贝育园自闭症儿童康复学校以其独特的优势脱颖而出。 一、专业的师资团队 我们拥有一支经验丰富、专业素养极高的师资队伍。每位老师都经过严格的专业培训&#xff0c;深入了解自闭症儿童的特点和需求。他们不仅…

苍穹外卖项目 常用注解 + 动态sql

常用注解 常见的注解解析方法有两种&#xff1a; 编译期直接扫描&#xff1a;编译器在编译 Java 代码的时候扫描对应的注解并处理&#xff0c;比如某个方法使用Override 注解&#xff0c;编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。运行期通过反射处理&…

【基于R语言群体遗传学】-3-计算等位基因频率

书接上文&#xff0c;我们讲完了哈代温伯格基因型频率&#xff0c;也使用数据进行了拟合&#xff0c;那么接下来就是考虑一些计算的问题&#xff1a; 【基于R语言群体遗传学】-1-哈代温伯格基因型比例-CSDN博客 【基于R语言群体遗传学】-2-模拟基因型&#xff08;simulating …

WebKey备受瞩目的Web3.0新叙事,硬件与加密生态完美融合特性成为数字世界的新入口

在当今迅速发展的科技领域&#xff0c;Web3.0正在引领一场颠覆性的变革。而作为这一变革的先锋&#xff0c;WebKey无疑是备受瞩目的创新项目。它不仅代表了一种全新的技术趋势&#xff0c;更是数字世界中硬件与加密生态完美融合的典范。 硬件与加密生态的完美融合 WebKey的核心…

【QT】输入类控件

目录 Line Edit 核心属性 核心信号 正则表达式 示例&#xff1a;使用正则表达式验证输入框内容 示例&#xff1a;切换输入框密码模式下的显示状态 Text Edit 核心属性 核心信号 示例&#xff1a;获取多行输入框的内容同步显示到label 示例&#xff1a;获取文本的选…

一文讲懂npm link

前言 在本地开发npm模块的时候&#xff0c;我们可以使用npm link命令&#xff0c;将npm 模块链接到对应的运行项目中去&#xff0c;方便地对模块进行调试和测试 用法 包链接是一个两步过程&#xff1a; 1.为依赖项创建全局软链npm link。一个符号链接&#xff0c;简称软链&a…

二刷力扣——DP算法(子序列问题)

300. 最长递增子序列 定义是以本元素结尾&#xff0c;所以公式初始化都好弄。但是太慢 class Solution {public int lengthOfLIS(int[] nums) {int nnums.length;int[] dp new int[n];//以自己结尾的最长递增子序列dp[0]1;int maxzi1;for(int i1;i<n;i){dp[i]1;for(int j…

QT中QDomDocument读写XML文件

一、XML文件 <?xml version"1.0" encoding"UTF-8"?> <Begin><Type name"zhangsan"><sex>boy</sex><school>Chengdu</school><age>18</age><special>handsome</special>&l…

【YOLOv5进阶】——引入注意力机制-以SE为例

声明&#xff1a;笔记是做项目时根据B站博主视频学习时自己编写&#xff0c;请勿随意转载&#xff01; 一、站在巨人的肩膀上 SE模块即Squeeze-and-Excitation 模块&#xff0c;这是一种常用于卷积神经网络中的注意力机制&#xff01;&#xff01; 借鉴代码的代码链接如下&a…

在C#中使用RabbitMQ做个简单的发送邮件小项目 _

前言 好久没有做项目了&#xff0c;这次做一个发送邮件的小项目。发邮件是一个比较耗时的操作&#xff0c;之前在我的个人博客里面回复评论和友链申请是会通过发送邮件来通知对方的&#xff0c;不过当时只是简单的进行了异步操作。那么这次来使用RabbitMQ去统一发送邮件&#x…

vue中路由来回切换页面直接卡死

今天发现一个很严重的问题&#xff0c;项目好不容易做好了&#xff0c;结果页面多了&#xff0c;切换之后卡死。页面所有的交互效果都失效了。 排查了许久的错误原因最后发现原来是路由名称重复了。 如上图当页面跳转到riskdetails详细页面之后&#xff0c;框架则被这个详情页…

node.lib下载失败,手动下载并配置

在无网络环境&#xff0c;或者网络不好的环境&#xff0c;node.lib会下载失败&#xff0c;此时可手动下载并进行配置。 我们以 node16.17.0 为例&#xff1a; 下载地址 分别下载node.lib和headers https://registry.npmmirror.com/-/binary/node/v16.17.0/win-x64/node.lib…

Linux rpm与yum

一、rpm包管理 rpm用于互联网下载包的打包及安装工具&#xff0c;它包含在某些Linux分发版中。它生成具有.RPM扩展名的文件。RPM是RedHat Package Manager (RedHat软件包管理工具&#xff09;的缩写&#xff0c;类似windows的setup.exe&#xff0c;这一文件格式名称虽然打上了R…