java修改字节码技术,Javassist修改class,ASM修改class
背景:
? ? ? ? 項目使用的Logback 1.1.11版本的類ch.qos.logback.core.rolling.helper.RollingCalendar的periodBarriersCrossed方法long轉(zhuǎn)換成int發(fā)生溢出,導(dǎo)致日志無法刪除,最終決定在不升級logback版本的前提下使用java修改字節(jié)碼技術(shù)修復(fù)此bug。
知識點:
? ? ? 提到j(luò)ava字節(jié)碼技術(shù),總是離不開ASM,cglib,Javassist, java Agent這些名詞,下面先簡單介紹下這些名詞。
? ? ? ASM:可以直接生產(chǎn) .class字節(jié)碼文件,也可以在類被加載入JVM之前動態(tài)修改類行為,ASM是在指令層次上操作字節(jié)碼的,使用難度較高。
? ? ??cglib: 基于ASM的字節(jié)碼操作庫,spring中有非常多cglib的運用,尤其是實現(xiàn)動態(tài)代理功能。
? ? ?Javassist:面向高級編程語言操作字節(jié)碼,無須關(guān)注字節(jié)碼刻板的結(jié)構(gòu),編程簡單,不需要了解虛擬機指令,就能動態(tài)改變類的結(jié)構(gòu)或者動態(tài)生成類。Javassist內(nèi)部使用了java動態(tài)編譯技術(shù)(JavaCompiler?)。
? ? ?java Agent:能夠在加載 Java 字節(jié)碼之前進行攔截并對字節(jié)碼進行修改(修改一般需借助ASM等類庫)或者在運行時替換已加載的class,支持目標(biāo)JVM啟動時加載,也支持在目標(biāo)JVM運行時加載。
下面介紹使用ASM與Javassist解決Logback bug的過程。
1:ASM直接修改字節(jié)碼:
? ? maven:
<dependency><groupId>asm</groupId><artifactId>asm</artifactId><version>3.3.1</version></dependency>? ? ?創(chuàng)建方法訪問器類,找到錯誤字節(jié)碼位置,并且進行替換,這里用logback1.3.0-alpha5的字節(jié)碼進行替換,該版本已修復(fù)此bug。?
import jdk.internal.org.objectweb.asm.*;/*** @function:asm修改logback錯誤方法的字節(jié)碼*/ 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){//出錯字節(jié)碼行數(shù): 559-560,進行字節(jié)碼替換}super.visitLineNumber(line, start);}} }? ? ? ? 使用idea的ASM Bytecode Outline插件查看出錯方法的字節(jié)碼:
? ? ? ?再查看正確方法的字節(jié)碼,再將正確字節(jié)碼替換到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();//保存替換結(jié)果Files.write(Paths.get("B:\\projects\\Result.class"), data, StandardOpenOption.CREATE);}結(jié)論:使用ASM修改不夠靈活,根據(jù)代碼行替換靈活性太差,因為一旦logback版本升級,替換的字節(jié)碼位置就錯了。這里只是一個使用demo,正式使用ASM可選擇覆蓋整個class文件或整個方法的方式。
2:Javassist修改字節(jié)碼:
? ? ?maven:
<dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.25.0-GA</version></dependency>? ? 根據(jù)出錯的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代表方法第一,第二個參數(shù),由于switch的枚舉periodicityType動態(tài)編譯時一直報找不到類,因此這里用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.");}} }編寫邏輯處理代碼,實現(xiàn)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字節(jié)碼失敗!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");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文件,這里我就直接加載已經(jīng)修改的class到j(luò)vm了,這樣在logback不打破雙親委托機制下從jvm獲取到的是我修改后的class就能修復(fù)此bug了。
總結(jié)
以上是生活随笔為你收集整理的java修改字节码技术,Javassist修改class,ASM修改class的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: logback1.1.11日志无法自动删
- 下一篇: mysql数据类型所占空间大小