2025/02/24

Spring Boot 3 - Basic

Spring 核心有兩個技術:IoC (Invertion of Control) 控制反轉 與 AOP (Aspect Oriented Programming)

IoC 就是以 DI (Dependency Injection) 實現,不修改程式就能把某一個變數所參考到的物件換成某一個相容的物件。

AOP 就是以 proxy design pattern 區分商業與一般業務邏輯,讓開發者更聚焦商業邏輯的開發,一般業務邏輯例如使用者權限檢查、資料庫交易 transaction,或是 log 記錄,這些重複的通用功能。很像是將 business logic functions 以橫切面的方式,加上一般業務邏輯的一些通用功能。

Spring Boot 以 Convention over Configuration 的概念,減少大量設定,只需要定義預設以外的設定。

核心模組

  1. spring-boot

    這是 Spring Boot 最主要的模組,主要功能:

    • 啟動 Spring application 的主類別,有靜態 method 能產生 Spring container 的 context

    • 提供內嵌可自由搭配的 servlet container,例如 Tomcat, Jetty, Undertow

  2. spring-boot-autoconfigure

    常用的自動設定模組,可用 @EnableAutoConfiguration 來做自動設定

  3. spring-boot-starters

    這是所有 starter 的基礎

  4. spring-boot-cli

    CLI 工具,這也是產生 spring application 的方法

  5. spring-boot-actuator

    application 監控模組

  6. spring-boot-actuator-autoconfigure

    為 spring-boot-actuator 提供自動設定的模組

  7. spring-boot-test

    測試模組

  8. spring-boot-test-autoconfigure

    為 spring-boot-test 提供自動設定的模組

  9. spring-boot-loader

    可將 spring boot application 打包成一個可單獨執行的 jar,用 java -jar 執行

  10. spring-boot-devtools

    開發者工具,用在 application 的開發階段。ex: 修改程式可自動重新啟動 application。打包後會自動被禁用。

spring-boot-starter-web 依賴於 spring-webmvc,spring-webmvc 依賴於 spring-beans, spring-core。

Spring Cloud - Spring Boot - Spring MVC - Spring 這四個由上到下的關係

版本

  1. GA (General Availability) 正式版

  2. Current:最新的 GA 正式版

  3. SNAPSHOT:最新的變更,每天編譯的版本

  4. PRE:預覽版

    • Milestore:例如 3.0.0-M3

    • Release Candidate RC:例如 3.0.0-RC2

系統需求

ref: System Requirements :: Spring Boot

Spring Boot 3.3.5 的開發環境需求

  • JDK 17+

  • Spring 6.1.14+

  • Maven 3.6.3+

  • Gradle 7.5+, 8.x

Servlet Container 的版本,必須要是 servlet 5+ 相容的 container

  • Tomcat 10+

  • Jetty 11+

  • Undertow 2.2+

Spring Boot application 可透過 GraalVM 22.3+ 的 native-image tool 或是 Gradle/Maven 的 native build tools plugins 被轉換為 Native Image。

  • GraalVM Community 22.3

  • Native Build Tools 0.10.3

Demo Project

到網站 https://start.spring.io/ 可產生 Spring Boot 的專案

原本預設右邊 Dependencies 是空的,這邊因為要開發一個簡單的 web service,所以加上 Spring Web library。Package 方式有兩種,jar 跟 war,jar 是可以直接執行的,war 必須放到一個 Servlet container 裡面執行,這邊是最後封裝的結果,開發時還是用單獨的 application 執行。

產生的專案,用 Generate 可下載到 zip,解壓縮後,用 IDE 打開

pom.xml 裡面因為 web dependencies 的關係,多了 spring-boot-starter-web 與 spring-boot-starter-tomcat,因封裝方式為 war,所以 spring-boot-starter-tomcat 必須標記 provided

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>tw.com.test</groupId>
    <artifactId>demo1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>demo1</name>
    <description>Test project for Spring Boot</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project

專案中有 mvnw 的 script,這是 maven wrapper。mvnw 只是 mvn 的封裝,指令參數一樣,如果有自己安裝了 maven,就不需要使用 mvnw。

修改 Demo1Application.java

package tw.com.test.demo1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

    @RequestMapping("/hello")
    public String helloWorld() {
        return "hello world";
    }
}

加上 @RestController 以及 @RequestMapping("/hello") 這兩個部分。

專案中還有一個 ServletInitializer.java,這是因為 packaging 方式設定為 war 才有的。

啟動方式,可在 IDE 裡面執行 Demo1Application,或是在 console 用以下方式啟動

$ mvn spring-boot:run
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< tw.com.test:demo1 >--------------------------
[INFO] Building demo1 0.0.1-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] >>> spring-boot-maven-plugin:3.3.5:run (default-cli) > test-compile @ demo1 >>>
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:resources (default-resources) @ demo1 ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] Copying 0 resource from src/main/resources to target/classes
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:compile (default-compile) @ demo1 ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 2 source files with javac [debug parameters release 17] to target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.3.1:testResources (default-testResources) @ demo1 ---
[INFO] skip non existing resourceDirectory /Users/charley/project/idea/book/test/demo1/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.13.0:testCompile (default-testCompile) @ demo1 ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug parameters release 17] to target/test-classes
[INFO]
[INFO] <<< spring-boot-maven-plugin:3.3.5:run (default-cli) < test-compile @ demo1 <<<
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:3.3.5:run (default-cli) @ demo1 ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.5)

啟動後,可在 browser 瀏覽 http://localhost:8080/hello ,會看到網頁內容是 "hello world"

Spring Boot CLI

Spring Boot Command Line Interface 可跟  start.spring.io 一樣,產生一個新的 spring boot project,或是用來加密。

該工具是用打包在一起的 Groovy 實作。直接從 Installing Spring Boot :: Spring Boot 下載壓縮檔後,解壓縮,需要設定 SPRING_HOME 環境變數,以及 PATH要增加 $SPRING_HOME/bin

Spring Boot CLI 提供在 bash/zsh 自動補上指令的功能。

ln -s ./shell-completion/bash/spring /etc/bash_completion.d/spring
ln -s ./shell-completion/zsh/_spring /usr/local/share/zsh/site-functions/_spring

RestController vs Controller

RestController 是處理 RESTful Web method,通常用在頁面裡面的資料JSON 或 XML。

Controller 是傳統的 html/jsp 網頁,在 Spring Boot 可搭配 thymeleaf template engine 使用。

在剛剛的 Demo Project 裡面,修改 XML 增加 thymeleaf

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

新增 Test.java

package tw.com.test.demo1.controller;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Test {

    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        request.setAttribute("name", "UserName");
        // "test" 會映射到 Thymeleaf templates/test.html
        return "test";
    }
}

新增 resources/templates/test.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
    <meta charset="UTF-8">
    <title>Thymeleaf Template</title>
</head>

<body>
<span th:text="${name}" /> ,Spring Boot!
</body>

</html>

啟動測試後,http://localhost:8080/hello 會看到 hello world 網頁內容。

http://localhost:8080/test 會看到 UserName ,Spring Boot! 網頁內容

IDE Plugin

如果是使用 Eclipse,可安裝 Spring Tool Suite (STS) plugin,該 plugin 有跟網站 https://start.spring.io/ 一樣的功能,可產生 Spring Boot 的專案

如果是使用 IDEA,在 New Project 可選擇 Spring Initializer,也有類似的功能。

2025/02/17

Queue in Java

Queue 是 java.util package 裡面的 Collection framwork 中的其中一個介面,主要是定義 First-in-First-out FIFO queue 這種介面。除了基本的 Collection 操作方法以外,Queue 還提供了專屬的 insert/get/inspect method。

throw Exception return false/null
insert add(e) offer(e)
remove remove() poll()
examine element() peek()
    @Test
    public void queue_test1() {
        Queue<String> queue = new LinkedList<>();
        queue.add("one");
        queue.add("two");
        queue.add("three");
        assertEquals("[one, two, three]", queue.toString());

        queue.remove("two");
        assertEquals("[one, three]", queue.toString());

        String element = queue.element();
        assertEquals("one", element);
        assertEquals("[one, three]", queue.toString());

        // To empty the queue
        queue.clear();
        queue.offer("one");
        queue.offer("two");
        queue.offer("three");
        assertEquals("[one, two, three]", queue.toString());

        // poll 是取得 queue 的第一個 element
        String pollElement = queue.poll();
        assertEquals("one", pollElement);
        assertEquals("[two, three]", queue.toString());

        // peek 是取得 queue 的第一個 element,但只是偷看,不會從 queue 移除該 element
        String peakElement = queue.peek();
        assertEquals("two", peakElement);
        assertEquals("[two, three]", queue.toString());
    }

    @Test
    public void queue_test2() {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(2);
        queue.add("one");
        queue.add("two");
        // offer 在 insert 超過 Queue 容量時,會產生 exception
        IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
            queue.add("three");
        });

        // offer 在 insert 超過 Queue 容量時,不會產生 exception
        // 只是回傳一個 true/false flag 代表這個 insert 有沒有成功
        queue.clear();
        queue.offer("one");
        assertTrue( queue.offer("two") );
        assertFalse( queue.offer("three") );
        assertEquals("[one, two]", queue.toString());

        queue.clear();
        // remove, element 在 Queue 沒有任何資料時,會產生 exception
        NoSuchElementException exception2 = assertThrows(NoSuchElementException.class, () -> {
            queue.remove();
        });
        NoSuchElementException exception3 = assertThrows(NoSuchElementException.class, () -> {
            queue.element();
        });
        // poll, peek 會在 Queue 為空的時候,回傳 null
        assertNull(queue.poll());
        assertNull(queue.peek());
    }

sub interfaces

Queue 有三個主要的子介面: Blocking Queue, Transfer Queue, Deque

Blocking Queue

增加 methods,可強制 threads 等待 queue,例如在取得 queue 的元素時,可一直等待 queue 裡面有元素才回傳。或是可以等待 queue 清空後,再新增元素時。

Blocking Queue 的實作包含了 LinkedBlockingQueue, SynchronousQueue 及ArrayBlockingQueue

除了既有的 add(), offer() 以外,另外還有

  • put()

    insert 一個元素,等待 queue 有空間才能 put 進去

  • offer(E e, long timeout, TimeUnit unit)

    insert 一個元素,等待 queue 有空間才能 put 進去,等待的時間有 timeout 機制

remove 部分除了既有的 remove(), poll() 以外,還有

  • take()

    取得第一個元素,當 queue 為空的時候,會 blocking thread,等待 queue 有元素可以取得

  • poll(long timeout, TimeUnit int)

    取得第一個元素,當 queue 為空的時候,會 blocking thread,等待 queue 有元素可以取得,等待的時間有 timeout 機制

import java.util.Random;
import java.util.concurrent.BlockingQueue;

class Producer extends Thread {
    protected BlockingQueue<Integer> blockingQueue;
    private int limit;

    Producer(BlockingQueue<Integer> blockingQueue, int limit) {
        this.blockingQueue = blockingQueue;
        this.limit = limit;
    }

    public void run() {
        Random random = new Random();
        for(int i = 1; i <= limit; i++) {
            try {
                // random 放入 1/2 個 integer
                int randomProducer = random.nextInt(2);
//                System.out.println("randomProducer=" + randomProducer);
                for(int j = 0; j <= randomProducer; j++) {
                    System.out.println("Producer put " + (i+j));
                    blockingQueue.put((i+j)); // to produce data
                }
                i = i+randomProducer;
                // produce data with an interval of 0.5 sec
                Thread.sleep(500);
            } catch (InterruptedException exp) {
                System.out.println("An interruption occurred at Producer");
            }
        }
    }
}

Consumer.java

import java.util.concurrent.BlockingQueue;

class Consumer extends Thread {
    protected BlockingQueue<Integer> blockingQueue;
    Consumer(BlockingQueue<Integer> blockingQueue) { // constructor
        this.blockingQueue = blockingQueue;
    }
    public void run() { // overriding run method
        try {
            while (true) {
                Integer elem = blockingQueue.take(); // to consume data
                System.out.println("Consumer take " + elem);
            }
        }
        // to handle exception
        catch (InterruptedException exp) {
            System.out.println("An interruption occurred at Consumer");
        }
    }
}

CPTest.java

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class CPTest {
    public static void main(String[] args) throws InterruptedException {
        // create an object of BlockingQueue
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>(5);

        // passing object of BlockingQueue as arguments
        Producer threadProd = new Producer(blockingQueue, 20);
        Consumer threadCon = new Consumer(blockingQueue);

        // to start the process
        threadProd.start();
        threadCon.start();

        // to exit the process after 5 sec
        Thread.sleep(2000);
        System.exit(0);
    }
}

執行結果

Producer put 1
Producer put 2
Consumer take 1
Consumer take 2
Producer put 3
Consumer take 3
Producer put 4
Consumer take 4
Producer put 5
Consumer take 5

Transfer Queue

extends BlockingQueue 介面,並套用 producer-consumer pattern,可控制 producer 到 consumer 資料流動的速度。

Transfer Queue 的實作包含了 LinkedTrasferQueue。

Producer.java

import java.util.concurrent.TimeUnit;
import java.util.concurrent.TransferQueue;

class Producer extends Thread {
    protected TransferQueue<Integer> transferQueue;
    private int limit;

    Producer(TransferQueue<Integer> transferQueue, int limit) {
        this.transferQueue = transferQueue;
        this.limit = limit;
    }

    public void run() {
        for(int i = 1; i <= limit; i++) {
            try {
                System.out.println("Producer put " + i);
                boolean added = transferQueue.tryTransfer(i, 4000, TimeUnit.MILLISECONDS);
                if( !added ) {
                    i = i-1;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Consumer.java

import java.util.concurrent.TransferQueue;

class Consumer extends Thread {
    protected TransferQueue<Integer> transferQueue;
    Consumer(TransferQueue<Integer> transferQueue) { // constructor
        this.transferQueue = transferQueue;
    }
    public void run() {
        try {
            while (true) {
                Integer elem = transferQueue.take(); // to consume data
                System.out.println("Consumer take " + elem);
            }
        } catch (InterruptedException exp) {
            System.out.println("An interruption occurred at Consumer");
        }
    }
}

TransferQueueTest.java

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;

public class TransferQueueTest {
    public static void main(String[] args) throws InterruptedException {
        TransferQueue<Integer> transferQueue = new LinkedTransferQueue<>();

        // passing object of BlockingQueue as arguments
        Producer threadProd = new Producer(transferQueue, 5);
        Consumer threadCon = new Consumer(transferQueue);

        // to start the process
        threadProd.start();
        threadCon.start();

        // to exit the process after 5 sec
        Thread.sleep(2000);
        System.exit(0);
    }
}

Deque

Deque 是 Double-Ended Queue 的縮寫,也就是雙向的 Queue,頭尾都可以 insert/get 資料

Deque 的實作包含了 ArrayDeque。

Operation Method Method throwing Exception
Insertion from Head offerFirst(e) addFirst(e)
Removal from Head pollFirst() removeFirst()
Retrieval from Head peekFirst() getFirst()
Insertion from Tail offerLast(e) addLast(e)
Removal from Tail pollLast() removeLast()
Retrieval from Tail peekLast() getLast()

測試程式

    @Test
    public void deque_test() {
        // Deque as Stack
        Deque<String> stack = new ArrayDeque<>();
        stack.push("one");
        stack.push("two");
        assertEquals("two", stack.getFirst());
        assertEquals("two", stack.pop());
        stack.pop();
        NoSuchElementException exception = assertThrows(NoSuchElementException.class, () -> {
            stack.pop();
        });

        // Deque as Queue
        Deque<String> queue = new ArrayDeque<>();
        queue.offer("one");
        queue.offer("two");
        assertEquals("two", queue.getLast());
        assertEquals("one", queue.poll());
        queue.poll();
        assertNull(queue.poll());
    }

Priority Queue

新的元素要加入 PriorityQueue 時,會立刻以 natural order 或是已經定義的 Comparator 排序

    @Test
    public void priority_queue_test() {
        PriorityQueue<String> integerQueue = new PriorityQueue<>();

        integerQueue.add("one");
        integerQueue.add("two");
        integerQueue.add("three");

        String first = integerQueue.poll();
        String second = integerQueue.poll();
        String third = integerQueue.poll();

        assertEquals("one", first);
        assertEquals("three", second);
        assertEquals("two", third);
    }

Reference

Guide to the Java Queue Interface | Baeldung

Java Queue – Queue in Java | DigitalOcean