39

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

 4 years ago
source link: https://www.tuicool.com/articles/Ff6jAjZ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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完成啦


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK