背景:

项目使用的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);}@Overridepublic 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);}@Overridepublic void visitCode() {mv.visitCode();super.visitCode();}@Overridepublic 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 staticnewMethod.setModifiers(Modifier.PUBLIC);newMethod.setBody(codeContent);ctCls.addMethod(newMethod);//加载此类,确保logback在不打破双亲委托机制前提下获取的是同一个class对象ctCls.toClass();//releasectCls.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了。

java修改字节码技术,Javassist修改class,ASM修改class相关推荐

  1. OpenRasp Java运行时修改字节码技术

    Java运行时修改字节码技术 Java运行时动态修改字节码技术,常用的有javassist asm来实现.不过最近在分析openrasp-java这块时,程序使用的javassist来动态插桩关键类, ...

  2. trace java_使用java动态字节码技术简单实现arthas的trace功能。

    参考资料 用过[Arthas]的都知道,Arthas是alibaba开源的一个非常强大的Java诊断工具. 不管是线上还是线下,我们都可以用Arthas分析程序的线程状态.查看jvm的实时运行状态.打 ...

  3. JAVA的字节码技术

    1.什么是字节码? 字节码 byteCode JVM能够解释执行的.java程序的归宿,但是从规范上来讲和Java已没有任何关系了.一些动态语言也可以编译成字节码在JVM上运行.字节码就相当于JVM上 ...

  4. 【Java 虚拟机原理】动态字节码技术 | Dalvik ART 虚拟机 | Android 字节码打包过程

    文章目录 一.动态字节码技术 二.Dalvik & ART 虚拟机 三.Android 字节码打包过程 总结 一.动态字节码技术 动态字节码技术 就是在 运行时 , 动态修改 Class 字节 ...

  5. java探针 字节码增强_深入浅出Java探针技术1--基于java agent的字节码增强案例

    Java agent又叫做Java 探针,本文将从以下四个问题出发来深入浅出了解下Java agent 一.什么是java agent? Java agent是在JDK1.5引入的,是一种可以动态修改 ...

  6. 框架手写系列---javassist修改字节码方式,实现美团Robust热修复框架

    本文用javassist方式,模拟美团Robust插件的前置处理:用插入代码的方式,针对apk中的每个方法都插入一段静态代码判断语句,用于控制是否启用热修复fix(也就是动态加载patch包到原apk ...

  7. java 修改字节码_使用JBE(Java Bytecode Editor)修改Java字节码

    JBE JBE(Java Bytecode Editor)是一个Java字节码编辑工具,而且是开源的,该项目是基于jclasslib ej-technologies(https://github.co ...

  8. Java字节码技术(二)字节码增强之ASM、JavaAssist、Agent、Instrumentation

    文章目录 前言 从AOP说起 静态代理 动态代理 JavaProxy CGLIB 字节码增强实现AOP ASM JavaAssist 运行时类加载 Instrumentation接口 JavaAgen ...

  9. Java 字节码技术:不积细流,无以成江河

    Java 中的字节码,英文名为 bytecode, 是 Java 代码编译后的中间代码格式.JVM 需要读取并解析字节码才能执行相应的任务. 从技术人员的角度看,Java 字节码是 JVM 的指令集. ...

最新文章

  1. Vivado中TCL的使用
  2. NYOJ 636 世界末日
  3. MySQL:常见错误01
  4. MongoDB compact 命令详解
  5. mysql并发 node_nodejs写入mysql单次数据量过大的解决方法_沃航科技
  6. Qt5\MinGw编译器快速解决-QMYSQL driver not loaded问题
  7. Jquery的知识图谱
  8. java流作为参数,java-8 – 将Java 8流映射函数作为参数传递
  9. php手册 mac版,PHP中文手册for mac-PHP中文手册Mac版下载 V1.0.2-PC6苹果网
  10. docker安装elasticsearch教程
  11. 什么模拟器可以完全模拟真机_什么是模拟器?
  12. 阿里云服务器购买价格表:国内和国外地域云服务器活动报价表
  13. 用scrapy-splash爬取淘宝
  14. gnu assembler最新官方手册和.macro介绍
  15. 51单片机-TLC5615代码
  16. 整理2008-2017年的所有上市公司海外收入数据
  17. 微信开发网页授权获取用户信息
  18. nyoj239 月老的难题 (匈牙利算法,最大匹配,邻接表)
  19. 项目管理100问 | NO.6 如何为项目制定里程碑?
  20. java星座测试需求分析_如何进行软件测试需求分析

热门文章

  1. php要字符串的后四位,php如何截取字符串后四位
  2. php数组修改键值,php数组中子数组如何修改键值
  3. lamda list 分组_java8lambda表达式对集合分组并且排序(记一次性能优化案例)
  4. angular select设置默认选中_技术分享 | Charset 和 Collat??ion 设置对 MySQL 性能的影响...
  5. macos 此服务器的证书无效_跨平台本地SSL证书生成工具,本地也能优雅的调试https...
  6. dataset中的数据批量导入oracle数据库,c#如何将dataset中的数据批量导入oracle数据库...
  7. mysql高精度类型_mysql中常见的数据类型
  8. 方舟服务器维护公告11月19日,明日方舟11月19日10点停机维护 更新内容一览
  9. android 字符串转bitmap,android – 如何将Base64字符串转换为BitMap图像显示在ImageView?...
  10. 计算机组成原理设计一个累加和,组成原理课设关于累加器.doc