AOP思想实现android集中式登录架构

853 阅读6分钟

最近正好有时间把项目重构一遍,发现好多功能都是需要先登录才能用,产生了大量重复的代码。以前了解过AspectJ,可以做一个集中式的架构,趁着有时间研究了下。相关代码AopTest已上传到github,文章和代码结合看效果更佳。

AOP

说到AspectJ就不得不提AOP,相信学编程的人都知道OOP(Object Oriented Programming ,面向对象编程),把功能封装在一个类中,使用的时候创建该类的对象,调用对象的方法或者使用其属性即可,OOP具有可重用性、灵活性和扩展性。那么AOP又是什么呢?

AOP(Aspect Oriented Programming,面向切面编程),通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

想了解更多关于AOP的还是,大家可以看看深入理解Android之AOP

关于AspectJ本文就不多做介绍,介绍类的文章网上一搜一大把,感兴趣的朋友可以自行去看看,下面来介绍下如何使用AspectJ完成集中式登录的。

首先是在module的gradle中添加AspectJ的相关引用,

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

跟android模块平级,在dependencies中添加相关的引用

implementation 'org.aspectj:aspectjrt:1.8.9'

而在完成这一切之后还需要添加以下代码,跟android模块平级

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
//libraryVariants
variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return
    }

    JavaCompile javaCompile = variant.getJavaCompileProvider().get()
    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
            }
        }
    }
}

这些都是模板化的代码,不需要死记硬背,做完这一切之后就可以开始使用AspectJ了。

集中式登录架构

先回顾下要实现的需求,使用相关功能模块时要先判断有没有登录,没有则去登录,已登录的则直接进入功能模块。用AspectJ实现的话先创建一个注解,

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckLogin {
    int loginCode();
}

代码很简单,之所以加个loginCode参数是为了登录成功之后直接跳转到相关功能页面,省去用户登录成功之后再次点击的麻烦。下面是跳转到相关功能页面的方法,非常简单:

    @CheckLogin(loginCode = 1011)
    public void toSecond() {
        Intent intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }

可以看到方法中已经添加了之前写的检查是否登录的注解CheckLogin,那么这个注解是怎么生效的呢,重点则是下面这段代码

@Aspect
public class LoginAspect {
    ......

    @Pointcut("execution(@com.rxy.aop.aoptest.annotation.CheckLogin * *(..))")
    public void checkLogin() {
    }

    @Around("checkLogin()")
    public Object dealCheckLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        AppCompatActivity context = (AppCompatActivity) joinPoint.getThis();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        Log.d("Peter", "className is " + className + "---methodName is " + methodName);
        SharedPreferences sp = context.getSharedPreferences("default", Context.MODE_PRIVATE);
        boolean isLogin = sp.getBoolean(Constant.KEY_IS_LOGIN, false);
        if (isLogin) {
            return joinPoint.proceed();
        } else {
            int loginCode = methodSignature.getMethod().getAnnotation(CheckLogin.class).loginCode();
            Intent intent = new Intent(context, LoginActivity.class);
            context.startActivityForResult(intent, loginCode);
            return null;
        }
    }
    ......
}

从代码中可以看到LoginAspect类添加了Aspect注解,这个注解是告诉系统这个类是AspectJ相关的类,让@CheckLogin生效的则是下面这段代码

 @Pointcut("execution(@com.rxy.aop.aoptest.annotation.CheckLogin * *(..))")
 public void checkLogin() {
 }

这段代码必须非常严格,错一个地方都会使代码无法生效。Pointcut是切点的意思,表示会在这个地点介入。execution处理JPoint的类型,例如call、execution,execution里的值则是注解的全路径加上通配符构成。而我们处理介入后的相关逻辑则是在下面这个方法中

  @Around("checkLogin()")
    public Object dealCheckLogin(ProceedingJoinPoint joinPoint) throws Throwable {
      ......
    }
  • @Around:是advice,也就是具体的插入点。@Around该方法的逻辑会包含切入点前后,如果用到该注解,记得自己需要控制切入点的执行逻辑,调用joinPoint.proceed()。如果使用@Before注解,表示的是在切入点之前执行,@After表示在切入点之后执行,此时不需要调用joinPoint.proceed(),更多关于切入点的语法大家可以自行网上搜一下。

方法的逻辑很简单,判断有没有登录,没有则跳转到登录页面后return null,已登录则return joinPoint.proceed(),这段代码的意思是直接执行原有的代码逻辑,到这里代码就写完了,下面看效果

aop登录效果

效果非常完美,看到这里你也许会思考,aop除了可以做集中式登录外还能做什么呢,其实还能做很多事情,全局的网络判断,方法执行耗时,功能统计等等。

下面就简答看下aop是怎么计算方法耗时的,同样也是三个步骤,写注解,为方法添加注解,然后处理注解,下面是见证代码的时刻

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCount {
}
  @TimeCount
    public void count() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < 1000; i++) {
        }
    }
@Aspect
public class LoginAspect {
    .......
    @Pointcut("execution(@com.rxy.aop.aoptest.annotation.TimeCount * *(..))")
    public void timeCount() {
    }

    @Around("timeCount()")
    public Object dealTimeCount(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();
        Log.d("Peter", "start time is " + start + " end time is " + end + "---execute time is " + (end - start));
        Log.e("Peter", String.format("这是%s类里面的%s方法的耗时,总耗时是%s", className, methodName, end - start));
        return result;
    }
    .......
}

这个count()方法只是用来测试的,写的不严谨,大家写代码的时候千万别这么写,下面是运行的log日志

2020-05-06 15:07:41.079 14016-14016/com.rxy.aop.aoptest D/Peter: start time is 1588748860077 end time is 1588748861078---execute time is 1001
2020-05-06 15:07:41.083 14016-14016/com.rxy.aop.aoptest E/Peter: 这是MainActivity类里面的count方法的耗时,总耗时是1001

从上面这部分代码中发现计算方法耗时的功能其实很简单,如果有需求需要统计某个方法执行的时间完全可以用AspectJ来实现。

总结

本文简单介绍了AspectJ实现集中式登录和计算方法耗时的方法,篇幅有限就不再继续介绍其他用法,感兴趣的同学可以自己尝试写一写,希望大家可以从本文中学到想要的东西。

相关链接

PS:喜欢的同学可以扫码关注我的公众号呦