Retrofit使用技巧

Retrofit 配合 RxJava 的使用是越来越广泛,小到个人项目,大到公司项目,都能看到它,它的使用也是挺简单的,它本身就是对 OkHttp 的一个封装,让开发者在使用网络请求时更加方便、快捷。我用了一段时间之后,觉得虽然封装的挺好的,但是在用到一些东西的时候还是需要来自定义一下的,下面就来分享一下,我在使用过程中的一些自定义小功能。

Retrofit 的使用

连接超时

平时对连接超时没什么印象,因为你即使不设置,默认情况下也是有超时时长的,如果想要设置连接超时时长的话,需要在 OkHttpClient.Builder 的初始化的时候进行设置。例如:

        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(Mark.HTTP_CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
                .readTimeout(Mark.HTTP_READ_TIMEOUT, TimeUnit.MILLISECONDS)
                .writeTimeout(Mark.HTTP_WRITE_TIMEOUT, TimeUnit.MILLISECONDS);
                

在这里我设置了三个连接超时的时长,分别是:

  • connectTimeout 是 TCP Socket 的三次握手的时长,在这个时间范围内进行 3 次握手;
  • readTimeout 是 TCP Socket 的三次握手和单独读取 IO 操作的时长,包括响应的来源,如果在这个过程中连接失败,则获取不到数据;
  • writeTimeout 则是用于 IO 写入的超时限制。

从 Reftofit 2.5.0 的版本开始,这三个的默认连接超时时长均为 10 秒。

参考地址

公共参数拦截器

在一些情况下,我们需要在请求上添加一些公共信息,比如,用户登录之后携带的验证信息,或是针对不同的请求地址加上一些固有的请求参数。要做到这一点就需要把一些公共的信息提取出来,放在一个拦截器中,之后每一次的网络请求根据限制条件进行判断是否使用参数连接器对请求地址或头部信息进行二次改造。

Interceptor mTokenInterceptor = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Request originalRequest = chain.request();
        if (Your.sToken == null || alreadyHasAuthorizationHeader(originalRequest)) {
            return chain.proceed(originalRequest);
        }
        Request authorised = originalRequest.newBuilder()
            .header("Authorization", Your.sToken)
            .build();
        return chain.proceed(authorised);
    }
};

或是

Interceptor mTokenInterceptor = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        HttpUrl originalHttpUrl = original.url();
        HttpUrl url = originalHttpUrl.newBuilder()
                            .addQueryParameter("accountsuite", "someThing")
                            .build();
        return chain.proceed(authorised);
    }
};

设置拦截器之后,需要在 OkHttpClient.Builder 上进行应用,通过这个方法.addInterceptor(mTokenInterceptor)加上即可,而且 OkHttp 的拦截器可以同时设置多个,比如在添加参数连接器之后,再加上下面的日志拦截器。

参考地址

网络请求日志

网络请求日志输出是非常重要的,在调试的时候能及时的查看客户端与后台的数据交互是否存在异常。可是,我在第一次使用的时候是不知道该怎么去配置,看了文档才知道可以自己定义一个 HttpLoggingInterceptor(日志拦截器) 然后设置日志拦截等级并像上面那样给添加到 OkHttpClient.Builder 即可,HttpLoggingInterceptor 一共有 4 个等级:

  • NONE 不打印
  • BASIC 只打印基础信息
    • 请求方法类型(GET、POST……)
    • HTTP 版本(HTTP1.0/HTTP1.1)
    • 请求内容体大小
    • 是否成功返回
    • 耗时时长
    • 内容体大小
  • HEADERS 在 BASIC 级别的基础上增加请求 HEADER 信息
  • BODY 在 HEADERS 级别的基础上增加请求及返回的 BODY 信息

OkHttp 设置网络日志请求拦截器

        HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor();
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
        
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
                .readTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
                .writeTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS)
                .addInterceptor(loggingInterceptor)
                .build();

其实,这四个等级已经能满足需求,不过,由于是使用 系统的 log 打印模块,每次只能把单个信息打印出来,而且还不能定位到具体的行数以及线程,真的不方便。

所以,这个时候就需要自己动手去改造原有的 OkHttp 的日志打印,不过我们想要的日志内容基本上是全覆盖了 HttpLoggingInterceptor 的后三个等级。所以,可以参考着他的源码来进行自己修改。
我想要达到的效果就是取消等级限制并使用第三方的日志输出统一打印:

话不多说,上代码:

    private static Interceptor logInterceptor() {
        Interceptor logInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request original = chain.request();
                Request.Builder requestBuilder = original.newBuilder();
                Request request = requestBuilder.url(original.url()).build();

                RequestBody requestBody = request.body();
                boolean hasRequestBody = requestBody != null;

                StringBuilder requestSb = new StringBuilder();
                Connection connection = chain.connection();
                Protocol protocol = connection != null ? connection.protocol() : Protocol.HTTP_1_1;
                requestSb.append("请求-->")
                        .append(' ')
                        .append(request.method())
                        .append(' ')
                        .append(request.url())
                        .append(' ')
                        .append(protocol)
                        .append("\n");

                if (hasRequestBody) {
                    if (requestBody.contentType() != null) {
                        requestSb.append("Content-Type: ")
                                .append(requestBody.contentType())
                                .append("\n");
                    }
                    if (requestBody.contentLength() != -1) {
                        requestSb.append("Content-Length: ")
                                .append(requestBody.contentLength())
                                .append("-byte body").append("\n");
                    }
                }

                Headers requestHeaders = request.headers();
                for (int i = 0, count = requestHeaders.size(); i < count; i++) {
                    String name = requestHeaders.name(i);
                    requestSb.append(name)
                            .append(": ")
                            .append(requestHeaders.value(i));
                    requestSb.append("\n");
                }

                if (hasRequestBody) {
                    Buffer buffer = new Buffer();
                    requestBody.writeTo(buffer);

                    Charset charset = UTF8;
                    MediaType contentType = requestBody.contentType();
                    if (contentType != null) {
                        charset = contentType.charset(UTF8);
                    }
                    if (isPlaintext(buffer)) {
                        requestSb.append(buffer.readString(charset))
                                .append("\n");
                        requestSb.append("--> END ").
                                append(request.method());
                    } else {
                        requestSb.append("--> END ")
                                .append(request.method())
                                .append(" (binary ")
                                .append(requestBody.contentLength())
                                .append("-byte body omitted)");
                    }
                }
                Logger.i(requestSb.toString());

                long startNs = System.nanoTime();
                Response response;
                try {
                    response = chain.proceed(request);
                } catch (Exception e) {
                    Logger.e("<-- HTTP FAILED: " + e);
                    throw e;
                }

                long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
                ResponseBody responseBody = response.body();
                BufferedSource source = null;
                Buffer buffer = null;
                int responseCode = response.code();
                if (responseBody != null) {
                    source = responseBody.source();
                    source.request(Long.MAX_VALUE);
                    buffer = source.buffer();
                }
                long contentLength = responseBody.contentLength();

                StringBuilder responseSb = new StringBuilder();
                responseSb.append("返回<-- ")
                        .append(response.code())
                        .append(' ')
                        .append(response.message())
                        .append(' ')
                        .append(response.request().url())
                        .append(" (")
                        .append(tookMs)
                        .append("ms").append(')')
                        .append("\n");

                Headers headers = response.headers();
                for (int i = 0, count = headers.size(); i < count; i++) {
                    responseSb.append(headers.name(i))
                            .append(": ")
                            .append(headers.value(i))
                            .append("\n");
                }
                Charset charset = UTF8;
                MediaType contentType = responseBody.contentType();
                if (contentType != null) {
                    try {
                        charset = contentType.charset(UTF8);
                    } catch (UnsupportedCharsetException e) {
                        Logger.i("Couldn't decode the response body; charset is likely malformed.");
                        Logger.i("<-- END HTTP");
                        return response;
                    }
                }
                if (!isPlaintext(buffer)) {
                    Logger.i("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)");
                    return response;
                }

                if (contentLength != 0) {
                    Logger.i("返回数据: " + buffer.clone().readString(charset));
                }

                responseSb.append("<-- END HTTP (")
                        .append(buffer.size())
                        .append("-byte body)");
                Logger.i(responseSb.toString());

                return response;
            }
        };
        return logInterceptor;
    }

    private static boolean isPlaintext(Buffer buffer) {
        try {
            Buffer prefix = new Buffer();
            long byteCount = buffer.size() < 64 ? buffer.size() : 64;
            buffer.copyTo(prefix, 0, byteCount);
            for (int i = 0; i < 16; i++) {
                if (prefix.exhausted()) {
                    break;
                }
                int codePoint = prefix.readUtf8CodePoint();
                if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) {
                    return false;
                }
            }
            return true;
        } catch (EOFException e) {
            return false;
        }
    }

代码虽然有些长,但还是很容易理解。就是分别将 Request 以及 Response 进行拦截,得到想展示出来的内容信息,然后拼接起来,最后使用 Logger 进行打印。打印出来的效果还是比较不错的。在这里拦截到 Response 之后,我把所有的异常统一进行捕捉了,还可以添加一些其他的异常信息,比如:连接超时的异常、URL 错误的异常等等。

主要就是参考 OkHttp 里的 HttpLoggingInterceptor 这个类,可以自己尝试一下。

  • 请求打印
01-30 11:09:55.085 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:288)
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 请求--> POST http://192.168.199.186:8080/report/api/v1/drp/checkInputBill/create?accountsuite=demo http/1.1
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Type: application/json; charset=UTF-8
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Length: 1576-byte body
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Conteng-type: application/json;charset=UTF-8
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Authorization: Basic YWRtaW46MTExMTEx
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ {"checkInputBill":[{"createOperatorName":"admin","createOperatorCode":"admin","handWorkBill":"4001","checkNoticeBillId":"31e4fb9dd3b441dea77904ec9a435b7e","checkInputBillItems":[{"quantity":"1","barCode1":"0001V002201","sizeName":"XS","skuIds":"4028471e6116e54201611c4311070062","productCode":"0001V","colorName":"米红","barCode2":"0001V002201"},{"quantity":"1","barCode1":"0010003","sizeName":"165/92A","skuIds":"aaf9426940bb5b7b0140bee72e873f5e","productCode":"001首饰","colorName":"均色","barCode2":"0010003"},{"quantity":"1","barCode1":"000DX0039","sizeName":"160/88A","skuIds":"4028810c5b88f46c015b896754810002","productCode":"0040Z","colorName":"纯白","barCode2":"0040Z0102"},{"quantity":"4","barCode1":"000DY0340","sizeName":"165/92A","skuIds":"4028810c5b88f46c015b896754820006","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0203"},{"quantity":"1","barCode1":"000DY0539","sizeName":"170/96A","skuIds":"4028810c5b88f46c015b896754820007","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0204"},{"quantity":"2","barCode1":"1030620027103","sizeName":"165/92A","skuIds":"aaf9426948850ec40148893130014f7c","productCode":"杏色V领下摆纱衫","colorName":"浅杏","barCode2":"null"}],"createOperatorId":"40288996207b57eb01207b5c81ee0004","remark":"4001:4001","updateTime":"2018-01-12 16:42:38","createTime":"2018-01-12 15:29:49","backGroundColorId":"1","checkNoticeBill":"PDT18011100009","checkInputBillId":"dc1c7d1d3b454da3903e0beec7da2e8f"}],"checkType":1,"checkTime":"2018-02-01 02:30:00","operatorId":"40288996207b57eb01207b5c81ee0004"}
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ --> END POST
01-30 11:09:55.086 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════
  • 结果打印
01-30 11:09:55.264 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.265 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:386)
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 返回数据: {"result":true,"checkInputBill":[{"createOperatorName":"admin","createOperatorCode":"admin","handWorkBill":"4001","checkNoticeBillId":"31e4fb9dd3b441dea77904ec9a435b7e","checkInputBillItems":[{"quantity":"1","barCode1":"0001V002201","sizeName":"XS","skuIds":"4028471e6116e54201611c4311070062","productCode":"0001V","colorName":"米红","barCode2":"0001V002201"},{"quantity":"1","barCode1":"0010003","sizeName":"165/92A","skuIds":"aaf9426940bb5b7b0140bee72e873f5e","productCode":"001首饰","colorName":"均色","barCode2":"0010003"},{"quantity":"1","barCode1":"000DX0039","sizeName":"160/88A","skuIds":"4028810c5b88f46c015b896754810002","productCode":"0040Z","colorName":"纯白","barCode2":"0040Z0102"},{"quantity":"4","barCode1":"000DY0340","sizeName":"165/92A","skuIds":"4028810c5b88f46c015b896754820006","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0203"},{"quantity":"1","barCode1":"000DY0539","sizeName":"170/96A","skuIds":"4028810c5b88f46c015b896754820007","productCode":"0040Z","colorName":"米白","barCode2":"0040Z0204"},{"quantity":"2","barCode1":"1030620027103","sizeName":"165/92A","skuIds":"aaf9426948850ec40148893130014f7c","productCode":"杏色V领下摆纱衫","colorName":"浅杏","barCode2":"null"}],"createOperatorId":"40288996207b57eb01207b5c81ee0004","remark":"4001:4001","updateTime":"2018-01-12 16:42:38","createTime":"2018-01-12 15:29:49","backGroundColorId":"1","checkNoticeBill":"PDT18011100009","checkInputBillId":"dc1c7d1d3b454da3903e0beec7da2e8f","method":"update"}]}
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.266 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╔════════════════════════════════════════════════════════════════════════════════════════
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Thread: RxCachedThreadScheduler-6
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ RealInterceptorChain.proceed  (RealInterceptorChain.java:67)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║    RealInterceptorChain.proceed  (RealInterceptorChain.java:92)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║       NetWorkManager$4.intercept  (NetWorkManager.java:390)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╟────────────────────────────────────────────────────────────────────────────────────────
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ 返回<-- 200 OK http://192.168.199.186:8080/report/api/v1/drp/checkInputBill/create?accountsuite=demo (176ms)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Content-Type: application/json;charset=UTF-8
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Transfer-Encoding: chunked
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Server: Jetty(7.6.14.v20131031)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ Cache-Control: public, max-age=60
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ║ <-- END HTTP (1512-byte body)
01-30 11:09:55.267 24755-29465/com.soooft.android.baicaipos I/BaiCaiPOS: ╚════════════════════════════════════════════════════════════════════════════════════════

可以看到 Logger 打印的内容还是挺丰富的,包含了行号,线程再加上那个范围框,更能直观的看出日志内容。

与 RxJava 相结合

现在网络请求都是 Retrofit 配合 RxJava ,针对 RxJava 有两点可以进行优化改进的地方。

IO 线程与 Android 主线程的切换

要知道在 Android 中是不能将耗时操作放在主线程的,以前自己通过 Thread + HttpURLConnection 实现网络请求的时候使用的是 Handler 进行处理线程切换,现在换成了 RxJava 就更方便了,不过,每次都要写那两行去切换线程,虽然就那么两行,但也还是不想写。

RxJava 有一个操作符 compose() 异常的强大,可以在保持原有 RxJava 编程风格不变的情况下完成自定义的操作。 ObservableTransformer 是 RxJava 中一个强大的接口,用于将类型 A 转换成类型 B ,这么说有些牵强了,其实就是把上游的内容经过 ObservableTransformer 转换成下游想要的类型,当然如果类型不变,也是可以进行的或是进行一些操作。

这里就需要结合 compose 和 ObservableTransformer 来一起使用,看代码吧。

    public static <T> ObservableTransformer<T, T> applySchedulers() {
        return new ObservableTransformer<T, T>() {
            @Override
            public ObservableSource<T> apply(Observable<T> upstream) {
                return upstream.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

当然,如果习惯用 Java 8 的 Lambda 表达式,看起来就更简洁了。简洁是挺简洁的,不过有些不熟悉的方法,就完全不知道什么意思了,像我这样的新手还是不用的好。

    public static <T> ObservableTransformer<T, T> applySchedulers() {
        return upstream -> upstream.subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

网络数据封装

在实际的开发过程中,基本上服务器返回的内容都是这样的:

{
    resultNo: 200,
    message: "成功"
    result: [],
    error: false
}

这样的返回信息,包含了一些状态信息及具体的数据,其中 resultNo 和 message 以及 error 都是类型不会变的状态信息,只有其中的 result 是会动态变化的具体数据,可能里面是一个 List 也可能里面是一个 布尔值变量,甚至还有可能嵌套好几层。所以可以把 result 定义为泛型 T ,这样在具体到单一个接口数据的时候再指定类型即可。

public class BaseResponse<T> {
    private boolean error;
    private T results;

    /*
     * get and set 
     */
}

如果是这样的话,我们在定义网络接口的时候,就可以指明实际返回的数据类型了。例如:

    @GET("data/{type}/{size}/{page}")
    Observable<BaseResponse<List<Map<String, Object>>>> typeData(@Path("type") String type, @Path("size") int size, @Path("page") int page);

不过,这样使用了 BaseResponse<T> 之后,在 Observable 实现订阅( subscribe )的时候,提供的参数类型也就成了 BaseResponse<List<Map<String, Object>>> response ,这时候,需要我们从 response 中再取一次,多写一行?不可能的。

上面在进行线程切换的时候,用到了 ObservableTransformer 这里也可以用,上面是相同的事件类型使用它来进行转换,只不过现在变成了,不同类型的数据转换。

    public static <T> ObservableTransformer<BaseResponse<T>, T> handleResult() {
        return new ObservableTransformer<BaseResponse<T>, T>() {
            @Override
            public ObservableSource<T> apply(Observable<BaseResponse<T>> upstream) {
                return upstream.flatMap(new Function<BaseResponse<T>, ObservableSource<T>>() {
                    @Override
                    public ObservableSource<T> apply(BaseResponse<T> response) throws Exception {
                        if (response == null) {
                            return Observable.empty();
                        } else if (response.getResults() == null) {
                            return Observable.empty();
                        } else {
                            return Observable.just(response.getResults());
                        }
                    }
                });
            }
        };
    }

同样的,实现 ObservableTransformer 中的 apply() 方法,这时候就不是简单的线程切换了,而是需要进行类型之间的转换。这里我只是简单的进行类型转换,其实还可以加入一些简单的逻辑判断。在这里,我们已经可以获取到 BaseResponse 了,那么我们可以根据其中的一些公共内容进行判断,然后进行发射(RxJava)不同的事件,比如,跟后台开发约定好一些错误代码,如 404 ,那么在这里,就可以加入 针对 404 的事件处理,可以是 发射一个自定义异常或是其他的操作;也可以是针对返回的 result 进行内容判断,判断一次是否为空。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,062评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,651评论 18 139
  • 李茂书法
    洗墨人阅读 120评论 0 1
  • 当我发现Ego潜入头脑世界,感觉被它困在监狱中,被它戏弄,带我遛弯到未来世界中。突然,惊醒,力量夺回!这种决斗,奇妙!
    芯仪优品阅读 171评论 0 0
  • 2017年12月13日 星期三 晴 【故事】 当石头城全城再次响起防空警报声时,丰子的车刚从那座雕塑前驶过,就是...
    木徒阅读 610评论 3 0