在运行时动态重新定义类

huangapple go评论106阅读模式
英文:

Dynamic class redefinition at runtime

问题

我最近一直在使用Java Instrumentation API和Byte Buddy进行实验。我的目标是改变已加载类的行为。我已经成功地更改了现有方法,但是在添加全新方法时遇到了问题。

第一种方法:

public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
    System.out.println("[Agent] In agentmain/premain method");

    Class<?> clazz = Class.forName("com.example.instrumentation.agent.AppService");

    inst.addTransformer(new AppServiceTransformer(), true);
    inst.retransformClasses(clazz);
}
public class AppServiceTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] byteCode = null;
        System.out.println("Transformation");
        try {
            byteCode = new ByteBuddy()
                .redefine(classBeingRedefined)
//                .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
//                .intercept(FixedValue.value("This is a message from the ByteBuddy hacker !!!"))
                .method(named("getAnswer"))
                .intercept(FixedValue.value("Service has been hacked :)"))
                .make()
                .getBytes();
        } catch (Throwable e) {
            System.err.println(e);
            System.err.println("Failed to transform");
        }
        return byteCode;
    }
}

上述代码在我将代理附加到已运行的VM时有效,它改变了指定方法的行为。然而,当我取消对定义新方法的代码的注释时,我得到了一个异常:

Exception in thread "main" com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize

我尝试将上述示例作为在应用程序启动时加载的premain代理运行。对于此情况,更改方法的行为有效,但是添加新方法会抛出:

    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

第二种方法:

public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
    System.out.println("[Agent] In agentmain/premain method");

    new AgentBuilder.Default()
        .type(named("com.jarek.example.instrumentation.agent.AppService"))
        .transform(new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
                System.out.println("Entered transform");
                return builder.method(named("getAnswer"))
                    .intercept(FixedValue.value("Service has been hacked :)"))
                    .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
                    .intercept(FixedValue.value("This is experimental feature"));
            }
        })
        .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
        .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
        .installOn(inst);
}

我可以在控制台中看到代理已进入transform方法,但新方法未添加到类中,现有方法的行为也未更改。将此解决方案用作premain代理在这两种情况下都能正常工作。

第三种方法:

public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
    System.out.println("[Agent] In agentmain/premain method");

    Class<?> clazz = Class.forName("com.example.instrumentation.agent.AppService");

    ByteBuddyAgent.install();
    new ByteBuddy()
        .redefine(clazz)
//        .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
//        .intercept(FixedValue.value("This is a message from the ByteBuddy hacker !!!"))
        .method(named("getAnswer"))
        .intercept(FixedValue.value("Service has been hacked :)"))
        .make()
        .load(clazz.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
}

这种情况只适用于用于更改现有方法的premain代理。尝试将此作为代理附加到已运行的应用程序时,什么也不会发生。

  1. 有人知道我一直在努力做的事情是否可能实现吗?
  2. 以上哪种方法是正确的,为什么它们的行为不同?
  3. 如何可能改变已加载到VM中的类的行为?这个机制有没有特定的名称?我尝试在互联网上找了一些信息,但是找不到任何相关信息。
  4. 也许其他库如JavaAssist或ASM更适合这种情况吗?
英文:

I have been playing recently with java instrumentation API and a byte buddy. My goal is to change the behavior of an already loaded class. I was able to change the existing method but I`ve failed with adding a completely new one.

First approach:

public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
    System.out.println((&quot;[Agent] In agentmain/premain method&quot;));

    Class&lt;?&gt; clazz = Class.forName(&quot;com.example.instrumentation.agent.AppService&quot;);

    inst.addTransformer(new AppServiceTransformer(), true);
    inst.retransformClasses(clazz);
}
public class AppServiceTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class&lt;?&gt; classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] byteCode = null;
        System.out.println(&quot;Transformation&quot;);
        try {
            byteCode = new ByteBuddy()
                .redefine(classBeingRedefined)
//                .defineMethod(&quot;getExperimental&quot;, String.class, Opcodes.ACC_PUBLIC)
//                .intercept(FixedValue.value(&quot;This is a message from the ByteBuddy hacker !!!&quot;))
                .method(named(&quot;getAnswer&quot;))
                .intercept(FixedValue.value(&quot;Service has been hacked :)&quot;))
                .make()
                .getBytes();
        } catch (Throwable e) {
            System.err.println(e);
            System.err.println(&quot;Failed to transform&quot;);
        }
        return byteCode;
    }
}

Above code works, when I attach this agent to an already running VM it alters the behavior of the specified method. However when I uncomment the code responsible for defining a new method what I get is a

Exception in thread &quot;main&quot; com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize

I tried running above example as a premain agent loaded at application start-up. For this case altering the behaviour of a the method works but adding a new one throws

Failed to transform
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:567)
	at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
	at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

Second approach:

    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println((&quot;[Agent] In agentmain/premain method&quot;));

        new AgentBuilder.Default()
            .type(named(&quot;com.jarek.example.instrumentation.agent.AppService&quot;))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder&lt;?&gt; transform(DynamicType.Builder&lt;?&gt; builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
                    System.out.println(&quot;Entered transform&quot;);
                    return builder.method(named(&quot;getAnswer&quot;))
                        .intercept(FixedValue.value(&quot;Service has been hacked :)&quot;))
                        .defineMethod(&quot;getExperimental&quot;, String.class, Opcodes.ACC_PUBLIC)
                        .intercept(FixedValue.value(&quot;This is experimental feature&quot;));
                }
            })
            .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
            .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
            .installOn(inst);
    }

I can see in the console that the agent have entered the transform method, however the new method isn`t added to the class and the behaviour of the existing one is not altered.
Using this solution as a premain agent works perfectly in both cases.

Third approach:

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println((&quot;[Agent] In agentmain/premain method&quot;));

        Class&lt;?&gt; clazz = Class.forName(&quot;com.example.instrumentation.agent.AppService&quot;);

        ByteBuddyAgent.install();
        new ByteBuddy()
            .redefine(clazz)
//            .defineMethod(&quot;getExperimental&quot;, String.class, Opcodes.ACC_PUBLIC)
//            .intercept(FixedValue.value(&quot;This is a message from the ByteBuddy hacker !!!&quot;))
            .method(named(&quot;getAnswer&quot;))
            .intercept(FixedValue.value(&quot;Service has been hacked :)&quot;))
            .make()
            .load(clazz.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
}

This case works only for the premain agent for altering an existing method. The attempt to add a new method throws

Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

When attaching this as a agent to already running application nothing happens.

  1. Does anybody knows if this is possible to achieve what I`ve been trying too ?
  2. Which of those approaches is correct and why do they behave differently ?
  3. How this is it possible that We can change the behaviour of a class which is already loaded to the VM?
    Has this mechanism some specific name? I've tried to look for some information on the internet but couldn't find anything.
  4. Perhaps some other library i.e. JavaAssist or ASM would be better suited for this case?

答案1

得分: 2

使用 AgentBuilder,您应该注册一个 Listener,以查看在重新转换过程中是否发生错误。您可能应该设置 .disableClassFormatChanges(),因为普通的 JVM 不支持向已定义的类添加方法或字段。

如今,添加字段或方法是不可能的,只有 代码演进 VM 在今天支持此功能,而且这个功能是否会被纳入 OpenJDK 尚存疑虑。

英文:

With an AgentBuilder, you should register a Listener to see if errors happen during retransformation. You probably should set .disableClassFormatChanges() as the average JVM does not support adding methods or fields to a class that already is defined.

Adding a field or method is impossible as it is today, only the code evolution VM supports it as of today and it is doubtful if this feature ever makes it to OpenJDK.

答案2

得分: -1

因为您的要求是构建一个框架,用于收集应用程序指标。首先,有一些工具,比如VisualVM,它可以帮助您从正在运行的Java应用程序中获取指标并查看洞见。这将完全与您的应用程序外部,并且不需要任何代码更改。

如果您想对指标有更大的控制权,您可以使用Spring Boot Admin,它可以在不需要任何代码更改的情况下实时为您处理。Spring Boot Admin中有大量的功能。

英文:

Since your requirement is to build a framework using which you can collect application metrics. Firstly, there are tools like VisualVM which helps you get metrics from a running java application and see the insights. This will be completely external to your application and won't require any code changes.

If you want a greater control over the metrics, you may onboard Spring Boot Admin and this will do it realtime for you without any code changes. There are plethora of features present in Spring Boot Admin.

huangapple
  • 本文由 发表于 2020年4月5日 18:20:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/61041092.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定