すきま風

勉強したことのメモとか

Spring Integration Ftpのexists メソッドがどんなときもTrueを返す件について

なんてアホな子なのだ!

調べた

DefaultのExists ModeはFTP STAT なのでここで判定している。

https://github.com/spring-projects/spring-integration/blob/main/spring-integration-ftp/src/main/java/org/springframework/integration/ftp/session/FtpRemoteFileTemplate.java#L87-L88

   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 を設定していたほうが安全 (らしい) です

参考

Work with Amazon S3 objects - AWS SDK for Java

AWS::S3::Bucket Rule - AWS CloudFormation

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

参考

GitHub - GoogleCloudPlatform/buildpacks: Builders and buildpacks designed to run on Google Cloud's container platforms

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とかにも簡単に組み込めそうです 😌

aws Fargate × golangでIAM Database認証をする

仕事で必要になったのでサンプルを書きました。FargateでIAM Database認証をしているサンプルをネット上で見つけることができなかったことと、awsに掲載されているコードのコピペでは動かなかったので2日くらいハマっていました。誰かの助けになればと思います 😉

やること一覧

  • Aurora (postgres) 作成
  • IAM Database認証ユーザ設定
  • go application 作成
  • Fargate taskRoleArnを作成


くっそ長いので注意 🙃

続きを読む