转载

AOP面向切面编程--解放你的双手

AOP面向切面编程

假如现在有一个需求,在对数据库进行增删改查的时候,假如执行每个操作之前都要求把数据备份一下。这个时候怎么做比较好呢,难道要在每个方法之前都写一个save()方法吗,如果用到增删改查的地方非常多,这时候就非常麻烦了。

通过java中的动态代理就可以很方便的实现。比如

首先有个操作数据库的类

public interface DBOperation {
    int save();

    int delete();

    int insert();

    Object get();
}

定义一个activity,实现数据库操作接口,通过Proxy.newProxyInstance方法创建出DBOperation的代理实现类,这个方法需要一个InvocationHandler参数,

自定义一个InvocationHandler,在其invoke方法中我们就可以在执行每个方法之前和之后做一些自己的操作了。

public class ProxyActivity extends AppCompatActivity implements DBOperation{
    private final static String TAG = "myTag >>> ";
    DBOperation db;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_proxy);

        db = (DBOperation) Proxy.newProxyInstance(DBOperation.class.getClassLoader()
                ,new Class[]{DBOperation.class},new DBHandler(this));
    }

    public void action(View view) {
        db.delete();
    }

    class DBHandler implements InvocationHandler{
        DBOperation db;

        public DBHandler(DBOperation db) {
            this.db = db;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (db != null) {
                Log.e("TAG","before");
                save();
                Log.e("TAG","after");
                return method.invoke(db,args);
            }
            return null;
        }
    }

    @Override
    public int save() {
        Log.e(TAG, "保存数据");
        return 0;
    }

    @Override
    public int delete() {
        Log.e(TAG, "删除数据");
        return 0;
    }

    @Override
    public int insert() {
        return 0;
    }

    @Override
    public Object get() {
        return null;
    }
}

上面的代码点击执行action方法,执行结果如果下

2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: before
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/myTag >>>: 保存数据
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: after
2019-07-02 22:49:55.297 7516-7516/com.chs.architecturetest E/myTag >>>: 删除数据

在项目开发中,我们经常会遇到这样的需求

  1. 统计用户的点击行为
  2. 在进入某些页面之前先判断是否登录,如果没登录就去登录页面

我们不可能去每个方法中都写相关的统计代码,如果类很多的情况下会麻烦死还容易出错,如果使用动态代理也是比较麻烦的,这时候我们可以使用AspectJ。

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

下面使用它来解决前面的两个问题

首先配置AspectJ

app下的build.gralde中添加依赖

implementation 'org.aspectj:aspectjrt:1.8.13'

工程的build.gralde中添加classpath AspectJ还需要添加maven的依赖

dependencies {
       classpath 'com.android.tools.build:gradle:3.4.1'
       classpath 'org.aspectj:aspectjtools:1.8.10'
       classpath 'org.aspectj:aspectjweaver:1.8.10'
   }

buildscript {
   repositories {
       mavenCentral()
   }

最后app下的build.gralde中添加AspectJ的编译代码,在dependencies同级添加。

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

OK配置完毕下面开始解决第一个行为统计的问题

首先定义一个注解ClickBehavior,运行时注解,作用在方法上,并且有一个参数代表需要统计的行为的名称

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClickBehavior {
    String value();
}

然后定义一个切面类 ClickBehaviorAspectJ

@Aspect//定义切面类
public class ClickBehaviorAspectJ {
    private final static String TAG = "myTag >>> ";
    //execution 定义切入点
    //* *(..)) 通配符 可以处理所有ClickBehavior注解的方法
    @Pointcut("execution(@com.chs.architecturetest.annotation.ClickBehaviorAspectJ * *(..))")
    public void methodPointCut() {}


    //对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
    @Around("methodPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取签名方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //获取方法名
        String methodName = signature.getName();
        //获取class名
        String className = signature.getDeclaringType().getSimpleName();
        //获取需要统计的value值
        String funName = signature.getMethod().getAnnotation(com.chs.architecturetest.annotation.ClickBehavior.class).value();
        //当前时间
        long begin = System.currentTimeMillis();
        Log.e(TAG,"ClickBehaviorAspectJ Method Before");
        Object proceed = joinPoint.proceed();
        Log.e(TAG,"ClickBehaviorAspectJ Method End");
        //执行时间
        long duration =  System.currentTimeMillis() - begin;

        Log.e(TAG, String.format("统计了:%s功能,在%s类的%s方法,用时%d ms",
                funName, className, methodName, duration));
        return proceed;
    }
}

这里面有几个注解,一般用前三个就能完成

  • @Aspect 代表这是一个切面类
  • @Pointcut 设置需要切入的方法,这里设置的所有的有ClickBehavior注解的方法。我们也可以指定某一个类下的所有方法 ("execution(com.chs.architecturetest.MainActivity *(..))") ,或者整个工程中的所有方法 ("execution(* *(..))") //execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)
  • @Around 对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
  • @Before(“methodPointCut()”) 切入之前执行
  • @After(“methodPointCut()”)切入之后执行
  • @AfterReturning(value = “methodPointCut()”, returning = “returnValue”) 返回通知,切点方法返回结果之后执行
  • @AfterThrowing(value = “methodPointCut()”, throwing = “throwable”) 异常通知,切点抛出异常时执行

在Activity中整3个按钮分别为登录,VIP,账户,并设置点击方法。给这几个点击方法设置行为点击注解

@ClickBehavior("VIP页面")
public void goToVip(View view) {
  Log.e(TAG,"去VIP页面");
  startActivity(new Intent(this,OtherActivity.class));
}
@ClickBehavior("账户页面")
public void goToZh(View view) {
    Log.e(TAG,"去账户页面");
    startActivity(new Intent(this,OtherActivity.class));
}

@ClickBehavior("登录页面")
public void goToLogin(View view) {
    Log.e(TAG,"去登录页面");
}

OK完成到这里行为统计就完成了,执行带@ClickBehavior注解的方法都会执行统计的代码, 比如点击登录按钮打印日志

2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: 去登录页面
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End
2019-07-02 23:23:48.640 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:登录页面功能,在ProxyActivity类的goToLogin方法,用时0 ms

检查登录的功能

首先写一个注解ClickBehavior。它不需要有值

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginBehavior {
}

定义登录的AspectJ类

@Aspect//定义切面类
public class LoginAspectJ {
    private final static String TAG = "myTag >>> ";
    //execution 定义切入点
    //* *(..)) 通配符 可以处理所有ClickBehavior注解的方法
    @Pointcut("execution(@com.chs.architecturetest.annotation.LoginBehavior * *(..))")
    public void methodPointCut() {}


    //对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
    @Around("methodPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //是否登录真实项目中去sharedprefrence中去那
        Context context = (Context) joinPoint.getThis();
        if(false){
            Log.e(TAG, "检测到已登录!");
            return joinPoint.proceed();
        }else {
            Log.e(TAG, "检测到没有登录!");
            context.startActivity(new Intent(context,LoginActivity.class));
            return null;
        }
    }
}

在around方法中就可以执行判断是否登录的逻辑了,真实项目中一般都是从SharedPreferences中拿到数据判断是否登录。

最后给需要判断登录状态的地方添加@LoginBehavior注解

@LoginBehavior
    @ClickBehavior("VIP页面")
    public void goToVip(View view) {
      Log.e(TAG,"去VIP页面");
      startActivity(new Intent(this,OtherActivity.class));
    }
    @LoginBehavior
    @ClickBehavior("账户页面")
    public void goToZh(View view) {
        Log.e(TAG,"去账户页面");
        startActivity(new Intent(this,OtherActivity.class));
    }

    @ClickBehavior("登录页面")
    public void goToLogin(View view) {
        Log.e(TAG,"去登录页面");
    }

比如这里将前面代码if判断中直接改为false,点击去VIP页面的按钮测试结果如下,会跳转到到登录页面

2019-07-02 23:26:00.057 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:26:00.058 8640-8640/com.chs.architecturetest E/myTag >>>: 检测到没有登录!
2019-07-02 23:26:00.067 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End
2019-07-02 23:26:00.068 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:VIP页面功能,在ProxyActivity类的goToVip方法,用时10 ms

OK完成啦

原文  https://chsmy.github.io/2019/07/07/architecture/AOP面向切面编程-解放你的双手/
正文到此结束
Loading...