Android AOP拯救混乱的代码架构

时间:2021-12-23 01:08:24

为什么要写这篇文章:

如今各大平台能搜xx框架如何使用的一大堆,但提及如何利用写出优雅的代码的文章却少之又少。所以本文主要提供一个思路来优化代码,也算抛砖引玉。若各位有不同看法或意见,可以在评论区提出,或者私信。博主看到会及时回复。

本文介绍:

  1. 拿过来直接可以运行

  1. 没有多余的废话,不会涉及到原理。先看到效果,用起来再去探究原理。

  1. 对小白友好

场景:

一个线上需要登录的App,许多函数在使用之前都要校验用户是否已登录。于是最直接的方案就是在这些函数中增加大量的if else来进行判断用户是登录......如下所示:

//需要验证的登录状态的函数
    private void sendMsg(String msg){
        //Manger.getInstance().getState() 获取登录状态
        switch (Manger.getInstance().getState()){
            case -1://用户被强制下线
                //提醒用户 账号密码可能已泄漏
                //然后跳转到登录页面
                return;
            case 0://用户未登录
                //跳转登录页面
                return;
            case 1://用户已登录
                sendImMsg(msg);//验证通过发送消息
                break;
        }
    }

这种使用if else用来控制权限,着实不太优雅。且随着项目业务逐渐增多,管理登录状态的类(Manger)会跟多处代码耦合,浸入量极大。要解决这个问题,我们可以使用AOP思想来实现对登录模块的控制。(若不清楚AOP思想,可以先去了解一下大概意思)这里我们使用Aspectj这个框架来优化整段代码。

引入插件与依赖

  1. 在Project中的build.gradle中引入

buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.0.2"
        
        //引入Aspectj插件 非常重要!!
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2.在Module中的build.gradle中引入

dependencies {
    //引入Aspectj框架
    implementation 'org.aspectj:aspectjrt:1.8.13'
}

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;
            }
        }
    }
}

编写注解

每次调用函数时,我们需要先检查检查用户是否登录。如果用注解自动帮我们去检查,那么代码会优雅很多。这里我们定义一个注解,告诉Aspect 切入点在哪。

@CheckLogin 作用:加了@CheckLogin注解的函数运行之前都会自动去检查用户是否登录

//注意此处的包名 后面需要用,若你之间复制到自己项目中,包名改了,注解起不了作用
package com.chj.chjaj.loginmode.annotation;

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

/***
 * 作者:chj233
 * 时间:2023/3/17 23:16
 * 描述:用于检查登录
 */
@Target(ElementType.METHOD)//此注解只能写在函数上
@Retention(RetentionPolicy.RUNTIME)//注解生效 运行时
public @interface CheckLogin {
}

@LoginOut 作用:当校验未通过时(Manger.getInstance().getState() == -1),运行加了@LoginOut注解的函数

//注意此处的包名 后面需要用,若你之间复制到自己项目中,包名改了,注解起不了作用
package com.chj.chjaj.loginmode.annotation;

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

/***
 * 作者:chj233
 * 时间:2023/3/17 23:15
 * 描述:登录退出触发此注解的函数
 */
@Target(ElementType.METHOD)//此注解只能写在函数上
@Retention(RetentionPolicy.RUNTIME)//注解生效 运行时
public @interface LoginOut {
}

@LoginTo 作用:当校验为通过时(Manger.getInstance().getState() == 0),运行加了@LoginTo注解的函数

//注意此处的包名 后面需要用,若你之间复制到自己项目中,包名改了,注解起不了作用
package com.chj.chjaj.loginmode.annotation;

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

/***
 * 作者:chj233
 * 时间:2023/3/17 23:15
 * 描述:需要去登录
 */
@Target(ElementType.METHOD)//此注解只能写在函数上
@Retention(RetentionPolicy.RUNTIME)//注解生效 运行时
public @interface LoginTo {
}

AspectLogin 作用:这个类最为关键,注解是否按照我们编写好的执行,就看这个类

package com.chj.chjaj.loginmode;

import com.chj.chjaj.loginmode.annotation.LoginOut;
import com.chj.chjaj.loginmode.annotation.LoginTo;

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

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/***
 * 作者:chj233
 * 时间:2023/3/17 23:13
 * 描述:注解控制
 */
@Aspect
public class AspectLogin {

    //切点 com.chj.chjaj.loginmode.annotation包下的CheckLogin注解为切点所有函数
    @Pointcut("execution(@com.chj.chjaj.loginmode.annotation.CheckLogin  * *(..))")
    public void loginCheck(){
    }

    //切面 loginCheck() 就是上面切点的函数名称 这个名称可以随便改 但两个地方需要保持一致
    @Around("loginCheck()")
    public void check(final ProceedingJoinPoint point) throws Throwable {
        switch (Manger.getInstance().getState()){
            case 1://已登录验证通过
                point.proceed();//往下执行,若不执行point.proceed(),那么 加了切点注解的函数都不会运行
                break;
            case -1://已退出登录
                invokeAnnotion(point.getThis(), LoginOut.class); //point.getThis() 获取注解所在的类的对象
                break;
            case 0://被强制下线
                invokeAnnotion(point.getThis(), LoginTo.class); //point.getThis() 获取注解所在的类的对象
                break;
            default:
                break;
        }
    }

    //通过对象反射
    public static void invokeAnnotion(Object object, Class annotionClass) {
        Class<?> objectClass = object.getClass(); // 获取class对象
        // 遍历所有函数
        Method[] methods = objectClass.getDeclaredMethods(); // 得到所有的 对象中所有 公开 私有 函数
        for (Method method : methods) {//循环这些函数
            method.setAccessible(true); // 让虚拟机不要去检测 private的函数

            // 判断是否被 annotionClass 注解过的函数
            boolean annotationPresent = method.isAnnotationPresent(annotionClass); //若是被注解的函数
            if (annotationPresent) { //那么之间执行
                // 当前函数 annotionClass 注解过的函数
                try {
                    method.invoke(object);//执行函数
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

MainActivity 作用:就是用来测试的

package com.chj.chjaj;

import android.os.Bundle;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.chj.chjaj.loginmode.Manger;
import com.chj.chjaj.loginmode.annotation.CheckLogin;
import com.chj.chjaj.loginmode.annotation.LoginOut;
import com.chj.chjaj.loginmode.annotation.LoginTo;

//简化业务层的代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.denglu).setOnClickListener((view) ->{
            Manger.getInstance().setState(1);
            toast("已登录");
        });
        findViewById(R.id.tuichu).setOnClickListener((view) ->{
            Manger.getInstance().setState(-1);
            toast("已退出");
        });
        findViewById(R.id.xiaxian).setOnClickListener((view) ->{
            Manger.getInstance().setState(0);
            toast("已下线");
        });
        findViewById(R.id.fasong).setOnClickListener((view) ->{
            send();
        });
        findViewById(R.id.getinfo).setOnClickListener((view )-> {
            getInfo();
        });
    }

    @CheckLogin//只需要增加@CheckLogin注解 即可去检查当前的登录状态
    protected void send(){
        toast("发送消息");
    }

    @CheckLogin//只需要增加@CheckLogin注解 即可去检查当前的登录状态
    protected void getInfo(){
        toast("获取信息");
    }

    @LoginOut//用户已退出 执行此函数
    public void logout(){
        toast("登录已退出");
    }

    @LoginTo//需要用户登录 执行此函数
    public void loginTo(){
        toast("跳转登录.....");
    }

    protected void toast(String msg){
        if (msg == null) return;
        Toast.makeText(this,msg,Toast.LENGTH_LONG).show();
    }

}

总结一下:

此方案使用Aspect 通过反射的方式来执行注解标记的函数,所以在性能上会略低,所以对性能要求非常高的函数并不太适用。