AOP面向切面编程
假如现在有一个需求,在对数据库进行增删改查的时候,假如执行每个操作之前都要求把数据备份一下。这个时候怎么做比较好呢,难道要在每个方法之前都写一个save()方法吗,如果用到增删改查的地方非常多,这时候就非常麻烦了。
通过java中的动态代理就可以很方便的实现。比如
首先有个操作数据库的类
public interface DBOperation { int save(); int delete(); int insert(); Object get(); }
定义一个activity,实现数据库操作接口,通过Proxy.newProxyInstance方法创建出DBOperation的代理实现类,这个方法需要一个InvocationHandler参数,
自定义一个InvocationHandler,在其invoke方法中我们就可以在执行每个方法之前和之后做一些自己的操作了。
public class ProxyActivity extends AppCompatActivity implements DBOperation{ private final static String TAG = "myTag >>> "; DBOperation db; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_proxy); db = (DBOperation) Proxy.newProxyInstance(DBOperation.class.getClassLoader() ,new Class[]{DBOperation.class},new DBHandler(this)); } public void action(View view) { db.delete(); } class DBHandler implements InvocationHandler{ DBOperation db; public DBHandler(DBOperation db) { this.db = db; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (db != null) { Log.e("TAG","before"); save(); Log.e("TAG","after"); return method.invoke(db,args); } return null; } } @Override public int save() { Log.e(TAG, "保存数据"); return 0; } @Override public int delete() { Log.e(TAG, "删除数据"); return 0; } @Override public int insert() { return 0; } @Override public Object get() { return null; } }
上面的代码点击执行action方法,执行结果如果下
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: before 2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/myTag >>>: 保存数据 2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: after 2019-07-02 22:49:55.297 7516-7516/com.chs.architecturetest E/myTag >>>: 删除数据
在项目开发中,我们经常会遇到这样的需求
我们不可能去每个方法中都写相关的统计代码,如果类很多的情况下会麻烦死还容易出错,如果使用动态代理也是比较麻烦的,这时候我们可以使用AspectJ。
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
下面使用它来解决前面的两个问题
首先配置AspectJ
app下的build.gralde中添加依赖
implementation 'org.aspectj:aspectjrt:1.8.13'
工程的build.gralde中添加classpath AspectJ还需要添加maven的依赖
dependencies { classpath 'com.android.tools.build:gradle:3.4.1' classpath 'org.aspectj:aspectjtools:1.8.10' classpath 'org.aspectj:aspectjweaver:1.8.10' } buildscript { repositories { mavenCentral() }
最后app下的build.gralde中添加AspectJ的编译代码,在dependencies同级添加。
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; } } } }
OK配置完毕下面开始解决第一个行为统计的问题
首先定义一个注解ClickBehavior,运行时注解,作用在方法上,并且有一个参数代表需要统计的行为的名称
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ClickBehavior { String value(); }
然后定义一个切面类 ClickBehaviorAspectJ
@Aspect//定义切面类 public class ClickBehaviorAspectJ { private final static String TAG = "myTag >>> "; //execution 定义切入点 //* *(..)) 通配符 可以处理所有ClickBehavior注解的方法 @Pointcut("execution(@com.chs.architecturetest.annotation.ClickBehaviorAspectJ * *(..))") public void methodPointCut() {} //对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情 @Around("methodPointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //获取签名方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //获取方法名 String methodName = signature.getName(); //获取class名 String className = signature.getDeclaringType().getSimpleName(); //获取需要统计的value值 String funName = signature.getMethod().getAnnotation(com.chs.architecturetest.annotation.ClickBehavior.class).value(); //当前时间 long begin = System.currentTimeMillis(); Log.e(TAG,"ClickBehaviorAspectJ Method Before"); Object proceed = joinPoint.proceed(); Log.e(TAG,"ClickBehaviorAspectJ Method End"); //执行时间 long duration = System.currentTimeMillis() - begin; Log.e(TAG, String.format("统计了:%s功能,在%s类的%s方法,用时%d ms", funName, className, methodName, duration)); return proceed; } }
这里面有几个注解,一般用前三个就能完成
("execution(com.chs.architecturetest.MainActivity *(..))")
,或者整个工程中的所有方法 ("execution(* *(..))")
//execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?) 在Activity中整3个按钮分别为登录,VIP,账户,并设置点击方法。给这几个点击方法设置行为点击注解
@ClickBehavior("VIP页面") public void goToVip(View view) { Log.e(TAG,"去VIP页面"); startActivity(new Intent(this,OtherActivity.class)); } @ClickBehavior("账户页面") public void goToZh(View view) { Log.e(TAG,"去账户页面"); startActivity(new Intent(this,OtherActivity.class)); } @ClickBehavior("登录页面") public void goToLogin(View view) { Log.e(TAG,"去登录页面"); }
OK完成到这里行为统计就完成了,执行带@ClickBehavior注解的方法都会执行统计的代码, 比如点击登录按钮打印日志
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before 2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: 去登录页面 2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End 2019-07-02 23:23:48.640 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:登录页面功能,在ProxyActivity类的goToLogin方法,用时0 ms
检查登录的功能
首先写一个注解ClickBehavior。它不需要有值
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LoginBehavior { }
定义登录的AspectJ类
@Aspect//定义切面类 public class LoginAspectJ { private final static String TAG = "myTag >>> "; //execution 定义切入点 //* *(..)) 通配符 可以处理所有ClickBehavior注解的方法 @Pointcut("execution(@com.chs.architecturetest.annotation.LoginBehavior * *(..))") public void methodPointCut() {} //对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情 @Around("methodPointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { //是否登录真实项目中去sharedprefrence中去那 Context context = (Context) joinPoint.getThis(); if(false){ Log.e(TAG, "检测到已登录!"); return joinPoint.proceed(); }else { Log.e(TAG, "检测到没有登录!"); context.startActivity(new Intent(context,LoginActivity.class)); return null; } } }
在around方法中就可以执行判断是否登录的逻辑了,真实项目中一般都是从SharedPreferences中拿到数据判断是否登录。
最后给需要判断登录状态的地方添加@LoginBehavior注解
@LoginBehavior @ClickBehavior("VIP页面") public void goToVip(View view) { Log.e(TAG,"去VIP页面"); startActivity(new Intent(this,OtherActivity.class)); } @LoginBehavior @ClickBehavior("账户页面") public void goToZh(View view) { Log.e(TAG,"去账户页面"); startActivity(new Intent(this,OtherActivity.class)); } @ClickBehavior("登录页面") public void goToLogin(View view) { Log.e(TAG,"去登录页面"); }
比如这里将前面代码if判断中直接改为false,点击去VIP页面的按钮测试结果如下,会跳转到到登录页面
2019-07-02 23:26:00.057 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before 2019-07-02 23:26:00.058 8640-8640/com.chs.architecturetest E/myTag >>>: 检测到没有登录! 2019-07-02 23:26:00.067 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End 2019-07-02 23:26:00.068 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:VIP页面功能,在ProxyActivity类的goToVip方法,用时10 ms
OK完成啦