前言
在项目中有很多功能都是需要在用户登录之后才能使用,所以我们在开发过程中就需要在很多地方判断用户的登录状态,在没有登录的状态下要先跳转到登录页面,用户完成登录之后才能使用后续功能。如果我们在每个需要用户登录的地方都写代码去判断,不仅耗时耗力,而且代码看起来也不优雅。基于AOP思想,我们可以以一种更优雅的方式实现登录拦截功能。
AOP思想
AOP(面向切面编程)是一种设计思想,是对oop(面向对象编程)一种补充;AOP采取横向收取机制,取代了传统纵向继承体系重复性代码,把某一类问题集中在一个地方进行处理,比如处理程序中的点击事件、打印日志等。
面向切面编程,传统的oop开发中的代码逻辑是自上而下的,这些自上而下的过程中会产生一些重复的横切性问题,这些横切的问题和我们主要的业务逻辑关系不大,会散落在代码的各个功能地方,维护麻烦;AOP是编程思想就是把业务逻辑和横切问题进行分离,从而达到解耦的目的,提高代码的重用性和开发效率。关于AOP与OOP的区别,可以引用邓凡平老师在深入理解Android之AOP中说的话:OOP和AOP都是方法论,表示的是我们从什么角度来看待问题。OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有功能都能完美得划分到模块中。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。
实现登录拦截
了解了AOP之后我们来看一下如何基于AOP思想实现登录拦截。这里需要借助第三方框架AspectJ。AspectJ是Java中流行的AOP编程扩展框架,AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。AspectJ的更多信息可以参考看AspectJ在Android中的强势插入。
废话不多说,直接上代码,首先自定义两个注解,这两个注解的作用是用来定义需要进行登录拦截的切点:
//弹出登录界面后 不需要触发用户登录成功后的后续操作
//使用此注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
//弹出登录界面后 需要一个回调用来触发用户登录成功后的
//后续操作 使用此注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCallback {
}
在Activity中声明两个点击方法:
@Login
private void go1() {
Intent intent = new Intent(this,TargetActivity.class);
intent.putExtra("target","go1");
startActivity(intent);
}
@LoginCallback
private void go2() {
Intent intent = new Intent(this,TargetActivity.class);
intent.putExtra("target","go2");
startActivity(intent);
}
这两个方法都需要在用户处于登录状态下才能跳转到新的页面,如果用户没有登录,会先跳转到登录页面。两个方法的区别在于go1方法在用户登录成功后不会被执行,需要用户再次点击才会跳转到新的页面。而go2方法在用户登录成功后会被立即执行跳转到新的页面。两个方法都属于切点方法,我们会利用AspectJ在切点方法的前后插入想要执行的代码。
然后看一下如何使用AspectJ解析@Login和@LoginCallback,并且将登录拦截的代码插入到切点前后:
//声明当前类需要由AspectJ进行处理
@Aspect
public class LoginAspect {
//声明需要处理的注解类型 @Login
@Pointcut("execution(@com.zzd.logintest.Login * *(..))")
public void Login() {
}
//声明需要处理的注解类型 @LoginCallback
@Pointcut("execution(@com.zzd.logintest.LoginCallback * *(..))")
public void LoginCallback() {
}
//注释1
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable{
Signature signature = joinPoint.getSignature();
if (!(signature instanceof MethodSignature))return;
LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);
if (loginCallback==null)return;
if (LoginManager.getLoginManager().isLogin){
joinPoint.proceed();
}else {
LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();
//注释2
LiveDataBus.getDefault().subscribe("login").observe(lifecycleOwner, new Observer<Object>() {
@Override
public void onChanged(@Nullable Object integer) {
try {
//注释3
joinPoint.proceed();
LiveDataBus.getDefault().remove("login");
} catch (Throwable throwable) {
LiveDataBus.getDefault().remove("login");
throwable.printStackTrace();
}
}
});
LoginManager.getLoginManager().goLoinActivity();
}
}
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable{
Signature signature = joinPoint.getSignature();
if (!(signature instanceof MethodSignature))return;
Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login==null)return;
if (LoginManager.getLoginManager().isLogin){
joinPoint.proceed();
}else {
LoginManager.getLoginManager().goLoinActivity();
}
}
}
简单说明一下代码中使用到的AspectJ注解:
@Aspect:在类上使用,用来声明当前类需要由AspectJ进行处理
@Pointcut 在方法上使用,声明需要处理我们的自定义注解,参数是我们之前自定义的注解全类名
注释1处的@Around注解 ,声明一个具体的切点。@Around 表明会在切点方法的前后插入我们自己的代码。另外在AspectJ中还有@Before注解,表明需要在切点方法之前插入代码,@After注解 表明需要在切点方法之后插入代码。
注释2处代码是使用LiveData写的事件总线,用来发射用户登录成功之后的通知。
注释3处执行切点方法,也就是之前在Activity中定义的go1、go2方法。在joinPoint.proceed()前后的代码都会在编译期间被AspectJ插入进go1、go2方法的前后。
loginCallbackJoinPoint()方法是用来处理@LoginCallback注解的。主要逻辑是判断用户是否登录,如果是登录状态则直接执行切点方法,如果没有登录会先跳转到登录页面,登录成功之后LiveDataBus会收到通知然后执行切点方法。
loginJoinPoint()方法方法是用来处理@Login注解的。主要逻辑是判断用户是否登录,如果是登录状态则直接执行切点方法,如果没有登录会先跳转到登录页面。
总结
到目前为止,我们就基于AOP思想实现了登录拦截功能,以后我们对于需要用户登录之后才能使用的功能只需要在对应的方法上添加@LoginCallback注解或者@Login注解就行了,彻底摆脱传统耗时耗力的开发方式。
需要注意的是AspectJ虽然使用起来很方便,能帮我们轻松完成函数插桩功能,但是它也有自己的缺点。AspectJ 在实现时会包装自己的一些类,不仅会影响切点方法的性能,还会导致安装包体积的增大。所有在使用时需要仔细权衡是否适合自己的项目。