一、Retrofit简介
Retrofit是现下Android端开发非常流行的一款网络请求框架,它通过动态代理的方式将Java接口翻译成网络请求,通过OkHttp发送请求,并且其具备强大的可扩展性,支持各种数据格式的转换以及RxJava。说到这里,我们来分析一下网络请求框架的本质,网络请求框架是一套提供给开发者使用的用于网络请求的API接口,我们知道,Android网络请求一般是基于Http协议的,而Http协议属于应用层的协议,具体的数据传输需要依赖传输层的TCP协议,Android系统提供了Socket编程接口给开发者建立TCP请求,所以具体数据的发送需要依赖Socket;Http协议属于应用层的协议,它是用来规定数据的传输格式的,用于传输双方都能按照固定的格式解读数据;所以,一个网络请求框架至少要包含以下几个功能:
1、提供接口给开发者传入请求参数;
2、编写Socket代码,建立TCP;
3、通过TCP连接,严格按照Http协议的格式将请求参数发送给服务端;
4、严格按照Http协议的格式解读服务端返回的数据;
5、提供相应的接口给开发者获得返回数据(一般是通过回调处理的)。
上面五点是一个网络请求框架必须具备的功能,当然,一个好的网络请求框架还应该具备如下特点:
1、提供给开发者使用的API尽可能简单;
2、添加了对网络缓存的处理,避免不必要的请求;
3、具有较高的性能;
4、具有较高的可扩展性。
我们常用的OkHttp、HttpURLConnection等网络请求框架,其内部就会将开发者传入的请求参数按照Http协议的格式组织好,并通过TCP连接发送给服务端,在收到服务端返回数据后,又会按照Http协议去解读数据,并提供相应的API(一般是回调)给开发者获得数据。由于OkHttp属于比较底层的网络请求框架,开发者在使用时还是会比较复杂,于是Retrofit对OkHttp进行了再度的封装,使得开发者在使用时更加的方便。
二、使用Retrofit请求网络数据的基本流程
1、在build中添加依赖
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
2、定义请求接口类
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
/**
* Created by dell on 2018/9/6.
*/
public interface TranslateApi {
@POST("ajax.php?a=fy&f=auto&t=auto")
@FormUrlEncoded
Call<TranslateBean> translateRequest(@Field("w") String input);
}
3、创建Retrofit实例
Retrofit retrofit = new Retrofit.Builder() // 通过Builder模式构造一个Retrofit对象
.baseUrl("http://fy.iciba.com/") // 配置HOST
.addConverterFactory(GsonConverterFactory.create()) // 添加解读返回数据的对象,因为返回数据是Json格式的,所以这里采用GsonConverterFactory来解读
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 添加适配OkHttpCall对象的CallAdapterFactory
.build();
4、根据请求接口生成具体的请求实体对象
TranslateApi translateApi = retrofit.create(TranslateApi.class); // 传入定义的接口,获得一个实现了TranslateApi接口的实体对象
5、调用请求对象的请求方法生成能够发起网络请求的Call对象
Call<TranslateBean> call = translateApi.translateRequest(mInputEditText.getText().toString()); // 调用请求方法,返回一个Call对象
6、调用Call对象的enqueue方法发起异步网络请求
call.enqueue(new Callback<TranslateBean>() { // 调用Call对象的enqueue方法发起异步网络请求,enqueue方法需要传入一个Callback对象,用来处理网络请求的回调
@Override
public void onResponse(Call<TranslateBean> call, Response<TranslateBean> response) { // 网络请求正确的回调
TranslateBean translateBean = response.body(); // 处理返回结果
if (translateBean.getContent().getOut() != null) {
mOutputText.setText(translateBean.getContent().getOut());
} else {
mOutputText.setText(translateBean.getContent().getWordMeanString());
}
}
@Override
public void onFailure(Call<TranslateBean> call, Throwable t) { // 网络请求错误的回调
mOutputText.setText("翻译出错了!");
}
总结一下,首先我们定义了网络请求的接口,接口中配置了请求的基本信息,包括请求方法、请求参数等;接着我们生成了一个Retrofit对象,在这里可以配置请求的默认Host信息、添加解析返回数据格式的Factory、添加适配OkHttpCall的Factory等功能;然后调用retrofit对象的create方法,传入网络请求接口类,生成一个具体的网络请求实体对象;接下来我们调用了请求方法生成一个能够发起网络请求的Call对象,最后调用Call对象的enquene方法将发起异步的网络请求,并且在传入的Callback中处理网络请求的回调。
三、Retrofit原理分析
从上面的使用方法可以看出,Retrofit的核心是根据接口生成一个能够发起网络请求的对象,然后根据这个对象再发起网络请求
1、生成Retrofit对象
public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient(); // 如果没有设置网络请求框架,模式使用OkHttp处理网络请求
}
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor(); // 默认使用platform.defaultCallbackExecutor()处理返回结果
}
// Make a defensive copy of the adapters and add the default Call adapter.
List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));
// Make a defensive copy of the converters.
List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);
return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly); // 根据配置信息生成一个Retrofit对象
}
}
Retrofit通过build模式来生成一个Retrofit对象,通过代码我们知道,Retrofit默认会使用OkHttp来发送网络请求,当然,我们也可以自己定制。
2、create方法根据接口生成具体的网络请求实体类
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, @Nullable 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<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method); // 根据方法生成一个ServiceMethod对象(内部会将生成的ServiceMethod放入在缓存中,如果已经生成过则直接从缓存中获取)
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args); // 根据ServiceMethod对象和请求参数生成一个OkHttpCall对象,这个OkHttpCall能够调用OkHttp的接口发起网络请求
return serviceMethod.callAdapter.adapt(okHttpCall); // 调用serviceMethod的callAdapter的adapt方法,并传入okHttpCall,返回一个对象,这个的目的主要是为了适配返回类型,其内部会对OkhttpCall对象进行包装
}
});
}
Retrofit的create方法通过动态代理的模式,生成了实现了具体的网络请求接口的对象,并在InvocationHandler的invoke方法中统一处理网络请求接口实体对象的方法,invoke方法会通过方法构造一个ServiceMethod对象,并将其放入缓存中,然后根据ServiceMethod对象和网络请求的参数args去构造一个OkHttpCall对象,最后调用serviceMethod的callAdapter的adapt方法,传入将OkHttpCall对象,callAdapter的目的主要是为了适配OkHttpCall对象,其内部会对OkHttpCall对象进行包装,生成对应返回类型的对象。
动态代理的原理主要是在运行时动态生成代理类,然后根据代理类生成一个代理对象,在这个代理对象的方法中中又会调用InvocationHandler的invoke来转发对方法的处理,比如,TranslateApi生成的代码类代码大致应该如下:
public Class Translate implement Translate {
InvokeHandler mInvokeHandler;
Call<TranslateBean> translateRequest(String input) {
Method translateRequestMethod = Class.forName("packagename.TranslateApi").getMethod("translateRequest", String.class); // 通过反射获得TranslateApi的translateRequest方法
mInvokeHandler.invoke(this, translateRequestMethod, new Object[] {input}); // 调用InvokeHandler的invoke方法,并传入当前对象,方法,方法传入参数
}
}
代理类的代码是动态生成的,生成代码后我们就可以用ClassLoader将其加载到内存中,并通过反射生成代理对象,代理类会将方法的处理转发给InvokeHandler,所以所有对代理对象方法的调用都会由InvocationHandler的invoke方法处理。
3、loadServiceMethod方法
ServiceMethod<?, ?> loadServiceMethod(Method method) { // 根据方法返回一个对应的ServiceMethod对象
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;
}
loadServiceMethod首先会从缓存中获取ServiceMethod对象,如果没有,则通过Method和Retrofit对象构造一个ServiceMethod对象,并将其放入缓存中。
4、ServiceMethod的构造
ServiceMethod其实是用来存储一次网络请求的基本信息的,比如Host、URL、请求方法等,同时ServiceMethod还会存储用来适配OkHttpCall对象的CallAdpater。ServiceMethod的build方法会解读传入的Method,首先ServiceMethod会在CallAdpaterFactory列表中寻找合适的CallAdapter来包装OkHttpCall对象,这一步主要是根据Method的返回参数来匹配的,比如如果方法的返回参数是Call对象,那么ServiceMethod就会使用默认的CallAdpaterFactory来生成CallAdpater,而如果返回对象是RxJava的Obserable对象,则会使用RxJavaCallAdapterFactory提供的CallAdpater。然后build方法会解读Method的注解,来获得注解上配置的网络请求信息,比如请求方法、URL、Header等。
public ServiceMethod build() {
callAdapter = createCallAdapter(); // 查找能够适配返回类型的CallAdpater
responseType = callAdapter.responseType();
if (responseType == Response.class || responseType == okhttp3.Response.class) {
throw methodError("'"
+ Utils.getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
responseConverter = createResponseConverter();
// 解读方法的注解
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
if (httpMethod == null) {
throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
if (!hasBody) {
if (isMultipart) {
throw methodError(
"Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
}
if (isFormEncoded) {
throw methodError("FormUrlEncoded can only be specified on HTTP methods with "
+ "request body (e.g., @POST).");
}
}
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
Type parameterType = parameterTypes[p];
if (Utils.hasUnresolvableType(parameterType)) {
throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
parameterType);
}
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
if (parameterAnnotations == null) {
throw parameterError(p, "No Retrofit annotation found.");
}
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
}
if (relativeUrl == null && !gotUrl) {
throw methodError("Missing either @%s URL or @Url parameter.", httpMethod);
}
if (!isFormEncoded && !isMultipart && !hasBody && gotBody) {
throw methodError("Non-body HTTP method cannot contain @Body.");
}
if (isFormEncoded && !gotField) {
throw methodError("Form-encoded method must contain at least one @Field.");
}
if (isMultipart && !gotPart) {
throw methodError("Multipart method must contain at least one @Part.");
}
return new ServiceMethod<>(this);
}
我们来看一下查找CallAdpater的代码:
private CallAdapter<T, R> createCallAdapter() {
Type returnType = method.getGenericReturnType(); // 获得方法的返回类型
if (Utils.hasUnresolvableType(returnType)) {
throw methodError(
"Method return type must not include a type variable or wildcard: %s", returnType);
}
if (returnType == void.class) {
throw methodError("Service methods cannot return void.");
}
Annotation[] annotations = method.getAnnotations();
try {
//noinspection unchecked
return (CallAdapter<T, R>) retrofit.callAdapter(returnType, annotations); // 调用retrofit的callAdpater方法查找合适的CallAdpater
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(e, "Unable to create call adapter for %s", returnType);
}
}
可以看到,会调用retrofit的callAdapter去查找合适的CallAdapter,传入的参数为方法的返回类型和注解
Retrofit的callAdapter方法:
public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
Annotation[] annotations) {
checkNotNull(returnType, "returnType == null");
checkNotNull(annotations, "annotations == null");
int start = adapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = adapterFactories.size(); i < count; i++) { // 遍历所有的CallAdpaterFactory,找到能够适配的CallAdpater
CallAdapter<?, ?> adapter = adapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
...
}
Retrofit的CallAdpater方法会遍历所有的CallAdpaterFactory,找到能够适配的CallAdpater,我们首先来看一下默认的CallAdpaterFactory是如何生成CallAdapter的:
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
static final CallAdapter.Factory INSTANCE = new DefaultCallAdapterFactory();
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
if (getRawType(returnType) != Call.class) { // 首先判断方法的返回类型是不是Call类型,如果不是则说明这个CallAdpaterFactory适配不了
return null;
}
final Type responseType = Utils.getCallResponseType(returnType);
return new CallAdapter<Object, Call<?>>() { // 返回CallAdpater
@Override public Type responseType() {
return responseType;
}
@Override public Call<Object> adapt(Call<Object> call) {
return call; // 直接将call返回
}
};
}
}
DefaultCallAdapterFactory是默认的CallAdpaterFactory,它在Retrofit构造时会加入到CallAdapterFactory列表中,可以看到,它只会去适配返回类型为Call的方法。
我们再来看一下返回类型为Observable是如何适配的:
首先,我们在构造Retrofit对象时需要加入适配RxJava返回对象的
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
我们来看一下RxJava2CallAdpaterFactory的get方法:
@Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
Class<?> rawType = getRawType(returnType); // 获得返回类型
if (rawType == Completable.class) { // 如果返回类型为RxJava的Completable类型,则可以适配
// Completable is not parameterized (which is what the rest of this method deals with) so it
// can only be created with a single configuration.
return new RxJava2CallAdapter(Void.class, scheduler, isAsync, false, true, false, false,
false, true);
}
boolean isFlowable = rawType == Flowable.class;
boolean isSingle = rawType == Single.class;
boolean isMaybe = rawType == Maybe.class;
// 如果返回类型为RxJava的Flowable、Single、Maybe、Observable类型,则可以适配
if (rawType != Observable.class && !isFlowable && !isSingle && !isMaybe) {
return null;
}
boolean isResult = false;
boolean isBody = false;
Type responseType;
if (!(returnType instanceof ParameterizedType)) {
String name = isFlowable ? "Flowable"
: isSingle ? "Single"
: isMaybe ? "Maybe" : "Observable";
throw new IllegalStateException(name + " return type must be parameterized"
+ " as " + name + "<Foo> or " + name + "<? extends Foo>");
}
Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
Class<?> rawObservableType = getRawType(observableType);
if (rawObservableType == Response.class) {
if (!(observableType instanceof ParameterizedType)) {
throw new IllegalStateException("Response must be parameterized"
+ " as Response<Foo> or Response<? extends Foo>");
}
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
} else if (rawObservableType == Result.class) {
if (!(observableType instanceof ParameterizedType)) {
throw new IllegalStateException("Result must be parameterized"
+ " as Result<Foo> or Result<? extends Foo>");
}
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
isResult = true;
} else {
responseType = observableType;
isBody = true;
}
return new RxJava2CallAdapter(responseType, scheduler, isAsync, isResult, isBody, isFlowable,
isSingle, isMaybe, false);
}
可以看到,RxJava2CallAdpaterFactory能够处理返回类型为RxJava的Completable、Flowable、Single、Maybe、Observable类型,并提供一个RxJava2CallAdpater适配器。
5、根据ServiceMethod和args生成OkHttpCall对象
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
我们知道,ServiceMethod封装了网络请求的基本信息,比如Host、URL等,我们根据ServiceMethod和请求参数args就可以确定本次网络请求的所有信息了,OkHttpCall主要是将这些信息封装起来,并调用OkHttp的接口去发送网络请求,这里,我们就将OkHttpCall看成是一个处理网络请求的类即可。
6、serviceMethod.callAdpater.adpat(okHttpCall)
这一步主要是将能够处理网络请求的OkHttpCall对象通过适配器适配成方法返回类型的对象,以Observable为例,我们知道RxJava和Retrofit结合使用的代码大致如下:
Observable<DataBean> observable = requestApi.request("xiaoming");
observable.subscribleOn(Sechedulers.io())
.observerOn(AndroidSecheduler.mainThread())
.subscrible(new Observer() {
public void onNext(DataBean dataBean) {
}
public void onError(Error e) {
}
...
})
也就是说我们通过调用接口返回了一个observable(被观察者)对象,然后给obserable绑定了一个Observer(观察者)对象,并且指定了处理的subscrible线程为子线程,处理observer回调的线程为主线程。
那么RxJava2CallAdapter的adapt方法是如何将一个Call对象适配成Observable对象的呢?
final class RxJava2CallAdapter<R> implements CallAdapter<R, Object> {
@Override public Object adapt(Call<R> call) {
Observable<Response<R>> responseObservable = isAsync
? new CallEnqueueObservable<>(call)
: new CallExecuteObservable<>(call); // 生成自定义的Observable对象,并且其中封装了进行网络请求的Call对象
Observable<?> observable;
if (isResult) {
observable = new ResultObservable<>(responseObservable);
} else if (isBody) {
observable = new BodyObservable<>(responseObservable);
} else {
observable = responseObservable;
}
if (scheduler != null) {
observable = observable.subscribeOn(scheduler);
}
if (isFlowable) {
return observable.toFlowable(BackpressureStrategy.LATEST);
}
if (isSingle) {
return observable.singleOrError();
}
if (isMaybe) {
return observable.singleElement();
}
if (isCompletable) {
return observable.ignoreElements();
}
return observable;
}
}
主要看第一行代码:
Observable<Response<R>> responseObservable = isAsync
? new CallEnqueueObservable<>(call)
: new CallExecuteObservable<>(call); // 生成自定义的Observable对象,并且其中封装了进行网络请求的Call对象
这句代码会将能够进行网络请求的Call对象封装成一个自定义的Observable对象并返回,以CallExecuteObservable为例:
final class CallExecuteObservable<T> extends Observable<Response<T>> {
private final Call<T> originalCall; // 能够进行网络请求的Call对象,也就是我们前面说的OkHttpCall对象
CallExecuteObservable(Call<T> originalCall) {
this.originalCall = originalCall;
}
@Override protected void subscribeActual(Observer<? super Response<T>> observer) { // 这个方法会在Observable调用subscribe方法订阅观察者时调用
// Since Call is a one-shot type, clone it for each new observer.
Call<T> call = originalCall.clone();
observer.onSubscribe(new CallDisposable(call));
boolean terminated = false;
try {
Response<T> response = call.execute(); // 调用OkHttpCall对象,执行网络请求并获得响应结果
if (!call.isCanceled()) {
observer.onNext(response); // 调用Obserber的onNext方法,发送返回接口
}
if (!call.isCanceled()) {
terminated = true;
observer.onComplete();
}
} catch (Throwable t) {
Exceptions.throwIfFatal(t);
if (terminated) {
RxJavaPlugins.onError(t);
} else if (!call.isCanceled()) {
try {
observer.onError(t); // 请求出错则调用Observer的onError方法
} catch (Throwable inner) {
Exceptions.throwIfFatal(inner);
RxJavaPlugins.onError(new CompositeException(t, inner));
}
}
}
}
...
}
可以看到CallExecuteObserable继承了Obserable,并重写了其subscribeActual方法,subscribeActual会在Obserable对象调用subcrible方法时调用,在subscribeActual方法中,首先是调用了OkHttpCall的execute()方法发起网络请求,并获得网络请求结果,如果请求成功,会调用Observer的onNext方法,并将请求结果传递给onNext方法,所以我们可以在Observer的onNext方法种处理网络请求成功的情况,而如果网络请求失败,则会调用Observer的onError方法。由上,我们知道,CallAdapter的作用是将OkHttpCall对象适配成方法的返回类型的对象。
至此,我们知道了Retrofit的原理,它内部通过动态代理生成接口的实体对象,然后通过解读注解来获得接口中定义的请求信息,通过CallAdapterFactory将OkHttpCall对象适配成接口中定义的返回类型,通过ConverFactory来解读数据,其底层真正处理网络请求的还是OkHttp框架(OkHttpCall通过调用OkHttp框架提供的Api处理网络请求)。
四、总结
Retrofit是一款能够将Java接口转换成一个能够进行网络请求对象的框架,具有使用简单,可扩展性强等优点,其内部通过动态代理模式生成接口的实体对象,并且在InvocationHandler中统一处理请求方法,通过解读方法的注解来获得接口中配置的网络请求信息,并将网络请求信息和请求参数一起封装成一个OkHttpCall对象,这个OkHttpCall对象内部通过OkHttp提供的Api来处理网络请求,为了将OkHttpCall对象适配成方法的返回类型,Retrofit提供了配置CallAdpaterFactory的Api,比如RxJava2CallAdapterFactory就会将OkHttpCall对象适配成一个Observable对象,并在Obserable的subscribleActual方法中调用OkHttpCall对象发起网络请求并回调Observser的onNext方法来处理网络请求返回的数据。Retrofit还提供了配置数据格式转换的API,可以针对不同的数据类型进行处理。
反观一下Retrofit,其内部的设计结构非常清晰,通过动态代理来处理接口,通过OkHttp来处理网络请求,通过CallAdapterFactory来适配OkHttpCall,通过ConverterFactory来处理数据格式的转换,这符合面对对象设计思想的单一职责原则,同时,Retrofit对CallAdpaterFactory和ConverterFactory的依赖都是依赖其接口的,这就让我们可以非常方便的扩展自己的CallAdpaterFactory和ConverterFactory,这符合依赖倒置原则;不管Retrofit内部的实现如何复杂,比如动态代理的实现、针对注解的处理以及寻找合适的适配器等,Retrofit对开发者隐藏了这些实现细节,只提供了简单的Api给开发者调用,开发者只需要关注通过的Api即可实现网络请求,这种对外隐藏具体的实现细节的思想符合迪米特原则。另外,Retrofit内部大量使用了设计模式,比如构造Retrofit对象时使用了Builder模式,处理接口时是用来动态代理模式,适配OkHttpCall时使用了Adapter模式,生成CallAdpater和Converter时使用了工厂模式。Retrofit的设计正是因为遵循了面向对象的思想,以及对设计模式的正确应用,才使得其具备结构清晰、易于扩展的特点。
————————————————
版权声明:本文为CSDN博主「xiao_nian」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiao_nian/article/details/87802483