hatenob

プログラムって分からないことだらけ

Spring WebFluxで複数ファイルアップロード

特に必要に迫られたわけでもないのですが、WebFluxでリアクティブプログラミングにも触れておいたほうがよいのかなぁという気になりました。
浅い理解では、従来の1リクエスト1スレッドから、複数リクエスト1スレッドで処理できるのでスケーラビリティが向上するものだと思っています。言い換えれば、レスポンスやスループットが向上するというものではないということでいいんだと思います。ただし、ストリームでやり取りすることで終わったものから結果を受け取れて応答性はあがるんですかね。

ちょうど、ファイルをアップロードしてあれこれしたい要件があったのでファイルアップロードを対象に調べてみました。
コードは以下にあります。(気まぐれで消すかもしれないのでリンク切れていたらすいません)

https://github.com/nobrooklyn/garden/tree/master/ap-flux-fileup

複数ファイルをアップロードするのでFluxで受け取り、順番に保存して、ファイル情報をレスポンスとして返しています。
Non-Blockingで実装しないといけないんだろうなぁということは分かっていてもお作法が分からないのでこれで良いのかはわかりませんが、動きはしました。

@RestController
@Slf4j
public class FileController {
    private final FileConfiguration conf;

    @Autowired
    public FileController(FileConfiguration conf) {
        this.conf = conf;
    }

    @ResponseBody
    @PostMapping(
        path = "/upload",
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
        produces = MediaType.APPLICATION_JSON_VALUE)
    public Flux<FileUploadResponse> upload(@RequestPart("f") Flux<FilePart> parts) {
        return parts.flatMap(part -> save(part));
    }

    private Mono<FileUploadResponse> save(FilePart part) {
        String fileName = part.filename();
        File file = new File("tmp/" + fileName);
        log.info("save to {}", file.getAbsolutePath());
        return part.transferTo(file)
            .thenReturn(new FileUploadResponse(file, conf.getDownloadEndpoint()));
    }
}

@Getter
@ToString
@Slf4j
public class FileUploadResponse {
    private String fileName;
    private String downloadUrl;
    private String contetnType;
    private long size;
    private String md5;

    FileUploadResponse(File file, String downloadEndpoint) {
        this.fileName = file.getName();
        this.downloadUrl = downloadEndpoint + "/" + fileName;
        try {
            this.contetnType = Files.probeContentType(file.toPath());
        } catch (IOException e) {
            this.contetnType = "no data";
        }
        this.size = file.length();
        try {
            this.md5 = DigestUtils.md5DigestAsHex(new FileInputStream(file));
        } catch (IOException e) {
            this.md5 = "no data";
        }
    }
}

@Component
public class FileConfiguration {
    final static String downloadPath = "/download";

    @Value("${file.download.url:http://localhost:8080}")
    private String downloadUrl;

    String getDownloadEndpoint() {
        return downloadUrl + downloadPath;
    }
}

Servlet APIやその振る舞いに慣れている身としては「この場合やどうやって書くんや?」というのがちょいちょいあって慣れるまでもどかしい感じです。
少しずつ勉強していきたいと思います。