一、AOP概念
百度百科中对AOP的解释如下:
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。 利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP只是一种思想的统称,实现这种思想的方法有挺多。AOP通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率。
(1)AOP与OOP的关系
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。但是也有它的缺点,最明显的就是关注点聚焦时,面向对象无法简单的解决这个问题,一个关注点是面向所有而不是单一的类,不受类的边界的约束,因此OOP无法将关注点聚焦来解决,只能分散到各个类中。
AOP(面向切面编程)则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
AOP并不是与OOP对立的,而是为了弥补OOP的不足。OOP解决了竖向的问题,AOP则解决横向的问题。因为有了AOP我们的调试和监控就变得简单清晰。
简单的来讲,AOP是一种:可以在不改变原来代码的基础上,通过“动态注入”代码,来改变原来执行结果的技术。
(2)AOP主要应用场景
日志记录,性能统计,安全控制,事务处理,异常处理等等。
(3)主要目标
将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
上图是一个APP模块结构示例,按照照OOP的思想划分为“视图交互”,“业务逻辑”,“网络”等三个模块,而现在假设想要对所有模块的每个方法耗时(性能监控模块)进行统计。这个性能监控模块的功能就是需要横跨并嵌入众多模块里的,这就是典型的AOP的应用场景。
AOP的目标是把这些横跨并嵌入众多模块里的功能(如监控每个方法的性能) 集中起来,放到一个统一的地方来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。
对比:
功能 | OOP | AOP |
---|---|---|
增加日志 | 所有功能模块单独添加,容易出错 | 能够将同一个关注点聚焦在一处解决 |
修改日志 | 功能代码分散,不方便调试 | 能够实现一处修改,处处生效 |
例如:在不改变 main 方法的同时通过代码注入的方式达到目的
/**
* Before
*/
public class Test {
public static void main(String[] args) {
// do something
}
}
/**
* After
*/
public class Test {
public static void main(String[] args) {
long start = System.currentTimeMillis();
// do something
long end = System.currentTimeMillis() - start;
}
}
二、AOP代码注入时机
代码注入主要利用了Java的反射和注解机制,根据注解时机的不同,主要分为运行时、加载时和编译时。
运行时:你的代码对增强代码的需求很明确,比如,必须使用动态代理(这可以说并不是真正的代码注入)。
加载时:当目标类被Dalvik或者ART加载的时候修改才会被执行。这是对Java字节码文件或者Android的dex文件进行的注入操作。
编译时:在打包发布程序之前,通过向编译过程添加额外的步骤来修改被编译的类。
三、AOP的几种实现方式
- Java 中的动态代理,运行时动态创建 Proxy 类实例
- APT,注解处理器,编译时生成 .java 代码
- Javassist for Android:一个移植到Android平台的非常知名的操纵字节码的java库,对 class 字节码进行修改
- AspectJ:和Java语言无缝衔接的面向切面的编程的扩展工具(可用于Android)。
1、动态代理
动态代理本质上还是java中的“代理设计模式”,不需要依赖其他类库,主要涉及到两个类
InvocationHandler.java InvocationHandler is the interface implemented by the invocation handler of a proxy instance.
Proxy.java Proxy provides static methods for creating dynamic proxy classes and instances, and it is also the superclass of all
dynamic proxy classes created by those methods.
编码实现步骤
(1)创建目标接口UserService
public interface UserService
{
@Log
void addUser(String name);
void remove(String name);
}
(2)创建具体实现类
public class UserServiceImpl implements UserService
{
@Override
public void addUser(String name)
{
System.out.println("addUser " + name);
}
@Override
public void remove(String name) {
System.out.println("remove " + name);
}
}
(3)创建代理对象 implements InvocationHandler
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
if (method.isAnnotationPresent(Log.class)) {
System.out.println("before method do something...");
Object object = method.invoke(src, args);
System.out.println("after method do something...");
return object;
} else {
return method.invoke(src, args);
}
}
(4)客户端调用
static void testProxy() {
// 设置这个值,可以把生成的代理类,输出出来。
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
UserService service = new UserServiceImpl();
//生成被代理类的接口的子类
UserService proxy = (UserService) Proxy.newProxyInstance(LogProxy.class.getClassLoader(), service.getClass().getInterfaces(),
new LogProxy(service));
proxy.addUser("name1");
proxy.remove("name2");
}
代理对象的生成实际上是在运行时利用反射获取构造函数,通过加载构造函数在内存中生成的,其中生成的对象持有调用处理器InvocationHandler,最后会调用h.invoke()方法
想要实现特定方法写入日志,可以使用注解等方式。
2、编译时注解APT实现
代表项目:ButterKnife, Dagger2, DataBinding
(1)APT的介绍
全名Annotation Processing Tool,注解处理器。对源代码文件进行检测找出其中的Annotation,使用 Annotation 进行额外的处理。
APT在处理 Annotation 时可以根据源文件中的 Annotation 生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
总结一句话,就是在编译时候,根据注解生成对应需要的文件,这样在app运行的时候就不会导致性能损耗。
(2)APT的处理要素
注解处理器(AbstractProcess)+ 代码处理(javaPoet)+ 注册处理器(AutoService)
(3)使用APT来处理 Annotation 的流程
1.定义注解(如@ViewBind)
2.定义注解处理器 继承 AbstractProcessor
3.在处理器里面完成处理方式,生成java代码。
4.注册 处理器 @AutoService(Processor.class)
/**
* 要把处理器注册到javac中,需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下
* AutoService 会自动生成配置文件,注册处理器
*/
@AutoService(Processor.class)
public class ViewBindProcessor extends AbstractProcessor {
/**
* 处理器的初始化方法,可以获取相关的工具类
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 处理器的主方法,用于扫描处理注解,生成java文件
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
}
}
3、Javassist 实现
代表项目:Hotfix Instant Run
(1)原理
Javassist 可以直接操作字节码,从而实现代码注入,所以使用 Javassist 的时机就是在构建工具 Gradle 将源文件编译成 .class 文件之后,将 .class 打包成 dex 文件之前。
该方式需要借助Google提供的Transform API
先说一下Transform是什么
gradle从1.5开始,gradle 插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,
这个API的目标是简化自定义类操作,而不必处理Task,并且在操作上提供更大的灵活性。
官方文档:http://google.github.io/android-gradle-dsl/javadoc/
(2)开发步骤:
- 创建自定义 Gradle plugin module
新建Android library module 留下src/main和build.gradle,其他的文件删除 - Gradle Transform API
- 在main目录下创建 groovy 文件夹,然后在 groovy 目录下就可以创建我们的包名和 groovy 文件了,记得后缀要已 .groovy 结尾。在这个文件中引入创建的包名,然后写一个Class继承于Plugin< Project > 并重写apply方法
- 创建 MyPlugin.groovy 文件
- 利用 javassist 或者 ASM 修改原有的class文件或者新增class
public class MyPlugin implements Plugin<Project> {
void apply(Project project) {
System.out.println("------------------开始----------------------");
//AppExtension就是build.gradle中android{...}这一块
def android = project.extensions.getByType(AppExtension)
//注册一个Transform
def classTransform = new MyClassTransform(project)
android.registerTransform(classTransform)
System.out.println("------------------结束----------------------");
}
}
public class MyTransform extends Transform {
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
super.transform(context, inputs, referencedInputs, outputProvider, isIncremental)
// javassist 操作字节码
// 获取MainActivity.class
CtClass ctClass = pool.getCtClass("io.github.android9527.javassistdemo.MainActivity");
if (ctClass.isFrozen())
ctClass.defrost()
// 获取到OnCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
String insetBeforeStr = """ android.widget.Toast.makeText(this, "插入了Toast代码~", android.widget.Toast.LENGTH_SHORT).show();
"""
//在方法开头插入代码
ctMethod.insertBefore(insetBeforeStr)
ctClass.writeFile(path)
ctClass.detach() //释放
}
}
- 配置plugin 在main目录下创建resources文件夹,继续在resources下创建META-INF文件夹,继续在META-INF文件夹下创建gradle-plugins文件夹,最后在gradle-plugins文件夹下创建一个xxx.properties文件,
注意:这个xxx就是在app下的build.gradle中引入时的名字,例如:apply plugin: ‘xxx’。
在文件中写 implementation-class=io.github.android9527.MyPlugin。
需改build.gradle 内容,然后执行 uploadArchives 这个task 上传到 maven 库,就将我们的这个插件打包上传到了本地 maven 中,可以去本地的 maven 库中查看
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
// gradle sdk
compile gradleApi()
// groovy sdk
compile localGroovy()
// 可以引用其它库
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.tools.build:transform-api:1.5.0'
compile 'javassist:javassist:3.12.1.GA'
compile 'com.android.tools.build:gradle:3.1.2'
}
uploadArchives{
repositories{
mavenDeployer{
repository(url:uri('../repo'))
pom.groupId = 'com.android9527.plugin' // 组名
pom.artifactId = 'test' // 插件名
pom.version = '1.0.1-SNAPSHOT' // 版本号
}
}
}
group='com.android9527.plugin'
version='1.0-SNAPSHOT'
- 项目主 module 依赖该 plugin 运行项目,反编译之后查看字节码
四、总结:
动态代理
优点:
- Java API 提供的,兼容性好,无需依赖其他库,
- 动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
- 动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。
缺点:
- 只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
- 没有代码注入步骤,必须手动实例化并应用,
- 功能有限,只能在方法前后执行一些代码
APT
优点:
- 任何你不想做的繁杂的工作,它可以帮你减少样板代码
- 生成代码位置的可控性(可以在任意包位置生成代码),与原有代码的关联性更为紧密方便
缺点:
- 只有被注解标记了的类或方法等,才可以被处理或收集信息。
- APT可以自动生成代码,但在运行时却需要主动调用
Javassist :
- 功能强大,使用方便,
- 由于Javassist可以直接操作修改编译后的字节码,直接绕过了java编译器,所以可以做很多突破限制的事情,例如,跨dex引用,解决热修复中CLASS_ISPREVERIFIED的问题。
- 运行时生成,减少不必要的生成开销;通过将切面逻辑写入字节码,减少了生成子类的开销,不会产生过多子类。运行时加入切面逻辑,产生性能开销。
Aspectj:
-
AspectJ除了hook之外,AspectJ还可以为目标类添加变量,接口。另外,AspectJ也有抽象,继承等各种更高级的玩法。它能够在编译期间直接修改源代码生成class。
-
AspectJ语法比较多,但是掌握几个简单常用的,就能实现绝大多数切片,完全兼容Java(纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。)