最近非常流行 Retrofit+RxJava+OkHttp 这一整套的网络请求和异步操作的开源框架,从 Jake Wharton 的 Github 主页也能看出其趋势。
Retrofit+RxJava+OkHttp.png本文主要介绍 Retrofit 的基本原理,基于 2.1.0 版本的源码。
1. 基本用法
1.1 定义 API 接口
Retrofit
的使用非常简单,先来看一个 官网 上的示例代码。
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
官方的解释是
Retrofit turns your HTTP API into a Java interface.
首先定义了一个 API 接口 GitHubService
,包含 HTTP 请求的方法 GET
和参数 user
,及成功后的返回类型 List<Repo>
,方法和参数由注解声明,非常清晰。
1.2 创建 Retrofit 对象并生成 API 实例
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
然后创建一个 Retrofit
对象,这里采用 Builder 模式,传入 baseUrl
和 ConverterFactory
等参数,后面会讲到。
通过 Retrofit
对象用动态代理的方式生成我们需要的 API 实例 GitHubService
。
1.3 API 实例去请求服务
Call<List<Repo>> repoCall = service.listRepos("danke77");
用生成的 API 实例调用相应的方法,默认返回 Call<T>
,然后调用 Call#execute
方法同步或调用 Call#enqueue
方法异步请求 HTTP。
List<Repo> repos = repoCall.execute().body();
请求返回的数据直接转化成了 List<Repo>
,非常方便。
如果要使用 RxJava,在创建 Retrofit
对象时要调用 addCallAdapterFactory(RxJavaCallAdapterFactory.create())
,则返回类型会从 Call<T>
转换成 Observable<T>
。
2. Retrofit
2.1 build
先看一下 Retrofit.Builder
类里的成员变量。
private Platform platform;
private okhttp3.Call.Factory callFactory;
private HttpUrl baseUrl;
private List<Converter.Factory> converterFactories = new ArrayList<>();
private List<CallAdapter.Factory> adapterFactories = new ArrayList<>();
private Executor callbackExecutor;
private boolean validateEagerly;
Platform
提供了3个平台:Java8
,Android
和 IOS
,全都继承自 Platform
,初始化时静态方法 Platform#findPlatform
会自动识别属于哪一个。
private static Platform findPlatform() {
try {
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
try {
Class.forName("java.util.Optional");
return new Java8();
} catch (ClassNotFoundException ignored) {
}
try {
Class.forName("org.robovm.apple.foundation.NSObject");
return new IOS();
} catch (ClassNotFoundException ignored) {
}
return new Platform();
}
通过 Retrofit.Builder#client
或 Retrofit.Builder#callFactory
可以自定义 OkHttpClient
。
public Builder client(OkHttpClient client) {
return callFactory(checkNotNull(client, "client == null"));
}
public Builder callFactory(okhttp3.Call.Factory factory) {
this.callFactory = checkNotNull(factory, "factory == null");
return this;
}
如果不指定 callFactory
,则默认使用 OkHttpClient
。
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}
converterFactories
和 adapterFactories
提供了2个工厂列表,用于用户自定义数据转换和类型转换,后面会详细说明。
最后调用 Retrofit.Builder#build
创建一个 Retrofit
对象。
public Retrofit build() {
// ... configured values
return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
callbackExecutor, validateEagerly);
}
2.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, 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);
}
});
}
首先调用 Utils.validateServiceInterface
去判断 service
是否是一个 Interface
且没有继承其他 Interface
,否则抛非法参数异常。
static <T> void validateServiceInterface(Class<T> service) {
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
if (service.getInterfaces().length > 0) {
throw new IllegalArgumentException("API interfaces must not extend other interfaces.");
}
}
如果 validateEagerly
为 true
,则会调用 eagerlyValidateMethods
方法,会去预加载 service
中的所有方法,默认为 false
。
private void eagerlyValidateMethods(Class<?> service) {
Platform platform = Platform.get();
for (Method method : service.getDeclaredMethods()) {
if (!platform.isDefaultMethod(method)) {
loadServiceMethod(method);
}
}
}
然后就是通过动态代理生成 service
。前两个 if
分支分别判断是否是 Object
的方法及 default
方法,后者除了 Java8
其他都是 false
。
再看 loadServiceMethod
ServiceMethod loadServiceMethod(Method method) {
ServiceMethod result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}
通过 serviceMethodCache
实现缓存机制,同一个 service
的同一个方法只会创建一次,生成 ServiceMethod
同时存入 Cache
。
用 ServiceMethod
和需要的参数生成一个 OkHttpCall
对象。然后用 CallAdapter
将生成的 OkHttpCall
转换为我们需要的返回类型,这个后面会说到。
3. ServiceMethod
Adapts an invocation of an interface method into an HTTP call.
ServiceMethod
的作用就是把一个 API 方法转换为一个 HTTP 调用。
从 Retrofit#loadServiceMethod
方法中可以看出一个 API 方法对应一个 ServiceMethod
。
以 GitHubService
为例
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
@GET("users/{user}/repos")
是 MethodAnnotations
,@Path("user") String user
是 ParameterAnnotations
,Call<List<Repo>>
是 CallAdapter
。
3.1 ServiceMethod.Builder
先看下 ServiceMethod.Builder
的构造函数
public Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
this.methodAnnotations = method.getAnnotations();
this.parameterTypes = method.getGenericParameterTypes();
this.parameterAnnotationsArray = method.getParameterAnnotations();
}
传入 retrofit
和 method
对象,通过 method
获取到方法注解 methodAnnotations
、参数类型 parameterTypes
和参数注解 parameterAnnotationsArray
,parameterAnnotationsArray
是一个二维数组 Annotation[][]
,因为一个参数可以有多个注解。
关键看 ServiceMethod.Builder#build
方法,会生成一个 ServiceMethod
对象,接下来按顺序解析。
3.2 createCallAdapter
callAdapter = createCallAdapter()
会遍历 adapterFactories
,通过 API 方法的 annotations
和 returnType
取到第一个符合条件的 CallAdapter
。
for (int i = start, count = adapterFactories.size(); i < count; i++) {
CallAdapter<?> adapter = adapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
然后取到 responseType = callAdapter.responseType()
并进行判断,如果是 retrofit2.Response<T>
或 okhttp3.Response
则抛异常,目前支持的有默认的 retrofit2.Call<T>
,RxJava 的 rx.Observable
,Java8 的 java.util.concurrent.CompletableFuture
和 Guava 的 com.google.common.util.concurrent.ListenableFuture
。
3.3 createResponseConverter
responseConverter = createResponseConverter()
会遍历 converterFactories
,通过 API 方法的 annotations
和 responseType
取到第一个符合条件的 Converter<ResponseBody, T>
。
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
return (Converter<ResponseBody, T>) converter;
}
}
3.4 parseMethodAnnotation
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
遍历 API 方法的所有 annotations
,如实例中的 @GET("users/{user}/repos")
,根据所属类型和值解析成 HTTP 请求需要的数据。
在 package retrofit2.http
下包含了所有的 Method Annotation
和 Parameter Anootation
。
DELETE
、GET
、HEAD
、PATHC
、POST
、PUT
、OPTIONS
、HTTP
类型的 annotaion
会调用 parseHttpMethodAndPath
,生成 httpMethod
、hasBody
、relativeUrl
、relativeUrlParamNames
。
retrofit2.http.Headers
类型的 annotaion
会调用 parseHeaders
,生成 headers
。
Multipart
和 FormUrlEncoded
类型的 annotaion
分别生成 isMultipart
和 isFormEncoded
,两者互斥,不能同时为 true
,否则会抛异常。
然后对生成的部分参数检查,httpMethod
不能为空,如果 hasBody
为 false
,则 isMultipart
和 isFormEncoded
必须也为 false
,否则会抛异常。
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).");
}
}
3.5 parseParameterAnnotation
int parameterCount = parameterAnnotationsArray.length;
parameterHandlers = new ParameterHandler<?>[parameterCount];
for (int p = 0; p < parameterCount; p++) {
...
Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
}
遍历每个参数的 parameterAnnotations
,如实例中的 @Path("user") String user
,根据所属类型和值解析成对应的 ParameterHandler
,每个 Parameter Anootation
类型都有对应的 ParameterHandler
,且每个参数只能有一个 ParameterHandler
。
可以看下 parseParameter
的逻辑
private ParameterHandler<?> parseParameter(
int p, Type parameterType, Annotation[] annotations) {
ParameterHandler<?> result = null;
for (Annotation annotation : annotations) {
ParameterHandler<?> annotationAction = parseParameterAnnotation(
p, parameterType, annotations, annotation);
if (annotationAction == null) {
continue;
}
if (result != null) {
throw parameterError(p, "Multiple Retrofit annotations found, only one allowed.");
}
result = annotationAction;
}
if (result == null) {
throw parameterError(p, "No Retrofit annotation found.");
}
return result;
}
关键在 parseParameterAnnotation
,根据 annotation
的类型并做参数校验,会生成不同的 ParameterHandler
,如 RelativeUrl
if (type == HttpUrl.class
|| type == String.class
|| type == URI.class
|| (type instanceof Class && "android.net.Uri".equals(((Class<?>) type).getName()))) {
return new ParameterHandler.RelativeUrl();
} else {
throw parameterError(p, "@Url must be okhttp3.HttpUrl, String, java.net.URI, or android.net.Uri type.");
}
由于 Retrofit 不依赖 Android SDK,判断 type
时无法获取到 android.net.Uri.class
,因此采用了 "android.net.Uri".equals(((Class<?>) type).getName())
的技巧。
每个 Parameter Anootation
都会对应一个 ParameterHandler
,如 static final class Path<T> extends ParameterHandler<T>
,它们都实现了 ParameterHandler<T>
。
ParameterAnnotation | ? extends ParameterHandler |
---|---|
Url | RelativeUrl |
Path | Path |
Query | Query |
QueryMap | QueryMap |
Header | Header |
HeaderMap | HeaderMap |
Field | Field |
FieldMap | FieldMap |
Part | Part |
PartMap | PartMap |
Body | Body |
每种 ParameterHandler
都通过 Converter<F, T>
将我们的传参类型转化成 RequestBuilder
需要的类型,并设置其参数。举个例子
static final class Query<T> extends ParameterHandler<T> {
private final String name;
private final Converter<T, String> valueConverter;
private final boolean encoded;
Query(String name, Converter<T, String> valueConverter, boolean encoded) {
this.name = checkNotNull(name, "name == null");
this.valueConverter = valueConverter;
this.encoded = encoded;
}
@Override void apply(RequestBuilder builder, T value) throws IOException {
if (value == null) return; // Skip null values.
builder.addQueryParam(name, valueConverter.convert(value), encoded);
}
}
valueConverter.convert
将我们的传参类型 T
转换成 String
,并设置到 RequestBuilder
中。其他的配置也是按同样的方式。
4. OkHttpCall
OkHttpCall
实现了 retrofit2.Call<T>
,看下它的构造函数,传入 ServiceMethod
和请求参数。
OkHttpCall(ServiceMethod<T> serviceMethod, Object[] args) {
this.serviceMethod = serviceMethod;
this.args = args;
}
4.1 createRawCall
先看下 OkHttpCall#createRawCall
private okhttp3.Call createRawCall() throws IOException {
Request request = serviceMethod.toRequest(args);
okhttp3.Call call = serviceMethod.callFactory.newCall(request);
if (call == null) {
throw new NullPointerException("Call.Factory returned null.");
}
return call;
}
将已经生成的 serviceMethod
通过 toRequest(args)
转成一个 okhttp3.Request
对象。再用创建 Retrofit
时指定的 okhttp3.Call.Factory
创建一个 okhttp3.Call
,这里如果不指定 okhttp3.Call.Factory
,则默认是 okhttp3.OkHttpClient
。
在 ServiceMethod#toRequest
方法中,用 method
相关的配置生成一个 retrofit2.RequestBuilder
后,再用之前准备好的 parameterHandlers
处理每一个参数,最后生成一个 okhttp3.Request
。
/** Builds an HTTP request from method arguments. */
Request toRequest(Object... args) throws IOException {
RequestBuilder requestBuilder = new RequestBuilder(httpMethod, baseUrl, relativeUrl, headers, contentType, hasBody, isFormEncoded, isMultipart);
@SuppressWarnings("unchecked") // It is an error to invoke a method with the wrong arg types.
ParameterHandler<Object>[] handlers = (ParameterHandler<Object>[]) parameterHandlers;
// ... 校验
for (int p = 0; p < argumentCount; p++) {
handlers[p].apply(requestBuilder, args[p]);
}
return requestBuilder.build();
}
4.2 execute
okhttp3.Call#execute
用于同步请求 HTTP,线程会被阻塞,请求成功后返回我们指定的数据类型。
@Override public Response<T> execute() throws IOException {
okhttp3.Call call;
synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true;
if (creationFailure != null) {
if (creationFailure instanceof IOException) {
throw (IOException) creationFailure;
} else {
throw (RuntimeException) creationFailure;
}
}
call = rawCall;
if (call == null) {
try {
call = rawCall = createRawCall();
} catch (IOException | RuntimeException e) {
creationFailure = e;
throw e;
}
}
}
if (canceled) {
call.cancel();
}
return parseResponse(call.execute());
}
首先检查 okhttp3.Call
是否已被执行。
一个 okhttp3.Call
只能被执行一次,可以调用 OkHttpCall#clone
重新创建一个新的相同配置的 HTTP 请求。
@Override public OkHttpCall<T> clone() {
return new OkHttpCall<>(serviceMethod, args);
}
然后检查并创建 okhttp3.Call
,调用 okhttp3.Call#execute
执行同步请求。
最后调用 parsePesponse
将返回的 okhttp3.Response
解析成我们需要的数据。
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
// Remove the body's source (the only stateful object) so we can pass the response along.
rawResponse = rawResponse.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
int code = rawResponse.code();
// ... 状态码检查
ExceptionCatchingRequestBody catchingBody = new ExceptionCatchingRequestBody(rawBody);
try {
T body = serviceMethod.toResponse(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught();
throw e;
}
}
对 okhttp3.Response
进行一些状态码检查后调用 ServiceMethod#toResponse
生成我们需要的数据类型。这里就用到了我们之前准备好的 responseConverter
。最后封装成一个 retrofit2.Response<T>
,包含了原始的 rawResponse
、我们需要的 body
和 errorBody
,我们取需要的数据。
4.3 enqueue
okhttp3.Call#enqueue
和 okhttp3.Call#execute
流程类似,异步请求 HTTP,然后将回调都交给 retrofit2.Callback<T>
处理。
看下 retrofit2.Callback#onFailure
的注释
Invoked when a network exception occurred talking to the server or when an unexpected exception occurred creating the request or processing the response.
这里 retrofit2.Callback#onFailure
除了处理网络异常外,还会处理创建网络请求和解析数据的异常,在回调中处理,而不是直接 crash,这点做的非常好。
-
createRawCall
抛出的异常try { call = rawCall = createRawCall(); } catch (Throwable t) { failure = creationFailure = t; }
如果出异常,直接回调,不执行接下来
enqueue
方法。 -
执行
okhttp3.Call#enqueue
时okhttp3.Callback
抛出的网络请求异常 -
网络请求成功后在
okhttp3.Callback#onResponse
中parseResponse
时抛出的异常try { response = parseResponse(rawResponse); } catch (Throwable e) { callFailure(e); return; }
5. CallAdapter
前面已经简单介绍过 ServiceMethod#createCallAdapter
,它会从 adapterFactories
中找到第一个符合条件的 CallAdapter.Factory
。
5.1 retrofit-adapters
先来看 Retrofit 提供的 retrofit-adapters
模块,目前供我们选择使用的有 guava
、java8
和 rxjava
,分别对应的 CallAdapter.Factory
是 GuavaCallAdapterFactory
、Java8CallAdapterFactory
和 RxJavaCallAdapterFactory
。
在 Retrofit#build
时,除了我们自己添加的 CallAdapter.Factory
, 还会添加两个默认的 CallAdapter.Factory
:ExecutorCallAdapterFactory
和 DefaultCallAdapterFactory
。
// 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));
在 Retrofit
源码中有大量类似 List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
的例子,不直接使用成员变量,而是将成员变量重新拷贝给一个新的临时变量,这样虽然多申请了4个字节内存,但如果以后将成员变量改成入参,就可以不用改代码直接使用了,是一种好的编码习惯。
5.2 default
再看默认的 CallAdapter.Factory
CallAdapter.Factory defaultCallAdapterFactory(Executor callbackExecutor) {
if (callbackExecutor != null) {
return new ExecutorCallAdapterFactory(callbackExecutor);
}
return DefaultCallAdapterFactory.INSTANCE;
}
DefaultCallAdapterFactory
和 ExecutorCallAdapterFactory
返回的类型都是 retrofit2.Call<R>
。
在 DefaultCallAdapterFactory
中,CallAdapter#adapt
什么都不做,直接返回 retrofit2.Call<R>
。而 ExecutorCallAdapterFactory
的 CallAdapter#adapt
则返回 ExecutorCallbackCall<T>
,它实现了 retrofit2.Call<R>
,会传入一个 Executor
,同步调用不变,异步调用时会在指定的 Executor
上执行。
5.3 adapt
看下 CallAdapter
的 adapt
方法
/**
* Returns an instance of {@code T} which delegates to {@code call}.
*/
<R> T adapt(Call<R> call);
它的作用就是把 retrofit2.Call<R>
转换成我们需要的 T
。
CallAdapters | ? extends CallAdapter.Factory | T |
---|---|---|
guava | retrofit2.adapter.guava.GuavaCallAdapterFactory | com.google.common.util.concurrent.ListenableFuture |
java8 | retrofit2.adapter.java8.Java8CallAdapterFactory | java.util.concurrent.CompletableFuture |
rxjava | retrofit2.adapter.rxjava.RxJavaCallAdapterFactory | rx.Observable |
default | retrofit2.ExecutorCallAdapterFactory | retrofit2.Call<R> |
default | retrofit2.DefaultCallAdapterFactory | retrofit2.Call<R> |
List<CallAdapter.Factory>
中添加的顺序是我们指定的一个或多个 CallAdapter.Factory
,默认的 ExecutorCallAdapterFactory
和 DefaultCallAdapterFactory
,查找时按顺序查找。
6. Converter
Converter
的作用就是将 HTTP 请求返回的数据格式转换成我们需要的对象,或将我们提供的对象转换成 HTTP 请求需要的数据格式。
public interface Converter<F, T> {
T convert(F value) throws IOException;
}
接口非常清晰,将 F
转换成 T
。
6.1 Factory
来看 Converter.Factory
abstract class Factory {
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}
public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return null;
}
public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
return null;
}
}
-
responseBodyConverter
用于将 HTTP 请求返回的 response 转换成我们指定的类型。 -
requestBodyConverter
用于将Body
、Part
、PartMap
3种类型的参数转换成 HTTP 的请求体。 -
stringConverter
用于将Field
、FieldMap
、Header
、Path
、Query
、QueryMap
这几种类型的参数转换成String
。
6.2 retrofit-converters
和 CallAdapter
类似,Retrofit 也提供了默认的 Converter.Factory
——BuiltInConverters
,只能处理基本的 ResponseBody
、RequestBody
和 String
类型。
在 Retrofit 提供的 retrofit-converters
模块,供我们选择的有 Gson
、Jackson
、Moshi
、Protocol Buffers
、XML
、Scalar
和 Wire
,我们常用的有 GsonConverterFactory
。
需要注意的是,Jake Wharton 在他的一篇演讲 Simple HTTP with Retrofit 2 中说道
I want to stress that the order matters. This is the order in which we’re going to ask each one whether or not it can handle a type. What I have written above is actually wrong. If we ever specify a proto, it’s going to be encoded as JSON, which will try and deserialize the response buddy as JSON. That’s obviously not what we want. We will have to flip these because we want to check protocol buffers first, and then JSON through GSON.
说的就是 Retrofit.Builder#addConverterFactory
的顺序非常重要,先添加的 Converter.Factory
会先用来解析,而 Gson 非常强大,如果第一个添加 GsonConverterFactory
,则其他想要转换的类型如 Protocol Buffers
就不会执行,因此建议将 GsonConverterFactory
作为最后一个添加。
7. 总结
Retrofit 的源码还是很难的,反反复复看了很多遍,除了其原理和流程外,从中也学到了一些技巧和设计模式。在写这篇文章的同时又把思路和流程重新理了一遍,对自己帮助还是非常大的。关于源码最大的感受就是各个类之间传对象调用感觉非常乱,也增加了理解的难度,如 Retrofit
和 ServiceMethod
之间就互相依赖。
本文是 慌不要慌 原创,发表于 https://danke77.github.io/,请阅读原文支持原创 https://danke77.github.io/2016/08/06/retrofit-source-analysis/,版权归作者所有,转载请注明出处。
网友评论