名称含义
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,是Java后端Spring框架中的一个重要内容,是Android插桩技术的灵魂所在,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
主要功能
日志记录,性能统计,安全控制,事务处理,异常处理,权限控制等等。
主要意图
将日志记录,性能统计,安全控制,事务处理,异常处理,权限控制等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
常用术语
-
连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,在AOP中表示为在哪里干;
-
切入点(Pointcut): 选择一组相关连接点的模式,即可以认为连接点的集合,在AOP中表示为在哪里干的集合;
-
通知(Advice):在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),;在AOP中表示为干什么;
-
方面/切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;在AOP中表示为在哪干和干什么集合;
-
引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法, 在AOP中表示为干什么(引入什么);
-
目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象,在AOP中表示为对谁干;
-
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。在AOP中表示为怎么实现的;
-
AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在AOP中表示为怎么实现的一种典型方式;
AOP/OOP
区分
AOP、OOP在字面上虽然非常类似,但却是面向不同领域的两种设计思想。OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。
上面的陈述可能过于理论化,举个简单的例子,对于“用户”这样一个业务实体进行封装,自然是OOP的任务,我们可以为其建立一个User类,并将“用户”相关的属性和行为封装其中。而用AOP设计思想对“用户”进行封装将无从谈起。
同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOP对一个动作进行封装,则有点不伦不类。
换而言之,OOP面向名词领域,AOP面向动词领域。
关系
很多人在初次接触 AOP 的时候可能会说,AOP 能做到的,一个定义良好的 OOP 的接口也一样能够做到,我想这个观点是值得商榷的。AOP和定义良好的 OOP 的接口可以说都是用来解决并且实现需求中的横切问题的方法。但是对于 OOP 中的接口来说,它仍然需要我们在相应的模块中去调用该接口中相关的方法,这是 OOP 所无法避免的,并且一旦接口不得不进行修改的时候,所有事情会变得一团糟;AOP 则不会这样,你只需要修改相应的 Aspect,再重新编织(weave)即可。 当然,AOP 也绝对不会代替 OOP。核心的需求仍然会由 OOP 来加以实现,而 AOP 将会和 OOP 整合起来,以此之长,补彼之短。
实现方式
AspectJ
AspectJ是一种严格意义上的AOP技术,因为它提供了完整的面向切面编程的注解,这样让使用者可以在不关心字节码原理的情况下完成代码的织入,因为编写的切面代码就是要织入的实际代码。
AspectJ实现代码织入有两种方式,一是自行编写.ajc文件,二是使用AspectJ提供的@Aspect、@Pointcut等注解,二者最终都是通过ajc编译器完成代码的织入。
官网地址:www.eclipse.org/aspectj/ 。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
基本语法
Join Points
Join Point 表示连接点,即 AOP 可织入代码的点,下表列出了 AspectJ 的所有连接点:
| Join Point | 说明 |
|---|---|
| Method call | 方法被调用,常用 |
| Method execution | 方法执行,常用 |
| Constructor call | 构造函数被调用 |
| Constructor execution | 构造函数执行 |
| Field get | 读取属性 |
| Field set | 写入属性 |
| Pre-initialization | 与构造函数有关,很少用到 |
| Initialization | 与构造函数有关,很少用到 |
| Static initialization | static 块初始化 |
| Handler | 异常处理 |
| Advice execution | 所有 Advice 执行 |
Pointcuts
Pointcuts 是具体的切入点,可以确定具体织入代码的地方,基本的 Pointcuts 是和 Join Point 相对应的。
| Join Point | Pointcuts syntax |
|---|---|
| Method call | call(MethodPattern) |
| Method execution | execution(MethodPattern) |
| Constructor call | call(ConstructorPattern) |
| Constructor execution | execution(ConstructorPattern) |
| Field get | get(FieldPattern) |
| Field set | set(FieldPattern) |
| Pre-initialization | initialization(ConstructorPattern) |
| Initialization | preinitialization(ConstructorPattern) |
| Static initialization | staticinitialization(TypePattern) |
| Handler | handler(TypePattern) |
| Advice execution | adviceexcution() |
除了上面与 Join Point 对应的选择外,Pointcuts 还有其他选择方法:
| Pointcuts synatx | 说明 |
|---|---|
| within(TypePattern) | 符合 TypePattern 的代码中的 Join Point |
| withincode(MethodPattern) | 在某些方法中的 Join Point |
| withincode(ConstructorPattern) | 在某些构造函数中的 Join Point |
| cflow(Pointcut) | Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,包括 P 本身 |
| cflowbelow(Pointcut) | Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,不包括 P 本身 |
| this(Type or Id) | Join Point 所属的 this 对象是否 instanceOf Type 或者 Id 的类型 |
| target(Type or Id) | Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type 或者 Id 的类型 |
| args(Type or Id, ...) | 方法或构造函数参数的类型 |
| if(BooleanExpression) | 满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象 |
- Pointcut 表达式还可以 !、&&、|| 来组合,
- !Pointcut 选取不符合 Pointcut 的 Join Point
- Pointcut0 && Pointcut1 选取符合 Pointcut0 和 Pointcut1 的 Join Point
- Pointcut0 || Pointcut1 选取符合 Pointcut0 或 Pointcut1 的 Join Point
// 签名:消息发送切面 @Pointcut("execution(* com.lbz.test.MessageSender.*(..))") private void logSender(){} // 签名:消息接收切面 @Pointcut("execution(* com.lbz.test.MessageReceiver.*(..))") private void logReceiver(){} // 只有满足发送 或者 接收 这个切面都会切进去 @Pointcut("logSender() || logReceiver()") private void logMessage(){}
通配符以及表达式规则
上面 Pointcuts 的语法中涉及到一些 Pattern,下面是这些 Pattern 的规则,[]里的内容是可选的:
| Pattern | 规则 |
|---|---|
| MethodPattern | [@注解] [访问权限] 返回值类型 [类名.]方法名(参数) [throws 异常类型] |
| ConstructorPattern | [@注解] [访问权限] [类名.]new(参数) [throws 异常类型] |
| FieldPattern | [@注解] [访问权限] 变量类型 [类名.]变量名 |
| TypePattern | 其他 Pattern 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数 |
| 通配符 | 说明 |
|---|---|
| * | 匹配任何数量字符 |
| .. | 匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数 |
| + | 匹配指定类型的子类型;仅能作为后缀放在类型模式后边 |
Advice
Advice 是在切入点上织入的代码,在 AspectJ 中有五种类型:Before、After、AfterReturning、AfterThrowing、Around。
| Advice | 说明 |
|---|---|
| @Before | 在执行 Join Point 之前 |
| @After | 在执行 Join Point 之后,包括正常的 return 和 throw 异常 |
| @AfterReturning | Join Point 为方法调用且正常 return 时,不指定返回类型时匹配所有类型 |
| @AfterThrowing | Join Point 为方法调用且抛出异常时,不指定异常类型时匹配所有类型 |
| @Around | 替代 Join Point 的代码,如果要执行原来代码的话,要使用 ProceedingJoinPoint.proceed() |
注意: After 和 Before 没有返回值,但是 Around 的目标是替代原 Join Point 的,所以它一般会有返回值,而且返回值的类型需要匹配被选中的 Join Point 的代码。而且不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。
Advice 注解修改的方法必须为 public,Before、After、AfterReturning、AfterThrowing 四种类型修饰的方法返回值也必须为 void,Advice 需要使用 JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart 时,要在方法中声明为额外的参数,@Around 方法可以使用 ProceedingJoinPoint,用以调用 proceed() 方法。
更多
更多AspectJ的语法请参考www.eclipse.org/aspectj/doc…
举个栗子
我们常用 execution切入点指示符。执行表达式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- modifiers-pattern 修饰符匹配
- ret-type-pattern 返回值匹配,可以为*表示任何返回值,全路径的类名等
- declaring-type-pattern? 类路径匹配
- name-pattern 可以指定方法名 或者 代表所有, set 代表以set开头的所有方法
- param-pattern 参数匹配,可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(…)表示零个或多个任意参数
- throws-pattern? 异常类型匹配
- 其中后面跟着“?”的是可选项
// 所有方法的执行:
execution(* *(..))
// 任意公共方法的执行:
execution(public * *(..))
// 任何一个名字以“set”开始的方法的执行:
execution(* set*(..))
// UserService接口定义的任意方法的执行:
execution(* com.lbz.service.UserService.*(..))
// 在service包中定义的任意方法的执行:
execution(* com.lbz.service.*.*(..))
// 在service包或其子包中定义的任意方法的执行:
execution(* com.lbz.service..*.*(..))
// 在service包中的任意连接点:
within(com.lbz.service.*)
// 在service包或其子包中的任意连接点:
within(com.lbz.service..*)
// 实现了UserService接口的代理对象的任意连接点:
this(com.lbz.service.UserService)
// 实现UserService接口的目标对象的任意连接点 :
target(com.lbz.service.UserService)
// 任何一个只接受一个参数,并且运行时所传入的参数是Serializable 接口的连接点
// 不同于 execution(* *(java.io.Serializable)): args版本只有在动态运行时候传入参数是Serializable时才匹配,而execution版本在方法签名中声明只有一个 Serializable类型的参数时候匹配。
args(java.io.Serializable)
// 目标对象中有一个 @Transactional 注解的任意连接点
@target(org.springframework.transaction.annotation.Transactional)
// 任何一个目标对象声明的类型有一个 @Transactional 注解的连接点:
@within(org.springframework.transaction.annotation.Transactional)
// 任何一个执行的方法有一个 @Transactional 注解的连接点
@annotation(org.springframework.transaction.annotation.Transactional)
// 任何一个只接受一个参数,并且运行时所传入的参数类型具有@Classified 注解的连接点
@args(com.lbz.test.Classified)
小试牛刀
现在我们尝试在一个下面目标函数*testAdvice()*前后,通过AspectJ注解的方式实现代码增强,添加两个Log
//目标函数
private fun testAdvice(): Button {
Log.d(AdviceAspect.TAG, "testAdvice method run")
return findViewById(R.id.button0) as Button
}
新建一个切面类AdviceAspect
//通过注解@Aspect 申明这是一个切面类,才会生效参与编译
@Aspect
class AdviceAspect {
companion object {
const val TAG = "AdviceAspect"
const val EXECUTION = "execution(* *..*.testAdvice**())"
const val CALL = "call(* *..*.testAdvice**())"
}
//在目标函数调用前
@Before(EXECUTION)
fun before(joinPoint: JoinPoint) {
Log.d(TAG, joinPoint.signature.toString() + " before run")
}
//在目标函数调用后
@After(EXECUTION)
fun after(joinPoint: JoinPoint) {
Log.d(TAG, joinPoint.signature.toString() + " after run")
}
}
//Log
D Button com.example.aspectjandroid.MainActivity.testAdvice$app_debug() before run
D testAdvice method run
D Button com.example.aspectjandroid.MainActivity.testAdvice$app_debug() after run
工作原理
AspectJ的核心是ajc编译器 (aspectjtools)和织入器 (aspectjweaver)。
ajc编译器:
基于Java编译器之上的,它是用来编译.aj文件,aspectj在Java编译器的基础上增加了一些它自己的关键字和方法。因此,ajc也可以编译Java代码。
weaver织入器:
为了在java编译器上使用AspectJ而不依赖于Ajc编译器,aspectJ 5出现了 @AspectJ,使用注释的方式编写AspectJ代码,可以在任何Java编译器上使用。 由于Android Studio默认是没有ajc编译器的,所以在Android中使用@AspectJ来编写。
通过
Gradle Transform API,在class文件生成后至dex文件生成前,遍历并匹配所有符合AspectJ文件中声明的切点,然后将Aspect的代码织入到目标.class。织入代码后的新.class会加入多个JoinPoint,这个JoinPoint会建立目标.class与Aspect代码的连接,比如获得执行的对象、方法、参数等。
如下,目标class,经过反编译class。发现目标函数*testAdvice()*在经过AspectJ插件被动态修改了。
private final Button testAdvice() {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_6, this, this);
try {
AdviceAspect.aspectOf().before(makeJP);
Log.d("AdviceAspect", "testAdvice method run");
View findViewById = findViewById(2131230818);
Intrinsics.checkNotNull(findViewById, "null cannot be cast to non-null type android.widget.Button");
return (Button) findViewById;
} finally {
AdviceAspect.aspectOf().after(makeJP);
}
}
Android实现
方式一:以Gradle的Plugin形式使用
-
首先在项目根目录的build.gradle中添加:
classpath 'org.aspectj:aspectjtools:1.9.7' -
app的build.gradle中添加:
dependencies { ... implementation 'org.aspectj:aspectjrt:1.9.7' } 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 -> // 注意这里控制debug下生效,可以自行控制是否生效 if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return } JavaCompile javaCompile = variant.javaCompileProvider.get() javaCompile.doLast { String[] args = ["-showWeaveInfo", "-1.8", "-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 } } } }
方式二:用已有插件
这种方式接入简单。但是此插件截止目前已经一年多没有维护了,考虑到AGP的兼容性,现在已经用不了了。
可以考虑其他开发者基于上者fork继续维护的插件:
常用示例
对于实战中,对于快速找到切点,可以使用AspectJ提供的通配符,或者搭配注解使用。
一, 防止快速点击
1.功能
防止段时间内对view控件的多次点击,这里可以设置时间间隔,即多长时间内不能多次点击。
2.注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class ExceptSingleClick
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class SingleClick(val interval: Long = 3000L)
3. aspectj
@Aspect
class SingleClickAspect {
companion object {
const val TAG = "SingleClick"
//正常的点击事件(非侵入式)
private const val ON_CLICK_POINTCUTS = "execution(* android.view.View.OnClickListener.onClick(..))"
//写在xml中的点击事件(非侵入式)
private const val ON_CLICK_IN_XML_POINTCUTS = "execution(* androidx.appcompat.app.AppCompatViewInflater.DeclaredOnClickListener.onClick(..))"
//用注解标注的防抖时间(侵入式)
private const val ON_SINGLE_CLICK = "execution(@com.example.aspectjandroid.example1.SingleClick * *(..))"
private const val SINGLE_CLICK_KEY = R.string.app_name
}
@Pointcut(ON_SINGLE_CLICK)
fun onClick() {
}
@Around("onClick()")
@Throws(Throwable::class)
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint) {
var view: View? = null
for (arg in joinPoint.args) {
if (arg is View) {
view = arg
break
}
}
if (view == null) {
return
}
val signature: MethodSignature = joinPoint.signature as MethodSignature
val method: Method = signature.method
if (!method.isAnnotationPresent(SingleClick::class.java)) return
val singleClick = method.getAnnotation(SingleClick::class.java) ?: return
if (!FastClickCheckUtil.isFastClick(view, singleClick.interval)) {
joinPoint.proceed()
}
}
@Pointcut(ON_CLICK_POINTCUTS)
fun onClickPointcuts() {
}
@Pointcut(ON_CLICK_IN_XML_POINTCUTS)
fun onClickInXmlPointcuts() {
}
@Around("onClickPointcuts() || onClickInXmlPointcuts()")
fun throttleClick(joinPoint: ProceedingJoinPoint) {
val signature: Signature = joinPoint.signature
if (signature is MethodSignature) {
val methodSignature: MethodSignature = signature
val method = methodSignature.method
// 如果有 ExceptSingleClick 注解,就不需要做点击防抖处理
val isExcept = method.isAnnotationPresent(ExceptSingleClick::class.java)
if (isExcept) {
joinPoint.proceed()
return
}
}
val args = joinPoint.args
val view = getViewFromArgs(args)
if (view == null) {
joinPoint.proceed()
return
}
val lastClickTime: Long? = view.getTag(SINGLE_CLICK_KEY) as Long?
if (lastClickTime == null) {
view.setTag(SINGLE_CLICK_KEY, SystemClock.elapsedRealtime())
joinPoint.proceed()
return
}
if (canClick(lastClickTime)) {
view.setTag(SINGLE_CLICK_KEY, SystemClock.elapsedRealtime())
joinPoint.proceed()
return
}
}
private fun getViewFromArgs(args: Array<Any>?): View? {
if (args != null && args.isNotEmpty()) {
val arg = args[0]
if (arg is View) {
return arg
}
}
return null
}
private fun canClick(lastClickTime: Long): Boolean {
return (SystemClock.elapsedRealtime() - lastClickTime >= 1000L)
}
}
二, 统计方法耗时
1. 功能
计算方法执行的时间并打印出来
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class BehaviorTrace
3. aspectj
@Aspect
class BehaviorTraceAspect {
companion object {
const val TAG = "BehaviorTrace"
private const val POINTCUTS = "execution(@com.example.aspectjandroid.example2.BehaviorTrace * *(..))"
}
@Pointcut(POINTCUTS)
fun methodAnnotatedWithBehaviorTrace() {
}
@Around("methodAnnotatedWithBehaviorTrace()")
@Throws(Throwable::class)
fun weaveJoinPoint(joinPoint: ProceedingJoinPoint) {
try {
val signature: MethodSignature = joinPoint.signature as MethodSignature
val method: Method = signature.method
if (!method.isAnnotationPresent(BehaviorTrace::class.java)) return
//开始时间
val startTime = System.currentTimeMillis()
//方法执行
joinPoint.proceed()
//结束时间
val diffTime = System.currentTimeMillis() - startTime
val clzName: String = signature.declaringType.simpleName
val methodName = signature.name
Log.d(TAG, String.format("类:%s中方法:%s执行耗时:%d ms", clzName, methodName, diffTime))
} catch (e: Exception) {
Log.e(TAG, "weaveJoinPoint: ", e)
}
}
}
三, 网络请求状态判断
1. 功能
在执行某个方法前判断是否连接网络。
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class CheckNet
3. aspectj
@Aspect
class CheckNetAspect {
companion object {
const val TAG = "CheckNet"
private const val POINTCUT = "execution(@com.example.aspectjandroid.example3.CheckNet * *(..))"
}
@Pointcut(POINTCUT)
fun onCheckNetMethod() {
}
@Around("onCheckNetMethod()")
@Throws(Throwable::class)
fun doCheckNetMethod(joinPoint: ProceedingJoinPoint) {
if (NetUtils.networkIsAvailable(MyApp.appContext)) {
joinPoint.proceed()
} else {
Toast.makeText(MyApp.appContext, "没有网络", Toast.LENGTH_SHORT).show()
}
}
}
四, 请求权限
1. 功能
在执行某个方法前判断是否连接网络。
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RequirePermission(val value: Array<String>)
3. aspectj
@Aspect
class RequirePermissionAspect {
companion object {
const val TAG = "RequirePermission"
private const val POINTCUT = "execution(@com.example.aspectjandroid.example4.RequirePermission * *(..))"
}
@Pointcut(POINTCUT)
fun onRequirePermissionMethod() {
}
@Around("onRequirePermissionMethod() && @annotation(requirePermission)")
@Throws(Throwable::class)
fun doRequirePermissionMethod(joinPoint: ProceedingJoinPoint, requirePermission: RequirePermission) {
var activity: FragmentActivity? = null
val any = joinPoint.`this`
if (any is FragmentActivity) {
activity = any
} else if (any is Fragment) {
activity = any.activity
}
if (activity == null) {
joinPoint.proceed()
} else {
PermissionX.init(activity).permissions(listOf(*requirePermission.value)).onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(deniedList, "Core fundamental are based on these permissions", "OK", "Cancel")
}.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, "You need to allow necessary permissions in Settings manually", "OK", "Cancel")
}.request { allGranted, _, _ ->
if (allGranted) {
try {
joinPoint.proceed()
} catch (e: Throwable) {
e.printStackTrace()
}
} else {
Toast.makeText(MyApp.appContext, "授权失败!", Toast.LENGTH_SHORT).show();
}
}
}
}
}
五, 日志打点
1. 功能
添加埋点信息,用于数据统计功能。
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class EventTracking(val key: String, val value: Array<String>)
3. aspectj
@Aspect
class StatisticReportAspect {
companion object {
const val TAG = "ActivityCycle"
private const val POINTCUT = "execution(* android.app.Activity+.onResume(..))"
private const val EVENT_POINTCUT = "execution(@com.example.aspectjandroid.example5.EventTracking * *(..))"
}
@Pointcut(EVENT_POINTCUT)
fun onEventTrackingMethod() {
}
@Around(POINTCUT)
@Throws(Throwable::class)
fun openActivityMethodAround(joinPoint: ProceedingJoinPoint) {
val target = joinPoint.target
val className = target.javaClass.name
StatisticReport.reportAction(className)
joinPoint.proceed()
}
@Around("onEventTrackingMethod() && @annotation(eventTracking)")
@Throws(Throwable::class)
fun doEventTrackingMethod(joinPoint: ProceedingJoinPoint, eventTracking: EventTracking) {
val key = eventTracking.key
StatisticReport.reportAction(key, eventTracking.value)
joinPoint.proceed()
}
}
六, 异步执行
1. 功能
保证方法是通过异步方式执行,这里使用flow实现异步。
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Asynchronous
3. aspectj
@Aspect
class AsynchronousAspect {
companion object {
const val TAG = "Asynchronous"
private const val POINTCUT = "execution(@com.example.aspectjandroid.example6.Asynchronous * *(..))"
}
@Pointcut(POINTCUT)
fun onAsynchronousMethod() {
}
@Around("onAsynchronousMethod()")
@Throws(Throwable::class)
fun doAsynchronousMethod(joinPoint: ProceedingJoinPoint) {
Log.d(TAG, "doAsynchronousMethod target=" + joinPoint.target + " this=" + joinPoint.`this`)
var activity: FragmentActivity? = null
val any = joinPoint.`this`
if (any is FragmentActivity) {
activity = any
} else if (any is Fragment) {
activity = any.activity
}
if (activity == null) {
joinPoint.proceed()
} else {
activity.lifecycleScope.launch(Dispatchers.Main) {
flow {
emit(1)
}.flowOn(Dispatchers.Main)
.onEach {
joinPoint.proceed()
}.flowOn(Dispatchers.IO).collect()
}
}
}
}
六, 异常捕获
1. 功能
捕获此方法所可能产生的异常情况,保证执行此方法不会导致app崩溃。
2. 注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class CatchException
3. aspectj
@Aspect
class CatchExceptionAspect {
companion object {
const val TAG = "CatchException"
private const val POINTCUT = "execution(@com.example.aspectjandroid.example7.CatchException * *(..))"
}
@Pointcut(POINTCUT)
fun onCatchExceptionMethod() {
}
@Around("onCatchExceptionMethod()")
@Throws(Throwable::class)
fun doCatchExceptionMethod(joinPoint: ProceedingJoinPoint) {
try {
joinPoint.proceed()
} catch (e: Exception) {
Log.e(TAG, getException(e))
}
}
private fun getException(ex: Throwable): String {
val errors = StringWriter()
ex.printStackTrace(PrintWriter(errors))
return errors.toString()
}
}
七,Hugo项目
现存问题
-
如果我们代码中有使用lambda,例如点击事件会变为:
tv.setOnClickListener(v -> Log.e("test", "点击事件执行"));这样之前的点击切入点就无效了,这里涉及到D8这个脱糖工具和invokedynamic字节码指令相关知识,这里不展开来说,简单说使用lambda会生成
lambda$开头的中间方法,所以只能如下处理:@Around("execution(* *..lambda$*(android.view.View))")这种暂时处理起来比较麻烦,且可以看出容错率也比较低,很容易切入其他无关方法,所以建议AOP不要使用lambda。
-
重复织入,不织入。
假如我们想对Activity生命周期织入埋点统计,我们可能写出这样的切点代码。
@Pointcut("execution(* android.app.Activity+.on*(..))") public void callMethod() {}但是,这样做会导致两个问题:
- 如果我们的Activity没有复写onPause方法,那么将不会织入。这是因为android.app.Activity位于android设备内,不参与打包的过程。
- 如果我们的Activity继承了BaseActivity,BaseActivity又继承了android.app.Activity,那么这两个Activity都会被织入,这就造成了重复统计的问题。
APT
定义和用处
APT全称为:"Anotation Processor Tools",意为注解处理器。顾名思义,其用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。
实现步骤
-
Android Studio创建一个java library
-
自定义一个注解(Annotation),用于存储元数据
@Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD}) public @interface Print { } -
创建一个自定义Annotation Processor继承于AbstractProcessor
@AutoService(Print.class) @SupportedAnnotationTypes("com.lbz.apt_annotation.Print") public class MyProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { return false; } @Override public synchronized void init(ProcessingEnvironment processingEnv) { System.out.println("Hello APT"); processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello APT"); super.init(processingEnv); } @Override public Set<String> getSupportedAnnotationTypes() { HashSet<String> strings = new HashSet<>(); strings.add(Print.class.getCanonicalName()); return super.getSupportedAnnotationTypes(); } @Override public SourceVersion getSupportedSourceVersion() { return processingEnv.getSourceVersion(); }@AutoService(MyProcessor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。 AutoService这里主要是用来生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册,具体手动注册方法如下: 创建一个META-INF/services/javax.annotation.processing.Processor文件, 其内容是一系列的自定义注解处理器完整有效类名集合:
com.lbz.apt_processor.MyProcessor -
编译process方法,生成java代码。通常使用javapoet生成。
实现原理
APT是javac提供的一种工具,它在编译时扫描、解析、处理注解。它会对源代码文件进行检测,找出用户自定义的注解,根据注解、注解处理器和相应的apt工具自动生成代码。这段代码是根据用户编写的注解处理逻辑去生成的。最终将生成的新的源文件与原来的源文件共同编译(注意:APT并不能对源文件进行修改操作,只能生成新的文件,例如往原来的类中添加方法)。具体流程图如下图所示:
它用来在编译时扫描和处理注解,扫描过程可使用 auto-service 来简化寻找注解的配置,在处理过程中可生成java文件(创建java文件通常依赖 javapoet 这个库)。常用于生成一些模板代码或运行时依赖的类文件,比如常见的ButterKnife、Dagger、ARouter,它的优点是简单方便。
**在编译时,java编译器(javac)会去META-INF中查找实现了的AbstractProcessor的子类,并且调用该类的process函数,最终生成
.java文件。**其实就像activity需要注册一样,就是要到META-INF注册 ,javac才知道要给你调用哪个类来处理注解。
现存问题
APT并不能对源文件进行修改操作,只能生成新的文件
ASM
定义
ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码,添加成员变量,修改父类,添加接口等等。
ASM 用于许多项目,包括:
- OpenJDK,生成 lambda 调用站点,也在Nashorn 编译器中,
- Groovy 编译器和Kotlin编译 器,
- Cobertura 和Jacoco, 检测 类以测量代码覆盖率,
- Byte Buddy,动态 生成类,本身用于其他项目,如Mockito( 生成模拟类),
- Gradle,在运行时 生成一些类。
小试牛刀
ASM是操作class文件的,我们首先生成一个简单的Java类AsmTest.java,并通过javac编译生成.class文件 AsmTest.class
// AsmTest.java
class AsmTest {
public void methodA() {
System.out.println("methodA");
}
}
package com.example.asmtest;
//AsmTest.class
class AsmTest {
AsmTest() {
}
public void methodA() {
System.out.println("methodA");
}
}
我们在methodA方法前后计时并计算它的运行时间,首先,我们在Java类上模拟加上插桩代码:
class AsmTest {
public void methodA() {
long start = System.currentTimeMillis();
System.out.println("methodA");
long end = System.currentTimeMillis();
System.out.println("execute: " + (end - start) + "ms");
}
}
通过ASM Bytecode Viewer工具可以查看指令代码如下
public methodA()V
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "methodA"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LLOAD 3
LLOAD 1
LSUB
INVOKEDYNAMIC makeConcatWithConstants(J)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
// arguments:
"execute: \u0001ms"
]
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
MAXSTACK = 5
MAXLOCALS = 5
或者使用它提供的ASM提示
根据ASM指令函数,编写测试类AsmMain.kt 对AsmTest.class执行插桩
class AsmMain {
private val filePath = "/Users/laibinzhi/AndroidStudioProjects/AsmTest/app/src/test/java/com/example/asmtest/AsmTest.class"
@Test
fun handleTime() {
try {
//读取待插桩的class
val fis = FileInputStream(File(filePath))
//执行分析与插桩 ClassReader是class字节码的读取与分析引擎
val classReader = ClassReader(fis)
// ClassWriter写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
val classWriter = ClassWriter(ClassWriter.COMPUTE_FRAMES)
//执行分析,处理结果写入classWriter, EXPAND_FRAMES表示栈图以扩展格式进行访问 执行插桩的代码就在MyClassVisitor中实现
classReader.accept(MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES)
//获得执行了插桩之后的字节码数据
val bytes: ByteArray = classWriter.toByteArray()
// 重新写入InjectTest.class中(也可以写入到其他class中,InjectTest1.class),完成插桩
val fos = FileOutputStream(File(filePath))
fos.write(bytes)
fos.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
class MyClassVisitor(api: Int, classVisitor: ClassVisitor?) : ClassVisitor(api, classVisitor) {
override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
return MyMethodVisitor(api, methodVisitor, access, name, descriptor)
}
}
class MyMethodVisitor(
api: Int, methodVisitor: MethodVisitor?, access: Int, name: String?, descriptor: String?
) : AdviceAdapter(api, methodVisitor, access, name, descriptor) {
private var startIdentifier = 0
override fun onMethodEnter() {
super.onMethodEnter()
invokeStatic(Type.getType("Ljava/lang/System;"), Method("currentTimeMillis", "()J"))
//调用newLocal创建一个long类型的变量,返回一个int类型索引identifier
startIdentifier = newLocal(Type.LONG_TYPE)
//保存到本地变量索引中,用一个本地变量接收上一步执行的结果
storeLocal(startIdentifier)
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
invokeStatic(Type.getType("Ljava/lang/System;"), Method("currentTimeMillis", "()J"))
val endIdentifier = newLocal(Type.LONG_TYPE)
storeLocal(endIdentifier)
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"))
newInstance(Type.getType("Ljava/lang/StringBuilder;"))
dup()
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), Method("<init>", "()V"))
visitLdcInsn("execute: ")
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"))
loadLocal(endIdentifier)
loadLocal(startIdentifier)
math(SUB, Type.LONG_TYPE)
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), Method("append", "(J)Ljava/lang/StringBuilder;"))
visitLdcInsn("ms")
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"))
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), Method("toString", "()Ljava/lang/String;"))
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), Method("println", "(Ljava/lang/String;)V"))
}
}
}
通过运行该程序,会发现AsmTest.class已经发生了变化,可以看到在方法体前后加上了代码。
package com.example.asmtest;
class AsmTest {
public void methodA() {
long var1 = System.currentTimeMillis();
System.out.println("methodA");
long var3 = System.currentTimeMillis();
System.out.println("execute: " + (var3 - var1) + "ms");
}
}
实现原理
- 先通过ClassReader读取编译好的.class文件
- 其通过访问者模式(Visitor)对字节码进行修改,常见的Visitor类有:对方法进行修改的MethodVisitor,或者对变量进行修改的FieldVisitor等
- 通过ClassWriter重新构建编译修改后的字节码文件、或者将修改后的字节码文件输出到文件中。
Android实现-ASM
弄懂了ASM的使用和实现原理之后,我们尝试把它用在Android上。首先Android的打包流程如下
由此可见,我们要在Android中使用ASM框架,必须要在class->dex这个过程中完成插桩。所以,我们要借助Gradle plugin 、Transform来完成。
Gradle是一个框架,他负责定义流程和规则,而具体的工作都是通过插件实现的。比如:编译Java的插件,编译Groovy的插件,编译Android APP的插件。Gradle 插件简单概括就是将构建逻辑的可重用部分打包起来,应用到不同的项目和构建中。gradle插件介入编译构建过程,以达到扩展功能目的。
Gradle Transform是Android 官方提供给开发者在项目构建阶段(class->dex期间)用来修改.class文件的一套标准API。目前比较常用的就是,字节码插桩技术。
通过Transform API拿到应用程序的class文件,对class文件中的方法进行遍历,然后找到我们需要改动的方法,修改目标方法,插入我们的代码保存,这就是字节码插桩技术。
Transform并不是必须的,只要找到class编译为dex的task之前的节点,通过before插入一个自定义task也可以实现,但使用Transform更简单。
Google 在 AGP 8.0 会将 Gradle Transform 给移除,因此如果项目升级了 AGP 8.0,就需要做好 Gradle Transform 的兼容。
Gradle Transform被废弃之后,它的代替品是Transform Action,可以参考这篇文章
自定义Gradle Plugin插件
-
构建脚本
可以直接在构建脚本中包含插件的源代码。缺点:插件只能在定义它的构建脚本之内可见,不能在其他脚本中复用插件。
-
buildSrc
Gradle会自动找到并编译buildSrc模块里面的插件,并使其在构建脚本的类路径中可用。 该插件对整个项目里的每个构建脚本都是可见的, 但是,它在项目外部不可见,因此不能其他项目中复用该插件。优点:方便调试。
-
独立项目
可以为插件创建一个单独的项目,将项目打包成一个JAR包,并通过然后可以在多个项目中复用。创建一个publishing task将插件上传到maven库或本地。参考developer.android.com/studio/publ…
javassit
定义
-
javassit是一个开源的字节码创建、编辑类库,现属于Jboss web容器的一个子模块,特点是简单、快速,与AspectJ一样,使用它不需要了解字节码和虚拟机指令,官方网站
-
核心类
- ClassPool:一个基于HashMap实现的CtClass对象容器。
- CtClass:表示一个类,可从ClassPool中通过完整类名获取。
- CtMethods:表示类中的方法。
- CtFields :表示类中的字段。
小试牛刀
fun test() {
val pool = ClassPool.getDefault()
// 创建类
val clz = pool.makeClass("com.example.javassit.Person")
//创建属性String name
val field = CtField(pool.get("java.lang.String"), "name", clz)
//定义name访问属性是私有的
field.modifiers = Modifier.PRIVATE
//添加get和set方法
clz.addMethod(CtNewMethod.setter("setName", field))
clz.addMethod(CtNewMethod.getter("getName", field))
//把属性添加到类的成员属性中
clz.addField(field)
//添加无参的构造方法
val defaultConstructor = CtConstructor(arrayOf(), clz)
//在无参的构造方法中初始化name位空字符串
defaultConstructor.setBody("{name = \"\";}")
clz.addConstructor(defaultConstructor)
// 为Person类创建一个sayHello方法
val sayHello = CtMethod(CtClass.voidType, "sayHello", arrayOf(), clz);
sayHello.modifiers = Modifier.PUBLIC;
sayHello.setBody("System.out.println(\"hello222, this is \" + $0.name);");
clz.addMethod(sayHello);
clz.writeFile()
}
生成了Person类
package com.example.javassit;
public class Person {
private String name = "";
public void setName(String var1) {
this.name = var1;
}
public String getName() {
return this.name;
}
public Person() {
}
public void sayHello() {
System.out.println("hello222, this is " + this.name);
}
}
我们在原来类的基础上修改类中的信息
@Test
fun test2() {
val pool = ClassPool.getDefault()
pool.appendClassPath("/Users/laibinzhi/AndroidStudioProjects/JavassitTest/app");
val ctClass = pool.getCtClass("com.example.javassit.Person")
val sayHello: CtMethod = ctClass.getDeclaredMethod("sayHello")
//在原来sayHello的方法前面再插入一句sayHello
sayHello.insertBefore("System.out.println(\"this log is inserted before call sayHello()\");")
ctClass.writeFile()
}
再观察类信息
public void sayHello() {
System.out.println("this log is inserted before call sayHello()");
System.out.println("hello222, this is " + this.name);
}
Android实现-Javassist
和ASM在Android实现-ASM基本相同:Gradle Transform + Javassist
动态代理
定义以及使用
在运行时再创建代理类和其实例,因此显然效率更低。要完成这个场景,需要在运行期动态创建一个Class。JDK提供了 Proxy 来完成这件事情。基本使用如下:
- 创建接口,定义目标列要完成的功能。
- 创建目标类实现接口。
- 创建InvocationHandler接口的实现类,在invoke方法中完成代理类的功能。
- 调用目标方法
- 增强功能
- 使用Proxy类的静态方法,创建代理对象,并把返回值转为接口类型。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//目标接口
interface Api {
float test(String a);
}
//目标类
class ApiImpl implements Api {
@Override
public float test(String a) {
System.out.println("目标类中,执行目标方法:" + a);
return 1F;
}
}
//必须实现InvocationHandler接口,完成代理类要做的功能(1.调用目标方法 2,功能增强)
class MyInvocationHandler implements InvocationHandler {
private Object target;
//动态代理:目标对象是活动的,不是固定的,需要传入进来
//传入是谁,就给谁创建代理
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object res;
res = method.invoke(target, args);//执行目标方法
if (res != null) {
Float price = (Float) res;
price = price + 100;
res = price;
}
//在目标类的方法调用后,你做的其他功能,都是增强的意思
System.out.println("增强--------------------");
return res;
}
}
class Main {
public static void main(String[] args) {
//1.创建目标对象
Api factory = new ApiImpl();
//2.创建InvocationHandler对象
InvocationHandler invocationHandler = new MyInvocationHandler(factory);
//3.创建代理对象
Api proxy = (Api) Proxy.newProxyInstance(factory.getClass().getClassLoader(), factory.getClass().getInterfaces(), invocationHandler);
//4.通过代理执行方法
float hello = proxy.test("hello");
System.out.println("通过动态代理对象,调用方法:" + hello);
}
}
目标类中,执行目标方法:hello
增强--------------------
通过动态代理对象,调用方法:101.0
实际上, Proxy.newProxyInstance 会创建一个Class,与静态代理不同,这个Class不是由具体的.java源文件编译而来,即没有真正的文件,只是在内存中按照Class格式生成了一个Class。
源码分析
本文以jdk提供的Proxy作为分析
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
//安全管理器.判断有没有创建代理的权限
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* 这是动态代理中最重要的方法,代理类就是从中得到的
*/
Class<?> cl = getProxyClass0(loader, intfs);
/*
* 使用我们的InvocationHandler实现类来调用构造方法
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
//判断代理类是否是被public修饰的,如果不是,设计代理类是可以通过反射访问到的
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
//创建代理类对象
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
为什么动态代理需要传入classLoader? 1⃣️需要校验传入的接口是否可被当前的类加载器加载,假如无法加载,证明这个接口与类加载器不是同一个,按照双亲委派模型,那么类加载层次就被破坏了 2⃣️需要类加载器去根据生成的类的字节码去通过defineClass方法生成类的class文件,也就是说没有类加载的话是无法生成代理类的
我们主要看一下怎么获取到代理类的
Class<?> cl = getProxyClass0(loader, intfs);
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 通过缓存得到代理类,如果没有就通过ProxyClassFactory创建
return proxyClassCache.get(loader, interfaces);
}
接下来,看看ProxyClassFactory如何创建代理类
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 代理类类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";
// 代理类名字计数器 AtomicLong具有原子性
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* 判断相同名字的接口是否是同一个Class对象
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* 判断相应接口是否是一个真正的接口
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* 验证接口是否重复
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
String proxyPkg = null; // 生成的代理类的包名
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* 验证所有非公共接口的包是否在同一个包
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// 如果是公共接口,则用com.sun.proxy包
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* 为代理类生成名字,规则 包名+前缀+数字 例:com.sun.proxy.$Proxy0
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
//生成代理类的二进制文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
//动态生成代理类,这就是最后一步了,由于是native方法,所以就无法向下追究了
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}
接下来我们看一下具体是怎么获取代理类的二进制文件
ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();
//发现可以通过 saveGeneratedFiles参数来决定是否把代理类代存到本地
if (saveGeneratedFiles) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if (var1 > 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
Files.createDirectories(var3);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class");
}
//文件写入
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file: " + var4x);
}
}
});
}
return var4;
}
发现可以通过 saveGeneratedFiles参数来决定是否把代理类代存到本地,点击它来查看在哪进行设置
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
我们可以把sun.misc.ProxyGenerator.saveGeneratedFiles这个属性设为true来把生成的类保存到本地,再运行,就会看到系统帮我们生成的代理类。以下为部分截取
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("agent.Massage").getMethod("message", Class.forName("java.lang.String"));
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
在初始化时,获得 method 备用。而这个代理类中所有方法的实现变为:
public final void message(String var1) throws {
try {
super.h.invoke(this, m3, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
这里的 h 其实就是 InvocationHandler 接口,所以我们在使用动态代理时,传递的 InvocationHandler 就是一个监听,在代理对象上执行方法,都会由这个监听回调出来。
最后用一张图作为总结
现存问题
java动态代理最大的问题是只能代理接口,而不能代理普通类或者抽象类,这是因为默认创建的代理类继承Porxy,而java又不支持多继承,这一点极大的限制了动态代理的使用场景,cglib可代理普通类。
其他
-
cglib
上面说到的jdk提供的动态代理,只能为接口实现代理,是有局限性。cglib这个库可以实现代理没有实现接口类的代理
但是对于Android来说,有一个缺点:cglib底层采用的是ASM字节码生成框架,使用字节码技术生成代理类,即生成.class文件,而我们在Android中加载的是优化后的.dex文件,也就是说我们需要可以动态生成.dex文件的代理类,因此cglib在Android中是无法使用的。
不过有人根据dexmaker框架(dex代码生成工具)来仿照cglib库动态生成.dex文件,实现了类似于cglib的AOP的功能。
可以根据该项目获取灵感
-
LancetX
LancetX 是一个为Android项目设计的字节码插桩框架,其使用方式类似AspectJ。
该项目核心实现原理参考了 ele开源的 lancet 字节码插桩框架,与原有的lancet的不同点在, 新增使用字节跳动的ByteX进行 class 文件的并行化 以便加快编译速度。
各种AOP技术对比以及如何选型
时机
特点
| AOP技术框架 | 特点 | 开发难度 | 优势 | 劣势 |
|---|---|---|---|---|
| AspectJ | 目前最成熟的AOP工具,能根据方法名,注解等自行决定切入时机,功能齐全使用方便; | ⭐️⭐️ | 真正意义的AOP,支持通配、继承结构的AOP,无需硬编码切面。 | 重复织入、不织入问题 |
| APT | 常用于通过注解减少模板代码,对类的创建于增强需要依赖其他框架。 | ⭐️⭐️ | 开发注解简化上层编码。 | 使用注解对原工程具有侵入性。以及APT并不能对源文件进行修改操作,只能生成新的文件 |
| ASM | 面向字节码指令编程,功能强大。 | ⭐️⭐️⭐️ | 直接操作字节码,性能最好 | 切面能力不足,部分场景需硬编码。难度最大 |
| Javaassit | API简洁易懂,快速开发。 | ⭐️ | 上手快,新人友好,具备运行时加载class能力。 | 切点代码编写需注意class path加载问题。 |
| 动态代理 | 运行时扩展代理接口功能。 | ⭐️ | 运行时动态增强。 | 仅支持代理接口,扩展性差,使用反射性能差。 |