个人使用RxJava
接近十年,是国内比较早的一批把RxJava
做为主要技术栈的Android
开发者,一些在当时看来比较复杂的业务逻辑,最后都通过RxJava
的操作符解决,包括后面开发语言切换到kotlin
后使用协程做异步开发,我个人也比较偏向于使用Flow
这个跟RxJava
很像的数据流式 操作框架。现如今开发语言又切回到Java
,对Rxjava
的使用场景也更丰富了,下面简单介绍一下我用RxJava
在Android
开发中的一些实用案例。
结合 Retrofit RxJava
异步处理 + Retrofit
网络请求 + Gson
数据解析是Android
开发曾经比较流行的开发组合,其中RxJava
是可选的也是最难的部分,数据流式的链式调用、简单的异步操作和简洁优雅的代码处理是其优势,同样其操作符的丰富度与复杂度也是门槛之一,熟练地在业务中使用也需要一定的积累。
这里简单介绍一下RxJava
在Retrofit
网络请求中的使用,代码示例如下:
引入依赖:
1 implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
构建 Retrofit 对象:
1 2 3 4 5 6 7 8 RxJava2CallAdapterFactory rxJava3CallAdapterFactory = RxJava3CallAdapterFactory.createWithScheduler(Schedulers.io())Retrofit retrofit = new Retrofit .Builder() .baseUrl(Constants.BASE_URL) .client(okHttpClient) .addCallAdapterFactory(rxJava3CallAdapterFactory) .addConverterFactory(gsonConverterFactory) .build();
定义接口:
我们在定义Retrofit
支持的网络接口请求类时,请求方法的返回类型直接使用RxJava
支持的被观察者类型包裹,这种处理正是上面添加的CallAdapter
类支持Call<T>
到RxJava
观察者类型的转换,下面是示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface ApiService { @GET("api/{someApi}") Call<ApiResponse<T>> getServerData () ; @GET("api/someApi") Single<ApiResponse<T>> getServerData () ; @GET("api/someApi") Observable<ApiResponse<T>> getServerData () ; @GET("api/someApi") Flowable<ApiResponse<T>> getServerData () ; @GET("api/someApi") Maybe<ApiResponse<T>> getServerData () ; @GET("api/someApi") Completable getServerData () ; ... }
网络请求方法的返回类型可以根据具体的情况做合适的选择,我目前比较常用的是Single
和Completable
这两类。
通用逻辑复用 现在的接口请求返回的数据格式一般都是json
并且是restful
风格,这样的数据风格非常方便进行统一的封装与业务处理。
统一数据泛型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class ApiResponse <T> { @SerializedName(value = "code", alternate = {"status"}) private int code; @SerializedName(value = "message", alternate = {"msg"}) private String message; private T data; public ApiResponse () { } public ApiResponse (int code, String message, T data) { this .code = code; this .message = message; this .data = data; } ... public boolean isSuccess () { return code == 200 ; } }
对于网络请求返回的数据,界面上需要显示的数据都在data
字段里,从ApiResponse<T>
->T
的转换对每一个接口来说是一个重复的过程,利用RxJava
的Transformer
类可以把转换的逻辑封装起来,下面是示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private <T> ObservableTransformer<ApiResponse<T>,T> observableTransform () { return new ObservableTransformer <ApiResponse<T>, T>() { @Override public @NonNull ObservableSource<T> apply (@NonNull Observable<ApiResponse<T>> upstream) { return upstream .flatMap(new Function <ApiResponse<T>, ObservableSource<? extends T >>() { @Override public ObservableSource<? extends T > apply(ApiResponse<T> dataWithApiResponse) throws Throwable { if (dataWithApiResponse.isSuccess()) return Observable.just(dataWithApiResponse.getData()); ApiException customException = ...; return Observable.error(customException); } }); return upstream .map(new Function <ApiResponse<T>, T>() { @Override public T apply (ApiResponse<T> dataWithApiResponse) throws Throwable { if (dataWithApiResponse.isSuccess()) return dataWithApiResponse.getData(); ApiException customException = ...; throw customException; } }); } } } Observable<T> serverData = apiService.getServerData() .compose(observableTransform());
XXXTransformer
类内可以把一些通用的逻辑像网络请求错误重试、Result
包裹类转换和Token
过期刷新等业务逻辑统一处理,然后使用compose
操作符进行业务逻辑复用,这样既可以把复杂的通用逻辑通过compose
操作符组合应用,又能让代码更加简洁,逻辑更加清晰。
节流与防抖 节流的定义是在连续高频事件触发时,保证在固定时间间隔内只执行一次操作,常用的案例是View
的点击事件在固定时间间隔内防止触发多次。
常规实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public abstract class ThrottleClickListener implements View .OnClickListener { private static final long DEFAULT_THROTTLE_INTERVAL = 500 ; private final long throttleInterval; private long lastClickTime = 0 ; public ThrottleClickListener () { this (DEFAULT_THROTTLE_INTERVAL); } public ThrottleClickListener (long throttleInterval) { this .throttleInterval = throttleInterval; } @Override public void onClick (View v) { long currentTime = System.currentTimeMillis(); if (currentTime - lastClickTime > throttleInterval) { lastClickTime = currentTime; onThrottleClick(v); } } public abstract void onThrottleClick (View v) ; } button.setOnClickListener(new ThrottleClickListener () { @Override public void onThrottleClick (View v) { doSomething(); } });
项目里如果不使用RxJava
的话,可以通过自定义点击事件的方式实现节流的效果,但是如果使用RxJava
的话,可以使用RxBinding
配合throttleFirst
操作符处理。
RxJava
操作符实现:
1 2 3 4 5 6 7 8 private static final long DEFAULT_THROTTLE_INTERVAL = 500 ; RxView.clicks(button) .throttleFirst(DEFAULT_THROTTLE_INTERVAL, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .subscribe(v -> { doSomething(); });
节流的时间间隔要选取合适的值,时间太长可能造成用户点击没有响应,太短则起不到优化的效果。
防抖 的定义是用于控制高频率触发的事件,只在事件停止触发一段时间后才执行一次操作,避免短时间内多次重复执行的操作,常用的案例根据EditText
的文本变化事件在固定时间间隔后只触发最后一次事件进行实时搜索操作。
常规实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public abstract class DebounceTextWatcher implements TextWatcher { private static final long DEFAULT_DEBOUNCE_INTERVAL = 500 ; private final long debounceInterval; private final Handler handler = new Handler (Looper.getMainLooper()); private Runnable workRunnable; public DebounceTextWatcher () { this (DEFAULT_DEBOUNCE_INTERVAL); } public DebounceTextWatcher (long debounceInterval) { this .debounceInterval = debounceInterval; } @Override public void afterTextChanged (final Editable s) { if (workRunnable != null ) { handler.removeCallbacks(workRunnable); } workRunnable = new Runnable () { @Override public void run () { onDebounceTextChanged(s.toString()); } }; handler.postDelayed(workRunnable, debounceInterval); } @Override public void beforeTextChanged (CharSequence s, int start, int count, int after) {} @Override public void onTextChanged (CharSequence s, int start, int before, int count) {} public void clear () { if (workRunnable != null ) { handler.removeCallbacks(workRunnable); } } public abstract void onDebounceTextChanged (String text) ; } editText.addTextChangedListener(new DebounceTextWatcher () { @Override public void onDebounceTextChanged (String text) { searchSomething(); } });
同样的Rxbinding
配合debounce
操作符处理也可以达到相同的效果
RxJava 操作符实现
1 2 3 4 5 6 7 8 9 private static final long DEFAULT_DEBOUNCE_INTERVAL = 500 ; RxTextView.afterTextChangeEvents(editText) .debounce(DEFAULT_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .subscribe(v -> { searchSomething(); });
防抖的时间间隔同样要根据场景设置合适的值,如果是网络请求进行搜索的场景下则需要在Repository
层对前一次的搜索请求进行取消,因为网络请求时间的不确定性会造成前一次的搜索结果覆盖后续的搜索结果的情况出现,虽然出现这种情况的概率很低,但也要做一些防范。
使用Rxbinding
结合RxJava
操作符的方式需要注意的是如果中间操作符的Function
参数或者onNext
参数方法体里发生了异常,RxJava 会自动将异常传递给subscribe
的onError
参数,没有设置的话则会传递给全局的RxJavaPlugins.onErrorHandler
,下面有一个章节专门说RxJava
异常方面的细节。
注意无论是RxView.clicks
还是RxTextView.afterTextChangeEvents
,一定要把在subscribe
后返回的Disposable
对象管理起来,不然很容易造成内存泄露。
串行/并行请求 图片上传功能涉及到图片本地压缩、使用SDK
请求临时Token
(可选)、图片上传,其中图片本地压缩和请求临时Token
可以并行处理,图片上传则依赖前一步的结果,把前面的并行处理看做一个个体的话,那么这个整体就是一个串行的请求,使用RxJava
的zip
和flatMap
操作符可以实现这个效果。
伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Observable<File> compressObservable = ...; Observable<String> tokenObservable = ...; Observable.zip(compressObservable, tokenObservable, new BiFunction <File, String, Pair<File, String>>() { @Override public Pair<File, String> apply (File file, String token) throws Exception { return new Pair <>(file, token); } }) .flatMap(new Function <Pair<File, String>, ObservableSource<ApiResponse<UploadResult>>() { @Override public ObservableSource<ApiResponse<UploadResult>> apply (Pair<File, String> fileTokenPair) throws Exception { File compressedFile = pair.first; String token = pair.second; return mRepository.uploadFile(compressedFile, token); } }) .compose(observableTransform()) .subscribe(uploadResult -> { }, throwable -> { });
日常的开发过程中这种串行/并行的异步处理是比较常见的,使用RxJava
的操作符把多个异步操作连接起来,使其一系列的的操作看上去像一个整体,代码看起来也比较简单整洁,这正是使用RxJava
开发所带来的好处。
磁盘/内存缓存 数据缓存是比较常见的业务场景之一,在数据的有效期内对数据进行缓存,可以提高数据的访问效率、降低网络请求的次数,使用RxJava
的concat
和firstElement
操作符可以实现这个效果,下面是一个简单的例子:
RxJava 伪代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Observable<Data> mermoryData = Observable.create(new ObservableOnSubscribe <Data>() { @Override public void subscribe (ObservableEmitter<Data> emitter) throws Exception { emitter.onNext(data); emitter.onComplete(); } }); Observable<Data> diskData = Observable.create(new ObservableOnSubscribe <Data>() { @Override public void subscribe (ObservableEmitter<Data> emitter) throws Exception { emitter.onNext(data); emitter.onComplete(); } }).map(data -> { return data; }); Observable<Data> networkData = ...; .map(data -> { return data; }); Maybe<Data> maybeData = Observable.concat(mermoryData, diskData, networkData) .filter(data -> data.isNotEmpty()) .firstElement() .subscribe(); 或者 Observable<Data> data = Observable.concat(mermoryData, diskData, networkData) .filter(data -> data.isNotEmpty()) .firstElement() .toObservable() .subscribe();
内存缓存推荐使用LruCache
,为了统一处理所有数据的内存缓存,这里我们使用LruCache<String, String>
封装底层数据存取,实际使用过程中需要进行具体类型 数据到String 数据的序列化与反序列化。磁盘缓存也是同样的处理,只不过磁盘缓存的底层可以使用database
或file
的存储方式,无论采用何种方式都要进行一定程度的封装。
轮询请求接口 需求场景:
正常搜索,根据返回结果是否开启深度搜索
code
为0
或者200
,正常搜索结束,返回搜索结果
code
为特定业务码,则使用同级别的searchId
字段值进行深度(轮询)搜索
轮询间隔为1s
,每次接口请求都会返回code
决定是否继续轮询。
这个业务场景并不多见,轮询请求接口一般是在等待后端的某个耗时操作处理完成,轮询虽然不是最优的方式,但却是实现成本比较低的方式。
RxJava
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 Observable<SearchResult> searchObservable = apiService.search(keyword) .compose(observableTransform()); Observable<SearchResult> deepSearchWithPolling (String searchId) { return apiservice.deepSearch(searchId) .compose(observableTransform()); .repeatWhen(new Function <Observable<Object>, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Observable<Object> upstream) throws Throwable { return upstream.delay(1 , TimeUnit.SECONDS); return upstream .flatMap(new Function <Object, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Object o) throws Throwable { return needRepeat() ? Observable.timer(1 , TimeUnit.SECONDS) : Observable.empty(); } }); } }) .takeUntil(new Predicate <SearchResult>() { @Override public boolean test (SearchResult searchResult) throws Exception { return searchResult.isSuccess(); } }) .lastElement() .toObservable(); } Disposable disposable = searchObservable .flatMap(new Function <SearchResult, ObservableSource<SearchResult>>() { @Override public ObservableSource<SearchResult> apply (SearchResult searchResult) throws Exception { if (searchResult.isSuccess()) { return Observable.just(searchResult); } else { return deepSearchWithPolling(searchResult.getSearchId()); } } }) .subscribe();
深度(轮询)搜索停止的条件是needRepeat()
返回false
或者takeUntil
返回true
,needRepeat()
方法是根据特定条件 判断是否需要继续轮询,takeUntil
是根据返回的数据 判断是否需要结束轮询。
业务重试 网络错误重试 需求场景:
在弱网或者高并发的场景下请求接口容易发生Timeout
等异常,需要进行网络请求重试
重试有次数限制,且每次重试的间隔时间随次数递增,重试次数跟相应的重试间隔时间可配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public static <T> ObservableTransformer<T, T> observableApiRetry (List<Long> retryConditions) { return new ObservableTransformer <T, T>() { @Override public @NonNull ObservableSource<T> apply (@NonNull Observable<T> upstream) { return upstream .retryWhen(new Function <Observable<Throwable>, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Throwable { return throwableObservable .zipWith(retryConditions, new BiFunction <Throwable, Long, Pair<Throwable, Long>>() { @Override public Pair<Throwable, Long> apply (Throwable throwable, Long aLong) throws Throwable { return new Pair <>(throwable, aLong); } }) .flatMap(new Function <Pair<Throwable, Long>, ObservableSource<Long>>() { @Override public ObservableSource<Long> apply (Pair<Throwable, Long> throwableLongPair) throws Throwable { Throwable throwable = throwableLongPair.first; long time = throwableLongPair.second; if (needRetry(throwable)) { return time == retryConditions.get(retryConditions.size() - 1 ) ? Observable.error(new RetryFailException (throwable)) : Observable.timer(time, TimeUnit.SECONDS); } return Observable.error(throwable); } }); } }); } }; }
Token
过期刷新需求场景:
用户登录成功后返回refreshToken
和accessToken
,refreshToken
过期时间较长一般以年为单位,accessToken
过期时间较短一般以分钟为单位
accessToken
用于一般的接口请求时放到请求头里用于后端鉴权使用,refreshToken
用于accessToken
过期后调用刷新接口获取新的accessToken
使用
accessToken
过期时在用户层面是无感知的,需要在逻辑层自动做刷新然后业务接口重试,refreshToken
过期需要弹出重新登录确认框。
自动刷新accessToken
后并重试网络请求的需求,这里实现方式有两种,分别是在OkHttp
的自定义Interceptor
里处理和RxJava
的retryWhen
操作符配合处理
自定义Interceptor
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class TokenAuthenticatorInterceptor implements Interceptor { private static final int HTTP_EXPIRED_CODE = ...; private static final int TOKEN_EXPIRED_CODE = ...; private final TokenManager tokenManager; public TokenAuthenticatorInterceptor (TokenManager tokenManager) { this .tokenManager = tokenManager; } @Override public Response intercept (Chain chain) throws IOException { Request request = chain.request(); Request authRequest = request.newBuilder() .header("Authorization" , "Bearer " + tokenManager.getAccessToken()) .build(); Response response = chain.proceed(authRequest); if (isTokenExpired(response)) { response.close(); synchronized (TokenManager.class) { return retryWithRefreshedToken(chain, request); } } return response; } private boolean isTokenExpired (Response response) { if (response.code() == 200 ) { return false ; } int code = ... return response.code() == HTTP_EXPIRED_CODE && code == TOKEN_EXPIRED_CODE; } private Response retryWithRefreshedToken (Chain chain, Request request) throws IOException { String newToken = tokenManager.getAccessToken(); if (newToken.equals(tokenManager.getLastTriedToken())) { newToken = tokenManager.refreshTokenBlocking(); } tokenManager.setLastTriedToken(newToken); Request newRequest = request.newBuilder() .header("Authorization" , "Bearer " + newToken) .build(); return chain.proceed(newRequest); } }
注意事项:
刷新token
要同步阻塞,否则多个线程会重复刷新(用synchronized
保证)。
只重试一次,避免陷入死循环,若刷新失败,应返回失败,不要死循环。
刷新token
的网络请求不要用当前带Interceptor
的OkHttpClient
,避免递归调用。
retryWhen
操作符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 Observable<Data> requestObservable = apiService.getServerData() .compose(observableTransform()); Observable<Data> refreshObservable = apiService.refreshToken() .compose(observableTransform()) .compose(observableApiRetry(Arrays.asList(1 ,2 ))) .doOnNext(new Consumer <Token> (){ @Override public void accept (Token token) throws Exception { tokenManager.setAccessToken(token.getAccessToken()); } }) .onErrorResumeNext(new Function <Throwable, ObservableSource<Data>>() { @Override public ObservableSource<Data> apply (Throwable throwable) throws Exception { return Observable.error(throwable); } }); requestObservable .retryWhen(new Function <Observable<Throwable>, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Observable<Throwable> throwableObservable) throws Exception { return throwableObservable .flatMap(new Function <Throwable, ObservableSource<?>>() { @Override public ObservableSource<?> apply(Throwable throwable) throws Exception { if (throwable instanceof TokenExpiredException) { return refreshObservable; } return Observable.error(throwable); } }); } });
这两种方式都可以实现token
过期自动刷新,但是在全局高并发的场景下并不能保证刷新Token
操作同一时间只执行一次,需要做一些额外的处理,基本思想就是把refreshToken
方法进行封装,然后用锁(synchronized/Mutex)保护刷新的唯一性 。
统一错误处理 一些用户体验比较友好的应用会把应用使用过程中出现的错误 体现在应用交互上,比如Toast
、Dialog
弹框或者特别的错误界面,这里的错误 在数据层看来是发生了异常且大部分场景是在跟后台的交互过程中出现的,这时候需要做一些错误的统一处理,包括非200
的网络状态码 、网络请求失败 、服务器特定错误码 、数据解析错误 、本地数据库异常 、自定义业务异常 等等,使用RxJava
的另一个好处就是对异常的处理可以更加灵活,可以根据不同的情况做一些不同的处理,我做了如下举例:
在自定义Interceptor
里根据Http
状态码或者json
数据里的业务码throw
自定义异常
在创建Observable
的方法体里使用emitter.onError
向下游传递异常
在ApiResponse<T>
-> T
转换时根据json
数据里的业务码传递或者throw
自定义异常
针对个别Observable
使用onErrorXXX
系列接口处理异常
Observer
实现onError
处理异常,比如在ViewModel
里使用LiveData
向UI
层传递显示对应状态 的数据
Observer
未实现onError
,或者使用RxJavaPlugins.onError(throwable)
都会向全局的RxJavaPlugins.onErrorHandler
传递异常
了解这些异常的发送、传递和处理的场景后我们接下来实现一个refreshToken
过期后弹出重新登录 弹框的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private static final int USER_EXPIRED_CODE = ...;@Override public Response intercept (Interceptor.Chain chain) throws IOException { Response response = chain.proceed(chain.request()); if (response.code() == 401 ) { int code = ... if (code == USER_EXPIRED_CODE) { throw new LoginExpiredException (); } } return response; } new Transformer <ApiResponse<T>, T>() { @Override public Observable<T> call (Observable<ApiResponse<T>> observable) { return observable.flatMap(new Function <ApiResponse<T>, Observable<T>>() { @Override public Observable<T> apply (ApiResponse<T> apiResponse) throws Exception { if (apiResponse.getCode() == USER_EXPIRED_CODE) { return Observable.error(new LoginExpiredException ()); } return Observable.just(apiResponse.getData()); } }); } }
在封装的Observer
中统一处理,或者Result
包裹类转换时处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class Result <T> { @NonNull public final Status status; @Nullable public final T data; @Nullable public final Error error; private Result (@NonNull Status status, @Nullable T data, @Nullable Error error) { this .status = status; this .data = data; this .error = error; } public boolean isSuccess () { return status == Status.SUCCESS; } public static <T> Result<T> success (@NonNull T data) { return new Result <>(Status.SUCCESS, data, null ); } public static <T> Result<T> error (Error error) { return new Result <>(Status.ERROR, null , error); } }
在Transformer
中处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 new ObservableTransformer <T, Result<T>>() { @Override public Observable<Result<T>> call (Observable<T> observable) { return observable .map(new Function <T, Result<T>>() { @Override public Result<T> apply (T t) throws Exception { return Result.success(t); } }) .onErrorReturn(new Function <Throwable, Result<T>>() { @Override public Result<T> apply (Throwable throwable) throws Exception { if (throwable instanceof LoginExpiredException) { RxJavaPlugins.onError(throwable); } return Result.error(throwableMappingToError(throwable)); } }); } }
在Application
中设置全局setErrorHandler
:
1 2 3 4 5 6 7 8 RxJavaPlugins.setErrorHandler(new Consumer <Throwable>() { @Override public void accept (Throwable throwable) throws Exception { if (throwable instanceof LoginExpiredException) { } } });
总结 ReactiveX
函数式编程是一种思想,RxJava
只是这种思想在Java
语言方面的实现,其它语言上也有体现像 RxSwift
、RxJs
和RxDart
等,想要掌握RxJava
除了熟悉它的基本概念、常用操作符以及这些操作符的组合外,更要了解它的编程思想,这也是我对RxJava
接下来的学习的主要目标。RxJava
用的好当然就是神兵利器 ,效率和简洁代码上都会有很大的提升,但相对的缺点是入门比较难,使用不当会引发一些比较严重的问题,如果链式调用比较长同样也会增加出现问题后的排查难度,在使用的过程中要有意识的去规避一些潜在的问题。