如果你用java做过后台开发,那么你一定知道AOP这个概念。如果不知道也无妨,套用百度百科的介绍,也能让你明白这玩意是干什么的:
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
项目开发过程中,可能会有这样的需求,需要我们在方法执行完成后,记录日志(后台开发中比较常见~),或是计算这个方法的执行时间,在不使用AOP的情况下,我们可以在方法最后调用另一个专门记录日志的方法,或是在方法体的首尾分别获取时间,然后通过计算时间差来计算整个方法执行所消耗的时间,这样也可以完成需求。那如果不只一个方法要这么玩怎么办?每个方法都写上一段相同的代码吗?后期处理逻辑变了要怎么办?最后老板说这功能不要了我们还得一个个删除?
很明显,这是不可能的,我们不仅仅是代码的搬运工,我们还是有思考能力的软件开发工程师。这么low的做法绝对不干,这种问题我们完全可以用AOP来解决,不就是在方法前和方法后插入一段代码吗?AOP分分钟搞定。
要注意了,AOP仅仅只是个概念,实现它的方式(工具和库)有以下几种:
本篇的主角就是AspectJ,下面就来看看AspectJ方式的AOP如何在Android开发中进行使用吧。
对于eclipse与Android Studio的引入是不一样的,本篇只介绍Android Studio如何引入AspectJ,eclipse请自行百度。Android Studio需要在app模块的build.gradle文件中引入,总共分为3个步骤:
- dependencies {
- ...
- compile 'org.aspectj:aspectjrt:1.8.9'
- }
- buildscript {
- repositories {
- mavenCentral()
- }
- dependencies {
- classpath 'org.aspectj:aspectjtools:1.8.9'
- classpath 'org.aspectj:aspectjweaver:1.8.9'
- }
- }
AspectJ需要依赖maven仓库。
- dependencies {
- ...
- }
- // 贴上面那段没用的代码是为了说明:下面的任务代码与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;
- }
- }
- }
- }
直接粘贴到build.gradle文件的末尾即可,不要嵌套在别的指令中。
在使用AspectJ之前,还是需要先介绍下AOP的基本知识,熟悉的看官可以跳过这部分。
上述术语的解释引用自《AOP中的概念通知、切点、切面》这篇文章,作者的描述非常直白,很容易理解,点个赞。
@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing需要在切面类中使用,即在使用@Aspect的类中。
这就是切点表达式:execution (* com.lqr..*.*(..))。切点表达式的组成如下:
- execution( < 修饰符模式 > ?<返回类型模式 > <方法名模式 > ( < 参数模式 > ) < 异常模式 > ?)
除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。
修饰符模式指的是public、private、protected,异常模式指的是NullPointException等。
对于切点表达式的理解不是本篇重点,下面列出几个例子说明一下就好了:
- @Before("execution(public * *(..))")
- public void before(JoinPoint point) {
- System.out.println("CSDN_LQR");
- }
匹配所有public方法,在方法执行之前打印"CSDN_LQR"。
- @Around("execution(* *to(..))")
- public void around(ProceedingJoinPoint joinPoint) {
- System.out.println("CSDN");
- joinPoint.proceed();
- System.out.println("LQR");
- }
匹配所有以"to"结尾的方法,在方法执行之前打印"CSDN",在方法执行之后打印"LQR"。
- @After("execution(* com.lqr..*to(..))")
- public void after(JoinPoint point) {
- System.out.println("CSDN_LQR");
- }
匹配com.lqr包下及其子包中以"to"结尾的方法,在方法执行之后打印"CSDN_LQR"。
- @AfterReturning("execution(int com.lqr.*(..))")
- public void afterReturning(JoinPoint point, Object returnValue) {
- System.out.println("CSDN_LQR");
- }
匹配com.lqr包下所有返回类型是int的方法,在方法返回结果之后打印"CSDN_LQR"。
- @AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex")
- public void afterThrowing(Throwable ex) {
- System.out.println("ex = " + ex.getMessage());
- }
匹配com.lqr包及其子包中的所有方法,当方法抛出异常时,打印"ex = 报错信息"。
@Pointcut是专门用来定义切点的,让切点表达式可以复用。
你可能需要在切点执行之前和切点报出异常时做些动作(如:出错时记录日志),可以这么做:
- @Before("execution(* com.lqr..*(..))")
- public void before(JoinPoint point) {
- System.out.println("CSDN_LQR");
- }
- @AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex")
- public void afterThrowing(Throwable ex) {
- System.out.println("记录日志");
- }
可以看到,表达式是一样的,那要怎么重用这个表达式呢?这就需要用到@Pointcut注解了,@Pointcut注解是注解在一个空方法上的,如:
- @Pointcut("execution(* com.lqr..*(..))")
- public void pointcut() {}
这时,"pointcut()"就等价于"execution(* com.lqr..*(..))",那么上面的代码就可以这么改了:
- @Before("pointcut()")
- public void before(JoinPoint point) {
- System.out.println("CSDN_LQR");
- }
- @AfterThrowing(value = "pointcut()", throwing = "ex")
- public void afterThrowing(Throwable ex) {
- System.out.println("记录日志");
- }
经过上面的学习,下面是时候实战一下了,这里我们来一个简单的例子。
这是界面上一个按钮的点击事件,就是一个简单的方法而已,我们拿它来试刀。
- public void test(View view) {
- System.out.println("Hello, I am CSDN_LQR");
- }
要织入一段代码到目标类方法的前前后后,必须要有一个切面类,下面就是切面类的代码:
- @Aspect
- public class TestAnnoAspect {
- @Pointcut("execution(* com.lqr.androidaopdemo.MainActivity.test(..))")
- public void pointcut() {
- }
- @Before("pointcut()")
- public void before(JoinPoint point) {
- System.out.println("@Before");
- }
- @Around("pointcut()")
- public void around(ProceedingJoinPoint joinPoint) throws Throwable {
- System.out.println("@Around");
- }
- @After("pointcut()")
- public void after(JoinPoint point) {
- System.out.println("@After");
- }
- @AfterReturning("pointcut()")
- public void afterReturning(JoinPoint point, Object returnValue) {
- System.out.println("@AfterReturning");
- }
- @AfterThrowing(value = "pointcut()", throwing = "ex")
- public void afterThrowing(Throwable ex) {
- System.out.println("@afterThrowing");
- System.out.println("ex = " + ex.getMessage());
- }
- }
先来试试看,这几个注解的执行结果如何。
不对啊,按钮的点击事件中有打印"Hello, I am CSDN_LQR"的,这里没有,怎么肥事?
这里因为@Around环绕通知会拦截原方法内容的执行,我们需要手动放行才可以。代码修改如下:
- @Around("pointcut()")
- public void around(ProceedingJoinPoint joinPoint) throws Throwable {
- System.out.println("@Around");
- joinPoint.proceed();// 目标方法执行完毕
- }
也不对啊,少了一个@AfterThrowing通知。这个通知只有在切点抛出异常时才会执行,我们可以让代码出现一个简单的运行时异常:
- public void test(View view) {
- System.out.println("Hello, I am CSDN_LQR");
- int a = 1 / 0;
- }
这下@AfterThrowing通知确实被调用了,而且也打印出了错误信息(divide by zero)。但@AfterReturning通知反而不执行了,原因很简单,都抛出异常了,切点肯定是不能返回结果的。也就是说:@AfterThrowing通知与@AfterReturning通知是冲突的,在同个切点上不可能同时出现。
因为@Around是环绕通知,可以在切点的前后分别执行一些操作,AspectJ为了能肯定操作是在切点前还是在切点后,所以在@Around通知中需要手动执行joinPoint.proceed()来确定切点已经执行,故在joinPoint.proceed()之前的代码会在切点执行前执行,在joinPoint.proceed()之后的代码会切点执行后执行。于是,方法耗时计算的实现就是这么简单:
- @Around("pointcut()")
- public void around(ProceedingJoinPoint joinPoint) throws Throwable {
- long beginTime = SystemClock.currentThreadTimeMillis();
- joinPoint.proceed();
- long endTime = SystemClock.currentThreadTimeMillis();
- long dx = endTime - beginTime;
- System.out.println("耗时:" + dx + "ms");
- }
发现没有,上面所有的通知都会至少携带一个JointPoint参数,这个参数包含了切点的所有信息,下面就结合按钮的点击事件方法test()来解释joinPoint能获取到的方法信息有哪些:
- MethodSignature signature = (MethodSignature) joinPoint.getSignature();
- String name = signature.getName(); // 方法名:test
- Method method = signature.getMethod(); // 方法:public void com.lqr.androidaopdemo.MainActivity.test(android.view.View)
- Class returnType = signature.getReturnType(); // 返回值类型:void
- Class declaringType = signature.getDeclaringType(); // 方法所在类名:MainActivity
- String[] parameterNames = signature.getParameterNames(); // 参数名:view
- Class[] parameterTypes = signature.getParameterTypes(); // 参数类型:View
前面的切点表达式结构是这样的:
- execution( < 修饰符模式 > ?<返回类型模式 > <方法名模式 > ( < 参数模式 > ) < 异常模式 > ?)
但实际上,上面的切点表达式结构并不完整,应该是这样的:
- execution( < @注解类型模式 > ?<修饰符模式 > ?<返回类型模式 > <方法名模式 > ( < 参数模式 > ) < 异常模式 > ?)
这就意味着,切点可以用注解来标记了。
如果用注解来标记切点,一般会使用自定义注解,方便我们拓展。
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface TestAnnoTrace {
- String value();
- int type();
- }
其中的value和type是自己拓展的属性,方便存储一些额外的信息。
这个自定义注解只能注解在方法上(构造方法除外,构造方法也叫构造器,需要使用ElementType.CONSTRUCTOR),像平常使用其它注解一样使用它即可:
- @TestAnnoTrace(value = "lqr_test", type = 1)
- public void test(View view) {
- System.out.println("Hello, I am CSDN_LQR");
- }
既然用注解来标记切点,那么切点表达式肯定是有所不同的,要这么写:
- @Pointcut("execution(@com.lqr.androidaopdemo.TestAnnoTrace * *(..))")
- public void pointcut() {}
切点表达式使用注解,一定是@+注解全路径,如:@com.lqr.androidaopdemo.TestAnnoTrace。
亲测可用 ,不贴图了。
上面在编写自定义注解时就声明了两个属性,分别是value和type,而且在使用该注解时也都为之赋值了,那怎么在通知中获取这两个属性值呢?还记得JoinPoint这个参数吧,它就可以获取到注解中的属性值,如下所示:
- MethodSignature signature = (MethodSignature) joinPoint.getSignature();
- Method method = signature.getMethod();
- // 通过Method对象得到切点上的注解
- TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
- String value = annotation.value();
- int type = annotation.type();
github.com/GitLqr/Andr…
来源: https://juejin.im/post/5a0d640c51882509e5433bf8