1. application.properties
關閉Redis相關設定
---省略其他設定---
#L2 Cache 關閉 (Redis)
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=false
# 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
spring.data.redis.repositories.enabled=false
# 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
spring.session.store-type=none
開啟Redis相關設定
---省略其他設定---
# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}
**調整後的 application.properties **
#各類資料庫的 url 參考:
# H2 database ==> jdbc:h2:mem:testdb
# SQL-Server ==> jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
# example ==> jdbc:sqlserver://sql.lewis-home.tw:1433;databasename=MBS;encrypt=false
# MySQL ==> jdbc:mysql://[hosts][:portNumber][/database]
# example ==> jdbc:mysql://mysql.lewis-home.tw:33060
# AS400(Jt400)==> jdbc:as400://[hosts][;property=value]
# example ==> jdbc:as400://as400system;transaction isolation=none;translate binary=true;date format=iso;prompt=false
# Oracle ==> jdbc:oracle:thin:@[HOST][:PORT]:SID or jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE
# example ==> jdbc:oracle:thin:@oracle.lewis-home.tw:1521:oracle.lewis-home.tw
# postgres ==> jdbc:postgresql://@[netloc][:port][/dbname][?param1=value1&...]
# example ==> jdbc:postgresql://postsql.lewis-home.tw:5432/database
#各類資料庫的 driver class Name
# H2 database ==> org.h2.Driver
# SQL-Server ==> com.microsoft.sqlserver.jdbc.SQLServerDriver
# MySQL ==> com.mysql.jdbc.Driver
# AS400(Jt400)==> com.ibm.as400.access.AS400JDBCDriver
# Oracle ==> oracle.jdbc.driver.OracleDriver
# postgres ==> org.postgresql.Driver
#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
primary.datasource.enabled=true
primary.datasource.jdbcurl=jdbc:sqlserver://sql.lewishome.tw:1433;databasename=DbMuser1;encrypt=false;characterEncoding=utf-8
primary.datasource.username=Muser1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: primary_datasource_password
primary.datasource.password=${KEYSTORE:primary_datasource_password}
primary.datasource.driver_class_name=com.microsoft.sqlserver.jdbc.SQLServerDriver
primary.datasource.hibernate.hbm2ddl.auto=update
#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
# secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/pdb1
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSER1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
# Encrypted in KeyStore with alias: secondary_datasource_password
secondary.datasource.password=${KEYSTORE:secondary_datasource_password}
secondary.datasource.driver_class_name=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update
# secondary.datasource.hibernate.dialect=org.hibernate.dialect.Oracle12cDialect
# # CSRF protection whitelist - add /doMenu endpoint
# csrf.endpoint.white-list=/jwtAuth/**;/systemApiTest/**;/opt/fonts/**;{base}/callback/**;/home;/doMenu
# mail_resource_jndi=java=jboss/mail/mailSmtp
mail.smtp.host=mail.lewis-home.tw
mail.smtp.port=25
mail.sender=lewis@lewis-home.tw
mail.sender_name=lewis.yang
mail.subject.prefix=[WebAppSystem]
# #L2 Cache 關閉 (Redis)
# redis.l2.cache.enabled=false
# # 關閉 Spring Data Redis repositories 功能,避免不必要的 Redis 相關功能被啟用
# spring.data.redis.repositories.enabled=false
# # 指定 Session 儲存庫為 none,表示不使用任何 Session 儲存庫,適用於不需要 Session 管理的應用程式
# spring.session.store-type=none
# 當 Redis 開啟時 (L1 + L2 模式) ---
# 建議同時設定 Session 刷新模式(由 Redis 索引管理)
spring.main.allow-bean-definition-overriding=true
redis.l2.cache.enabled=true
# 指定 Session 儲存庫為 Redis
spring.session.store-type=redis
# 使用 Redis 索引管理 Session 刷新,確保 Session 在 Redis 中的有效性和一致性
spring.session.redis.repository-type=indexed
# Redis 連線設定
redis_host=redis.lewishome.tw
redis_port=6379
# 若沒有實作加密機制,則直接使用明文密碼(不建議)
redis_password=${KEYSTORE:redis_password}
2. WebappApplication.java
位置: tw.lewishome.webapp;
---省略其他設定---
@SpringBootApplication(exclude = {
// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
RedisAutoConfiguration.class
,RedisRepositoriesAutoConfiguration.class
// ,SessionAutoConfiguration.class
})
**調整後的 WebappApplication.java **
package tw.lewishome.webapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
import lombok.extern.slf4j.Slf4j;
/**
* WebappApplication 是 Spring Boot 應用程式的主要入口點。
* 此類別負責啟動應用程式並加載 Spring 上下文。
*
* @author Lewis
* @version 1.0
*/
@SpringBootApplication(exclude = {
// 當Redis開關關閉時,我們不希望 Spring Session 去碰 Redis
RedisAutoConfiguration.class
,RedisRepositoriesAutoConfiguration.class
// ,SessionAutoConfiguration.class
})
@Slf4j
public class WebappApplication {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new AsyncServiceWorkerSample instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
private WebappApplication() {
// This constructor is intentionally empty. Nothing special is needed here.
}
/**
* 主方法是應用程式的進入點。
* 此方法啟動 Spring 應用程式。
*
* @param args 命令列參數
*/
public static void main(String[] args) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ SpringApplication WebappApplication started! │");
log.info("└────────────────────────────────────────────────────┘");
SpringApplication springApplication = new SpringApplication(WebappApplication.class);
// 手動添加 EnvironmentPostProcessor,以確保在應用程式啟動時處理安全屬性(如加密的密碼)
springApplication.addInitializers(new PropertySecureProcessInitializer(
new PropertySecureProcessor(),
new PropertySecureConverter()));
springApplication.run(args);
}
}
3. CachePolicy.java
-位置: package tw.lewishome.webapp.base.cache;
package tw.lewishome.webapp.base.cache;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* 集中定義系統中所有快取空間的名稱與過期時間
*/
public class CachePolicy {
// 統一管理快取名稱字串,避免 typo
public static final String USER_MENU_ITEMS = "catchUserMenuItems";
public static final String USER_AUTH_SEQ = "catchUserAuthSeq";
public static final String SHORT_LIVED = "shortLivedCache";
/**
* L1 (Caffeine) 的特定配置
* 回傳 Map<CacheName, TTL_Minutes>
*/
public static Map<String, Integer> getL1CustomPolicies() {
Map<String, Integer> policies = new HashMap<>();
policies.put(USER_MENU_ITEMS, 1); // 1 分鐘 (測試用)
policies.put(USER_AUTH_SEQ, 3); // 3 分鐘 (對應原本的預設)
policies.put(SHORT_LIVED, 1); // 1 分鐘
return policies;
}
/**
* L2 (Redis) 的特定配置
* 回傳 Map<CacheName, Duration>
*/
public static Map<String, Duration> getL2CustomPolicies() {
Map<String, Duration> policies = new HashMap<>();
policies.put(USER_MENU_ITEMS, Duration.ofMinutes(30));
policies.put(USER_AUTH_SEQ, Duration.ofHours(12));
return policies;
}
}
4. CaffeineConfiguration.java
-位置: package tw.lewishome.webapp.base.cache.caffeine;
package tw.lewishome.webapp.base.cache.caffeine;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.cache.redis.MultiCacheInterceptor;
/**
* Caffeine 快取設定(CaffeineConfiguration)。
*
* 修正重點:
* 1. 統一管理攔截器,解決 NoUniqueBeanDefinitionException。
* 2. 自動探測 RedisManager (L2),支援動態聯動與降級。
*/
@EnableCaching
@Configuration
@Slf4j
public class CaffeineConfiguration {
/**
* 建立 L1 (Caffeine) CacheManager
*/
@Bean(name = "caffeineCacheManager")
@Primary
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static CacheManager caffeineCacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
List<CaffeineCache> caches = new ArrayList<>();
CachePolicy.getL1CustomPolicies().forEach((name, ttlMinutes) -> {
caches.add(createCaffeineCache(name, ttlMinutes));
});
cacheManager.setCaches(caches);
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [L1 Cache] CaffeineCacheManager 初始化完成 │");
log.info("└────────────────────────────────────────────────────┘");
return cacheManager;
}
/**
* 全系統唯一的 Primary 攔截器。
* 負責整合 L1 (Caffeine) 與探測到的 L2 (Redis)。
*/
@Bean(name = "multiCacheInterceptor") // 使用自定義名稱避免與 Spring 內建衝突
@Primary
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public static CacheInterceptor customerCacheInterceptor(
@Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager,
ObjectProvider<CacheManager> allManagersProvider) {
MultiCacheInterceptor interceptor = new MultiCacheInterceptor();
// 1. 設定註冊解析器 (使用正確的 AnnotationCacheOperationSource)
interceptor.setCacheOperationSources(new AnnotationCacheOperationSource());
// 2. 注入 L1 Manager 給自定義邏輯使用
interceptor.setCaffeineCacheManager(caffeineCacheManager);
// 3. 動態探測 L2 Manager (排除掉自己,剩下的就是 RedisCacheManager)
CacheManager redisManager = allManagersProvider.orderedStream()
.filter(cm -> cm != caffeineCacheManager)
.findFirst()
.orElse(null);
if (redisManager != null) {
// 重要:必須設定父類別 Manager 為 Redis,super.doGet/doPut 才會生效
interceptor.setCacheManager(redisManager);
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [Cache] L1+L2 (Caffeine + Redis) 聯動模式啟動中... │");
log.info("└────────────────────────────────────────────────────┘");
} else {
// 降級:僅使用 Caffeine
interceptor.setCacheManager(caffeineCacheManager);
log.warn("┌────────────────────────────────────────────────────┐");
log.warn("│ [Cache] 未偵測到 L2,系統僅以 L1 (Caffeine) 模式運作 │");
log.warn("└────────────────────────────────────────────────────┘");
}
return interceptor;
}
private static CaffeineCache createCaffeineCache(String name, int ttlMinutes) {
return new CaffeineCache(name, Caffeine.newBuilder()
.expireAfterWrite(ttlMinutes, TimeUnit.MINUTES)
.maximumSize(1000)
.recordStats()
.build());
}
}
4. RedisConfiguration.java
-位置: package tw.lewishome.webapp.base.cache.redis;
package tw.lewishome.webapp.base.cache.redis;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.CachePolicy;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.base.utility.common.TypeConvert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.AnnotationCacheOperationSource;
import org.springframework.cache.interceptor.CacheInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Role;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* Redis 二級快取配置 (L2)。
* 整合已存在的 CaffeineCacheManager (L1) 並註冊 MultiCacheInterceptor。
*/
@Configuration
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(name = "redis.l2.cache.enabled", havingValue = "true")
public class RedisConfiguration {
@Autowired
private SystemEnvReader systemEnvReader;
/**
* 【核心聯動】註冊自定義的 MultiCacheInterceptor。
* 透過 @Primary 確保標註了 @Cacheable 的方法優先使用此攔截器。
*
* @param redisCacheManager 由下方 Bean 產生的 L2 管理器
* @param caffeineCacheManager 由 CaffeineConfiguration 產生的 L1 管理器
*/
@Bean(name = "multiCacheInterceptor") // 使用具體名稱避免與內建 Bean 混淆
@Primary
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheInterceptor cacheInterceptor(
@Qualifier("redisCacheManager") CacheManager redisCacheManager,
@Qualifier("caffeineCacheManager") CacheManager caffeineCacheManager) {
log.info("┌────────────────────────────────────────────────────┐");
log.info("│ [Cache] L1 (Caffeine) + L2 (Redis) 聯動模式啟動 │");
log.info("└────────────────────────────────────────────────────┘");
MultiCacheInterceptor interceptor = new MultiCacheInterceptor();
// 1. 注入 L1 管理器:供 MultiCacheInterceptor.doGet 優先從記憶體讀取
interceptor.setCaffeineCacheManager(caffeineCacheManager);
// 2. 注入 L2 管理器:供父類別處理 Redis 存取。
// 解決「Redis 讀不到資料」的關鍵:必須讓父類別知道主要 CacheManager 是誰
interceptor.setCacheManager(redisCacheManager);
// 3. 設定註冊解析器:讀取方法上的 @Cacheable 註解參數
interceptor.setCacheOperationSources(new AnnotationCacheOperationSource());
return interceptor;
}
@Bean(name = "redisCacheManager")
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
String redisTimeToLife = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_LIFE_HOUR", "1") : "1";
int defaultHours = TypeConvert.toInteger(redisTimeToLife);
// 預設配置:包含 JSON 序列化與預設過期時間
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(defaultHours))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(getL2Configurations(defaultConfig))
.build();
}
private Map<String, RedisCacheConfiguration> getL2Configurations(RedisCacheConfiguration defaultConfig) {
Map<String, RedisCacheConfiguration> configurations = new HashMap<>();
// 根據 CachePolicy 設定不同區塊的 TTL
CachePolicy.getL2CustomPolicies().forEach((name, ttl) -> {
configurations.put(name, defaultConfig.entryTtl(ttl));
});
return configurations;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public LettuceConnectionFactory redisConnectionFactory() {
try {
RedisStandaloneConfiguration config = getRedisStandaloneConfiguration();
return new LettuceConnectionFactory(config);
} catch (Exception ex) {
log.error("CRITICAL: Failed to create RedisConnectionFactory: {}", ex.getMessage());
return null;
}
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
private RedisStandaloneConfiguration getRedisStandaloneConfiguration() {
String redisHostName = systemEnvReader != null
? systemEnvReader.getProperty("REDIS_HOST", "redis.lewishome.tw")
: "redis.lewishome.tw";
// 確保 Port 為數字字串,避免轉換出錯
String redisPort = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PORT", "6379") : "6379";
String redisHostPassword = systemEnvReader != null ? systemEnvReader.getProperty("REDIS_PASSWORD", "XXXXXXX")
: "XXXXXXXX";
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(redisHostName,
TypeConvert.toInteger(redisPort));
config.setPassword(RedisPassword.of(redisHostPassword));
return config;
}
}
5. MultiCacheInterceptor.java
-位置:package tw.lewishome.webapp.base.cache.redis;
寫入/刪除快取時,同步更新 L1 和 L2 的資料
package tw.lewishome.webapp.base.cache.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheInterceptor;
/**
* MultiCacheInterceptor 最終聯動版
*
* 修正重點:
* 1. 透過 doGet 實現 L1 -> L2 -> 回填 L1 的完整讀取鏈。
* 2. 透過 doPut 確保 @Cacheable 執行完後,資料同步存入 Caffeine 與 Redis。
* 3. 補強 L2 操作的異常捕捉,確保 Redis 離線時系統不崩潰。
*/
@Slf4j
public class MultiCacheInterceptor extends CacheInterceptor {
/** Caffeine CacheManager (L1) */
private CacheManager caffeineCacheManager;
/**
* 注入 L1 CacheManager
*
* @param caffeineCacheManager Caffeine CacheManager 實例
*/
public void setCaffeineCacheManager(CacheManager caffeineCacheManager) {
this.caffeineCacheManager = caffeineCacheManager;
}
/**
* 讀取快取攔截 (L1 -> L2)
* 覆寫自 CacheAspectSupport
*/
@Override
protected Cache.ValueWrapper doGet(Cache cache, Object key) {
// 1. 優先從本地 L1 (Caffeine) 檢索
if (caffeineCacheManager != null) {
Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
if (caffeineCache != null) {
Cache.ValueWrapper l1Result = caffeineCache.get(key);
if (l1Result != null) {
log.debug("L1 (Caffeine) Hit! [{} : {}]", cache.getName(), key);
return l1Result;
}
}
}
// 2. L1 未命中,嘗試從 L2 (Redis) 讀取
Cache.ValueWrapper result = null;
try {
// 此處 super.doGet 會使用 RedisConfiguration 中設定的 redisCacheManager
result = super.doGet(cache, key);
} catch (Exception e) {
log.warn("L2 Cache 讀取失敗(Redis 可能未啟動或逾時),系統自動降級: {}", e.getMessage());
}
// 3. 若 L2 命中,將結果回填至 L1,確保下次存取變快
if (result != null && caffeineCacheManager != null) {
Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
if (caffeineCache != null) {
log.debug("L2 Hit. Populating L1... [{} : {}]", cache.getName(), key);
caffeineCache.put(key, result.get());
}
}
return result;
}
/**
* 寫入快取攔截 (同步更新 L1 與 L2)
* 覆寫自 CacheAspectSupport
*/
@SuppressWarnings("null")
@Override
protected void doPut(Cache cache, Object key, Object value) {
// 1. 同步寫入 L1 (Caffeine)
if (caffeineCacheManager != null) {
Cache caffeineCache = caffeineCacheManager.getCache(cache.getName());
if (caffeineCache != null) {
caffeineCache.put(key, value);
log.debug("L1 (Caffeine) Updated. [{} : {}]", cache.getName(), key);
}
}
// 2. 呼叫父類別寫入 L2 (Redis)
try {
super.doPut(cache, key, value);
} catch (Exception e) {
// 寫入 Redis 失敗時僅記錄 Log,不中斷業務邏輯,實現韌性設計
log.warn("L2 Cache 寫入失敗: {}", e.getMessage());
}
}
}
6. 調整SystemEnvReader.java
-位置:package tw.lewishome.webapp.base.utility.common;
-因為系統開機時有以下警告訊息:
trationDelegate$BeanPostProcessorChecker : Bean 'systemEnvReader' of type [tw.lewishome.webapp.base.utility.common.SystemEnvReader] is not eligible for getting processed by all BeanPostProcessors
(for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a currently created BeanPostProcessor [meterRegistryPostProcessor]?
Check the corresponding BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be post-processed, declare it with ROLE_INFRASTRUCTURE.
所以依建議,將SystemEnvReader.java宣告為ROLE_INFRASTRUCTURE
@Component
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SystemEnvReader {
--省略程式邏輯 --
}
7. 使用Catchable方式
@Cacheable(cacheNames = CachePolicy.USER_AUTH_SEQ, key = "#userId")
public List<String> getSysUserLastAuth(String userId) {
-- 省略程式邏輯 --
}