當RxJava遇到AOP

Tony沈哲發表於2017-07-28

如來神掌.jpg
如來神掌.jpg

背景

公司打算開發一款全新的To C產品,因此我開始做一些搭建框架的事兒以及POC。新的產品能夠使用一些比較新的技術,在新產品中我大量使用了Rx。這就導致了原先的AOP框架在某些場景下是無法使用的,藉此機會我順便升級了一下原先的AOP框架。

回顧一下之前寫過的一篇文章歸納AOP在Android開發中的幾種常見用法
AOP框架地址:github.com/fengzhizi71…

@Trace

在之前的文章中講過@Trace,它能追蹤某個方法花費的時間。如果想這樣追蹤匿名內部類花費的時間,原先的程式碼是無法使用的,只能追蹤到initData()花費的時間。

    @Trace
    private void initData() {

        Observable.create(new ObservableOnSubscribe<String>() {

            @Trace
            @Override
            public void subscribe(@NonNull ObservableEmitter<String> e) throws Exception {

                e.onNext("111");
                e.onNext("222");
                e.onNext("333");

            }
        }).subscribe(new Consumer<String>() {

            @Trace
            @Override
            public void accept(@NonNull String str) throws Exception {

            }
        });
    }複製程式碼

改了一下TraceAspect

package com.safframework.aop;

import com.safframework.aop.annotation.Trace;
import com.safframework.log.L;
import com.safframework.tony.common.utils.Preconditions;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

/**
 * Created by Tony Shen on 16/3/22.
 */
@Aspect
public class TraceAspect {

    private static final String POINTCUT_METHOD = "execution(@com.safframework.aop.annotation.Trace * *(..))";

    private static final String POINTCUT_CONSTRUCTOR = "execution(@com.safframework.aop.annotation.Trace *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodAnnotatedWithTrace() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructorAnnotatedTrace() {
    }

    @Around("methodAnnotatedWithTrace() || constructorAnnotatedTrace()")
    public Object traceMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

        Trace trace = methodSignature.getMethod().getAnnotation(Trace.class);
        if (!trace.enable()) {
            return joinPoint.proceed();
        }

        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        final StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = joinPoint.proceed();
        stopWatch.stop();

        if (Preconditions.isBlank(className)) {
            className = "Anonymous class";
        }

        L.i(className, buildLogMessage(methodName, stopWatch.getTotalTimeMillis()));

        return result;
    }

    /**
     * Create a log message.
     *
     * @param methodName A string with the method name.
     * @param methodDuration Duration of the method in milliseconds.
     * @return A string representing message.
     */
    private static String buildLogMessage(String methodName, long methodDuration) {
        StringBuilder message = new StringBuilder();
        message.append(methodName);
        message.append("()");
        message.append(" take ");
        message.append("[");
        message.append(methodDuration);
        message.append("ms");
        message.append("]");

        return message.toString();
    }
}複製程式碼

在這裡,
@Pointcut 表示攔截的切入點方法,是方法級別之上的註解,但是不執行方法體,只表示切入點的入口。

@Around 用於判斷是否執行以上的攔截。

這樣,原先的程式碼就能work了,在建立Observable時發射了三次,所以在subscribe時也接收到三次accept(),符合預期。

@Trace的追蹤效果.jpeg
@Trace的追蹤效果.jpeg

@HookMethod

@HookMethod在之前的文章中也講到過,是比較經典的AOP使用方式,能在方法執行前後進行hook。

我同樣也修改了HookMethodAspect,使得@HookMethod也能夠在匿名內部類中使用,滿足相關的業務場景。

RxView.clicks(holder.imageView)
                .compose(RxJavaUtils.preventDuplicateClicksTransformer())
                .subscribe(new Consumer<Object>() {

                    @HookMethod(beforeMethod = "saveContentToDB")
                    @Override
                    public void accept(@NonNull Object o) throws Exception {

                        Intent intent = new Intent(mContext, ContentDetailActivity.class);
                        intent.putExtra("content_item",item);
                        intent.putExtra("page_start_time",new DateTime());
                        mContext.startActivity(intent);
                    }

                    private void saveContentToDB() {

                        if (User.currentUser().isLoggedIn()){

                            App.getInstance().queue.addOperation(new Operation() {
                                @Override
                                public void run(Queue queue, Bundle bundle) {

                                    DBUtils.insertContent(item);
                                }
                            });
                        }
                    }
                });複製程式碼

需要注意的是,目前beforeMethod、afterMethod所對應的方法只能位於所在類中。可能,未來會針對這一塊做一些優化。

@CheckLogin

由於跟業務結合緊密,@CheckLogin並不在框架中。但是在專案中使用它完全沒問題。

首先,定義註解

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;

/**
 * Created by Tony Shen on 2017/7/26.
 */

@Target({METHOD, CONSTRUCTOR})
@Retention(RetentionPolicy.CLASS)
public @interface CheckLogin {
}複製程式碼

再定義一個Aspect

/**
 * Created by Tony Shen on 2017/7/26.
 */

@Aspect
public class CheckLoginAspect {

    private static final String POINTCUT_METHOD = "execution(@cn.magicwindow.toutiao.aop.CheckLogin * *(..))";

    private static final String POINTCUT_CONSTRUCTOR = "execution(@cn.magicwindow.toutiao.aop.CheckLogin *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodAnnotatedWithCheckLogin() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructorAnnotatedCheckLogin() {
    }

    @Around("methodAnnotatedWithCheckLogin() || constructorAnnotatedCheckLogin()")
    public void checkLogin(final ProceedingJoinPoint joinPoint) throws Throwable {

        if (!User.currentUser().isLoggedIn()) {

            Router.getInstance().open("login");
            return;
        }

        joinPoint.proceed();

    }
}複製程式碼

@CheckLogin可以直接在匿名內部類中使用,它會先判斷使用者是否登入,如果沒有登入就會跳轉到登入頁面。如果已經登入,則處理後面的業務邏輯。

RxView.clicks(favorite)
                .compose(RxJavaUtils.preventDuplicateClicksTransformer())
                .compose(RxLifecycle.bind(ContentDetailActivity.this).toLifecycleTransformer())
                .subscribe(new Consumer<Object>() {

                    @CheckLogin
                    @Override
                    public void accept(@NonNull Object o) throws Exception {
                        ......
                    }
                });複製程式碼

@CheckLogin 也可以結合類似butterknife這樣的框架使用。

    @CheckLogin
    @OnClick(id=R.id.button_favorite)
    void onClickFav() {
           ......
    }複製程式碼

總結

本文有一點小小的標題黨,其實我所做的修改並不僅僅是為了RxJava。但是RxJava已經變得越來越流行,AOP也能夠跟Rx很好地相結合。本文除了記錄工作中個人所使用的一些東西,也希望能夠起到一些拋磚引玉的作用。

相關文章