必要なもの: 簡単な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 の短所:
このチュートリアルは簡単のため、本来であれば推奨されるいくつかの作業をしていない。 この例を参考に本格的なアプリを作る場合は、以下の改良点を参考のこと: