kotlinで勉強がてら遊ぶ(Dockerも少し触る)

2024-02-20に作成

個人的にkotlinを勉強していくのでその作業記録をつけたい

前提

筆者はwebエンジニア歴10年だが内製ばっかやってる弱小
昔php + FuelPHP、今Java + spring boot、DDDをちょっとかじる
最近の新卒の子がElixir使いだったけどナニソレ?関数型プログラミング全然わからん
じゃあ遊んでみるしかないな
kotlinはjavaに似ててオブジェクト指向的にも関数型プログラミング的にも書けるらしいな、とりあえず触ろう
そのうちscala → Elixirみたいに手を広げるゾ!
そして体を壊して休職したのでこれ幸いと手を出す

やりたいこと

  • javaに近いっぽいkotlinで関数型言語を触ってみる
  • ローカルで動く適当なAPI作って見よっか
  • ついでにDockerもちゃんと触り直すか
    • あ、ちょっとまって公式の解説英語の動画なんすか
    • ドキュメント系は公式以外あんま参照したくないんだよなぁ
  • 時間空けると何がなんだかわからないね!どこかに記録残しとこうね!
  • あと前から気になってたcrieitさんを使ってみたかった
所有者限定モードのためこのボードには投稿できません ボードとは?

春コミの原稿やってました。時間が空いてしまったのですが続き。

postgresへの接続設定を追加

src/main/application.yamlを作成して以下を記入

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://db:5432/example
    username: postgres
    password: ****

ここでは仮置きとしてpasswordを直接書いちゃってますが、
危ないので本当はやってはだめです。

中身の確認用にcompose.yamlに以下を追加。
これを入れておくと、postgresの中身をそのまま見ることができます。

# servicesの下
  adminer:
    image: adminer
    restart: always
    ports:
      - 8082:8080

初期化用のSQLを作り、

db/init.sql

DROP TABLE IF EXISTS tamesi;
CREATE TABLE IF NOT EXISTS tamesi (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  age INTEGER NOT NULL
);

それを初期化用に設定

    volumes:
      - db-data:/var/lib/postgresql/data
# ↓を追加
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql

docker-entrypoint-initdb.dに置くと初期化してくれるらしい。

公式ドキュメント

で、エラーとか起きるので都度修正して、

plugins {
    id("org.springframework.boot") version "3.2.2"
    id("io.spring.dependency-management") version "1.1.4"
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"
    kotlin("plugin.jpa") version "1.9.22" # 追加
}

https://spring.io/guides/tutorials/spring-boot-kotlin

@Entity
@Table(name = "tamesi")
data class TamesiTableData(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) # 追加
    val id: Int?, # Int型に変更
    val name: String,
    val age: Int
)

実行

以下で実行。

$ docker compose up -d --build

スクリーンショット 0006-04-18 16.52.21.png
スクリーンショット 0006-04-18 16.52.42.png
スクリーンショット 0006-04-18 16.52.36.png
いい感じに動いてそうなのでsaveから

$ curl -X POST -H "Content-Type: application/json" -d '{"name":"taro", "age":11}' localhost:8081/save

レスポンスはこれ

{"name":"taro","age":11}

Adminerからも見てみる

スクリーンショット 0006-04-18 16.50.37.png
スクリーンショット 0006-04-18 16.54.10.png
OKっぽい

取得の方も叩いてみる

$ curl "localhost:8081/get/1"

レスポンス

{"name":"taro","age":11}

OK!

終了時は以下コマンドで

$ docker compose down -v

ところでこれって関数型プログラミングとか全然触れてないですね。


DBを起動してマルチコンテナにしてみるぞ

PostgreSQLを起動する

    # The commented out section below is an example of how to define a PostgreSQL
    # database that your application can use. `depends_on` tells Docker Compose to
    # start the database before your application. The `db-data` volume persists the
    # database data between container restarts. The `db-password` secret is used
    # to set the database password. You must create `db/password.txt` and add
    # a password of your choosing to it before running `docker compose up`.

と言われているのでせっかくなのでPostgreSQLを使用します。

Learning centerの"Multi-container applications"が参考になるかも。
こちらのサンプルのcompose.yamlを見ます。

どうやらservicesのすぐ下に複数定義するとMulti-containerになるようですね。
サービスとコンテナの言葉の違い的なものがよくわからなかったのでchatGPTに聞いたらこんな感じでした。ほんとかどうか理解するには知識が足りない。

*Dockerにおける「service」とは、コンテナーの実行単位を指します。コンテナーは、アプリケーションやサービスの独立した実行環境を提供するための軽量な仮想化技術です。そして、Docker Composeを使って複数のコンテ ナーをまとめて定義する場合、各コンテナーは1つ以上のサービスとして定義されます。

具体的には、Docker Composeの設定ファイル(通常はdocker-compose.yml)で、services セクションの下に複数のサービスが定義されます。各サービスの定義には、そのサービスに関連するイメージ、ポートの公開、環境変 数の設定などが含まれます。そして、各サービスは1つ以上のコンテナーを起動します。つまり、コンテナーは実際にリソースを提供する実体であり、サービスはそのコンテナーの定義や管理を担当します。

要するに、Dockerにおける「service」とは、論理的なグループ化や管理の単位であり、そのサービスによって実行される1つ以上のコンテナーを指します。一方で、コンテナーは実際にリソースを提供する単位です。*

特に何も考えずにコメントどおりに depends_on の項目をコメントアウト、同様にservice直下のdbの項目もコメントアウトします。

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/

# Here the instructions define your application as a service called "app".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
  app:
    container_name: tamesikotlin20240122comp # この行を追加してコンテナ名を指定する
    build:
      context: .
    #  target: final
    # If your application exposes a port, uncomment the following lines and change
    # the port numbers as needed. The first number is the host port and the second
    # is the port inside the container.
    ports:
      - 8081:8080
    depends_on:
      db:
        condition: service_healthy

    # The commented out section below is an example of how to define a PostgreSQL
    # database that your application can use. `depends_on` tells Docker Compose to
    # start the database before your application. The `db-data` volume persists the
    # database data between container restarts. The `db-password` secret is used
    # to set the database password. You must create `db/password.txt` and add
    # a password of your choosing to it before running `docker compose up`.
  db:
    image: postgres
    restart: always
    user: postgres
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=example
      - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
    expose:
      - 5432
    healthcheck:
      test: [ "CMD", "pg_isready" ]
      interval: 10s
      timeout: 5s
      retries: 5
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt

起動確認をします。
今回はバックグラウンド起動を試すために-dを実行します。

$ docker compose up -d --build
Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /host_mnt/Users/********/IdeaProjects/tamesi/db/password.txt
zsh: exit 1     docker compose up -d --build

おっとよく読んでなかった、パスワード指定するためにファイル作れと書いてありますね 作って再実行。

 ✔ Container tamesi-db-1               Healthy
 ✔ Container tamesikotlin20240122comp  Started

起動に成功した様子。接続してみたいと思います。
クライアントはDBeavweを使います。

$ brew install dbeaver-community

docker-compose.yamlの方に設定を加え、ポート5432を公開。

  db:
    image: postgres
    restart: always
    user: postgres
    secrets:
      - db-password
    volumes:
      - db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=example
      - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
    expose:
      - 5432
    # ここの下2行を追加
    ports:
      - 5432:5432

起動して接続確認。

$ docker compose up -d --build

OK!

kotlinの関数型言語の描き方がわからない

ところで私はAtomが死んでからメモ帳としてターミナル開いて素のvimを使ってるんですが、最近vim-plugの使い方を忘れてしまい、思い出しがてら新しいcolorschemeにしてみました。
redditで検索しておすすめされてたEverforest、使いやすいです。それまでicebergを使ってたので、VISUALモードが見にくかったんですよね。

閑話休題、関数型的なAPIの書き方の参考になるものを探します。が、なかなか難しいですね。

とりあえず、controllerを実装してる例って無いですね……
なんとなくのイメージで、Router Functionsのような例が出てくると思ったんですが違うようです。
もしかして関数型プログラミングの学習としてkotlinを選んだのは良くなかったのかもしれない。kotlinで学ぶ関数型言語みたいな書籍が無い(英語ならある)時点で察するべきか……
最初からやり直すならHaskell、scala、Elixir、Lispみたいので始めるべきだったかもしれません。

なんとなくで書いてみる

chatGPTさんに聞きつつやってますが、この人結構な確率で嘘をいうのであくまで参考程度に、いろんな資料を斜め読みしつつとりあえずで実装していきます。
なんとなくですが、
- 「関数そのものを変数に代入できる」
- 「数式っぽく扱うためにvoid関数を使わない」
- 「ラムダ式を多用しつつデータを変形させる」
- 「副作用を起こさない」
- 「メソッドチェーンっぽい書き方をする」
らへんなのかなと思います。

副作用云々はDDDでも触れるのでそれっぽい感じでいいんですかね……

Controller

RESTFul APIを目指してこんな感じ

package jp.gooye.toy.tamesi.controller;

import jp.gooye.toy.tamesi.model.TamesiDataResource
import jp.gooye.toy.tamesi.model.TamesiResponse
import jp.gooye.toy.tamesi.service.TamesiDataService
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class TamesiController(
    private val service: TamesiDataService
) {

    @GetMapping("/tamesi")
    fun welcome() = TamesiResponse(message = "hello world!!")

    @PostMapping("/save")
    fun create(tamesiDataResource: TamesiDataResource): TamesiDataResource {
        return service.save(tamesiDataResource)
    }

    @GetMapping("/get")
    fun read(@PathVariable id: String): TamesiDataResource {
        // 後で投げる例外は修正する
        return service.findById(id).orElseThrow()
    }

}

Service

package jp.gooye.toy.tamesi.service

import jp.gooye.toy.tamesi.model.TamesiDataResource
import jp.gooye.toy.tamesi.repository.TamesiDataRepository
import org.springframework.stereotype.Service
import java.util.Optional

@Service
class TamesiDataService(
    val repository: TamesiDataRepository,
    val tableDataFactory: TamesiTableDataFactory,
    val resourceFactory: TamesiResourceFactory
) {
    fun findById(id: String): Optional<TamesiDataResource> {
        return repository.findById(id).map { resourceFactory.from(it) }

    }

    fun save(resource: TamesiDataResource): TamesiDataResource {
        return resourceFactory.from(repository.save(tableDataFactory.from(resource)))
    }
}

Factory

package jp.gooye.toy.tamesi.service

import jp.gooye.toy.tamesi.model.TamesiDataResource
import jp.gooye.toy.tamesi.repository.TamesiTableData

class TamesiResourceFactory {
    fun from(data: TamesiTableData): TamesiDataResource {
        return TamesiDataResource(data.name, data.age)
    }
}
package jp.gooye.toy.tamesi.service

import jp.gooye.toy.tamesi.model.TamesiDataResource
import jp.gooye.toy.tamesi.repository.TamesiTableData

class TamesiTableDataFactory {

    fun from(resource: TamesiDataResource): TamesiTableData {
        return TamesiTableData(null, resource.name, resource.age)
    }
}

Repository

package jp.gooye.toy.tamesi.repository

import org.springframework.data.repository.CrudRepository

interface TamesiDataRepository : CrudRepository<TamesiTableData, String>

data

package jp.gooye.toy.tamesi.repository

import jakarta.persistence.Id
import jakarta.persistence.Table

@Table(name = "tamesi")
data class TamesiTableData(
    @Id
    val id: String?,
    val name: String,
    val age: Int
)

とりあえず形にしましたが、これだけでは動きません。次回、DBの接続まわりとユニットテストに手を出していきます。


急遽北海道に一週間行ってたので時間が空いてしまった。悪天候の冬の札幌駅に人生ではじめて降り立ったとき、エルデンリングの例のフォーマットで禁域って文字とドオォォォンみたいなSEが鳴った気がする。

Docker の公式チュートリアルを見つつ進める

といいつつbrewからdokcer desktop無いか確認はする。
dockerのコマンドラインツールはあるけどdocker desktop無いな。公式サイト行きましょ

https://hub.docker.com/

……動画のチュートリアルあるのか。こりゃ便利。英語だけど雰囲気でなんとかなんべ。大体見ます。
スクリーンショット 0006-03-02 16.48.14.png

この前作ったkotlinのapiのルートに移動、以下実行

$ docker init

結果

 ~/IdeaProjects/toykotlin
$ docker init       

Welcome to the Docker Init CLI!

This utility will walk you through creating the following files with sensible defaults for your project:
  - .dockerignore
  - Dockerfile
  - compose.yaml
  - README.Docker.md

Let's get started!

? What application platform does your project use?  [Use arrows to move, type to filter]
  Go - suitable for a Go server application
  Python - suitable for a Python server application
  Node - suitable for a Node server application
  Rust - suitable for a Rust server application
  ASP.NET Core - suitable for an ASP.NET Core application
  PHP with Apache - suitable for a PHP web application
  Java - suitable for a Java application that uses Maven and packages as an uber jar
> Other - general purpose starting point for containerizing your application
  Don't see something you need? Let us know!
  Quit

kotlinなのでOtherにします。Javaともちょっと迷ったけどMavenは使ってないしね。uber jarって何?届けてくれるの?

? What application platform does your project use? Other

CREATED: .dockerignore
CREATED: Dockerfile
CREATED: compose.yaml
CREATED: README.Docker.md

✔ Your Docker files are ready!

Take a moment to review them and tailor them to your application.

When you're ready, start your application by running: docker compose up --build

Consult README.Docker.md for more information about using the generated files.

ファイルができました。中を見ます。
docker desktopのLearning centerでもこのように言っている。
however, that the Dockerfile and compose.yaml file created for your project need additional changes. In this case, you may need to look up the Dockerfile reference⁠ and Compose file reference⁠ in our documentation.

.dockerignore

# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/

**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
(後略)

コンテナにコピーしたくないファイルをここに記述しろと書いてあります。大体必要なものは入ってるかな?

compose.yaml

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/

# Here the instructions define your application as a service called "app".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
  app:
    build:
      context: .
      target: final
    # If your application exposes a port, uncomment the following lines and change
    # the port numbers as needed. The first number is the host port and the second
    # is the port inside the container.
    # ports:
    #   - 8080:8080

    # The commented out section below is an example of how to define a PostgreSQL
    # database that your application can use. `depends_on` tells Docker Compose to
    # start the database before your application. The `db-data` volume persists the
    # database data between container restarts. The `db-password` secret is used
    # to set the database password. You must create `db/password.txt` and add
    # a password of your choosing to it before running `docker compose up`.
    #     depends_on:
    #       db:
    #         condition: service_healthy
    #   db:
    #     image: postgres
    #     restart: always
    #     user: postgres
    #     secrets:
    #       - db-password
    #     volumes:
    #       - db-data:/var/lib/postgresql/data
    #     environment:
    #       - POSTGRES_DB=example
    #       - POSTGRES_PASSWORD_FILE=/run/secrets/db-password
    #     expose:
    #       - 5432
    #     healthcheck:
    #       test: [ "CMD", "pg_isready" ]
    #       interval: 10s
    #       timeout: 5s
    #       retries: 5
    # volumes:
    #   db-data:
    # secrets:
    #   db-password:
    #     file: db/password.txt

ここが肝になります。docker 起動するときに必要な設定を全部ここに書いておけば、docker compose up --buildを実行するときに読んでくれます。詳しいことは公式ドキュメント見ろと書いてあります。
一番最初にやることはこれ

services:
  app:
    container_name: tamesikotlin20240122comp # この行を追加してコンテナ名を指定する

コンテナ名指定しないままbuildすると勝手に中二臭い名前にされます。

次、ポート番号

    # If your application exposes a port, uncomment the following lines and change
    # the port numbers as needed. The first number is the host port and the second
    # is the port inside the container.
    ports:
      - 8081:8080

開きたいポートがあるならコメントアウトしろと書いてますね。
前回8080ポート指定でAPI実行したので、今回は8081をフォワーディングしてくれるように設定してみます。
portsの指定場所はservices.app.portsです。字下げの位置に気をつけます。

次。

    # The commented out section below is an example of how to define a PostgreSQL
    # database that your application can use.

来ましたね。PostgreSQLを使ったマルチコンテナ的なやつ。一旦今回はDocker上でKotlin動かすことに集中し、ここは次回に回します。
composeについてはここまで。

Dockerfile

# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/

# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7

################################################################################
# Pick a base image to serve as the foundation for the other build stages in
# this file.
#
# For illustrative purposes, the following FROM command
# is using the alpine image (see https://hub.docker.com/_/alpine).
# By specifying the "latest" tag, it will also use whatever happens to be the
# most recent version of that image when you build your Dockerfile.
# If reproducability is important, consider using a versioned tag
# (e.g., alpine:3.17.2) or SHA (e.g., alpine@sha256:c41ab5c992deb4fe7e5da09f67a8804a46bd0592bfdf0b1847dde0e0889d2bff).
FROM alpine:latest as base

################################################################################
# Create a stage for building/compiling the application.
#
# The following commands will leverage the "base" stage above to generate
# a "hello world" script and make it executable, but for a real application, you
# would issue a RUN command for your application's build process to generate the
# executable. For language-specific examples, take a look at the Dockerfiles in
# the Awesome Compose repository: https://github.com/docker/awesome-compose
FROM base as build
RUN echo -e '#!/bin/sh\n\
echo Hello world from $(whoami)! In order to get your application running in a container, take a look at the comments in the Dockerfile to get started.'\
> /bin/hello.sh
RUN chmod +x /bin/hello.sh

################################################################################
# Create a final stage for running your application.
#
# The following commands copy the output from the "build" stage above and tell
# the container runtime to execute it when the image is run. Ideally this stage
# contains the minimal runtime dependencies for the application as to produce
# the smallest image possible. This often means using a different and smaller
# image than the one used for building the application, but for illustrative
# purposes the "base" image is used here.
FROM base AS final

# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
    --disabled-password \
    --gecos "" \
    --home "/nonexistent" \
    --shell "/sbin/nologin" \
    --no-create-home \
    --uid "${UID}" \
    appuser
USER appuser

# Copy the executable from the "build" stage.
COPY --from=build /bin/hello.sh /bin/

# What the container should run when it is started.
ENTRYPOINT [ "/bin/hello.sh" ]

hello worldしかしないDockerfileが置かれています。今回やりたいのはkotlinの起動なのでまるっと書き換えます。

FROM eclipse-temurin:17.0.9_9-jre # java 17でなんか良さそうなやつ
# 適当に作業ディレクトリ
WORKDIR /app
# 作業内容全部コピー
COPY . /app/. 

# buildはあらかじめ実行しておく前提
COPY build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app/app.jar"]

これで実行

$ docker compose up --build
[+] Running 1/0
 ✔ Container tamesikotlin20240122comp  Recreated                                                                                                                                                                                                                      0.1s 
Attaching to tamesikotlin20240122comp
tamesikotlin20240122comp  | 
tamesikotlin20240122comp  |   .   ____          _            __ _ _
tamesikotlin20240122comp  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
tamesikotlin20240122comp  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
tamesikotlin20240122comp  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
tamesikotlin20240122comp  |   '  |____| .__|_| |_|_| |_\__, | / / / /
tamesikotlin20240122comp  |  =========|_|==============|___/=/_/_/_/
tamesikotlin20240122comp  |  :: Spring Boot ::                (v3.2.2)
tamesikotlin20240122comp  | 
tamesikotlin20240122comp  | 2024-03-03T15:16:40.531Z  INFO 1 --- [           main] j.gooye.toy.tamesi.TamesiApplicationKt   : Starting TamesiApplicationKt v0.0.1-SNAPSHOT using Java 17.0.9 with PID 1 (/app/app.jar started by root in /app)
tamesikotlin20240122comp  | 2024-03-03T15:16:40.535Z  INFO 1 --- [           main] j.gooye.toy.tamesi.TamesiApplicationKt   : No active profile set, falling back to 1 default profile: "default"
tamesikotlin20240122comp  | 2024-03-03T15:16:41.425Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
tamesikotlin20240122comp  | 2024-03-03T15:16:41.434Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
tamesikotlin20240122comp  | 2024-03-03T15:16:41.435Z  INFO 1 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.18]
tamesikotlin20240122comp  | 2024-03-03T15:16:41.458Z  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
tamesikotlin20240122comp  | 2024-03-03T15:16:41.459Z  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 853 ms
tamesikotlin20240122comp  | 2024-03-03T15:16:41.761Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
tamesikotlin20240122comp  | 2024-03-03T15:16:41.771Z  INFO 1 --- [           main] j.gooye.toy.tamesi.TamesiApplicationKt   : Started TamesiApplicationKt in 1.601 seconds (process running for 1.936)

なんか動いた気がする
じゃあcurlしましょ

$ curl localhost:8081/tamesi
{"message":"hello world!!"}

動きました!
明日はもうちょっといろいろDocker周りをきれいにしたいとおもいます。


環境構築は昨日やったので今日はskeleton作っていきます

公式ドキュメントのチュートリアルをチラ見しながら進める

公式ドキュメント、英語でも気にしない雰囲気わかれば良し
Building web applications with Spring Boot and Kotlin

initializr使ってskeletonを取得

Creating a New Projectの項目で
Using the Initializr Websiteとコマンドライン、IntelliJ IDEA Ultimateを使う手順とかGradleじゃなくてMavenの手順とか紹介されているが、素直に一番上を使用していきます。

spring initializr

いっつも思うんだけどinitializrのzrの部分、よくzerと書いてしまうけどスペルはzrが正しいんだよな。

追加するよう言われているDependenciesは以下の通り

plugin 内容
Spring Web おなじみのMVCするやつ
Mustache 意味は「口ひげ」web テンプレートシステム 一応いれる
Spring Data JPA こちらもおなじみのJPA DBに関してはちょっとあとで自分でやりたいので今回は外しとく
H2 Database 同じく外しとく
Spring Boot DevTools これもおなじみのあれ 開発中の変更がすぐ反映されるよ

spring initializr

これでGENERATEボタンを押すと(Initializrで設定した名前).zipファイルがダウンロードされる。

作ったskeletonをintelliJで開いて編集する

公式チュートリアルではGradle解説みたいなこと言ってるのでIntelliJで開いていこう。

zip解凍してこちらに移動。

# IntelliJのデフォのディレクトリに持っていく
$ mv (解凍したtamesiフォルダ) ~/IdeaProjects

IntelliJでopenを選択し↑のディレクトリを指定。
今回入れたpluginとかmain関数とかHtmlControllerとか解説されてるけど作りたいのはAPIなので全部飛ばします。

kotlinに触るのがガチ初めてなので文法とかはとほほ先生にお世話になりつつ、書いたのがこれ。

コントローラー (jp/gooye/toy/tamesi/controller/TamesiController.kt)

```kt:jp/gooye/toy/tamesi/controller/TamesiController.kt
package jp.gooye.toy.tamesi.controller;

import jp.gooye.toy.tamesi.model.TamesiResponse
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class TamesiController {
@GetMapping("/tamesi")
fun welcome() = TamesiResponse(message = "hello world!!")
}

リソース(jp/gooye/toy/tamesi/model/TamesiResponse.kt)
```kt:jp/gooye/toy/tamesi/model/TamesiResponse.kt
package jp.gooye.toy.tamesi.model

data class TamesiResponse(var message: String)

あ、jdkのバージョン指定とかIntelliJの設定とかgradleの導入手順とか書き忘れてるな……まあいいかjenvだけ念の為合わせとこ。
intelliJの中でターミナル開いて

$ jenv local 17.0.9

$ java --version   
openjdk 17.0.9 2023-10-17
OpenJDK Runtime Environment Homebrew (build 17.0.9+0)
OpenJDK 64-Bit Server VM Homebrew (build 17.0.9+0, mixed mode, sharing)

この2つのktファイルを追加して、intelliJの右上のぞうさんマークから実行してみる

gradle clean bootRun

gradle clean bootRun

springの起動ログが見えたら、

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.2)

 (中略)

 2024-02-21T19:05:42.670+09:00  INFO 18060 --- [  restartedMain] j.gooye.toy.tamesi.TamesiApplicationKt   : Started TamesiApplicationKt in 0.759 seconds (process running for 1.007)

curl!

$ curl localhost:8080/tamesi
{"message":"hello world!!"}

ローカルで動くとこまで行きました。
明日はいよいよDockerに触ります。


環境構築

何が どれ
PC iMac
チップ Apple M1
OS Sonoma
IDE IntelliJ IDEA (Community Edition)
パッケージ管理システム 当然!「Homebrew」だッ!
javaバージョン管理 jenv
使用言語 kotlin 1.9.22
フレームワーク spring boot
ターミナル iTerm2

Homebrewはインストールされているところから開始

各種パッケージ導入

特にバージョン違いの管理をする予定はないが手癖でいれる

$ brew install jenv

IDE

$ brew install intellij-idea-ce

JDK Apple M1に対応してるJDKを探しつつ、なぜかjava17も入れておく jenvの挙動試すの久々だったし

$ brew install java
$ brew install openjdk@17

M1向けjdkはzulu使えみたいなんあるけどbrewのページだとおkになってんだよな よくわからん

jenvいれる
公式ドキュメントを参考に

$ brew install jenv

メモ忘れたけど公式ドキュメント参考にdoctorとかして最終的にzshrcにはこれを追加

export PATH="/Users/sakamotokeika/.jenv/shims:${PATH}"
export JENV_SHELL=zsh
export JENV_LOADED=1
unset JAVA_HOME
unset JDK_HOME
source '/opt/homebrew/Cellar/jenv/0.5.6/libexec/libexec/../completions/jenv.zsh'
jenv rehash 2>/dev/null
jenv refresh-plugins
jenv() {
  type typeset &> /dev/null && typeset command
  command="$1"
  if [ "$#" -gt 0 ]; then
    shift
  fi

  case "$command" in
  enable-plugin|rehash|shell|shell-options)
    eval `jenv "sh-$command" "$@"`;;
  *)
    command jenv "$command" "$@";;
  esac
}

jenvにjdk追加

$ brew info openjdk@17
==> openjdk@17: stable 17.0.9 (bottled) [keg-only]
Development kit for the Java programming language
[https://openjdk.java.net/](https://openjdk.java.net/)
/opt/homebrew/Cellar/openjdk@17/17.0.9 (635 files, 304.9MB)
  Poured from bottle using the formulae.brew.sh API on 2024-02-13 at 18:57:08
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/o/[email protected]
License: GPL-2.0-only with Classpath-exception-2.0
==> Dependencies
Build: autoconf ✔, pkg-config ✔
Required: giflib ✔, harfbuzz ✔, jpeg-turbo ✔, libpng ✔, little-cms2 ✔
==> Requirements
Build: Xcode (on macOS) ✘
==> Caveats
For the system Java wrappers to find this JDK, symlink it with
  sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk

openjdk@17 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.

If you need to have openjdk@17 first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zshrc

For compilers to find openjdk@17 you may need to set:
  export CPPFLAGS="-I/opt/homebrew/opt/openjdk@17/include"
==> Analytics
install: 18,294 (30 days), 59,204 (90 days), 273,258 (365 days)
install-on-request: 15,800 (30 days), 49,966 (90 days), 180,903 (365 days)
build-error: 101 (30 days)

おっふXcodeないやんけ

==> Requirements
Build: Xcode (on macOS) ✘
% brew search Xcode
==> Formulae
xcode-build-server                xcode-kotlin                      xcodegen                          xcodes                            coder

==> Casks
copilot-for-xcode                 swiftformat-for-xcode             xcodeclangformat                  xcodes                            xscope

If you meant "Xcode" specifically:
Xcode can be installed from the App Store.

しゃあないAppStoreから落とすか

If you meant "Xcode" specifically:
Xcode can be installed from the App Store.

今度こそadd

$  jenv add /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home

動作確認

$ jenv versions
* system (set by /Users/sakamotokeika/.jenv/version)
  17.0
  17.0.9
  20.0
  20.0.2
  21.0
  21.0.2
  openjdk64-17.0.9
  openjdk64-20.0.2
  openjdk64-21.0.2

適当なディレクトリほって確認

$ jenv local 17.0.9

$ jenv versions
  system
  17.0
* 17.0.9 (set by /Users/sakamotokeika/dev/scala/.java-version)
  20.0
  20.0.2
  21.0
  21.0.2
  openjdk64-17.0.9
  openjdk64-20.0.2
  openjdk64-21.0.2

OK!

kotlin

$ brew install kotlin

一旦ここまで