java ast 添加注解_基于AST的组件化自动插桩方案
本文将带你实现一个一百多行代码实现的自动化插桩方案,解决组件化子模块的初始化和路由器的自动注册,支持多种类型的插桩、支持前插后插、支持插入代码的优先级设置。我们将使用编辑器的API来操作AST实现代码插桩,而非重量级的编译器(Aspectj)或者Gradle插件(ASM/Javassisit)。
第一步,定义AST注解:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface AST {
/**
* 类型 0插入点 1 需要插入的原始代码块
*/
int type();
/**
* 插桩的类型的id,支持多种需求的代码插桩
*/
int value();
/**
* 类型 为0 时 0表示前插 1表示后插
* 类型 为1 时 level表示优先级 0在最前 值越大 插入时排序的优先级越低
*/
int level();
/**
* 类型 0插入点 1 需要插入的代码块
*/
interface TYPE {
int TARGET = 0;
int SOURCE = 1;
}
/**
* 插入类型的id
*/
interface ID {
int MODULE_INIT = 0x0001;//模块初始化的插桩
int ROUTER_INIT = 0x0002;//路由注册的插桩
}
/**
* 类型 为0 时 0表示前插 1表示后插
*/
interface LEVEL {
int BEFORE = 0;
int AFTER = 1;
}
}
第二步,实现注解处理器
private Trees trees;
private HashMap mTargets = new HashMap<>();
private HashMap> mSources = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
trees = Trees.instance(env);
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
roundEnv.getRootElements().stream()
.filter(it -> it.getKind() == ElementKind.CLASS)
.forEach(it -> ((JCTree) trees.getTree(it)).accept(new AOPTreeTranslator()));
} else {
mTargets.entrySet().stream()
.filter(it -> it.getValue().level == AST.LEVEL.AFTER)
.forEach(it -> mSources.get(it.getKey()).forEach(node
-> it.getValue().med.body.stats
= it.getValue().med.body.stats.appendList(node.med.body.stats)));
}
return false;
}
解读:
1、使用mSources存储插桩类型对应的插入代码块的优先级队列
2、使用mTargets存储插桩类型对应的插入点代码块
3、根据注解收集器处理被注解标注的源代码
4、把所有需要插入的代码按照优先级插入到相应的插入点
注意点:
javac的List不同于常见的list,操作方式完全颠覆你的习惯。编译器用了它自己的数据类型来实现List,而不是使用java集合框架(Java Collection Framework)。
并且有许多静态的方法,可以很方便的创建List:
l List.nil()
l List.of(A)
同样,非传统的命名方式也带来了更漂亮的代码
不像传统java中用的代码:
List list = new List();
list.add(a);
list.add(b);
list.add(c);
而现在只需要写:
List.of(a, b, c);
List.from(list);
同时改变list使用:
list=list.appendList(newList);
而list.appendList或者list.append 是不会影响原来list的值,是不是很有趣?
第三步、实现TreeTranslator:
private class AOPTreeTranslator extends TreeTranslator {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethod) {
super.visitMethodDef(jcMethod);
if (jcMethod.getModifiers().annotations.toString().contains(AST.class.getSimpleName())) {
String type = getValue(jcMethod, 0);
String id = getValue(jcMethod, 1);
String levelType = getValue(jcMethod, 2);
if (type.equals("AST.TYPE.SOURCE")) {
int level = Integer.parseInt(levelType);
if (mSources.get(id) != null) {
mSources.get(id).add(new CodeItem(level, jcMethod));
} else {
PriorityQueue list = new PriorityQueue<>(Comparator.comparingInt(p -> p.level));
list.add(new CodeItem(level, jcMethod));
mSources.put(id, list);
}
} else {
int level = levelType.equals("AST.LEVEL.BEFORE") ? AST.LEVEL.BEFORE : AST.LEVEL.AFTER;
mTargets.put(id, new CodeItem(level, jcMethod));
}
}
}
}
这里就没什么好说的了,直接扫描AST填充mTargets和mSources而已。
OK,你没看错,以上基本就是所有代码了。
下面进行具体的案例讲解:
1、gradle配置:
重头戏来了:由于annotationProcessor无法扫描子组件里被注解的代码,我们可以采用gradle的配置去曲线救国:
子组件单独建立一个ast文件夹,仅仅在单独编译的时候属于他自己
sourceSets {
main {
//默认的作为application运行时Manifest文件路径
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
java {
srcDirs = ['src/main/java',
'src/main/ast']
}
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'ast/**'
}
}
}
}
主模块也是仅仅在最终打包时才包涵子模块的ast文件夹,实现代码完全隔离下的特殊情况特殊编译:
sourceSets {
main {
if (isDebug.toBoolean()) {
java {
srcDirs = ['src/main/java']
}
}else{
java {
srcDirs = ['src/main/java',
'../module_a/src/main/ast',
'../module_b/src/main/ast',
'../module_service/src/main/ast']
}
}
}
}
这样的话,子组件需要集中注册的代码,放在ast文件夹下即可,子组件单独运行时,赋予它意义,最终打包时,赋予它功能。
2、子模块配置
例如:a模块的ast:
public class ModuleA {
@AST(type = AST.TYPE.SOURCE, value = AST.ID.MODULE_INIT, level = 1)//插入源,插桩类型是模块初始化,优先级是1
public static void autoInitModule() {
(new com.easy.moduler.module_a.AModule()).afterConnected();
}
@AST(type = AST.TYPE.SOURCE, value = AST.ID.ROUTER_INIT, level = 1)//插入源,插桩类型是路由器自动注册,优先级是1
public static void autoInitRouter() {
com.easy.moduler.lib.router.Router.addRouterRule(
new com.easy.moduler.apt.processor.module_a_AutoRouterRuleCreator());
}
}
1、类名用全路径,IDE是不会自动帮你倒入的
2、注解注明,你的插桩类型,插桩id,插桩优先级
3、主模块配置:
public class App extends Application {
public void onCreate() {
super.onCreate();
initModule();
initRouter();
}
@AST(type = AST.TYPE.TARGET, value = AST.ID.MODULE_INIT, level = AST.LEVEL.BEFORE)//插桩目标点,模块初始化的插桩,插桩方式为前插
private void initModule() {
Log.e("TAG", "Module初始化本地逻辑");
}
@AST(type = AST.TYPE.TARGET, value = AST.ID.ROUTER_INIT, level = AST.LEVEL.AFTER)//插桩目标点,路由初始化的插桩,插桩方式为后插
private void initRouter() {
Log.e("TAG", "Router初始化本地逻辑");
}
}
为了表明插入的类型(前插或者后插),在方法体内放了一个log做参考。
最终结果:
插桩结果
如图所示,子模块的初始化和路由注册都被自动按照优先级插桩到相应的位置上。
即实现了代码的完全隔离,又实现了子组件的自动注册,而且没用反射,没用编译器,没用插件,使用编辑器的API实现了代码的自动插桩。
总结:
因为AST是IDE编辑器级别,Aspectj是java编译器级别,ASM/javassisit是字节码处理器插件级别。AST使用编辑器的API,还有注解过滤带来的效率提升,而不是把所有代码都扫描一遍。如果插入的代码逻辑有错误,AST在编译之前就会直接报错,而其他由于处理得是编译后的字节码,所以只能在运行期间才会崩溃。有了AST的错误前置,再也不用担心有人在代码里下毒了。
你TMD在代码里下毒
你还在为Aspectj难懂的切片语法烦恼吗,你还在为ASM晦涩的字节码操作而纠结吗,你还正在为javassisit动不动就无法编译的占用而无奈吗,试试AST吧,一切都是一个清爽的开始。
老司机赶紧进群开车: 555343041
例子十分简单,直接上源码:
@AutoService(Processor.class)//自动生成 javax.annotation.processing.IProcessor 文件
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes({"com.easy.moduler.annotation.AST"})//注意替换成你自己的注解名
public class ASTProcessor extends AbstractProcessor {
private Trees trees;
private HashMap> mCodes = new HashMap<>();
private HashMap mTargets = new HashMap<>();
private HashMap> mSources = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
trees = Trees.instance(env);
}
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
roundEnv.getRootElements().stream()
.filter(it -> it.getKind() == ElementKind.CLASS)
.forEach(it -> ((JCTree) trees.getTree(it)).accept(new AOPTreeTranslator()));
} else {
mTargets.entrySet().stream()
.filter(it -> it.getValue().level == AST.LEVEL.AFTER)
.forEach(it -> mSources.get(it.getKey()).forEach(node
-> it.getValue().med.body.stats
= it.getValue().med.body.stats.appendList(node.med.body.stats)));
mTargets.entrySet().stream()
.filter(it -> it.getValue().level == AST.LEVEL.BEFORE)
.forEach(it -> mSources.get(it.getKey())
.forEach(node -> {
List list;
if (mCodes.get(it.getKey()) != null)
list = mCodes.get(it.getKey()).appendList(node.med.body.stats);
else list = List.from(node.med.body.stats);
mCodes.put(it.getKey(), list);
}));
mCodes.forEach((key, value) ->
mTargets.get(key).med.body.stats = mTargets.get(key).med.body.stats.prependList(value));
}
return false;
}
class CodeItem {
int level;//优先级
JCTree.JCMethodDecl med;//方法体
CodeItem(int levelType, JCTree.JCMethodDecl jcMethodDecl) {
this.level = levelType;
this.med = jcMethodDecl;
}
}
private class AOPTreeTranslator extends TreeTranslator {
@Override
public void visitMethodDef(JCTree.JCMethodDecl jcMethod) {
super.visitMethodDef(jcMethod);
if (jcMethod.getModifiers().annotations.toString().contains(AST.class.getSimpleName())) {
String type = getValue(jcMethod, 0);
String id = getValue(jcMethod, 1);
String levelType = getValue(jcMethod, 2);
if (type.equals("AST.TYPE.SOURCE")) {
int level = Integer.parseInt(levelType);
if (mSources.get(id) != null) {
mSources.get(id).add(new CodeItem(level, jcMethod));
} else {
PriorityQueue list = new PriorityQueue<>(Comparator.comparingInt(p -> p.level));
list.add(new CodeItem(level, jcMethod));
mSources.put(id, list);
}
} else {
int level = levelType.equals("AST.LEVEL.BEFORE") ? AST.LEVEL.BEFORE : AST.LEVEL.AFTER;
mTargets.put(id, new CodeItem(level, jcMethod));
}
}
}
}
private String getValue(JCTree.JCMethodDecl jcMethod, int index) {
return jcMethod.getModifiers().annotations.get(0).getArguments().get(index).toString().split("=")[1].trim();
}
}
java ast 添加注解_基于AST的组件化自动插桩方案相关推荐
- automake生成静态库文件_基于CocoaPods的组件化原理及私有库实践
轮子为什么会存在 智人能在残酷的进化大战中存活下来,原因之一就是智人懂得将知识沉淀成外物,辅助彼此之间的合作,从而使得整个群体产生了规模效应,即1+1>2的效果. 从一个角度上说,石器时代是基于 ...
- 京东java前后端联调_前端工程化、组件化实践JDM分享
前端技术原创文 前端工程化.组件化实践JDM分享 该文由孵化创新一部曾瑞文在研究院技术分享会的分享内容总结而成,主要讲解了团队创新的前端工程化.组件化的思想及实践应用. 为什么要搞前端框架? Java ...
- java项目----教务管理系统_基于Java的教务管理系统
java项目----教务管理系统_基于Java的教务管理系统 2022-04-22 18:18·java基础 最近为客户开发了一套学校用教务管理系统,主要实现学生.课程.老师.选课等相关的信息化管理功 ...
- 基于SOA的组件化业务基础平台[转]
转自https://www.ibm.com/developerworks/cn/webservices/1111_xiaojg_soa/index.html 业务基础平台是业务逻辑和基础架构平台之间的 ...
- 基于 SOA 的组件化业务基础平台
基于 SOA 的组件化业务基础平台 原文:基于 SOA 的组件化业务基础平台 前言 业务基础平台是业务逻辑应用和基础架构平台之间的一个中间层,解决 "应用软件的业务描述和操作系统平台.软件基 ...
- java 添加注解_你知道Java中的package-info的作用吗?
package-info.java对于日常业务开发的开发者来说,可能有点陌生,如果我们再项目中创建一个package-info.java,你会发现该类只有一个packge,如下 我们无法在该java文 ...
- java spring框架 注解_史上最全的java spring注解
史上最全的java spring注解,没有之一 注解是个好东西,但好东西我们也是看见过,整理过,理解过,用过才知道好.不求我们每个都记住,但求保有印象,在需要的时候能提取出来再查找相关资料,平时工作就 ...
- java编译时注解_简单介绍 Java 中的编译时注解
1. 前言 上一篇 主要介绍了什么是 注解 (Annotation) 以及如何读取 运行时注解 中的数据, 同时用注解实现了简单的 ORM 功能. 这次介绍另一部分: 如何读取 编译时注解 ( Ret ...
- java批量添加注解到所有业务接口
背景: 已经完成或者持续更新的一个项目,已经有上千个业务接口,需要在这上千个业务接口上添加一个我自定义的注解,不可能一个一个添加呀!!! 所以应该批量吧:批量操作使用缓存流 package com.d ...
最新文章
- 计算机视觉怎样实现自我超越?更大规模更精准的数据
- 《Adobe Acrobat DC经典教程》—第1章1.11节在阅读模式下查看PDF文件
- Vmware vSphere 5.0安装和配置
- hdu 1087 Super Jumping! Jumping! Jumping! 动态规划
- java properties读取缓存_Java读取Properties文件
- 1431. Kids With the Greatest Number of Candies
- 读书笔记_探索式测试_混合探索式测试
- ogm session_带有Hibernate OGM的NoSQL –第一部分:持久化您的第一个实体
- word流程图整体怎么拆分_word流程图-不会做流程图?这三种方法超级简单!
- 关于数据型驱动自动化测试实现
- 总结CSS3新特性(Transition篇)
- Python数据分析与展示[第二周]
- 3dsmax怎么添加uv坐标_RizomUV C++ Library:只需几分钟即可展开和打包UV地图
- nfc apdu指令_stm32——NFC芯片PN532的使用
- 服务器自动ip使用,自动或手动设置IP地址图解教程
- iphone控制中心自定义没有计算机,科技:如何在iOS上使用和自定义控制中心
- 复杂的密码学也可以人人可懂
- Encoded password does not look like BCrypt 使用SpringSecurity中BCryptPasswordEncoder做盐值加密时出现错误
- 221007工作日志:SPSS logistic回归分析结果释读详解
- 基于深度学习的玫瑰向日葵分类系统
热门文章
- python write非法字符报错_Python爬虫实现的微信公众号文章下载器
- 只做决定的架构师会成为团队的瓶颈!
- Firefox 突然拉黑中国用户
- 尽快卸载这两款恶意浏览器插件!已有近50万用户安装
- Redis主从握手流程,你真的了解了吗?
- 百度申请“员工工作状态预测”专利,意欲何为?
- Node.js 15 正式发布,14 将支持到 2023 年
- 如果你不习惯新版的 Github 的 UI 界面,可以试试这款插件
- 为什么request的页面与原页面不同_如何在不同页面上使用不同的Joomla CSS
- ps随机排列_漂亮!自然材料:人工可控微米级胶体粒子“堆积木”——粒子随心所欲的组装排列!...