2025/03/10

SpringBoot3 - Spring Boot Starter

starter 包含 自動設定及相關 library

官方都是以 spring-boot-starter-* 命名。第三方要以 *-spring-boot-starter 命名,ex: druid-spring-boot-starter

分類

application

name desc
spring-boot-starter 核心 starter,含自動設定、log 及支援 YAML
spring-boot-starter-amqp Spring AMQP, Rabbit MQ
spring-boot-starter-aop Spring AOP
spring-boot-starter-artemis Apache Artemis,支援 JMS 的 MQ
spring-boot-starter-batch Spring Batch
spring-boot-starter-cache Spring Cache
spring-boot-starter-data-cassandra Cassandra + Spring Data Cassandra
spring-boot-starter-data-cassandra-reactive Cassandra + Spring Data Cassandra Reactive
spring-boot-starter-data-couchbase Couchbase + Spring Data Couchbase
spring-boot-starter-data-couchbase-reactive Couchbase + Spring Data Couchbase Reactive
spring-boot-starter-data-elasticsearch Elasticsearch + Spring Data Elasticsearch
spring-boot-starter-data-jdbc Spring Data JDBC
spring-boot-starter-data-jpa Spring Data JPA + Hibernate
spring-boot-starter-data-ldap Spring Data LDAP
spring-boot-starter-data-mongodb MongoDB + Spring Data MongoDB
spring-boot-starter-data-mongodb-reactive MongoDB + Spring Data MongoDB Reactive
spring-boot-starter-data-neo4j Neo4J + Spring Data Neo4J
spring-boot-starter-data-r2dbc Spring Data R2DBC
spring-boot-starter-data-redis Redis + Spring Data Redis + Lettuce
spring-boot-starter-data-redis-reactive Redis + Spring Data Redis Reactive + Lettuce Client
spring-boot-starter-data-rest Spring Data REST + Spring Data repositories,輸出 REST
spring-boot-starter-freemarker 以 FreeMarker View 建立 Spring Web application
spring-boot-starter-graphql Spring GraphQL
spring-boot-starter-grovvy-templates Groovy View 建立 Spring Web application
spring-boot-starter-hateoas Spring MVC + Spring HATEOAS 建立 RESTful Web application
spring-boot-starter-integration Spring Integration
spring-boot-starter-jdbc JDBC + HikariCP connection pool
spring-boot-starter-jersey JAX-RS + Jersey 建立 RESTful Web application,可替代 spring-boot-starter-web
spring-boot-starter-jooq jOOQ 存取 SQL database。可替代 spring-boot-starter-data-jpa 或 spring-boot-starter-jdbc
spring-boot-starter-json 讀寫 JSON
spring-boot-starter-mail Java Mail + Spring Mail Sender
spring-boot-starter-mustache 以 Mustache view 建立 Web Application
spring-boot-starter-oauth2-client Spring Security's OAuth2/OpenID 客戶端連線
spring-boot-starter-oauth2-resource-server Spring Security's OAuth2 資源伺服器
spring-boot-starter-quartz Quartz
spring-boot-starter-rsocket RSocket client and server
spring-boot-starter-security Spring Security
spring-boot-starter-test JUnit Jupiter + Hamcrest + Mockito
spring-boot-starter-thymeleaf Thymeleaf View 建立 MVC web application
spring-boot-starter-validation Java Bean Validation + Hibernate Validator
spring-boot-starter-web Spring MVC 建立 RESTful Web application,以 Tomcat 為內嵌伺服器
spring-boot-starter-web-services Spring Web Services
spring-boot-starter-webflux Spring Reactive Web 建立 WebFlux application
spring-boot-starter-websocket Spring WebSocket

如果官方沒有的 starter,可使用第三方自制的 Spring Boot Starter,ex: Dubbo, ZooKeeper, MyBatis

production

name desc
spring-boot-starter-actuator Sprign Boot Actuator,正式環境的監控與應用管理

technical

可排除或替換預設的技術套件

name desc
spring-boot-starter-jetty 以 Jetty 為 servlet container,替代 spring-boot-starter-tomcat
spring-boot-starter-log4j2 log4j2,替代 spring-boot-starter-logging
spring-boot-starter-logging Logback
spring-boot-starter-reactor-netty 以 Netty 作為內嵌的 reactive http server
spring-boot-starter-tomcat 以 Tomcat 為內嵌的 servlet container,這是預設的,也被用在 spring-boot-starter-web
spring-boot-starter-undertow 以 Undertow 作為內嵌的 servlet container,可替代 spring-boot-starter-tomcat

ex: 使用 Jetty 替代 tomcat

修改pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <!-- exclude tomcat -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
            <!-- <artifactId>spring-boot-starter-undertow</artifactId> -->
        </dependency>

自動設定

所有自動設定的類別都是由 spring-boot-autoconfigure 模組提供的

ref: # 深入理解自動配置原理之@SpringApplcation


MailSender

spring-boot-start-mail 提供了

  • org.springframework.mail.javamail.JavaMailSender

  • org.springframework.mail.javamail.JavaMailSenderImpl

另外有一個自動設定類別

  • org.springframework.boot.autoconfigure.mail.MailSnederAutoConfiguration

該類別被註冊到這個檔案裡面,檔案內容就是自動設定類別的字串

/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

pom.xml

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

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

application.yml

spring:
  mail:
    host: smtp.gmail.com
    username: service@larzio.com
    password: XXXXXX
    properties:
      "[mail.smtp.socketFactory.class]": javax.net.ssl.SSLSocketFactory
      "[mail.smtp.socketFactory.fallback]": false
      "[mail.smtp.socketFactory.port]": 465
      "[mail.smtp.connectiontimeout]": 5000
      "[mail.smtp.timeout]": 3000
      "[mail.smtp.writetimeout]": 5000

mail:
  from: service@test.com
  fromname: service
  bcc:
  subject: Spring Boot Mail Test

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.bind.annotation.RestController;

@EnableConfigurationProperties({MailProperties.class})
@RequiredArgsConstructor
@SpringBootApplication
@RestController
public class Demo1Application {
    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class);
    }
}

MailProperties.java

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
    private String from;
    private String fromname;
    private String bcc;
    private String subject;

}

EmailController.java

package tw.com.test.demo1;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.UnsupportedEncodingException;

@Slf4j
@RequiredArgsConstructor
@RestController
public class EmailController {
    private final JavaMailSender javaMailSender;
    private final MailProperties mailProperties;

    @RequestMapping("/sendSimpleEmail")
    @ResponseBody
    public boolean sendSimpleEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            SimpleMailMessage msg = createSimpleMsg(email, text);
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    @RequestMapping("/sendMimeEmail")
    @ResponseBody
    public boolean sendMimeEmail(@RequestParam("email") String email, @RequestParam("text") String text) {
        try {
            MimeMessage msg = createMimeMsg(email, text, "java.png");
            javaMailSender.send(msg);
        } catch (Exception ex) {
            log.error("Error:", ex);
            return false;
        }
        return true;
    }

    /**
     * @param email
     * @param text
     * @param attachmentClassPathFilename
     * @return
     * @throws MessagingException
     * @throws UnsupportedEncodingException
     */
    private MimeMessage createMimeMsg(String email, String text, String attachmentClassPathFilename) throws MessagingException, UnsupportedEncodingException {
        MimeMessage msg = javaMailSender.createMimeMessage();
        MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(msg, true);
        mimeMessageHelper.setFrom(mailProperties.getFrom(), mailProperties.getFromname());
        mimeMessageHelper.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            mimeMessageHelper.setBcc(mailProperties.getBcc());
        }
        mimeMessageHelper.setSubject(mailProperties.getSubject());
        mimeMessageHelper.setText(text);
        mimeMessageHelper.addAttachment(attachmentClassPathFilename, new ClassPathResource(attachmentClassPathFilename));
        return msg;
    }

    /**
     * @param email
     * @param text
     * @return
     */
    private SimpleMailMessage createSimpleMsg(String email, String text) {
        SimpleMailMessage msg = new SimpleMailMessage();
        msg.setFrom(mailProperties.getFrom());
        msg.setTo(email);
        if (!mailProperties.getBcc().equals("")) {
            msg.setBcc(mailProperties.getBcc());
        }
        msg.setSubject(mailProperties.getSubject());
        msg.setText(text);
        return msg;
    }

}

測試

http://localhost:8080/sendMimeEmail?email=charley@maxkit.com.tw&text=hello

http://localhost:8080/sendSimpleEmail?email=charley@maxkit.com.tw&text=hello

2025/03/03

SpringBoot3 Configuration

Spring Boot 3 的相關設定

設定類別

spring 3.0 以前,都是使用 XML 設定檔。3.0 以後,是透過 @Configuration 註解的類別做設定。

@SpringBootApplication 裡面包含了 @SpringBootConfiguration,該設定可代替 @Configuration

example: @Bean 就類似以前 xml 的 <bean>

@SpringBootConfiguration
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

SprintBoot 可使用一個 @SpringBootConfiguration或是 @Configuration 類別做設定,但也可以區分不同功能的設定,然後再使用 @Import 彙整在一起。

@SpringBootConfiguration
@Import({Config1.class, Config2.class})
public class MainConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

但如果設定類別都在類別掃描路徑上,可以用 @ComponentScan 掃描所有 packages。因 @SpringBootConfiguration 包含了 @ComponentScan,Application 以下的所有 packages。


也可以用 @ImportResource 匯入 XML 設定檔

設定檔

Spring Boot 的主要設定檔在 application.properties

如果有使用 Spring Cloud,有另一個 bootstrap.properties,該設定檔的優先權比 application.properties 高。

  • bootstrap 由 ApplicationContext 載入,優先權高

  • bootstrap 的設定不能被覆寫

設定檔可以用 .properties 或是 .yml (yaml 格式)

server.port = 8090
server.servlet.contextPath = /test

可使用 @PropertySource 匯入

.yml

server:
  port: 8090
  servlet:
    contextPath: /test

可用 YamlPropertiesFactoryBean 轉換為 Properties,或是用 YamlMapFactoryBean 轉換為 Map。也可以用 YamlPropertySourceLoader 在入為 PropertySource


@ConfigurationProperties 支援兩種格式的設定檔


設定綁定

所有已載入 Spring 的設定,都可以用 Environment 取得

@Autowired
private Environment env;

//
String getProperties(String key);

使用 @Value

使用 @Value

  1. ${ property : default_value }

  2. #{ obj.property ?: default_value }

example:

application.properties

jdbc.driverClass=com.mysql

在類別中,可用以下方式取得

    @Value("${jdbc.driverClass}")
    private String driver;

也可以用 @PropertySource

@Data
@Component
@PropertySource(value={"/config/db.properties"})
public class DBProperties {
    @Value("${jdbc.driverClass}")
    private String driverClass;
}

映射到類別、Constructor

可直接以 @ConfigurationProperties 將設定映射到一個類別

ex:

application.yaml

demo1:
  name: test
  users:
    - user1
    - user2
  params:
    place : userplace

demo2:
  name: test2
  age: 11
  birthday: 2024/11/11 11:11:11

demo3:
  name: test3
  age: 22

Demo1.properties

package tw.com.test.demo1;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;
import java.util.Map;

@Data
@ConfigurationProperties(prefix="demo1")
public class Demo1Properties {
    private String name;
    private List<String> users;
    private Map<String, String> params;
}

Demo2Properties.java

package tw.com.test.demo1;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

@Data
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    private String name;
    private int age;
    private Date birthday;

    @ConstructorBinding
    public Demo2Properties(String name,
                           @DefaultValue("1") int age,
//                           @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") Date birthday,
                           @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss", timezone="GMT+8") Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }
}

Demo1Application.java

package tw.com.test.demo1;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;

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

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
        };
    }
}

CommandLineRunner 的用途是,在 CLI 啟動 application 後,會被呼叫的 method

執行結果

demo1 properties: Demo1Properties(name=test, users=[user1, user2], params={place=userplace})
demo2 properties: Demo2Properties(name=test2, age=11, birthday=Mon Nov 11 11:11:11 CST 2024)

Bean 綁定

@ConfigurationProperties 除了用在 class,也可以用在 @Bean

MainConfig.java

@SpringBootConfiguration
public class MainConfig {
    @Bean
    @ConfigurationProperties(prefix="demo3")
    public Demo3Properties demo3Properties() {
        return new Demo3Properties();
    }
}

Demo3Properties.java

package tw.com.test.demo1;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
public class Demo3Properties {
    private String name;
    private int age;
}

Demo1Application 不需要寫在 @EnableConfigurationProperties

@SpringBootApplication
@RequiredArgsConstructor
@EnableConfigurationProperties(value = {Demo1Properties.class, Demo2Properties.class})
@Slf4j
public class Demo1Application {

    private final Demo1Properties demo1Properties;
    private final Demo2Properties demo2Properties;
    private final Demo3Properties demo3Properties;

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

    @Bean
    public CommandLineRunner commandLineRunner() {
        return (args) -> {
            log.info("demo1 properties: {}", demo1Properties);
            log.info("demo2 properties: {}", demo2Properties);
            log.info("demo3 properties: {}", demo3Properties);
        };
    }
}

執行結果

demo3 properties: Demo3Properties(name=test3, age=22)

設定類別掃描

在 Application 加上 @ConfigurationPropertiesScan 可掃描該 package 以下的所有設定類別,如果要掃描特定package,就加上 basePackages

@SpringBootApplicaiton
@RequiredArgsConstructor
@ConfigurationPropertiesScan
public class Application {

}

設定驗證

使用 @ConfigurationProperties 可利用 JSR-303 javax.validation 驗證設定值

pom.xml 要加上套件

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
</dependency>

在設定欄位上加上 @NotNull

@Data
@Validated
@NoArgsConstructor
@ConfigurationProperties(prefix="demo2")
public class Demo2Properties {
    @NotNull
    private String name;

    private int age;
    private Date birthday;

外部設定

設定來源

  • properties

  • YAML

  • 環境變數

  • CLI 參數

設定的優先等級

由低到高

  1. 預設參數 SpringApplication.setDefaultProperties

  2. @PropertySource 綁定的設定

  3. 套用 application 檔案的參數

  4. 設定了 random.* 的參數

  5. 系統環境變數

  6. Java System Properties

  7. java:comp/env 的 JNDI 參數

  8. ServletContext 初始化參數

  9. ServletConfig 初始化參數

  10. 來自 SPRING_APPLICATION_JSON 的參數

  11. CLI 參數

  12. 單元測試裡面的參數

  13. 使用 @TestPropertySource 綁定的設定

  14. Devtools 全域設定參數,來自 $HOME/.config/spring-boot

多個設定檔案的優先等級,低到高

  1. jar 裡面的設定檔

  2. 指定了 profile 的設定檔,ex: application-dev.properties (jar 裡面)

  3. application 設定檔 (jar 外面)

  4. 指定了 profile 的設定檔,ex: application-dev.properties (jar 外面)


CLI 參數,是啟動時,用 -- 開頭的參數

ex:

java -jar application.jar --server.port=8090

用 maven 啟動

mvn spring-boot:run -Dspring-boot.run.jvmArguments='-Dserver.port=8090'

如果不希望把CLI 參數增加到 spring 環境中,可以禁用

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

匯入設定檔

可利用 spring.config.import 指定匯入設定檔的路徑

spring:
  config:
    import:
      - optional:classpath:/config/app.yml

random

RandomVlauePropertySource 可用來注入亂數值,可以產生 int, long, uuid, string

ex:

demo1:
  age: ${random.int[10,100]}
  security:
    security-key: ${random.value}
    security-code: ${random.uuid}

多個設定環境

有可能遇到 正式、測試、開發環境不同的設定

yaml 用 --- 區隔

ex:

spring:
  application:
    name: "Demo1"
---
spring:
  application:
    name: "Production1"
  config:
    activate:
      on-cloud-platform: "kubernates"

properties

spring.application.name=Demo1
#---
spring.application.name=Production1
config.activate.on-cloud-platform=kubernates

以下設定檔,可限制只在 dev 或是 test 環境使用

  • spring.config.activate.on-profile

  • spring.config.activate.on-cloud-platform

spring:
  profiles:
    active: dev
---
spring:
  config:
    activate:
      on-profile: "dev|test"

Profile

profile可以分離設定

如果不指定時,就會使用預設的 Profiledefault,可以修改預設 profile

spring:
  profiles:
    default: dev

啟用 profile

啟用 dev, test

spring:
  profiles:
    active: dev, test

也可以在程式裡面啟用

    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication(Demo1Application.class);
        springApplication.setAdditionalProfiles("dev", "test");
        springApplication.run(args);
    }

profile 可啟用設定跟 bean,可用在 @Component, @Configuration, @ConfigurationProperties

ex: 以下設定只有在啟用 "main" 時,才會載入這個設定

@Profile("main")
@SpringBootConfiguration
public class MainConfig {
}

yaml

spring:
  profiles:
    default: dev
    active: dev, main

@Profile 用在 @ConfigurationProperties 時要注意

  • 如果是用掃描的註冊方式,@Profile 可直接用在 @ConfigurationProperties

  • 如果是用 @EditConfigurationProperties,則要把 @Profile 用在 @EditConfigurationProperties 設定類別上


切換 Profile

spring.profiles.active 設定啟用的 profile

但還是能在 CLI 參數調整,因為 CLI 參數優先權比設定檔高

java -jar application.jar --spring.profiles.active=test
mvn spring-boot:run -Dapp.profiles=test

如果不希望被替代,可改用 include

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main

profile 分組

在 yaml 將 profile 分組,當 main 啟用時, main1, main2 也會被啟用

spring:
  profiles:
    default: dev
    active: dev, main
    # include 的 profile 不會被覆蓋
    include:
      - dev
      - main
    group:
      main:
        - main1
        - main2
spring:
  config:
    active:
      on-profile: main1
---
spring:
  config:
    active:
      on-profile: main2

指定 profile 的設定檔

application-${profile}

ex:

application.yml

application-dev.yml

application-test.yml

application-main.yml

active: dev, main優先順序 default > dev > main


使用限制

  • application.profiles.default

    不指定 profile 的預設 profile

  • application.profiles.active

    啟用的 profiles

  • application.profiles.include

    要包含的 profiles,不會被 CLI 參數覆蓋

  • application.profiles.group

    分組

這些參數不能用在多個設定環境跟指定 profile 設定檔

ex: 以下是錯誤用法

在 application.yml

---
---
spring:
  config:
    activate:
      on-profile: main2
  profiles:
    active: main

在 application-dev.yml 使用 application.profiles.active

spring:
  profiles:
    active: main

設定加密

Spring Boot 沒有提供標準加解密的方法,但有提供 EnvironmentPostProcessor interface,可在 application 啟動前控制 spring 環境

spring 加解密的方法:

  • 使用第三方設定中心,支援自動解密~~~~

  • 使用自訂加解密

  • 使用 Jasypt Spring Boot

第三方設定

Spring Cloud Config

需要加解密的內容,前面會加上 {cipher}

自訂加解密

模仿 spring cloud 的方法

spring:
  datasource:
  username: '{cipher}XXXXXXXX'

用以下方式解密

@Bean
public DataSource dataSource() {
    DataSource dataSource = new DruidDataSource();
    String username = this.getUsername();
    if( username.startWith("{cipher}") ) {
        username = Encrypt.decrypt( username, this.getKey() );
    }
    dataSource.setUsername(username);
    //...
    return dataSource;
}

使用 Jasypt Spring Boot

ref: GitHub - ulisesbocchio/jasypt-spring-boot: Jasypt integration for Spring boot

ref: Spring boot 開發 - 使用 Jasypt 進行加密 | roi's blog

可針對 Spring Boot 專案中的屬性,提供加解密的方案

設定升級

當 Spring Boot 更新版本後,某些設定參數可能已經被修改或刪除,在 pom 加上這個 library 就能自動分析。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-properties-migrator</artifactId>
        </dependency>

在啟動後,就可在 console 看到需要調整的參數

  1. 在程式啟動後,再加入 Spring 的設定參數,這個自動分析不支援。ex: @PropertySource 載入的設定

  2. 設定修改完成後,要將此 library 移除

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

2025/01/20

afick

afick: another file integrity checker Files 是檔案檢查工具,可監控是否有檔案被異動。

install

afick 試用 perl 開發的,故安裝前要先安裝 perl

dnf -y intall perl

根據 [install 文件](afick installation) 的說明,可以到 another file integrity checker - Browse /afick/3.8.0 at SourceForge.net 下載套件或 source code。

Rocky Linux 安裝方式,是安裝 rpm file

rpm -ivh afick-3.8.0-1.noarch.rpm

也可以直接從 source code 編譯安裝。參考 source code 裡面的 INSTALL 文件的說明,編譯安裝步驟為

tar xvfz afick*.tgz
cd  afick*
perl Makefile.pl
make install

如果要直接安裝成 service,就改為以下步驟。會有一個 cronjob 放在 /etc/cron.daily 目錄

tar xvfz afick*.tgz
cd  afick*
perl Makefile.pl Makefile_sys.in
make install

設定

設定檔在 /etc/afick.conf,在設定檔最後面加上要監控的目錄,例如

/var/www/html DIR

使用

# 初始化 afick
afick -c /etc/afick.conf -init

# 監控並檢查檔案
afick -c /etc/afick.conf -k

# 檢查檔案並更新資料庫
afick -c afick.conf --update

在執行初始化時,花了不少時間,25 萬個檔案,大約用了 10 分鐘

afick -c /etc/afick.conf -init

# #################################################################
# MD5 hash of /var/lib/afick/afick => LXVxgXA/BosPirqMhDpowg

# Hash database created successfully. 251267 files entered.
# user time : 88.79; system time : 32.05; real time : 625

修改兩個檔案內容後,測試檢查檔案。

檢查時也將 afick.conf 納入檢查範圍。因預設 exclude_suffix 把 html, htm 排除了,所以用 css file 測試。

afick -c /etc/afick.conf -k

# archive:=/var/lib/afick/archive
# database:=/var/lib/afick/afick
# exclude_suffix:=log LOG html htm HTM txt TXT xml hlp pod chm tmp old bak fon ttf TTF bmp BMP jpg JPG gif png ico wav WAV mp3 avi pyc
# history:=/var/lib/afick/history
# max_checksum_size:=10000000
# running_files:=1
# timing:=1
# dbm:=Storable
# last run on 2024/08/12 10:17:35 with afick version 3.8.0
WARNING: (control) afick internal change : /etc/afick.conf (see below)

# summary changes
deleted directory : /
    number of deleted files         : 1
changed file : /etc/afick.conf
changed file : /var/www/html/index.css

# detailed changes
deleted directory : /
    parent_date         : Thu Aug  1 14:23:12 2024
    number of deleted files         : 1
changed file : /etc/afick.conf
    md5         : 877b96dc1be6083fd4589a96a2767006    f604ce2893a4bda0750b6564c84020b9
    filesize         : 7268    7269
changed file : /var/www/html/index.css
    inode         : 402693690    402705600
# #################################################################
# MD5 hash of /var/lib/afick/afick => ddgOifAlUpJfbRakzwY9tQ

# Hash database : 251265 files scanned, 4 changed (new : 0; delete : 2; changed : 2; dangling : 16; exclude_suffix : 26665; exclude_prefix : 0; exclude_re : 0; masked : 0; degraded : 244)
# user time : 104.85; system time : 20.72; real time : 383

afick_cron

在 /etc/cron.daily/afick_cron 裡面,ACTION 參數決定,cronjob 檢查檔案後,是否要更新資料庫

# the default action is "update" (-u), you can also use "compare" (-k)
ACTION="-u"

References

不只是資安: [工具介紹] Linux 下的檔案完整性偵測工具 - afick

CentOS 7 安裝 AFICK – 檔案安全監控 (更新內容 – 2018/12/12) – Ken Wu

2025/01/13

Netty in Java 2

如何處理 Stream-based Transport

在 TCP/IP 的 stream-based transport 中,接收資料後,會存放在 socket receive buffer,但資料是一連串的 bytes,這代表說,即使用兩個訊息,以獨立的資料封包傳送,在 OS 也只會認為是一連串的 bytes,不代表解讀時也是這樣。

假設 OS TCP/IP 收到這三個資料包要傳送

ABC   DEF   GHI

在接收端可能會讀取到

AB    CDEFG    H   I

因此接收端必須自己整理 bytes 資料,並恢復為原本的狀態,才能正確解讀資料

ABC   DEF   GHI

方案1

在 Time client 的例子中,雖然 32 bits 資料很少,不太可能分片,但也有可能因為 traffic 增加,而出現這種狀況。

最簡單的方式,就是建立一個內部 buffer,累積到 4 bytes,就能繼續處理。

TimeClientHandler1.java

package netty.time;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class TimeClientHandler1 extends ChannelInboundHandlerAdapter {
    private ByteBuf buf;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        // 在 channel 產生時,建立一個 4 bytes buffer
        buf = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        // 當 channel 被移除時,就 release buffer
        buf.release();
        buf = null;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        // 一次讀取一個 byte,寫入 ByteBuf
        buf.writeBytes(m);
        m.release();

        // 檢查是否已經累積到 4 bytes
        if (buf.readableBytes() >= 4) {
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

方案2

將訊息組合的部份拆開,移到 Decoder

TimeDecoder.java

package netty.time.stream;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

// 處理碎片問題
public class TimeDecoder extends ByteToMessageDecoder {

    // 每次收到資料時,都會用內部的 buffer,呼叫這個 method
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        // 當 in 不足 4 bytes 時,就不寫入到 out
        if (in.readableBytes() < 4) {
            return;
        }
        // 當 in 有 4 bytes
        out.add(in.readBytes(4));
    }
}

TimeDecoder1.java

TimeDecoder 可用另一種簡化的方式實作

package netty.time.stream;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

// 改用 ReplayingDecoder,可簡化 TimeDecoder 的寫法
public class TimeDecoder1 extends ReplayingDecoder<Void> {
    @Override
    protected void decode(
            ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        out.add(in.readBytes(4));
    }
}

TimeClient.java

修改 ChannelInitializer 的部分,加上 Decoder

package netty.time.stream;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TimeClient {
    public static void main(String[] args) throws Exception {

        String host = "localhost";
        int port = 8080;
        if (args.length > 0) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        }
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // client 要使用 Bootstrap,跟 ServerBootstrap 不同
            Bootstrap b = new Bootstrap();
            // client 只需要一個 worker EventLoopGroup
            b.group(workerGroup);
            // client 要改用 NioSocketChannel,跟 NioServerSocketChannel 不同
            b.channel(NioSocketChannel.class);
            // 不要使用 childOption
            b.option(ChannelOption.SO_KEEPALIVE, true);
//            b.handler(new ChannelInitializer<SocketChannel>() {
//                @Override
//                public void initChannel(SocketChannel ch) throws Exception {
//                    ch.pipeline().addLast(new TimeClientHandler());
//                }
//            });
            // 要在 TimeClientHandler 之前,使用 TimeDecoder
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
                }
            });

            // Start the client.
            // 呼叫 connect 而不是 bind
            ChannelFuture f = b.connect(host, port).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

TimeClientHandler.java

程式不變

package netty.time.stream;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 收到的資料是 ByteBuf
        ByteBuf m = (ByteBuf) msg;
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

POJO

改用 POJO java class,讓 protocol 解讀更清楚

UnixTime.java

package netty.time.pojo;

import java.util.Date;

public class UnixTime {

    private final long value;

    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }

    public UnixTime(long value) {
        this.value = value;
    }

    public long value() {
        return value;
    }

    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

修改 TimeDecoder

TimeDecoder.java

package netty.time.pojo;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

// 處理碎片問題
public class TimeDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            return;
        }

        out.add(new UnixTime(in.readUnsignedInt()));
    }
}

TimeClientHandler.java

package netty.time.pojo;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        UnixTime m = (UnixTime) msg;
        System.out.println(m);
        ctx.close();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

TimeClient.java

package netty.time.pojo;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TimeClient {
    public static void main(String[] args) throws Exception {

        String host = "localhost";
        int port = 8080;
        if (args.length > 0) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        }
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // client 要使用 Bootstrap,跟 ServerBootstrap 不同
            Bootstrap b = new Bootstrap();
            // client 只需要一個 worker EventLoopGroup
            b.group(workerGroup);
            // client 要改用 NioSocketChannel,跟 NioServerSocketChannel 不同
            b.channel(NioSocketChannel.class);
            // 不要使用 childOption
            b.option(ChannelOption.SO_KEEPALIVE, true);
//            b.handler(new ChannelInitializer<SocketChannel>() {
//                @Override
//                public void initChannel(SocketChannel ch) throws Exception {
//                    ch.pipeline().addLast(new TimeClientHandler());
//                }
//            });
            // 要在 TimeClientHandler 之前,使用 TimeDecoder
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
                }
            });

            // Start the client.
            // 呼叫 connect 而不是 bind
            ChannelFuture f = b.connect(host, port).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

POJO Server

UnixTime.java

package netty.time.pojoserver;

import java.util.Date;

public class UnixTime {

    private final long value;

    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }

    public UnixTime(long value) {
        this.value = value;
    }

    public long value() {
        return value;
    }

    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

TimeServerHandler.java

package netty.time.pojoserver;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ChannelFuture f = ctx.writeAndFlush(new UnixTime());
        f.addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

TimeEncoder.java

package netty.time.pojoserver;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        // 將 ChannelPromise 傳給 Netty,讓 netty 可決定 success or failure
        ctx.write(encoded, promise);
    }
}

TimeEncoder1.java

簡化寫法

package netty.time.pojoserver;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class TimeEncoder1 extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

TimeServer.java

package netty.time.pojoserver;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class TimeServer {

    private int port;

    public TimeServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        // NioEventLoopGroup 是 multithread event loop,處理 IO operation
        // 第一個 NioEventLoopGroup 處理 incoming connection,稱為 boss
        // 第二個 NioEventLoopGroup 處理已接收連線的 traffic,稱為 worker
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // ServerBootstrap 是 helper class,可設定 server
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)  // 使用 NioServerSocketChannel 接受 incoming connection
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new TimeEncoder(), new TimeServerHandler());
                        }
                    })
                    // 設定 ChannelOption, ChannelConfig 可取得所有可設定的參數
                    // option 是設定 NioServerSocketChannel 的參數
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // childOption 是設定 NioSocketChannel 的參數
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            // 綁定 TCP Port
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 這邊開始會等待所有 server socket 關閉
            // 但這個例子不會發生這種狀況
            // 如果要以正常方式關閉 server,可呼叫以下 method
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new TimeServer(port).run();
    }
}

References

Netty.docs: User guide for 4.x

2025/01/06

Netty in Java 1

Netty是一個 non-blokcing I/O socket framework,主要用於開發網路應用程式。非同步事件驅動的框架和工具可簡化程式開發。Netty最初由JBoss開發,現在由Netty項目社區開發和維護。Netty還支援了HTTP、HTTP2、DNS及其他協定,WebSockets、Google Protocol Buffers、支援 SSL/TLS 以及支援用於SPDY協定和訊息壓縮。

早期 Java Socket framework 會使用 Mina 這個 library,但因為 netty 跟 mina 作者相同,且 mina 已經很久都沒有在維護了,所以目前大部分的文章都建議要使用 netty。不過討論中有提到一個最大的不同點,在 UDP 的處理部分,mina 跟 netty 有不同的作法,mina 有高階的封裝,可讓 connection less 的 UDP 連線,使用起來很像有連線的狀況,netty 是比較貼近原本的 UDP,保持了 connection less 的特性。

另外在使用 netty 之前,要注意使用了哪一個版本的 netty。根據 Remove master branch · Issue #4466 · netty/netty · GitHub 的討論,由於 netty 5 開發時,發現新的作法增加了城市的複雜度,但卻沒有帶來明顯的效能提升,所以 netty 5 目前是被放棄的狀態,建議還是要使用 4.1 版,4.1 版的 user guide 在 Netty.docs: User guide for 4.x

netty 的 libary 切割為以下這些部分

Core 是核心,Protocol Support 是在 socket 的上層的通訊協定,Transport Support 則是資料傳輸,也就是 Socket/Datagram、HTTP Tunnel 或 In-VM Pipe,這幾個都是實際傳輸資料的實作。

maven

使用netty最簡單的方式是引用所有 netty 的 libary,可引用 netty-all

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.111.Final</version>
</dependency>

DISCARD

網路協定最簡單的是 DISCARD,server side 不管 client 送什麼資料,都會直接丟棄,不做任何回應。

DiscardServer.java

package netty.discard;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * Discards any incoming data.
 */
public class DiscardServer {

    private int port;

    public DiscardServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        // NioEventLoopGroup 是 multithread event loop,處理 IO operation
        // 第一個 NioEventLoopGroup 處理 incoming connection,稱為 boss
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 第二個 NioEventLoopGroup 處理已接收連線的 traffic,稱為 worker
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // ServerBootstrap 是 helper class,可設定 server
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // 使用 NioServerSocketChannel 接受 incoming connection
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        // ChannelInitializer 用來設定新的 Channel
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new DiscardServerHandler());
                        }
                    })
                    // 設定 ChannelOption, ChannelConfig 可取得所有可設定的參數
                    // option 是設定 NioServerSocketChannel 的參數
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // childOption 是設定 NioSocketChannel 的參數
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            // 綁定 TCP Port
            ChannelFuture f = b.bind(port).sync();

            // 這邊開始會等待所有 server socket 關閉
            // 但這個例子不會發生這種狀況
            // 如果要以正常方式關閉 server,可呼叫以下 method
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new DiscardServer(port).run();
    }
}

DiscardServerHandler.java

package netty.discard;

import io.netty.buffer.ByteBuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

// ChannelInboundHandlerAdapter 實作了 ChannelInboundHandler 介面所有 methods
// DiscardServerHandler 只需要 繼承 ChannelInboundHandlerAdapter
// 就可以只 override 必要的 methods
public class DiscardServerHandler extends ChannelInboundHandlerAdapter {

    // channelRead 會在收到 message 時被呼叫
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // Discard the received data silently.
//        ((ByteBuf) msg).release(); // msg 必須要呼叫 release 釋放記憶體
//
//        channelRead 通常會用以下的方式實作內容
//        try {
//            // Do something with msg
//        } finally {
//            ReferenceCountUtil.release(msg);
//        }
        ByteBuf in = (ByteBuf) msg;
        try {
            while (in.isReadable()) {
                System.out.print((char) in.readByte());
                System.out.flush();
            }
        } finally {
            ReferenceCountUtil.release(msg);
        }

        // 上面的 while loop 可替代為以下寫法
        System.out.print(in.toString(io.netty.util.CharsetUtil.US_ASCII));
        in.release();
    }

//    // 上面的 while loop 可替代為以下寫法
//    @Override
//    public void channelRead(ChannelHandlerContext ctx, Object msg) {
//        ByteBuf in = (ByteBuf) msg;
//        System.out.print(in.toString(io.netty.util.CharsetUtil.US_ASCII));
//        in.release();
//    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        // 發生 IO error 或 handler 實作有問題時,會產生 exception,由這個 method 處理
        cause.printStackTrace();
        ctx.close();
    }
}

測試

啟動 DiscardServer 後,可用 telnet/nc 測試

# 等同 telnet 127.0.0.1 8080
nc -nvv 127.0.0.1 8080

ECHO

echo 協定基於 discard 做一些修改,在 echo server 收到 client 發送的資料後,會直接將收到的資料回傳給 client。

EchoServer.java

package netty.echo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class EchoServer {

    private int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        // NioEventLoopGroup 是 multithread event loop,處理 IO operation
        // 第一個 NioEventLoopGroup 處理 incoming connection,稱為 boss
        // 第二個 NioEventLoopGroup 處理已接收連線的 traffic,稱為 worker
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // ServerBootstrap 是 helper class,可設定 server
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)  // 使用 NioServerSocketChannel 接受 incoming connection
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    })
                    // 設定 ChannelOption, ChannelConfig 可取得所有可設定的參數
                    // option 是設定 NioServerSocketChannel 的參數
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // childOption 是設定 NioSocketChannel 的參數
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            // 綁定 TCP Port
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 這邊開始會等待所有 server socket 關閉
            // 但這個例子不會發生這種狀況
            // 如果要以正常方式關閉 server,可呼叫以下 method
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new EchoServer(port).run();
    }
}

EchoServerHandler.java

package netty.echo;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;

/**
 * Handles a server-side channel.
 */
public class EchoServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // ChannelHandlerContext ctx 可驅動 I/O
        // 這邊將收到的 msg 透過 ctx 寫回 channel,但不需要 release msg
        // 因為 netty 會在寫入時,自動 release msg
        ctx.write(msg);
        ctx.flush();
        // ctx.write 不是直接寫到網路上,而是先放到 buffer,然後再 flush
    }

//    // 可改用 writeAndFlush
//    @Override
//    public void channelRead(ChannelHandlerContext ctx, Object msg) {
//        ctx.writeAndFlush(msg);
//    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

測試

一樣用 nc 去測試

nc -nvv 127.0.0.1 8080

TIME

TIME protocol 的 server 會在 client 連線後發送一個 32 bits 整數,然後就直接關閉連線。

因為 server 必須忽略 client 發送的所有資料,所以不能用 channelRead() 要改用 channelActive()

TimeServer.java

package netty.time;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class TimeServer {

    private int port;

    public TimeServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        // NioEventLoopGroup 是 multithread event loop,處理 IO operation
        // 第一個 NioEventLoopGroup 處理 incoming connection,稱為 boss
        // 第二個 NioEventLoopGroup 處理已接收連線的 traffic,稱為 worker
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // ServerBootstrap 是 helper class,可設定 server
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)  // 使用 NioServerSocketChannel 接受 incoming connection
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new TimeServerHandler());
                        }
                    })
                    // 設定 ChannelOption, ChannelConfig 可取得所有可設定的參數
                    // option 是設定 NioServerSocketChannel 的參數
                    .option(ChannelOption.SO_BACKLOG, 128)
                    // childOption 是設定 NioSocketChannel 的參數
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            // 綁定 TCP Port
            ChannelFuture f = b.bind(port).sync(); // (7)

            // 這邊開始會等待所有 server socket 關閉
            // 但這個例子不會發生這種狀況
            // 如果要以正常方式關閉 server,可呼叫以下 method
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }

        new TimeServer(port).run();
    }
}

TimeServerHandler.java

package netty.time;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) {
        // channelActive 會在 connection 建立後,就被呼叫
        // 先取得一塊 32 bits integer (4 bytes) 的 ByteBuf,然後將現在的時間填進去
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));

        // 這邊不需要跟 NIO 的 ByteBuffer 一樣,在發送前,呼叫 java.nio.ByteBuffer.flip()
        // 因為 ByteBuf 已經自動處理了 read/write,會使用兩種不同的 pointer (index)
        final ChannelFuture f = ctx.writeAndFlush(time);
        // ChannelHandlerContext.write() 跟 writeAndFlush 會回傳 ChannelFuture
        // 代表 Netty 的 IO operation 是非同步的
        // 必須用非同步的方式,確認 future 已經完成,才能將 context 關閉
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        });  // 這邊可以簡化為這種寫法
        // f.addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

TimeClient.java

package netty.time;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class TimeClient {
    public static void main(String[] args) throws Exception {

        String host = "localhost";
        int port = 8080;
        if (args.length > 0) {
            host = args[0];
            port = Integer.parseInt(args[1]);
        }
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // client 要使用 Bootstrap,跟 ServerBootstrap 不同
            Bootstrap b = new Bootstrap();
            // client 只需要一個 worker EventLoopGroup
            b.group(workerGroup);
            // client 要改用 NioSocketChannel,跟 NioServerSocketChannel 不同
            b.channel(NioSocketChannel.class);
            // 不要使用 childOption
            b.option(ChannelOption.SO_KEEPALIVE, true);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });

            // Start the client.
            // 呼叫 connect 而不是 bind
            ChannelFuture f = b.connect(host, port).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

TimeClientHandler.java

package netty.time;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 收到的資料是 ByteBuf
        ByteBuf m = (ByteBuf) msg;
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        } finally {
            m.release();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

References

Netty.docs: User guide for 4.x

netty/example/src/main/java/io/netty/example at 4.1 · netty/netty · GitHub