2023年4月14日

Spring Boot Beginning

Spring Boot Beginning

Index

使用 VSCode Dev Containers 開發環境

開啟 VSCode,點選 F1Ctrl+Shift+P,輸入 Dev Containers: Open Folder in containers...。選擇專案目錄,並選用 Java 作為 Dev Containers 為 Image。等待機分鐘後完成 container 啟動後,開啟 TERMINAL,並輸入下列指令來確定 Java 可正確執行:

$ java -version
$ javac -version

因為 Dev Containers 為 Linux 環境,可以用下列列指令, OS 版號,以及查詢 java 安裝位置

# 查詢 OS
cat /etc/os-release

# 查詢 Java 安裝位置
which javac

加入 GitLab

  1. 在 GitLab Create new project, 選擇 Create blank project.
  2. 執行下列指令, 以便將 local 的資料上傳到 GitLab.
git init
git remote add origin https://gitlab.com/<group>/<project>.git
git add .
git commit -m "Initial commit"
git push -u origin master

建立 Java Spring Boot 專案

要建立 Spring Boot 專案,可以利用 VS Code 的 Extension: Spring Initializr Java Support。輸入 Ctrl+Shift+X 查詢,並點選 Install in Dev Container: Java 按鍵來安裝。

安裝完成後,開啟 Command Palette Ctrl+Shift+PF1 並輸入 spring 後,選擇 Maven 或 Gradle 來建立專案。

本篇文章以 Spring Boot API 為主,使用 Maven 管理套件。因 Spring Boot 已內建 Tomcat, Jetty, Undertow 等 web server,其輸出檔案採用 Jar 即可。而 dependencies 可勾選 Spring Web, Spring Boot DevTools, 及 Spring Data。

執行 mvn -v 檢查 Marven 是否正確安裝。

接下來透過 Maven 來建立 package

mvn package

假設產出的檔案為 backend-api-0.0.1-SNAPSHOT.jar, 那麼可以執行下列指令開啟

java -jar ./target/backend-api-0.0.1-SNAPSHOT.jar

在開發期間,可以使用下列指令,以便修改程式後,即可立即編譯與重啟

mvn spring-boot:run

建立 Open API 與 Swagger UI

我們可以使用 springdoc-openapi v2.1.0 在 Spring Boot 3.0.0 顯示 swagger ui。請參考 springdoc-openapi v2.1.0 官方文件,只要在 pom.xml 加入下列文字即可:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
  <version>2.1.0</version>
</dependency>

網址 http://server:port/context-path/swagger-ui.html 可以顯示 Swagger UI 頁面,而 Open API Spec (JSON) 則是位於 http://server:port/context-path/v3/api-docs 路徑下。

如果只想產出 OpenAPI 文件 (json),而不要產出 UI, 可以指引入下列 dependency:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
  <version>2.1.0</version>
</dependency>

若要使用 spring boot 2.x 版,其套件為:

<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-ui</artifactId>
  <version>1.7.0</version>
</dependency>

Wwagger-ui 與 open api spec 路徑相同。

Spring Boot Layerd Architecture

原始碼目錄結構建議如下:

src/
├── main/
│   ├── java/
│   │   ├── beginning/
│   │   │   ├── example/
|   │   │   │   ├── backendapi/
|   │   │   │   │   ├── filters/
|   │   │   │   │   ├── configs/
|   │   │   │   │   ├── controllers/
|   │   │   │   │   ├── services/
|   │   │   │   │   ├── repositories/
|   │   │   │   │   └── models/
|   │   │   │   └── BackendApiApplication.java
|   │   │   └── resources/
|   │   │       └── application.properties
|   │   └── ...
│   └── ...
└── ...
  • controllers: 接收 http request 並回應 response。
  • models: 定義各項傳送與處理的資料結構。
  • services: 處理資料、商業邏輯。
  • repositories: 與後端資料庫連結,以存取資料。
  • filters: 應用於 middleware 程式,以 OncePerRequestFilter 為 base class,可以在 http request 與 response 加入 log 或安全檢核等作業。
  • configs: 實踐 WebMvcConfigurer 以便將上列的 filter 程式加入適當位置。

Controllers

用來簡化了開發 RESTful Web 服務。使用 @RestController Annotation 加註於 class 上方,可以接收 Get、Post、…等等 HTTP Request。

Services

負責處理業務邏輯,並且是其他層(例如控制器)的媒介。Class 上方加註 @Service,Spring 框架就會將於程式啟動時,自動建立 instance。Controller 可以透過 @Autowired 來宣告 service 變數,即可自動取得其 instance 來使用。若系統同時存在多個 service,可以利用 @Qualifier 來指定。

例如程式中包含 userService1, 與 userService2

@Service("userService1")
public class UserService1Impl implements UserService {
    // ...
}

@Service("userService2")
public class UserService2Impl implements UserService {
    // ...
}

若 controller 選用其中一個 service,範例如下

@RestController
public class UserController {
    private final UserService userService;

    @Autowired
    public UserController(@Qualifier("userService1") UserService userService) {
        this.userService = userService;
    }

    // ...
}

// 也可以不透過建構式,直接將 @Qualifier 設定在 service 變數上,使程式碼較為簡潔
@RestController
public class UserController {
    @Autowired
    @Qualifier("userService1")
    private UserService userService;

    // ...
}

若同時使用兩組 service,範例如下

@RestController
public class UserController {
    private final UserService userService1;
    private final UserService userService2;

    @Autowired
    public UserController(@Qualifier("userService1") UserService userService1, @Qualifier("userService2") UserService userService2) {
        this.userService1 = userService1;
        this.userService2 = userService2;
    }

    // ...
}

Repositories

負責處理資料的存取,例如與 database 連結,進行讀寫作業。

Models

定義資料的物件形式,例如定義 HTTP Request、Response 所需要的資料,或內部轉換所需要的資料格式等。若要透過 JPA 直接對應到 SQL 型態的 database,可以加註 @Entity、@Table、@Column 與其對應,範例如下

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name")
    private String name;

    @Column(name = "email")
    private String email;

    // getter and setter methods...
}

Filters

Spring Boot 的 Filter 為 http request 的 middleware,可以攔截 http request 與 response 的資訊,並進行處理,例如加入 log、安全檢核、加解密等作業。

方式很簡單,只要繼承 OncePerRequestFilter 並複寫 doFilterInternal 方法即可。

但完成 Filter 程式碼後,還需要透過 configure 向 spring 註冊。

@Configuration

利用 @Configuration 將 Filter 定義為 @Bean,以便注入容器 (Dependency Injection Container)。Spring Boot 會在 DI Container 中尋找 interface 的 Filter,在接收到 http request 時來呼叫。Interface Filter 定義如下:

package javax.servlet;
import java.io.IOException;

public interface Filter {
    void init(FilterConfig filterConfig) throws ServletException;
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
    void destroy();
}

Configure File: application.properties

執行 mvn package 後,會將設定檔案 application.properties 複製到 target/classes 目錄下。application.properties 可以用來存放因安裝環境不同而需要變動的資料,例如資料庫連線位置。

Dockerfile

建立 Docker Image:

docker build -t try-spring-boot .

反覆執行時,舊的 try-spring-boot image 不會刪除,而會將名稱改為 <none>,下列語法可以刪除這些不用的 <none> image

docker image prune -f
# 或串聯之前 build 語法
docker build -t try-spring-boot . && docker image prune -f

執行 Container:

docker run -d --name try-spring-boot -p 8080:8080 try-spring-boot

若要查看執行中 container 的 log, 可以使用下列指令

docker logs try-spring-boot

# 或持續輸出 log, 直到輸入 Ctrl-C
docker logs try-spring-boot -f

停止 Container:

docker stop try-spring-boot

開啟網頁:

連接 MongoDB

首先,我們可以先試著執行兩個 container,其中一個負責 mongoDB,另一個執行 mongosh,透過 docker network 的指派,來進行連線,與存取資料。下列範例會以 mongo shell 建立並進入 test dbs。

docker network create try-spring-boot-network
docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d mongo
docker run --name try-spring-boot-mongo-sh --rm --network try-spring-boot-network -it mongo mongosh --host try-spring-boot-mongo test

在上列指令中,第二段指令,在沒有指定 --hostname 參數下,host name 預設等於 container name。因此 shell 可以經由此 host name 與 mongoDB 進行連線。但本機連接容器仍然需要使用 localhost 來進行連接。
要離開 mongo shell,可以輸入 .exit 即可。

如果要透過環境變數設定帳號與密碼,可以使用下列指令

docker network create try-spring-boot-network
docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=example mongo

docker run --name try-spring-boot-mongo-sh --rm --network try-spring-boot-network -it mongo /bin/bash
# 進入 try-spring-boot-mongo-sh 後,輸入下列命令啟動 mongosh
mongosh --host try-spring-boot-mongo --username root --password example --authenticationDatabase admin

若要在 dev container 中與上列所建立的 mongo 連線,則需要先在 ./devcontainer/devcontainer.json 指定相同的 network

{
	"name": "Java",
	"image": "mcr.microsoft.com/devcontainers/java:0-17",
  // 將 dev container 也加入 try-spring-boot-network 網路中
	"runArgs": [
		"--network=try-spring-boot-network"
	],

	"features": {
		"ghcr.io/devcontainers/features/java:1": {
			"version": "none",
			"installMaven": "true",
			"installGradle": "false"
		}
	}
}

此外,修改 application.properties,及 vscode launch.json 來設定連線。

spring.data.mongodb.uri=mongodb://${MONGO_USERNAME:root}:${MONGO_PASSWORD:example}@${MONGO_HOST:mongo}:${MONGO_PORT:27017}/${MONGO_DATABASE:test}?authSource=${MONGO_AUTH_SOURCE:admin}&authMechanism=${MONGO_AUTHMECHANISM:SCRAM-SHA-1}
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "java",
            "name": "BackendApiApplication",
            "request": "launch",
            "mainClass": "beginning.example.backendapi.BackendApiApplication",
            "projectName": "backend-api",
            "env": {
                "MONGO_USERNAME": "root",
                "MONGO_PASSWORD": "example",
                "MONGO_HOST": "try-spring-boot-mongo",
                "MONGO_PORT": "27017",
                "MONGO_DATABASE": "test",
                "MONGO_AUTH_SOURCE": "admin",
                "MONGO_AUTHMECHANISM": "SCRAM-SHA-1"
            }
        }
    ]
}

要同時啟動多個容器,docker-compose 會是一個更加便利的選擇,透過下列方式,可以將先前的 Java 程式也一併啟用。

version: '3.1'

services:

  mongo:
    image: mongo
    container_name: try-spring-boot-mongo
    restart: always
    volumes:
      - ${PWD}/temp-db-storage:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    networks:
      - try-spring-boot-network

  mongo-express:
    image: mongo-express
    container_name: try-spring-boot-mongo-express
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
      ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
    networks:
      - try-spring-boot-network

  try-spring-boot:
    image: try-spring-boot
    container_name: try-spring-boot
    ports:
      - "8080:8080"
    environment:
      MONGO_USERNAME: root,
      MONGO_PASSWORD": example,
      MONGO_HOST: try-spring-boot-mongo,
      MONGO_PORT: 27017,
      MONGO_DATABASE: test,
      MONGO_AUTH_SOURCE: admin,
      MONGO_AUTHMECHANISM: SCRAM-SHA-1
    networks:
      - try-spring-boot-network      

networks:
  try-spring-boot-network:
    driver: bridge

上列 yaml 可以透過下列指令來啟動或停止

docker-compose up -d
docker-compose down

建立 DevContainer 測試環境

要在 DevContainer 中進行 debug,可以參考專案根目錄的 PowerShell Script setup-mongo-container.ps1 來建立 docker network 並啟動 mongo。同時注意,.vscode/launch.json 應設定相關的 configure.env 資訊。

setup-mongo-container.ps1 內容如下:

# 1. Check if the directory does not exist, then create it
if (!(Test-Path -Path .\temp-db-storage)) {
    New-Item -ItemType Directory -Path .\temp-db-storage
}

# 2. Check if the try-spring-boot-network does not exist, then create it
$networkExists = docker network ls --filter name=try-spring-boot-network --format "{{.Name}}" -q
if (!$networkExists) {
    docker network create try-spring-boot-network
}

# 3. Check if the container exists, stop and remove it if it does, then run the new container
$containerExists = docker container ls -a --filter name=try-spring-boot-mongo --format "{{.Names}}" -q
if ($containerExists) {
    docker container stop try-spring-boot-mongo
    docker container rm try-spring-boot-mongo
}

docker run --name try-spring-boot-mongo --rm --network try-spring-boot-network -v ${pwd}/temp-db-storage:/data/db -d -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=example mongo

Deploying Vue & .NET with Google OAuth on GCP Cloud Run

Deploying Vue & .NET with Google OAuth on GCP Cloud Run Deploying Vue & .NET with Google OAuth on GCP Cloud Run...