背景:
项目使用的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了。