Achieving the effect of changing the Java working

2020-04-11 00:47发布

Context: My software depends on calling a library which can only accept relative paths as an input because of an old limitation. I need the paths to be relative to a known directory. The library might make a call internally like

java.io.File fooBar = new java.io.File("foo/bar");

I need this to give me /nwd/foo/bar and not, say, /cwd/foo/bar where /cwd is the working directory from which java was run.

For all intents and purposes, I cannot modify the internal behavior of this library. Manually overriding the methods which instantiate these objects would involve basically rewriting the entire library.

A tempting solution would be to just System.setProperty("user.dir", "/nwd") before calling the library, but this doesn't actually give me the desired effect. Indeed, if I called fooBar.getAbsolutePath(), I would get the desired /nwd/foo/bar, but if I checked fooBar.exists() or tried to open the file for reading or writing, it would appear that the file doesn't exist, because it's actually trying to open /cwd/foo/bar. In fact, if fooBar were instead initialized by

java.io.File fooBar = new java.io.File(new java.io.File("foo/bar").getAbsolutePath());

that would actually work, because then the File object actually contains absolute references.

At this point, I'm so frustrated that I don't care if this requires a hacky solution. I just need the effect of changing the working directory.

4条回答
对你真心纯属浪费
2楼-- · 2020-04-11 01:26

A big big big disclaimer here! Don't do this!!!!

You aren't supposed to be able to set the root directory once it is set. I'm not exactly sure what the design philosophy is here, but I imagine it's to do with security and not allowing malicious code to get your application to do something unexpected. That said, there are ways we can get around this using some nasty reflection.

I achieved this hack by debugging where the Paths class finds it's default directory and loads files from. I use reflection to edit the values of the FileSystem class which I saw that it uses. This hack is going to be operating system dependent as it edits the FileSystem instance. It might also need to be tweaked as there's no guarantee that the instance wont be renewed. You'll have to make sure to test it on all the target systems as they will have different FileSystem implementations (BUT PLEASE DON'T ACTUALLY DO THIS).

Further with the new changes in Java 9 you wont be able to make this hack work easily (they disallow using reflection to edit classes not exposed by the "module"). Your best bet is to spin up a new JVM instance and make the call there with the -Droot property passed in. It's not a pretty solution but neither is the code-base you're working with. You should actively attempt to fix this in a more sensible manner. (Maybe a re-write of the library is the best way to go if the business function is actually important)

Working demo with a file in my working directory and a file in my 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);
}

This outputs:

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

Please note and heed the warnings the JVM just produced.

查看更多
祖国的老花朵
3楼-- · 2020-04-11 01:28

A very alternative way to change the CWD of the library would be to launch it in a different Java process, for which you can specify CWD at launch time (see for example ProcessBuilder documentation). If I understand the problem correctly, your current flow is somewhat similar to

 launch program with CWD 'a'
 use library X that expects CWD to be 'b' // <-- problem here

The new flow would be

 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

Of course, this will force you to write a full wrapper, and communicate it using sockets&serialization, or any other communication strategy of your choice. On the plus side, this will allow you to launch several instances of your library side-by-side, without their CWDs interfering with each other -- at the cost of JVM and communication overheads.

查看更多
淡お忘
4楼-- · 2020-04-11 01:32

why not copy the file from /nwd/foo/bar to /cwd/foo/bar then you can use your existing library as it is :)

查看更多
你好瞎i
5楼-- · 2020-04-11 01:34

I don't think this is a big issue, because a relative path can start from root as well.

So let assume your current directory is /user/home and you want to refer to /user/tarun/foo/bar then you would give the relative path to be ../tarun/foo/bar to your library.

And for that you can use the code as discussed in below SO thread

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();
查看更多
登录 后发表回答