Spring Integration Ftpのexists メソッドがどんなときもTrueを返す件について
なんてアホな子なのだ!
調べた
DefaultのExists ModeはFTP STAT なのでここで判定している。
public boolean exists(final String path) { return doExecuteWithClient(client -> { try { switch (FtpRemoteFileTemplate.this.existsMode) { case STAT: return client.getStatus(path) != null;
ここで使われる client は apache.common.net.ftp なのだが、このclient.getStatus(path)
はファイルが存在しない場合でもFTP Statusを文字列として返してしまう。
ファイルがある場合
213-STAT\r\n-rw-r--r-- 1 1000 ftpgroup 0 Dec 12 15:32 /foo/bar.txt\r\n213 End.
ファイルがない場合
213-STAT\r\n213 End.
ファイルがない場合でも文字列が返るので、どんなときも existsはTrueを返してくる。イエスマンかな?
じゃあどうすればいいの
Exists Mode を NLST にしてあげれば正しく動作する。
val template = FtpRemoteFileTemplate(sessionFactory) template.setExistsMode(FtpRemoteFileTemplate.ExistsMode.NLST) // 正しく動作する template.exists(path)
Spring WebFlux × Kotlinで NoHandlerFoundException 対策をする
背景
Spring BootでRest APIを作る際、Spring MVCならNoHandlerFoundExceptionがあるため、設定していないURLにRequestが来た場合のハンドリングは ExceptionHandlerで簡単に実装できる。
@RestControllerAdvice class ErrorAdvice { @ExceptionHandler(value = [NoHandlerFoundException::class]) @ResponseStatus(HttpStatus.BAD_REQUEST) fun noHandleFound(ex: NoHandlerFoundException, req: WebRequest): ErrorResponse { return ErrorResponse("no handler found") } }
しかし、WebFluxでは前述のNoHandlerFoundExceptionが存在しないので、ExceptionHandlerでは実装できない。なので、自分で実装するしかない、多分 (自信なし)。
実装
AbstractErrorWebExceptionHandler を継承したClassを実装する。
ライブラリ
Library | Version |
---|---|
Kotlin | 1.6.0 |
Spring Boot | 2.6.0 |
@Component @Order(-2) class MyExceptionHandler( errorAttributes: ErrorAttributes, applicationContext: ApplicationContext, serverCodecConfigurer: ServerCodecConfigurer, ) : AbstractErrorWebExceptionHandler( errorAttributes, WebProperties.Resources(), applicationContext ) { init { // https://spring.pleiades.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-config-message-codecs // defaultを設定 this.setMessageWriters(serverCodecConfigurer.writers) this.setMessageReaders(serverCodecConfigurer.readers) } override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> { return route(all(), this::renderErrorResponse) } private fun renderErrorResponse(serverRequest: ServerRequest): Mono<ServerResponse> { val errorProperties = getErrorAttributes( serverRequest, ErrorAttributeOptions.defaults() ) logger.debug { errorProperties } val errorAttribute = serverRequest .attribute( "org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR" ) if (errorAttribute.isPresent) { val th = errorAttribute.get() if (th is Throwable && th.message == NO_HANDLER_MESSAGE) { return ServerResponse.status(HttpStatus.BAD_REQUEST) .contentType(MediaType.APPLICATION_JSON) .body( BodyInserters.fromValue(ErrorResponse(NO_HANDLER_MESSAGE)) ) } } return ServerResponse.status(HttpStatus.BAD_REQUEST) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(ErrorResponse("something wrong ;-("))) } companion object { private const val NO_HANDLER_MESSAGE = "404 NOT_FOUND \"No matching handler\"" private val logger = KotlinLogging.logger { } } } data class ErrorResponse( val reason: String )
あとはapplication.ymlでwhitelabelを無効にしておく。
spring: mvc: throw-exception-if-no-handler-found: true web: resources: add-mappings: false server: error: whitelabel: enabled: false
一応動いているから、多分大丈夫、大丈夫だよな。。KotlinでのAbstractErrorWebExceptionHandlerがいくらググっても全然出てこないから若干ゃ不安
参考
Spring Boot Reference Documentation
spring - String insted of whitelabel error page in webflux? - Stack Overflow
【雑記】転職したいけど世の中に興味がない
俺の会社の開発部門はもう限界かもしれない
自分が今の会社に入ったとき、社内はプログラムの内製化に向けて熱があり、実力もある社員が大勢いた。日々が勉強であり、毎日が楽しかった。しかし、会社が一部の自社サービスを身売り (本当は全然違うんだけど、内製エンジニアからしたらそうとしか言えない) したことをきっかけに上述の実力あるエンジニアが大量に、かつ一気に会社を去ってしまった。
しかし会社は「内製化」という熱を持ち続け、新卒社員を一定数採用し続けている。その結果として、「プログラムを書かない (書けない) マネージャー」と「新卒社員」がエンジニアの大半を占めるようになってしまった。自分のような「そこそこ業務経験があるプログラマ」は極々少数になってしまった。認識する限り3人もいない。
当然の帰結として、プロジェクトは回らなくなる。進捗が芳しくないプロジェクトには仕方なく ? SESが大量投入されるようになる。内製エンジニアはベンダーの管理で手一杯になり技術的な業務に携わることも勉強することもできなくなる。労働意欲と技術力が低下する。
また、ベンダーは、10年前に見たとしても、ひっでーなこれ、とため息がでるようなコードしか納品してこない。例えばSpring Bootのコードで
@RestController class FooController { @RequestMapping(value = ["foo"]) @ResponseBody // 不要なアノテーションをつけてくる fun foo( @RequestBody request: FooRequest ): String { // Logic省略 // なぜか自分でJsonを作る return Gson().toJson(...) } }
みたいなものを納品してくる。Spring Bootのチュートリアルすら読んでない連中しかいない。ここでは書かないが具体的なビジネスロジックの実装部分については目を覆うほかない。
しかし新卒社員はその低品質コードが低品質である、ということもわからないまま受け入れざるを得ない。結果として1つのページを描画するのに20秒かかるWebサイトが出来上がったりする。これが今自分が勤めている会社の「内製化」の哀れな哀れな末路である。 近い将来上のほうから「全然駄目じゃんうちの開発」と言われ、首を切られるかよくわからない部署になってしまってよくわからない仕事をすることになる可能性も十分にある。
このような現状を改善するべく、自分なりに精一杯のことをやってきたつもりだ。プロジェクト初期の段階からコードの低品質に対して警告し続けてきたし、品質を向上させるような取り組みも色々行った。勉強会を企画したこともあるし、プログラムのお手本を配布したこともあるし、コードの説明会をしたこともあるし、実装の概念を図にして説明したこともある。 しかし、自分ひとりの努力では大量のベンダーに (往々にしてプログラムを書くことになんの興味も関心も抱いていない人たち) 思いを伝えることはできなかった。新卒の勉強意欲に火をつけることはできなかった。
最近は傾ききった「内製化」をもとの状態に戻すことは現実的に不可能だと感じるようになってきた。今の会社の環境を良くするよりも転職したほうがいいのではないか。しかし、記事タイトルに書いたように自分は世の中に興味がないので転職サイトを見ても全然受ける気がしないし、受けても採用されるはずがない。
どうしたらいいのかわからないまま一日が終わる。
AWS SDK for JAVA V2 でS3にMultipart Uploadする
検索してもAWSのサンプルコード以外にヒットしないので需要があるのかわからないけどコードを書いたので記事にします。 localstack で確認していますが、AWSでは動かしていません 🙃
テスト環境
Library | Version |
---|---|
Kotlin | 1.4.21 |
Spring Boot | 2.4.2 |
AWS SDK for JAVA V2 | 2.15.38 |
コード
fun uploadByMultipart(file: Path) { // s3 client for localstack // aws環境ではendpointOverrideは不要 val s3 = S3Client.builder() .region(DefaultAwsRegionProviderChain().region) .endpointOverride(URI.create(ENDPOINT_URL)) .build() val size = file.toFile().length() val key = file.fileName.toString() if (size < MIN_MULTIPART_SIZE) { throw IllegalArgumentException() } // 分割してファイルを読み込めるようにRandomAccessFileにする val randomAccessFile = RandomAccessFile(file.toFile(), "r") val channel = randomAccessFile.channel val request = CreateMultipartUploadRequest.builder() .bucket(BUCKET) .key(key) .build() val response = s3.createMultipartUpload(request) val uploadId = response.uploadId() val parts = mutableListOf<CompletedPart>() val multipart = ceil((size / MIN_MULTIPART_SIZE).toDouble()).toInt() for (i in 0..multipart) { val filePosition = MIN_MULTIPART_SIZE * i val contentLength = min(MIN_MULTIPART_SIZE, size - filePosition) if (contentLength <= 0) { break } val partNumber = i + 1 val byteBuffer = ByteBuffer.wrap(ByteArray(contentLength.toInt())) channel.read(byteBuffer, filePosition) val req = UploadPartRequest.builder() .bucket(BUCKET) .key(key) .uploadId(uploadId) .contentLength(contentLength) .partNumber(partNumber) .build() val etag = s3.uploadPart( req, RequestBody.fromByteBuffer(byteBuffer) ).eTag() val part = CompletedPart.builder() .partNumber(partNumber) .eTag(etag) .build() parts += part } val completedMultipartUpload = CompletedMultipartUpload.builder() .parts(parts) .build() val completedMultipartUploadRequest = CompleteMultipartUploadRequest.builder() .bucket(BUCKET) .key(key) .uploadId(uploadId) .multipartUpload(completedMultipartUpload) .build() s3.completeMultipartUpload(completedMultipartUploadRequest) } companion object { private const val BUCKET = "foo-bucket" private const val URL = "http://localhost:4566" private const val MB: Long = 1024L * 1024L private const val MIN_MULTIPART_SIZE = 5L * MB }
Bucket Lifecycle
Multipart Uploadを利用する場合 AbortIncompleteMultipartUpload rule を設定していたほうが安全 (らしい) です
参考
Spring Boot WebFluxでThymeleafを使う
WebFluxのサンプルをネットで探すとAPIの例しか見つからなくて、俺はHTML出力したいんだよ!って思ったので自分で書きました。 Data Drivenは試していない 🤗
Controller
Spring MVC風のやつ。Rendering を返す。Kotlinならsuspend functionにするだけでReactiveになる (はず)
@Controller @RequestMapping("/foo") class FooController( private val repository: FooRepository, ) { @GetMapping("bar") suspend fun list(): Rendering { val fooFlow = repository.findAll() return Rendering .view("index") .modelAttribute("fooPresenter", FooPresenter(fooFlow.toList(mutableListOf()))) .build() } }
Router
Functional Endpointのやつ。ServerResponseを返す
@Configuration(proxyBeanMethods = false) class FooRoute { @Bean fun router( repository: FooRepository ) = coRouter { val handler = FookHandler(repository) GET("/foo/bar", handler::list) } } class FooHandler( private val repository: FooRepository ) { suspend fun list(req: ServerRequest): ServerResponse { val fooFlow = repository.findAll() return ok().contentType(MediaType.TEXT_HTML).renderAndAwait( "index", mapOf("fooPresenter" to FooPresenter(fooFlow.toList(mutableListOf()))) ) }
参考
Going Reactive with Spring, Coroutines and Kotlin Flow
Spring I/O 2017: Getting Thymeleaf Ready for Spring 5 and Reactive - Speaker Deck
Spring Boot の bootBuildImage で custom runImageを利用する
以前、↓みたいな記事を書きました。
Cloud Native Buildpacksのrun:base-cnb に curlを入れたい - 秋の魔法使い
Spring Bootのコードを読んで研究したところ、もっと一般的な解決方法に気づいたので記事にしておきます。
(普通にリファレンスにも書いてあったので、俺がアホなだけ、ということなのですが 😎)
custom run image build
buildImageをカスタマイズするためにDockerfileを書きます。今回はcurlとgrpcurlを追加します。
FROM paketobuildpacks/run:base-cnb # custom start USER root ARG package_args='--no-install-recommends' ARG grpcurl_version='1.7.0' RUN echo "debconf debconf/frontend select noninteractive" | debconf-set-selections && \ export DEBIAN_FRONTEND=noninteractive && \ apt-get -y $package_args update && \ apt-get -y $package_args install curl && \ apt-get clean && \ cd /usr/local/src && \ curl -OL "https://github.com/fullstorydev/grpcurl/releases/download/v${grpcurl_version}/grpcurl_${grpcurl_version}_linux_x86_64.tar.gz" && \ tar -zxvf "grpcurl_${grpcurl_version}_linux_x86_64.tar.gz" && \ mv grpcurl /usr/local/bin && \ rm -f "grpcurl_${grpcurl_version}_linux_x86_64.tar.gz" LICENSE && \ rm -rf \ /usr/share/man/* /usr/share/info/* \ /usr/share/groff/* /usr/share/lintian/* /usr/share/linda/* \ /var/lib/apt/lists/* /tmp/* # custom end USER cnb
$ docker build -t my-run-image -f run.Dockerfile
参考
bootBuildImage
bootBuildImageでImageを作成する際に pullPolicyにIF_NOT_PRESENTを指定します。こうすることで、runImageは作成したLocalのものを利用し、builderImageはdefaultのbuilder:base-cnbをダウンロードして利用することになります。すでにbase-cnbがLocalに存在する場合落としてくれない (と思う) ので、最新を利用したい場合事前に削除するなり更新するなりしておきましょう。
$ ./gradlew bootBuildImage --runImage my-run-image --pullPolicy IF_NOT_PRESENT
参考
Spring Boot Gradle Plugin Reference Guide
まとめ
この方法ならCodeBuildとかにも簡単に組み込めそうです 😌