一、简述
1.AOP概念
AOP为
Aspect Oriented Programming
的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。一句话概括:将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
何为切面?
- 一个和业务没有任何耦合相关的代码段,诸如:调用日志,发送邮件,甚至路由分发。一切能为代码所有且能和代码充分解耦的代码都可以作为一个业务代码的切面。
2.运用场景
项目开发过程中,可能会有这样的需求,需要我们在方法执行完成后,记录日志(后台开发中比较常见~),或是计算这个方法的执行时间,在不使用AOP的情况下,我们可以在方法最后调用另一个专门记录日志的方法,或是在方法体的首尾分别获取时间,然后通过计算时间差来计算整个方法执行所消耗的时间,这样也可以完成需求。那如果不只一个方法要这么玩怎么办?每个方法都写上一段相同的代码吗?后期处理逻辑变了要怎么办?其实这种问题我们完全可以用AOP来解决。
3.AOP的实现方式
首先,从切入的时机的角度看,可以分为源码阶段、class阶段、dex阶段、运行时切入。
对于前三项源码阶段、class阶段、dex切入,由于他们都发生在class加载到虚拟机前,我们统称为静态切入,而在运行阶段发生的改动,我们统称为动态切入。而具体切入方式可以分为静态代理和动态代理。
根据Android编译执行流程来看,不同的时期都有机会可以注入代码,如下图所示:
AOP仅仅只是个概念,实现它的方式(工具和库)有以下几种:
切入时机 | 技术框架 |
---|---|
静态切入 | APT,AspectJ、ASM、Javassit |
动态切入 | jdk动态代理,cglib、Javassit |
- APT:(Annotation Processing Tool )及注解处理器,是一种处理注解的工具,确切的说它是 javac 的一个工具,它用来在编译时扫描和处理注解。注解处理器以 Java 代码( 或者编译过的字节码)作为输入,生成 .java 文件作为输出。简单来说就是在编译期,通过注解生成 .java 文件。
- AspectJ:一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。AspectJ是一个代码生成框架,它扩展了Java语言,定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件
- Javassist for Android:用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。Javassist 是一个编辑字节码的框架,作用是修改编译后的 class 字节码。
- ASM:ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。
- ASMDEX:一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。
- DexMaker:Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
- jdk动态代理:jdk提供一个Proxy类可以直接给实现接口类的对象直接生成代理对象 API
下面主要介绍一下Android中常用的AspectJ和jdk动态代理实现方式
二、AspectJ
AspectJ 是静态代理的增强,所谓的静态代理就是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强。
AspectJ 是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现,也借鉴或采纳了 AspectJ 中很多设计。在 Java 领域,AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。
1.AspectJ导入
项目gradle添加
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.4.0'
classpath 'org.aspectj:aspectjtools:1.9.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
主model添加依赖
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.9.4'
}
添加gradle任务,直接粘贴到build.gradle文件的末尾即可,不要嵌套在别的指令中。
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.9",
"-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;
}
}
}
}
2.AspectJ基础知识
3.AspectJ实现AOP
3.1 创建自定义注解
用注解来标记切点,一般会使用自定义注解,方便我们拓展。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
String value();
int type();
}
- @Target(ElementType.METHOD):表示该注解只能注解在方法上。如果想类和方法都可以用,那可以这么写:@Target({ElementType.METHOD,ElementType.TYPE}),依此类推。
- @Retention(RetentionPolicy.RUNTIME):表示该注解在程序运行时是可见的(还有SOURCE、CLASS分别指定注解对于那个级别是可见的,一般都是用RUNTIME)。
其中的value和type是自己拓展的属性,方便存储一些额外的信息。
3.2 使用自定义注解标记切点
这个自定义注解只能注解在方法上(构造方法除外,构造方法也叫构造器,需要使用ElementType.CONSTRUCTOR),像平常使用其它注解一样使用它即可:
@TestAnnoTrace(value = "test", type = 1)
public void test(View view) {
System.out.println("Hello");
}
3.3 创建切面类
切点表达式的组成如下:
要织入一段代码到目标类方法的前前后后,必须要有一个切面类,下面就是切面类的代码:
@Aspect
public class TestAnnoAspect {
//切点表达式使用自定义的注解,一定是@+注解全路径
@Pointcut("execution(@com.hxl.androidaopdemo.TestAnnoTrace * *(..))")
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");
joinPoint.proceed();// 目标方法执行完毕
}
@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());
}
}
打印结果:
@Before
@Around
Hello
@After
@AfterReturning
少了一个@AfterThrowing通知。这个通知只有在切点抛出异常时才会执行
3.4 JoinPoint的作用
发现没有,上面所有的通知都会至少携带一个JointPoint参数,这个参数包含了切点的所有信息,下面就结合按钮的点击事件方法test()来解释joinPoint能获取到的方法信息有哪些:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test
Method method = signature.getMethod(); // 方法:public void com.hxl.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
//上面在编写自定义注解时就声明了两个属性,分别是value和type,
//通过Method对象得到切点上的注解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();
3.5 方法耗时计算的实现
因为@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");
}
三、jdk动态代理
动态代理也叫做JDK代理、接口代理。不需要实现目标对象的接口。生成代理对象,使用的是Java的API,动态的在内存中构件代理对象(这需要我们指定创建代理对象/目标对象的接口的类型)。
1.实现AOP
在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。
InvocationHandler:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
每一个代理实类例的invocation handler都要实现InvocationHandler这个接口。并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。
- proxy:指代生成的代理对象;
- method:指代的是我们所要调用真实对象的某个方法的Method对象;
- args:指代的是调用真实对象某个方法时接受的参数;
我们来看看Proxy这个类,这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但用的最多的就是 newProxyInstance 这个方法:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
- loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
- interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
- h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上。
通过上面的需要传入接口的参数可以看出,JDK动态代理需要借助接口来实现,如果我们要代理的对象功能没有抽成任何接口,那么我们就无法通过JDK动态代理的方式来实现。
好了,在介绍完这两个接口(类)以后,我们来通过一个实例来看看我们的动态代理模式是什么样的。首先我们定义了一个Subject类型的接口,为其声明了两个方法,这两个方法表示被代理类需要实现的功能:
public interface Subject {
public void sayGoodBye();
public void sayHello(String str);
}
接着,定义了一个类来实现这个接口,这个类就是我们的真实对象(被代理类),RealSubject类:
public class RealSubject implements Subject {
@Override
public void sayGoodBye() {
System.out.println("RealSubject sayGoodBye");
}
@Override
public void sayHello(String str) {
System.out.println("RealSubject sayHello " + str);
}
}
下一步,我们就要定义一个InvocationHandler了,相当于一个代理处理器。前面说个,每一个动态代理类实例的invocation handler 都必须要实现 InvocationHandler 这个接口:
public class SubjectInvocationHandler implements InvocationHandler {
//这个就是我们要代理的真实对象
private Object subject;
//构造方法,给我们要代理的真实对象赋初值
public SubjectInvocationHandler(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
//在代理真实对象前我们可以添加一些自己的操作
System.out.println("before Method invoke");
System.out.println("Method:" + method);
//当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
method.invoke(subject, args);
//在代理真实对象后我们也可以添加一些自己的操作
System.out.println("after Method invoke");
return null;
}
}
**SubjectInvocationHandler并不是真正的代理类,而是用于定义代理类需要扩展、增强那些方法功能的类。**在invoke函数中,对代理对象的所有方法的调用都被转发至该函数处理。在这里可以灵活的自定义各种你能想到的逻辑。
最后,来看看我们的Client类:
public static void main(String[] args) {
//被代理类
Subject realSubject = new RealSubject();
//我们要代理哪个类,就将该对象传进去,最后是通过该被代理对象来调用其方法的
SubjectInvocationHandler handler = new SubjectInvocationHandler(realSubject);
//生成代理类
Subject subject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(), handler);
//输出代理类对象
System.out.println("Proxy : "+ subject.getClass().getName());
System.out.println("Proxy super : "+ subject.getClass().getSuperclass().getName());
System.out.println("Proxy interfaces : "+ subject.getClass().getInterfaces()[0].getName());
//调用代理类sayGoodBye方法
subject.sayGoodBye();
System.out.println("--------");
//调用代理类sayHello方法
subject.sayHello("Test");
}
输出结果:
Proxy : com.sun.proxy.$Proxy0
Proxy super : java.lang.reflect.Proxy
Proxy interfaces : com.company.ha.Subject
before Method invoke
Method:public abstract void com.company.ha.Subject.sayGoodBye()
RealSubject sayGoodBye
after Method invoke
--------
before Method invoke
Method:public abstract void com.company.ha.Subject.sayHello(java.lang.String)
RealSubject sayHello Test
after Method invoke
如果我们定义的方法有返回值,那么可以通过invoke中把该方法的返回值进行返回,因为返回值的对象是Object,所以支持返回值为空(void)的返回。
@Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
//直接返回调用对象的返回值
return method.invoke(subject,args);
}
2.jdk动态代理应用(Retrofit)
Android中的网络注解框架retrofit内部实现其实就是应用了动态代理技术,通常我们定义网络接口是这样的:
public interface ApiStore {
// 员工登录
@FormUrlEncoded
@POST("/resource/d/member/login")
Observable<BaseResponse<LoginResult>> login(@FieldMap Map<String, String> params);
// 退出登录
@FormUrlEncoded
@POST("/resource/d/member/signOut")
Observable<BaseResponse<LogOutResult>> logout(@FieldMap Map<String, String> params);
//....
}
创建Retrofit:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://ww.xxx.com/")
.build();
ApiStore service = retrofit.create(ApiStore.class);
我们可以看retrofit.create内部具体的实现方式:
public <T> T create(final Class<T> service) {
//...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
@Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod<Object, Object> serviceMethod =
(ServiceMethod<Object, Object>) loadServiceMethod(method);
OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
看到没,retrofit就是通过Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },new InvocationHandler() )的方式创建的一个代理类,invoke中的代码就是当网络接口被调用的时候需要做的处理。在这个方法里,首先根据接口定义的方法,生成一个ServiceMethod对象,在ServiceMethod对象中会反射接口中定义的注解,解析出具体的网络请求方式,然后拿到封装好的ServiceMethod对象后,构造一个OkHttpCall对象,以便与进行真正的网络请求(OkHttp)。
四、动态代理和静态代理
区别:
- 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。
- 动态:在程序运行时运用反射机制动态创建而成。
静态代理和动态代理的区别和联系