登陆应该是应用开发中一个很常见的功能,一般在应用中有两种登陆,一种是一进入应用就必须登陆才能使用(如微信和QQ等),另一种是需要登录的时候才会去登陆(如淘宝京东等)。我在工作中遇到的大部分是第二种情况,针对于第二种的登陆,我之前都是通过if(){}else()去判断是否登录的,但是这样项目结构庞大了之后就会使代码臃肿。因为判断用户登陆状态是一个频次很高的操作,所以针对这方面我就考虑有没有一种方案既能很方便的判断登陆状态又使代码很简洁。
想来想去方案有两种,一种是hook到AMS拦截startActivity中的intent,在启动activity的时候判断是否登录,如果没有对intent做动态替换,另一种就是通过AOP实现方法添加判断登陆代码片段。hook对系统有兼容性,需要考虑到各个版本的api是否改动,而aop的实现方式与版本没有任何兼容性问题,所以最后就采用了aop的方式去实现app集中式登陆。
为什么我先讲架构的使用,是因为你只有知道了使用这种架构是多么方便,才会有兴趣去了解如何实现这种架构。下面看代码:
我们在Application里进行初始化(初始化之后才能接收登陆事件,所以越早越好)。
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); LoginSDK.getInstance().init(this, new ILogin() { @Override public void login(Context applicationContext, int userDefine) { switch (userDefine) { case 0: startActivity(new Intent(applicationContext, LoginActivity.class)); break; case 1: Toast.makeText(applicationContext, "您还没有登录,请登陆后执行", Toast.LENGTH_SHORT).show(); break; case 2: new AlertDialog.Builder(MyApplication.this)... break; default: Toast.makeText(applicationContext, "执行失败,因为您还没有登录!", Toast.LENGTH_SHORT).show(); break; } } @Override public boolean isLogin(Context applicationContext) { return SharePreferenceUtil.getBooleanSp(SharePreferenceUtil.IS_LOGIN, applicationContext); } }); } } 复制代码
可以看到初始化方法实现了ILogin接口,ILogin接口有两个方法,第一个login()用于接收登陆事件,第二个方法isLogin是判断登陆状态,这两个方法留给用户自己实现,提高架构的可用性。我们所有的登陆请求都会回调到ILogin接口,这也意味着登陆事件只有一个统一的入口,这也就是我们集中式登陆架构的核心好处了。
好了,我们先来使用以下。
@LoginFilter(userDefine = 0) public void skip(View view) { startActivity(new Intent(this, SecondActivity.class)); } 复制代码
上面代码就是监听一个Button的点击事件,然后加入注解@LoginFilter,看方法实现只是跳转到SecondActivity,并没有登陆逻辑的判断,但通过这个注解我们就可以在运行时检测是否登录,如果没有登录就会中断方法的执行,转而调用MyApplication里init()方法中我们自己实现的login()方法,login(Context applicationContext, int userDefine)方法中userDefine是留给用户自定义的一个值,为了区别使用哪种登录方式。是不是很简单?再来看例子二:
如果我们嫌弃在需要判断登陆状态的按钮上加入@LoginFilter()注解麻烦,而是想实现启动一个Activity自动判断是否登录,如果没有登录就回调到我们的ILogin接口,那么你只需要创建一个LoginFilterActivity如下:
public class LoginFilterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (lib_login_filter_onCreate(true)) { //TOOD: 你可以做想做的逻辑,如跳转到登录界面或给用户提示 finish(); } } @LoginFilter public Boolean lib_login_filter_onCreate(Boolean aspectParam) { return aspectParam; } } 复制代码
然后我们让需要登陆才能进入的Activity继承自LoginFilterActivity就可以了。假如UserActivity继承了LoginFilterActivity,当用户没有登陆的时候,我们启动UserActivity的时候便会回调到我们的ILogin接口,是不是很方便,这就是我们今天要讲的集中式登陆架构。
下面,我们来讲一讲如何实现这个架构。
我们先来了解一下AOP,因为这个架构是基于AOP编程实现的。
关于AOP是什么,这里我简单介绍一下,AOP是Aspect Oriented Programming的缩写,即面向切面编程,与面向对象编程(oop)是两种不同的思维方式,也可以看做是对oop的一种补充。传统的oop开发会提倡功能模块化等,而aop适合于针对某一类型的问题统一处理。AOP思想的讲解不是我们本篇文章的重点,如果有同学对AOP思想不是很理解,这里我推荐一篇文章,讲得很不错 Java AOP & Spring AOP 原理和实现
AspectJ是一个面向切面编程的一个框架,它扩展了java语言,并定义了实现AOP的语法。我们知道,在将.java文件编译为.class文件时默认使用javac编译工具,而AspectJ会有一套符合java字节码编码规范的编译工具来替代javac,在将.java文件编译为.class文件时,会动态的插入一些代码来做到对某一类特定东西的统一处理。我举个例子,比如在应用中有很多个button的onClick事件需要检测是否登录,如果没有登录则需要去登陆之后才能继续执行,针对这一类型的问题,相对笨一点的做法就是在每一个onClick方法中都显式的去判断登陆状态,这样不免过于麻烦。而我们用AOP的方式实现的话,就需要在每一个onClick方法上加入一个标注,让编译器在编译时能识别到这个标注,然后根据标注来生成一些代码检测登录状态。好了,如果有同学对AOP还不是很理解的话也不用急,下面我会用例子来给大家演示如何使用AOP实现统一的集中式登陆。
首先,我们导入AspectJ的jar包,AspectJ的jar网上一搜就有,也可以直接去我demo里面拿, LoginArchitecture AOP实现集中式登陆 github链接点我 。demo里jar包导入: 好了,导入jar后还需要在app.gradle配置如下:
buildscript { repositories { mavenCentral() } dependencies { classpath 'org.aspectj:aspectjtools:1.8.8' classpath 'org.aspectj:aspectjweaver:1.8.8' } } 复制代码
然后在文件末尾添加如下代码:
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; } } } } 复制代码
这一大片代码就是为了在编译时打印信息如警告、error等等,这些东西在网上也有很多,不再一一解释。
好了,配置完上面的内容之后,我们就开始编写代码了,首先,定义一个注解LoginFilter,用来注解方法,以便在编译期被编译器检测到需要做切面的方法。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LoginFilter { int userDefine() default 0; } 复制代码
大家看到我在注解里加了个userDefine,就是为了给用户提供自定义实现,如根据userDifine值不同做不同的登陆处理。
然后,编写LoginSDK文件用于初始化和接收登录事件,代码如下:
public class LoginSDK { public void init(Context context, ILogin iLogin) { applicationContext = context.getApplicationContext(); LoginAssistant.getInstance().setApplicationContext(context); LoginAssistant.getInstance().setiLogin(iLogin); } //... } 复制代码
然后,新建LoginFilterAspect.java文件用来处理加入LoginFilter注解的方法,对这些方法做统一的切面处理。
@Aspect public class LoginFilterAspect { private static final String TAG = "LoginFilterAspect"; @Pointcut("execution(@com.xsm.loginarchitecture.lib_login.annotation.LoginFilter * *(..))") public void loginFilter() {} @Around("loginFilter()") public void aroundLoginPoint(ProceedingJoinPoint joinPoint) throws Throwable { //标注1 ILogin iLogin = LoginAssistant.getInstance().getiLogin(); if (iLogin == null) { throw new NoInitException("LoginSDK 没有初始化!"); } //标注2 Signature signature = joinPoint.getSignature(); if (!(signature instanceof MethodSignature)) { throw new AnnotationException("LoginFilter 注解只能用于方法上"); } MethodSignature methodSignature = (MethodSignature) signature; LoginFilter loginFilter = methodSignature.getMethod().getAnnotation(LoginFilter.class); if (loginFilter == null) { return; } Context param = LoginAssistant.getInstance().getApplicationContext(); //标注3 if (iLogin.isLogin(param)) { joinPoint.proceed(); } else { //标注4 Object target = joinPoint.getTarget(); Method method = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); String name = method.getName(); if (name.contains("lib_login_filter_onCreate")) { //标注5 Object[] args = joinPoint.getArgs(); if (args != null && args.length == 1 && (args[0] instanceof Boolean)) { joinPoint.proceed(new Object[] {true}); } else { iLogin.login(param, loginFilter.userDefine()); } } else { iLogin.login(param, loginFilter.userDefine()); } } } } 复制代码
代码并不多,我们来一一解释。首先看loginFilter方法,这个方法上加入@Pointcut注解,并指定了LoginFilter注解的路径,@Pointcut注解包括aroundLoginPoint()方法上的@Around注解等都是AspectJ定义的API。@Pointcut注解代表切入点,具体就是指哪些方法需要被执行"AOP"。execution()里指定了LoginFilter注解的路径,即加入LoginFilter注解的方法就是需要处理的切面。@Around注解表示这个方法执行时机的前后都可以做切面处理,常用到的还有@Before、@After等等。@Before即方法执行前做处理,@After反之。
好了,aroundLoginPoint(ProceedingJoinPoint joinPoint)方法就是对切面的具体实现了,这里ProceedingJoinPoint参数意为环绕通知,这个类里面可以获取到方法的签名等各种信息。
首先看标注1处,我们先获取用户实现的ILogin类,如果没有调用init()设置初始化就抛出异常。
标注2处先得到方法的签名methodSignature,然后得到@LoginFilter注解,如果注解为空,就不再往下走。
然后看标注3,调用iLogin的isLogin()方法判断是否登陆,这个isLogin是留给使用者自己实现的,如果登陆,就会继续执行方法体调用方法直到完成,如果没有登录,执行标注4。
首先获取到方法的对象,通过对象获取到方法名,然后判断方法名是否是“lib_login_filter_onCreate”,如果不是,调用iLogin.login()方法,这个login()方法也是留给用户自己实现的,如果方法名是“lib_login_filter_onCreate”,那么久执行标注5。
我们还记得在LoginFilterActivity里面有如下方法:
public class LoginFilterActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (lib_login_filter_onCreate(true)) { //TOOD: 你可以做想做的逻辑,如跳转到登录界面或给用户提示 finish(); } } @LoginFilter public Boolean lib_login_filter_onCreate(Boolean aspectParam) { return aspectParam; } } 复制代码
这个lib_login_filter_onCreate方法参数是Boolean类型,并且直接把参数当作返回值返回。其实这个方法的调用就是在标注5处,判断方法名等于lib_login_filter_onCreate并且参数为Boolean类型的时候,会调用这个方法然后传入true。那么为何要这么做呢?是因为当我们在继承LoginFilterActivity的时候,需要自动检测是否登陆,如果没有登录就finish()掉启动的Activity,所以你也就知道了,这个lib_login_filter_onCreate(Boolean aspectParam)方法是不能随便乱改的,如果需要进行修改,也要同时对LoginFilterAspect进行修改。
好了,切面代码的处理介绍完了,这个时候我们build一下项目,会在项目下/build/intermediates/classes/debug文件夹生成经过AspectJ编译器编译后的.class文件,我们看下上面例子1中的方法skip(View v)方法,编译成class文件的方法体变成了如下这样:
@LoginFilter public void skip(View view) { JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, view); skip_aroundBody1$advice(this, view, var3, LoginFilterAspect.aspectOf(), (ProceedingJoinPoint)var3); } 复制代码
可以看到我们的点击事件方法已经被植入了一些代码,而原来startActivity(new Intent(this, SecondActivity.class));也不见了,实际上这里是把我们方法的执行给封装了,这里会在运行期,目标类加载后,为接口动态生成代理类,将切面织入到代理类中,从而实现对方法进行统一的处理。