使用教程:

http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/1016/3588.html

retrofit2 与okhttp关系

http://blog.csdn.net/lmj623565791/article/details/51304204

DEMO,用api调用天气数据

http://blog.csdn.net/a553181867/article/details/52093695

Retrofit2 源码解读

构造方法

Retrofit构造方法采用构建者模式构造,源码如下。

private final Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();//key为接口中定义方法,value为转换过后的方法

  public static final class Builder {
private Platform platform;//平台:安卓、java等
private okhttp3.Call.Factory callFactory; //okhttp的Call工厂类,自定义newCall将Request转为Call
private HttpUrl baseUrl;//okhttp中的类,保存解析过的url
private List<Converter.Factory> converterFactories = new ArrayList<>();//类型转换工厂列表。
private List<CallAdapter.Factory> adapterFactories = new ArrayList<>();//CallAdapter工厂列表。
private Executor callbackExecutor;//回调线程池
private boolean validateEagerly;//急需验证?作用在于直接将所有方法加入前面的map缓存中。 Builder(Platform platform) {
this.platform = platform;
converterFactories.add(new BuiltInConverters());//添加默认的转换器
} public Builder() {
this(Platform.get());//通过Platform.get()获取关于当前平台的实现
} public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
} okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();//默认使用OkHttpClient
} Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {//默认使用平台默认回调线程池
callbackExecutor = platform.defaultCallbackExecutor();
} //将平台默认CallAdapter.Factory加入列表中
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor)); //将默认Converter.Factory加入列表中
List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories); return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly);
}

可以看出,首选通过Platform.get()来获取平台实现然后添加了默认转换工厂。由于我们是Android平台,毋容置疑,接下来就去看看Andoird类的源码。

static class Android extends Platform {
//回调线程池
@Override public Executor defaultCallbackExecutor() {
return new MainThreadExecutor();//主线程
} @Override CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
//ExecutorCallAdapterFactory继承CallAdapter.Factory,内部代理了原来的Call<T>,用于将Callback回调到指定线程中。
return new ExecutorCallAdapterFactory(callbackExecutor);
} static class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper()); @Override public void execute(Runnable r) {
handler.post(r);//采用Hanlder#post回调到主线程
}
}
}

Andoird类的源码主要实现了两个方法,一个是实现了默认的回调线程池,用于在主线程中执行任务,另一个是实现了CallAdapter工厂,通过代理的方式,将执行结果回调到callbackExecutor中去执行。所以,我们只需将callbackExecutor赋值为MainThreadExecutor即可实现主线程间的回调。

BuiltInConverters继承于Converter.Factory,Converter.Factory中有三个方法:

  • public Converter<ResponseBody, ?> responseBodyConverter(xx)用于将ResponseBody转换为指定类型,通常用于对响应结果的类型转换。
  • public Converter<?, RequestBody> requestBodyConverter(xx)用于将指定类型转为RequestBody。一般用于将@Body,@Part,@PartMap转为RequestBody
  • public Converter<?, String> stringConverter用于将指定类型转为String,用于将@Field,@FieldMap,@Path,@Query,@Header等注解的参数类型转为String。

Retrofit提供的用于自定义的方法如下:

  • client(OkHttpClient) 用于自定义客户端
  • callFactory(okhttp3.Call.Factory factory)用于自定义Call工厂,重写newCall将Request转为Call。OkHttpClient就是实现了这个接口。
  • addConverterFactory添加类型转换工厂(Gson转换等)
  • addCallAdapterFactory添加CallAdapter代理工厂,用来代理原始的Call(RxJavaCallAdapter等)。
  • callbackExecutor自定义回调线程池,默认为主线程
  • validateEagerly是否继续验证,是就提前将所有方法转为ServiceMethod放入缓存中,而不是调用一个缓存一个
  • baseUrl用于定义基本链接,必须以”/”结尾

假如基本地址为http://example.com/api/,关于baseUrl与注解中路径的拼接问题如下:

注解中的路径 最终Url (baseUrl为http://example.com/api/
foo/bar/ http://example.com/api/foo/bar/
/foo/bar/ http://example.com/foo/bar/
https://github.com/square/retrofit/ https://github.com/square/retrofit/
//github.com/square/retrofit/ http://github.com/square/retrofit/

接下来看使用:

Retrofit retrofit = new Retrofit.Builder()
.baseUrl(httpurl)
.addConverterFactory(GsonConverterFactory.create())
.build();
IWeather iWeather = retrofit.create(IWeather.class);
Call<City> call = iWeather.getCity(API_KEY, "shenzhen");
call.enqueue(new Callback<City>() {
@Override
public void onResponse(Call<City> call, retrofit2.Response<City> response) {}
@Override
public void onFailure(Call<City> call, Throwable t)
{}
});

以这段代码为例

我给Retrofit对象传了一个IWeather接口的Class对象,怎么又返回一个IWeather对象呢?进入create方法一看,没几行代码,但是我觉得这几行代码就是Retrofit的精妙的地方:

/** Create an implementation of the API defined by the {@code service} interface. */
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, 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 = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});

create方法就是返回了一个Proxy.newProxyInstance动态代理对象。那么问题来了...

动态代理是个什么东西?

看Retrofit代码之前我知道Java动态代理是一个很重要的东西,比如在Spring框架里大量的用到,但是它有什么用呢?

Java动态代理就是给了程序员一种可能:当你要调用某个Class的方法前或后,插入你想要执行的代码

比如你要执行某个操作前,你必须要判断这个用户是否登录,或者你在付款前,你需要判断这个人的账户中存在这么多钱。这么简单的一句话,我相信可以把一个不懂技术的人也讲明白Java动态代理是什么东西了。

为什么要使用动态代理

你看上面代码,获取数据的代码就是这句:

Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");

上面api对象其实是一个动态代理对象,并不是一个真正的ZhuanLanApi接口的implements产生的对象,当api对象调用getAuthor方法时会被动态代理拦截,然后调用Proxy.newProxyInstance方法中的InvocationHandler对象,它的invoke方法会传入3个参数:

  • Object proxy: 代理对象,不关心这个
  • Method method:调用的方法,就是getAuthor方法
  • Object... args:方法的参数,就是"qinchao"

而Retrofit关心的就是method和它的参数args,接下去Retrofit就会用Java反射获取到getAuthor方法的注解信息,配合args参数,创建一个ServiceMethod对象

ServiceMethod就像是一个中央处理器,传入Retrofit对象和Method对象,调用各个接口和解析器,最终生成一个Request,包含api 的域名、path、http请求方法、请求头、是否有body、是否是multipart等等。最后返回一个Call对象,Retrofit2中Call接口的默认实现是OkHttpCall,它默认使用OkHttp3作为底层http请求client

使用Java动态代理的目的就要拦截被调用的Java方法,然后解析这个Java方法的注解,最后生成Request由OkHttp发送

3 Retrofit的源码分析

想要弄清楚Retrofit的细节,先来看一下Retrofit源码的组成:

  1. 一个retrofit2.http包,里面全部是定义HTTP请求的Java注解,比如GETPOSTPUTDELETEHeadersPathQuery等等
  2. 余下的retrofit2包中几个类和接口就是全部retrofit的代码了,代码真的很少,很简单,因为retrofit把网络请求这部分功能全部交给了OkHttp了

Retrofit接口

Retrofit的设计非常插件化而且轻量级,真的是非常高内聚而且低耦合,这个和它的接口设计有关。Retrofit中定义了4个接口:

Callback<T>

这个接口就是retrofit请求数据返回的接口,只有两个方法

  • void onResponse(Response<T> response);
  • void onFailure(Throwable t);

Converter<F, T>

这个接口主要的作用就是将HTTP返回的数据解析成Java对象,主要有Xml、Gson、protobuf等等,你可以在创建Retrofit对象时添加你需要使用的Converter实现(看上面创建Retrofit对象的代码)

Call<T>

这个接口主要的作用就是发送一个HTTP请求,Retrofit默认的实现是OkHttpCall<T>,你可以根据实际情况实现你自己的Call类,这个设计和Volley的HttpStack接口设计的思想非常相似,子类可以实现基于HttpClientHttpUrlConnetction的HTTP请求工具,这种设计非常的插件化,而且灵活

CallAdapter<T>

上面说到过,CallAdapter中属性只有responseType一个,还有一个<R> T adapt(Call<R> call)方法,这个接口的实现类也只有一个,DefaultCallAdapter。这个方法的主要作用就是将Call对象转换成另一个对象,可能是为了支持RxJava才设计这个类的吧

Retrofit的运行过程

上面讲到ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);代码返回了一个动态代理对象,而执行Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");代码时返回了一个OkHttpCall对象,拿到这个Call对象才能执行HTTP请求

上面api对象其实是一个动态代理对象,并不是一个真正的ZhuanLanApi接口的implements产生的对象,当api对象调用getAuthor方法时会被动态代理拦截,然后调用Proxy.newProxyInstance方法中的InvocationHandler对象, 创建一个ServiceMethod对象

ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

loadServiceMethod的方法很简陋,直接传入了Method然后build(),那么这个build()又做了什么?

ServiceMethod loadServiceMethod(Method method) {
ServiceMethod result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);//首先从map中取看看是否已经缓存过
if (result == null) {//否则构造ServiceMethod。
result = new ServiceMethod.Builder(this, method).build();
serviceMethodCache.put(method, result);//缓存map中
}
}
return result;
} public Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();//赋值方法注解数组
this.parameterTypes = method.getGenericParameterTypes();//赋值参数类型数组
this.parameterAnnotationsArray = method.getParameterAnnotations();//赋值参数注解数组
} public ServiceMethod build() {
callAdapter = createCallAdapter();//创建CallAdapter,用来代理Call
responseType = callAdapter.responseType();//获取返回类型
//...
responseConverter = createResponseConverter();//创建ResponseConverter,用来转换ResponseBody为指定类型 for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);//遍历解析方法注解
}
//...
int parameterCount = parameterAnnotationsArray.length;//parameterAnnotationsArray为参数注解数组
parameterHandlers = new ParameterHandler<?>[parameterCount];//初始化ParameterHandler,用来处理参数相关
for (int p = 0; p < parameterCount; p++) {//遍历参数注解数组
Type parameterType = parameterTypes[p];//获取参数类型
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];//获取参数注解数组
//...
//通过注解和参数类型,解析并赋值到parameterHandlers中
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
} //... return new ServiceMethod<>(this);
}

创建ServiceMethod

刚才说到,ServiceMethod就像是一个中央处理器,具体来看一下创建这个ServiceMethod的过程是怎么样的

第一步,获取到上面说到的3个接口对象:

callAdapter = createCallAdapter();
responseType = callAdapter.responseType();
responseConverter = createResponseConverter();

第二步,解析Method的注解,主要就是获取Http请求的方法,比如是GET还是POST还是其他形式,如果没有,程序就会报错,还会做一系列的检查,比如如果在方法上注解了@Multipart,但是Http请求方法是GET,同样也会报错。因此,在注解Java方法是需要严谨

for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
} if (httpMethod == null) {
throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}

parseMethodAnnotation用于遍历解析方法上的注解,比如请求方法,请求头之类的。

private void parseMethodAnnotation(Annotation annotation) {
//请求方法注解
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
if (!Void.class.equals(responseType)) {
throw methodError("HEAD method must use Void as response type.");
}
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
//自定义HTTP请求注解
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
//请求头注解
String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
if (headersToParse.length == 0) {
throw methodError("@Headers annotation is empty.");
}
headers = parseHeaders(headersToParse);//解析Header
} else if (annotation instanceof Multipart) {
//Multipart
if (isFormEncoded) {
throw methodError("Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
//FormUrlEncoded
if (isMultipart) {
throw methodError("Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}

从上面源码可以看出,使用parseHttpMethodAndPath这个方法用于解析请求方法注解和路径参数保存到Set中

private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
if (this.httpMethod != null) {
throw methodError("Only one HTTP method is allowed. Found: %s and %s.",
this.httpMethod, httpMethod);
}
this.httpMethod = httpMethod;
this.hasBody = hasBody; if (value.isEmpty()) {
return;
} int question = value.indexOf('?');//查询参数开始的符号
if (question != -1 && question < value.length() - 1) {
//如果在查询参数中使用了{},则抛出异常。
String queryParams = value.substring(question + 1);
Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
if (queryParamMatcher.find()) {
throw methodError("URL query string \"%s\" must not have replace block. "
+ "For dynamic query parameters use @Query.", queryParams);
}
} //赋值相对链接
this.relativeUrl = value;
//解析{}路径参数保存到Set中
this.relativeUrlParamNames = parsePathParameters(value);
}

从源码可以发现,不允许在查询参数中使用{}进行占位,否则就会抛出异常,然后将请求方法中的注解值赋值给relativeUrl,通过parsePathParameters将{}路径参数保存到Set中。

第三步,比如上面api中带有一个参数{user},这是一个占位符,而真实的参数值在Java方法中传入,那么Retrofit会使用一个ParameterHandler来进行替换:

int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];

最后,ServiceMethod会做其他的检查,比如用了@FormUrlEncoded注解,那么方法参数中必须至少有一个@Field@FieldMap

执行Http请求

之前讲到,OkHttpCall是实现了Call接口的,并且是真正调用OkHttp3发送Http请求的类。OkHttp3发送一个Http请求需要一个Request对象,而这个Request对象就是从ServiceMethodtoRequest返回的

总的来说,OkHttpCall就是调用ServiceMethod获得一个可以执行的Request对象,然后等到Http请求返回后,再将response body传入ServiceMethod中,ServiceMethod就可以调用Converter接口将response body转成一个Java对象

结合上面说的就可以看出,ServiceMethod中几乎保存了一个api请求所有需要的数据,OkHttpCall需要从ServiceMethod中获得一个Request对象,然后得到response后,还需要传入ServiceMethodConverter转换成Java对象

你可能会觉得我只要发送一个HTTP请求,你要做这么多事情不会很“慢”吗?不会很浪费性能吗?

我觉得,首先现在手机处理器主频非常高了,解析这个接口可能就花1ms可能更少的时间(我没有测试过),面对一个HTTP本来就需要几百ms,甚至几千ms来说不值得一提;而且Retrofit会对解析过的请求进行缓存,就在Map<Method, ServiceMethod> serviceMethodCache = new LinkedHashMap<>();这个对象中

调用服务接口

以上介绍了retrofit.create(XX.class);用于创建服务方法接口的过程。现在该进行相关调用了。我们知道,创建服务方法会返回一个Call<XX>对象,通过Call<XX>可以进行相关异步同步调用。

@Override public void enqueue(final Callback<T> callback) {
if (callback == null) throw new NullPointerException("callback == null"); okhttp3.Call call;//okhttp中的call对象
Throwable failure; synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true; call = rawCall;
failure = creationFailure;
if (call == null && failure == null) {
try {
call = rawCall = createRawCall();//创建原始Call,即okhttp中的Call对象
} catch (Throwable t) {
failure = creationFailure = t;
}
}
} if (failure != null) {
callback.onFailure(this, failure);
return;
} if (canceled) {
call.cancel();
} //通过enqueue进行异步调用
call.enqueue(new okhttp3.Callback() {
@Override public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse)
throws IOException {
Response<T> response;
try {
response = parseResponse(rawResponse);//解析响应内容
} catch (Throwable e) {
callFailure(e);
return;
}
callSuccess(response);
}
}

从源码不难看出,首先通过createRawCall()来创建OkHttp中的Call对象,然后通过Call进行异步/同步调用,获取结果后通过parseResponse解析OkHttp中的Response,然后进行相应回调。 
createRawCall()用于创建OkHttp中的Call对象,源码如下:

private okhttp3.Call createRawCall() throws IOException {
Request request = serviceMethod.toRequest(args);//通过 serviceMethod.toRequest进行转换成Request
okhttp3.Call call = serviceMethod.callFactory.newCall(request);//调用newCall返回Call对象,默认callFactory为OkHttpClient,OkHttpClient也实现了okhttp3.Call.Factory接口。
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}

4 最后

Retrofit非常巧妙的用注解来描述一个HTTP请求,将一个HTTP请求抽象成一个Java接口,然后用了Java动态代理的方式,动态的将这个接口的注解“翻译”成一个HTTP请求,最后再执行这个HTTP请求

Retrofit的功能非常多的依赖Java反射,代码中其实还有很多细节,比如异常的捕获、抛出和处理,大量的Factory设计模式(为什么要这么多使用Factory模式?)

Retrofit中接口设计的恰到好处,在你创建Retrofit对象时,让你有更多更灵活的方式去处理你的需求,比如使用不同的Converter、使用不同的CallAdapter,这也就提供了你使用RxJava来调用Retrofit的可能

Retrofit 2.0 使用和原理的更多相关文章

  1. Retrofit 2.0 超能实践(一),okHttp完美支持Https传输

    http: //blog.csdn.net/sk719887916/article/details/51597816 Tamic首发 前阵子看到圈子里Retrofit 2.0,RxJava(Andro ...

  2. Retrofit 2.0 超能实践,完美支持Https传输

    http://blog.csdn.NET/sk719887916/article/details/51597816 前阵子看到圈子里Retrofit 2.0,RxJava(Android), OkHt ...

  3. Retrofit 2.0基于OKHttp更高效更快的网络框架 以及自定义转换器

    时间关系,本文就 Retrofit 2.0的简单使用 做讲解  至于原理以后有空再去分析 项目全面.简单.易懂  地址: 关于Retrofit 2.0的简单使用如下:  https://gitee.c ...

  4. Android Retrofit 2.0使用

    实例带你了解Retrofit 2.0的使用,分享目前开发Retrofit遇到的坑和心得. 添加依赖 app/build.gradle 1 compile 'com.squareup.retrofit2 ...

  5. Android Retrofit 2.0 使用-补充篇

    推荐阅读,猛戳: 1.Android MVP 实例 2.Android Retrofit 2.0使用 3.RxJava 4.RxBus 5.Android MVP+Retrofit+RxJava实践小 ...

  6. Retrofit 2.0使用

    最近在想能不能把之前项目里的网络请求改下 想通过Retrofit重构下,因为Retrofit完美兼容Rxjava后面会用到Rxjava 所以 开个坑写点 由于网上Retrofit 2.0的架构介绍很详 ...

  7. Retrofit 2.0 超能实践(四),完成大文件断点下载

    作者:码小白 文/CSDN 博客 本文出自:http://blog.csdn.net/sk719887916/article/details/51988507 码小白 通过前几篇系统的介绍和综合运用, ...

  8. Retrofit 2.0 超能实践(三),轻松实现文件/多图片上传/Json字符串

    文:http://blog.csdn.net/sk719887916/article/details/51755427 Tamic 简书&csdn同步 通过前两篇姿势的入门 Retrofit ...

  9. android -------- Retrofit + RxJava2.0 + Kotlin + MVP 开发的 WanAndroid 项目

    简介 wanandroid项目基于 Retrofit + RxJava2.0 + Kotlin + MVP 用到的依赖 implementation 'io.reactivex.rxjava2:rxj ...

随机推荐

  1. 【BZOJ4755】 [Jsoi2016]扭动的回文串

    BZOJ4755 [Jsoi2016]扭动的回文串 Solution 考虑对于他给出的 A中的一个回文串: B中的一个回文串: 或者某一个回文的扭动字符串S(i,j,k) 这样子几个限制,我们1,2就 ...

  2. 最小割(zjoi2011,bzoj2229)(最小割树)

    小白在图论课上学到了一个新的概念--最小割,下课后小白在笔记本上写下了如下这段话: "对于一个图,某个对图中结点的划分将图中所有结点分成两个部分,如果结点\(s,t\)不在同一个部分中,则称 ...

  3. php获取指定日期的前一天,前一月,前一年日期

    ## php获取指定日期的前一天,前一月,前一年日期   前一天的日期为: date("Y-m-d",strtotime("-1 days",strtotime ...

  4. Vue 项目配置

    配置Vue的app项目首先需要配置本地环境. 1.下载node.js并且安装.(根据自己电脑参数进行选择) 打开cmd,检查是否安装成功. 分别输入: node -v npm -v 结果如图正确显示出 ...

  5. JAVA实现微信支付V3

    喜欢的朋友可以关注下,粉丝也缺. 相信很多的码友在项目中都需要接入微信支付,虽说微信支付已成为一个普遍的现象,但是接入的过程中难免会遇到各种各样的坑,这一点支付宝的SDK就做的很好,已经完成的都知道了 ...

  6. centos 安装nginx笔记

    添加nginx 存储库 yum install epel-release 安装 yum install nginx 启动 systemctl start nginx

  7. 两台linux主机使用unison + inotify实现web文件夹同步

    两台服务器同步数据 unison 是一款跨平台的文件同步对象,不仅支撑本地对本地同步,也支持通过SSH,RSH和Socket 等网络协议进行同步. unison 支持双向同步,你可以同A同步到B ,也 ...

  8. 【sping揭秘】6、IOC容器之统一资源加载策略

    Spring中的resource 我们先看看类之间的关系 注意我们的application是间接继承了resourceloader的,也就是说我们的application其实就是一个resourcel ...

  9. oracle跨平台数据迁移 expdp/impdp 字符集问题 导致ORA-02374 ORA-12899 ORA-02372

    环境描述: 源数据库环境:     操作系统:Windows SERVER 2008R2     数据库版本:单实例 ORACLE 11.2.0.1 目标端数据库环境:     操作系统:redhat ...

  10. 剑指offer三从头到尾打印链表

    一.题目: 输入一个链表,从尾到头打印链表每个节点的值. 二.解题方法: 方法一:采用递归的方式实现 方法二:借助堆栈的“后进先出”实现 import java.util.ArrayList; imp ...