波尔多葡萄酒产区:基于AST的組件化自動插樁方案 [復制鏈接]

2019-5-31 09:48
ceshishangchuan 閱讀:410 評論:0 贊:0
Tag:  

恋恋波尔多 www.luaogj.com.cn 本文將帶你實現一個一百多行代碼實現的自動化插樁方案,解決組件化子??櫚某跏薊吐酚善韉淖遠⒉?,支持多種類型的插樁、支持前插后插、支持插入代碼的優先級設置。我們將使用編輯器的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<String, CodeItem> mTargets = new HashMap<>();
    private HashMap<String, PriorityQueue<CodeItem>> 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<CodeItem> 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/**'
    }
}
        }
    }

主??橐彩墻黿鱸謐鈧沾虬輩虐幽?櫚腶st文件夾,實現代碼完全隔離下的特殊情況特殊編譯:

 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、子??榕渲?/h3>

例如:a??櫚腶st:

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、主??榕渲茫?/h3>
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的錯誤前置,再也不用擔心有人在代碼里下毒了。

圖片描述

你還在為Aspectj難懂的切片語法煩惱嗎,你還在為ASM晦澀的字節碼操作而糾結嗎,你還正在為javassisit動不動就無法編譯的占用而無奈嗎,試試AST吧,一切都是一個清爽的開始。

例子十分簡單,直接上源碼:

@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<String, List<JCTree.JCStatement>> mCodes = new HashMap<>();
   private HashMap<String, CodeItem> mTargets = new HashMap<>();
   private HashMap<String, PriorityQueue<CodeItem>> 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<JCTree.JCStatement> 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<CodeItem> 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[1]()(0)()(index)()("=")().trim();   }
}


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反?。[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 恋恋波尔多 )

养宠物犬能赚钱吗 甘肃麻将玩法是顺抓顺打吗 蔡林记热干面赚钱吗 欢聚龙江麻将怎么下载挂 写qq日志能赚钱吗 德州麻将听牌 喜马拉雅fm直播赚钱 卖炒河粉米粉赚钱吗 30元入场棋牌二人麻将 qq捕鱼大亨 cdkey 职业车手怎样赚钱 东北地区冷棚种植哪些蔬菜赚钱 乐8彩票首页 5g网时代最好赚钱的东西 帮他人打字赚钱网址 承包工地食堂赚钱吗