背景一切皆有因果,所有事情,都有事件驱动。本方案的日志级别切换是由这样的背景下产生的:
单个生产环境上,有几百近千个微服务
日志级别切换不重启服务,要求即时生效果
由业务开发人员去修改代码或增加相关依赖配置等涉及面广,推动进度慢
后期动态实时过滤垃圾日志,减少io和磁盘空间成本
logback简介在跟敌人发起战争之前,只有先发解敌方的情况,才能做到百战百胜。要想对logback的日志级别做动态切换,首先至少对logback做个初步的了解、和看看它有没有提供现成的实现方案。下面简单介绍一下logback跟这次需求有关的内容。
logback是java的日志开源组件,是log4j创始人写的,目前主要分为3个模块
logback-core:核心代码模块
logback-classic:log4j的一个改良版本,同时实现了slf4j的接口
logback-access:访问模块与servlet容器集成提供通过http来访问日志的功能
contextinitializer类是logback自动配置流程的逻辑实现
日志级别由logger维护和使用。其成员变量level正是由logger维护
logger中有filterandlog_0_or3plus、filterandlog_1、filterandlog_2三个不同参数的过滤日志输出方法
logger中的setlevel就是对日志级别的维护
解决方案在满头苦干之前,先了解市面上的方案。是设计师们乃至产品大佬们寻求最优解决方案的思路。
方案一:logback自动扫描更新这个方案是logback自带现成的实现,只要开启配置就可以实现所谓的日志级别动态切换。配置方法:在logback的配置文件中,增加定时扫描器即可,如:
<configuration scan="true" scanperiod="30 seconds" debug="false">
该方案可以不需要研发成本,运维人员自己配上并能使用。
它的缺点是:
每次调整扫描间隔时间都要重启服务
90%以上的扫描都是无用功,因为生产上的日志级别不可能经常有切换需求,也不允许这么做
生效不实时,如果设定在一分钟或几分钟扫描一次,那么让日志级别调整后生效就不是即时生效的,不过这个可以忽略
该方案满足不了我们的垃圾日志丢弃的需求,比如根据某些关键字丢弃日志的输出。针对这种历史原因打印很多垃圾日志的情况,考虑到时间成本,不可能让业务研发去优化。
方案二:asm动态修改字节码当然,还有其它方案,如:自己定义接口api。来直接调用logger中的setlevel方法,达到调整级别的目的;springboot的集成。
这些方案都不避免不了专主于业务开发角色的参与。
通过asm动态修改指令,该方案除了能满足调整日志级别即时生效之外。还可以满足过滤日志的需求
具体实现如下,在这里就不对asm做介绍了,不了解的同学,需要先去熟悉asm、java agent和jvm的指令:
一、idea创建maven工程
二、maven引入依赖
<dependencies> <dependency> <groupid>org.ow2.asm</groupid> <artifactid>asm</artifactid> <version>7.1</version> </dependency> <dependency> <artifactid>asm-commons</artifactid> <groupid>org.ow2.asm</groupid> <version>7.1</version> </dependency> <dependency> <groupid>com.sun</groupid> <artifactid>tools</artifactid> <version>1.8</version> <scope>system</scope> <systempath>/library/java/javavirtualmachines/jdk1.8.0_191.jdk/contents/home/lib/tools.jar</systempath> </dependency> </dependencies><build> <plugins> <plugin> <groupid>org.apache.maven.plugins</groupid> <artifactid>maven-jar-plugin</artifactid> <version>3.2.0</version> <configuration> <archive> <manifestentries> <!-- 主程序启动类 --> <agent-class> agent.logbackagentmain </agent-class> <!-- 允许重新定义类 --> <can-redefine-classes>true</can-redefine-classes> <!-- 允许转换并重新加载类 --> <can-retransform-classes>true</can-retransform-classes> </manifestentries> </archive> </configuration> </plugin> <plugin> <artifactid>maven-compiler-plugin</artifactid> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf-8</encoding> <compilerarguments> <verbose /> <!-- 将jdk的依赖jar打入项目中--> <bootclasspath>${java.home}/lib/rt.jar</bootclasspath> </compilerarguments> </configuration> </plugin> </plugins></build>
三、编写attrach启动类
package agent;import java.lang.instrument.instrumentation;import java.lang.instrument.unmodifiableclassexception;/** * @author dengbp * @classname logbackagentmain * @description attach 启动器 * @date 3/25/22 6:27 pm */public class logbackagentmain { private static string filter_class = "ch.qos.logback.classic.logger"; public static void agentmain(string agentargs, instrumentation inst) throws unmodifiableclassexception { system.out.println("agentargs:" + agentargs); inst.addtransformer(new logbackfiletransformer(agentargs), true); class[] classes = inst.getallloadedclasses(); for (int i = 0; i < classes.length; i++) { if (filter_class.equals(classes[i].getname())) { system.out.println("----重新加载logger开始----"); inst.retransformclasses(classes[i]); system.out.println("----重新加载logger完毕----"); break; } } }}
四、实现字节码转换处理器
package agent;import jdk.internal.org.objectweb.asm.classreader;import jdk.internal.org.objectweb.asm.classvisitor;import jdk.internal.org.objectweb.asm.classwriter;import java.lang.instrument.classfiletransformer;import java.security.protectiondomain;/** * @author dengbp * @classname logbackfiletransformer * @description 字节码文件转换器 * @date 3/25/22 6:25 pm */public class logbackfiletransformer implements classfiletransformer { private final string level; private static string class_name = "ch/qos/logback/classic/logger"; public logbackfiletransformer(string level) { this.level = level; } @override public byte[] transform(classloader loader, string classname, class<?> classbeingredefined, protectiondomain protectiondomain, byte[] classfilebuffer) { if (!class_name.equals(classname)) { return classfilebuffer; } classreader cr = new classreader(classfilebuffer); classwriter cw = new classwriter(cr, classwriter.compute_frames); classvisitor cv1 = new logbackclassvisitor(cw, level); /*classvisitor cv2 = new logbackclassvisitor(cv1);*/ // asm框架使用到访问模式和责任链模式 // classreader 只需要 accept 责任链中的头节点处的 classvisitor即可 cr.accept(cv1, classreader.skip_frames | classreader.skip_debug); system.out.println("end..."); return cw.tobytearray(); }}
五、实现logger元素的访问者
package agent;import jdk.internal.org.objectweb.asm.classvisitor;import jdk.internal.org.objectweb.asm.methodvisitor;import org.objectweb.asm.opcodes;/** * @author dengbp * @classname logbackclassvisitor * @description logger类元素访问者 * @date 3/25/22 5:01 pm */public class logbackclassvisitor extends classvisitor { private final string level; /** * asm版本 */ private static final int asm_version = opcodes.asm4; public logbackclassvisitor(classvisitor classvisitor, string level) { super(asm_version, classvisitor); this.level = level; } @override public methodvisitor visitmethod(int access, string name, string descriptor, string signature, string[] exceptions) { methodvisitor mv = super.visitmethod(access, name, descriptor, signature, exceptions); return new logfiltermethodvisitor(api, mv, access, name, descriptor, level); }}
六、最后实现logger关键方法的访问者
该访问者(类),实现日志级别的切换,需要对logger的三个日志过滤方法进行指令的修改。原理是把命令行入参的日志级别参数值覆盖其成员变量effectivelevelint的值,由于篇幅过大,只贴核心部分代码,请看下面:
package agent;import jdk.internal.org.objectweb.asm.label;import jdk.internal.org.objectweb.asm.methodvisitor;import jdk.internal.org.objectweb.asm.commons.adviceadapter;import org.objectweb.asm.opcodes;/** * @author dengbp * @classname logfiltermethodvisitor * @description logger类日志过滤方法元素访问者 * @date 3/25/22 5:01 pm */public class logfiltermethodvisitor extends adviceadapter { private string methodname; private final string level; private static final string filterandlog_1 = "filterandlog_1"; private static final string filterandlog_2 = "filterandlog_2"; private static final string filterandlog_0_or3plus = "filterandlog_0_or3plus"; protected logfiltermethodvisitor(int api, methodvisitor methodvisitor, int access, string name, string descriptor, string level) { super(api, methodvisitor, access, name, descriptor); this.methodname = name; this.level = level; } /** * description 在访问方法的头部时被访问 * @param * @return void * @author dengbp * @date 3:36 pm 4/1/22 **/ @override public void visitcode() { system.out.println("visitcode method"); super.visitcode(); } @override protected void onmethodenter() { system.out.println("开始重写日志级别为:"+level); system.out.println("----准备修改方法----"); if (filterandlog_1.equals(methodname)) { modifyloglevel_1(); } if (filterandlog_2.equals(methodname)) { modifyloglevel_2(); } if (filterandlog_0_or3plus.equals(methodname)) { modifyloglevel_3(); } system.out.println("重写日志级别成功...."); }
其中modifyloglevel_1(); modifyloglevel_2();modifyloglevel_3();分别对应filterandlog_1、filterandlog_2、filterandlog_0_or3plus方法指令的修改。下面只贴modifyloglevel_1的实现
/** * description 修改目标方法:filterandlog_1 * @param * @return void * @author dengbp * @date 2:20 pm 3/31/22 **/ private void modifyloglevel_1(){ label l0 = new label(); mv.visitlabel(l0); mv.visitlinenumber(390, l0); mv.visitvarinsn(opcodes.aload, 0); mv.visitldcinsn(level); mv.visitmethodinsn(opcodes.invokestatic, "ch/qos/logback/classic/level", "tolevel", "(ljava/lang/string;)lch/qos/logback/classic/level;", false); mv.visitfieldinsn(opcodes.getfield, "ch/qos/logback/classic/level", "levelint", "i"); mv.visitfieldinsn(opcodes.putfield, "ch/qos/logback/classic/logger", "effectivelevelint", "i"); label l1 = new label(); mv.visitlabel(l1); mv.visitlinenumber(392, l1); mv.visitvarinsn(opcodes.aload, 0); mv.visitfieldinsn(opcodes.getfield, "ch/qos/logback/classic/logger", "loggercontext", "lch/qos/logback/classic/loggercontext;"); mv.visitvarinsn(opcodes.aload, 2); mv.visitvarinsn(opcodes.aload, 0); mv.visitvarinsn(opcodes.aload, 3); mv.visitvarinsn(opcodes.aload, 4); mv.visitvarinsn(opcodes.aload, 5); mv.visitvarinsn(opcodes.aload, 6); mv.visitmethodinsn(opcodes.invokevirtual, "ch/qos/logback/classic/loggercontext", "getturbofilterchaindecision_1", "(lorg/slf4j/marker;lch/qos/logback/classic/logger;lch/qos/logback/classic/level;ljava/lang/string;ljava/lang/object;ljava/lang/throwable;)lch/qos/logback/core/spi/filterreply;", false); mv.visitvarinsn(opcodes.astore, 7); label l2 = new label(); mv.visitlabel(l2); mv.visitlinenumber(394, l2); mv.visitvarinsn(opcodes.aload, 7); mv.visitfieldinsn(opcodes.getstatic, "ch/qos/logback/core/spi/filterreply", "neutral", "lch/qos/logback/core/spi/filterreply;"); label l3 = new label(); mv.visitjumpinsn(opcodes.if_acmpne, l3); label l4 = new label(); mv.visitlabel(l4); mv.visitlinenumber(395, l4); mv.visitvarinsn(opcodes.aload, 0); mv.visitfieldinsn(opcodes.getfield, "ch/qos/logback/classic/logger", "effectivelevelint", "i"); mv.visitvarinsn(opcodes.aload, 3); mv.visitfieldinsn(opcodes.getfield, "ch/qos/logback/classic/level", "levelint", "i"); label l5 = new label(); mv.visitjumpinsn(opcodes.if_icmple, l5); label l6 = new label(); mv.visitlabel(l6); mv.visitlinenumber(396, l6); mv.visitinsn(opcodes.return); mv.visitlabel(l3); mv.visitlinenumber(398, l3); mv.visitframe(opcodes.f_append, 1, new object[]{"ch/qos/logback/core/spi/filterreply"}, 0, null); mv.visitvarinsn(opcodes.aload, 7); mv.visitfieldinsn(opcodes.getstatic, "ch/qos/logback/core/spi/filterreply", "deny", "lch/qos/logback/core/spi/filterreply;"); mv.visitjumpinsn(opcodes.if_acmpne, l5); label l7 = new label(); mv.visitlabel(l7); mv.visitlinenumber(399, l7); mv.visitinsn(opcodes.return); mv.visitlabel(l5); mv.visitlinenumber(402, l5); mv.visitframe(opcodes.f_same, 0, null, 0, null); mv.visitvarinsn(opcodes.aload, 0); mv.visitvarinsn(opcodes.aload, 1); mv.visitvarinsn(opcodes.aload, 2); mv.visitvarinsn(opcodes.aload, 3); mv.visitvarinsn(opcodes.aload, 4); mv.visitinsn(opcodes.iconst_1); mv.visittypeinsn(opcodes.anewarray, "java/lang/object"); mv.visitinsn(opcodes.dup); mv.visitinsn(opcodes.iconst_0); mv.visitvarinsn(opcodes.aload, 5); mv.visitinsn(opcodes.aastore); mv.visitvarinsn(opcodes.aload, 6); mv.visitmethodinsn(opcodes.invokespecial, "ch/qos/logback/classic/logger", "buildloggingeventandappend", "(ljava/lang/string;lorg/slf4j/marker;lch/qos/logback/classic/level;ljava/lang/string;[ljava/lang/object;ljava/lang/throwable;)v", false); label l8 = new label(); mv.visitlabel(l8); mv.visitlinenumber(403, l8); mv.visitinsn(opcodes.return); label l9 = new label(); mv.visitlabel(l9); mv.visitlocalvariable("this", "lch/qos/logback/classic/logger;", null, l0, l9, 0); mv.visitlocalvariable("localfqcn", "ljava/lang/string;", null, l0, l9, 1); mv.visitlocalvariable("marker", "lorg/slf4j/marker;", null, l0, l9, 2); mv.visitlocalvariable("level", "lch/qos/logback/classic/level;", null, l0, l9, 3); mv.visitlocalvariable("msg", "ljava/lang/string;", null, l0, l9, 4); mv.visitlocalvariable("param", "ljava/lang/object;", null, l0, l9, 5); mv.visitlocalvariable("t", "ljava/lang/throwable;", null, l0, l9, 6); mv.visitlocalvariable("decision", "lch/qos/logback/core/spi/filterreply;", null, l2, l9, 7); mv.visitmaxs(9, 8); mv.visitend(); }
七、最后再编写加载attach agent的加载类
import com.sun.tools.attach.virtualmachine;import java.io.ioexception;import java.io.unsupportedencodingexception;/** * @author dengbp * @classname myattachmain * @description jar 执行命令: * @date 3/25/22 4:12 pm */public class myattachmain { private static final int args_size = 2; public static void main(string[] args) { if (args == null || args.length != args_size) { system.out.println("请输入进程id和日志级别(all、trace、debug、info、warn、error、off),如:31722 info"); return; } virtualmachine vm = null; try { system.out.println("修改的进程id:" + args[0]); vm = virtualmachine.attach(args[0]); system.out.println("调整日志级别为:" + args[1]); vm.loadagent(getjar(), args[1]); } catch (exception e) { e.printstacktrace(); } finally { if (vm != null) { try { vm.detach(); } catch (ioexception e) { e.printstacktrace(); } } } } private static string getjar() throws unsupportedencodingexception { string jarfilepath = myattachmain.class.getprotectiondomain().getcodesource().getlocation().getfile(); jarfilepath = java.net.urldecoder.decode(jarfilepath, "utf-8"); int beginindex = 0; int endindex = jarfilepath.length(); if (jarfilepath.contains(".jar")) { endindex = jarfilepath.indexof(".jar") + 4; } if (jarfilepath.startswith("file:")) { beginindex = jarfilepath.indexof("file:") + 5; } jarfilepath = jarfilepath.substring(beginindex, endindex); system.out.println("jar path:" + jarfilepath); return jarfilepath; }}
八、打包执行
寻找目标程序
执行jar
java -xbootclasspath/a:/library/java/javavirtualmachines/jdk1.8.0_191.jdk/contents/home/lib/tools.jar -cp change-log-agent-1.0.1.jar myattachmain 52433 debug
java -xbootclasspath/a:/library/java/javavirtualmachines/jdk1.8.0_191.jdk/contents/home/lib/tools.jar -cp change-log-agent-1.0.1.jar myattachmain 52433 error
java -xbootclasspath/a:/library/java/javavirtualmachines/jdk1.8.0_191.jdk/contents/home/lib/tools.jar -cp change-log-agent-1.0.1.jar myattachmain 52433 info
效果
ps:如果出现校验失败(caused by: java.lang.verifyerror),请配上jvm参数:-noverify
以上就是java asm使用logback日志级别动态切换方法的详细内容。