Java 检测运行带字节伙伴的SpringBoot应用程序时出现不兼容的ClassChangeError
我想向我的公司介绍Byte Buddy,我已经为我的同事准备了一个演示。由于我们经常使用Spring,我认为最好的例子是SpringBoot应用程序的插装。我决定将日志添加到RestController方法中 插入指令的应用程序是一个简单的SpringBoot Hello World示例:Java 检测运行带字节伙伴的SpringBoot应用程序时出现不兼容的ClassChangeError,java,spring-boot,byte-buddy,attach-api,Java,Spring Boot,Byte Buddy,Attach Api,我想向我的公司介绍Byte Buddy,我已经为我的同事准备了一个演示。由于我们经常使用Spring,我认为最好的例子是SpringBoot应用程序的插装。我决定将日志添加到RestController方法中 插入指令的应用程序是一个简单的SpringBoot Hello World示例: @RestController public class HelloController { private static final String template = "Hello, %s!";
@RestController
public class HelloController {
private static final String template = "Hello, %s!";
@RequestMapping("/hello")
public String greeting(
@RequestParam(value = "name", defaultValue = "World") String name) {
return String.format(template, name);
}
@RequestMapping("/browser")
public String showUserAgent(HttpServletRequest request) {
return request.getHeader("user-agent");
}
}
这是我的字节好友代理:
public class LoggingAgent {
public static void premain(String agentArguments,
Instrumentation instrumentation) {
install(instrumentation);
}
public static void agentmain(String agentArguments,
Instrumentation instrumentation) {
install(instrumentation);
}
private static void install(Instrumentation instrumentation) {
createAgent(RestController.class, "greeting")
.installOn(instrumentation);
}
private static AgentBuilder createAgent(
Class<? extends Annotation> annotationType, String methodName) {
return new AgentBuilder.Default().type(
ElementMatchers.isAnnotatedWith(annotationType)).transform(
new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder
.method(ElementMatchers.named(methodName))
.intercept(
MethodDelegation
.to(LoggingInterceptor.class)
.andThen(
SuperMethodCall.INSTANCE));
}
});
}
}
当使用-javaagent参数执行此示例时,效果良好。但是,当我尝试使用Attach API在运行的JVM上加载代理时:
VirtualMachine vm = VirtualMachine.attach(args[0]);
vm.loadAgent(args[1]);
vm.detach();
我在第一次尝试记录日志时遇到以下异常:
Exception in thread "ContainerBackgroundProcessor[StandardEngine[Tomcat]]" java.lang.IncompatibleClassChangeError: Class ch.qos.logback.classic.spi.ThrowableProxy does not implement the requested interface ch.qos.logback.classic.spi.IThrowableProxy
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.subjoinExceptionMessage(ThrowableProxyConverter.java:180)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.subjoinFirstLine(ThrowableProxyConverter.java:176)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.recursiveAppend(ThrowableProxyConverter.java:159)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.throwableProxyToString(ThrowableProxyConverter.java:151)
at org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter.throwableProxyToString(ExtendedWhitespaceThrowableProxyConverter.java:35)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.convert(ThrowableProxyConverter.java:145)
at ch.qos.logback.classic.pattern.ThrowableProxyConverter.convert(ThrowableProxyConverter.java:1)
at ch.qos.logback.core.pattern.FormattingConverter.write(FormattingConverter.java:36)
at ch.qos.logback.core.pattern.PatternLayoutBase.writeLoopOnConverters(PatternLayoutBase.java:114)
at ch.qos.logback.classic.PatternLayout.doLayout(PatternLayout.java:141)
at ch.qos.logback.classic.PatternLayout.doLayout(PatternLayout.java:1)
at ch.qos.logback.core.encoder.LayoutWrappingEncoder.doEncode(LayoutWrappingEncoder.java:130)
at ch.qos.logback.core.OutputStreamAppender.writeOut(OutputStreamAppender.java:187)
at ch.qos.logback.core.OutputStreamAppender.subAppend(OutputStreamAppender.java:212)
at ch.qos.logback.core.OutputStreamAppender.append(OutputStreamAppender.java:100)
at ch.qos.logback.core.UnsynchronizedAppenderBase.doAppend(UnsynchronizedAppenderBase.java:84)
at ch.qos.logback.core.spi.AppenderAttachableImpl.appendLoopOnAppenders(AppenderAttachableImpl.java:48)
at ch.qos.logback.classic.Logger.appendLoopOnAppenders(Logger.java:270)
at ch.qos.logback.classic.Logger.callAppenders(Logger.java:257)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:421)
at ch.qos.logback.classic.Logger.filterAndLog_0_Or3Plus(Logger.java:383)
at ch.qos.logback.classic.Logger.log(Logger.java:765)
at org.slf4j.bridge.SLF4JBridgeHandler.callLocationAwareLogger(SLF4JBridgeHandler.java:221)
at org.slf4j.bridge.SLF4JBridgeHandler.publish(SLF4JBridgeHandler.java:303)
at java.util.logging.Logger.log(Unknown Source)
at java.util.logging.Logger.doLog(Unknown Source)
at java.util.logging.Logger.logp(Unknown Source)
at org.apache.juli.logging.DirectJDKLog.log(DirectJDKLog.java:181)
at org.apache.juli.logging.DirectJDKLog.error(DirectJDKLog.java:147)
at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1352)
at java.lang.Thread.run(Unknown Source)
我使用Java8在64位热点上运行该示例:
java version "1.8.0_112"
Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
字节伙伴版本为1.4.32。以下是代理maven配置:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.halun.demo.bytebuddy</groupId>
<artifactId>byte-buddy-agent-demo</artifactId>
<version>1.0</version>
<properties>
<jdk.version>1.8</jdk.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.4.32</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${project.artifactId}-${project.version}-full</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Premain-Class>pl.halun.demo.bytebuddy.logging.LoggingAgent</Premain-Class>
<Agent-Class>pl.halun.demo.bytebuddy.logging.LoggingAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
4.0.0
pl.halun.demo.bytebuddy
字节好友代理演示
1
1.8
UTF-8
org.springframework.boot
spring启动程序父级
1.4.1.1发布
net.bytebuddy
字节伙伴
1.4.32
回写
回归经典
org.springframework.boot
SpringBootStarterWeb
javax.servlet
javax.servlet-api
假如
org.apache.maven.plugins
maven编译器插件
${jdk.version}
${jdk.version}
org.apache.maven.plugins
maven汇编插件
带有依赖项的jar
${project.artifactId}-${project.version}-完整
假的
pl.halun.demo.bytebuddy.logging.LoggingAgent
pl.halun.demo.bytebuddy.logging.LoggingAgent
真的
真的
真的
集合所有
包裹
单一的
下面是插入指令的应用程序的pom文件:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pl.halun.demo.bytebuddy.instrumented.app</groupId>
<artifactId>byte-buddy-agent-demo-instrumented-app</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
4.0.0
pl.halun.demo.bytebuddy.instrumented.app
字节好友代理演示程序
1
罐子
org.springframework.boot
spring启动程序父级
1.4.1.1发布
org.springframework.boot
SpringBootStarterWeb
1.8
org.springframework.boot
springbootmaven插件
春假
https://repo.spring.io/libs-release
春假
https://repo.spring.io/libs-release
在我看来,在正在运行的服务器上添加日志是一个非常有价值的选择,我不想失去演示的这一部分。我试图尝试不同的重新定义策略,但到目前为止似乎没有任何效果。我认为,你观察到的是一个经典版本的冲突。Spring Boot很可能附带了与Java代理添加的版本不兼容的
ThrowableProxy
。在运行时加载Java代理时,Spring的版本已经加载,而启动附件在加载代理版本的类路径上预先加载代理捆绑版本
Java代理通常添加到类路径中。这也是Spring boot应用程序的所在。您需要确保Java代理不包含与应用程序的依赖项不兼容的依赖项,或者需要隐藏所有依赖项以避免此类冲突
然而,还有另一个问题:在编写运行时附加的Java代理时,在大多数JVM上会遇到额外的限制,在HotSpot上,不允许更改任何已加载类的类文件格式。还有一种可能性是,您的类已经加载到当前的位置,因为您没有启用重新传输,所以不会看到任何效果
支持运行时的代理需要使用Advice
组件,该组件将代码内联到目标代码中,而不是使用经典的委托模型:
class MyAdvice {
@Advice.OnMethodEnter
static void intercept(@Advice.BoxedArguments Object[] allArguments,
@Advice.Origin Method method) {
Logger logger = LoggerFactory.getLogger(method.getDeclaringClass());
logger.info("Method {} of class {} called", method.getName(), method
.getDeclaringClass().getSimpleName());
for (Object argument : allArguments) {
logger.info("Method {}, parameter type {}, value={}",
method.getName(), argument.getClass().getSimpleName(),
argument.toString());
}
}
}
您可以通过将上述advice类注册为访问者来使用它。此类访问者仅适用于声明的方法,即不适用于继承的方法并将其代码内联到现有方法中。这样,日志记录将不会在调用堆栈上可见,并且重新传输已加载的类也是合法的:
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(isAnnotatedWith(annotationType))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder.visit(Advice.to(MyAdvice.class).on(named(methodName)));
}
});
上面的帮助程序支持附件API通常位于不同命名空间中的其他VM
更新:这里是Spring引导的问题。Spring Boot创建自定义类装入器,该类装入器将系统类装入器(类路径)作为其父类。这些类加载器首先从系统类加载器中考虑类。添加代理时,整个Spring boot应用程序都在类装入器和这些子类装入器中。像IThrowableProxy
这样的类现在在两个类加载器中存在两次,但JVM并不认为它们相等。根据VM的状态,某些类可能已经链接到原始的IThrowableProxy
,而其他类则在附加代理后加载,并从代理链接到新的IThrowableProxy
。这两个类不相等,当VM抱怨该类没有实现正确的IThrowableProxy
(但前一个)时,会抛出您看到的错误。如果在启动时连接代理,则不存在此问题,因为始终加载类路径的IThrowableProxy
这不是一个容易修复的错误,最终,Byte Buddy无法帮助您解决此类类路径问题,Spring Boot在解释类装入器契约时非常自由。e
new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(isAnnotatedWith(annotationType))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader) {
return builder.visit(Advice.to(MyAdvice.class).on(named(methodName)));
}
});
ByteBuddyAgent.attach(agentJar, processId);
isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))