教你用java字節碼作點有趣的事之脫敏插件

一些重複的活,能交給程序作就毫不本身作,這就是程序員精神。java

0 寫在前面

本篇是本系列的最後一篇,在這篇中教你用ASM實際開發中作一些可用的東西。包括以前說的如何修改toString,完成一些脫敏。git

1 Instrumentation

上一篇字節碼之ASM教你瞭如何去修改字節碼?相信看過的同窗已經對如何修改字節碼已經有必定印象了,可是這裏有個問題,上一節咱們是經過讀取.class文件在內存裏面使用,並不能影響咱們實際jvm中使用的class。這個的確是一個比較難解決的問題,至少在jdk1.5以前是這樣的,在jdk1.5的時候java.lang.instrument出世了。它把Java的instrument功能從本地代碼中解放出來,使之能夠用 Java 代碼的方式解決問題。java.lang.instrument是在JVM TI的基礎上提供的Java版本的實現。 Instrumentation提供的主要功能是修改jvm中類的行爲。 Java SE6中有兩種應用Instrumentation的方式,premain(命令行)和agentmain(運行時)。程序員

1.1 premain

咱們知道java程序啓動都得經過main方法啓動,而premain的意思就是在Main啓動以前會運行premain。 首先編寫一個Java類,而後包含下面兩個中的一個方法便可:github

public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
複製代碼

上面兩個同時存在時1比2優先級高。這個方法有兩個參數:web

  • agentArgs:這個是main函數中傳入的參數,這裏傳入的參數的字符串數組,須要本身解析。
  • Instrumentation:這個是咱們的核心, instrument 包中定義的一個接口,也是這個包的核心部分,集中了其中幾乎全部的功能方法,例如類定義的轉換和操做等等。

而後實現ClassFileTransformer接口,ClassFileTransform用於類的轉換,其接口transform是轉換類的關鍵,其第四個入參也是咱們後續修改字節碼的關鍵:面試

public class ClassTransformerImpl implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("類的名字爲:" + className);
        return classfileBuffer;
    }
}
複製代碼

上面再transform中咱們打印了全部類的名字, 回到咱們的premain中咱們的方法以下:apache

public class PerfMonAgent {
    static private Instrumentation inst = null;
    public static void premain(String agentArgs, Instrumentation _inst) {
        System.out.println("PerfMonAgent.premain() was called.");
        // Initialize the static variables we use to track information.
        inst = _inst;
        // Set up the class-file transformer.
        ClassFileTransformer trans = new ClassTransformerImpl();
        System.out.println("Adding a PerfMonXformer instance to the JVM.");
        //將咱們自定義的類轉換器傳入進去
        inst.addTransformer(trans);
    }
}
複製代碼

咱們能夠把上面的premain方法修改以下:json

public class PerfMonAgent {
    static private Instrumentation inst = null;
    public static void premain(String agentArgs, Instrumentation _inst) {
        System.out.println("PerfMonAgent.premain() was called.");
        // Initialize the static variables we use to track information.
        inst = _inst;
        // Set up the class-file transformer.
        ClassFileTransformer trans = new ClassTransformerImpl();
        System.out.println("Adding a PerfMonXformer instance to the JVM.");
        //將咱們自定義的類轉換器傳入進去
        inst.addTransformer(trans);
    }
}
複製代碼

代碼方面的已經定義完畢。接下來須要將其進行打包若是你沒用Maven那麼你須要在其中的 manifest 屬性當中加入」 Premain-Class」來指定當中編寫的那個帶有 premain 的 Java 類。若是你是使用的maven那麼你能夠用api

<plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>2.2</version>
        <configuration>
            <archive>
                    <manifestEntries>
                           <Premain-Class>instrument.PerfMonAgent</Premain-Class>
                           //這個是用來引入第三方包,須要在這裏引入 <Boot-Class-Path>/Users/lizhao/.m2/repository/org/ow2/asm/asm/5.0.4/asm-5.0.4.jar</Boot-Class-Path>
                    </manifestEntries>
            </archive>
        </configuration>
    </plugin>
</plugins>
複製代碼

最後你可使用了,你隨意編寫一個帶main方法的類:數組

java -javaagent:jar 文件的位置 [= 傳入 premain 的參數 ] 
複製代碼

若是是idea編譯器你能夠在vm配置中輸入

而後run main方法,就會輸出你的類名字。

1.2 agentmain

premain是Java SE5開始就提供的代理方式,給了開發者諸多驚喜,不過也有些須不變,因爲其必須在命令行指定代理jar,而且代理類必須在main方法前啓動。所以,要求開發者在應用前就必須確認代理的處理邏輯和參數內容等等,在有些場合下,這是比較困難的。好比正常的生產環境下,通常不會開啓代理功能,全部java SE6以後提供了agentmain,用於咱們動態的進行修改,而不須要在設置代理。在 JavaSE6文檔當中,開發者也許沒法在 java.lang.instrument包相關的文檔部分看到明確的介紹,更加沒法看到具體的應用 agnetmain 的例子。不過,在 Java SE 6 的新特性裏面,有一個不太起眼的地方,揭示了 agentmain 的用法。這就是 Java SE 6 當中提供的 Attach API。

Attach API 不是Java的標準API,而是Sun公司提供的一套擴展 API,用來向目標JVM」附着」(Attach)代理工具程序的。有了它,開發者能夠方便的監控一個JVM,運行一個外加的代理程序。 這裏不作篇幅介紹attach api怎麼運行的,總而言之須要依靠accach api整個過程依然比較麻煩,感興趣的同窗能夠自行閱讀: https://www.ibm.com/developerworks/cn/java/j-lo-jse61/

1.3小結

有了咱們的Instrument以後咱們就找到了咱們class的來源,依靠上一節的知識,咱們就能爲所欲爲的修改字節碼了。

2.動手爲toString脫敏

2.1設計

首先咱們須要對咱們接下來要作的東西進行設計,作到內心有底,這樣才能遇事不慌。

2.1.1 目標

修改toString的字節碼,讓之前打印明文的toString(),能針對咱們自定義的需求進行脫敏。

2.1.2 自定義

打算經過註解進行自定義脫敏,@DesFiled進行標記要脫敏的field,@Desenstized進行標記脫敏的類,經過繼承一個basefilter進行脫敏的擴展。

2.2動手以前

動手以前要先明確一下,必須明確下工具是否已經準備好了

  • asm插件是否已經下載?
  • asm的maven包是否已經引入?
  • 個人公衆號是否已經關注? 若是都完成了咱們即可以作下面的事了,咱們首先定義好咱們的註解:
@java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Inherited
public @interface DesFiled {
    /**
     * 加密類型
     * @return
     */
    public Class<? extends BaseDesFilter> value() default BaseDesFilter.class;

}
@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface Desensitized {
}
複製代碼

還有咱們的脫敏的filter接口,以及他的實現類用於手機號field的脫敏,其實也就是轉換:

public interface BaseDesFilter <T>{
    default T desc(T needDesc){
        return needDesc;
    };
}
public class MobileDesFilter implements BaseDesFilter {
    //不一樣類型轉換
    @Override
    public Object desc(Object needDesc) {
        if(needDesc instanceof Long ){
            needDesc = String.valueOf(needDesc);
        }
        if (needDesc instanceof String){
            return DesensitizationUtil.mobileDesensitiza((String) needDesc);
        }
        //若是這個時候是枚舉類,todo
        return needDesc;
    }
}
複製代碼

而後咱們編寫一個用於脫敏的類:

@Desensitized
public class StreamDemo1 {


    @DesFiled(MobileDesFilter.class)
    private String name;
    private String idCard;
    @DesFiled(AddressDesFilter.class)
    private List<String> mm;
    

    @Override
    public String toString() {
        return "StreamDemo1{" +
                "name='" + name + '\'' + ", idCard='" + idCard + '\'' + ", mm=" + mm + '}'; } } 複製代碼

這個時候你的asm插件就能夠大顯神威了,(不只是這裏,之後若是你們開發asm相關的,用插件看他原本的代碼,而後進行對比),這裏咱們經過asm插件生成一版asm的代碼這個時候能夠截圖保存,而後咱們手動的修改toString方法:

@Override
    public String toString() {
        return "StreamDemo1{" +
                "name='" + DesFilterMap.getByClassName("MobileDesFilter").desc(name) + '\'' + ", idCard='" + idCard + '\'' + ", mm=" + mm + '}'; } 複製代碼

用插件生成,這裏經過對比咱們能知道若是要加一個脫敏的方法,咱們須要在ASM中增長什麼。

咱們能夠看見兩張圖在append之間是有一些區別的(這裏要說明下編譯器會把+號優化成StringBuilder的append)

而咱們須要作的就是把第二張圖裏面紅框寫的替換成第一張圖裏紅框的。簡單的來講第一張圖只是先獲取this引用,而後進行field的獲取。第二張圖是須要先獲取到脫敏方法的引用而後傳入this.name進行脫敏。

這下咱們就知道本身須要作的了,這個時候其實徹底不須要看接下來的細節了,能夠本身去嘗試一下,看看是如何去實現。

2.2開始動手

首先定義一個類轉換器:

public class PerfMonXformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] transformed = null;
        System.out.println("Transforming " + className);
        ClassReader reader = new ClassReader(classfileBuffer);
        //自動計算棧幀
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        //選擇支持Java8的asm5
        ClassVisitor classVisitor = new DesClassVistor(Opcodes.ASM5,classWriter);
        reader.accept(classVisitor,ClassReader.SKIP_DEBUG);
        return classWriter.toByteArray();
    }
}
複製代碼

在類轉換器中用到了咱們上一節ASM的知識,而後咱們自定義一個ClassVisitor叫DesClassVistor,用來進行訪問類的處理,而後經過咱們的classWriter生成byte數組:

public class DesClassVistor extends ClassVisitor implements Opcodes{

    private static final String classAnnotationType = "L"+ Desensitized.class.getName().replaceAll("\\.","/")+";";
    /**
     * 用來標誌是否進行脫敏
     */
    private boolean des;
    private String className;
    private Map<String, FiledInfo> filedMap = new HashMap<>();
    public DesClassVistor(int i) {
        super(i);
    }

    public DesClassVistor(int api, ClassVisitor cv) {
        super(api, cv);
    }

    @Override
    public void visit(int jdkVersion, int acc, String className, String generic, String superClass, String[] superInterface) {
        this.className = className;
        super.visit(jdkVersion, acc, className, generic, superClass, superInterface);
    }

    /**
     *
     * @param type 註解類型
     * @param seeing 可見性
     * @return
     */
    @Override
    public AnnotationVisitor visitAnnotation(String type, boolean seeing) {
        if (classAnnotationType.equals(type)){
            this.des = true;
        }
        return super.visitAnnotation(type, seeing);
    }

    /**
     *
     * @param acc 訪問權限
     * @param name 字段名字
     * @param type 類型
     * @param generic 泛型
     * @param defaultValue 默認值
     * @return
     */
    @Override
    public FieldVisitor visitField(int acc, String name, String type, String generic, Object defaultValue) {
        FieldVisitor fv = super.visitField(acc, name, type, generic, defaultValue);
        if (des == false || acc >= ACC_STATIC){
            return fv;
        }
        FiledInfo filedInfo = new FiledInfo(acc, name, type, generic, defaultValue);
        filedMap.put(name, filedInfo);
        FieldVisitor testFieldVisitor = new DesFieldVisitor(filedInfo,fv);
        return testFieldVisitor;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (this.des == false || !"toString".equals(name)){
            return mv;
        }
        MethodVisitor testMethodVistor = new DesMethodVistor(mv, filedMap);
        return testMethodVistor;
    }

}
複製代碼

這裏重寫了三個比較重要的方法:

  • visitAnnotation:用於判斷是否有@Desensitized的註解,若是有則設置des=true用來表示開啓註解
  • visitField:用來將asm中的filed轉換成咱們本身自定義的FieldInfo並放入map,後續方便處理,並將filed交給自定義的DesFieldVisitor進行處理filed
  • visitMethod:用來將asm中的toString方法放入自定義的DesMethodVistor用來處理toString方法。

對於filed的處理有以下代碼:

public class DesFieldVisitor extends FieldVisitor {

    private static final String desFieldAnnotationType = "L"+ DesFiled.class.getName().replaceAll("\\.","/")+";";
    private FiledInfo info;
    public DesFieldVisitor(int i) {
        super(i);
    }

    public DesFieldVisitor(int i, FieldVisitor fieldVisitor) {
        super(i, fieldVisitor);
    }

    public DesFieldVisitor(FiledInfo filedInfo, org.objectweb.asm.FieldVisitor fv) {
        super(Opcodes.ASM5, fv);
        info = filedInfo;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String s, boolean b) {
        AnnotationVisitor av = super.visitAnnotation(s, b);
        if (!desFieldAnnotationType.equals(s)){
            return av;
        }
        info.setDes(true);
        AnnotationVisitor avAdapter = new DesTypeAnnotationAdapter(Opcodes.ASM5, av, this.info);
        return avAdapter;
    }
}
複製代碼

經過重寫了visitAnnotation,進行判斷來獲取是否有DesFiled註解以及註解上的信息。

public class DesMethodVistor extends MethodVisitor implements Opcodes{
    Map<String, FiledInfo> filedMap;
    public DesMethodVistor(int i) {
        super(i);
    }

    public DesMethodVistor(int i, MethodVisitor methodVisitor) {
        super(i, methodVisitor);
    }

    public DesMethodVistor(MethodVisitor mv, Map<String, FiledInfo> filedMap) {
        super(ASM5, mv);
        this.filedMap = filedMap;
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        if (!(opcode == Opcodes.ALOAD && var == 0)){
            super.visitVarInsn(opcode, var);
        }
    }

    /**
     * 添加過濾邏輯
     * @param opcode
     * @param owner
     * @param name
     * @param desc
     */
    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        FiledInfo filedInfo = filedMap.get(name);
        if (filedInfo.isNotDes()){
            super.visitVarInsn(ALOAD, 0);
            super.visitFieldInsn(opcode, owner, name, desc);
            return;
        }
        mv.visitLdcInsn(filedInfo.getFilterClass().getName());
        mv.visitMethodInsn(INVOKESTATIC, ASMUtil.getASMOwnerByClass(DesFilterMap.class), "getByClassName", "(Ljava/lang/String;)Lasm/filter/BaseDesFilter;", false);
        super.visitVarInsn(ALOAD, 0);
        super.visitFieldInsn(opcode, owner, name, desc);
        mv.visitMethodInsn(INVOKEINTERFACE, ASMUtil.getASMOwnerByClass(BaseDesFilter.class), "desc", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
        mv.visitMethodInsn(INVOKESTATIC, ASMUtil.getASMOwnerByClass(String.class), "valueOf", "(Ljava/lang/Object;)Ljava/lang/String;", true);
    }
}
複製代碼

經過重寫visitFieldInsn方法進行脫敏的字節碼的改造。 具體的代碼能夠參照個人asm-log,在StreamDemo中配置好vm參數,執行main方法便可。 參照個人代碼:

@Desensitized
public class StreamDemo1 {


    @DesFiled(MobileDesFilter.class)
    private String name;
    private String idCard;
    @DesFiled(AddressDesFilter.class)
    private List<String> mm;


    @Override
    public String toString() {
        return "StreamDemo1{" +
                "name='" + name + '\'' + ", idCard='" + idCard + '\'' + ", mm=" + mm + '}'; } public static void main(String[] args) throws Exception { StreamDemo1 streamDemo1 = new StreamDemo1(); streamDemo1.setName("18428368642"); streamDemo1.setIdCard("22321321321"); streamDemo1.setMm(Arrays.asList("北京是朝陽區打撒所大所大","北京是朝陽區打撒所大所大")); System.out.println(streamDemo1); } } 複製代碼

在類上和類的變量是都寫上註解,一個使用手機號的脫敏類,一個使用地址的脫敏類,執行main方法,就能輸出以下:

StreamDemo1{name='184****8642', idCard='22321321321', mm=[北京是朝陽區打*****, 北京是朝陽區打*****]}
複製代碼

這樣就避免你用本身寶貴的時間重複的去每一個類中,去修改toString,這樣的確是過低效,做爲程序員那就須要有本身的hack精神,能交給程序作的決不用本身作。

2.3作完以後的思考

用字節碼作一個工具,的確學到了不少,至少之後對看懂字節碼,看懂一些Java對語法糖處理有很大的幫助,可是這個工具不是很通用,打個jar包出來,你須要配置agent或者你用attach api,這樣的話對業務配置還挺麻煩的。因此能夠經過其餘的技術來完成咱們的工具,好比註解處理器修改抽象語法樹,就像Lombok同樣對業務入侵較小。

同時ASM的做用不只僅是和instrument搭配,你們能夠看看cglib切面的源碼,或者看看fastjson的源碼,你能夠根據jvm中已經加載好的類,而後修改其字節碼修改爲新的其餘類,這裏能夠是代理類,也能夠是一個徹底新的類。

最後

因爲本身的水平有限,尤爲是在描述這種比較冷門的知識的時候不能抽象得很好,但願你們能理解體諒,同時也但願你們看完以後能本身作一個有關於asm的小工具,能夠是打方法耗時時間,也能夠是統一事務管理。

原本打算接下來立刻寫修改語法樹教程,想教你們如何手擼一個Lombok(java必備神器),可是發現這類知識點比較生僻的文章的確比較難懂,修改語法樹又比字節碼可能稍微困難一點,各類文檔都比較少,又加上最近工做比較忙,只有下班後寫到凌晨,感受不是能很好將比較複雜的知識點抽象成簡單的,決定先暫時不寫了。若是對Lombok原理或者若是對如何實現本身的Lombok有興趣的能夠參考個人slothlog github(順便求下star)裏面不少地方都標註了註釋,若是有什麼不明白的能夠關注個人公衆號,加我微信私聊。

若是你們以爲這篇文章對你有幫助,或者想提早獲取後續章節文章,或者你有什麼疑問想提供1v1免費vip服務,均可以關注個人公衆號,關注便可免費領取上百G最新java學習資料視頻,以及最新面試資料,你的關注和轉發是對我最大的支持,O(∩_∩)O:

相關文章
相關標籤/搜索