必要なもの: 簡単なWebアプリ制作経験。 IntelliJ IDEA Ultimate。
/usr/libexec/java_home -V を実行する。
Spring Boot + Kotlin を使った To Do (やること管理) アプリのバックエンド。 以下のAPIエンドポイントを提供する:
POST /todos を実行すると、JSONで与えられた項目をデータベースに追加する。
このとき、新しく追加された項目の ID を返す。
$ curl -H 'Content-Type: application/json' -d '{"text": "Hello!"}' http://localhost:8080/todos
1
GET /todos を実行すると、データベースにある項目すべてを JSON形式で返す。
$ curl http://localhost:8080/todos
[{"id":1, "text":"Hello!"}]
GET /todos/ID を実行すると、与えられた IDをもつ項目ひとつをJSON形式で返す。
$ curl http://localhost:8080/todos/1
{"id":1, "text":"Hello!"}
DELETE /todos/ID を実行すると、与えられた IDをもつ項目をデータベースから削除する。
$ curl -X DELETE http://localhost:8080/todos/1 $ curl http://localhost:8080/todos []
$ curl -i http://localhost:8080/todos/999 HTTP/1.1 404 Content-Length: 0 Date: Tue, 20 May 2025 02:57:31 GMT
| 行頭・行末 |
Command ⌘ + ← Command ⌘ + → |
| 1単語前・1単語後 |
Option ⌥ + ← Option ⌥ + → |
| 文字選択 (1文字ごと) |
Shift + ← Shift + → |
| 文字選択 (行頭・行末まで) |
Shift + Command ⌘ + ← Shift + Command ⌘ + → |
| 文字選択 (単語ごと) |
Shift + Option ⌥ + ← Shift + Option ⌥ + → |
| 文字選択 (範囲) | Shift + クリック |
| 単語選択 | ダブルクリック |
| 単語選択 (範囲) | ダブルクリック + ドラッグ |
| 行選択 | トリプルクリック |
| 行選択 (範囲) | トリプルクリック + ドラッグ |
<!DOCTYPE html> <html> <body> Welcome! </body> </html>
xxx.java) をコンパイルすると、
classファイル (xxx.class) が生成される。
jarファイル (xxx.jar) は複数のclassファイルをまとめたもので、
JVM (Java Virtual Machine) と呼ばれる仮想マシンで実行する。
xxx.kt) をclassファイルにコンパイルし、
Javaと同じく JVMで実行できる。
@ClassAnnotation
class MyClass {
@MethodAnnotation
fun foo() {
@VariableAnnotation
val x = 123
}
}
IntelliJ を使わず、コマンドラインからアプリのビルド・起動をする場合は、 以下のようにする:
$ ./gradlew build (アプリのビルドおよびテスト) Starting a Gradle Daemon, 1 stopped Daemon could not be reused, use --status for details ... BUILD SUCCESSFUL in 11s 8 actionable tasks: 8 executed $ ./gradlew bootrun (アプリの実行) > Task :bootRun . ____ _ __ _ _ /∖∖ / ___'_ __ _ _(_)_ __ __ _ ∖ ∖ ∖ ∖ ( ( )∖___ | '_ | '_| | '_ ∖/ _` | ∖ ∖ ∖ ∖ ∖∖/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_∖__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.4.6) ...
package com.example.todoApp
...
@SpringBootTest
class TodoAppApplicationTests {
@Test
fun contextLoads() {
}
@Test
fun `最初のテスト`() {
assertThat(1+2, equalTo(3))
}
}
@Test アノテーションがついているメソッドは、テストとして実行される。
fun `~` の部分には、テスト名を書く。
assertThat(1+2, equalTo(3)) は「1+2 の結果が 3 に等しい」ことをチェックしている。
コード中の赤色になっている部分で Option + Return を押すと、 その問題を解決するための手段を提案してくれる。 たとえば上の例では「どのモジュールからインポートするか?」を訊かれるので
assertThat では org.hamcrest.MatcherAssert を
equalTo では org.hamcrest.Matchers を
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TodoAppApplicationTests(@Autowired val restTemplate: TestRestTemplate, @LocalServerPort val port: Int) { ...@Test fun `GETリクエストはOKステータスを返す`() { // localhost/todos に GETリクエストを発行する。 val response = restTemplate.getForEntity("http://localhost:$port/todos", String::class.java) // レスポンスのステータスコードは OK である。 assertThat(response.statusCode, equalTo(HttpStatus.OK)) }...
Springでは
@Autowired や @LocalServerPort のようなアノテーションを書くことによって、
実行時に必要な値やオブジェクトをフレームワークが自動的に作成・注入してくれるようになっている。
この機能を「依存注入 (Dependency Injection, DI)」と呼ぶ。
Spring では、コントローラーやデータベース接続など、アプリの動作に関連するほとんどのオブジェクトは
DI を使って作成させる (ビジネスロジックに関連するオブジェクトを除く)。
package com.example.todoApp
...
@RestController
class TodoController {
@GetMapping("/todos")
fun getTodos(): String {
return "Hello"
}
}
$ curl http://localhost:8080/todos Hello
Springのコードにおいては、ファイル先頭のパッケージ宣言
package com.example.todoAppは非常に重要である。 Springの依存注入は、同一パッケージ内のクラスをスキャンすることによって行うので、 パッケージ宣言を忘れると「あるはずのコンポーネントが見つからない」 「宣言したはずのメソッドが動作しない」という状況が発生する。
Springにおける
@RestController や @GetMapping のようなアノテーションは、
依存注入のもう一方の側面を表している。
ここまでのコードでは明示的に TodoController を作成するロジックは存在していない。
にもかかわらず、TodoController オブジェクトが作成され、動作しているように見える。
ここでは、@RestController は、当該クラスが Spring Boot の DI候補として
利用可能であることを示す。さらに @GetMapping("/todos") は、当該メソッドが
GET /todos リクエストに対するハンドラとして利用可能であることを示す。
この2つのアノテーションが存在することによって、
Spring Boot は getTodos() メソッドの周囲に
HTTP リクエスト・レスポンスを処理する機構をくっつけ全体を Webサーバとして動作させる。
この仕組みを使って、数行で新しい Web API を作成することができる。
package com.example.todoApp data class TodoRequest(val text: String)
Kotlin における data class は、データの格納に使うクラスを定義するためのショートカットである。 通常であれば、以下のようなクラス定義を書く必要があるが、data class の定義は自動でこれらを追加してくれる。
class TodoRequest {
val text: String
constructor(text: String) {
this.text = text
}
getText(): String {
return text
}
...
}
@Test
fun `POSTリクエストはOKステータスを返す`() {
// localhost/todos に POSTリクエストを送る。このときのボディは {"text": "hello"}
val request = TodoRequest("hello")
val response = restTemplate.postForEntity("http://localhost:$port/todos", request, String::class.java)
// レスポンスのステータスコードは OK であること。
assertThat(response.statusCode, equalTo(HttpStatus.OK))
}
Kotlinファイル内で Java言語のコードをペーストすると、 IntelliJ は自動的に Java → Kotlin に変換してくれる。 これはJavaのサンプルコードをKotlinで使いたいときに役に立つ。 たとえば、以下のJavaコードを上記のメソッド内にペーストしてみよう:
TodoRequest request = new TodoRequest("hello");
ResponseEntity<String> response = restTemplate.postForEntity("http://localhost:"+port+"/todos", request, String.class);
@GetMapping ではなく @PostMapping を使う。
また、このとき引数の前に @RequestBody を使うと
ボディとして受け取った JSON を TodoRequestオブジェクトに変換できる。
...
@PostMapping("/todos")
fun postTodo(@RequestBody todoRequest: TodoRequest): String {
return "gotcha"
}
...
package com.example.todoApp
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
@Entity
class TodoEntity(
@Id
@GeneratedValue
var id: Long? = null,
var text: String,
)
package com.example.todoApp import org.springframework.data.repository.CrudRepository import org.springframework.stereotype.Repository @Repository interface TodoRepository : CrudRepository<TodoEntity, Long>
JPA (Java Persistence API) とは、Java/Kotlin 言語における ORM (オブジェクト関係マッピング) の共通の枠組みである。
JPA を使うと SQLを書かずに Java API を使ってオブジェクト ("エンティティ") をあたかも
直接データベースに保存 (save)、読み込み (find) するような処理を書くことができる。
Spring では、JPA は Hibernate ORM を使って実装されている。
@Entity や @Repository などのアノテーションをつけることによって、指定した
クラスを使った実装が自動的に生成される。
コード中のクラス名・関数名・変数名などを Command ⌘ キーを押しながらクリックすると、 そのクラスや関数の定義箇所に移動する。
@Test
fun `GETリクエストは空のTodoリストを返す`() {
// localhost/todos に GETリクエストを送り、レスポンスを TodoEntity の配列として解釈する。
val response = restTemplate.getForEntity("http://localhost:$port/todos", Array<TodoEntity>::class.java)
val todos = response.body!!
// 配列は0個の要素をもつこと。
assertThat(todos.size, equalTo(0))
}
...
@GetMapping("/todos")
fun getTodos(): List<TodoEntity> {
// 空のリストを返すだけ。
return emptyList()
}
...
IntelliJ では、(Javaではなく) Kotlin言語の独自機能で書かれている部分は
emptyList() ではなく
emptyList() のように斜体で表示される。
POST /todos でなにか適当な Todoをひとつ追加する。
GET /todos を実行し、得られた Todoリストに
新しい Todo項目が追加されているかどうかをチェックする。
@Test
fun `POSTリクエストはTodoオブジェクトを格納する`() {
// localhost/todos に POSTリクエストを送る。このときのボディは {"text": "hello"}
...
// localhost/todos に GETリクエストを送り、レスポンスを TodoEntity の配列として解釈する。
// このときのレスポンスを todos として記憶。
...
// 配列 todos の長さは 1。
assertThat(todos.size, equalTo(1))
// 配列 todos[0] には "hello" をもつTodoEntity が含まれている。
assertThat(todos[0].text, equalTo("hello"))
}
テストするときは、基本的にテスト対象は「ブラックボックス」であると考える。 つまり、テスト対象の内部は未知であり、ふるまいを外からテストする唯一の手段は その入力を変化させ、その出力を観察することしかできない。 したがって、テストする場合には 「どういう入力→出力の変化を確認すれば、期待通りのふるまいであると確信できるのか?」 を考えることになる。
@RestController
class TodoController(
@Autowired private val repository: TodoRepository
) {
...
}
つぎに getTodos(), postTodo() の両方を書き換えてテストが通るようにする:
...
@GetMapping("/todos")
fun getTodos(): List<TodoEntity> {
val todos = repository.findAll()
return todos.toList()
}
@PostMapping("/todos")
fun postTodo(@RequestBody todo: TodoRequest) {
val entity = TodoEntity(text=todo.text)
repository.save(entity)
}
...
なぜ Spring が「依存注入」「IoCコンテナ」などという概念を使っているのかというと、 それはコードを変えずにシステムのふるまいを変更するためである。 たとえば、以下のようなコードを考えてみよう:
name = "tododb" repository = H2TodoRepository(name) repository.save(entity)
このコードでは、ふるまいは完全に固定されている。 データベース名を変更したり、H2 以外のデータベースを使うためには コードを変更する必要がある。しかしテストの際には できるだけコードを変えずに、本番コードそのものをテストしたい。 上のようなコードでは本番・テスト時にあわせてリポジトリを切り替えることができない。 そこで Spring を使ったプログラミングでは、コードの各部分を分解し、 フレームワークに「配管作業」を任せている。そしてこの接続を 切り替えられるようにしておくことで、システムの柔軟性を実現している。 この配管作業が依存注入に相当する。
Spring における依存注入や機能拡張は、基本的にアプリケーション ロジックの各箇所に Springのコードを挿入することによって行われる。 このために、Springではアプリのロジックを細かく分割して書く必要がある。
@BeforeEach
fun setup() {
// 各テストは項目が空の状態で始める。
repository.deleteAll()
}
これですべてのテストがうまく動く状態になるはずである。
spring.application.name=todoApp spring.h2.console.enabled=true
$ curl -H 'Content-Type: application/json' -d '{"text": "Hello!"}' http://localhost:8080/todos
1
$ curl -H 'Content-Type: application/json' -d '{"text": "Goodbye!"}' http://localhost:8080/todos
2
$ curl http://localhost:8080/todos
[{"id":1, "text":"Hello!"}, {"id":2, "text":"Goodbye!"}]
ここでH2データベースに接続して、実際にデータが格納されているかどうか確認する。 まず todoApp のログの中から以下のような行を探す:
... H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:f5f5a25c-05be-4b71-8b5e-248bf4365353' ...
そして http://localhost:8080/h2-console にアクセスし、 "JDBC URL" の欄に上記の文字列を入力してデータベースに接続する。 ここで SQL クエリを発行すると格納された Todo 項目が確認できるはずである。
@Test
fun `POSTリクエストは新規作成した項目のIDを返す`() {
// 項目を新規作成し、返されたIDを取得する。
...
// TodoEntity の配列を取得する。
...
// 配列中に返されたIDをもつ要素があること。
...
}
@Test
fun `特定の項目をIDを指定してGETできる`() {
// 項目を新規作成する。
...
// localhost/todos/$id に GETリクエストを送り、レスポンスを1個の TodoEntity として解釈する。
...
// 新規作成したものと内容が一致している。
...
}
@Test
fun `存在しないIDでGETするとステータス404を返す`() {
// id=999 を指定して GETリクエストを送る。
...
// レスポンスのステータスコードは NOT_FOUND である。
...
}
@Test
fun `DELETEでIDを指定して削除できる`() {
// 項目を新規作成する。
...
// localhost/todos/$id に DELETEリクエストを送る。
...
// 再度GETすると、その項目は存在しない (削除されている)。
...
}
注意: "Java Bean" という用語もあるが、 これは特定の規則に従って作られた Javaオブジェクトのことをさし、 Spring Beanとは別物。
@Controller (または @RestController) … Web APIのエンドポイントを提供する。
@Repository … データの保存・変更・削除等の機能を提供する。
@Service … ビジネスロジックに関連した機能を提供する。
@Component … 上記以外の目的で使われる。
コードがうまく動かない場合、以下の2つの方法で問題を特定する:
... spring.jpa.show-sql=true logging.level.org.springframework.web=DEBUG
アプリの実行時にターミナルに現れる Spring Bootのログは、 何かイベントが発生するたびに記録される。 ログの各行は以下のような書式になっている。
注意: 通常は DEBUGレベルまたはTRACEレベルのログは出力すべきでない (出力量が膨大なため)。
c.e = com.example
.s.d.r.c = (org).spring.data.repository.config
o.a.c.c = org.apache.catalina.core
/Users/euske/Library/Java/JavaVirtualMachines/corretto-21.0.7/Contents/Home/bin/java ...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.4.6)
2025-05-22T14:04:19.527+09:00 INFO 12389 --- [todoApp] [ main] c.example.todoApp.TodoAppApplicationKt : Starting TodoAppApplicationKt using Java 21.0.7 with PID 12389 (/Users/euske/work/ws/spring-todoApp-2025/build/classes/kotlin/main started by euske in /Users/euske/work/ws/spring-todoApp-2025)
2025-05-22T14:04:19.528+09:00 INFO 12389 --- [todoApp] [ main] c.example.todoApp.TodoAppApplicationKt : No active profile set, falling back to 1 default profile: "default"
2025-05-22T14:04:19.700+09:00 INFO 12389 --- [todoApp] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2025-05-22T14:04:19.714+09:00 INFO 12389 --- [todoApp] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 11 ms. Found 1 JPA repository interface.
2025-05-22T14:04:19.828+09:00 INFO 12389 --- [todoApp] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2025-05-22T14:04:19.832+09:00 INFO 12389 --- [todoApp] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2025-05-22T14:04:19.832+09:00 INFO 12389 --- [todoApp] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.40]
2025-05-22T14:04:19.845+09:00 INFO 12389 --- [todoApp] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2025-05-22T14:04:19.845+09:00 INFO 12389 --- [todoApp] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 302 ms
2025-05-22T14:04:19.880+09:00 INFO 12389 --- [todoApp] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2025-05-22T14:04:19.942+09:00 INFO 12389 --- [todoApp] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:f5f5a25c-05be-4b71-8b5e-248bf4365353 user=SA
2025-05-22T14:04:19.943+09:00 INFO 12389 --- [todoApp] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2025-05-22T14:04:19.958+09:00 INFO 12389 --- [todoApp] [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2025-05-22T14:04:19.973+09:00 INFO 12389 --- [todoApp] [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.6.13.Final
2025-05-22T14:04:19.982+09:00 INFO 12389 --- [todoApp] [ main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled
2025-05-22T14:04:20.058+09:00 INFO 12389 --- [todoApp] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
2025-05-22T14:04:20.080+09:00 INFO 12389 --- [todoApp] [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
Database JDBC URL [Connecting through datasource 'HikariDataSource (HikariPool-1)']
Database driver: undefined/unknown
Database version: 2.3.232
Autocommit mode: undefined/unknown
Isolation level: undefined/unknown
Minimum pool size: undefined/unknown
Maximum pool size: undefined/unknown
Hibernate: create global temporary table HTE_todo_entity(rn_ integer not null, id bigint, text varchar(255), primary key (rn_)) TRANSACTIONAL
2025-05-22T14:04:20.315+09:00 INFO 12389 --- [todoApp] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate: drop table if exists todo_entity cascade
Hibernate: drop sequence if exists todo_entity_seq
Hibernate: create sequence todo_entity_seq start with 1 increment by 50
Hibernate: create table todo_entity (id bigint not null, text varchar(255), primary key (id))
2025-05-22T14:04:20.323+09:00 INFO 12389 --- [todoApp] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2025-05-22T14:04:20.410+09:00 WARN 12389 --- [todoApp] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2025-05-22T14:04:20.417+09:00 INFO 12389 --- [todoApp] [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2025-05-22T14:04:20.434+09:00 DEBUG 12389 --- [todoApp] [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : 6 mappings in 'requestMappingHandlerMapping'
2025-05-22T14:04:20.460+09:00 DEBUG 12389 --- [todoApp] [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Patterns [/webjars/**, /**] in 'resourceHandlerMapping'
2025-05-22T14:04:20.470+09:00 DEBUG 12389 --- [todoApp] [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
2025-05-22T14:04:20.483+09:00 DEBUG 12389 --- [todoApp] [ main] .m.m.a.ExceptionHandlerExceptionResolver : ControllerAdvice beans: 0 @ExceptionHandler, 1 ResponseBodyAdvice
2025-05-22T14:04:20.498+09:00 INFO 12389 --- [todoApp] [ main] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:f5f5a25c-05be-4b71-8b5e-248bf4365353'
2025-05-22T14:04:20.523+09:00 INFO 12389 --- [todoApp] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2025-05-22T14:04:20.527+09:00 INFO 12389 --- [todoApp] [ main] c.example.todoApp.TodoAppApplicationKt : Started TodoAppApplicationKt in 1.102 seconds (process running for 1.269)
2025-05-22T14:04:32.424+09:00 INFO 12389 --- [todoApp] [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2025-05-22T14:04:32.425+09:00 INFO 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2025-05-22T14:04:32.425+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver
2025-05-22T14:04:32.425+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected AcceptHeaderLocaleResolver
2025-05-22T14:04:32.425+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected FixedThemeResolver
2025-05-22T14:04:32.426+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator@31a80c88
2025-05-22T14:04:32.426+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected org.springframework.web.servlet.support.SessionFlashMapManager@2dd4a7a9
2025-05-22T14:04:32.426+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
2025-05-22T14:04:32.426+09:00 INFO 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
2025-05-22T14:04:32.429+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/todos", parameters={}
2025-05-22T14:04:32.432+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.todoApp.TodoController#getTodos()
Hibernate: select te1_0.id,te1_0.text from todo_entity te1_0
2025-05-22T14:04:32.592+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json', given [*/*] and supported [application/json, application/*+json]
2025-05-22T14:04:32.592+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing [[]]
2025-05-22T14:04:32.597+09:00 DEBUG 12389 --- [todoApp] [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK
Spring Boot の長所:
Spring Boot の短所:
このチュートリアルは簡単のため、本来であれば推奨されるいくつかの作業をしていない。 この例を参考に本格的なアプリを作る場合は、以下の改良点を参考のこと:
findBy... メソッドの命名規則。