【初心者】SpringBootでTodoアプリのREST APIを書いてみる

追記

  • Controllerのコードの一部が切れていたので修正
  • Controllerのアノテーション周りの話が抜けていたので追記

内容

  • Java初心者が夏休み中にSpringの勉強をする
  • Spring Frameworkはハードルが高そうだったので一先ずSpringBootから
  • お題は初心者がよくやるTodoアプリのREST API

詳しく触れないこと

  • gradle周辺
  • Database周辺
    • JPA
    • H2 Database

本編

  • 開発はIntelliJ IDEAを使用
    • ビルドとかアプリケーションの起動はお任せ

雛形作成

  • https://start.spring.io/ で作成
    • Project: Gradle Project
    • Language: Java
    • Spring Boot: 2.1.8
    • Project Metadaba:
      • Group: com.example
      • Artifact: todoapp
      • Options: そのまま
    • Dependencies:
      • Spring Web
      • Spring Data JPA
      • H2 Database
      • Lombok
  • ダウンロードされたzipファイルをunzipしてIntelliJでopenしたところから開始

パッケージ構成

  • com.example.todoapp直下に以下を並べる
    • controllers
    • repositories
    • models
  • servicesとかもあるべきかもしれないけど今回は割愛

コード

models

package com.example.todoapp.models;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Data
@Entity
public class Task {

    @Id
    @GeneratedValue
    private Long id;
    private String summary;
    private Boolean done = false;

    public Task(){}
}
  • アノテーションについて
    • @Data
      • 各フィールドのsetterとgetterを定義してくれる
      • toString()メソッドやhashCode()メソッドを定義してくれる
    • @Entity
      • エンティティであることを示す。
      • このクラスのフィールドを持つテーブルがDBに作られる
    • @Id
      • エンティティの主キーのフィールドを指定する
    • @GeneratedValue
      • 主キーの値の生成方法を指定する
      • strategygeneratorを指定して挙動を変えられそう(試してない)

repositories

  • TaskRepository.java
package com.example.todoapp.repositories;

import com.example.todoapp.models.Task;
import org.springframework.data.jpa.repository.JpaRepository;

public interface TaskRepository extends JpaRepository<Task, Long> {
}
  • JpaRepositoryを継承したinterfaceを定義
    • JpaRepositoryの祖先にいるCrudRepositoryインターフェースがfindByIdなどのインターフェースを定義している
    • 実装はSimpleJpaRepositoryなのかな?
  • JpaRepositoryは対象のエンティティクラスとその主キーの型を指定する
    • ここで指定した主キーの型は、findByIddeleteByIdの引数の型として使われる
    • 今回作成したTaskエンティティの主キーはLong型なのでここではLongを指定

controllers

  • ApiController.java
package com.example.todoapp.controllers;

import com.example.todoapp.models.Task;
import com.example.todoapp.repositories.TaskRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping(value = "/api")
public class ApiController {

    @Autowired
    TaskRepository taskrepo;

    @GetMapping(value = "/todos")
    public List<Task> tasklist(){
        return taskrepo.findAll();
    }

    @GetMapping(value = "todo/{id}")
    public Optional<Task> retrieve(@PathVariable Long id){
        return taskrepo.findById(id);
    }

    @PostMapping(value = "/todo")
    public ResponseEntity<Task> newTask(@RequestBody Task task){
        Task result = taskrepo.save(task);
        return new ResponseEntity<Task> (result, HttpStatus.CREATED);
    }

    @PutMapping(value = "/todo/{id}")
    public Task update(@PathVariable Long id, @RequestBody Task task){
        task.setId(id);
        return taskrepo.save(task);
    }

    @DeleteMapping(value = "/todo/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id){
        taskrepo.deleteById(id);
    }
}
  • アノテーションについて
    • @RestController
      • 各メソッドの戻り値がそのままレスポンスボディになる
      • @Controllerを使う場合、メソッドの戻り値はStringにしてテンプレートに埋め込む形で使うらしい
    • @RequestMapping
      • このコントローラが受け付けるリクエストのパスやメソッドやクエリパラメータを指定できる
    • @Autowired
      • @Componentアノテーションをつけて登録されているBeanとの紐付けを行う
      • @Repository@Serviceをつけたクラスも、内部では@Componentが付加されている
      • ここではTaskRepositoryインターフェースを満たすクラスが自動的にtaskrepoに格納される
    • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping
      • @RequestMappingのメソッドを固定したもの
      • @GetMappingの場合、@RequestMapping(method=RequestMethod.GET)と同等
    • @ResponseStatus

動作確認

新規登録

$ curl http://localhost:8080/api/todo -XPOST -H 'Content-Type: application/json' -d '{"summary": "my first task"}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    75    0    47  100    28    170    101 --:--:-- --:--:-- --:--:--   170
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

リスト取得

$ curl http://localhost:8080/api/todos | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    49    0    49    0     0    285      0 --:--:-- --:--:-- --:--:--   286
[
  {
    "id": 1,
    "summary": "my first task",
    "done": false
  }
]

1件取得

$ curl http://localhost:8080/api/todo/1 | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    47    0    47    0     0   2045      0 --:--:-- --:--:-- --:--:--  2136
{
  "id": 1,
  "summary": "my first task",
  "done": false
}

更新

$ curl http://localhost:8080/api/todo/1 -XPUT -H 'Content-Type: application/json' -d '{"summary": "updated my task", "done": true}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    92    0    48  100    44   2632   2412 --:--:-- --:--:-- --:--:--  2666
{
  "id": 1,
  "summary": "updated my task",
  "done": true
}

削除(レスポンスボディがないのでStatusCodeで確認)

$ curl http://localhost:8080/api/todo/1 -XDELETE -v
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> DELETE /api/todo/1 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 204
< Date: Wed, 25 Sep 2019 15:29:37 GMT
<
* Connection #0 to host localhost left intact

まとめ

  • 動かしたいもの自体はめっちゃ簡単に動かせた
  • アノテーション種類多すぎて何使えばいいのかわからん
    • とりあえず今回使ったものくらいは最低限おさえておきたい
    • あとは何かするときに都度調べられればよさそう