实现改变Java工作目录的效果
Achieving the effect of changing the Java working directory
上下文:我的软件依赖于调用一个库,由于旧的限制,该库只能接受相对路径作为输入。我需要相对于已知目录的路径。库可能会在内部调用
java.io.File fooBar = new java.io.File("foo/bar");
我需要这个来给我 /nwd/foo/bar
,而不是 /cwd/foo/bar
,其中 /cwd
是工作目录,java
来自 运行。
出于所有意图和目的,我无法修改此库的内部行为。手动覆盖实例化这些对象的方法将涉及基本上重写整个库。
一个诱人的解决方案是在调用库之前 System.setProperty("user.dir", "/nwd")
,但这实际上并没有给我想要的效果。事实上,如果我调用 fooBar.getAbsolutePath()
,我会得到想要的 /nwd/foo/bar
,但如果我检查 fooBar.exists()
或试图打开文件进行读取或写入,文件似乎没有不存在,因为它实际上正在尝试打开 /cwd/foo/bar
。事实上,如果 fooBar
被
初始化
java.io.File fooBar = new java.io.File(new java.io.File("foo/bar").getAbsolutePath());
这实际上可行,因为 File
对象实际上包含绝对引用。
在这一点上,我很沮丧,我不在乎这是否需要一个 hacky 解决方案。我只需要更改工作目录的效果。
我认为这不是什么大问题,因为相对路径也可以从根目录开始。
因此,假设您当前的目录是 /user/home
,并且您想要引用 /user/tarun/foo/bar
,那么您可以将相对路径设为 ../tarun/foo/bar
到您的库。
为此,您可以使用下面 SO 线程中讨论的代码
How to construct a relative path in Java from two absolute paths (or URLs)?
String path = "/var/data/stuff/xyz.dat";
String base = "/var/data";
String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath();
这里有一个大大大大的免责声明!不要这样做!!!!
一旦设置了根目录,您就不能设置它。我不太确定这里的设计理念是什么,但我想这与安全有关,并且不允许恶意代码让您的应用程序做一些意想不到的事情。也就是说,我们可以通过一些讨厌的反射来解决这个问题。
我通过调试 Paths
class 找到它的默认目录并从中加载文件来实现此 hack。我使用反射来编辑我看到它使用的 FileSystem
class 的值。这个 hack 将依赖于操作系统,因为它编辑 FileSystem
实例。它可能还需要调整,因为不能保证实例不会被更新。您必须确保在所有目标系统上对其进行测试,因为它们将具有不同的 FileSystem
实现 (但实际上请不要这样做)。
随着 Java 9 中的新变化,您将无法轻松实现此 hack(他们不允许使用反射来编辑未被 "module" 公开的 classes) .您最好的选择是启动一个新的 JVM 实例,并使用传入的 -Droot
属性 进行调用。这不是一个很好的解决方案,但您正在使用的代码库也不是。您应该积极尝试以更明智的方式解决此问题。 (如果业务功能真的很重要,也许重写库是最好的方法)
使用我的工作目录中的一个文件和我的 C:\
:
中的一个文件进行工作演示
public static void main(final String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);
final Class windowsFileSystemClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsFileSystem");
final Field windowsFileSystemClassField = windowsFileSystemClass.getDeclaredField("defaultDirectory");
windowsFileSystemClassField.setAccessible(true);
final Class windowsPathClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsPath");
final Field windowsPathFsField = windowsPathClass.getDeclaredField("fs");
windowsPathFsField.setAccessible(true);
// Hack that uses a path instance to grab reference to the shared FileSystem instance.
final Object fileSystem = windowsPathFsField.get(Paths.get(""));
windowsFileSystemClassField.set(fileSystem, "C:\");
Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);
}
这输出:
I'm in the working directory
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.Main (file:/C:/Users/calve/IdeaProjects/untitled1/out/production/untitled1/) to field sun.nio.fs.WindowsFileSystem.defaultDirectory
WARNING: Please consider reporting this to the maintainers of com.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
I'm in C: now
请注意并留意 JVM 刚刚生成的警告。
另一种更改库的 CWD 的方法是在不同的 Java 进程中启动它,为此您可以在启动时指定 CWD(例如,参见 ProcessBuilder 文档).如果我理解正确的话,你现在的流程有点类似于
launch program with CWD 'a'
use library X that expects CWD to be 'b' // <-- problem here
新流程将是
launch program with CWD 'a'
determine desired CWD for launching library X
launch wrapper for library X with CWD 'b'
internally, library X is happy because its CWD is as expected
当然,这将迫使您编写一个完整的包装器,并使用套接字和序列化或您选择的任何其他通信策略进行通信。从好的方面来说,这将允许您并排启动库的多个实例,而它们的 CWD 不会相互干扰——以 JVM 和通信开销为代价。
为什么不将文件从 /nwd/foo/bar 复制到 /cwd/foo/bar 然后您可以按原样使用现有的库:)
上下文:我的软件依赖于调用一个库,由于旧的限制,该库只能接受相对路径作为输入。我需要相对于已知目录的路径。库可能会在内部调用
java.io.File fooBar = new java.io.File("foo/bar");
我需要这个来给我 /nwd/foo/bar
,而不是 /cwd/foo/bar
,其中 /cwd
是工作目录,java
来自 运行。
出于所有意图和目的,我无法修改此库的内部行为。手动覆盖实例化这些对象的方法将涉及基本上重写整个库。
一个诱人的解决方案是在调用库之前 System.setProperty("user.dir", "/nwd")
,但这实际上并没有给我想要的效果。事实上,如果我调用 fooBar.getAbsolutePath()
,我会得到想要的 /nwd/foo/bar
,但如果我检查 fooBar.exists()
或试图打开文件进行读取或写入,文件似乎没有不存在,因为它实际上正在尝试打开 /cwd/foo/bar
。事实上,如果 fooBar
被
java.io.File fooBar = new java.io.File(new java.io.File("foo/bar").getAbsolutePath());
这实际上可行,因为 File
对象实际上包含绝对引用。
在这一点上,我很沮丧,我不在乎这是否需要一个 hacky 解决方案。我只需要更改工作目录的效果。
我认为这不是什么大问题,因为相对路径也可以从根目录开始。
因此,假设您当前的目录是 /user/home
,并且您想要引用 /user/tarun/foo/bar
,那么您可以将相对路径设为 ../tarun/foo/bar
到您的库。
为此,您可以使用下面 SO 线程中讨论的代码
How to construct a relative path in Java from two absolute paths (or URLs)?
String path = "/var/data/stuff/xyz.dat";
String base = "/var/data";
String relative = new File(base).toURI().relativize(new File(path).toURI()).getPath();
这里有一个大大大大的免责声明!不要这样做!!!!
一旦设置了根目录,您就不能设置它。我不太确定这里的设计理念是什么,但我想这与安全有关,并且不允许恶意代码让您的应用程序做一些意想不到的事情。也就是说,我们可以通过一些讨厌的反射来解决这个问题。
我通过调试 Paths
class 找到它的默认目录并从中加载文件来实现此 hack。我使用反射来编辑我看到它使用的 FileSystem
class 的值。这个 hack 将依赖于操作系统,因为它编辑 FileSystem
实例。它可能还需要调整,因为不能保证实例不会被更新。您必须确保在所有目标系统上对其进行测试,因为它们将具有不同的 FileSystem
实现 (但实际上请不要这样做)。
随着 Java 9 中的新变化,您将无法轻松实现此 hack(他们不允许使用反射来编辑未被 "module" 公开的 classes) .您最好的选择是启动一个新的 JVM 实例,并使用传入的 -Droot
属性 进行调用。这不是一个很好的解决方案,但您正在使用的代码库也不是。您应该积极尝试以更明智的方式解决此问题。 (如果业务功能真的很重要,也许重写库是最好的方法)
使用我的工作目录中的一个文件和我的 C:\
:
public static void main(final String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);
final Class windowsFileSystemClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsFileSystem");
final Field windowsFileSystemClassField = windowsFileSystemClass.getDeclaredField("defaultDirectory");
windowsFileSystemClassField.setAccessible(true);
final Class windowsPathClass = Thread.currentThread().getContextClassLoader().loadClass("sun.nio.fs.WindowsPath");
final Field windowsPathFsField = windowsPathClass.getDeclaredField("fs");
windowsPathFsField.setAccessible(true);
// Hack that uses a path instance to grab reference to the shared FileSystem instance.
final Object fileSystem = windowsPathFsField.get(Paths.get(""));
windowsFileSystemClassField.set(fileSystem, "C:\");
Files.readAllLines(new File("foo.txt").toPath()).forEach(System.out::println);
}
这输出:
I'm in the working directory
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.Main (file:/C:/Users/calve/IdeaProjects/untitled1/out/production/untitled1/) to field sun.nio.fs.WindowsFileSystem.defaultDirectory
WARNING: Please consider reporting this to the maintainers of com.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
I'm in C: now
请注意并留意 JVM 刚刚生成的警告。
另一种更改库的 CWD 的方法是在不同的 Java 进程中启动它,为此您可以在启动时指定 CWD(例如,参见 ProcessBuilder 文档).如果我理解正确的话,你现在的流程有点类似于
launch program with CWD 'a'
use library X that expects CWD to be 'b' // <-- problem here
新流程将是
launch program with CWD 'a'
determine desired CWD for launching library X
launch wrapper for library X with CWD 'b'
internally, library X is happy because its CWD is as expected
当然,这将迫使您编写一个完整的包装器,并使用套接字和序列化或您选择的任何其他通信策略进行通信。从好的方面来说,这将允许您并排启动库的多个实例,而它们的 CWD 不会相互干扰——以 JVM 和通信开销为代价。
为什么不将文件从 /nwd/foo/bar 复制到 /cwd/foo/bar 然后您可以按原样使用现有的库:)