前言
最近做项目,又涉及到Linux Java文件的相对路径,但是相对路径在不同的服务器或者docker上居然不一样,这个就很难受,只能用绝对路径解决,因为绝对路径是固定的路径,但是相对路径为什么会在不同的服务器不一样呢?
Java源码分析与Demo
因为文件夹或者文件的创建是native方式C++实现的,笔者本地是MacOS系统,Linux类似
创建目录如上,创建文件如下:
功能大同小异,毕竟Linux一切皆文件,注意默认情况下Linux Java创建文件夹是777的权限,跟umask也相关。
创建Demo 代码
public class Main {
public static void main(String[] args) throws InterruptedException {
File file = new File("hello");
boolean ok = file.mkdir();
System.out.println(file.getAbsoluteFile());
if (ok) {
Thread.sleep(60*1000);
file.delete();
}
}
}
运行后结果
为什么是项目的根目录??? 如果加上-Duser.dir=xxx的JVM参数,那么Java绝对路径是不正确的
C++原理与Demo
为了验证这个是为什么,构建JNI代码,注意JNI非常关键的包名
package org.example;
public class Demo {
public native String sayHello(File file);
}
在java目录下,执行javah org.example.Demo
生成C++头文件,原因是idea把java目录作为classpath;也可以使用-cp指定classpath,javah 默认支持-jni可以不写
创建C++ lib文件
但是头文件在放在C++项目中执行cmake时,报错
Could NOT find JNI (missing: JAVA_INCLUDE_PATH JAVA_INCLUDE_PATH2 AWT)
明明已经设置了JAVA_HOME,但是还是不行,查询资料,cmake官方资料,解决问题准确FindJNI — CMake 3.27.0 Documentation
只需在CMakeLists文件中设置,根据自己的实际情况而定
# JAVA_INCLUDE_PATH为jni.h所在路径,一般在jdk目录下的include中
set(JAVA_INCLUDE_PATH /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/include)
# JAVA_INCLUDE_PATH2为jni_md.h所在路径,一般在jdk目录下的include/xxx系统类别目录中
set(JAVA_INCLUDE_PATH2 /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/include/darwin)
set(JAVA_AWT_INCLUDE_PATH /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/include)
注意cmakelists文件的lib命名,可自定义
add_library(org_example_Demo SHARED org_example_Demo.cpp)
这个名称就是Java代码加载的名称,编译代码构建可执行文件就可以得到lib文件,linux是so文件
static {
System.loadLibrary("org_example_Demo");
}
public static void main(String[] args) {
File file = new File("demo");
String path = new Demo().sayHello(file);
System.out.println(path);
}
加入JVM参数-Djava.library.path=xxx目录,启动即可执行
C++可执行代码
#include "org_example_Demo.h"
#include <jni.h>
#include <iostream>
#include <sys/stat.h>
using namespace std;
//实现sayHello方法
JNIEXPORT jstring JNICALL Java_org_example_Demo_sayHello(JNIEnv* env, jobject obj, jobject file) {
jclass fileClass = env->FindClass("java/io/File");
if (!fileClass) return NULL;
jfieldID path = env->GetFieldID(fileClass,"path", "Ljava/lang/String;");
jstring jstr = static_cast<jstring>(env->GetObjectField(file, path));
int success = mkdir("demo", 0777);
cout << success << endl;
return jstr;
}
在拿到文件后使用path创建文件,这里仿造JDK的实现,先通过类读取fieldid,然后读取field,这里直接强转jstring了
读取field,JDK使用宏定义
参考Java jni.h C++数据类型
java | jni定义的类型与C++类 | C++ | 字节数 |
boolean | jboolean | unsigned char | 1 |
byte | jbyte | signed char | 1 |
char | jchar | unsigned short | 2 |
short | jshort | short | 2 |
int | jint/jsize | long | 4 |
long | jlong | __int64 | 8 |
float | jfloat | float | 4 |
double | jdouble | double | 8 |
String | jstring | string(char*) |
执行Java的main方法
说明相对路径是C代码函数执行的结果;但是当mkdir使用相对路径时,如果在C++的环境执行,直接就失败
可以看到结果是-1。JNI执行和C++原生执行的结果是不一样的😳,如果使用绝对路径可以成功
得出结论,JNI中间的一些设置数据跟C、C++底层函数执行结果相关性很大,比如创建目录或者文件,在相对路径下,C++不能成功;Java却可以成功。
总结
linux、unix环境下Java创建相对路径,表现为C、C++的函数执行,但是跟C++等原生执行的结果不一样,JNI在C类代码执行时,使用的堆外空间,里面是有一些默认设定的,比如创建相对路径的目录,JNI会默认使用项目的根路径,作为堆外空间,在linux的服务器上,可能与user.dir有关联
笔者在配置user.dir的linux docker上出现过不一致的情况,相同JVM参数(-Duser.dir)不同的docker容器创建的相对路径不一致,猜测JVM的native空间,可能跟user.dir和docker的work.dir都有关系,需要进一步查看JNI的执行原理和C++函数的源码进一步分析。