AspectJ 的优势与局限性
最常用的字节码处理框架有 AspectJ、ASM 等等,它们的相同之处在于输入输出都是 Class 文件。并且,它们都是 在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行。
而 AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能。
1、AspectJ 的优势
它的优势有两点:成熟稳定、使用非常简单。
1、成熟稳定
字节码的处理并不简单,特别是 针对于字节码的格式和各种指令规则,如果处理出错,就会导致程序编译或者运行过程中出现问题。
而 AspectJ 作为从 2001 年发展至今的框架,它已经发展地非常成熟,通常不用考虑插入的字节码发生正确性相关的问题。
2、使用非常简单
AspectJ 的使用非常简单,并且它的功能非常强大,我们完全不需要理解任何 Java 字节码相关的知识,就可以在很多情况下对字节码进行操控。例如,它可以在如下五个位置插入自定义的代码:
- 1)、*在方法(包括构造方法)被调用的位置。
- 2)、在方法体(包括构造方法)的内部。
- 3)、在读写变量的位置。
- 4)、在静态代码块内部。
- 5)、在异常处理的位置的前后。
此外,它也可以 直接将原位置的代码替换为自定义的代码。
2、AspectJ 的缺陷
AspectJ
的缺点可以归结为如下 三点:
1、切入点固定
AspectJ 只能在一些固定的切入点来进行操作,如果想要进行更细致的操作则很难实现,它无法针对一些特定规则的字节码序列做操作。
2、正则表达式的局限性
AspectJ 的匹配规则采用了类似正则表达式的规则,比如 匹配 Activity 生命周期的 onXXX 方法,如果有自定义的其他以 on 开头的方法也会匹配到,这样匹配的正确性就无法满足。
3、性能较低
AspectJ 在实现时会包装自己一些特定的类,它并不会直接把 Trace 函数直接插入到代码中,而是经过一系列自己的封装。
这样不仅生成的字节码比较大,而且对原函数的性能会有不小的影响。
如果想对 App 中所有的函数都进行插桩,性能影响肯定会比较大。如果你只插桩一小部分函数,那么 AspectJ 带来的性能损耗几乎可以忽略不计。
AspectJ 核心语法简介
AspectJ 其实就是一种 AOP 框架,AOP 是实现程序功能统一维护的一种技术。
利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时大大提高了开发效率。
- 1)、无侵入性。
- 2)、修改方便。
此外,AOP 不同于 OOP 将问题划分到单个模块之中,它把 涉及到众多模块的同一类问题进行了统一处理。
比如我们可以设计两个切面,一个是用于处理 App 中所有模块的日志输出功能,另外一个则是用于处理 App 中一些特殊函数调用的权限检查。
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理。
2、切面(Aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象。
3、连接点(JoinPoint)
JPoint 是一个程序的关键执行点,也是我们关注的重点。它就是指被拦截到的点(如方法、字段、构造器等等)。
4、切入点(PointCut)
对 JoinPoint 进行拦截的定义。PointCut 的目的就是提供一种方法使得开发者能够选择自己感兴趣的 JoinPoint。
5、通知(Advice)
切入点仅用于捕捉连接点集合,但是,除了捕捉连接点集合以外什么事情都没有做。事实上实现横切行为我们要使用通知。
它 一般指拦截到 JoinPoint 后要执行的代码,分为 前置、后置、环绕 三种类型。
这里,我们需要 注意 Advice Precedence(优先权) 的情况,比如我们对同一个切面方法同时使用了 @Before 和 @Around 时就会报错,此时会提示需要设置 Advice 的优先级。
切入点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而切面则是对所有这些新结构的封装。
在 Android
平台上要使用 AspectJ
还是有点麻烦的,这里我们可以直接使用沪江的 AspectJX 框架。下面,我们就来使用 AspectJX
进行 AOP
切面编程。
AspectJX 实战
首先,为了在 Android 使用 AOP 埋点需要引入 AspectJX,在项目根目录的 build.gradle 下加入:
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
然后,在 app 目录下的 build.gradle 下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
JoinPoint 一般定位在如下位置:
- 1)、函数调用。
- 2)、获取、设置变量。
- 3)、类初始化。
使用 PointCut 对我们指定的连接点进行拦截,通过 Advice,就可以拦截到 JoinPoint 后要执行的代码。Advice 通常有以下 三种类型:
- 1)、Before:PointCut 之前执行。
- 2)、After:PointCut 之后执行。
- 3)、Around:PointCut 之前、之后分别执行。
1、最简单的 AspectJ 示例
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
Log.d(...)
}
其中,在 execution 中的是一个匹配规则,第一个 * 代表匹配任意的方法返回值,后面的语法代码匹配所有 Activity 中以 on 开头的方法。这样,我们就可以 在 App 中所有 Activity 中以 on 开头的方法中输出一句 log。
上面的 execution 就是处理 Join Point 的类型,通常有如下两种类型:
- 1)、call:代表调用方法的位置,插入在函数体外面。
- 2)、execution:代表方法执行的位置,插入在函数体内部。
2、统计 Application 中所有方法的耗时
@Aspect
public class ApplicationAop {
@Around("call (* com.json.chao.application.BaseApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(TAG, name + " cost" + (System.currentTimeMillis() - time));
}
}
需要注意的是,当 Action 为 Before、After 时,方法入参为 JoinPoint。当 Action 为 Around 时,方法入参为 ProceedingPoint。
而 Around 和 Before、After 的最大区别就是 ProceedingPoint 不同于 JoinPoint,其提供了 proceed 方法执行目标方法。
3、对 App 中所有的方法进行 Systrace 函数插桩
我们就可以利用它实现对 App 中所有的方法进行 Systrace 函数插桩了,代码如下所示:
@Aspect
public class SystraceTraceAspectj {
private static final String TAG = "SystraceTraceAspectj";
@Before("execution(* **(..))")
public void before(JoinPoint joinPoint) {
TraceCompat.beginSection(joinPoint.getSignature().toString());
}
@After("execution(* **(..))")
public void after() {
TraceCompat.endSection();
}
}
使用 AspectJ 打造自己的性能监控框架
将以奇虎360的 ArgusAPM 性能监控框架来全面分析下 AOP 技术在性能监控方面的应用。
监控 OKHttp3 的每一次网络请求
首先,我们看到 OKHttp3 的切面文件,代码如下所示:
/**
* OKHTTP3 切面文件
*
* @author ArgusAPM Team
*/
@Aspect
public class OkHttp3Aspect {
// 1、定义一个切入点,用于直接调用 OkHttpClient 的 build 方法。
@Pointcut("call(public okhttp3.OkHttpClient build())")
public void build() {
}
// 2、使用环绕通知在 build 方法执行前添加一个 NetWokrInterceptor。
@Around("build()")
public Object aroundBuild(ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
if (target instanceof OkHttpClient.Builder && Client.isTaskRunning(ApmTask.TASK_NET)) {
OkHttpClient.Builder builder = (OkHttpClient.Builder) target;
builder.addInterceptor(new NetWorkInterceptor());
}
return joinPoint.proceed();
}
}
在注释1、2处,在调用 OkHttpClient 的 build 方法之前添加了一个 NetWokrInterceptor。我们看看它的实现代码,如下所示:
@Override
public Response intercept(Chain chain) throws IOException {
// 1、获取每一个 OkHttp 请求的开始时间
long startNs = System.currentTimeMillis();
mOkHttpData = new OkHttpData();
mOkHttpData.startTime = startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp request 开始时间:" + mOkHttpData.startTime);
}
Request request = chain.request();
// 2、记录当前请求的请求 url 和请求数据大小
recordRequest(request);
Response response;
try {
response = chain.proceed(request);
} catch (IOException e) {
if (Env.DEBUG) {
e.printStackTrace();
Log.e(TAG, "HTTP FAILED: " + e);
}
throw e;
}
// 3、记录这次请求花费的时间
mOkHttpData.costTime = System.currentTimeMillis() - startNs;
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed 耗时:" + mOkHttpData.costTime);
}
// 4、记录当前请求返回的响应码和响应数据大小
recordResponse(response);
if (Env.DEBUG) {
Log.d(TAG, "okhttp chain.proceed end.");
}
// 5、记录 OkHttp 的请求数据
DataRecordUtils.recordUrlRequest(mOkHttpData);
return response;
}
首先,在注释1处,获取了每一个 OkHttp 请求的开始时间。接着,在注释2处,通过 recordRequest 方法记录了当前请求的请求 url 和请求数据大小。然后,注释3处,记录了这次 请求所花费的时间。
接下来,在注释4处,通过 recordResponse 方法记录了当前请求返回的响应码和响应数据大小。最后,在注释5处,调用了 DataRecordUtils 的 recordUrlRequest 方法记录了 mOkHttpData 中保存好的数据。我们继续看到 recordUrlRequest 方法,代码如下所示:
/**
* recordUrlRequest
*
* @param okHttpData
*/
public static void recordUrlRequest(OkHttpData okHttpData) {
if (okHttpData == null || TextUtils.isEmpty(okHttpData.url)) {
return;
}
QOKHttp.recordUrlRequest(okHttpData.url, okHttpData.code, okHttpData.requestSize,
okHttpData.responseSize, okHttpData.startTime, okHttpData.costTime);
if (Env.DEBUG) {
Log.d(Env.TAG, "存储okkHttp请求数据,结束。");
}
}
可以看到,这里调用了 QOKHttp 的 recordUrlRequest 方法用于记录网络请求信息。我们再看到 QOKHttp 的 recordUrlRequest 方法,如下所示:
/**
* 记录一次网络请求
*
* @param url 请求url
* @param code 状态码
* @param requestSize 发送的数据大小
* @param responseSize 接收的数据大小
* @param startTime 发起时间
* @param costTime 耗时
*/
public static void recordUrlRequest(String url, int code, long requestSize, long responseSize,
long startTime, long costTime) {
NetInfo netInfo = new NetInfo();
netInfo.setStartTime(startTime);
netInfo.setURL(url);
netInfo.setStatusCode(code);
netInfo.setSendBytes(requestSize);
netInfo.setRecordTime(System.currentTimeMillis());
netInfo.setReceivedBytes(responseSize);
netInfo.setCostTime(costTime);
netInfo.end();
}
可以看到,这里 将网络请求信息保存在了 NetInfo 中,并最终调用了 netInfo 的 end 方法,代码如下所示:
/**
* 为什存储的操作要写到这里呢?
* 历史原因
*/
public void end() {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "end :");
}
this.isWifi = SystemUtils.isWifiConnected();
this.costTime = System.currentTimeMillis() - startTime;
if (AnalyzeManager.getInstance().isDebugMode()) {
AnalyzeManager.getInstance().getNetTask().parse(this);
}
ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_NET);
if (task != null) {
// 1
task.save(this);
} else {
if (DEBUG) {
LogX.d(TAG, SUB_TAG, "task == null");
}
}
}
可以看到,这里 最终还是调用了 NetTask 实例的 save 方法保存网络请求的信息。而 NetTask 肯定是使用了与之对应的 NetStorage 实例将信息保存在了 ContentProvider 中
监控 HttpConnection 和 HttPClient 的每一次网络请求
在 ArgusAPM 中,使用的是 TraceNetTrafficMonitor 这个切面类对 HttpConnection 的每一次网络请求进行监控。关键代码如下所示:
@Aspect
public class TraceNetTrafficMonitor {
// 1
@Pointcut("(!within(com.argusapm.android.aop.*) && ((!within(com.argusapm.android.**) && (!within(com.argusapm.android.core.job.net.i.*) && (!within(com.argusapm.android.core.job.net.impl.*) && (!within(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient) && !target(com.qihoo360.mobilesafe.mms.transaction.MmsHttpClient)))))))")
public void baseCondition() {
}
// 2
@Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))")
public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {
}
// 3
@Around("httpClientExecuteOne(httpClient, request)")
public HttpResponse httpClientExecuteOneAdvice(HttpClient httpClient, HttpUriRequest request) throws IOException {
return QHC.execute(httpClient, request);
}
// 排查一些处理异常的切面代码
// 4
@Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
public void URLOpenConnectionOne(URL url) {
}
// 5
@Around("URLOpenConnectionOne(url)")
public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
return QURL.openConnection(url);
}
// 排查一些处理异常的切面代码
}
TraceNetTrafficMonitor 里面的操作分为 两类,一类是用于切 HttpClient 的 execute 方法,即注释1、2、3处所示的切面代码;一类是用于切 HttpConnection 的 openConnection 方法,对应的切面代码为注释4、5处。我们首先分析 HttpClient 的情况,这里最终 调用了 QHC 的 execute 方法进行处理
public static HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException {
return isTaskRunning()
? AopHttpClient.execute(client, request)
: client.execute(request);
}
这里又 继续调用了 AopHttpClient 的 execute 方法,代码如下所示:
public static HttpResponse execute(HttpClient httpClient, HttpUriRequest request) throws IOException {
NetInfo data = new NetInfo();
// 1
HttpResponse response = httpClient.execute(handleRequest(request, data));
// 2
handleResponse(response, data);
return response;
}
首先,在注释1处,调用了 handleRequest 处理请求数据,如下所示:
private static HttpUriRequest handleRequest(HttpUriRequest request, NetInfo data) {
data.setURL(request.getURI().toString());
if (request instanceof HttpEntityEnclosingRequest) {
HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest) request;
if (entityRequest.getEntity() != null) {
// 1、将请求实体使用 AopHttpRequestEntity 进行了封装
entityRequest.setEntity(new AopHttpRequestEntity(entityRequest.getEntity(), data));
}
return (HttpUriRequest) entityRequest;
}
return request;
}
可以看到,在注释1处,使用 AopHttpRequestEntity 对请求实体进行了封装,这里的目的主要是为了 便于使用封装实体中的 NetInfo 进行数据操作。
接着,在注释2处,将得到的响应信息进行了处理,这里的实现很简单,就是 使用 NetInfo 这个实体类将响应信息保存在了 ContentProvider 中。
我们接着分析下 HTTPConnection 的切面部分代码,如下所示:
// 4
@Pointcut("call(java.net.URLConnection openConnection()) && (target(url) && baseCondition())")
public void URLOpenConnectionOne(URL url) {
}
// 5
@Around("URLOpenConnectionOne(url)")
public URLConnection URLOpenConnectionOneAdvice(URL url) throws IOException {
return QURL.openConnection(url);
}
可以看到,这里是 调用了 QURL 的 openConnection 方法进行处理。我们来看看它的实现代码:
public static URLConnection openConnection(URL url) throws IOException {
return isNetTaskRunning() ? AopURL.openConnection(url) : url.openConnection();
}
这里 又调用了 AopURL 的 openConnection 方法,继续 看看它的实现:
public static URLConnection openConnection(URL url) throws IOException {
if (url == null) {
return null;
}
return getAopConnection(url.openConnection());
}
private static URLConnection getAopConnection(URLConnection con) {
if (con == null) {
return null;
}
if (Env.DEBUG) {
LogX.d(TAG, "AopURL", "getAopConnection in AopURL");
}
// 1
if ((con instanceof HttpsURLConnection)) {
return new AopHttpsURLConnection((HttpsURLConnection) con);
}
// 2
if ((con instanceof HttpURLConnection)) {
return new AopHttpURLConnection((HttpURLConnection) con);
}
return con;
}
最终,在注释1处,会判断如果是 https 请求,则会使用 AopHttpsURLConnection 封装 con,如果是 http 请求,则使用 AopHttpURLConnection 进行封装。
AopHttpsURLConnection 的实现与它类似,仅仅是多加了 SSL 证书验证的部分。
- 1)、在回调 getHeaderFields()、getInputStream()、getLastModified() 等一系列方法时会调用 inspectAndInstrumentResponse 方法把响应大小和状态码保存在 NetInfo 中。
- 在回调 onInputstreamComplete()、onInputstreamError()等方法时,即请求完成或失败时,此时会直接调用 myData 的 end 方法将网络响应信息保存在 ContentProvider 中。