2

我想向我的公司介绍 Byte Buddy,我已经为我的同事准备了一个演示。由于我们经常使用 Spring,我认为最好的例子是 SpringBoot 应用程序的检测。我决定将日志添加到 RestController 方法。

Instrumented 应用程序是一个简单的 SpringBoot Hello World 示例:

@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");
  }
}

这是我的 Byte Buddy 代理:

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));
                    }
                });
    }
}

拦截器记录方法执行:

public static void intercept(@AllArguments Object[] allArguments,
        @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());
    }
}

当使用 -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 位 HotSpot 上运行示例:

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>

这是检测应用程序的 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

1 回答 1

0

我认为您观察到的是经典版本冲突。Spring Boot 很可能带有与ThrowableProxy随 Java 代理添加的版本不兼容的版本。在运行时加载 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());
    }
  }
}

您可以通过将其注册为访问者来使用上述建议类。此类访问者仅适用于声明的方法,即不适用于继承的方法并将其代码内联到现有方法中。这样,日志在调用堆栈上将不可见,并且重新转换已加载的类也变得合法:

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)));
      }
   });

至于附件,请查看byte-buddy-agent允许您调用的项目:

ByteBuddyAgent.attach(agentJar, processId);

上述帮助程序支持附件 API 通常位于不同命名空间中的其他 VM。

更新:这是 Spring Boot 的问题。Spring Boot 创建以系统类加载器(类路径)为父的自定义类加载器。这些类加载器首先考虑来自系统类加载器的类。添加代理时,整个 Spring Boot 应用程序都在类加载器和这些子类加载器中。像IThrowableProxy现在这样的类在两个类加载器中存在两次,但 JVM 认为它们不相等。根据 VM 的状态,一些类可能已经链接到原始IThrowableProxy类,而其他类在附加代理后加载并IThrowableProxy从代理链接到新的类。两个类不相等,并且在 VM 抱怨该类未实现正确的情况下抛出您看到的错误IThrowableProxy(但前一个)。如果代理在启动时附加,则不会存在此问题,因为IThrowableProxy始终加载类路径。

这不是一个容易修复的错误,最后,Byte Buddy 无法帮助您解决此类路径问题,而且 Spring Boot 对类加载器合约的解释非常自由。最简单的方法是不在代理中使用 Spring Boot 类型。您仍然可以将注释与例如

isAnnotatedWith(named("org.springframework.web.bind.annotation.RestController"))

问题是如何与 Spring Boot 通信。一种解决方法是在启动时将所有共享类添加到类路径中。通常,我确实完全避免使用共享类,但只Advice在目标应用程序的类加载器中内联代码的类中使用它们。只需在提供的范围内设置 Spring Boot 依赖项,建议代码本身就不会执行。

于 2016-10-24T08:05:34.710 回答