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, 收工。

posted @ 2023-01-05 13:07  aaacarrot  阅读(3719)  评论(0编辑  收藏  举报