2025/6/9

Spring Boot 3 Cache

spring 提供對 cache 的 API 介面

  • org.springframework.cache.Cache

  • org.springframework.cache.CacheManager

如果 application 沒有註冊 CacheManager 或 CacheResolver,spring 會依照以下順序檢測 cache component

Generic -> JCache (JSR-107) (EhCache3, Hazelcast, Infinispan..) -> Hazelcast -> Infinispan -> Couchbase -> Redis -> Caffeine -> Cache2k -> Simple

org.springframework.boot.autoconfigure.AutoConfiguration.imports 有註冊自動設定類別

自動設定類別為 CacheAutoConfiguration,參數綁定類別為 CacheProperties

設定參數 spring.cache.*

pom.xml

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

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

application.yml

spring:
  cache:
    type: none
    # none 代表禁用 cache
    # type: redis

type 可設定為 COUCHBASE/GENERIC/REDIS/HAZELCAST/CACHE2K/CAFFEINE/JCACHE/INFINISPAN/NONE/SIMPLE


預設簡單 cache

如果沒有設定任何 cache 元件,就是使用 Simple,也就是 thread-safe 的 ConcurrentHashMap

pom.xml 加上 web

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

application 要加上 @EnableCaching

@EnableCaching
@SpringBootApplication
public class CacheApplication {

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

}

先建立一個兩數相乘的 cache service

CacheService.java

package com.test.cache;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class CacheService {

    @Cacheable("calc")
    public int multiply(int a, int b) {
        int c = a * b;
        log.info("{} * {} = {}", a, b, c);
        return c;
    }

}

@Cacheable 代表該 method 使用 cache,這是用 AOP 的方法做的

一個測試用的 web service

package com.test.cache;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class CacheController {

    private final CacheService cacheService;

    @RequestMapping("/multiply")
    public int multiply(@RequestParam("a") int a,
                        @RequestParam("b") int b) {
        return cacheService.multiply(a, b);
    }

}

測試網址 http://localhost:8080/multiply?a=2&b=3

重複多測試幾次,console log 上面都還是只有一行 log。代表重複的參數,會直接從 cache 取得結果,不會進入 service

com.test.cache.CacheService              : 2 * 3 = 6

Redis Cache

pom.xml 加上 redis

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

application.yml

spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      password: password

redis-cli

# redis-cli -a password
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
1) "calc::SimpleKey [2, 3]"
127.0.0.1:6379> get "calc::SimpleKey [2, 3]"
"\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x06"

可在建立 cache 名稱,time-to-live 代表只存放 10s

spring:
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      password: password
  cache:
    type: redis
    cache-names: "calc,test"
    redis:
      time-to-live: "10s"

針對不同 cache 設定不同規則,可透過 RedisCacheManagerBuilderCustomizer

package com.test.cache;

import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;

import java.time.Duration;

@Configuration
public class CacheConfiguration {

    /**
     * 比 application.yml 的設定內容優先權高
     * @return
     */
    @Bean
    public RedisCacheManagerBuilderCustomizer myRedisCacheManagerBuilderCustomizer() {
        return (builder) -> builder
                .withCacheConfiguration("calc", RedisCacheConfiguration
                        .defaultCacheConfig().entryTtl(Duration.ofSeconds(5)))
                .withCacheConfiguration("test", RedisCacheConfiguration
                        .defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));

    }

}

網址 http://localhost:8080/multiply?a=2&b=3

calc, test 兩個 redis cache 都有資料

# redis-cli -a password -n test
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
1) "calc::SimpleKey [2, 3]"

# redis-cli -a password -n calc
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
1) "calc::SimpleKey [2, 3]"

2025/6/2

Spring Boot 3 quartz

簡單的任務可用 spring task scheduler,複雜的要改用 quartz

pom.xml 要加上 spring-boot-starter-quartz

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

自動設定類別是 QuartzAutoConfiguration,透過註冊的 SchedulerFactoryBean 就能產生 Scheduler。參數綁定類別為 QuartzProperties,參數為 spring.quartz.*

產生 task

只要繼承 QuartzJobBean,實作 executeInternal,就可以產生 quartz 的 Job

SimpleTask.java

package com.test.quartz;

import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.springframework.scheduling.quartz.QuartzJobBean;

@Slf4j
public class SimpleTask extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext context) {
        log.info("simple task");
    }

}

設定

  • JobDetail

  • Calendar: 指定/排除特定時間

  • Trigger

TaskConfig.java

package com.test.quartz;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@RequiredArgsConstructor
@Configuration
public class TaskConfig {

    public static final String SIMPLE_TASK = "simple-task";
//    private final SchedulerFactoryBean schedulerFactoryBean;
//
//    @PostConstruct
//    public void init() throws SchedulerException {
//        Scheduler scheduler = schedulerFactoryBean.getScheduler();
//        boolean exists = scheduler.checkExists(JobKey.jobKey(SIMPLE_TASK));
//        if (!exists) {
//            scheduler.scheduleJob(simpleTask(), simpleTaskTrigger());
//        }
//    }

    @Bean
    public JobDetail simpleTask() {
        return JobBuilder.newJob(SimpleTask.class)
                .withIdentity(SIMPLE_TASK)
                .withDescription("simple-task")
                .storeDurably()
                .build();
    }

    @Bean
    public Trigger simpleTaskTrigger() {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/3 * * * * ? *");
        return TriggerBuilder.newTrigger()
                .withIdentity("simple-task-trigger")
                .forJob(simpleTask())
                .withSchedule(cronScheduleBuilder)
                .build();
    }

}

cron expression 多了最後一個 [<year>] 可以不填寫

<second> <minute> <hour> <day of month> <month> <day of week> [<year>]

ex:

"0/3 * * * * ? 2025-2030" 代表 2025-2030 每 3s 執行一次

啟動後,會看到

org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

設定參數

以下代表將 thread 設定為 5 個

spring:
  quartz:
    properties:
      org:
        quartz:
          threadPool:
            threadCount: 5

persistence

quartz 支援兩種 persistence 方式

  • memory

    預設。每次停止 application 就會把資料丟掉

  • JDBC

pom.xml 加上 JDBC

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/testweb
    username: root
    password: password
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always # always
    overwrite-existing-jobs: true
    properties:
      org:
        quartz:
          threadPool:
            threadCount: 5

initialize-schema 有三種

  • ALWAYS: 每一次都會重建

  • EMBEDDED: 只初始化嵌入式 DB

  • NEVER


動態維護 task

如果 task 太多,會有大量設定的 code,可改用 SchedulerFactoryBean 動態 add/remove task

package com.test.quartz;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@RequiredArgsConstructor
@Configuration
public class TaskConfig {

    public static final String SIMPLE_TASK = "simple-task";
    private final SchedulerFactoryBean schedulerFactoryBean;

    @PostConstruct
    public void init() throws SchedulerException {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        boolean exists = scheduler.checkExists(JobKey.jobKey(SIMPLE_TASK));
        if (!exists) {
            scheduler.scheduleJob(simpleTask(), simpleTaskTrigger());
        }
    }

//    @Bean
    public JobDetail simpleTask() {
        return JobBuilder.newJob(SimpleTask.class)
                .withIdentity(SIMPLE_TASK)
                .withDescription("simple-task")
                .storeDurably()
                .build();
    }

//    @Bean
    public Trigger simpleTaskTrigger() {
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/3 * * * * ? *");
        return TriggerBuilder.newTrigger()
                .withIdentity("simple-task-trigger")
                .forJob(simpleTask())
                .withSchedule(cronScheduleBuilder)
                .build();
    }

}