- 一AOP
- 1 主要功能
- 2 主要目标
- 3 适用对象
- 4 AOP与OOP的关系
- 二Android中使用AspectJ
- 1 Gradle 配置示
- 2 基本概念
- 21 切面Aspect
- 22 连接点JoinPoint
- 23 切点PointCut
- 24 通知Advise
- 3 执原
- 31 BeforeAfterAfterThrowing插入示意图
- 32 Around替换逻辑示意图
- 33 代码分析
- 4 AspectJ切面编写
- 41 日志打印
- 42 耗时监控
- 43 异常处
- 44 降级替代方案吐司
- 45 其他的系统横切关注点问题
- 三相关问题
- 1 编织速度
- 2 调试工具
一、AOP
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑
各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
1.1 主要功能
日志记录,性能统计,安全控制,事务处理,异常处理等等。
1.2 主要目标
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变
这些行为的时候不影响业务逻辑的代码。
1.3 适用对象
比较大型的项目,而且迭代较快,使用OOP太消耗内力。
有日志、性能、安全、异常处理等横切关注点需求。
1.4 AOP与OOP的关系
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。但是也有它的缺点,最明显的就是关注点聚焦时,面向对象无法简单的解决这个问题,一个关注点是面向所有而不是单一的类,不受类的边界的约束,因此OOP无法将关注点聚焦来解决,只能分散到各个类中。
AOP(面向切面编程)则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
AOP并不是与OOP对立的,而是为了弥补OOP的不足。OOP解决了竖向的问题,AOP则解决横向的问题。因为有了AOP我们的调试和监控就变得简单清晰。它们之间的关系如下图所示:
1.4.1 对比一——单一横切关注点
1.4.2 对比二——多横切关注点
结论:
二、Android中使用@AspectJ
AspectJ 意思就是Java的Aspect,Java的AOP。它其实不是一个新的语言,它的核心是ajc(编译器)\weaver(织入器)。
- ajc编译器:基于Java编译器之上的,它是用来编译.aj文件,aspectj在Java编译器的基础上增加了一些它自己的关键字和方法。因此,ajc也可以编译Java代码。
- weaver织入器:为了在java编译器上使用AspectJ而不依赖于Ajc编译器,aspectJ 5出现了@AspectJ,使用注释的方式编写AspectJ代码,可以在任何Java编译器上使用。
由于AndroidStudio默认是没有ajc编译器的,所以在Android中使用@AspectJ来编写(包括SpringAOP也是如此)。它在代码的编译期间扫描目标程序,根据切点(PointCut)匹配,将开发者编写的Aspect程序编织(Weave)到目标程序的.class文件中,对目标程序作了重构(重构单位是JoinPoint),目的就是建立目标程序与Aspect程序的连接(获得执行的对象、方法、参数等上下文信息),从而达到AOP的目的。
2.1 Gradle 配置示例
要引入AspectJ到Android工程中,最重要的就是两个包:
//在buildscript中添加该编织器,gradle构建时就会对class文件进行编织
classpath 'org.aspectj:aspectjweaver:1.8.9'
//在dependencies中添加该依赖,提供@AspectJ语法
compile 'org.aspectj:aspectjrt:1.8.9'
此外还有一个工具包,用于Gradle构建时进行打日志等操作:
//在buildscript中添加该工具包,在构建工程的时候执行一些任务:打日志等
classpath 'org.aspectj:aspectjtools:1.8.9'import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
//打印gradle日志
android.libraryVariants.all { variant >
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["showWeaveInfo",
"1.5",
"inpath", javaCompile.destinationDir.toString(),
"aspectpath", javaCompile.classpath.asPath,
"d", javaCompile.destinationDir.toString(),
"classpath", javaCompile.classpath.asPath,
"bootclasspath",
project.android.bootClasspath.join(
File.pathSeparator)]
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler)
def log = project.logger
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:
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
附:美团RoboAspectJ
buildscript {
repositories {
mavenLocal()
}
dependencies {
classpath 'com.meituan.gradle:roboaspectj:0.9.2'
classpath 'jaop.gradle.plugin:gradleplugin:1.0.2'
}
// Exclude the version that the android plugin depends on.
configurations.classpath.exclude group: 'com.android.tools.external.lombok'
}
配置参数
// AspectJ
aspectj {
disableWhenDebug true
javartNeeded true
// 排除不需要AOP扫描的包
exclude group: 'xxxx', module: 'xxxx'
compileOptions {
defaultJavaVersion = JavaVersion.VERSION_1_7
}
}
2.2 基本概念
2.2.1 切面——Aspect
实现了crosscutting功能,是针对切面的模块。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。
2.2.2 连接点——JoinPoint
连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,可以添加新的方法。比如:我们的切点可以认为是findInfo(String)方法。
AspectJ将面向对象的程序执行流程看成是JoinPoint的执行链,每一个JoinPoint是一个单独的闭包,在执行的时候将上下文环境赋予闭包执行方法体逻辑。
下面列表上的是被AspectJ认为是joinpoint的:
2.2.3 切点——PointCut
切点的声明决定需要切割的JoinPoint的集合,就结果上来说,它是JoinPoint的一个实际子集合。
pointcut可以控制你把哪些advice应用于jointpoint上去,通常通过正则表达式来进行匹配应用,决定了那个jointpoint会获得通知。分为call、execution、target、this、within等关键字,含义下面会附图。
1.直接针对JoinPoint的选择
pointcuts中最常用的选择条件和Joinpoint的类型密切相关,比如图5:
2.间接针对JPoint的选择
除了根据前面提到的Signature信息来匹配JPoint外,AspectJ还提供其他一些选择方法来选择JPoint。比如某个类中的所有JPoint,每一个函数执行流程中所包含的JPoint。
特别强调,不论什么选择方法,最终都是为了找到目标的JPoint。
表2列出了一些常用的非JPoint选择方法:
3.匹配规则
(1)类型匹配语法
首先让我们来了解下AspectJ类型匹配的通配符:
*:匹配任何数量字符;
..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。
(2)匹配模式
call(<注解?> <修饰符?> <返回值类型> <类型声明?>.<方法名>(参数列表) <异常列表>?)
- 精确匹配
//表示匹配 com.davidkuper.MainActivity类中所有被@Describe注解的public void方法。
@Pointcut("call(@Describe public void com.davidkuper.MainActivity.init(Context))")
public void pointCut(){}
- 单一模糊匹配
//表示匹配 com.davidkuper.MainActivity类中所有被@Describe注解的public void方法。
@Pointcut("call(@Describe public void com.davidkuper.MainActivity.*(..)) ")
public void pointCut(){}
//表示匹配调用Toast及其子类调用的show方法,不论返回类型以及参数列表,并且该子类在以com.meituan或者com.sankuai开头的包名内
@Pointcut("call(* android.widget.Toast+.show(..)) && (within(com.meituan..*)|| within(com.sankuai..*))")
public void toastShow() {
}
- 组合模糊匹配
//表示匹配任意Activity或者其子类的onStart方法执行,不论返回类型以及参数列表,且该类在com.meituan.hotel.roadmap包名内
@Pointcut("execution(* *..Activity+.onStart(..))&& within(com.meituan.hotel.roadmap.*)")
public void onStart(){}
(3)获取参数
- 通过声明参数语法arg()显示获取参数
@Around(value = "execution(* BitmapFacade.picasso.init(java.lang.String,java.lang.String)) && args(arg1,arg2)"
public Object aroundArgs(String arg1,String arg2,ProceedingJoinPoint joinPoint){
System.out.println("aspects arg = " + arg1.toString()+" " + arg2);
Object resutObject = null;
try {
resutObject = joinPoint.proceed(new Object[]{arg1,arg2});
} catch (Throwable e) {
e.printStackTrace();
}
return resutObject;
}
- 通过joinPoint.getArg()获取参数列表
@Around("execution(static * tBitmapFacade.picasso.init(..)) && !within(aspectj.*) ")
public void pointCutAround(ProceedingJoinPoint joinPoint){
Object resutObject = null;
try {
//获取参数列表
Object[] args = joinPoint.getArgs();
resutObject = joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
}
return resutObject;
};
- 12
(4)异常匹配
/**
* 截获Exception及其子类报出的异常。
* @param e 异常参数
*/
@Pointcut("handler(java.lang.Exception+)&&args(e)")
public void handle(Exception e) {}
2.2.4 通知——Advise
advice是我们切面功能的实现,它是切点的真正执行的地方。比如像写日志到一个文件中,会在pointcut匹配到的连接点中插入advice(包括:before、after、around等)代码到应用程序中。
(1)@Before、@After
//所有实例方法调用截获
private static final String INSTANCE_METHOD_CALL =
"call(!static * com.meituan.hotel.roadmap..*.*(..))&&target(Object)";
@Pointcut(INSTANCE_METHOD_CALL) public void instanceMethodCall() {
}
//实例方法调用前后Advice
@Before("instanceMethodCall()") public void beforInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "before instance call");
}
@After("instanceMethodCall()") public void afterInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "after instance call");
}
(2)@Around
//横切项目中所有Activity的子类,以Layout命名、以及它的子类的所有方法的执行
private static final String POINTCUT_METHOD =
"(execution(* android.app.Activity+.*(..)) ||execution(* *..Layout+.*(..)))&& within(com.meituan.hotel.roadmap.*)";
@Pointcut(POINTCUT_METHOD) public void methodAnnotated() {
}
@Around("methodAnnotated()") public Object weaveJoinPoint(ProceedingJoinPoint joinPoint)throws Throwable{
//调用原方法的执行。
Object result = joinPoint.proceed();
return result;
}
(3)@AfterThrowing
/**
* 在异常抛出后,该操作优先于下一个切点的@Before()
* @param joinPoint
* @param e 异常参数
*/
@AfterThrowing(pointcut = "afterThrow()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e){
Log.e(TAG,joinPoint.getTarget().getClass().getSimpleName() + " afterThrowing() :" + e.toString());
}
2.3 执行原理
AspectJ是通过对目标工程的.class文件进行代码注入的方式将通知(Advise)插入到目标代码中。
第一步:根据pointCut切点规则匹配的joinPoint;
第二步:将Advise插入到目标JoinPoint中。
这样在程序运行时被重构的连接点将会回调Advise方法,就实现了AspectJ代码与目标代码之间的连接。
JoinPoint类包UML:
2.3.1 Before、After(AfterThrowing)插入示意图
Before和After的插入其实就是在匹配到的JoinPoint调用的前后插入我们编写的Before\After的Advise方法,以此来达到在目标JoinPoint执行之前先进入Advise方法,执行之后进入Advise方法。
如下图所示,目标JoinPoint为FuncB()方法,需要在他执行前后进行AOP截获:
2.3.2 Around替换逻辑示意图
总体来说,使用了代理+闭包的方式进行替换,将原方法体放置到新的函数中替换,通过一个单独的闭包拆分来执行,相当于对目标JoinPoint进行了一个代理。
2.3.3 代码分析
下面的Example作为目标源码,我们对它的printLog()方法进行替换、对getValue()方法调用前后插入Advise方法。
public class Example {
String value = "value";
public void printLog() {
String str = getValue();
}
public String getValue() {
return value;
}
}
切面代码:
@Aspect
public class LogAspect{
//所有实例方法调用截获
private static final String INSTANCE_METHOD_CALL = "call(!static * com.meituan.hotel.roadmap..*.*(..))&&target(Object)"
@Pointcut(INSTANCE_METHOD_CALL) public void instanceMethodCall() {
}
//实例方法调用前后Advice
@Before("instanceMethodCall()") public void beforInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "before instance call");
}
@After("instanceMethodCall()") public void afterInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "after instance call");
}
//所有实例方法执行截获
private static final String INSTANCE_METHOD_EXECUTING = "execution(!static * com.meituan.hotel.roadmap..*.*(..))&&target(Object)"
@Pointcut(INSTANCE_METHOD_EXECUTING) public void instanceMethodExecuting() {
}
//实例方法执行Advice
@Around("instanceMethodExecuting()") public Object InstanceMethodExecutingAround(ProceedingJoinPoint joinPoint){
Log.e(getClass().getSimpleName(),
"InstanceMethodExecuting()");
Object result = printLog(joinPoint, "instance
executing");
return result;
}
}
反编译后的结果
网上给的反编译过程都是apktool——>dex2jar——>jdgui,这个我用hotel_road_map的debug包试过,反编译出来的jar包里面只有几个系统类,不知道什么原因,其他的包又可以正常反编译。
推荐一个反编译工具:jadx(可以直接反编译apk)。
public class Example {
private static final StaticPart ajc$tjp_0 = null;
private static final StaticPart ajc$tjp_1 = null;
private static final StaticPart ajc$tjp_2 = null;
String TAG = "Example";
String value = "value";
static {
ajc$preClinit();
}
//初始化连接点静态部分:方法名、参数列表、返回值、包路径等等。
private static void ajc$preClinit() {
Factory factory = new Factory("Example.java", Example.class);
ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_CALL, factory.makeMethodSig("1", "getValue", "com.meituan.hotel.roadmap.Ex");
ajc$tjp_1 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, factory.makeMethodSig("1", "printLog", "com.meituan.hotel.roadm");
ajc$tjp_2 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, factory.makeMethodSig("1", "getValue", "com.meituan.hotel.roadm");
}
//原方法的闭包,在Aspect切面中joinPoint.proceed()会调用该闭包
public class AjcClosure1 extends AroundClosure {
public AjcClosure1(Object[] objArr) {
super(objArr);
}
public Object run(Object[] objArr) {
Object[] objArr2 = this.state;
Example.printLog_aroundBody0((Example) objArr2[0], (JoinPoint) objArr2[1]);
return null;
}
}
//原方法真正的方法体,在闭包中被调用
static final void printLog_aroundBody0(Example ajc$this, JoinPoint joinPoint) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, ajc$this, ajc$this);
try {
//@Before的Advise被插入了目标代码调用之前
LogAspect.aspectOf().beforInstanceCall(makeJP);
String str = ajc$this.getValue();
} finally {
//@After的Advise被插入到目标代码调用之后,通过Finally强制执行After逻辑,每一个Before
LogAspect.aspectOf().afterInstanceCall(makeJP);
}
}
//原来的printLog()方法被AspectJ给替换了,替换成为链接AspectJ和源代码的桥梁,真正的方法体被放在了新的方法中。
public void printLog() {
//连接点构造
JoinPoint makeJP = Factory.makeJP(ajc$tjp_1, this, this);
//将连接点与原方法的闭包连接,这样就可以在AspectJ的JoinPoint中通过joinPoint.proceed()调用闭包执行原方法。
LogAspect.aspectOf().InstanceMethodExecutingAround(new AjcClosure1(new Object[]{this, makeJP}).linkClosureAndJoinPoint(
}
public String getValue() {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_2, this, this);
return (String)LogAspect
.InstanceMethodExecutingAround(new AjcClosure3(new Object[]{this, makeJP})
.linkClosureAndJoinPoint();
}
}
Before、After(AfterThrowing)插入分析
Before\After的插入调用比较简单,通过PointCut定位匹配到JoinPoint之后,将我们编写的Before\After的切面方法直接插入到目标JoinPoint前后即可。这样就可以改变原有的代码调用轨
迹,在目标方法调用前后增加我们自己的AOP方法。
//原方法真正的方法体,在闭包中被调用
static final void printLog_aroundBody0(Example ajc$this, JoinPoint joinPoint) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, ajc$this, ajc$this);
try {
//@Before的Advise被插入了目标代码调用之前
LogAspect.aspectOf().beforInstanceCall(makeJP);
String str = ajc$this.getValue();
} finally {
//@After的Advise被插入到目标代码调用之后,通过Finally强制执行After逻辑,每一个Before
LogAspect.aspectOf().afterInstanceCall(makeJP);
}
}
Around替换代码分析
JoinPoint为printLog()方法,是被Around替换的,反编译后的部分代码如下:
首先,在静态初始化的时候,通过Factory会为每一个JPoint先构造出静态部分信息StaticPart。
//初始化连接点静态部分:方法名、参数列表、返回值、包路径等等。
private static void ajc$preClinit() {
Factory factory = new Factory("Example.java", Example.class);
ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_CALL, factory.makeMethodSig("1", "getValue", "com.meituan.hotel.roadmap.Exa");
ajc$tjp_1 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, factory.makeMethodSig("1", "printLog", "com.meituan.hotel.roadma");
ajc$tjp_2 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, factory.makeMethodSig("1", "getValue", "com.meituan.hotel.roadma");
}
public JoinPoint.StaticPart makeSJP(String kind, String modifiers, String methodName, String declaringType, String paramTypes,St
//构造方法签名实例,其中存储着方法的静态信息
Signature sig = this.makeMethodSig(modifiers, methodName, declaringType, paramTypes, paramNames, "", returnType);
return new JoinPointImpl.StaticPartImpl(count++, kind, sig, makeSourceLoc(l, 1));
}
其次,将printLog方法体替换,以XXX_aroundBodyN(args)命名,原方法体被替换如下:
//原来的printLog()方法被AspectJ给替换了,替换成为链接AspectJ和源代码的桥梁,真正的方法体被放在了新的方法中。
public void printLog() {
//连接点构造
JoinPoint makeJP = Factory.makeJP(ajc$tjp_1, this, this);
//将连接点与原方法的闭包连接,这样就可以在AspectJ的JoinPoint中通过joinPoint.proceed()调用闭包执行原方法。
LogAspect.aspectOf().InstanceMethodExecutingAround(new AjcClosure1(new Object[]{this, makeJP}).linkClosureAndJoinPoint(
}
AroundClosure闭包,将运行时对象和当前连接点JP对象传入其中,调用linkClosureAndJoinPoint()进行两端的绑定,这样在Around中就可以通过ProceedingJoinPoint.proceed()调用AroundClosure,进而调用目标方法。
public abstract class AroundClosure {
protected Object[] state;
protected Object[] preInitializationState;
public AroundClosure() {
}
public AroundClosure(Object[] state) {
this.state = state;
}
public ProceedingJoinPoint linkClosureAndJoinPoint() {
//获取执行链接点,默认数组最后一个是连接点
ProceedingJoinPoint jp =
(ProceedingJoinPoint)state[state.length1];
//设置执行时闭包
jp.set$AroundClosure(this);
return jp;
}
}
JoinPointImpl,包括一个JoinPoint的静态部分和实例部分:
class JoinPointImpl implements ProceedingJoinPoint {
//JP静态部分
static class StaticPartImpl implements
JoinPoint.StaticPart {
String kind;
Signature signature;
SourceLocation sourceLocation;
private int id;
//省略
}
Object _this;
Object target;
Object[] args;
org.aspectj.lang.JoinPoint.StaticPart staticPart;
//省略....
// To proceed we need a closure to proceed on
private AroundClosure arc;
public void set$AroundClosure(AroundClosure arc) {
this.arc = arc;
}
//通过proceed()调用闭包arc的run方法,并且传入JP的执行状态:参数列表等。进而调用原方法体执行
public Object proceed() throws Throwable {
//when called from a before advice, but be a noop
if (arc == null)
return null;
else
return arc.run(arc.getState());
}
//省略....
}
2.4 AspectJ切面编写
2.4.1 日志打印
(1)追踪某些特定方法的调用日志,统计调用的频率
(2)关注某类方法的日志
(3)全局日志的打印
AOP示例代码:
/**
* Created by malingyi on 2017/3/22.
*/
/**
* 日志打印。(分静态调用、静态执行、实例调用、实例执行四类日志)
*/
@Aspect public class LogAspect {
//所有静态方法调用截获
private static final String STATIC_METHOD_CALL =
"call(static * com.meituan.hotel.roadmap..*.*(..))";
@Pointcut(STATIC_METHOD_CALL) public void staticMethodCutting() {
}
@Before("staticMethodCutting()") public void beforStaticCall(JoinPoint joinPoint) {
printLog(joinPoint, "before static call");
}
@After("staticMethodCutting()") public void afterStaticCall(JoinPoint joinPoint) {
printLog(joinPoint, "after static call");
}
//所有实例方法调用截获
private static final String INSTANCE_METHOD_CALL =
"call(!static * com.meituan.hotel.roadmap..*.*(..))&&target(Object)";
@Pointcut(INSTANCE_METHOD_CALL) public void instanceMethodCall() {
}
//实例方法调用前后Advice
@Before("instanceMethodCall()") public void beforInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "before instance call");
}
@After("instanceMethodCall()") public void afterInstanceCall(JoinPoint joinPoint) {
printLog(joinPoint, "after instance call");
}
//所有静态方法执行截获
private static final String STATIC_METHOD_EXECUTING =
"execution(static * com.meituan.hotel.roadmap..*.*(..)) && !within(com.example.monitor.*)";
@Pointcut(STATIC_METHOD_EXECUTING) public void staticExecutionCutting() {
}
//所有实例方法执行截获
private static final String INSTANCE_METHOD_EXECUTING =
"execution(!static * com.meituan.hotel.roadmap..*.*(..))&&target(Object)&& !within(com.example.monitor.*)";
@Pointcut(INSTANCE_METHOD_EXECUTING) public void instanceMethodExecuting() {
}
//静态方法执行Advice
@Around("staticExecutionCutting()") public Object staticMethodExecuting(
ProceedingJoinPoint joinPoint) {
Log.e(getClass().getSimpleName(), "staticMethodExecuting()");
Object result = printLog(joinPoint, "static executing");
return result;
}
//实例方法执行Advice
@Around("instanceMethodExecuting()") public Object InstanceMethodExecuting(
ProceedingJoinPoint joinPoint) {
Log.e(getClass().getSimpleName(), "InstanceMethodExecuting()");
Object result = printLog(joinPoint, "instance executing");
return result;
}
/**
* 日志打印和统计
* @param joinPoint
* @param describe
* @return
*/
private Object printLog(JoinPoint joinPoint, String describe) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
try {
if (joinPoint instanceof ProceedingJoinPoint) {
return ((ProceedingJoinPoint) joinPoint).proceed(joinPoint.getArgs());
}
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
Log.e(getClass().getSimpleName(), describe + " : " + signature.toLongString());
}
return null;
}
}
结果:
0424 02:39:51.388 30813081/com.meituan.hotel.roadmap E/LogAspect: InstanceMethodExecuting()
0424 02:39:51.388 30813081/com.meituan.hotel.roadmap E/LogAspect: instance executing : public int
com.meituan.hotel.roadmap.MainActivity.ViewPagerFragmentAdapter.getCount()
0424 02:39:51.418 30813171/com.meituan.hotel.roadmap E/Surface: getSlotFromBufferLocked: unknown buffer: 0xf2ca76e0
0424 02:39:51.667 30813081/com.meituan.hotel.roadmap E/LogAspect: InstanceMethodExecuting()
0424 02:39:51.667 30813081/com.meituan.hotel.roadmap E/LogAspect: InstanceMethodExecuting()
0424 02:39:51.667 30813081/com.meituan.hotel.roadmap E/LogAspect: instance executing : public void
com.meituan.hotel.roadmap.RoadMapApplication.2.onActivityStopped(android.app.Activity)
0424 02:39:51.667 30813081/com.meituan.hotel.roadmap E/LogAspect: instance executing : protected void com.meituan.hotel.roadmap.BaseActivity.onStop()
0424 02:39:51.667 30813081/com.meituan.hotel.roadmap E/ContentValues: HotelDetailActivity 停留时间: 4657.063 ms
0424 02:39:51.668 30813081/com.meituan.hotel.roadmap E/LogAspect: InstanceMethodExecuting()
0424 02:39:51.668 30813081/com.meituan.hotel.roadmap E/LogAspect: instance executing : public void
com.meituan.hotel.roadmap.RoadMapApplication.2.onActivityDestroyed(android.app.Activity)
2.4.2 耗时监控
示例代码1:
步骤:
1、编写AspectJ的语法,横切需要关注的切点
2、在其执行前后增加计时器
3、输出时间日志,进行统计分析
/**
* 时间监控
*/
@Aspect public class TimeMonitorAspect {
//横切项目中所有Activity的子类,以Layout命名、以及它的子类的所有方法的执行
private static final String POINTCUT_METHOD =
"(execution(* android.app.Activity+.*(..)) ||execution(* *..Layout+.*(..)))&& within(com.meituan.hotel.roadmap.*)"
@Pointcut(POINTCUT_METHOD) public void methodAnnotated() {
}
/**
* 截获原方法的执行,添加计时器,监控单个方法的耗时
* @throws Throwable
*/
@Around("methodAnnotated()") public Object weaveJoinPoint(ProceedingJoinPoint joinPoint)
throws Throwable {
//初始化计时器
final StopWatch stopWatch = new StopWatch();
//开始监听
stopWatch.start();
//调用原方法的执行。
Object result = joinPoint.proceed();
//监听结束
stopWatch.stop();
//日志打印
printLog(joinPoint, stopWatch);
return result;
}
private void printLog(JoinPoint joinPoint, StopWatch stopWatch) {
//获取方法信息对象
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className;
//获取当前对象,通过反射获取类别详细信息
className = joinPoint.getThis().getClass().getName();
String methodName = methodSignature.getName();
String msg = buildLogMessage(methodName, stopWatch.getTotalTime(1));
//日志存储、打印
TimeMonitorLog.log(new MethodMsg(className, msg, (long) stopWatch.getTotalTime(1)));
// TimeMonitorLog.writeToSDCard(new Path()); //日志存储
// TimeMonitorLog.ReadIn(new Path()); //日志读取
}
/**
* 创建一个日志信息
* @param methodName 方法名
* @param methodDuration 执行时间
*/
private static String buildLogMessage(String methodName, double methodDuration) {
StringBuilder message = new StringBuilder();
message.append(methodName);
message.append(" > ");
message.append("[");
message.append(methodDuration);
if (StopWatch.Accuracy == 1) {
message.append("ms");
} else {
message.append("mic");
}
message.append("] \n");
return message.toString();
}
}
结果:
0327 04:31:35.681 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: class com.meituan.hotel.roadmap.BaseActivity onStop > [0.286ms]
0327 04:31:39.040 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: class com.meituan.hotel.roadmap.HotelDetailActivity getLayoutId > [0.003ms]
0327 04:31:39.047 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: class com.meituan.hotel.roadmap.BaseActivity onCreate > [7.217ms]
0327 04:31:39.048 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: class com.meituan.hotel.roadmap.HotelDetailActivity onCreate > [7.972ms]
0327 04:31:39.050 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: class com.meituan.hotel.roadmap.BaseActivity onStart > [0.276ms]
示例代码2——监控Activity页面的停留时间
步骤:
1、编写横切项目中Activity的onStart()、onStop()的切点语法
2、然后在onStart()切点执行之前启动计时器,将其与该页面对象存入Map中进行绑定
3、在onStop()切点执行完毕之后,从通过该对象从Map中获取计时器,然后结束计时,输出日志。
/**
* 时间监控
*/
@Aspect public class TimeMonitorAspect {
private static final String TAG = "TimeMonitorAspect";
//存放<页面对象,计时器>
private HashMap<Object,StopWatch> map = new HashMap<>();
/**
* 横切页面的onStart和onStop方法,监控两个方法之间的耗时
*/
@Pointcut("execution(* *..Activity+.onStart(..))&&this(java.lang.Object)&& within(com.meituan.hotel.roadmap.*)")
public void onStart(){}
@Pointcut("execution(* *..Activity+.onStop(..))&&this(java.lang.Object)&& within(com.meituan.hotel.roadmap.*)")
public void onStop(){}
@Pointcut("onStart() && !cflowbelow(onStart())")
public void realOnStart(){}
@Pointcut("onStop() && !cflowbelow(onStop())")
public void realOnStop(){}
/**
* 在onCreate()调用时,开启该页面的计时器,将计时器存入HashMap<Object,StopWatch>中。
* @param joinPoint
* @return
*/
@Around("realOnStart()")
public Object AroundOnStart(ProceedingJoinPoint joinPoint){
Object result = null;
Object target = joinPoint.getTarget();
StopWatch stopWatch = new StopWatch();
if (target != null){
map.put(target,stopWatch);
}
try {
stopWatch.start();
result = joinPoint.proceed(joinPoint.getArgs());
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
/**
* 在onStop()结束时,从HashMap<Object,StopWatch>中获取该计时器,停止该页面的计时器,并将时间打印出来。
* @param joinPoint
* @return
*/
@Around("realOnStop()")
public Object AroundOnStop(ProceedingJoinPoint joinPoint){
Object result = null;
Object target = joinPoint.getTarget();
StopWatch stopWatch = null;
if (target != null){
stopWatch = map.get(target);
}
try {
result = joinPoint.proceed(joinPoint.getArgs());
if (stopWatch != null){
stopWatch.stop();
//打印日志
Log.e(TAG,joinPoint.getTarget().getClass().getSimpleName() + " 停留时间: "+ stopWatch.getTotalTimeMillis() + " ms"
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
结果:
0327 05:01:16.629 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: HotelDetailActivity 停留时间: 4170.12 ms
0327 04:31:39.465 2721027210/com.meituan.hotel.roadmap E/TimeMonitorAspect: MainActivity 停留时间: 4112.293 ms
2.4.3 异常处理
示例代码1——截获谋类异常
/**
* 异常处理
*/
@Aspect
public class ExceptionHandleAspect {
private static final String TAG = "ExceptionHandleAspect";
/**
* 截获空指针异常
* @param e
*/
@Pointcut("handler(java.lang.NullPointerException)&&args(e)")
public void handle(NullPointerException e){
}
/**
* 在catch代码执行之前做一些处理
* @param joinPoint
* @param e 异常参数
*/
@Before(value = "handle(e)",argNames = "e")
public void handleBefore(JoinPoint joinPoint,NullPointerException e){
Log.e(TAG,joinPoint.getSignature().toLongString()+" handleBefore() :"+e.toString());
//汇总处理
}
}
结果:
0327 06:46:48.700 1161811618/com.meituan.hotel.roadmap E/ExceptionHandleAspect: MainActivity afterThrowing() :java.lang.NullPointerException: null
项目中所有的NullPointerException的catch()方法执行之前都会被截获。
示例代码2——截获指定方法的异常
/**
* 异常处理
*/
@Aspect
public class ExceptionHandleAspect {
private static final String TAG = "ExceptionHandleAspect";
/**
* 截获某一个方法抛出的异常
*/
@Pointcut("call(* com.meituan.hotel.roadmap.*.initTabLayout(..))")
public void afterThrow(){}
/**
* 在异常抛出后,该操作优先于下一个切点的@Before()
* @param joinPoint
* @param e 异常参数
*/
@AfterThrowing(pointcut = "afterThrow()",throwing = "e")
public void afterThrowing(JoinPoint joinPoint,Exception e){
Log.e(TAG,joinPoint.getTarget().getClass().getSimpleName() + " afterThrowing() :" + e.toString());
}
}
结果:
0327 06:46:48.700 1161811618/com.meituan.hotel.roadmap E/ExceptionHandleAspect: MainActivity afterThrowing() :java.lang.NullPointerException: null
在MainActivity上调用initTabLayout()方法时抛出一个空指针,抛出后被截获了。
2.4.4 降级替代方案——吐司
在Android 5.0以后,有些手机关闭通知权限会导致Toast通知无法显示。有些项目在这之前就已经有很多Toast的使用了,那么这个时候就需要在Toast代码前后做权限验证,然后再使用备用方法替代。
使用AOP就可以将这种重复的代码聚焦在一处进行处理。类似于这样的,在代码工程迭代过程中,会大量重复用到的降级替代都可以使用AOP的思想。
步骤:
1、自定义一个Toast的View,该View不依赖于Notification通知权限。
2、截获项目中调用Toast的.show()方法。
@Aspect
public class ToastAspect {
private static final String TAG = "toastAspect";
@Pointcut("call(* android.widget.Toast+.show(..)) && (within(com.meituan..*)|| within(com.sankuai..*))")
public void toastShow() {
}
@Pointcut("toastShow() && !cflowbelow(toastShow())")
public void realToastShow() {
}
@Around("realToastShow()")
public void toastShow(ProceedingJoinPoint point) {
try {
Toast toast = (Toast) point.getTarget();
Context context = (Context) getValue(toast, "mContext");
//如果当前没有context意味着可能页面被回收,或者的版本在19以上且通知可用,执行系统的Toast方案交给系统处理
if (context == null || Build.VERSION.SDK_INT >= 19 && NotificationManagerCompat.from(context).areNotificationsEnabl
//use system function
point.proceed(point.getArgs());
} else {//如果context存在,并且通知不可用,则使用自定义的Toast
//Toast params
int mDuration = toast.getDuration();
View mNextView = toast.getView();
int mGravity = toast.getGravity();
int mX = toast.getXOffset();
int mY = toast.getYOffset();
float mHorizontalMargin = toast.getHorizontalMargin();
float mVerticalMargin = toast.getVerticalMargin();
new MToast(context instanceof Application ? context : context.getApplicationContext())
.setDuration(mDuration)
.setView(mNextView)
.setGravity(mGravity, mX, mY)
.setMargin(mHorizontalMargin, mVerticalMargin).show();
}
} catch (Throwable exception) {
//ignore
}
}
// TODO: 2016/12/14 toast.cancel() can't be work with MToast
/**
* 通过字段名从对象或对象的父类中得到字段的值
*
* @param object 对象实例
* @param fieldName 字段名
* @return 字段对应的值
* @throws Exception
*/
public static Object getValue(Object object, String fieldName) throws Exception {
if (object == null || TextUtils.isEmpty(fieldName)) {
return null;
}
Field field = null;
Class<?> clazz = object.getClass();
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
} catch (Exception e) {
//ignore
}
}
return null;
}
}
2.4.5 其他的系统横切关注点问题
使用特点:
- 关注点具有普遍性需求,代码散乱分布在工程各处,可以抽出共同的代码。
- 访问控制,例如:字段、方法的访问前做一些验证,访问之后做一些处理。
- 代码约束,例如:限制某些方法只能在特定的地方使用,否则在编译期间抛出Error错误或者Warning。
- 项目中需要临时插入一些方法、逻辑,但是不希望影响到原工程,易插易拔。
三、相关问题
3.1 编织速度
(1)尽量使用精确的匹配规则,降低匹配时间。
(2)排除不需要扫描的包。
// AspectJ
aspectj {
disableWhenDebug true
javartNeeded true
// 支付方排除
exclude group: 'com.sankuai.pay', module: 'buymodel'
exclude group: 'com.meituan.android.cashier', module: 'library'
// 第三方异常包排除
exclude group: 'com.dianping.nova.common', module: 'push'
exclude group: 'org.freemarker', module: 'freemarker'
compileOptions {
defaultJavaVersion = JavaVersion.VERSION_1_7
}
}
(3)硬件配置升级。
3.2 调试工具
Eclipse和Intellij 12支持AJDT调试工具,但是目前AndroidStudio并不支持,只能在Gradle构建时查看日志。
(1)切点:
(2)目标代码:
Gradle构建日志: