Retrofit 2.0 使用和原理
使用教程:
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
转为RequestBodypublic 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源码的组成:
- 一个
retrofit2.http
包,里面全部是定义HTTP请求的Java注解,比如GET
、POST
、PUT
、DELETE
、Headers
、Path
、Query
等等 - 余下的
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
接口设计的思想非常相似,子类可以实现基于HttpClient
或HttpUrlConnetction
的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
对象就是从ServiceMethod
的toRequest
返回的
总的来说,OkHttpCall
就是调用ServiceMethod
获得一个可以执行的Request
对象,然后等到Http请求返回后,再将response body传入ServiceMethod
中,ServiceMethod
就可以调用Converter
接口将response body转成一个Java对象
结合上面说的就可以看出,ServiceMethod
中几乎保存了一个api请求所有需要的数据,OkHttpCall
需要从ServiceMethod
中获得一个Request
对象,然后得到response后,还需要传入ServiceMethod
用Converter
转换成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 使用和原理的更多相关文章
- Retrofit 2.0 超能实践(一),okHttp完美支持Https传输
http: //blog.csdn.net/sk719887916/article/details/51597816 Tamic首发 前阵子看到圈子里Retrofit 2.0,RxJava(Andro ...
- Retrofit 2.0 超能实践,完美支持Https传输
http://blog.csdn.NET/sk719887916/article/details/51597816 前阵子看到圈子里Retrofit 2.0,RxJava(Android), OkHt ...
- Retrofit 2.0基于OKHttp更高效更快的网络框架 以及自定义转换器
时间关系,本文就 Retrofit 2.0的简单使用 做讲解 至于原理以后有空再去分析 项目全面.简单.易懂 地址: 关于Retrofit 2.0的简单使用如下: https://gitee.c ...
- Android Retrofit 2.0使用
实例带你了解Retrofit 2.0的使用,分享目前开发Retrofit遇到的坑和心得. 添加依赖 app/build.gradle 1 compile 'com.squareup.retrofit2 ...
- Android Retrofit 2.0 使用-补充篇
推荐阅读,猛戳: 1.Android MVP 实例 2.Android Retrofit 2.0使用 3.RxJava 4.RxBus 5.Android MVP+Retrofit+RxJava实践小 ...
- Retrofit 2.0使用
最近在想能不能把之前项目里的网络请求改下 想通过Retrofit重构下,因为Retrofit完美兼容Rxjava后面会用到Rxjava 所以 开个坑写点 由于网上Retrofit 2.0的架构介绍很详 ...
- Retrofit 2.0 超能实践(四),完成大文件断点下载
作者:码小白 文/CSDN 博客 本文出自:http://blog.csdn.net/sk719887916/article/details/51988507 码小白 通过前几篇系统的介绍和综合运用, ...
- Retrofit 2.0 超能实践(三),轻松实现文件/多图片上传/Json字符串
文:http://blog.csdn.net/sk719887916/article/details/51755427 Tamic 简书&csdn同步 通过前两篇姿势的入门 Retrofit ...
- android -------- Retrofit + RxJava2.0 + Kotlin + MVP 开发的 WanAndroid 项目
简介 wanandroid项目基于 Retrofit + RxJava2.0 + Kotlin + MVP 用到的依赖 implementation 'io.reactivex.rxjava2:rxj ...
随机推荐
- 创建窗口句柄时出错(error creating window handle)
创建窗口句柄错误.这个错误非常头疼,难以排查,我从网络上搜集了一些排查方案. 可能的原因: 窗口句柄泄露,句柄数超过1W. 用户对象超过1W,错误提示"当前程序已使用了 Window 管理器 ...
- idea使用Protobuf插件
1.protobuf简介 Protobuf是一个灵活的.高效的用于序列化数据的协议.相比较XML和JSON格式,protobuf更小.更快.更便捷.Protobuf是跨语言的,并且自带了一个编译器(p ...
- linux02
Linux Day 21.命令命令格式: chagrp chgrp命令用于变更文件或目录的所属群组. cd change directory 切换目录 格式:cd 路径 ls list 显示当前目录信 ...
- 分组,命名分组,url的命名和反向解析
1.位置分组 匹配到参数,按照位置参数的方式传递给视图函数 视图函数需要定义形参接收变量 1.写在url里面的: # 删除 url(r'^del_class/(\d+)',views.del_clas ...
- Docker三剑客之Docker Swarm
一.什么是Docker Swarm Swarm是Docker公司推出的用来管理docker集群的平台,几乎全部用GO语言来完成的开发的,代码开源在https://github.com/docker/s ...
- odoo开发笔记 -- 前端开发相关
https://www.cnblogs.com/lyzg/p/5634565.html http://dmyz.org/archives/598 https://www.jianshu.com/p/6 ...
- (转)python WSGI框架详解
原文:https://www.cnblogs.com/shijingjing07/p/6407723.html?utm_source=itdadao&utm_medium=referral h ...
- 全网最详细的PLSQL Developer + Oracle client的客户端 或者 PLSQL Developer + Oracle server服务端的下载与安装过程(图文详解)
不多说,直接上干货! 环境说明: 本地没有安装Oracle服务端,oracle服务端64位,是远程连接,因此本地配置PLSQL Developer64位. Oracle database使用在本机部署 ...
- 软件架构设计学习总结(18):MVC三层架构在各框架(jsp+servlet + Struts1+ Struts2+ springMVC)中的特征
1.基于web开发中最原始的jsp+Servlet 图形化理解jsp+servlet结构: 1.从结构上分析jsp+servlet图解原理: 在基于mvc设计模式下的最原始的jsp+Servlet ...
- 每天一个linux命令(目录)
转:http://www.cnblogs.com/peida/archive/2012/12/05/2803591.html 开始详细系统的学习linux常用命令,坚持每天一个命令,所以这个系列为每天 ...