教你用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:

相关文章
相关标签/搜索