java.io.IOException: Permission denied——文件导出时报错
背景
业务系统中,有一个导出,就是很普通的列表查询,然后可以点击导出,生成一个文件。就这么一个功能。
使用的 excel 工具类是: org.apache.poi.xssf 用的poi这个工具类。
问题描述
在一天晚上项目上线后,发现这个功能点出现错误
点击导出时,后台报了一段错误:
Caused by: java.io.IOException: Permission denied
at java.io.UnixFileSystem.createFileExclusively(Native Method) ~[?:1.8.0_311]
at java.io.File.createTempFile(File.java:2061) ~[?:1.8.0_311]
at org.apache.poi.util.DefaultTempFileCreationStrategy.createTempFile(DefaultTempFileCreationStrategy.java:110) ~[poi-4.1.2.jar!/:4.1.2]
at org.apache.poi.util.TempFile.createTempFile(TempFile.java:66) ~[poi-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SheetDataWriter.createTempFile(SheetDataWriter.java:89) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SheetDataWriter.<init>(SheetDataWriter.java:72) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SheetDataWriter.<init>(SheetDataWriter.java:77) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SXSSFWorkbook.createSheetDataWriter(SXSSFWorkbook.java:342) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SXSSFSheet.<init>(SXSSFSheet.java:80) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
at org.apache.poi.xssf.streaming.SXSSFWorkbook.createAndRegisterSXSSFSheet(SXSSFWorkbook.java:684) ~[poi-ooxml-4.1.2.jar!/:4.1.2]
... 89 more
然后,一看啊,肯定是文件没有读写权限啊,(之前服务用 root 启动过,然后后来用了普通用户启动),方面是对的,但是我们把已知的一些路径都检查过,也没有读写权限不对的问题呀? 怎么回事呢? 真相只有一个,就是还有我们没发现的地方,存在读写权限问题!!
那怎样找到这个未被发现的路径呢?
找“未知的文件路径”
这一次还是用了 arthas来定位问题,用这个来定位出入参真的是很方便。
看报错,是在 java.io.UnixFileSystem.createFileExclusively(Native Method) ~[?:1.8.0_311] 这里抛的问题,用arthas定位一下:
watch java.io.UnixFileSystem createFileExclusively params
很可惜,arthas 在这个方法里面执行不了,因为它是一个 native 方法。(大概是这么回事,没细究)
然后只能退而求其次,去找到上一级的入口,就是: at java.io.File.createTempFile(File.java:2061) ~[?:1.8.0_311]
幸运的是,这一次可以被监听到
这一行的方法对应定义是这样子的:
public static File createTempFile(String prefix, String suffix,
File directory)
throws IOException
{
if (prefix.length() < 3)
throw new IllegalArgumentException("Prefix string too short");
if (suffix == null)
suffix = ".tmp";
File tmpdir = (directory != null) ? directory
: TempDirectory.location();
// ... 省略
return f;
}
可以看到它的入参就三个参数.
OK,搞起:
watch java.io.File createTempFile params
然后,参数看不到,加个 -x 3 试试:
watch java.io.File createTempFile params -x 3
好了,可以看到入参了。
如下:
method=java.io.File.createTempFile location=AtExit
ts=2023-01-05 12:16:58; [cost=0.214534ms] result=@Object[][
@String[poi-sxssf-template],
@String[.xlsx],
@File[
fs=@UnixFileSystem[
slash=@Character[/],
colon=@Character[:],
javaHome=@String[/usr/java/jdk1.8.0_311-amd64/jre],
cache=@ExpiringCache[java.io.ExpiringCache@1a8ec1ec],
javaHomePrefixCache=@ExpiringCache[java.io.ExpiringCache@7dd05f67],
BA_EXISTS=@Integer[1],
BA_REGULAR=@Integer[2],
BA_DIRECTORY=@Integer[4],
BA_HIDDEN=@Integer[8],
ACCESS_READ=@Integer[4],
ACCESS_WRITE=@Integer[2],
ACCESS_EXECUTE=@Integer[1],
SPACE_TOTAL=@Integer[0],
SPACE_FREE=@Integer[1],
SPACE_USABLE=@Integer[2],
useCanonCaches=@Boolean[true],
useCanonPrefixCache=@Boolean[true],
],
path=@String[/tmp/poifiles],
status=@PathStatus[
INVALID=@PathStatus[INVALID],
CHECKED=@PathStatus[CHECKED],
$VALUES=@PathStatus[][isEmpty=false;size=2],
name=@String[CHECKED],
ordinal=@Integer[1],
],
prefixLength=@Integer[1],
separatorChar=@Character[/],
separator=@String[/],
pathSeparatorChar=@Character[:],
pathSeparator=@String[:],
PATH_OFFSET=@Long[16],
PREFIX_LENGTH_OFFSET=@Long[12],
UNSAFE=@Unsafe[
theUnsafe=@Unsafe[sun.misc.Unsafe@3a1aa809],
INVALID_FIELD_OFFSET=@Integer[-1],
ARRAY_BOOLEAN_BASE_OFFSET=@Integer[16],
ARRAY_BYTE_BASE_OFFSET=@Integer[16],
ARRAY_SHORT_BASE_OFFSET=@Integer[16],
ARRAY_CHAR_BASE_OFFSET=@Integer[16],
ARRAY_INT_BASE_OFFSET=@Integer[16],
ARRAY_LONG_BASE_OFFSET=@Integer[16],
ARRAY_FLOAT_BASE_OFFSET=@Integer[16],
ARRAY_DOUBLE_BASE_OFFSET=@Integer[16],
ARRAY_OBJECT_BASE_OFFSET=@Integer[16],
ARRAY_BOOLEAN_INDEX_SCALE=@Integer[1],
ARRAY_BYTE_INDEX_SCALE=@Integer[1],
ARRAY_SHORT_INDEX_SCALE=@Integer[2],
ARRAY_CHAR_INDEX_SCALE=@Integer[2],
ARRAY_INT_INDEX_SCALE=@Integer[4],
ARRAY_LONG_INDEX_SCALE=@Integer[8],
ARRAY_FLOAT_INDEX_SCALE=@Integer[4],
ARRAY_DOUBLE_INDEX_SCALE=@Integer[8],
ARRAY_OBJECT_INDEX_SCALE=@Integer[4],
ADDRESS_SIZE=@Integer[8],
],
serialVersionUID=@Long[301077366599181567],
filePath=null,
$assertionsDisabled=@Boolean[true],
],
]
从这一段信息里面,
有一个关键信息,就是: path=@String[/tmp/poifiles], 看到了文件路径。也正是这一个路径,我们可怜的普通用户没有权限读取得到。因为这个文件夹的归属是 root。(因为第一次起服务的时候,用了root启动,当这个文件夹不存在,就会创建,于是用root创建了,它的归属也就是 root 了)
把这个文件夹的归属改掉后,问题解决。
等等,为什么是这个路径呢? 找了一下源码,发现了么这以一个东西。
刚才的日志,再往上翻一下,会发现第三个参数: File directory 是如何被定义出来的,
// org.apache.poi.util.DefaultTempFileCreationStrategy#createTempFile
public File createTempFile(String prefix, String suffix) throws IOException {
this.createPOIFilesDirectory();
File newFile = File.createTempFile(prefix, suffix, this.dir);
if (System.getProperty("poi.keep.tmp.files") == null) {
newFile.deleteOnExit();
}
return newFile;
}
这里有一行: this.createPOIFilesDirectory();
点进去,看一眼:
private void createPOIFilesDirectory() throws IOException {
if (this.dir == null) {
String tmpDir = System.getProperty("java.io.tmpdir");
if (tmpDir == null) {
throw new IOException("Systems temporary directory not defined - set the -Djava.io.tmpdir jvm property!");
}
this.dir = new File(tmpDir, "poifiles");
}
this.createTempDirectory(this.dir);
}
到这里就很清晰了,
而在 linux 下面, 这个 System.getProperty("java.io.tmpdir"); 就是 /tmp
OK, 收工。