面向切面编程(Aspect-Oriented Programming),是Java开发中常见的编程方式,很多著名的开源框架都是基于AOP思想实现,比如Spring、Retrofit、Mybatis等,实现AOP的方式有很多种,主要分为三类:静态编译
、编译期注入代码
、动态编译
,前两者都是在生成jar、dex包之前就完成代码处理,动态编译是在运行时实现,比如补丁就是动态编译实现。
示例图
.java
<静态编译>
.class
<编译期注入代码>
.dex
运行时
<动态编译与动态生成字节码、动态代理>
分类
我在这里总结了10种AOP的方式
名称 | 说明 | 支持 |
---|---|---|
APT | 静态编译 | Java、Android |
AspectJ | 使用专门的编译器,在编译期插入代码 | Java、Android |
Javassist | 动态编译与动态生成字节码 | Java、Android |
CGLIB | 动态编译与动态生成字节码 | Java |
ByteBuddy | 动态编译与动态生成字节码 | Java、Android |
JDK动态代理 | 动态代理 | Java、Android |
ASM | 动态编译与动态生成字节码 | Java、Android |
ASMDEX | 动态编译与动态生成字节码 | Android |
DexMaker | 动态编译与动态生成字节码 | Android |
Xposed | 需要root权限,在运行时插入字节码 | Android |
APT(Annotation Processing Tool)
APT技术Java应用并不是特别广泛,但是在Android中是主要实现AOP的方式之一,通过注解实现在编译期间生成代码,执行效率高。
代表框架:ButterKnife、GRouter、AptPreferences
教程
AspectJ
使用AspectJ编译器(ajc),在编译时期,在关键的的地方插入部分代码,处理相关逻辑,比如可以用于打印方法执行的效率,权限检查等,Spring就使用了AspectJ。在Android上的应用主要是做性能监控、基于注解的数据埋点等。
代表框架:Hugo、Spring
教程
AspectJ AOP教程:实现Android基于注解无侵入埋点、性能监控
CGLIB
CGLIB 是一个强大的,高性能,高质量的Code生成类库 ,是基于ASM封装,大名鼎鼎的Hibernate就是基于CGLIB实现,CGLIB是一个应用非常广泛的AOP框架。
代表框架:Hibernate
+--- cglib:cglib:3.3.0
| \--- org.ow2.asm:asm:7.1
教程
添加依赖
dependencies {
implementation 'cglib:cglib:3.3.0'
}
代码
public class CglibTest implements MethodInterceptor {
public static void main(String[] args) {
User user = (User) new CglibTest().getProxy(User.class);
user.setId(1);
user.getId();
}
public Enhancer enhancer = new Enhancer();
private Object getProxy(Class clazz) {
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("执行开始:" + method);
Object result = proxy.invokeSuper(obj, args);
System.out.println("执行结束:" + method);
return result;
}
public static class User {
private int id;
public int getId() {return id;}
public void setId(int id) {this.id = id;}
}
}
结果
执行开始:public void com.thejoyrun.aptpreferences.CglibTest$User.setId(int)
执行结束:public void com.thejoyrun.aptpreferences.CglibTest$User.setId(int)
执行开始:public int com.thejoyrun.aptpreferences.CglibTest$User.getId()
执行结束:public int com.thejoyrun.aptpreferences.CglibTest$User.getId()
ByteBuddy
Byte Buddy 是一个运行时动态生成字节码的库,它是依赖Java虚拟机,由于Android的字节码和Java不同,也提供了Android的支持包,大名鼎鼎的测试框架 Mockito 就是基于ByteBuddy实现,Mockito3也正式支持在Android运行时环境使用,但是包大小为3.3MB,建议只在debug环境下使用。
api 'net.bytebuddy:byte-buddy:1.10.1'
api 'net.bytebuddy:byte-buddy-android:1.10.1'
代表框架:AopPreferences、Mockito
教程
添加依赖
dependencies {
implementation 'net.bytebuddy:byte-buddy:1.10.1'
// implementation 'net.bytebuddy:byte-buddy-android:1.10.1'
}
代码
public class ByteBuddyTest {
public static void main(String[] args) {
User user = new ByteBuddyTest().getProxy(User.class);
user.setId(1);
user.getId();
}
private <T> T getProxy(Class<T> clazz) {
Class<? extends T> loaded = new ByteBuddy().subclass(clazz)
.method(ElementMatchers.<MethodDescription>any())
.intercept(MethodDelegation.to(ByteBuddyTest.class))
.make().load(getClass().getClassLoader()).getLoaded();
// 如果是 Android
// .load(getClass().getClassLoader(), new AndroidClassLoadingStrategy.Wrapping(StorageAndroid.context.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();
try {
return loaded.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@RuntimeType
public static Object intercept(@This Object proxy, @Origin Method method, @SuperCall Callable<?> callable) throws Exception {
System.out.println("执行开始:" + method);
Object result = callable.call();
System.out.println("执行结束:" + method);
return result;
}
public static class User {
private int id;
public int getId() {return id;}
public void setId(int id) {this.id = id;}
}
}
结果
执行开始:public void com.thejoyrun.aptpreferences.ByteBuddyTest$User.setId(int)
执行结束:public void com.thejoyrun.aptpreferences.ByteBuddyTest$User.setId(int)
执行开始:public int com.thejoyrun.aptpreferences.ByteBuddyTest$User.getId()
执行结束:public int com.thejoyrun.aptpreferences.ByteBuddyTest$User.getId()
JDK动态代理
JDK动态代理是基于JDK自带实现AOP的方式,缺点就是只支持接口代理。
教程
由于是基于JDK开发,所以不需要引入第三方包
public class InvocationHandlerTest implements InvocationHandler {
public static void main(String[] args) {
User user = new InvocationHandlerTest().getProxy(User.class);
user.setId(1);
System.out.println("Id: " + user.getId());
}
private <T> T getProxy(Class<T> clazz) {
T result = (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{clazz}, new InvocationHandlerTest());
return result;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("执行开始:" + method);
if (method.getName().startsWith("set")) {
System.out.println("参数:" + args[0]);
} else {
Object result = new Random().nextInt();
System.out.println("执行结果:" + result);
return result;
}
return null;
}
public interface User {
int getId();
void setId(int id);
}
}
结果
执行开始:public abstract void com.thejoyrun.aptpreferences.InvocationHandlerTest$User.setId(int)
参数:1
执行开始:public abstract int com.thejoyrun.aptpreferences.InvocationHandlerTest$User.getId()
执行结果:165584246
Id: 165584246
ASM
ASM 是一个 Java 字节码操控框架,直接操作字节码指令,执行效率高,要是使用者掌握Java类字节码文件格式及指令,对使用者的要求比较高。所以不建议直接使用,建议使用更加高阶的Javassist框架。
ASMDEX
ASMDEX 是SAM开发,针对Android字节码的框架。
Javassist
Javassist是基于ASM,提供了更高级的API,执行效率比ASM差一些,但无需掌握字节码指令的知识,对使用者要求较低,多数的Android热修复框架都是基于Javassist实现。
代表框架:InstantRun、HotFix。
教程
DexMaker
DexMaker 用于执行针对Dalvik VM的编译时或运行时代码生成的Java语言API,类似CGLIB和ASM。
Xposed
Xposed框架是一套开源的、在Android高权限模式下运行的框架服务,可以在不修改APK文件的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作,通常是用于实现特殊的编程,比如逆向工程,自动化测试。