使用Java代理快速转换一个类

问题描述 投票:0回答:1

我想测量服务器的启动时间而没有相当大的开销。

我真正想要衡量的是从服务器进程执行到服务器开始侦听一个众所周知的端口的时间。

例如,我想测量一个简单的Netty Server的启动时间。即从启动到准备接受请求的时间。

我使用Byte-Buddy开发了一个Java代理。

public class Agent {

public static void premain(String arg, Instrumentation instrumentation) {
    new AgentBuilder.Default()
            .type(ElementMatchers.named("io.netty.bootstrap.AbstractBootstrap"))
            .transform((builder, typeDescription, classLoader, javaModule) ->
                    builder.visit(Advice.to(TimeAdvice.class)
                            .on(ElementMatchers.named("bind").and(ElementMatchers.takesArguments(SocketAddress.class)))))
            .installOn(instrumentation);
}
}

以下是TimeAdvice的源代码

public class TimeAdvice {

@Advice.OnMethodExit
static void exit(@Advice.Origin String method) {
    System.out.println(String.format("Server started. Current Time (ms): %d", System.currentTimeMillis()));
    System.out.println(String.format("Server started. Current Uptime (ms): %d",
            ManagementFactory.getRuntimeMXBean().getUptime()));
}
}

使用此代理,启动时间约为1400毫秒。但是,当我通过修改服务器代码来测量启动时间时,服务器的启动时间约为650毫秒。

因此,在考虑启动时间时,似乎在使用byte-buddy Java代理时会产生相当大的开销。

我还尝试使用Javassist的另一个Java代理。

public class Agent {

private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

public static void premain(String arg, Instrumentation instrumentation) {
    instrumentation.addTransformer((classLoader, s, aClass, protectionDomain, bytes) -> {
        if (NETTY_CLASS.equals(s)) {
            System.out.println(aClass);
            long start = System.nanoTime();
            // Javassist
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("io.netty.bootstrap.AbstractBootstrap");
                CtMethod m = cc.getDeclaredMethod("bind", new CtClass[]{cp.get("java.net.SocketAddress")});
                m.insertAfter("{ System.out.println(\"Server started. Current Uptime (ms): \" + " +
                        "java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime());}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                System.out.println(String.format("Agent - Transformation Time (ms): %d", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
            }
        }

        return null;
    });
}
}

使用此代理,启动时间约为800毫秒。

如何最小化开销并测量启动时间?有没有办法直接转换特定的类而不通过所有类?如果我可以直接转换一个类,我想我应该能够尽可能地减少开销。

java instrumentation javassist javaagents byte-buddy
1个回答
2
投票

由于您使用的是premain,因此您可能会测量许多之前未使用过的类的加载和初始化时间。完全可能的是,在应用程序第一次使用它们时,无论如何都会加载和初始化大量这些类,而不是作为“启动时间”进行测量,因此将此时间转移到测量的启动时间时间可能不是一个实际问题。

请注意,您在两个变体中都使用lambda表达式,这会导致JRE提供的后端初始化。在OpenJDK的情况下,它使用ASM,但由于它已被重新打包以避免与使用ASM的应用程序发生冲突,因此Byte-Buddy在内部使用的类别不同,因此您需要支付两次初始化ASM的费用。

如上所述,如果这些类仍然会被使用,即如果应用程序稍后将使用lambda表达式或方法引用,则不应该担心这一点,因为“优化”它只会将初始化转移到以后的时间。但是如果应用程序没有使用lambda表达式或方法引用,或者你想不惜一切代价从测量的启动时间中删除这个时间跨度,你可以使用普通的接口实现,使用内部类或让Agent实现接口。

为了进一步减少启动时间,您可以直接使用ASM,跳过Byte-Buddy类的初始化,例如,

import java.lang.instrument.*;
import java.lang.management.ManagementFactory;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

public class Agent extends ClassVisitor implements ClassFileTransformer {
    private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

    public static void premain(String arg, Instrumentation instrumentation) {
        instrumentation.addTransformer(new Agent());
    }

    public Agent() {
        super(Opcodes.ASM5);
    }
    public byte[] transform(ClassLoader loader, String className, Class<?> cl,
                            ProtectionDomain pd, byte[] classfileBuffer) {
        if(!NETTY_CLASS.equals(className)) return null;

        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, 0);
        synchronized(this) {
            super.cv = cw;
            try { cr.accept(this, 0); }
            finally { super.cv = null; }
        }
        return cw.toByteArray();
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if(name.equals("bind")
        && desc.equals("(Ljava/net/SocketAddress;)Lio/netty/channel/ChannelFuture;")) {
            return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                @Override
                protected void onMethodExit(int opcode) {
                    super.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Agent.class.getName().replace('.', '/'),
                        "injectedMethod", "()V", false);
                    super.onMethodExit(opcode);
                }
            };
        }
        return mv;
    }
    public static void injectedMethod() {
        System.out.printf("Server started. Current Time (ms): %d",
                          System.currentTimeMillis());
        System.out.printf("Server started. Current Uptime (ms): %d",
                          ManagementFactory.getRuntimeMXBean().getUptime());
    }
}

(未测试)

显然,这个代码比使用Byte-Buddy的代码更复杂,所以你必须决定做出哪些权衡。

ASM已经非常轻巧。更深入的意思是仅使用ByteBufferHashMap进行类文件转换;这是可能的,但肯定不是你想去的道路......

© www.soinside.com 2019 - 2024. All rights reserved.