java修改字节码技术,Javassist修改class,ASM修改class

背景:

        项目使用的Logback 1.1.11版本的类ch.qos.logback.core.rolling.helper.RollingCalendar的periodBarriersCrossed方法long转换成int发生溢出,导致日志无法删除,最终决定在不升级logback版本的前提下使用java修改字节码技术修复此bug。

知识点:

      提到java字节码技术,总是离不开ASM,cglib,Javassist, java Agent这些名词,下面先简单介绍下这些名词。

      ASM:可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为,ASM是在指令层次上操作字节码的,使用难度较高。

      cglib: 基于ASM的字节码操作库,spring中有非常多cglib的运用,尤其是实现动态代理功能。

     Javassist:面向高级编程语言操作字节码,无须关注字节码刻板的结构,编程简单,不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。Javassist内部使用了java动态编译技术(JavaCompiler )。

     java Agent:能够在加载 Java 字节码之前进行拦截并对字节码进行修改(修改一般需借助ASM等类库)或者在运行时替换已加载的class,支持目标JVM启动时加载,也支持在目标JVM运行时加载。

下面介绍使用ASM与Javassist解决Logback bug的过程

1:ASM直接修改字节码:

    maven:

<dependency>
          <groupId>asm</groupId>
          <artifactId>asm</artifactId>
          <version>3.3.1</version>
      </dependency>

     创建方法访问器类,找到错误字节码位置,并且进行替换,这里用logback1.3.0-alpha5的字节码进行替换,该版本已修复此bug。 

import jdk.internal.org.objectweb.asm.*;

/**
 * @function:asm修改logback错误方法的字节码
 */
public class LogbackVisitor extends ClassVisitor implements Opcodes {

    public LogbackVisitor(ClassWriter classWriter) {
        super(ASM5, classWriter);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        //找到要处理的方法
        if ( "periodBarriersCrossed".equals(name) ){
            methodVisitor = new LogbackMethodVisitor(methodVisitor);
        }
        return methodVisitor;
    }

    class LogbackMethodVisitor extends MethodVisitor implements Opcodes {
        public LogbackMethodVisitor(MethodVisitor methodVisitor) {
            super(Opcodes.ASM5, methodVisitor);
        }

        @Override
        public void visitCode() {
            mv.visitCode();
            super.visitCode();
        }

        @Override
        public void visitLineNumber(int line, Label start) {
            if (line == 559){//出错字节码行数: 559-560,进行字节码替换

            }
            super.visitLineNumber(line, start);
        }
    }
}

        使用idea的ASM Bytecode Outline插件查看出错方法的字节码:

       再查看正确方法的字节码,再将正确字节码替换到visitLineNumber方法即可。

     读取出错类,注册编写的方法访问器类,并将修改后的class输出到本地目录。

public void process() throws IOException {
        //读取出错文件
        ClassReader classReader = new ClassReader("ch.qos.logback.core.rolling.helper.RollingCalendar");
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);

        //处理
        LogbackVisitor logbackVisitor = new LogbackVisitor(classWriter);
        classReader.accept(logbackVisitor, ClassReader.SKIP_DEBUG);
        byte[] data = classWriter.toByteArray();
        //保存替换结果
        Files.write(Paths.get("B:\\projects\\Result.class"), data, StandardOpenOption.CREATE);
    }

结论:使用ASM修改不够灵活,根据代码行替换灵活性太差,因为一旦logback版本升级,替换的字节码位置就错了。这里只是一个使用demo,正式使用ASM可选择覆盖整个class文件或整个方法的方式。

2:Javassist修改字节码:

     maven:

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>

    根据出错的logback源码编写正确的替换代码。

    logback源码:

public long periodBarriersCrossed(long start, long end) {
        if (start > end)
            throw new IllegalArgumentException("Start cannot come before end");

        long startFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(start, getTimeZone());
        long endFloored = getStartOfCurrentPeriodWithGMTOffsetCorrection(end, getTimeZone());

        long diff = endFloored - startFloored;

        switch (periodicityType) {

        case TOP_OF_MILLISECOND:
            return diff;
        case TOP_OF_SECOND:
            return diff / MILLIS_IN_ONE_SECOND;
        case TOP_OF_MINUTE:
            return diff / MILLIS_IN_ONE_MINUTE;
        case TOP_OF_HOUR:
            //溢出代码位置
            return (int) diff / MILLIS_IN_ONE_HOUR;
        case TOP_OF_DAY:
            return diff / MILLIS_IN_ONE_DAY;
        case TOP_OF_WEEK:
            return diff / MILLIS_IN_ONE_WEEK;
        case TOP_OF_MONTH:
            return diffInMonths(start, end);
        default:
            throw new IllegalStateException("Unknown periodicity type.");
        }
    }

用来代换的代码,保存到文件中:(这里的$0代表this, $1,$2代表方法第一,第二个参数,由于switch的枚举periodicityType动态编译时一直报找不到类,因此这里用if else代替switch)

{
    if ($1 > $2) {
        throw new IllegalArgumentException("Start cannot come before end");
    } else {
        long startFloored = this.getStartOfCurrentPeriodWithGMTOffsetCorrection($1, this.getTimeZone());
        long endFloored = this.getStartOfCurrentPeriodWithGMTOffsetCorrection($2, this.getTimeZone());
        long diff = endFloored - startFloored;
        if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_HOUR){
            return diff / 3600000L;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_DAY){
            return diff / 86400000L;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_WEEK){
            return diff / 604800000L;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_MILLISECOND){
            return diff;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_SECOND){
            return diff / 1000L;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_MINUTE){
            return diff / 60000L;
        } else if (this.periodicityType == ch.qos.logback.core.rolling.helper.PeriodicityType.TOP_OF_MONTH){
            return (long)diffInMonths($1, $2);
        } else {
            throw new IllegalStateException("Unknown periodicity type.");
        }
    }
}

编写逻辑处理代码,实现class替换:

public static void transformClass(ClassLoader classLoader) {
        String codeContent = readCodeReplacement();
        if ( ToolUtil.isNull(codeContent) ) return;

        try {
            ClassPool classPool = ClassPool.getDefault();
            classPool.appendClassPath( new LoaderClassPath(classLoader) );

            CtClass ctCls = classPool.get("ch.qos.logback.core.rolling.helper.RollingCalendar");
            CtClass paramLongCls = classPool.get("long");
            CtClass[] params = {paramLongCls, paramLongCls};

            //删除老方法
            CtMethod method = ctCls.getDeclaredMethod("periodBarriersCrossed", params);
            ctCls.removeMethod(method);

            CtMethod newMethod = new CtMethod(CtClass.longType, "periodBarriersCrossed", params, ctCls);
            //public static
            newMethod.setModifiers(Modifier.PUBLIC);
            newMethod.setBody(codeContent);

            ctCls.addMethod(newMethod);
            //加载此类,确保logback在不打破双亲委托机制前提下获取的是同一个class对象
            ctCls.toClass();
            //release
            ctCls.detach();
//           return ctCls.toBytecode();
        } catch (Exception e) {
            System.out.println("替换logback字节码失败!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            e.printStackTrace();
        }
    }

    private static String readCodeReplacement(){
        try (InputStream inputStream = LogBackCompiler.class.getClassLoader().getResourceAsStream("code-logback-periodBarriersCrossed.md")) {
            byte[] codeContentBytes = new byte[inputStream.available()];
            inputStream.read(codeContentBytes);
            return new String(codeContentBytes, Charset.defaultCharset());
        } catch (Exception ex) {
            ex.printStackTrace();
            return "";
        }
    }

     由上可见,使用Javassist比使用ASM方便得多,可读性也更强。需要说明的是上述代码并未完成,还需要transformClass生成的ByteCode替换到原class文件,这里我就直接加载已经修改的class到jvm了,这样在logback不打破双亲委托机制下从jvm获取到的是我修改后的class就能修复此bug了。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值