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, 收工。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南