2025/08/25

Spring Boot 3 JPA multiple datasource

在一個 Spring Boot 3 project 同時連接到兩個 database,需要用兩個設定檔指定兩個 datasource,分別設定不同的 entityManager, transactonManager, jdbcTemplate。

  • application.yml

    spring:
      application:
        name: project
      jpa:
        properties:
          hibernate:
            # dialect: org.hibernate.dialect.MySQLDialect
            dialect: org.hibernate.community.dialect.MySQLLegacyDialect
    
    project:
      kokods:
        url: jdbc:mariadb://localhost:3306/koko
        username: root
        password: password
        schema: koko
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 30000 # milliseconds that a client will wait for a new connection from the pool  30 seconds
          minimum-idle: 1           # minimum number of idle connections
          maximum-pool-size: 100    # maximum number of connections
          idle-timeout: 600000      # maximum amount of time that a connection may sit idle in the pool of connections  10 mins
          max-lifetime: 1800000     # a connection can be pooled for before being destroyed  30 mins
          auto-commit: true
          connection-test-query: SELECT CURRENT_TIMESTAMP
      db2ds:
        url: jdbc:mariadb://localhost:3306/db2
        username: root
        password: password
        schema: db2
        type: com.zaxxer.hikari.HikariDataSource
        hikari:
          connection-timeout: 30000 # milliseconds that a client will wait for a new connection from the pool  30 seconds
          minimum-idle: 1           # minimum number of idle connections
          maximum-pool-size: 100    # maximum number of connections
          idle-timeout: 600000      # maximum amount of time that a connection may sit idle in the pool of connections  10 mins
          max-lifetime: 1800000     # a connection can be pooled for before being destroyed  30 mins
          auto-commit: true
          connection-test-query: SELECT CURRENT_TIMESTAMP
    
    logging:
      level:
        com.zaxxer.hikari: TRACE
        com.zaxxer.hikari.HikariConfig: DEBUG
  • @Configuration設定

    • KoKoConfig.java

      package tw.com.maxkit.koko.config;
      
      import com.zaxxer.hikari.HikariDataSource;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
      import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
      import org.springframework.context.annotation.*;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.boot.jdbc.DataSourceBuilder;
      import org.springframework.core.env.Environment;
      import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
      import org.springframework.jdbc.core.JdbcTemplate;
      import org.springframework.orm.jpa.JpaTransactionManager;
      import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
      import org.springframework.transaction.PlatformTransactionManager;
      import org.springframework.transaction.annotation.EnableTransactionManagement;
      
      import javax.sql.DataSource;
      
      @Configuration
      //@PropertySource({"classpath:persistence-multiple-db.properties"})
      @EnableJpaRepositories(
              basePackages = "tw.com.maxkit.koko.dao.jpa",
              entityManagerFactoryRef = "kokoEntityManagerFactory",
              transactionManagerRef = "kokoTransactionManager"
      )
      @EnableTransactionManagement
      public class KoKoConfig {
          @Autowired
          private Environment env;
      
          @Primary
          @Bean("kokoDataSourceProperties")
          @ConfigurationProperties("project.kokods")
          public DataSourceProperties kokoDataSourceProperties() {
              return new DataSourceProperties();
          }
      
          @Primary
          @Bean("kokoDataSource")
          @Qualifier(value="kokoDataSourceProperties")
          @ConfigurationProperties(prefix = "project.kokods.hikari")
          public HikariDataSource kokoDataSource() {
              return kokoDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
          }
      
          // 如果不修改 Hikari 的參數,可直接使用這個 datasource,但要注意設定檔 url 要改為 jdbc-url
      //    @Bean("lartelDataSource")
      //    @ConfigurationProperties("lartel.kokods")
      //    public DataSource lartelDataSource() {
      //        return DataSourceBuilder.create().build();
      //    }
      
          @Primary
          @Bean("kokoEntityManagerFactory")
          public LocalContainerEntityManagerFactoryBean kokoEntityManagerFactory(
                  @Qualifier("kokoDataSource") DataSource kokoDataSource,
                  EntityManagerFactoryBuilder builder) {
              return builder //
                      .dataSource(kokoDataSource) //
                      .packages("tw.com.maxkit.koko.data.entity") //
                      .persistenceUnit("kokoDs") //
                      .build();
          }
      
          @Primary
          @Bean("kokoTransactionManager")
          public PlatformTransactionManager kokoTransactionManager(
                  @Qualifier("kokoEntityManagerFactory") LocalContainerEntityManagerFactoryBean kokoEntityManagerFactory) {
              return new JpaTransactionManager(kokoEntityManagerFactory.getObject());
          }
      
          @Primary
          @Bean("kokoJdbcTemplate")
          public JdbcTemplate kokoJdbcTemplate(
                  @Qualifier("kokoDataSource") DataSource kokoDataSource) {
              return new JdbcTemplate(kokoDataSource);
          }
      }
    • Db2Config.java

      package tw.com.maxkit.db2.config;
      
      import com.zaxxer.hikari.HikariDataSource;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.beans.factory.annotation.Qualifier;
      import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
      import org.springframework.boot.context.properties.ConfigurationProperties;
      import org.springframework.boot.jdbc.DataSourceBuilder;
      import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.context.annotation.Primary;
      import org.springframework.core.env.Environment;
      import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
      import org.springframework.jdbc.core.JdbcTemplate;
      import org.springframework.orm.jpa.JpaTransactionManager;
      import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
      import org.springframework.transaction.PlatformTransactionManager;
      import org.springframework.transaction.annotation.EnableTransactionManagement;
      
      import javax.sql.DataSource;
      
      @Configuration
      @EnableJpaRepositories(
              basePackages = "tw.com.maxkit.db2.dao.jpa",
              entityManagerFactoryRef = "lartelEntityManagerFactory",
              transactionManagerRef = "lartelTransactionManager"
      )
      @EnableTransactionManagement
      public class LartelConfig {
          @Autowired
          private Environment env;
      
          // 這兩個 method 會套用修改 Hikari 的參數
          @Bean("db2DataSourceProperties")
          @ConfigurationProperties("project.db2ds")
          public DataSourceProperties db2DataSourceProperties() {
              return new DataSourceProperties();
          }
      
          @Bean("db2DataSource")
          @Qualifier(value="db2DataSourceProperties")
          @ConfigurationProperties(prefix = "project.db2ds.hikari")
          public HikariDataSource db2DataSource() {
              return db2DataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
          }
      
          // 如果不修改 Hikari 的參數,可直接使用這個 datasource,但要注意設定檔 url 要改為 jdbc-url
      //    @Bean("lartelDataSource")
      //    @ConfigurationProperties("lartel.lartelds")
      //    public DataSource lartelDataSource() {
      //        return DataSourceBuilder.create().build();
      //    }
      
          @Bean("db2EntityManagerFactory")
          public LocalContainerEntityManagerFactoryBean db2EntityManagerFactory(
                  @Qualifier("db2DataSource") DataSource db2DataSource,
                  EntityManagerFactoryBuilder builder) {
              return builder //
                      .dataSource(db2DataSource) //
                      .packages("tw.com.maxkit.db2.data.entity") //
                      .persistenceUnit("db2Ds") //
                      .build();
          }
      
          @Bean("db2TransactionManager")
          public PlatformTransactionManager db2TransactionManager(
                  @Qualifier("db2EntityManagerFactory") LocalContainerEntityManagerFactoryBean lartelEntityManagerFactory) {
              return new JpaTransactionManager(db2EntityManagerFactory.getObject());
          }
      
          @Bean("db2JdbcTemplate")
          public JdbcTemplate db2JdbcTemplate(
                  @Qualifier("db2DataSource") DataSource db2DataSource) {
              return new JdbcTemplate(db2DataSource);
          }
      }
  • 第一個 datasource 部分的 DAO,另一個是類似的作法

    package tw.com.maxkit.koko.dao.jpa;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    import tw.com.maxkit.koko.data.entity.Uservo;
    
    import java.util.List;
    
    @Repository
    public interface UservoDAO extends JpaRepository<Uservo, Long> {
        public List<Uservo> queryAll();
    }
    • DAO 的 implementation

      package tw.com.maxkit.koko.dao.jpa;
      
      import jakarta.persistence.EntityManager;
      import jakarta.persistence.TypedQuery;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.data.jpa.repository.JpaContext;
      import org.springframework.stereotype.Component;
      import tw.com.maxkit.koko.data.entity.Uservo;
      
      import java.util.List;
      
      @Component
      public class UservoDAOImpl {
          private final EntityManager em;
      
          @Autowired
          public UservoDAOImpl(JpaContext context) {
              this.em = context.getEntityManagerByManagedType(Uservo.class);
          }
      
          public List queryAll() {
              String jpql = "SELECT u FROM Uservo u";
              TypedQuery query = this.em.createQuery(jpql, Uservo.class);
              return query.getResultList();
          }
      }
  • Service

    package tw.com.maxkit.koko.service;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import tw.com.maxkit.koko.dao.jpa.UservoDAO;
    import tw.com.maxkit.koko.data.entity.Uservo;
    
    @Service
    public class UservoService {
    
        @Autowired
        private UservoDAO uservoDAO;
    
        public void findAll() {
            System.out.println("\n使用 Spring Data JPA 衍生方法查詢 uservo 的資料:");
    //        this.uservoRepository.findAll().forEach(System.out::println);
            this.uservoDAO.findAll().forEach(
                    uservo -> System.out.println("uservoseq="+uservo.getUservoseq()+", userid="+uservo.getUserid())
            );
        }
    
        @Transactional("kokoTransactionManager")
        public Uservo testTransactional(String userid, String username) {
            // 刪除全部
    //        this.uservoRepository.deleteAll();
    
            // 寫入一筆
            Uservo a = new Uservo();
            a.setUserid(userid);
            a.setUsername(username);
            a = this.uservoDAO.save(a);
    
            // 故意埋入 RuntimeException: ArrayIndexOutOfBoundsException
            System.out.println(new String[] {}[1]);
    
            return a;
        }
    
        public void queryAll() {
            System.out.println("\n使用 queryAll 查詢 uservo 的資料:");
    //        this.uservoRepository.findAll().forEach(System.out::println);
            this.uservoDAO.queryAll().forEach(
                    uservo -> System.out.println("uservoseq="+uservo.getUservoseq()+", userid="+uservo.getUserid())
            );
        }
    }

References

Spring Boot + Spring Data JPA 配置多個 DataSource

HikariCP 连接池多数据源配置

2025/08/18

espeak-ng

How To Install espeak-ng on CentOS 8 | Installati.one

How to install eSpeak on CentOS 8 / Ubuntu 20.04 / 18.04? - Linux Windows and android Tutorials

eSpeak NG 安装和配置指南-CSDN博客

eSpeak NG,適用於 Linux 的文本到語音合成器

eSpeak NG (Next Generation) Text-to-Speech 是 open source speech synthesizer。使用了共振峰合成方法,用比較小的程式提供多種語言的合成方法。

1995年 Jonathan Duddington 提出在 RISC OS 運作,支援 British English 的語音合成器,後來在 2006/2/17,speak 1.05 以 GPLv2 released。目前已重新命名為 eSpeak

2010/6/25 Reece Dunn 在 github 以 1.43.46 版發起 eSpeak 的 fork,致力於讓 eSpeak 運作在其他 POSIX plarforms。到了 2015/12/11 epeak-ng 啟動,支援更多語言,且做了更多 bug fix。

CLI

# espeak-ng --help
eSpeak NG text-to-speech: 1.49.2  Data at: /usr/share/espeak-ng-data

espeak-ng [options] ["<words>"]

-f <text file>   Text file to speak
--stdin    Read text input from stdin instead of a file

If neither -f nor --stdin, then <words> are spoken, or if none then text
is spoken from stdin, each line separately.

-a <integer>
       Amplitude, 0 to 200, default is 100
-d <device>
       Use the specified device to speak the audio on. If not specified, the
       default audio device is used.
-g <integer>
       Word gap. Pause between words, units of 10mS at the default speed
-k <integer>
       Indicate capital letters with: 1=sound, 2=the word "capitals",
       higher values indicate a pitch increase (try -k20).
-l <integer>
       Line length. If not zero (which is the default), consider
       lines less than this length as end-of-clause
-p <integer>
       Pitch adjustment, 0 to 99, default is 50
-s <integer>
       Speed in approximate words per minute. The default is 175
-v <voice name>
       Use voice file of this name from espeak-ng-data/voices
-w <wave file name>
       Write speech to this WAV file, rather than speaking it directly
-b       Input text encoding, 1=UTF8, 2=8 bit, 4=16 bit
-m       Interpret SSML markup, and ignore other < > tags
-q       Quiet, don't produce any speech (may be useful with -x)
-x       Write phoneme mnemonics to stdout
-X       Write phonemes mnemonics and translation trace to stdout
-z       No final sentence pause at the end of the text
--compile=<voice name>
       Compile pronunciation rules and dictionary from the current
       directory. <voice name> specifies the language
--compile-debug=<voice name>
       Compile pronunciation rules and dictionary from the current
       directory, including line numbers for use with -X.
       <voice name> specifies the language
--compile-mbrola=<voice name>
       Compile an MBROLA voice
--compile-intonations
       Compile the intonation data
--compile-phonemes=<phsource-dir>
       Compile the phoneme data using <phsource-dir> or the default phsource directory
--ipa      Write phonemes to stdout using International Phonetic Alphabet
--path="<path>"
       Specifies the directory containing the espeak-ng-data directory
--pho      Write mbrola phoneme data (.pho) to stdout or to the file in --phonout
--phonout="<filename>"
       Write phoneme output from -x -X --ipa and --pho to this file
--punct="<characters>"
       Speak the names of punctuation characters during speaking.  If
       =<characters> is omitted, all punctuation is spoken.
--sep=<character>
       Separate phonemes (from -x --ipa) with <character>.
       Default is space, z means ZWJN character.
--split=<minutes>
       Starts a new WAV file every <minutes>.  Used with -w
--stdout   Write speech output to stdout
--tie=<character>
       Use a tie character within multi-letter phoneme names.
       Default is U+361, z means ZWJ character.
--version  Shows version number and date, and location of espeak-ng-data
--voices=<language>
       List the available voices for the specified language.
       If <language> is omitted, then list all voices.
-h, --help Show this help.
# espeak-ng --voices
Pty Language       Age/Gender VoiceName          File                 Other Languages
 5  af              --/M      Afrikaans          gmw/af
 5  am              --/M      Amharic            sem/am
 5  an              --/M      Aragonese          roa/an
 5  ar              --/M      Arabic             sem/ar
 5  as              --/M      Assamese           inc/as
 5  az              --/M      Azerbaijani        trk/az
 5  bg              --/M      Bulgarian          zls/bg
 5  bn              --/M      Bengali            inc/bn
 5  bpy             --/M      Bishnupriya_Manipuri inc/bpy
 5  bs              --/M      Bosnian            zls/bs
 5  ca              --/M      Catalan            roa/ca
 5  cmn             --/M      Chinese_(Mandarin) sit/cmn              (zh-cmn 5)(zh 5)
 5  cs              --/M      Czech              zlw/cs
 5  cy              --/M      Welsh              cel/cy
 5  da              --/M      Danish             gmq/da
 5  de              --/M      German             gmw/de
 5  el              --/M      Greek              grk/el
 5  en-029          --/M      English_(Caribbean) gmw/en-029           (en 10)
 2  en-gb           --/M      English_(Great_Britain) gmw/en               (en 2)
 5  en-gb-scotland  --/M      English_(Scotland) gmw/en-GB-scotland   (en 4)
 5  en-gb-x-gbclan  --/M      English_(Lancaster) gmw/en-GB-x-gbclan   (en-gb 3)(en 5)
 5  en-gb-x-gbcwmd  --/M      English_(West_Midlands) gmw/en-GB-x-gbcwmd   (en-gb 9)(en 9)
 5  en-gb-x-rp      --/M      English_(Received_Pronunciation) gmw/en-GB-x-rp       (en-gb 4)(en 5)
 2  en-us           --/M      English_(America)  gmw/en-US            (en 3)
 5  eo              --/M      Esperanto          art/eo
 5  es              --/M      Spanish_(Spain)    roa/es
 5  es-419          --/M      Spanish_(Latin_America) roa/es-419           (es-mx 6)(es 6)
 5  et              --/M      Estonian           urj/et
 5  eu              --/M      Basque             eu
 5  fa              --/M      Persian            ira/fa
 5  fa-Latn         --/M      Persian_(Pinglish) ira/fa-Latn
 5  fi              --/M      Finnish            urj/fi
 5  fr-be           --/M      French_(Belgium)   roa/fr-BE            (fr 8)
 5  fr-ch           --/M      French_(Switzerland) roa/fr-CH            (fr 8)
 5  fr-fr           --/M      French_(France)    roa/fr               (fr 5)
 5  ga              --/M      Gaelic_(Irish)     cel/ga
 5  gd              --/M      Gaelic_(Scottish)  cel/gd
 5  gn              --/M      Guarani            sai/gn
 5  grc             --/M      Greek_(Ancient)    grk/grc
 5  gu              --/M      Gujarati           inc/gu
 5  hi              --/M      Hindi              inc/hi
 5  hr              --/M      Croatian           zls/hr               (hbs 5)
 5  hu              --/M      Hungarian          urj/hu
 5  hy              --/M      Armenian_(East_Armenia) ine/hy               (hy-arevela 5)
 5  hy-arevmda      --/M      Armenian_(West_Armenia) ine/hy-arevmda       (hy 8)
 5  ia              --/M      Interlingua        art/ia
 5  id              --/M      Indonesian         poz/id
 5  is              --/M      Icelandic          gmq/is
 5  it              --/M      Italian            roa/it
 5  ja              --/M      Japanese           jpx/ja
 5  jbo             --/M      Lojban             art/jbo
 5  ka              --/M      Georgian           ccs/ka
 5  kl              --/M      Greenlandic        esx/kl
 5  kn              --/M      Kannada            dra/kn
 5  ko              --/M      Korean             ko
 5  kok             --/M      Konkani            inc/kok
 5  ku              --/M      Kurdish            ira/ku
 5  ky              --/M      Kyrgyz             trk/ky
 5  la              --/M      Latin              itc/la
 5  lfn             --/M      Lingua_Franca_Nova art/lfn
 5  lt              --/M      Lithuanian         bat/lt
 5  lv              --/M      Latvian            bat/lv
 5  mi              --/M      poz/mi             poz/mi
 5  mk              --/M      Macedonian         zls/mk
 5  ml              --/M      Malayalam          dra/ml
 5  mr              --/M      Marathi            inc/mr
 5  ms              --/M      Malay              poz/ms
 5  mt              --/M      Maltese            sem/mt
 5  my              --/M      Burmese            sit/my
 5  nb              --/M      Norwegian_Bokmål  gmq/nb               (no 5)
 5  nci             --/M      Nahuatl_(Classical) azc/nci
 5  ne              --/M      Nepali             inc/ne
 5  nl              --/M      Dutch              gmw/nl
 5  om              --/M      Oromo              cus/om
 5  or              --/M      Oriya              inc/or
 5  pa              --/M      Punjabi            inc/pa
 5  pap             --/M      Papiamento         roa/pap
 5  pl              --/M      Polish             zlw/pl
 5  pt              --/M      Portuguese_(Portugal) roa/pt               (pt-pt 5)
 5  pt-br           --/M      Portuguese_(Brazil) roa/pt-BR            (pt 6)
 5  ro              --/M      Romanian           roa/ro
 5  ru              --/M      Russian            zle/ru
 5  sd              --/M      Sindhi             inc/sd
 5  si              --/M      Sinhala            inc/si
 5  sk              --/M      Slovak             zlw/sk
 5  sl              --/M      Slovenian          zls/sl
 5  sq              --/M      Albanian           ine/sq
 5  sr              --/M      Serbian            zls/sr
 5  sv              --/M      Swedish            gmq/sv
 5  sw              --/M      Swahili            bnt/sw
 5  ta              --/M      Tamil              dra/ta
 5  te              --/M      Telugu             dra/te
 5  tn              --/M      Setswana           bnt/tn
 5  tr              --/M      Turkish            trk/tr
 5  tt              --/M      Tatar              trk/tt
 5  ur              --/M      Urdu               inc/ur
 5  vi              --/M      Vietnamese_(Northern) aav/vi
 5  vi-vn-x-central --/M      Vietnamese_(Central) aav/vi-VN-x-central
 5  vi-vn-x-south   --/M      Vietnamese_(Southern) aav/vi-VN-x-south
 5  yue             --/M      Chinese_(Cantonese) sit/yue              (zh-yue 5)(zh 8)

espeak-ng-data 的路徑

ls /usr/share/espeak-ng-data
af_dict   ca_dict  eu_dict   hu_dict      kl_dict   lv_dict    ne_dict            phontab  sr_dict  voices
am_dict   cs_dict  fa_dict   hy_dict      kn_dict   mbrola_ph  nl_dict            pl_dict  sv_dict  zh_dict
an_dict   cy_dict  fi_dict   ia_dict      ko_dict   mi_dict    no_dict            pt_dict  sw_dict  zhy_dict
ar_dict   da_dict  fr_dict   id_dict      kok_dict  mk_dict    om_dict            ro_dict  ta_dict
as_dict   de_dict  ga_dict   intonations  ku_dict   ml_dict    or_dict            ru_dict  te_dict
az_dict   el_dict  gd_dict   is_dict      ky_dict   mr_dict    pa_dict            sd_dict  tn_dict
bg_dict   en_dict  grc_dict  it_dict      la_dict   ms_dict    pap_dict           si_dict  tr_dict
bn_dict   eo_dict  gu_dict   ja_dict      lang      mt_dict    phondata           sk_dict  tt_dict
bpy_dict  es_dict  hi_dict   jbo_dict     lfn_dict  my_dict    phondata-manifest  sl_dict  ur_dict
bs_dict   et_dict  hr_dict   ka_dict      lt_dict   nci_dict   phonindex          sq_dict  vi_dict

從已編譯的 espeak-ng 版本是 1.49.2 版,但安裝後發現該版本不支援中文

# rpm -qa|grep espeak
espeak-ng-1.49.2-4.el8.x86_64

到 github 下載 1.52 版

wget https://github.com/espeak-ng/espeak-ng/archive/refs/tags/1.52.0.tar.gz

直接編譯

ref: questions about mandarin data packet · Issue #1044 · espeak-ng/espeak-ng · GitHub

./autogen.sh
./configure --with-extdict-cmn

這邊最後要看到

        Extended Dictionaries:
            Russian:                   yes
            Chinese (Mandarin):        yes
            Chinese (Cantonese):       yes

編譯

make
make install

測試

espeak-ng -v cmn "english text 你好 more english text" -w test1.wav

2025/08/11

Spring Boot 3 Actuator

Actuator 一般用在 production 環境,使用 http endpoint 或 JMX 方式監控 app,然後抓取指標數值給資料監控平台。

以此順序介紹 Actuator -> Endpoints -> Metrics -> Grafana


要啟用 actuator,只需要加上 library

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

Endpoint

ref: Endpoints :: Spring Boot

endpoint 是用來監控 spring boot app,spring boot 內建很多endpoints,每個 endpoint 都可透過 http/JMX 方式暴露出去,大部分都是用 http。endpoint 會映射為 /actuator/${ID} ID 就是 endpoint 的 ID,ex: /actuator/health

啟動 app 後,瀏覽網址 http://localhost:8080/actuator/health

結果 UP 代表健康運作中

{
    "status": "UP"
}

spring boot 內建的 endpoints

name desc
auditevents 當前 app 的 audit event
beans 所有 spring beans
caches 可使用的 cache
conditions 設定類別上評估條件及匹配成功與否的原因
configprops 所有 @ConfigurationProperties list
env spring 環境中暴露的所有 properties
flyway 所有 flyway 遷移記錄
health 健康資訊
httpexchanges http exchange message(預設顯示最新的 100個)
info app basic info
integrationgraph spring integration graph
loggers 顯示/修改 logger 設定
liquibase 顯示 liquibase db migration
metrics metric infos
mappings 顯示所有 @RequestMapping
quartz 顯示 quartz jobs
scheduledtasks 顯示 scheduled tasks
sessions 當有 servlet-based web app 使用 Spring Session 時,查詢/刪除 user sessions
shutdown 關閉 app
startup 顯示啟動的資料
threaddump thread dump

使用 web app 時,還有以下這些

name desc
heapdump 回傳 heap dump。HotSpot VM 是回傳 HPROF-format file。OpenJ9 JVM 是回傳 PHD-format file
logfile log file content (需要設定 logging.file.name/logging.file.path)
prometheus 可被 prometheus server 提取的 metric

除了 shotdown 其他所有 endpoint 預設都是開啟的。可透過 management.endpoint.<id>.enabled 啟用/禁用 endpoint。

ex:

management:
  endpoint:
    shutdown:
      enabled: true

禁用所有 endpoint, 只啟用某一個 endpoint

management:
  endpoints:
    enable-by-default: false
  endpoint:
    shutdown:
      enabled: true

expose endpoint

啟用後,不一定能被存取。spring boot 3 預設只以 JMX, Web 方式暴露了 health

Property Default
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include health
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.include health
management:
  endpoints:
    jmx:
      exposure:
        include: "health,info"
    web:
      exposure:
        include: "*"
        exclude: "env,beans"

security

endpoint 提供的資訊需要安全保護

只要有 Spring Security library,就會自動保護所有 endpoints

實作 actuator 的 ManagementWebSecurityAutoConfiguration

註冊在 spring-boot-actuator-autoconfigure

修改 pom.xml 後

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

存取 http://localhost:8080/actuator/info 會自動轉到 Please sign in


如果不想使用 spring boot 預設機制,可改用 SecurityFilterChain

package com.test.actuator;

import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests((authorize) -> {
                    authorize.requestMatchers("/").permitAll()
                            .requestMatchers(EndpointRequest.to("health")).hasRole("ENDPOINT_ADMIN")
                            .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll();
                })
                //                .csrf(csrf -> csrf.disable())
                .csrf(csrf -> csrf.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()))
                .formLogin(withDefaults())
                .logout(logout -> logout.logoutUrl("/"))
                .build();
    }

    @Bean
    protected UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("test").password("{noop}test")
                .roles("ENDPOINT_ADMIN", "ADMIN", "TEST").build());
        manager.createUser(User.withUsername("root").password("{noop}root")
                .roles("ADMIN").build());
        return manager;
    }

}

permit all

@Configuration(proxyBeanMethods = false)
public class MySecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher(EndpointRequest.toAnyEndpoint());
        http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll());
        return http.build();
    }

}

customize endpoint mapping

預設 http://localhost:8080/actuator 可取得所有暴露的 endpoints

ex:

{
    "_links": {
        "self": {
            "href": "http://localhost:8080/actuator",
            "templated": false
        },
        "health-path": {
            "href": "http://localhost:8080/actuator/health/{*path}",
            "templated": true
        },
        "health": {
            "href": "http://localhost:8080/actuator/health",
            "templated": false
        }
    }
}

可用以下設定禁用

management:
  endpoints:
    web:
      discovery:
        enabled: false

也可以修改 mapping,這樣要改用 http://localhost:8080/act/hth 取得 health

management:
  endpoints:
    web:
      base-path: /act
      path-mapping:
        health: hth

也可以修改 web port,不使用 service 的 web port。

management:
  server:
    port: 8088
    address: 127.0.0.1

把 port 改為 -1 等同 expose.exclude: "*"

management:
  server:
    port: -1

實作自訂 endpoint

endpoint 啟用時,會自動設定

heath endpoint 是用 HealthEndpointAutoConfiguration。 然後用 @Import 找到不同 web 類型的 endpoint class

health endpoint 是用 HealthEndpoint 類別實作的,該類別有 @Endpoint 註解,且要註冊為 Bean。支援以 @JmxEndpoint @WebEndpoint 暴露方式

@ReadOperation @WriteOperation @DeleteOperation 對應不同的 http method: GET/POST/DELETE

POST request Content-Type 只接受 application/vnd.spring-boot.actuator.v2+json, application/json


新增 User.java

package com.test.actuator;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private int id;
    private String name;
    private int age;

}

TestEndpoint.java

package com.test.actuator.endpoint;

import com.test.actuator.User;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

@Component
@WebEndpoint(id = "test")
public class TestEndpoint {

    @ReadOperation
    public User getUser(@Selector Integer id) {
        return new User(id, "james", 18);
    }

    @WriteOperation
    public User updateUser(int id, @Nullable String name, @Nullable Integer age) {
        User user = getUser(id);
        user.setName(StringUtils.defaultIfBlank(name, user.getName()));
        user.setAge(ObjectUtils.defaultIfNull(age, user.getAge()));
        return user;
    }

}

注意要 expose endpoint

management:
  endpoints:
    web:
      exposure:
        include: "*"

瀏覽網址 http://localhost:8080/actuator/test/1

結果

{"id":1,"name":"james","age":18}

POST/DELETE 預設會回應 403,因為 spring boot 預設會打開 CSRF

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http.authorizeHttpRequests((authorize) -> {
                    authorize.requestMatchers("/").permitAll()
                            .requestMatchers(EndpointRequest.to("health")).hasRole("ENDPOINT_ADMIN")
                            .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll();
                })
                .csrf(csrf -> csrf.disable())
//                .csrf(csrf -> csrf.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()))
                .formLogin(withDefaults())
                .logout(logout -> logout.logoutUrl("/"))
                .build();
    }

enpoint 也支援 CORS 設定

management:
  endpoints:
    web:
      cors:
        allowed-origins: "https://test.com"
        allowed-methods: "GET,POST"

Observability

系統外部觀測正在運作的內部狀態的能力,spring boot 3 透過 Micrometer 提高可觀測性,支援 Micrometer 1.10+

引用可觀測 API,並自動設定 micrometer 追蹤,支援 Brave, OpenTelemetry, Zipkin, Wavefront

使用 micrometer API 完成觀測後,可將數據交給 Zipkin


Metrics

/actuator/metrics 指標,包含了 JVM, system, tomcat, Logger, Spring MVC...

預設不會 expose

瀏覽網址 http://localhost:8080/actuator/metrics 可查閱所有 metrics

http://localhost:8080/actuator/metrics/jvm.memory.max 就是取得 jvm.memory.max 的資訊


可自訂 metric

package com.test.actuator.metrics;

import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class MetricsConfig {

    @Bean
    public MeterBinder initDate(Environment env) {
        return (registry) -> Gauge.builder("init.date", this::date).register(registry);
    }

    @Bean
    public MeterBinder systemDate(Environment env) {
        return (registry) -> Gauge.builder("system.date", this::date).register(registry);
    }

    private Number date() {
        return 2024.11;
    }

}

網址 http://localhost:8080/actuator/metrics/init.date

結果

{
    "name":"init.date",
    "measurements":[
        {
            "statistic":"VALUE",
            "value":2024.11
        }
    ],
    "availableTags":[

    ]
}

Tracing

  • 使用 OpenTelemetry 結合 Zipkin 或 Wavefront

  • 使用 OpenZipkin Brave 結合 Zipkin 或 Wavefront

OpenTelemetry 可生成/收集/匯出觀測資料 metrics, logs, traces,他只會收集資料,分析跟展示要用其他軟體

Zipkin 是 Twitter 開源的分佈式追蹤系統,可收集解決服務的延遲問題的時間資料。如果 log 有 Trace ID,可直接展示。

       <!-- 將 Micrometer Observation API 橋接到 OpenTelemetry -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-tracing-bridge-otel</artifactId>
        </dependency>

        <!-- 向 Zipkin 報告跟蹤資訊 -->
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-exporter-zipkin</artifactId>
        </dependency>

採樣設定,預設為 0.1,也就是只採樣 10% 的 request

management:
  tracing:
    sampling:
      probability: 1.0

在 log 顯示 Trace ID, Span ID

logging:
  pattern:
    level: ${spring.application.name:},%X{traceId:-},%X{spanId:-}

Spring Boot Admin

GitHub - codecentric/spring-boot-admin: Admin UI for administration of spring boot applications

這個是社群開發的

  • 有個 server 提供 Spring Boot Actuators UI

  • 有個 client 註冊到 server,並能存取所有 actuator endpoints.

ref: Spring Boot Admin – Getting started

配置 Admin Server

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-server</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

在 application 加上 @EnableAdminServer

@SpringBootApplication
@EnableAdminServer
public class SpringBootAdminApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootAdminApplication.class, args);
    }
}

配置 Client

<dependency>
    <groupId>de.codecentric</groupId>
    <artifactId>spring-boot-admin-starter-client</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

加上設定

spring:
  boot:
    admin:
      client:
        url: http://localhost:8080
management:
  endpoints:
    web:
      expose:
        include: '*'
  info:
    env:
      enabled: true

禁用所有安全機制

@Configuration
public static class SecurityPermitAllConfig {
    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) {
        return http.authorizeHttpRequests((authorizeRequests) -> authorizeRequests.anyRequest().permitAll())
            .csrf().disable().build();
    }
}

網址 http://localhost:8088/


參考 Spring Boot Admin server notification 可設定通知

Prometheus

要在 pom.xml 加上 library

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>${micrometer-registry-prometheus.version}</version>
        </dependency>

spring boot 就會提供 endpoint /actuator/prometheus 可 pull 資料


加上 libary

        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>simpleclient_pushgateway</artifactId>
            <version>${simpleclient_pushgateway.version}</version>
        </dependency>

修改設定

management:
  prometheus:
    metrics:
      export:
        pushgateway:
          enabled: true

可 push 資料


Prometheus 通常結合 Grafana 使用

2025/08/04

Spring Boot 3 打包

可用 jar, war, docker, GraalVM image 方式打包

jar

用 jar application 方式打包

<packaging>jar</packaging>

同時要加上

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

使用兩種指令打包,二選一

  • mvn package

    打包到 target 目錄

  • mvn install

    打包後,安裝到本地的 MAVEN repository


打包後,會產生兩個

  • test-0.0.1-SNAPSHOT.jar

    這個可透過 java -jar test-0.0.1-SNAPSHOT.jar 啟動

  • test-0.0.1-SNAPSHOT.jar.original


META-INF/MANIFEST.MF 內容為

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 17
Implementation-Title: test
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.test.test.TestApplication
Spring-Boot-Version: 3.4.0
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

另一個 test-0.0.1-SNAPSHOT.jar.original,無法直接啟動,只是一個 jar


war

要先產生一個繼承 SpringBootServletInitializer 抽象類別

package com.test.test;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(TestApplication.class);
    }

}

修改打包方式

<packaging>war</packaging>

排除 embedded tomcat

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

可同時支援 jar 跟 war

package com.test.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class TestApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return startBuilder(builder);
    }

    public static void main(String[] args) {
//        SpringApplication.run(TestApplication.class, args);
        startBuilder(new SpringApplicationBuilder()).run(args);
    }

    private static SpringApplicationBuilder startBuilder(SpringApplicationBuilder builder) {
        return builder.sources(TestApplication.class);
    }

}

build 會產生 test-0.0.1-SNAPSHOT.war,可以放在 tomcat 或是用 java -jar 啟動


啟動 application

使用 java 指令

java -jar test-0.0.1-SNAPSHOT-exec.war

直接啟動

在 pom.xml 要修改設定

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

然侯就能直接運作

./test-0.0.1-SNAPSHOT-exec.war

note: 在不支援的 OS 要修改 pom.xml

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>exec</classifier>
                    <executable>true</executable>
                    <embeddedLaunchScript>...</embeddedLaunchScript>
                </configuration>
            </plugin>
        </plugins>
    </build>

系統 service

可用 init.d 或 systemd service 啟動

init.d

在 /etc/init.d 建立一個 link

sudo ln -s test-0.0.1-SNAPSHOT.war /etc/init.d/test

然後就能用 service 啟動

sudo service test start

systemd

在 /etc/systemd/system 目錄中,建立檔案 test.service

[Unit]
Description-test
After=syslog.target

[Service]
User=test
ExecStart=/home/test/test-0.0.1-SNAPSHOT.war
# process receive SIGTERM signal will return 128+15=143 exit status code
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

然後

sudo systemctl start test
sudo systemctl enable test

Docker

docker -v
docker version

以 Dockerfile 產生 image

FROM eclipse-temurin:17-jre
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom","-jar","/application.jar"]
EXPOSE 8080

eclipse-temurin 是 Eclipse 提供的 open JDK image

ARG 定義參數

用指令產生 image

docker build -t test/docker-all:1.0 .

-t tags,image 的 name:tags

啟動

docker run -dp 8080:8080 test/docker-all:1.0 

分層 image

定義分層的是 laryers.idx 檔案,內容為

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

一般會異動的只有 classes

建立新的 Dockerfile

FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*-exec.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM eclipse-temurin:17-jre as builder
WORKDIR application
COPY --from-builder application/dependencies/ ./
COPY --from-builder application/spring-boot-loader/ ./
COPY --from-builder application/snapshot-dependencies/ ./
COPY --from-builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
EXPOSE 8081

注意 -Djarmode=layertools

layertools list

> java -Djarmode=layertools -jar test-0.0.1-SNAPSHOT.war list
dependencies
spring-boot-loader
snapshot-dependencies
application

產生 image

docker build -t test/docker-layer:1.0 .

分層建構不支援 executable jar,要修改 executable 設定

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

Cloud Native Buildpacks

可透過 Maven/Gradle 產生 Cloud Native Buildpacks image

Cloud Native Buildpacks 是 2018 由 Pivotal, Heroku 發起,於10月加入Cloud Native沙盒,目標是要統一Buildpacks生態系。

修改 pom.xml

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-image</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>false</executable>
                    <image>
                        <name>test/docker-cnb:${project.version}</name>
                    </image>
                </configuration>
            </plugin>
        </plugins>
    </build>

以指令產生

mvn clean package

也可以用 build-image target

mvn spring-boot:build-image

        <configuration>
          <executable>false</executable>
          <image>
            <name>test/docker-cnb:${project.version}</name>
            <pullPolicy>IF_NOT_PRESENT</pullPolicy>
          </image>
        </configuration>

可加上 pullPolicy

  • ALWAYS

  • IF_NOT_PRESENT: 不存在基礎 image 時才拉取

  • NEVER


GraalVM image

Spring Boot 3.0 支援 GraalVM 22.3+, Native Build Tools Plugin 0.9.17+

GraalVM 是 2019 年 Oracle 發表的高性能,跨語言通用 VM,可提升 Java, Scala, Grooby, Kotlin 等基於 JVM 的 application。

GraalVM 有一個 Java 撰寫的高級 JIT compiler,借助 Truffle,可執行 Javascript, Ruby, Python

# Getting Started with Oracle GraalVM

產生 native-image

javac HelloWorld.java
native-image HelloWorld
./helloworld
Hello, World!

GraalVM applicaiton 跟傳統 application 的差異

對於 Java applicaiton,GraalVM 提供兩種運作方法

  • 在 HotSpot VM 上使用 Graal 的 JIT compiler,一邊編譯,一邊運作

  • 以 AOT 編譯的 GraalVM 原生 image 可執行檔案運作,是靜態預先編譯

GraalVM applcation 的差別

  • GraalVM image 會進行靜態分析

  • 產生 image 時,無法執行到的 code 會被刪除,不會成為可執行檔案的一部分

  • GraalVM 不能直接偵測 code 的動態元素,ex: reflection, resource, 序列化, 動態代理,必須提前宣告設定

  • 應用的 classpath 路徑是固定的,不能修改

  • 不支援 bean 的延遲載入

  • 某些地方不完全相容傳統的 java app


spring boot 產生 image 時

  • classpath 固定,無法變更

  • bean 不能被修改,不支援 @Profile@ConditionalOnProperty

Spring AOT 在產生 image 時,同時會產生

  • Java source code

  • byte code

  • GraalVM 相關 JSON 描述檔案,包含

    • resource-config.json Resource hints

    • reflection.config.json Reflection hints

    • serialization-config.json Serialization hints

    • proxy-config.json Java Proxy hints

    • jni-config.json JNI hints


https://start.spring.io/ 產生的 project,增加 Graal VM Nartive Support,maven pom.xml 裡面就包含了 native, native test 兩個 profiles

        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
        </plugins>

使用 maven 產生 image

mvn -Pnative spring-boot:build-image

可用 docker 執行

docker run --rm -p 8080:8080 docker.io/library/spring-boot-graalvm:1.0

也可以安裝 GraalVM Native Build Tools

sdk install java 23.0.1-graal
sdk use java 23.0.1-graal

用 maven

mvn -Pnative native:compile

會產生

  • spring-boot-graalvm: app 可執行檔案

  • spring-boot-graalvm.build_artifacts.txt: 說明 txt