为什么要写这篇文章:
如今各大平台能搜xx框架如何使用的一大堆,但提及如何利用写出优雅的代码的文章却少之又少。所以本文主要提供一个思路来优化代码,也算抛砖引玉。若各位有不同看法或意见,可以在评论区提出,或者私信。博主看到会及时回复。
本文介绍:
拿过来直接可以运行
没有多余的废话,不会涉及到原理。先看到效果,用起来再去探究原理。
对小白友好
场景:
一个线上需要登录的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这个框架来优化整段代码。
引入插件与依赖
在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 通过反射的方式来执行注解标记的函数,所以在性能上会略低,所以对性能要求非常高的函数并不太适用。