Javassist实现无入侵日志注入

1,314 阅读4分钟

前言

最近由于公司项目的需要,对Redisson进行重新封装,封装的同时需要将日志和报警监控等信息暴露给监控平台。但是由于封装的方法较多,如果对每个方法都使用try-catch盖住的话,代码无疑会显得非常臃肿。因此,考虑使用字节码注入的方式实现无入侵插入日志。

实现过程

字节码注入

  1. 通过CtMethod获取所需要注入代码的返回值
  2. 通过methodName + "(?);\n" 实现对未注入之前的方法的调用。 在注入的时候,查阅资料发现大部分的注入都是针对无返回值的注入方式。但是如果需要用try-catch盖住每个方法,那么势必会出现有返回的值的方式。因此可以在调用原来的方法之前先声明一个常量,然后通过1中的方法进行初始化,记住一定要给初始化,因为在idea中声明一个变量的时候,不初始化好像程序也不会报错,那是因为JVM帮助做了初始化,这个部分可以参考JVM中类的生命周期。而在Javassist中,Class本来就已经在JVM中了,也就不会在给默认值了。 然后通过body.append(type + " result = null;\n")这样的方式,可以获取未注入之前的运行结果,进一步可以在finally块结束之后返回。
  3. 在StringBuffer中拼接所需要注入的代码。 经历过以上3个步骤之后,代码注入的过程就完成了。
private static void injectCode(StringBuffer body, String methodName /*原来方法中的代码*/, boolean returnVoid,CtMethod mold) throws NotFoundException {
        String type = mold.getReturnType().getName();
        body.append("{    int res = 7;\n");
        body.append("    long start = System.currentTimeMillis();\n" +
                "        long cost = -1;\n" +
                "        long end = start;\n" +
                "        boolean isSuccess = true;\n" +
                "        System.out.println(System.currentTimeMillis()); \n" );

        if (!type.equals("void")){
            if (type.equals("boolean")){
                body.append(type + " result = false;\n");
            }
            else if (type.equals("String")){
                body.append(type + " result = null;\n");
            }
            else if (type.equals("long")){
                body.append(type + " result = 0;\n");
            }
            else if (type.equals("int")){
                body.append(type + " result = 0;\n");
            }
            else{
                body.append(type + " result = null;\n");
            }
        }

        body.append("\n try{" + "\n");
        // 调用原有代码,类似于method();(?)表示所有的参数
        if (!"void".equals(type)){
            body.append("result = ");
        }
        body.append(methodName + "(?);\n");
        body.append("}catch (Exception e){\n" +
                "            e.printStackTrace();" +
                "        }finally {\n" +
                "            end = System.currentTimeMillis();\n" +
                "            cost = start - end;\n" +
                "            System.out.println(res + 111111111);"+
                "        }");
        if (!"void".equals(type)){
            body.append("return result;\n");
        }
        body.append("}");
    }

字节码替换

但是显然刚刚仅仅是完成了向目标方法中添加了所需要的代码。那么注入之后的代码又该如何生效呢? CtMethod方法提供了一个setBody的方法可以将上一过程中的代码插入原来的方法中。并通过CtClass添加一个新的方法。

    private static void addLog(String methodName,CtClass ctClass) throws CannotCompileException, NotFoundException, IllegalAccessException, InstantiationException {
        CtMethod mold = ctClass.getDeclaredMethod(methodName);
        // 修改原有的方法名称
        String newName = methodName + "$impl";
        mold.setName(newName);

        //创建新的方法,复制原来的方法
        // 主要的注入代码
        CtMethod mnew = CtNewMethod.copy(mold, methodName, ctClass, null);
        StringBuffer body = new StringBuffer();
        try {
            injectCode(body,newName,mold.getReturnType() == CtClass.voidType,mold);
        }catch (Exception e){
            e.printStackTrace();
        }
        // 替换新方法
        mnew.setBody(body.toString());
        // 增加新方法
        ctClass.addMethod(mnew);
    }

整体注入

上面介绍了对单个方法如何进行字节码注入的过程,但是尚未实现我们的目标-对工具类进行无入侵注入。要实现对工具类中的供开发同学使用的方法进行注入,需要用反射的方式获取工具类中的方法,将其中的public方法进行注入。 那么问题来了,如果直接使用RedissonTool.class.getMethods()的方法的话,在字节码替换的时候会提醒JVM中已经含有此类不能修改的问题。因为当在调用RedissonTool.class的时候,JVM就会加载class信息,而我们又是在原先的class中直接修改的,JVM在运行时期是不运行进行class修改的。 JVM是如何判断类是否已经加载了呢?我们知道,在JVM中判断一个类是否相同首先是判断类加载器是否相同,进一步判断类名是否相同。因此为了避免上述问题,可以通过自定义类加载器的方式进行加载(主要目的是打破双亲委派)。

public class MyClassLoader extends ClassLoader{
    private String path;

    public MyClassLoader(String clazzPath){
        this.path = clazzPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class log = null;
        byte[] bytes = gatDate();
        if (bytes != null){
            log = defineClass(name,bytes,0,bytes.length);
        }

        return log;
    }

    private byte[] gatDate(){
        File file = new File(path);
        if (file.exists()){
            FileInputStream inputStream = null;
            ByteArrayOutputStream outputStream = null;

            try{
                inputStream = new FileInputStream(file);
                outputStream = new ByteArrayOutputStream();

                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = inputStream.read(buffer)) != -1){
                    outputStream.write(buffer,0,size);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                try {
                    inputStream.close();
                    outputStream.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }

            return outputStream.toByteArray();
        }else{
            return null;
        }
    }
}

接下来的工作就是涉及到具体的业务了,根据自己的需求进行判断。

        // 定义类
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("redisson.RedissonTool");
        List<String> methodList = new ArrayList<>();
        URL path = Thread.currentThread().getContextClassLoader ().getResource("");
        MyClassLoader loader = new MyClassLoader(path.getPath() + "redisson/RedissonTool.class");
        Class<?>redissonTool = null;
        try {
            redissonTool = loader.findClass("redisson.RedissonTool");
            System.out.println(redissonTool.getClassLoader() + "11111");
            redissonTool.newInstance();

        }catch (Exception e){
            e.printStackTrace();
        }

        Method[] methods = redissonTool.getMethods();

        excludeObjectMethod(methods);
        for (Method i : methods){
            if (i != null){
                addLog(i.getName(),ctClass);
            }
        }

//        addLog("set",ctClass);
        RedissonTool redissonTool1 = (RedissonTool) ctClass.toClass().newInstance();

问题以及优化

其实这样的实现注入的方式同样稍显的臃肿,针对JVM中不能出现重复的类的问题Javassist中应该会有给出解决方法,无需自己创建类加载器。但目前尚未对Javassist进行研究,所以这一块需要深入挖掘一下。