本博客为作者原创,如需转载请注明原博客出处:http://www.cnblogs.com/wondertwo/p/5838528.html(博客园)/http://www.jianshu.com/p/dd2804030b89(简书)


0X00 写在前面


相信做过Android网络请求的同学都绕不开Volley,Retrofit,OkHttp这几座大山,至于他们的前世姻缘以及孰优孰劣,不在本博客的讨论范围。如题,这篇博客主要介绍一个小白(其实就是我自己)的Retrofit2进阶之路,会结合一个开发实例介绍5节内容:

  • Retrofit2 HTTP请求方法注解的字段说明
  • Call<T>响应结果的处理问题
  • Retrofit2+RxJava实现开发效率最大化
  • 自定义OkHttp Interceptor实现日志输出,保存和添加Cookie
  • 自定义ResponseConverter,自定义HTTP请求注解

先来回顾一下Retrofit2在项目中的完整使用流程:创建Bean类 --> 创建接口形式的http请求方法 --> 通过Retrofit.builder()创建接口对象并调用http方法请求网络数据 --> 在RxJavaObservable(被观察者)中异步处理请求结果!

那么Retrofit2 Http 请求方法注解有那么多字段,都代表什么含义呢?添加请求头或者大文件上传的请求方法该怎么写呢?这将在第二节介绍。另外,Retrofit2基本用法的网络响应结果是一个Call<T> ,那么怎样在Android中解析Call<T> 呢?将在第二节介绍。第三节根据Retrofit2使用流程介绍了一个实践项目是怎样使用Retrofit2+RxJava 做网络请求。第四节和四五节是Retrofit实现一些复杂需求的必杀技,介绍了自定义OkHttp Interceptor实现日志输出,保存和添加Cookie;自定义ResponseConverter,自定义HTTP请求注解等内容。

0X01 Retrofit2 HTTP请求方法注解的字段说明


从Retrofit2的官方文档来看,Retrofit2 进行网络请求的URL分为两部分:BaseURL和relativeURL。BaseURL需要以/ 结尾, 一般不需要变化直接定义即可,当然在特殊的情况下,比如后一次网络访问URL需要从前一次访问结果中获取相关参数,那么就需要动态的操作URL,这种用法会在第五节进行介绍;relativeURL与每次请求的参数相关,所以每个request 方法都需要 http annotation 来提供请求的relativeURL,Retrofit2内置的注解有五个:GET, POST, PUT, DELETE, and HEAD. 这些注解在使用时涉及到哪些相关的字段呢?我从参考文献的博客中引用了一张图:

可以看到,有URL请求参数,Query参数这些简单网络请求参数;同时还支持用@Header添加请求头;POST请求中常用@FormUrlEncoded提交表单,并用@Field定义表单域;@MultiPart文件上传并用@Part定义请求体。来看一个具体的例子(摘自Retrofit2官方文档):

public interface GitHubService {@GET("users/{user}/repos")Call<List<Repo>> listRepos(@Path("user") String user);
}

Retrofit2把网络请求定义成接口的形式,如上是一个GET请求,@Path表示一个占位符,@Path中的变量必须与@GET变量中{} 中间的部分一致。下面是一个POST请求,@FormUrlEncoded用于提交一个表单,@Field定义了表单的name和value。更多详细的用法详见Retrofit2官方文档API Declaration ,另外Retrofit请求参数注解字段说明 这篇博客介绍的比较详细可作参考:

public interface GitHubService {@FormUrlEncoded@POST("user/edit")Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);
}

0X02 Call<T> 响应结果的处理


细心的你有木有发现,发现官方文档中给出的请求方法示例,返回结果都是 Call<List<User>> 这种形式?没错!这就是Retrofit2最原始的网络请求用法,官方文档上介绍的很简洁,可以在 call<T> 响应对象上做异步或者同步的操作,每个 call<T> 对象只能用一次,要想多次使用可以调用 clone() 方法来克隆出多个 call 对象以供更多操作使用。因为Retrofit2 是一个类型安全的Java和Android网络请求库,所以以上的操作对 Java 网络请求也是适用的。

针对JVM而言,网络请求和结果处理会放在同一个线程中执行,那么在Android中,我们怎样处理请求结果对象 call 呢?官方文档也给出了答案,我们都知道Android中网络请求这类耗时操作都是放在工作线程(即worker thread)来执行的,然后在主线程(也即 UI thread)处理网络请求结果,自然Retrofit2也不例外,由于Retrofit2抛弃了饱受诟病的Apache HttpClient底层只依赖OkHttp3.0,网络访问层的操作都会交由OkHttp来完成,而OkHttp不仅拥有自动维护的socket连接池,减少握手次数,而且还拥有队列线程池,可以轻松写并发,同时还支持socket自动选择最好路线,并支持自动重连,OkHttp的优点远不止于此,所以Retrofit2选择用OkHttp作为网络请求执行器是一个再明智不过的决定。

如果你想异步的执行网络请求,最简单的就是在Activity或者Fragment中View控件的监听器中进行网络访问,并通过call.enqueue()处理请求结果,并更新UI,下面是一个小demo:

Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {GitHubService gitHubService = GitHubService.retrofit.create(GitHubService.class);final Call<List<Contributor>> call =gitHubService.repoContributors("square", "retrofit");call.enqueue(new Callback<List<Contributor>>() {@Overridepublic void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {final TextView textView = (TextView) findViewById(R.id.textView);textView.setText(response.body().toString());}@Overridepublic void onFailure(Call<List<Contributor>> call, Throwable t) {final TextView textView = (TextView) findViewById(R.id.textView);textView.setText("Something went wrong: " + t.getMessage());}});}
});

如果你需要在工作线程中执行网络请求,而不是在一个Activity或者一个Fragment中去执行,那么也就意味着,你可以在同一个线程中同步的去执行网络请求,使用call.execute()方法来处理请求结果即可,代码如下:

try {Response<User> response = call.execute();
} catch (IOException e ){// handle error
}

0X03 Retrofit2+RxJava实现开发效率最大化


Retorfit是支持RxJava,Guava,Java8 等等一系列扩展的,关于RxJava这个网红我就不做介绍了,RactiveX项目对 JVM 的扩展,你可以把它当做一个超级强大的异步事件处理库,可他的NB之处远不止于此,至少做Android的都应该听过他的鼎鼎大名,不熟悉的可以去看看RxJava Wiki!!而这里Retrofit2+RxJava组合就可以实现开发效率的大幅提升,至于怎样提升的?对比一下你以前写的网络请求的代码量就知道了!结合一个实践项目的源码来分析,这里是请求果壳网最新的100条文章数据,返回结果为Json,首先build.gradle 添加依赖:

    compile 'io.reactivex:rxandroid:1.1.0' // RxAndroidcompile 'io.reactivex:rxjava:1.1.0' // 推荐同时添加RxJavacompile 'com.squareup.retrofit2:retrofit:2.1.0' // Retrofit网络处理compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' // Retrofit的rx解析库compile 'com.squareup.retrofit2:converter-gson:2.1.0' // Retrofit的gson库compile 'com.squareup.okhttp3:okhttp:3.2.0' // OkHttp3

第一步,定义服务器Json数据对应的POJO类,这里我们可以偷一下懒可以直接通过jsonschema2pojo 这个网站自动生成POJO类,就不用我们手动去写了,然后copy到项目目录的bean包下。接着便是定义HTTP请求方法了,以接口的形式定义,如下:

// 服务器数据对应的实体类
public class Guokr {// 定义序列化后的名字public @SerializedName("ok") Boolean response_ok;// 定义序列化后的名字public @SerializedName("result") List<GuokrResult> response_results;public static class GuokrResult {public int id;public String title;public String headline_img_tb; // 用于文章列表页小图public String headline_img; // 用于文章内容页大图public String link;public String author;public String summary;}
}// HTTP请求方法
public interface GuokrService {@GET("handpick/article.json")Observable<Guokr> getGuokrs(@Query("retrieve_type") String type,@Query("category") String category,@Query("limit") int limit,@Query("ad") int ad);}

其中 Observable<Guokr> 是RxJava中的被观察者,对应请求结果Call<T>。只是因为Retrofit提供了非常强大的CallAdapterFactory 完美兼容了RxJava 这个超级大网红,才导致我们平常看到的写法是这样的。第二步, 需要通过Retrofit.builder() 创建 GuokrService 接口对象,通过接口对象执行 getGuokrs 方法进行网络访问,代码如下:

    // 封装 GuokrService 请求public static GuokrService getGuokrService() {if (guokrService == null) {Retrofit retrofit = new Retrofit.Builder().client(mClient).baseUrl("http://apis.guokr.com/").addCallAdapterFactory(rxJavaCallAdapterFactory).addConverterFactory(gsonConverterFactory).build();guokrService = retrofit.create(GuokrService.class);}return guokrService;}// 默认加载最新的100条数据
subscription = RetrofitClient.getGuokrService().getGuokrs("by_since", "all", 100, 1).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Guokr>() {@Overridepublic void onCompleted() {Log.e(TAG, "--------completed-------");}@Overridepublic void onError(Throwable e) {Log.e(TAG, "--------error-------");Log.e(TAG, e.getMessage());}@Overridepublic void onNext(Guokr guokr) {if (guokr.response_ok) {List<Guokr.GuokrResult> guokrResults = guokr.response_results;List<GuokrItem> guokrItems = new ArrayList<>(guokrResults.size());for (Guokr.GuokrResult result : guokrResults) {GuokrItem item = new GuokrItem();item.headline_img_tb = result.headline_img_tb;item.title = result.title;item.id = result.id;item.headline_img = result.headline_img;item.summary = result.summary;guokrItems.add(item);}mAdapter.addAll(guokrItems);mAdapter.notifyDataSetChanged();});

注意到封装 GuokrService 请求:

  1. addCallAdapterFactory(rxJavaCallAdapterFactory) 方法指定使用RxJava 作为CallAdapter ,需要传入一个RxJavaCallAdapterFactory对象:CallAdapter.Factory rxJavaCallAdapterFactory = RxJavaCallAdapterFactory.create()
  2. addConverterFactory(gsonConverterFactory) 方法指定 Gson 作为解析Json数据的ConverterConverter.Factory gsonConverterFactory = GsonConverterFactory.create()
  3. client(mClient)方法指定网络执行器为OkHttp 如下创建一个默认的OkHttp对象传入即可:OkHttpClient mClient = new OkHttpClient()

而加载网络数据这个链式调用就是RxJava最大的特色,用在这里逻辑就是,被观察者Observable<Guokr>订阅观察者Observer<Guokr>,当服务器一有response,观察者就会立即处理response result。因为RxJava最大的亮点就是异步,可以很方便的切换当前任务所在的线程,并能对事件流进行各种Map变换,比如压合、转换、缓存等操作。这里是最基本的用法,被观察者直接把事件流订阅到观察者,中间没有做转换处理。

到此网络访问就完成了,是不是很简洁?简洁就对了,那是因为太多东西Retrofit2和RxJava甚至是OkHttp都帮我们做好了!再回顾一下整个网络访问流程:创建Bean类 --> 创建接口形式的http 请求方法 --> 通过Retrofit.builder() 创建接口对象并调用http 方法请求网络数据 --> 在RxJavaObservable 中异步处理请求结果!

0X04 自定义OkHttp Interceptor实现日志输出,保存和添加Cookie


在Retrofit2做网络请求的第二步,我们需要通过Retrofit.builder()方法来创建Retrofit对象,其中client(mClient)这个方法指定一个OkHttpClient客户端作为请求的执行器,需要传入一个OkHttpClient对象作为参数,那么在这里,我们就可以进行一些OkHttp相关的操作,比如自定义Interceptor,通过自定义Interceptor可以实现网络请求日志的分级输出,可以实现保存和添加Cookie这些功能,当然,这些功能的实现都是基于OkHttp,所以要对OkHttp有一定的了解才能灵活运用。

Retrofit使用指南-->OkHttp配合Retrofit使用 这篇博客在OkHttp配合Retrofit使用这一节,关于OkHttpClient添加HttpLoggingInterceptor 进行日志输出,以及如何设置SslSocketFactory做了详细的说明,有兴趣的同学可以参考!值得注意的是,如果后一次请求的URL,需要从前一次请求结果数据中获取,这时候就需要动态的改变BaseURL,也可通过自定义Interceptor 来实现这一需求,在BaseURL改变时,只需要setHost()就可以让下次请求的BaseURL改变,代码如下:

public class DynamicBaseUrlInterceptor implements Interceptor {private volatile String host;public void setHost(String host) {this.host = host;}@Overridepublic Response intercept(Chain chain) throws IOException {Request originalRequest = chain.request();if (!TextUtils.isEmpty(host)) {HttpUrl newUrl = originalRequest.url().newBuilder().host(host).build();originalRequest = originalRequest.newBuilder().url(newUrl).build();}return chain.proceed(originalRequest);}
}

那么怎样在通过OkHttp保存和添加Cookie呢?其实实现原理和上面添加日志拦截器差不多,只是添加的Intercepter不同而已,其实就是自定义了一个Interceptor接口实现类,接收和保存返回结果中的Cookie,或者添加Cookie,最后,在创建OkHttp实例的时候,传入以上Interceptor实现类的对象即可。Retrofit使用OkHttp保存和添加cookie这篇博客讲的很好,可以作为参考!

简而言之,以上这Retorfit2些高级运用都是基于定制化OkHttp来实现的,如果想玩得很溜就必须对OkHttp了解一二,推荐看这篇博客OkHttp3源码分析综述!最起码需要弄清楚OkHttpClient自定义Interceptor这一块内容,推荐看OkHttp Github Wiki --> Interceptors!

0X05 自定义ResponseConverter,自定义HTTP请求注解


默认情况下,Retrofit会把HTTP响应体反序列化到OkHttp的ResponseBody中,加入Converter可以将返回的数据直接格式化成你需要的样子,Retrofit提供了如下6个Converter可以直接使用,使用前需要加上相应的Gradle依赖:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

在前面Retrofit2+RxJava实例中,我们指定GsonConverterFactory作为解析Json数据的Converter,当面对更复杂的需求时,仍然可以通过继承Converter.Factory 来自定义Converter,只需要重写以下这两个方法即可:

  @Overridepublic Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,Retrofit retrofit) {//your own implements}@Overridepublic Converter<?, RequestBody> requestBodyConverter(Type type,Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {//your own implements}

我们不妨来看看GsonConverterFactory 源码,果然GsonConverterFactory 也是继承Converter.Factory 来实现的,重写了responseBodyConverterrequestBodyConverter 这两个方法,代码只有70多行还是很简洁的,如下:

public final class GsonConverterFactory extends Converter.Factory {/*** Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and* decoding from JSON (when no charset is specified by a header) will use UTF-8.*/public static GsonConverterFactory create() {return create(new Gson());}/*** Create an instance using {@code gson} for conversion. Encoding to JSON and* decoding from JSON (when no charset is specified by a header) will use UTF-8.*/public static GsonConverterFactory create(Gson gson) {return new GsonConverterFactory(gson);}private final Gson gson;private GsonConverterFactory(Gson gson) {if (gson == null) throw new NullPointerException("gson == null");this.gson = gson;}@Overridepublic Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,Retrofit retrofit) {TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));return new GsonResponseBodyConverter<>(gson, adapter);}@Overridepublic Converter<?, RequestBody> requestBodyConverter(Type type,Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));return new GsonRequestBodyConverter<>(gson, adapter);}
}

这里需要详细解释一下TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type)) 中的TypeAdapter<?>TypeAdapte是Gson提供的自定义Json解析器,Type就是HTTP请求接口GuokrServicegetGuokrs()方法返回值的泛型类型,如果返回值类型是Call<T>,那么这里的Type就是泛型类型 T ,如果返回值类型是Observable<List<Guokr>> ,那么Type就是List<Guokr>;关于Gson的详细用法可以参考:你真的会用Gson吗?Gson使用指南(四)。

我们看到responseBodyConverter 方法返回的是一个GsonResponseBodyConverter 对象,跟进去看一下GsonResponseBodyConverter 源码,也很简单,源码 如下:

final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {private final Gson gson;private final TypeAdapter<T> adapter;GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {this.gson = gson;this.adapter = adapter;}@Override public T convert(ResponseBody value) throws IOException {JsonReader jsonReader = gson.newJsonReader(value.charStream());try {return adapter.read(jsonReader);} finally {value.close();}}
}

我们看到GsonResponseBodyConverter<T> 实现了Converter<ResponseBody, T>,重写了convert(ResponseBody value) 方法,这就给我们提供了一个思路:自定义Converter关键一步就是要实现Converter<ResponseBody, T> 接口并且重写convert(ResponseBody value) 方法,具体重写的代码我就不贴出来了,可以参考如何使用Retrofit请求非Restful API 这篇博客自定义Converter的做法!

另外,如果需求更复杂,需要我们自定义HTTP请求方法的注解,又该怎么做呢?我们还注意到GsonConverterFactory 类的重写方法responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) 中的Annotation[] methodAnnotations 这个参数,对的,或许你已经猜到了,这就是我们在HTTP请求接口方法中定义的注解,先看 @GET 注解的源码,如下:

/** Make a GET request. */
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {String value() default "";
}

那我们自定义注解的思路也就有了,模仿上面 @GET 注解写一个 @WONDERTWO 注解即可。这里我点到即止,主要是提供一种思路,具体实现仍然可以参考上面提到的 如何使用Retrofit请求非Restful API 这篇博客自定义HTTP请求注解的做法!

0X06 写在后面


有一个结论说的是在网络上,只有 1% 的用户贡献了内容,10% 的用户比较活跃,会评论和点赞,剩下的都是网络透明人,他们只是默默地在看,既不贡献内容,也不点赞。这篇文章希望能让你成为网络上贡献内容的 TOP 1%。如果暂时做不到,那就先点个赞吧,成为活跃的 10%。

参考文献

  1. Retrofit官方文档
  2. Getting started with Retrofit 2
  3. Consuming APIs with Retrofit
  4. Retrofit 2.0: The biggest update yet on the best HTTP Client Library for Android
  5. Retrofit使用指南
  6. Square全家桶正传——Retrofit使用及配合RxJava实现最大效率开发
  7. Retrofit使用OkHttp保存和添加cookie
  8. 如何使用Retrofit请求非Restful API
  9. Retrofit请求参数注解字段说明
  10. OkHttp-->Interceptors

转载于:https://www.cnblogs.com/wondertwo/p/5838528.html

Retrofit 2使用要点梳理:小白进阶回忆录相关推荐

  1. 思维导图结构化梳理Java进阶方向

    思维导图结构化梳理Java进阶方向 写在前面 公众号的后台有读者给我留言说,对java每一阶段应该会什么技术感到迷茫.有个几年经验的爪娃们都经历过成长的阶段,但每个人成长阶段接触到的技术不尽相同.作为 ...

  2. ab753变频器参数怎么拷贝到面板_【干货】一文让你从入门小白进阶为变频器高手...

    点击蓝字 关注我们 为确保 SINAMICS G120 的操作及监控便捷高效,提供了三种不同的操作面板: 1.基本操作面板(BOP-2). 2.智能操作面板(IOP-2) 3.智能连接模块(G120 ...

  3. 百格活动教你16种策略,从活动策划小白进阶为活动策划大神!

    ​ 文/kate 会展资深人士 高级活动策划顾问 策划.组织召开300场会议 职场如战场,看似没有硝烟,实则刀光剑影.如何在职场中脱颖而出.逆流而上,绝对是每一位活动策划者必须要思考和探究的重要课题. ...

  4. 【liteOS】小白进阶之移植 LiteOS 到 STM32

    原文地址::[liteOS]小白进阶之移植 LiteOS 到 STM32_产品人卫朋的博客-CSDN博客 相关文章 1.STM32F103移植LiteOS保姆级教程(基于Huawei-LiteOS-s ...

  5. 小白进阶之Scrapy安装.使用.爬取顶点小说信息

    感谢原作者的文章 小白进阶之Scrapy第一篇 里面写的非常详细,但是转存数据库的时候,用的模块是mysql.connector.这个模块官网显示只支持到python3.5. 我用的则是pymysql ...

  6. 小白进阶之文档快速比较功能 --- 比较两个文档并标记

    小白进阶之文档快速比较功能 --- 比较两个文档并标记 叮嘟!这里是小啊呜的学习课程资料整理.好记性不如烂笔头,今天也是努力进步的一天.一起加油进阶吧! 我们在使用WPS文字办公时,想要快速对比标出两 ...

  7. 用计算机录制一段30,测评!电脑录屏软件哪个好用?小白进阶第1阶段

    原标题:测评!电脑录屏软件哪个好用?小白进阶第1阶段 电脑录屏软件哪个好用?最近短视频越来越火,小卓发现身边不少人都在捣鼓视频,有拍摄的有录屏的比比皆是.问了一下,大多是对此感兴趣的,但是都有不小的问 ...

  8. 小白进阶之百度云加速Error522链接超时解决办法

    小白进阶之百度云加速Error522链接超时解决办法 问题描述 解决方案 具体解决办法处理步骤 问题分析 叮嘟!这里是小啊呜的学习课程资料整理.好记性不如烂笔头,今天也是努力进步的一天.一起加油进阶吧 ...

  9. 全网最全、最新App测试流程及要点梳理

    前言 1985年,加拿大的Therac-25放射治疗机由于软件Bug而发生故障,向患者提供了致命的辐射剂量,造成3人死亡,3人严重受伤. 1994年4月26日,中国航空公司空中客车A300因软件故障而 ...

最新文章

  1. linux中rpm命令管理
  2. HDU - 6598 Harmonious Army (最小割)
  3. 锁定弹出层(jquery语法)
  4. CodeForces - 2B The least round way
  5. 脚本运行显示服务器超时,java执行shell脚本超时
  6. C# winfrom listView
  7. 建立单链表 单链表的插入_单链列表插入
  8. 阿里面试题BIO和NIO数量问题附答案和代码
  9. java script 月日年转年月日_javasrcipt日期一些方法和格式转化
  10. PAT乙级(1016 部分A+B)
  11. Intel 64/x86_64/IA-32/x86处理器 - SIMD指令集 - SSE扩展(6) - 逻辑指令 比较指令
  12. 学习逆向知识之用于游戏外挂的实现.第二讲,快速寻找植物大战僵尸阳光基址.以及动态基址跟静态基址的区别...
  13. MFC程序通过命令行窗口输出cout等语句
  14. C与C++ 算法笔记中的代码
  15. python读音有道-[Python]通过有道词典API获取单词发音MP3
  16. FANUC机器人模拟仿真软件ROBOGUIDE的基本操作介绍(图文)
  17. 关于4418开发和6818开发
  18. 【rmzt:进击的巨人三笠帅气主题】
  19. 【千锋】网络安全学习笔记(一)
  20. 微信小程序连接第三方接口

热门文章

  1. php java if_phpjava(二)
  2. python霍夫变换检测直线_OpenCV-Python教程(9、使用霍夫变换检测直线)
  3. css 图片换行_前端学习口诀VI:html+css口诀结尾篇,值得收藏!
  4. java 命令 乱码_解决java 命令行乱码的问题
  5. Linux系统常用命令速查手册,建议打印
  6. seata-golang 一周年回顾
  7. 计算机 维修 pdf,简单计算机维修..pdf
  8. qt on android 桌面鼠标事件,關於Qt on Android,程序安裝到手機,界面只占到一小部分。...
  9. dao传递类参数 mybatis_MyBatis DAO层传递参数到mapping.xml 几种方式
  10. html dvi如何设置置顶不能空白位置,[html]关于html标签的一些总结