1. 概述
AOP(面向切面编程)的概念现在已经应用的非常广泛了,下面是从百度百科上摘抄的一段解释,比较浅显易懂
AOP 是一种编程思想,但是它的实现方式有很多,比如:Spring、AspectJ、JavaAssist、ASM 等。由于我是做 Android 开发的,所以会用 Android 中的一些例子。
- 的 就是一个典型的应用,其利用了自定义 Gradle 插件 + AspectJ 的方式,将有特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试
- 由于最近在学习 Java 字节码和 ASM 方面的知识,所以也照猫画虎,写了一个 ,实现了和 同样的功能,将特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试,不过我使用的是 自定义 Gradle 插件 + ASM 的方式
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
简单点说,通过 javac 将 .java 文件编译成 .class 文件,.class 文件中的内容虽然不同,但是它们都具有相同的格式,ASM 通过使用访问者(visitor)模式,按照 .class 文件特有的格式从头到尾扫描一遍 .class 文件中的内容,在扫描的过程中,就可以对 .class 文件做一些操作了,有点黑科技的感觉
二. Java 字节码 & 虚拟机
2.1 Java 字节码
提到 Java 字节码,可能很多人都不是很熟悉,大概都知道使用 javac 可以将 .java 文件编译成 .class 文件,.class 文件中存放的就是该 .java 文件对应的字节码内容,比如如下一段 Demo.java 代码很简单:
package com.lijiankun24.classpractice;
public class Demo {
private int m;
public int inc() {
return m + 1;
}
}
通过 javac 编译生成对应的 Demo.class 文件,使用纯文本文件打开 Demo.class,其中的内容是以 8 位字节为基础单位的二进制流,表面来看就是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守 Java 虚拟机规范的
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4465 6d6f 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 4465 6d6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0600
0100 0d00 0000 0200 0e
如果再使用 javap -verbose Demo.class
查看该 Demo.class 中的内容,如下图所示
2.2 Java 虚拟机类加载机制
上面一小节介绍了 .class 文件的结构,但是 .class 文件是静态的,它最终是会被虚拟机加载才能执行的,那么问题来了,.class 文件是什么时候会被加载呢?
一般来说,一个 .class 文件就包含一个 Java 类,.class 文件和 Java 类是息息相关的。要说 .class 文件的加载时机,就不得不提到 Java 类的生命周期了。想必大家都知道,Java 类的生命周期包含加载
、验证
、准备
、解析
、初始化
、使用
、卸载
七个步骤,在 Java 虚拟机规范中并没有规定 Java 类的加载
时机,但是却规定了 Java 类 初始化
的时机,而加载
又一定是在初始化的前面,所以也可以说是间接地规定了 .class 文件的加载
的时机。
有五种情况,是必须初始化
一个类的,这五种情况被称为对 Java 类的主动引用
,除了 主动引用
之外,其他的对 Java 类的引用称为 被动引用
。
上面也提到了 Java 类的生命周期总共分为加载
、验证
、准备
、解析
、初始化
、使用
、卸载
,其中最重要的是前五个步骤加载
、验证
、准备
、解析
、初始化
,那在这五个步骤中都发生了什么事情呢?
举一个简单的例子,如下所示。下面的 Constant
类中,有一个静态 static
代码块,和一个静态 static
变量, 是什么时候给 value 赋值的呢?什么时候会执行 static 代码块呢?答案是在类的 初始化
阶段。
public class Constant {
static {
System.out.println("Constant init!");
}
public static String value = "lijiankun24!";
}
在 Java 类中,如果有静态 static
代码块、静态 static
变量的话,编译器会为这个类自动生成一个类构造器
(注意,不是实例构造器
),在 类构造器
中会执行静态 static
代码块,初始化静态 static
变量,类构造器
就是在类的 初始化
阶段执行的
提到 Java 类的加载,就不得不说起 Java 中的类加载器 ClassLoader 了,双亲委派模型及其好处也是必须要清楚的。
2.3 Java 虚拟机字节码执行引擎
除了方法的执行过程,还需要了解一下 Java 中的方法调用
。方法调用就是指通过 .class 文件中方法的符号引用,确认方法的直接引用的过程,这个过程有可能发生在加载阶段,也有可能发生在运行阶段。
有一些方法是在加载阶段就已经确定了方法的直接引用,比如:静态方法、私有方法、实例构造器方法,这类方法的调用称为 解析
;除了解析
,方法的 静态分派
也是在加载阶段就确定了方法的直接引用,这类方法常见的就是 重载
的方法。
有一些方法是在运行阶段确认方法的直接引用的,比如:重写
的方法,调用重写
的方法时,需要具体到对象的实际类型,所以需要特定的 Java 字节码 invokevirtual
去确定合适的方法。
Java 虚拟机是基于栈的解释执行的,这里所说的栈
就是 Java 虚拟机栈,解释执行时相对于编译执行而言的,解释执行就是指:代码通过编译生成字节码指令集之后,通过解释器解释执行的。这个不用了解的太深,明白这几个定义就好
三. 访问者模式 & ASM
3.1 访问者模式
3.2 ASM 库的介绍和使用
四. Koala
4.1 添加 Koala Gradle Plugin 依赖
在项目工程的 build.gradle
中添加如下代码:
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath
}
}
在需要使用的 module 中的 build.gradle 中添加如下代码:
apply plugin: "com.lijiankun24.koala-plugin"
4.2 添加 Koala 依赖
Gradle:
compile 'com.lijiankun24:koala:1.1.2'
Maven:
<dependency>
<groupId>com.lijiankun24</groupId>
<artifactId>koala</artifactId>
<version>1.1.2</version>
<type>pom</type>
</dependency>
4.3 使用
使用起来还是非常简单的,在 Java 的方法上添加 @KoalaLog
注解,如下所示:
@KoalaLog
public String getName(String first, String last) {
SystemClock.sleep(15); // Don't ever really do this!
return first + " " + last;
}
当上述方法被调用的时候,Logcat 中的输出如下所示:
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/0KoalaLog: ┌───────────────────────────────────------───────────────────────────────────------
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/1KoalaLog: │ The class's name: com.lijiankun24.practicedemo.MainActivity
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/2KoalaLog: │ The method's name: getName(java.lang.String, java.lang.String)
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/3KoalaLog: │ The arguments: [li, jiankun]
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/4KoalaLog: │ The result: li jiankun
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/5KoalaLog: │ The cost time: 15ms
09-04 20:51:38.008 12076-12076/com.lijiankun24.practicedemo I/6KoalaLog: └───────────────────────────────────------───────────────────────────────────------
4.4 混淆规则
-keep class com.lijiankun24.koala.** { *; }
- Email:
- Home:
- 简书:
- 微博: