在 Java 系统 类 中记录方法调用

Log Method Calls in Java System Classes

我正在寻找一种方法来记录对 java.nio.ByteBuffer 中所有方法的调用。

我只想知道调用了哪些方法。

这在 JMockit 中是可能的,但是从 1.47 版开始,一些非常聪明的人决定删除对私有方法的支持,而 1.46 版在 JDK 9 及更高版本中不能很好地工作。

谁能推荐一个工具?它不一定需要是一个单元测试框架,但它应该在 Eclipse 中工作。

我至少需要支持 JDK 11(最好是 JDK 13)

仅作记录,以下是适用于 JMockit 1.46 和 JDK 1.8 的代码:

import java.nio.ByteBuffer;
import org.junit.Test;
import org.slf4j.*;
import mockit.*;

public class TestFakeByteBufferAdvice {

    private static final Logger LOG = LoggerFactory.getLogger(TestFakeByteBufferAdvice.class);

    public static final class FakeByteBuffer extends MockUp<ByteBuffer> {
        @Mock
        public Object $advice(Invocation invocation) {
            LOG.info("$advice.....: {} {}", invocation.getInvokedMember(), invocation);

            return invocation.proceed();
        }
    }

    @Test
    public void getFakeByteBuffer() {

        final ByteBuffer real  = ByteBuffer.wrap("abc".getBytes());
        LOG.info("Real........: {} {}", real, real.array());

        LOG.info("MockUp......: {}",    new FakeByteBuffer());

        final ByteBuffer fake  = ByteBuffer.wrap("def".getBytes());
        LOG.info("Fake........: {} {}", fake, fake.array());
    }
}

如果这是为了学习或分析,为什么不直接使用调试器呢?我将在 IntelliJ IDEA 中向您展示一个示例:

给定 您的 main 方法或测试中某处的代码:

final ByteBuffer real  = ByteBuffer.wrap("abc".getBytes());
LOG.info("Real........: {} {}", real, real.array());

在调试器中定义一个方法断点时:

这样定义它的属性(断点不挂起运行程序但记录方法入口,其他过滤条件和日志信息也是可以的):

然后你会得到这样的控制台日志(因为它很长所以缩短了):

Connected to the target VM, address: '127.0.0.1:54734', transport: 'socket'
Method 'java.nio.ByteBuffer.allocate()' entered at java.nio.ByteBuffer.allocate(ByteBuffer.java:333)
Method 'java.nio.ByteBuffer.<init>()' entered at java.nio.ByteBuffer.<init>(ByteBuffer.java:281)
Method 'java.nio.ByteBuffer.hasArray()' entered at java.nio.ByteBuffer.hasArray(ByteBuffer.java:970)
Method 'java.nio.ByteBuffer.array()' entered at java.nio.ByteBuffer.array(ByteBuffer.java:993)
Method 'java.nio.ByteBuffer.arrayOffset()' entered at java.nio.ByteBuffer.arrayOffset(ByteBuffer.java:1021)
(...)
Method 'java.nio.ByteBuffer.wrap()' entered at java.nio.ByteBuffer.wrap(ByteBuffer.java:396)
Method 'java.nio.ByteBuffer.wrap()' entered at java.nio.ByteBuffer.wrap(ByteBuffer.java:373)
Method 'java.nio.ByteBuffer.<init>()' entered at java.nio.ByteBuffer.<init>(ByteBuffer.java:281)
Method 'java.nio.ByteBuffer.array()' entered at java.nio.ByteBuffer.array(ByteBuffer.java:993)
Method 'java.nio.ByteBuffer.toString()' entered at java.nio.ByteBuffer.toString(ByteBuffer.java:1085)
08:23:16.943 [main] INFO  d.s.s.q.TestFakeByteBufferAdvice - Real........: java.nio.HeapByteBuffer[pos=0 lim=3 cap=3] [97, 98, 99]
Method 'java.nio.ByteBuffer.hasArray()' entered at java.nio.ByteBuffer.hasArray(ByteBuffer.java:970)
Method 'java.nio.ByteBuffer.array()' entered at java.nio.ByteBuffer.array(ByteBuffer.java:993)
(...)
Method 'java.nio.ByteBuffer.arrayOffset()' entered at java.nio.ByteBuffer.arrayOffset(ByteBuffer.java:1021)
Method 'java.nio.ByteBuffer.array()' entered at java.nio.ByteBuffer.array(ByteBuffer.java:993)
Method 'java.nio.ByteBuffer.arrayOffset()' entered at java.nio.ByteBuffer.arrayOffset(ByteBuffer.java:1021)
Disconnected from the target VM, address: '127.0.0.1:54734', transport: 'socket'

Process finished with exit code 0

尝试使用断点选项并找到最能帮助您的设置。例如,您可以过滤掉某些不感兴趣的方法或指定其他条件、评估表达式并记录它们或您想到的任何内容。


2020-05-17 更新: JetBrains 花了几周时间才做出反应,但他们同意改进文档并修复一个错误:

好吧,我设法找到了解决方案。

首先,JMockit 不能很好地与 JDK13 配合使用,因此我将 JDK13 Http Client 反向移植到 JDK8。这是一个邪恶的 hack,但足以满足我的测试用例。

然后我使用了 JMockit 的稍旧版本 (1.46),因为一些喜剧演员决定不允许任何人使用私有方法伪造 类。

然后我将要跟踪的对象存储在一个列表中,这使得可以通过身份 (==) 比较从日志记录中排除不需要的对象。

JMockit 仍然可能会在某些方法上崩溃(在下面的示例中,我已将它们注释掉),因此在异常之后抑制日志记录是一个想法。
我已将此报告给 JMockit 人员: https://github.com/jmockit/jmockit1/issues/667

同样,在构建测试用例时抑制日志记录以降低输出量是有意义的。我为此使用了 AtomicBoolean。

跟踪仍然没有完全解决它,但投入 Stacktrace 使我找到了解决方案:以下调用链正在读取我的 ByteBuffers:
sun.nio.ch.write(ByteBuffer[] srcs, int offset, int length)
sun.nio.ch.write(FileDescriptor fd, ByteBuffer src, long position, NativeDispatcher nd)
java.nio.DirectByteBuffer.put(ByteBuffer 源)

DirectByteBuffer 使用了一些巧妙的技巧来读出我的 ByteBuffer。

该解决方案仅适用于我的 Http Client hack,但无论如何,仅供参考。也许其中一些会帮助其他人调试其他 类:

package http.jmockit;

import java.net.*;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublisher;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.*;
import mockit.*;

public class TestHttpIdentity {

    private static final Logger        LOG         = LoggerFactory.getLogger(TestHttpIdentity.class);
    private static final AtomicBoolean LOG_ENABLED = new AtomicBoolean();
    private static final List<Object>  TRACKED     = new ArrayList<>();

    public static final class FakeByteBuffer extends MockUp<ByteBuffer> {
        @Mock
        public Object $advice(final Invocation invocation) {

            if (TRACKED.stream().noneMatch(tracked -> tracked == invocation.getInvokedInstance())) {
                return invocation.proceed();
            }

            if (LOG_ENABLED.get()) {
                LOG    .info("$advice.invokedInstance.: {}", invocation.getInvokedInstance(), "" /* (makes signature unique)*/);
                LOG    .info("$advice.invokedMember...: {}", invocation.getInvokedMember(),   "" /* (makes signature unique)*/);
//              Thread.dumpStack(); // Use Stack Trace as last measure if needs be
            }

            Object     result = "Not available due to Exception in invocation.proceed()";
            try {
                /**/   result = invocation.proceed();
                return result;
            }
            catch (final Throwable e) {

                for (final Object arg : invocation.getInvokedArguments()) {
                    LOG.info("$advice.arg.............: {} class={}", arg,   arg == null ? "?" : arg.getClass());
                }
                LOG    .info("$advice.Result..........: {}", result);

                LOG_ENABLED.set(false);  // Disable Logging when JMockit fails

                try {Thread.sleep(100);} catch (final InterruptedException shortDelayToSyncLoggingOutput) {}

                e.printStackTrace();
                throw e;
            }
        }
    }

    public static void main(final String[] args) throws Exception {

        LOG.info("MockUp..................: {}", new FakeByteBuffer());

        final ByteBuffer[] byteBuffers  = TestBytes.asWrappedByteBuffers();

        for (final ByteBuffer byteBuffer : byteBuffers) {
            LOG.info("byteBuffer..............: {}", byteBuffer);

            final int limit    = byteBuffer.limit();
            final int position = byteBuffer.position();

            TRACKED.add(byteBuffer);  // Track Objects via their Identity (==)

            LOG.info("Test Bytes..............: {}", byteBuffers, "");
            LOG.info("byteBuffer0.array().....: {}", byteBuffer.array());
            LOG.info("byteBuffer0.capacity()..: {}", byteBuffer.capacity());
            LOG.info("byteBuffer0.get().......: {}", byteBuffer.get());
//          LOG.info("byteBuffer0.get(byte[]).: {}", byteBuffers0.get(new byte[5]));  // ClassCastException
            LOG.info("byteBuffer0.get(byte[]->) {}", byteBuffer.get(new byte[5], 0, 5));
            LOG.info("byteBuffer0.get(0)......: {}", byteBuffer.get(0));
            LOG.info("byteBuffer0.hasArray()..: {}", byteBuffer.hasArray());
            LOG.info("byteBuffer0.hasRemaining: {}", byteBuffer.hasRemaining());
            LOG.info("byteBuffer0.isDirect()..: {}", byteBuffer.isDirect());
            LOG.info("byteBuffer0.isReadOnly(): {}", byteBuffer.isReadOnly());
            LOG.info("byteBuffer0.limit().....: {}", limit);
            LOG.info("byteBuffer0.limit(0)....: {}", byteBuffer.limit(limit));
            LOG.info("byteBuffer0.mark(0).....: {}", byteBuffer.mark());
            LOG.info("byteBuffer0.order().....: {}", byteBuffer.order());
            LOG.info("byteBuffer0.position()..: {}", position);
            LOG.info("byteBuffer0.position(99): {}", byteBuffer.position(99));
            LOG.info("byteBuffer0.remaining().: {}", byteBuffer.remaining());
//          LOG.info("byteBuffer0.reset().....: {}", byteBuffers0.reset());      // -> InvalidMarkException
            LOG.info("byteBuffer0.rewind()....: {}", byteBuffer.rewind());
            LOG.info("byteBuffer0.slice().....: {}", byteBuffer.slice());

            byteBuffer.rewind();
            byteBuffer.position(position);
            byteBuffer.limit   (limit);

            LOG.info("byteBuffer..............: {}", byteBuffer);
        }
        final BodyPublisher pub = new ByteArrayBodyPublisherIterator(byteBuffers);

        LOG_ENABLED.set(false);  // Enable Logging now we've got things set up.

        final HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("http://localhost:631"))
                .headers("Content-Type", "application/ipp")
                .POST(pub)
                .build();

        HttpResponse<byte[]> response = HttpClient
                .newBuilder()
                .build()
                .send(request, HttpResponse.BodyHandlers.ofByteArray());

        LOG.info("Result......: {} {}", response, response.body());
    }
}