返回

Commons Fileupload 2.x 迁移:设置仓库和阈值

java

如何在迁移到 Apache Commons Fileupload 2.x 时设置仓库和文件阈值?

老项目升级,挺麻烦的。最近我就碰到了一个,要把一个 Java 11 项目升级到 Java 17。 旧版本用的是 javax.servlet.*,所以得升级到 jakarta.servlet.*

升级过程中,DiskFileItemFactory 这块儿把我给整懵了。

旧代码是这样的:

ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory(DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD, dir));

到了 2.x 版本,构造函数变成私有的了,只能这么写:

ServletFileUpload upload = new JakartaServletFileUpload(DiskFileItemFactory.builder().get());

但是,DiskFileItemFactory.Builder 居然没有 setRepositorysetThreshold 方法! 这可咋整?我知道这个 repository 就是个临时文件夹,看了下 2.x 版本的 DiskFileItemFactory.Builder.get() 的 API,上面写着:

Constructs a new instance. This builder use the aspects Path and buffer size. You must provide an origin that can be converted to a Reader by this builder, otherwise, this call will throw an UnsupportedOperationException.

大致意思就是,这个 Builder 使用了 “aspect” 的 Path 和 buffer size。得提供一个能被转换成 Reader 的 origin,不然就报错。

问题来了,这个 “ASPECT” 到底是啥玩意? 虽说这个路径只是用来放临时文件的,但 DiskFileItemFactory 本身又有 .getRepository().getThreshold() 方法,我还是想设置一下,毕竟旧代码里设置了,说不定很重要呢。

DiskFileItemFactory.Builder 倒是从 org.apache.commons.io.build.AbstractOriginSupplier 继承了 .setPath() 方法,也从 org.apache.commons.io.build.AbstractStreamBuilder 继承了 .setBufferSize().setBufferSizeMax() 方法。但是,这些方法是不是对应之前的 setRepository()setThreshold(),还真不好说。这些类的 API 太抽象了,啥也没说清楚。.setPath() 方法只说了 “sets a new origin”,别的啥也没提。

所以,我就想搞清楚:

  1. DiskFileItemFactory.Builder.get() 里的 “ASPECT” 到底是个啥?
  2. Path 和 BufferSize 是不是跟之前的 Respository 和 Threshold 一回事?

问题根源:API 设计变更

问题的根源在于 Apache Commons Fileupload 从 1.x 版本到 2.x 版本,DiskFileItemFactory 的 API 发生了较大变化。 主要目的是为了:

  • 简化 API: 通过 Builder 模式,隐藏了对象创建的复杂性。
  • 增强灵活性: 通过 AbstractOriginSupplierAbstractStreamBuilder 提供更通用的配置方式。
  • 与 Jakarta EE 兼容: 为了适配 Jakarta EE,包名从 javax 变更为 jakarta

但是,这种变化也带来了迁移成本,让开发者需要重新理解 API 的用法。

解决方案

咱们一个一个来解决:

1. "ASPECT" 是什么?

“ASPECT” 并不是一个特定的类或接口,而是一个抽象的概念,指的是构建 DiskFileItemFactory 实例所需的配置信息来源。 实际上,这个 “ASPECT” 可以是任何实现了 FileItemFactoryBuilder 接口的对象.

我们可以通过提供不同的 builder 方式去获得FileItemFactory. 在使用默认情况下,使用的构建的builder会追溯到 AbstractOriginSupplier.

当没有指定构建时使用的类(Builder), 可以通过.get() 得到FileItemFactory

FileItemFactory<?> factory = DiskFileItemFactory.builder().get()

该实现相当于提供了 DiskFileItemFactory.Builder作为其 ASPECT, 会被AbstractOriginSupplier<Path, ?>中的getPath() 实现中转换为Path,并提供了 java.nio.file.Files; 中的系统临时目录作为 Path。而 BufferSizeMax和 BufferSize 都会被设置为默认值

2. Path 和 BufferSize 与 Repository 和 Threshold 的关系

  • Path 与 Repository: 基本等价。Path 指定了临时文件存放的目录,与之前的 Repository 功能一致。 在 DiskFileItemFactory 中, 如果你调用.getRepository()函数,则实际上是返回的 DiskFileItemFactory 所拥有的 Path.
  • BufferSize 与 Threshold: 不完全等价,但是强相关。
    • Threshold(阈值)在旧版本中是指:如果上传的文件大小超过这个阈值,就写入磁盘,否则保存在内存里。
    • BufferSize 在新的设置中并非完全直接对应 Threshold, 但是依旧担任如果文件过大时,将数据写入磁盘时,缓冲区的大小, 它的大小也直接影响写入效率和内存占用。

虽然不等价, 但是依旧具有相关性. 所以说,之前的代码是有关联性, 但是也应该进行一定更新的. 尤其是需要根据实际场景合理调整缓冲区大小。

具体的代码修改方法

既然弄明白了原理,改起代码来就简单了。以下提供几种方案:

方案一:使用 setPathsetBufferSize

import java.nio.file.Paths;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;

// ...

// 假设你需要设置的临时目录和阈值
String tempDir = "/path/to/temp/dir"; // 或者使用 Paths.get(...)
int bufferSize = 4096; // 设置合适的缓冲区大小

DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();

// 设置临时目录
builder.setPath(Paths.get(tempDir)); //原有的Repository

// 设置缓冲区大小,可以根据需求设置合适的 Threshold
builder.setBufferSize(bufferSize); //原有的 Threshold

DiskFileItemFactory fileItemFactory = builder.get();
JakartaServletFileUpload upload = new JakartaServletFileUpload(fileItemFactory);

// ... 使用 upload 对象处理文件上传 ...

原理:

  • setPath(Paths.get(tempDir)):设置临时文件存放的目录。
  • setBufferSize(bufferSize):间接控制文件写入磁盘的时机, 设置一个你认为适合的值。

安全建议:

  • 临时目录权限: 确保应用程序对临时目录有读写权限。
  • 定期清理: 定期清理临时目录,防止磁盘空间耗尽。

方案二:继承 DiskFileItemFactory.Builder

import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;

// 自定义 Builder
class MyDiskFileItemFactoryBuilder extends DiskFileItemFactory.Builder {

    private Path repository;
    private int threshold;

    public MyDiskFileItemFactoryBuilder(Path repository, int threshold) {
        this.repository = repository;
        this.threshold = threshold;
    }

    @Override
    public DiskFileItemFactory get() {
        setPath(repository);
        setBufferSize(threshold); // 将 threshold 设置为 bufferSize
        return super.get();
    }
}

// ...

//使用
Path tempDir = Paths.get("/path/to/temp");
int threshold = 1024 * 10; // 10KB
MyDiskFileItemFactoryBuilder myBuilder = new MyDiskFileItemFactoryBuilder(tempDir, threshold);
DiskFileItemFactory factory = myBuilder.get();
JakartaServletFileUpload upload = new JakartaServletFileUpload(factory);

原理

  • 直接了当, 我们自定义了构建函数, 在get() 时分别使用了 setPath()和setBufferSize()设置了对应参数.

方案三: 使用 Supplier 构建DiskFileItemFactory(进阶)

通过 Supplier 来自定义, 可以进一步精细化 FileItemFactory


import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Supplier;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.jakarta.JakartaServletFileUpload;
//...

//使用
 Supplier<DiskFileItemFactory.Builder> supplier =
         () -> DiskFileItemFactory.builder()
                      .setPath(Paths.get("你的临时目录"))
                      .setBufferSize(4096);  // 例如,4KB 缓冲区

      // 现在, 你每次要获取工厂的时候, 都可以获取对应的 Factory, 其均满足我们提前定义好的Path 和 BufferSize.
      // 这就解决了 ASPECT 不明确的问题.
 DiskFileItemFactory fileItemFactory = supplier.get().get();

 JakartaServletFileUpload upload = new JakartaServletFileUpload(fileItemFactory);

原理

  • 在工厂方法中指定了参数, 使用时则使用定义的参数。

安全建议:
同方案一。

总结一下,升级到 Apache Commons Fileupload 2.x,虽然 API 变了,但只要弄明白 Path 和 BufferSize 的作用,就能轻松搞定临时目录和阈值的设置。 实在不行,咱还可以自定义 Builder,想咋设置就咋设置。关键是理解背后的原理,对症下药.