Spring Boot でアプリを作成してみた【REST API 開発】

## プロジェクトを作成する

- Project: Maven
- Language: Java
- SpringBoot: 3.0.12

- Group: simple_spring_restapi
- Artifact: simple_spring_restapi
- Name : simple_spring_restapi
- Description: Demo project for Spring Boot
- Package name: simple_spring_restapi

Dependencies
- Spring Boot DevTools
- Spring Web

## Dockerを作成する

① .devcontainerフォルダとdevcontainer.jsonとdocker-compose.ymlを作成する

```.devcontainer / devcontainer.json
{
  "name": "workspace",
  "dockerComposeFile": "docker-compose.yml",
  "service": "workspace",
  "workspaceFolder": "/home/vscode/workspace",
  "remoteUser": "vscode",
  "shutdownAction": "stopCompose",
  "customizations": {
    "vscode": {
      "extensions": [
        "vscjava.vscode-java-pack",
        "vscjava.vscode-spring-initializr",
        "vscjava.vscode-gradle"
      ]
    }
  }
}
```

② docker/workspaceファイルとDockerfileを作成する

```.devcontainer/Dockerfile
# イメージのベースとなるイメージを指定
FROM ubuntu:22.04

# イメージのベースとなるイメージを指定（Mac用)
# FROM arm64v8/alpine:latest

# イメージに追加するファイルを指定
ARG USERNAME=vscode
ARG USER_GROUP_NAME=workspace
ARG USER_UID=1000
ARG USER_GID=1000

ARG PKG="git vim curl unzip zip"

# イメージの起動時に実行するコマンドを指定
SHELL [ "/bin/bash","-c" ]

RUN apt-get update \
    && apt-get install -y ${PKG} \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && grouapp --gid ${USER_GID} ${USER_GROUP_NAME} \
    && useradd --uid ${USER_UID} --shell /bin/bash --gid ${USER_GID} -m{USERNAME} \
    && echo %${USER_GROUP_NAME} ALL=\(ALL\) NOPASSWORD:ALL > /etc/sudoers.d/${USER_GROUP_NAME} \
    && chmod 0440 /etc/sudoers.d/${USER_GROUP_NAME}

## JAVAの環境構築
ARG JAVA _VARSION=１８.０.2-amzn
ARG GRADLE_VARSION=7.5

RUN su ${USERNAME} --command \
    'curl -s "https//get.sdkman.io" | bash \
    && source "${HOME}/.sdkman/bin/sdkman-init.sh" \
    && sdk install java "${JAVA_VARSION}" \
    && sdk install gradle "${GRADLE_VARSION}"'
```
③ docker-compose.ymlを編集する

```docker-compose.yml
version: '3.9'

services:
  workspace:
    container_name: ${PROJECT_NAME:default}-workspace
    build:
      context: ./docker/workspace
      args:
        USERNAME: ${USERNAME:-vscode}
        USER_GROUP_NAME: ${USER_GROUP_NAME:-workspace}
        USER_UID: ${USER_UID:-1000}
        USER_GID: ${USER_GID:-1000}
    tty: true
    volumes:
      - ../:/home/${USERNAME:-vscode}/workspace:cached
    ports:
      - '8080:8080'
```

④ .devcontainerに.envを作成する
```.devcontainer/.env

PROJECT_NAME=simple-spring-restapi

USERNAME=vscode
USER_GROUP_NAME=workspace
USER_UID=1000
USER_UID=1000
```

⑤ docker compose.yml buildを行う

```
docker compose.yml build
```

## コントローラーを作成する

① src/main/java/simple_spring_restapiにcontrollersフォルダとTodoController.javaファイルを作成する

```controller / TodoController.java
package simple_spring_restapi.controller;

import java.net.URI;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import jakarta.validation.Valid;
import simple_spring_restapi.models.TodoItem;
import simple_spring_restapi.services.TodoService;

@RequestMapping(path = TodoController.BASE_URL)
@RestController
public class TodoController {
    public static final String BASE_URL = "/api/v1/todos";
    @Autowired
    private TodoService _todoService;

    @GetMapping(path = "")
    public ResponseEntity<List<TodoItem>> getTodoItems() {
        List<TodoItem> todoItems = _todoService.getTodoItems();
        return ResponseEntity.ok(todoItems);
    }

    @GetMapping(path = "/{id}")
    public ResponseEntity<TodoItem> getTodoItems(@PathVariable int id) {
        TodoItem found = _todoService.getTodoItemById(id);
        return ResponseEntity.ok(found);
    }

    @PostMapping(path="")
    public ResponseEntity<todoItem> creteTodoItems(@Valid @RequestBody TodoItem newTodoItem) {
        TodoItem saveTodoItem = _todoService.saveTodoItem(newTodoItem);
        URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}");
             .bulidAndExpand(saveTodoItem.getId()).toUri();
        return ResponseEntity.created(location).body(newTodoItem);
    }

    @PutMapping(path = "/{id}")
    public ResponseEntity<?> updateTodoItems(@RequestBody TodoItem newTodoItem, @PathVariable int id) {
        _todoService.updateTodoItems(id, newTodoItem);
        return ResponseEntity.noContent().build();
    }

    @DeleteMapping(path = "/{id}")
    public ResponseEntity<?> removeTodoItems(@PathVariable int id) {
        _todoService.deleteTodoItem(id);
        return ResponseEntity.noContent().build();
    }

}
```


② build.grandleファイルにimplementation 'org.springdoc:springdoc-openapi-ui:1.6.9'を加えて、
swagger-uiが使用できるようにする

```build.grandle
//省略
dependencies {
//省略
	implementation 'org.springdoc:springdoc-openapi-ui:1.6.9'
}
```

③　src/main/java/simple_spring_restapiにmodelsフォルダとTodoItem.javaファイルを作成する

```TodoItem.java
package simple_spring_restapi.models;

import jakarta.persistence.Column;
import jakarta.annotation.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "todos", schema = "business")
public class TodoItem {
    @Id
    @GeneratedValue(strategy = GenerationType.Auto)
    @Column(name = "id", nullable = false)
    private int id;
    @NotBlank(message = "Title must not be blank.")
    @Column(name = "id", nullable = false)
    private String title;

}

```
④ resouces/application.propertiesをapplication.yamlに変更する
```application.yaml

spring:
  jpa:
    database: POSTGRESQL
    hibernate:
      ddl-auto: update
      show-sql: true
    properties:
      hibernate:
        hbm2dll: create_namespaces:true

  datasource:
    url: jdbc:postgresql://${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:5432}/ ${POSTGRES_DB:-dev}
    username: ${POSTGRES_USER:-postgres}
    password: ${POSTGRES_PASSWORD:-postgres}
    driver-class-name: org.postgresql.Driver
```

⑤ src/main/java/javnautas.fakeapiusにbusinessフォルダとserviceフォルダとFakeApiService.javaファイルを作成する

```service/FakeApiService.java


```


⑥ src/main/java/javnautas.fakeapius/appv1.dtoにFakeApi Controller.javaファイルを作成する


⑦ SpringDoc OpenAPI Starter WebMVC UI で、Spring Boot アプリケーションの RESTful API の OpenAPI ドキュメントを生成する
```build.gradle
// 省略
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
// 省略
```

⑧ Swagger UIを開き、fake-apiの定義を読み込み、HTML 形式で API のドキュメントを生成する

⑨ instructureにentitiesフォルダとProductoEntity.javaを作成する

[SpringDoc OpenAPI Starter WebMVC UI](https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui)
※ SpringDoc OpenAPI Starter WebMVC UI は、Spring Boot アプリケーションの RESTful API の OpenAPI ドキュメントを生成するための Spring Boot Starter 

## servicesを作成する

① src/main/java/simple_spring_restapiにservicesフォルダとTodoServices.javaファイルを作成する

② src/main/java/simple_spring_restapi/servicesに
TodoServiceImplWithoutRepo.javaファイルを作成する

## エラーハンドラーの追加する
① src/main/java/simple_spring_restapiにerrorsフォルダとNotFoundException.javaファイルを作成する


② src/main/java/simple_spring_restapi/errorsにBadRequestException.javaファイルを作成する

③　src/main/java/simple_spring_restapi/errorsにRestResponseEntityExceptionHandler.javaファイルを作成する

④ src/main/java/simple_spring_restapi/errorsにErrorResponse.javaファイルを作成する

⑤ src/main/java/simple_spring_restapi/errorsにHttpException.javaファイルを作成する

## モデルバリデーションの追加
① build.grandleファイルに
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.3'
compileOnlyorg.projectlombok:lombok:1.18.24'を加えて、modelヴァリデーションが使用できるようにする

```build.grandle
//省略
dependencies {
//省略
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.3'
	compileOnly 'org.projectlombok:lombok:1.18.24'
}
```


## データアクセスレイヤーの追加 
① .devcontainerフォルダとdevcontainer.jsonとdocker-compose.ymlに追加する

```docker-compose.yml
version: '3.9'

services:
  workspace:
    container_name: ${PROJECT_NAME:default}-workspace
    build:
      context: ./docker/workspace
      args:
        USERNAME: ${USERNAME:-vscode}
        USER_GROUP_NAME: ${USER_GROUP_NAME:-workspace}
        USER_UID: ${USER_UID:-1000}
        USER_GID: ${USER_GID:-1000}
    tty: true
    volumes:
      - ../:/home/${USERNAME:-vscode}/workspace:cached
    ports:
      - '8080:8080'

  postgres:
    container_name: ${PROJECT_NAME:default}-postgres
    image: postgres:14.2
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
      POSTGRES_DB: ${POSTGRES_DB:-dev}
    volumes:
      - ./data/db:/var/lib/postgresql/data
    ports:
      - ${POSTGRES_PORT:-5432}:5432
    restart: always

  pgadmin4:
    container_name: ${PROJECT_NAME:default}-pgadmin4
    restart: always
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-pgadmin4@pgadmin.com}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-password}
    volumes:
      - ./pgadmin:/var/lib/pgadmin
    ports:
      - 8080:80
    depends_on:
      - postgres
```
② .envを編集する

```.env
PROJECT_NAME=simple-spring-restapi

# workspace
USERNAME=vscode
USER_GROUP_NAME=workspace
USER_UID=1000
USER_UID=1000

# postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=dev

# pgadmin4
PGADMIN_DEFAULT_EMAIL=pgadmin4@pgadmin.com
PGADMIN_DEFAULT_PASSWORD=password
```
③ resources/application.propertiesを編集する

application.properties ⇨ application.ymlに変更する
```resources/application.yml
spring:
  jpa:
    database: POSTGRESQL
    hibernate:
      ddl-auto: update
      show-sql: true
    properties:
      hibernate:
        hbm2dll: create_namespaces:true

  datasource:
    url: jdbc:postgresql://${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:5432}/ ${POSTGRES_DB:-dev}
    username: ${POSTGRES_USER:-postgres}
    password: ${POSTGRES_PASSWORD:-postgres}
    driver-class-name: org.postgresql.Driver
```
④ build.grandleファイルにコードを加える

```build.grandle
//省略
dependencies {
//省略
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.3'
implementation 'org.postgresql:postgresql:42.5.0'
}
```
⑤ TodoItem.javaを編集する

```models / TodoItem.java
package simple_spring_restapi.models;

import jakarta.persistence.Column;
import jakarta.annotation.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "todos", schema = "business")
public class TodoItem {
    @Id
    @GeneratedValue(strategy = GenerationType.Auto)
    @Column(name = "id", nullable = false)
    private int id;
    @NotBlank(message = "Title must not be blank.")
    @Column(name = "id", nullable = false)
    private String title;

}
```

⑥ repositoriesフォルダとTodoRepositories.java を作成する

```repositories/TodoRepositories.java 
package simple_spring_restapi.repositories;

import org.springframework.stereotype.Repository;

import com.juniordevmind.simplespringrestapi.models.TodoItem;

@Repository
public interface TodoRepositories extends JpaRepository<TodoItem, Integer> {

}
```

⑦ src/main/java/simple_spring_restapi/services/TodoServiceImpl.javaファイルを作成する
```services/TodoServiceImpl.java
package simple_spring_restapi.services;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import org.webjars.NotFoundException;

import com.juniordevmind.simplespringrestapi.models.TodoItem;

import simple_spring_restapi.repositories.TodoRepositories;

@Service
public class TodoServiceImpl implements TodoService {

    @Autowired
    private TodoRepository _todoRepository;

    @Override
    public TodoItem getTodoItemsById(int id) {
        return _findTodoItemById(id);
    }

    @Override
    public List<TodoItem> getTodoItems() {
        return _todoRepository.findAll();
    }

    @Override
    public Todo removeTodoItemById(int id) {
        _todoRepository.deleteById(id);
    }

    @Override
    public TodoItem saveTodoItem(TodoItem todoItem) {
        return _todoRepository.save(todoItem);
    }

    @Override
    public TodoItem updateTodoItem(int id, TodoItem todoItem) {
        return _todoRepository.save(todoItem);
    }

    private TodoItem _findTodoItemById(int id) throws NotFoundException {
        Optional<TodoItem> found = _todoRepository.findById(id);
        if (!found.isPresent()) {
            throw new NotFoundException("The todo item is not available");
        }
        return found.get();
    }
}
```


## 参考サイト
[【Spring Boot】REST API 開発 - Dockerとvscodeを使った、Javaの開発環境構築 #1](https://www.youtube.com/watch?v=VHpO8I_kBCo&t=106s)
[【Spring Boot】REST API 開発 - Spring Boot プロジェクトの生成 #2](https://www.youtube.com/watch?v=Qy7ZCnkxXCY)
[【Spring Boot】REST API 開発 - コントローラーの立ち上げ #2.5]()


[【Spring Boot】REST API 開発 - モデルバリデーションの追加 #6](https://www.youtube.com/watch?v=qt86vGbU3sU)
[【Spring Boot】REST API 開発 - データアクセスレイヤーの追加 #7](https://www.youtube.com/watch?v=gZnXugbsRKc)