如何在无Spring依赖的ByteBuddy JavaAgent中拦截@RestController类
哈哈,这个需求我太熟了——既要精准拦截Spring的@RestController类,又不想让Agent绑死Spring依赖,核心就是绕开类加载,直接在字节码层面做注解检测。下面给你拆解具体实现步骤:
核心思路
我们不需要引入Spring的任何依赖包,只需要用@RestController的全限定名字符串"org.springframework.web.bind.annotation.RestController"作为匹配标识,通过字节码操作工具(比如ByteBuddy或ASM)在类加载前扫描类的注解元数据,判断是否需要添加Advice。
具体实现(推荐用ByteBuddy,省心高效)
ByteBuddy是JavaAgent场景下最易用的字节码库,它的匹配器支持直接通过注解全限定名匹配,而且完全不会触发Spring注解类的加载。
1. 编写Agent核心逻辑
import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.matcher.ElementMatchers; import java.lang.instrument.Instrumentation; public class RestControllerLoggingAgent { public static void premain(String agentArgs, Instrumentation inst) { new AgentBuilder.Default() // 匹配带有@RestController注解的类 .type(ElementMatchers.isAnnotatedWith( ElementMatchers.named("org.springframework.web.bind.annotation.RestController") )) // 给匹配类的所有非静态方法添加日志Advice .transform((builder, typeDesc, classLoader, module) -> builder.method(ElementMatchers.not(ElementMatchers.isStatic())) .intercept(MethodDelegation.to(LoggingAdvice.class)) ) .installOn(inst); } }
2. 编写日志Advice类
这个类是实际做日志逻辑的地方,要符合ByteBuddy的Advice规范:
import java.lang.reflect.Method; import net.bytebuddy.asm.Advice; public class LoggingAdvice { @Advice.OnMethodEnter public static void logEntry(@Advice.Origin Method method) { System.out.printf("[Agent] Entering %s#%s%n", method.getDeclaringClass().getName(), method.getName()); } @Advice.OnMethodExit(onThrowable = Throwable.class) public static void logExit(@Advice.Origin Method method, @Advice.Thrown Throwable throwable) { if (throwable == null) { System.out.printf("[Agent] Exiting %s#%s%n", method.getDeclaringClass().getName(), method.getName()); } else { System.out.printf("[Agent] Exiting %s#%s with exception: %s%n", method.getDeclaringClass().getName(), method.getName(), throwable.getMessage()); } } }
用ASM手动实现的思路(更轻量,适合追求极小体积的场景)
如果不想引入ByteBuddy依赖,可以用JDK自带的ASM库手动解析类字节码:
1. 自定义ClassFileTransformer
import org.objectweb.asm.ClassReader; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Opcodes; import java.lang.instrument.ClassFileTransformer; import java.security.ProtectionDomain; public class RestControllerTransformer implements ClassFileTransformer { private static final String REST_CONTROLLER_ANNOTATION = "Lorg/springframework/web/bind/annotation/RestController;"; @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { // 先判断类是否带有@RestController注解 ClassReader cr = new ClassReader(classfileBuffer); boolean isRestController = false; cr.accept(new AnnotationVisitor(Opcodes.ASM9) { @Override public void visit(String name, Object value) { if (REST_CONTROLLER_ANNOTATION.equals(name)) { isRestController = true; } super.visit(name, value); } }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); if (isRestController) { // 这里可以用ASM的ClassWriter给类的方法添加日志逻辑 // 具体ASM修改方法的代码可以参考官方文档,核心是插入日志字节码 return modifyClassWithLogging(classfileBuffer); } return classfileBuffer; } private byte[] modifyClassWithLogging(byte[] classfileBuffer) { // 实现ASM修改类的逻辑,比如给方法前后插入日志 // 示例代码省略,可参考ASM的MethodVisitor相关API return classfileBuffer; } }
关键注意事项
- 不要触发Spring类加载:绝对不能用
Class.forName("org.springframework.web.bind.annotation.RestController"),否则会强制Agent依赖Spring,甚至在非Spring环境下报错。 - 打包注意:用ByteBuddy的话,打包Agent时可以用ShadowJar把ByteBuddy核心类打包进去,但不要包含任何Spring类。
- 类加载器兼容:Spring应用可能用自定义类加载器(比如Tomcat的WebappClassLoader),ByteBuddy默认会处理类加载器隔离问题,ASM需要额外注意确保能正确读取类字节码。
内容的提问来源于stack exchange,提问作者Solve Johnsen
相关产品推荐
相关产品推荐

