2016-10-23 13 views
2

Byte Buddyを私の会社に紹介したいと思います。私は同僚のデモを準備しました。私たちはSpringをたくさん使っているので、SpringBootアプリケーションのインスツルメンテーションに最適な例があると思いました。 RestControllerメソッドにログを追加することに決めました。Byte BuddyでSpringBootアプリケーションを実行する際のIncompatibleClassChangeError

計装アプリケーションは、シンプルな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"); 
    } 
} 

そして、ここでは私のバイトバディ剤である:

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パラメータを指定すると、この例はうまくいきます。

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) 

は、私が64の例を実行します。私は最初のロギングの試みで、次の例外を持って

VirtualMachine vm = VirtualMachine.attach(args[0]); 
vm.loadAgent(args[1]); 
vm.detach(); 

:しかし、私はAPIを添付して実行しているJVM上でエージェントをロードしようとすると、 Java8とビットの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> 

私の視点から、追加する非常に貴重なオプションです実行中のサーバーにログオンし、私はデモのこの部分を緩めるのが嫌いです。私はさまざまな再定義戦略を試してみましたが、今までは何も動かないようです。

答えて

0

あなたが見ているのは、古典的なバージョンの競合です。 Springブートには、Javaエージェントで追加されたバージョンと互換性がないThrowableProxyのバージョンが付属している可能性が最も高いです。実行時にJavaエージェントをロードすると、Springのバージョンはすでにロードされていますが、スタートアップ・アタッチメントは、エージェントのバージョンがロードされているクラス・パスにエージェント・バンドルのバージョンを付加します。

通常、Javaエージェントはクラスパスに追加されます。これは、あなたのSpringブートアプリケーションが生きている場所でもあります。 Javaエージェントにアプリケーションの依存関係と互換性のない依存関係が含まれていないことを確認する必要があります。また、そのような競合を避けるためにすべての依存関係をシェーディングする必要があります。

しかし、別の問題があります。ランタイムにアタッチされたJavaエージェントを作成すると、HotSpot上ですでにロードされているクラスのクラスファイル形式を変更できないほとんどのJVMで追加の制約が満たされます。また、現在の場所にクラスが既にロードされている可能性があります。再変換を有効にしないと効果が表示されません。訪問者として登録することにより、

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

、あなたが呼び出すことができますbyte-buddy-agentプロジェクトに見えます

ByteBuddyAgent.attach(agentJar, processId); 

上記のヘルパーは、添付ファイルAPIが別の名前空間に存在することが多い他のVMをサポートしています。

更新:ここでは、Spring Bootの問題があります。 Springブートは、システムクラスローダー(クラスパス)を親として持つカスタムクラスローダーを作成します。これらのクラスローダーは、まずシステムクラスローダーのクラスを考慮します。エージェントを追加すると、Springブートアプリケーション全体がクラスローダーとこれらの子クラスローダーの両方にあります。 IThrowableProxyのようなクラスは、2つのクラスローダーに2回存在しますが、JVMでは同等とはみなされません。 VMの状態によっては、元のIThrowableProxyに既にリンクされているクラスもあれば、エージェントが接続された後に他のクラスがロードされ、エージェントから新しいIThrowableProxyにリンクするクラスもあります。両方のクラスが等しくなく、VMがクラスが正しいIThrowableProxy(ただし前のクラス)を実装していないとVMが告げるところで、表示されるエラーがスローされます。エージェントが起動時に接続されている場合、クラスパスのIThrowableProxyが常にロードされるため、この問題は存在しません。

これは簡単には解決できませんが、最終的にByte Buddyはこのようなクラスパスの問題を解決することはできません。また、Springブートはクラスローダー契約の解釈においてかなり自由です。最も簡単な方法は、エージェントでSpringブートタイプを使用しないことです。あなたはまだ注釈と一致することができます

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

質問は、あなたがSpring Bootと通信する方法です。 1つの作業領域は、起動時にすべての共有クラスをクラスパスに追加することです。通常、共有クラスの使用は避けますが、ターゲットアプリケーションのクラスローダーでコードがインライン化されるAdviceクラスでのみ使用します。提供されたスコープでSpring Boot依存性を設定するだけで、アドバイスコード自体は決して実行されません。

+0

1.どちらのプロジェクトも、spring-boot-starter-webと同じlogbackクラシックバージョンのSpringBoot親と同じバージョンを使用します。このようなエラーが発生するのは私にとっては奇妙なことです。あなたが提案したように、私はエージェントPOMのログバッククラスを陰影付けしました。このような変更により、実行中のVMにエージェントを接続しようとした後に、-javaagentパラメータを指定してエージェントを実行しようとすると、ログバックの依存関係が失われてしまいました(非表示の別の依存問題のように見えません)。 –

+0

2.アドバイスでさらに重要なのは、メソッドの最大スタックサイズを超えています。ターゲットメソッドで "java.lang.VerifyError:Operand stack overflow"が発生しています。私はASMで同様の問題と解決策を見つけましたが、Byte Buddyでこの問題に対処する方法がわかりません。 Byte Buddyにサイズを再計算するように指示するにはどうすればよいですか? –

+0

OK、問題が見つかりました:https://github.com/raphw/byte-buddy/commit/60703f49aa7578661a61f6e6235bd266b264949e - キャッシュされないため、@Originメソッドを使用する人はほとんどいません(クラスとストリングの両方で安価です) 。 –

関連する問題