我想在 java 13+ 中检测本机方法(最好使用 javassist)。假设我想为特定方法添加日志记录,然后调用真正的本机方法。之前可以通过本机方法前缀(问题https://bugs.openjdk.org/browse/JDK-6263317和我的答案以及使用javassist的可能解决方案使用javassist编辑本机方法类?),但自J13以来它是如果没有显式选项,则无法添加新方法(问题https://bugs.openjdk.java.net/browse/JDK-8221528),该方法从一开始就被弃用,可以随时删除。
这个话题有很多问题,例如:
但它们都没有涵盖如何使用 javassist 在 J13+ 中包装没有
-XX:+AllowRedefinitionToAddDeleteMethods
选项的本机方法。我可以替换方法体,但看不到下一步的好动作。在纯 java 中实现每个有趣的本机方法是非常困难或不可能的,并且扩展性不好,所以我不能只是替换方法。可能是我之前尝试使用 javassist 时做错了什么。或者我唯一的选择可能是使用 JNA 以某种方式从 jdk 库调用任意函数。
不完全是完整的答案,但也许会对某人有所帮助。我想在纯java中没有通用的解决方案,因为函数名称并不总是遵循JNI命名约定,并且可以通过
RegisterNatives
手动注册。所以你不能只在运行时为 JNA 创建接口并使用它。如果您不害怕 C/++,最重要的是管理库分发、特定平台编译和加载,您可以尝试从 Method(不是 java Method,参见 RegisterNatives
实现)对象查找指向本机函数的指针,从java签名,然后使用JNI/JNA调用它。话虽这么说,我没有找到纯java解决方案。不过,如果您知道相应的本机方法,您可以尝试一些方法。
例如,让我们看一下
Thread.sleep
方法(我最初想对其进行检测)。
等几年。现在它不再是原生的,您可以对其进行检测!问题解决了!
用纯java替换方法。您可以搜索类似的方法(如
parkNanos
)并围绕它构建解决方案(也处理“无原因”部分)或使用实用程序线程中的等待和调度任务来阻止当前线程,以通过通知唤醒当前线程。凌乱的。并且在中断期间你可以得到不同的异常。当然有一个问题 - 如何找到替代品以及如果您也想检测替代品该怎么办。
我使用了这个方法并有 2 个分支 - 是否启用了AllowRedefinitionToAddDeleteMethods。现在我需要考虑第一个解决方案并添加第三个分支。
假设您找到了本机方法名称,它是
JVM_Sleep
。现在你可以使用JNA来调用它了。只需添加依赖项,然后声明接口,如下所示:
public interface ThreadWrapper extends com.sun.jna.Library {
void JVM_Sleep(JNIEnv env, Class thisObj, long nanos) throws InterruptedException;
}
然后这样称呼它:
ThreadWrapper lib = com.sun.jna.Native.load("jvm", ThreadWrapper.class, Collections.singletonMap(Library.OPTION_ALLOW_OBJECTS, Boolean.TRUE));
long nanos = MILLISECONDS.toNanos(millis);
lib.JVM_Sleep(JNIEnv.CURRENT, Thread.class, nanos);
简单的解决方案,但需要了解方法名称、新的相当重的依赖项,并引入 JNA 的额外开销。 哦,还有一件事 -
JVM_Sleep
更改了版本之间的合同,因此它在旧版本中接收 millis,在新版本中接收 nanos。当然,您应该意识到这一点并为此拥有 if
。
Java现在有了外部函数API,我们可以用它来调用JNI方法。 主要优点 - 新的 API 不需要任何依赖项。 主要缺点 - 新的 API 并没有真正与旧的 JNI 函数建立良好而干净的桥梁。 您仍然可以这样做(错误处理超出了示例的范围):
public void invokeJvmSleepFull(long millis) {
System.out.println("Start " + new Date());
try (Arena arena = Arena.ofConfined()) {
Linker linker = Linker.nativeLinker();
SymbolLookup jvmLib = SymbolLookup.libraryLookup("jvm", arena);
// use JNI_GetCreatedJavaVMs to get address of running VM
MemorySegment jniGetCreatedVmsAddr = jvmLib.find("JNI_GetCreatedJavaVMs").get();
FunctionDescriptor jniGetCreatedVmsSig =
FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT,
ValueLayout.ADDRESS
);
MethodHandle jniGetCreatedVms = linker.downcallHandle(jniGetCreatedVmsAddr, jniGetCreatedVmsSig);
MemorySegment writtenVmsCount = arena.allocate(ValueLayout.OfInt.JAVA_INT, 0);
MemorySegment writtenVms = arena.allocateArray(ValueLayout.ADDRESS, 2);
int code = (int) jniGetCreatedVms.invokeExact(writtenVms, 2, writtenVmsCount);
if (code != JNI_OK) {
throw new RuntimeException("Unexpected code " + code);
}
MemorySegment jvmAddress = writtenVms.get(ValueLayout.ADDRESS, 0);
// navigate through JavaVM_ -> functions (aka JNIInvokeInterface_) -> GetEnv to get Env address
StructLayout JavaVM_Layout = MemoryLayout.structLayout(
POINTER.withName("functions")
).withName("JavaVM_");
long functionsOffset = JavaVM_Layout.byteOffset(MemoryLayout.PathElement.groupElement("functions"));
MemorySegment functionsAddr = jvmAddress.reinterpret(JavaVM_Layout.byteSize()).get(ValueLayout.ADDRESS, functionsOffset);
StructLayout JNIInvokeInterface_Layout = MemoryLayout.structLayout(
POINTER.withName("reserved0"),
POINTER.withName("reserved1"),
POINTER.withName("reserved2"),
POINTER.withName("DestroyJavaVM"),
POINTER.withName("AttachCurrentThread"),
POINTER.withName("DetachCurrentThread"),
POINTER.withName("GetEnv"),
POINTER.withName("AttachCurrentThreadAsDaemon")
).withName("JNIInvokeInterface_");
long getEnvOffset = JNIInvokeInterface_Layout.byteOffset(MemoryLayout.PathElement.groupElement("GetEnv"));
MemorySegment jniGetEnvAddr = functionsAddr.reinterpret(JNIInvokeInterface_Layout.byteSize()).get(ValueLayout.ADDRESS, getEnvOffset);
FunctionDescriptor jniGetEnvSig =
FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_INT
);
MethodHandle jniGetEnv = linker.downcallHandle(jniGetEnvAddr, jniGetEnvSig);
MemorySegment envAddress = arena.allocate(ValueLayout.ADDRESS);
code = (int) jniGetEnv.invokeExact(jvmAddress, envAddress, JNI_VERSION_21);
if (code != JNI_OK) {
throw new RuntimeException("Unexpected code " + code);
}
envAddress = envAddress.get(ADDRESS, 0);
// use env to call JVM_Sleep. It does not use class so just pass null instead.
MemorySegment jvmSleepAddr = jvmLib.find("JVM_Sleep").get();
FunctionDescriptor jvmSleepSig =
FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_LONG);
MethodHandle threadSleep = linker.downcallHandle(jvmSleepAddr, jvmSleepSig);
threadSleep.invokeExact(envAddress, MemorySegment.NULL, MILLISECONDS.toNanos(millis));
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println("End " + new Date());
}
除了新的依赖之外,该解决方案还存在 JNA 解决方案的问题。它引入了新的问题 - 它是预览功能,它需要了解
JavaVM_
和 JNIInvokeInterface_
的结构,我不确定是否可以通过 FFI 查找 jclass
而无需原生。