手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一

AOP介绍

  • 说到AOP ,我想大家是再熟悉不过。AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  • AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。还有诸如JDK、CGLIB、Javassist、ASM等都是很优秀的框架 。

  • AspectJ 的AOP,可以在三种时期进行代理,或者注入:
    编译时织入 : 利用ajc编译器替代javac编译器,直接将源文件(java或者aspect文件)编译成class文件并将切面织入进代码。
    编译后织入 : 利用ajc编译器向javac编译期编译后的class文件或jar文件织入切面代码。
    加载时织入 : 不使用ajc编译器,利用aspectjweaver.jar工具,使用java agent代理在类加载期将切面织入进代码。
    (aspectjweaver.jar中,存在着@Before,@After等注解,并且有他们织入的实现(ASM动态代理))

  • SpringAop动态代理实现:
    spring aop采用了aspectj语法来定义切面,但是在实现切面逻辑的时候还是采用CGLIB来进行动态代理的方法。

手写注解方式的字节码AOP实现

  • 这里主要基于AspectJ ,从class字节码,到 java 指令集,一步一步去完成一个简单的静态编译的AOP框架。
  • 通过 java编译时注解(APT技术),解析class字节码信息,构建AOP代码指令集,并将指令集织入二进制class中,完成字节码方式的AOP实现。

从 android aspectj 讲起

  • 由于之前写过一章关于android aspectj
    Aop的文章,那这篇就以这篇文章为例子,开始我们的手写字节码AOP框架之旅。
    首先还是让我们看看aspectj android 是如何工作的。

    这是一个关于aspectj 环绕通知,作用很简单就是在方法执行时,织入一段try catch代码块,防止未捕获的异常导致程序奔溃。

/**
 * Created by wuzhi on 14/03/2017.  异常处理
 */
@Aspect
public class TryCatchAOP {
   
    @Around("execution(@cn.jesse.apilib.annotation.aop.TryCatch * *(..))")
    public void tryCatchMethodTriggered(ProceedingJoinPoint joinPoint) throws Throwable{
        try{
            joinPoint.proceed();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
  • 我们来看看是如何实现的,反编译apk之后。

手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第1张图片

  • 我们可以看到原来的Activity class代码已经被织入了AOP代码。详细执行过程,可以看之前的文章。这篇就不在详述。
  • 从反编译之后的代码我们可以得知。通过java编译时注解,解析原class字节码信息,动态重构代码指令集,并织入其原来class字节码中。
注解静态注解技术
class字节码解析
构造 AOP java指令集
织入aop 指令集

于是我们以这四个知识点为切入点,开启我们的手写字节码AOP框架之旅。

手写字节码AOP

静态注解APT技术

关于静态注解,这里就不详细介绍其详细功能,列举了一些文章,写的也比较详细,这里就不过多介绍。这里只着重介绍我们围绕字节码AOP技术需要用到的一些静态编译注解知识点。
java自定义注解
java–自定义注解(注解在编译时生效)
关于java编译时注解你需要知道的二三事。解除你的顾虑!

注解 @interface

我们先来个注解,用于修饰方法,在代码方法中加注这个注解后,根据method 的值,去织入前置通知调用的方法。(原本RetentionPolicy 应该是CLASS编译时注解,但是关于字节码解析注解部分,只写RUNTIME部分,所以这里RetentionPolicy 为RUNTIME,后期在加上CLASS编译时注解的解析)

package cn.app.apt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) // 注解只在源码中保留
@Target(ElementType.METHOD) // 用于修饰方法
public @interface Before {

    String method() default "" ;
    
}

注解处理器 AbstractProcessor

于是我们紧接着来一个注解处理器

package cn.app.apt;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileManager.Location;
import javax.tools.JavaFileObject;

 
/**
 * Created by away on 2017/6/12.
 */
@SupportedAnnotationTypes("cn.app.apt.Before")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ClazzAptProcessor extends AbstractProcessor {

	public Filer mFiler; // 文件相关的辅助类
	public Elements mElements; // 元素相关的辅助类
	public Messager mMessager; // 日志相关的辅助类

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		// TODO Auto-generated method stub
		super.init(processingEnv);

		mFiler = processingEnv.getFiler();
		mElements = processingEnv.getElementUtils();
		mMessager = processingEnv.getMessager();

	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
	  processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello World!");
		 
	  try {

		 for (Element element : roundEnv.getElementsAnnotatedWith(Before.class)) {
			
			Before before = element.getAnnotation(Before.class);

			// 所属类
			TypeElement classElementAnnotationIn = (TypeElement) element.getEnclosingElement();
			// 类名
		    String invokeObjectName = classElementAnnotationIn.getQualifiedName().toString();

			// 所属方法
			ExecutableElement invokeMethodElement = (ExecutableElement) element;

			// 方法名
		    String NativeMethodName = invokeMethodElement.getSimpleName().toString();

	    }
	  } catch (Exception e) {
					e.printStackTrace();
  	  }
      return false;
	}
}

从Element 中我们可以获得注解所属的方法,以及方法信息,用于class字节码解析。这里只列举了大概代码,完整代码在工程myAspectj 中的cn.app.apt.ClazzAptProcessor.java 中。

注册注解处理器

手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第2张图片

导出jar

手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第3张图片

测试工程开启APT

手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第4张图片
于是我们有一下测试代码

package cn.app.wuzhi;

import cn.app.apt.Before;

public class MainHello {

	public static void main(String[] args) {
		MainHello mainHello = new MainHello();
		mainHello.method3();
	}

	public void Aoptest2() {
		System.out.println("Aoptest  Before");
	}

	@Before(method = "Aoptest2")  //执行前置通知 Aoptest2 函数
	public void method3() {
		System.out.println("我是 method3 函数 ");
	}
}

至此编译时注解都已经可以了,编译器检测到注解时,会调用ClazzAptProcessor#proces函数,我们在这里写上解析class字节码,和其织入AOP代码指令集。

于是下面我们开始讲这篇的重点,class字节码结构以及解析。

class字节码结构解析

APT解决了AOP字节码的触发问题,当标注了注解,就执行AOP字节码的修改。
上两篇博客简单的介绍了下字节码的结构。
手写 Class 字节码解析技术(一)
手写 Class 字节码解析技术(二)字段的操作
在重温一下,结构如下,
手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第5张图片
上图是class文件结果。
下图是class的十六进制。
对应关系不难看出。
magic 魔数是第一个结构,长度为U4,即8个字母长度。
手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第6张图片
看着class的结构毫无头绪,其实很好解决,通过IO流读取出来,分段截取一下.
即0-8位是魔数,8-12是jdk次版本号。

于是我们有以下代码,class读取为byte 数组,在转为十六进制,于是得到上图的字节码。
手写 Class 字节码解析技术(三)手写 Aspectj实现aop 一_第7张图片

你可能感兴趣的:(android技术)