Springでファイルアップロード/ダウンロード実装
前提知識など
アップロードの実装
MultipartFileで受けるようにすれば比較的簡単に実装できる。
注意点としてファイルを受けてどこかに書き込んだりする時はbyteArrayなどには乗せず、Streamつないで書き込んだ方がよい。
Controller以外でStreamを介した書き込み処理をするのが普通だと思うので、実際処理をするところへはresourceオブジェクト(MultipartFile#getResource)を渡して上げるのが良いと思う。
code: sample.java
@PostMapping("/stream/{fileName}")
fun uploadHandledByStream(
@PathVariable fileName: String,
@RequestParam("file") file: MultipartFile
): String {
val folderPath = FileSystems.getDefault().getPath(fileProperties.path)
Files.createDirectories(folderPath)
val filePath = folderPath.resolve(Paths.get(fileName))
// streamつないだ書き込み
file.resource.inputStream.use { input ->
Files.newOutputStream(filePath).buffered().use { output ->
input.copyTo(output)
}
}
return "OK"
}
byteArrayとかで書き込んでしまうと大きいファイルが来たときにヒープがかなり圧迫される(下記の例は60MBくらいのファイルを送った時)。
code: sample.java
@PostMapping("/array/{fileName}")
fun uploadHandledByArray(
@PathVariable fileName: String,
@RequestParam("file") file: MultipartFile
): String {
val folderPath = FileSystems.getDefault().getPath(fileProperties.path)
Files.createDirectories(folderPath)
val filePath = folderPath.resolve(Paths.get(fileName))
// bytesにするとファイルのbyteArrayがまるごとヒープに乗ってしまう
Files.write(filePath, file.bytes, StandardOpenOption.WRITE)
return "OK"
}
https://gyazo.com/c72e8fa9b2c247fbfe13b3ee38f15e6d
ファイルサイズの制限
application.ymlで設定可能。
max-file-sizeはファイルのみのサイズ制限、max-request-sizeはファイル以外のすべてのリクエストのファイルサイズになる。
なので、通常はmax-request-sizeの方が大きくなり、どちらも設定が必要。
code: application.yml
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 110MB
ダウンロードの実装
ResponseEntityにStreamingResponseBodyを指定してやると、ファイルの内容が非同期でOutputStreamに書き込まれて返却できる。
code: sample.java
@GetMapping("/zip")
fun downloadZip(): ResponseEntity<StreamingResponseBody> {
val headers = HttpHeaders().apply {
add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.zip\"")
add(HttpHeaders.CONTENT_TYPE, "application/zip;")
}
return ResponseEntity.ok()
.headers(headers)
.body(
// Streamで返却する (非同期で処理されるためTaskExecutorを明示的に構成したほうが良いらしい)
StreamingResponseBody { outputStream ->
ZipOutputStream(outputStream).use { zipOutputStream ->
val entry1 = ZipEntry("test/file1.txt")
val file1 = "これはfile1です".toByteArray()
entry1.size = file1.toUByteArray().size.toLong()
zipOutputStream.putNextEntry(entry1)
zipOutputStream.write(file1)
}
}
)
}
StreamingResponseBodyのドキュメントを見ると、Spring MVCの非同期処理で処理されるため、TaskExecutorを明示的に構成したほうが良いとの記述があった。
注意 : このオプションを使用する場合は、Spring MVC で非同期リクエストを実行するために使用される TaskExecutor を明示的に構成することを強くお勧めします。MVC Java 構成と MVC 名前空間の両方は、非同期処理を構成するオプションを提供します。これらを使用しない場合、アプリケーションは RequestMappingHandlerAdapter の taskExecutor プロパティを設定できます。
Spring MVCの非同期処理がそもそもよくわかっていないが、非同期で別スレッドが少しずつ返す形になるから非同期のタスク用のTaskExecutorでスレッドを管理したほうがよいよという話なきがする(MVC Java 構成っていうのは多分WebMvcConfigurerとかのことと思う)。
RequestMappingHandlerAdapterドキュメント
Spring MVCのドキュメントと突き合わせながらみていく
Callable 処理は次のように機能します。
コントローラーは Callable を返します
Spring MVC は request.startAsync() を呼び出し、Callable を TaskExecutor に送信して、別のスレッドで処理します。
とあるのでどうもCallableが返されて非同期タスク実行するためのTaskExecutorの設定が必要だというはなしっぽい。
実際試してみたところ、確かにResponseEntity<StreamingResponseBody>を叩くたびにThreadが新しく生成されていた(毎回スレッド名が変わっている)。
code: log
...
どうもSimpleAsyncTaskExecutorがデフォルトで使用されて毎回スレッドが作られて閉じられてる感じになっているっぽい。
以下のコードを参考にスレッドプールを指定してみた。
code: sample.java
@Configuration
// Bootの場合はEnableWebMvc不要
class MvcConfig : WebMvcConfigurer {
override fun configureAsyncSupport(configurer: AsyncSupportConfigurer) {
configurer.setTaskExecutor(mvcAsyncExecutor())
}
@Bean
fun asyncExecutor(): AsyncTaskExecutor {
// Bean登録しておけば適切なタイミングで初期化やファイナライズが走るとのこと
return ThreadPoolTaskExecutor().also {
it.corePoolSize = 10
it.maxPoolSize = 20
it.setQueueCapacity(100)
}
}
上記のConfigを追加したところ、Asyncタスク用のExecutorが上がって新しくスレッドがたちあがることもなくなった。
code: log
https://gyazo.com/a31963f3cad1de847dc201448bedfe4b
ちなみにExecutorのキャパシティを越えてリクエストが来た場合、キャパシティを越えた分のリクエストはRejected(エラー)になる
code: log
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@5c41504dRunning, pool size = 1, active threads = 1, queued tasks = 1, completed tasks = 2] did not accept task: org.springframework.web.context.request.async.WebAsyncManager$$Lambda$925/0x00000008008c3040@194ae357 キャパシティ越えた時の挙動としてはこんな感じらしい (ソースを見たところkeepAliveSecondsのデフォルトは60秒だった)