SpringBoot多級(jí)緩存實(shí)現(xiàn)方案總結(jié)
1.背景
緩存,就是讓數(shù)據(jù)更接近使用者,讓訪(fǎng)問(wèn)速度加快,從而提升系統(tǒng)性能。工作機(jī)制大概是先從緩存中加載數(shù)據(jù),如果沒(méi)有,再?gòu)穆僭O(shè)備(eg:數(shù)據(jù)庫(kù))中加載數(shù)據(jù)并同步到緩存中。
所謂多級(jí)緩存,是指在整個(gè)系統(tǒng)架構(gòu)的不同系統(tǒng)層面進(jìn)行數(shù)據(jù)緩存,以提升訪(fǎng)問(wèn)速度。主要分為三層緩存:網(wǎng)關(guān)nginx緩存、分布式緩存、本地緩存。這里的多級(jí)緩存就是用redis分布式緩存+caffeine本地緩存整合而來(lái)。
平時(shí)我們?cè)陂_(kāi)發(fā)過(guò)程中,一般都是使用redis實(shí)現(xiàn)分布式緩存、caffeine操作本地緩存,但是發(fā)現(xiàn)只使用redis或者是caffeine實(shí)現(xiàn)緩存都有一些問(wèn)題:
- 一級(jí)緩存:Caffeine是一個(gè)一個(gè)高性能的 Java 緩存庫(kù);使用 Window TinyLfu 回收策略,提供了一個(gè)近乎最佳的命中率。優(yōu)點(diǎn)數(shù)據(jù)就在應(yīng)用內(nèi)存所以速度快。缺點(diǎn)受應(yīng)用內(nèi)存的限制,所以容量有限;沒(méi)有持久化,重啟服務(wù)后緩存數(shù)據(jù)會(huì)丟失;在分布式環(huán)境下緩存數(shù)據(jù)數(shù)據(jù)無(wú)法同步;
- 二級(jí)緩存:redis是一高性能、高可用的key-value數(shù)據(jù)庫(kù),支持多種數(shù)據(jù)類(lèi)型,支持集群,和應(yīng)用服務(wù)器分開(kāi)部署易于橫向擴(kuò)展。優(yōu)點(diǎn)支持多種數(shù)據(jù)類(lèi)型,擴(kuò)容方便;有持久化,重啟應(yīng)用服務(wù)器緩存數(shù)據(jù)不會(huì)丟失;他是一個(gè)集中式緩存,不存在在應(yīng)用服務(wù)器之間同步數(shù)據(jù)的問(wèn)題。缺點(diǎn)每次都需要訪(fǎng)問(wèn)redis存在IO浪費(fèi)的情況。
綜上所述,我們可以通過(guò)整合redis和caffeine實(shí)現(xiàn)多級(jí)緩存,解決上面單一緩存的痛點(diǎn),從而做到相互補(bǔ)足。
2.整合實(shí)現(xiàn)
2.1思路
Spring 本來(lái)就提供了Cache的支持,最核心的就是實(shí)現(xiàn)Cache和CacheManager接口。但是Spring Cache存在以下問(wèn)題:
- Spring Cache 僅支持單一的緩存來(lái)源,即:只能選擇 Redis 實(shí)現(xiàn)或者 Caffeine 實(shí)現(xiàn),并不能同時(shí)使用。
- 數(shù)據(jù)一致性:各層緩存之間的數(shù)據(jù)一致性問(wèn)題,如應(yīng)用層緩存和分布式緩存之前的數(shù)據(jù)一致性問(wèn)題。
由此我們可以通過(guò)重新實(shí)現(xiàn)Cache和CacheManager接口,整合redis和caffeine,從而實(shí)現(xiàn)多級(jí)緩存。在講實(shí)現(xiàn)原理之前先看看多級(jí)緩存調(diào)用邏輯圖:

2.2實(shí)現(xiàn)
首先,我們需要一個(gè)多級(jí)緩存配置類(lèi),方便對(duì)緩存屬性的動(dòng)態(tài)配置,通過(guò)開(kāi)關(guān)做到可插拔。
@ConfigurationProperties(prefix = "multilevel.cache")
@Data
public class MultilevelCacheProperties {
?
? ?/**
? ? * 一級(jí)本地緩存最大比例
? ? */
? ?private Double maxCapacityRate = 0.2;
?
? ?/**
? ? * 一級(jí)本地緩存與最大緩存初始化大小比例
? ? */
? ?private Double initRate = 0.5;
?
? ?/**
? ? * 消息主題
? ? */
? ?private String topic = "multilevel-cache-topic";
?
? ?/**
? ? * 緩存名稱(chēng)
? ? */
? ?private String name = "multilevel-cache";
?
? ?/**
? ? * 一級(jí)本地緩存名稱(chēng)
? ? */
? ?private String caffeineName = "multilevel-caffeine-cache";
?
? ?/**
? ? * 二級(jí)緩存名稱(chēng)
? ? */
? ?private String redisName = "multilevel-redis-cache";
?
? ?/**
? ? * 一級(jí)本地緩存過(guò)期時(shí)間
? ? */
? ?private Integer caffeineExpireTime = 300;
?
? ?/**
? ? * 二級(jí)緩存過(guò)期時(shí)間
? ? */
? ?private Integer redisExpireTime = 600;
?
?
? ?/**
? ? * 一級(jí)緩存開(kāi)關(guān)
? ? */
? ?private Boolean caffeineSwitch = true;
?
}在自動(dòng)配置類(lèi)使用 @EnableConfigurationProperties(MultilevelCacheProperties.class) 注入即可使用。
接下來(lái)就是重新實(shí)現(xiàn)spring的Cache接口,整合caffeine本地緩存和redis分布式緩存實(shí)現(xiàn)多級(jí)緩存
package com.plasticene.boot.cache.core.manager;
?
import com.plasticene.boot.cache.core.listener.CacheMessage;
import com.plasticene.boot.cache.core.prop.MultilevelCacheProperties;
import com.plasticene.boot.common.executor.plasticeneThreadExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
?
import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.*;
?
/**
* @author fjzheng
* @version 1.0
* @date 2022/7/20 17:03
*/
@Slf4j
public class MultilevelCache extends AbstractValueAdaptingCache {
?
? ?@Resource
? ?private MultilevelCacheProperties multilevelCacheProperties;
? ?@Resource
? ?private RedisTemplate redisTemplate;
?
?
? ?ExecutorService cacheExecutor = new plasticeneThreadExecutor(
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 2,
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 20,
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 200,
? ? ? ? ? "cache-pool"
? );
?
? ?private RedisCache redisCache;
? ?private CaffeineCache caffeineCache;
?
? ?public MultilevelCache(boolean allowNullValues,RedisCache redisCache, CaffeineCache caffeineCache) {
? ? ? ?super(allowNullValues);
? ? ? ?this.redisCache = redisCache;
? ? ? ?this.caffeineCache = caffeineCache;
? }
?
?
? ?@Override
? ?public String getName() {
? ? ? ?return multilevelCacheProperties.getName();
?
? }
?
? ?@Override
? ?public Object getNativeCache() {
? ? ? ?return null;
? }
?
? ?@Override
? ?public <T> T get(Object key, Callable<T> valueLoader) {
? ? ? ?Object value = lookup(key);
? ? ? ?return (T) value;
? }
?
? ?/**
? ? * 注意:redis緩存的對(duì)象object必須序列化 implements Serializable, 不然緩存對(duì)象不成功。
? ? * 注意:這里asyncPublish()方法是異步發(fā)布消息,然后讓分布式其他節(jié)點(diǎn)清除本地緩存,防止當(dāng)前節(jié)點(diǎn)因更新覆蓋數(shù)據(jù)而其他節(jié)點(diǎn)本地緩存保存是臟數(shù)據(jù)
? ? * 這樣本地緩存數(shù)據(jù)才能成功存入
? ? * @param key
? ? * @param value
? ? */
? ?@Override
? ?public void put(@NonNull Object key, Object value) {
? ? ? ?redisCache.put(key, value);
? ? ? ?// 異步清除本地緩存
? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ?asyncPublish(key, value);
? ? ? }
? }
?
? ?/**
? ? * key不存在時(shí),再保存,存在返回當(dāng)前值不覆蓋
? ? * @param key
? ? * @param value
? ? * @return
? ? */
? ?@Override
? ?public ValueWrapper putIfAbsent(@NonNull Object key, Object value) {
? ? ? ?ValueWrapper valueWrapper = redisCache.putIfAbsent(key, value);
? ? ? ?// 異步清除本地緩存
? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ?asyncPublish(key, value);
? ? ? }
? ? ? ?return valueWrapper;
? }
?
?
? ?@Override
? ?public void evict(Object key) {
? ? ? ?// 先清除redis中緩存數(shù)據(jù),然后通過(guò)消息推送清除所有節(jié)點(diǎn)caffeine中的緩存,
? ? ? ?// 避免短時(shí)間內(nèi)如果先清除caffeine緩存后其他請(qǐng)求會(huì)再?gòu)膔edis里加載到caffeine中
? ? ? ?redisCache.evict(key);
? ? ? ?// 異步清除本地緩存
? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ?asyncPublish(key, null);
? ? ? }
? }
?
? ?@Override
? ?public boolean evictIfPresent(Object key) {
? ? ? ?return false;
? }
?
? ?@Override
? ?public void clear() {
? ? ? ?redisCache.clear();
? ? ? ?// 異步清除本地緩存
? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ?asyncPublish(null, null);
? ? ? }
? }
?
?
?
? ?@Override
? ?protected Object lookup(Object key) {
? ? ? ?Assert.notNull(key, "key不可為空");
? ? ? ?ValueWrapper value;
? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ?// 開(kāi)啟一級(jí)緩存,先從一級(jí)緩存緩存數(shù)據(jù)
? ? ? ? ? ?value = caffeineCache.get(key);
? ? ? ? ? ?if (Objects.nonNull(value)) {
? ? ? ? ? ? ? ?log.info("查詢(xún)caffeine 一級(jí)緩存 key:{}, 返回值是:{}", key, value.get());
? ? ? ? ? ? ? ?return value.get();
? ? ? ? ? }
? ? ? }
? ? ? ?value = redisCache.get(key);
? ? ? ?if (Objects.nonNull(value)) {
? ? ? ? ? ?log.info("查詢(xún)r(jià)edis 二級(jí)緩存 key:{}, 返回值是:{}", key, value.get());
? ? ? ? ? ?// 異步將二級(jí)緩存redis寫(xiě)到一級(jí)緩存caffeine
? ? ? ? ? ?if (multilevelCacheProperties.getCaffeineSwitch()) {
? ? ? ? ? ? ? ?ValueWrapper finalValue = value;
? ? ? ? ? ? ? ?cacheExecutor.execute(()->{
? ? ? ? ? ? ? ? ? ?caffeineCache.put(key, finalValue.get());
? ? ? ? ? ? ? });
? ? ? ? ? }
? ? ? ? ? ?return value.get();
? ? ? }
? ? ? ?return null;
? }
?
? ?/**
? ? * 緩存變更時(shí)通知其他節(jié)點(diǎn)清理本地緩存
? ? * 異步通過(guò)發(fā)布訂閱主題消息,其他節(jié)點(diǎn)監(jiān)聽(tīng)到之后進(jìn)行相關(guān)本地緩存操作,防止本地緩存臟數(shù)據(jù)
? ? */
? ?void asyncPublish(Object key, Object value) {
? ? ? ?cacheExecutor.execute(()->{
? ? ? ? ? ?CacheMessage cacheMessage = new CacheMessage();
? ? ? ? ? ?cacheMessage.setCacheName(multilevelCacheProperties.getName());
? ? ? ? ? ?cacheMessage.setKey(key);
? ? ? ? ? ?cacheMessage.setValue(value);
? ? ? ? ? ?redisTemplate.convertAndSend(multilevelCacheProperties.getTopic(), cacheMessage);
? ? ? });
? }
?
?
?
}
?緩存消息監(jiān)聽(tīng):我們通監(jiān)聽(tīng)caffeine鍵值的移除、打印日志方便排查問(wèn)題,通過(guò)監(jiān)聽(tīng)redis發(fā)布的消息,實(shí)現(xiàn)分布式集群多節(jié)點(diǎn)本地緩存清除從而達(dá)到數(shù)據(jù)一致性。
消息體
@Data
public class CacheMessage implements Serializable {
? ?private String cacheName;
? ?private Object key;
? ?private Object value;
? ?private Integer type;
}caffeine移除監(jiān)聽(tīng):
@Slf4j
public class CaffeineCacheRemovalListener implements RemovalListener<Object, Object> {
? ?@Override
? ?public void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause cause) {
? ? ? ?log.info("[移除緩存] key:{} reason:{}", k, cause.name());
? ? ? ?// 超出最大緩存
? ? ? ?if (cause == RemovalCause.SIZE) {
?
? ? ? }
? ? ? ?// 超出過(guò)期時(shí)間
? ? ? ?if (cause == RemovalCause.EXPIRED) {
? ? ? ? ? ?// do something
? ? ? }
? ? ? ?// 顯式移除
? ? ? ?if (cause == RemovalCause.EXPLICIT) {
? ? ? ? ? ?// do something
? ? ? }
? ? ? ?// 舊數(shù)據(jù)被更新
? ? ? ?if (cause == RemovalCause.REPLACED) {
? ? ? ? ? ?// do something
? ? ? }
? }
}
?redis消息監(jiān)聽(tīng):
@Slf4j
@Data
public class RedisCacheMessageListener implements MessageListener {
?
? ?private CaffeineCache caffeineCache;
? ?@Override
? ?public void onMessage(Message message, byte[] pattern) {
? ? ? ?log.info("監(jiān)聽(tīng)的redis message: {}" + message.toString());
? ? ? ?CacheMessage cacheMessage = JsonUtils.parseObject(message.toString(), CacheMessage.class);
? ? ? ?if (Objects.isNull(cacheMessage.getKey())) {
? ? ? ? ? ?caffeineCache.invalidate();
? ? ? } else {
? ? ? ? ? ?caffeineCache.evict(cacheMessage.getKey());
? ? ? }
? }
}最后,通過(guò)自動(dòng)配置類(lèi),注入相關(guān)bean:
**
* @author fjzheng
* @version 1.0
* @date 2022/7/20 17:24
*/
@Configuration
@EnableConfigurationProperties(MultilevelCacheProperties.class)
public class MultilevelCacheAutoConfiguration {
?
? ?@Resource
? ?private MultilevelCacheProperties multilevelCacheProperties;
?
? ?ExecutorService cacheExecutor = new plasticeneThreadExecutor(
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 2,
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 20,
? ? ? ? ? ?Runtime.getRuntime().availableProcessors() * 200,
? ? ? ? ? ?"cache-pool"
? );
?
? ?@Bean
? ?@ConditionalOnMissingBean({RedisTemplate.class})
? ?public ?RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
? ? ? ?RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
? ? ? ?template.setConnectionFactory(factory);
? ? ? ?template.setKeySerializer(new StringRedisSerializer());
? ? ? ?template.setHashKeySerializer(new StringRedisSerializer());
? ? ? ?template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
? ? ? ?template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
? ? ? ?return template;
? }
?
? ?@Bean
? ?public RedisCache redisCache (RedisConnectionFactory redisConnectionFactory) {
? ? ? ?RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
? ? ? ?RedisCacheConfiguration redisCacheConfiguration = defaultCacheConfig();
? ? ? ?redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.of(multilevelCacheProperties.getRedisExpireTime(), ChronoUnit.SECONDS));
? ? ? ?redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
? ? ? ?redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
? ? ? ?RedisCache redisCache = new CustomRedisCache(multilevelCacheProperties.getRedisName(), redisCacheWriter, redisCacheConfiguration);
? ? ? ?return redisCache;
? }
?
? ?/**
? ? * 由于Caffeine 不會(huì)再值過(guò)期后立即執(zhí)行清除,而是在寫(xiě)入或者讀取操作之后執(zhí)行少量維護(hù)工作,或者在寫(xiě)入讀取很少的情況下,偶爾執(zhí)行清除操作。
? ? * 如果我們項(xiàng)目寫(xiě)入或者讀取頻率很高,那么不用擔(dān)心。如果想入寫(xiě)入和讀取操作頻率較低,那么我們可以通過(guò)Cache.cleanUp()或者加scheduler去定時(shí)執(zhí)行清除操作。
? ? * Scheduler可以迅速刪除過(guò)期的元素,***Java 9 +***后的版本,可以通過(guò)Scheduler.systemScheduler(), 調(diào)用系統(tǒng)線(xiàn)程,達(dá)到定期清除的目的
? ? * @return
? ? */
? ?@Bean
? ?@ConditionalOnClass(CaffeineCache.class)
? ?@ConditionalOnProperty(name = "multilevel.cache.caffeineSwitch", havingValue = "true", matchIfMissing = true)
? ?public CaffeineCache caffeineCache() {
? ? ? ?int maxCapacity = (int) (Runtime.getRuntime().totalMemory() * multilevelCacheProperties.getMaxCapacityRate());
? ? ? ?int initCapacity = (int) (maxCapacity * multilevelCacheProperties.getInitRate());
? ? ? ?CaffeineCache caffeineCache = new CaffeineCache(multilevelCacheProperties.getCaffeineName(), Caffeine.newBuilder()
? ? ? ? ? ? ? ?// 設(shè)置初始緩存大小
? ? ? ? ? ? ? .initialCapacity(initCapacity)
? ? ? ? ? ? ? ?// 設(shè)置最大緩存
? ? ? ? ? ? ? .maximumSize(maxCapacity)
? ? ? ? ? ? ? ?// 設(shè)置緩存線(xiàn)程池
? ? ? ? ? ? ? .executor(cacheExecutor)
? ? ? ? ? ? ? ?// 設(shè)置定時(shí)任務(wù)執(zhí)行過(guò)期清除操作
// ? ? ? ? ? ? ? .scheduler(Scheduler.systemScheduler())
? ? ? ? ? ? ? ?// 監(jiān)聽(tīng)器(超出最大緩存)
? ? ? ? ? ? ? .removalListener(new CaffeineCacheRemovalListener())
? ? ? ? ? ? ? ?// 設(shè)置緩存讀時(shí)間的過(guò)期時(shí)間
? ? ? ? ? ? ? .expireAfterAccess(Duration.of(multilevelCacheProperties.getCaffeineExpireTime(), ChronoUnit.SECONDS))
? ? ? ? ? ? ? ?// 開(kāi)啟metrics監(jiān)控
? ? ? ? ? ? ? .recordStats()
? ? ? ? ? ? ? .build());
? ? ? ?return caffeineCache;
? }
?
? ?@Bean
? ?@ConditionalOnBean({CaffeineCache.class, RedisCache.class})
? ?public MultilevelCache multilevelCache(RedisCache redisCache, CaffeineCache caffeineCache) {
? ? ? ?MultilevelCache multilevelCache = new MultilevelCache(true, redisCache, caffeineCache);
? ? ? ?return multilevelCache;
? }
?
? ?@Bean
? ?public RedisCacheMessageListener redisCacheMessageListener(@Autowired CaffeineCache caffeineCache) {
? ? ? ?RedisCacheMessageListener redisCacheMessageListener = new RedisCacheMessageListener();
? ? ? ?redisCacheMessageListener.setCaffeineCache(caffeineCache);
? ? ? ?return redisCacheMessageListener;
? }
?
?
?
? ?@Bean
? ?public RedisMessageListenerContainer redisMessageListenerContainer(@Autowired RedisConnectionFactory redisConnectionFactory,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? @Autowired RedisCacheMessageListener redisCacheMessageListener) {
? ? ? ?RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
? ? ? ?redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
? ? ? ?redisMessageListenerContainer.addMessageListener(redisCacheMessageListener, new ChannelTopic(multilevelCacheProperties.getTopic()));
? ? ? ?return redisMessageListenerContainer;
? }
?
}
?3.使用
使用非常簡(jiǎn)單,只需要通過(guò) multilevelCache 操作即可:
@RestController
@RequestMapping("/api/data")
@Api(tags = "api數(shù)據(jù)")
@Slf4j
public class ApiDataController {
?
? ?@Resource
? ?private MultilevelCache multilevelCache;
?
? ?@GetMapping("/put/cache")
? ?public void put() {
? ? ? ?DataSource ds = new DataSource();
? ? ? ?ds.setName("多級(jí)緩存");
? ? ? ?ds.setType(1);
? ? ? ?ds.setCreateTime(new Date());
? ? ? ?ds.setHost("127.0.0.1");
? ? ? ?multilevelCache.put("test-key", ds);
? }
?
? ?@GetMapping("/get/cache")
? ?public DataSource get() {
? ? ? ?DataSource dataSource = multilevelCache.get("test-key", DataSource.class);
? ? ? ?return dataSource;
? }
?
}4.總結(jié)
以上全部就是關(guān)于多級(jí)緩存的實(shí)現(xiàn)方案總結(jié),多級(jí)緩存就是為了解決項(xiàng)目服務(wù)中單一緩存使用不足的缺點(diǎn)。應(yīng)用場(chǎng)景有:接口權(quán)限校驗(yàn),每次請(qǐng)求接口都需要根據(jù)當(dāng)前登錄人有哪些角色,角色有哪些權(quán)限,如果每次都去查數(shù)據(jù)庫(kù)性能開(kāi)銷(xiāo)比較嚴(yán)重,再加上權(quán)限一般不怎么會(huì)頻繁變更,所以使用多級(jí)緩存是最合適不過(guò)了;還有就是很多管理系統(tǒng)列表界面都有組織架構(gòu)信息(所屬部門(mén)、小組等),這些信息同樣可以使用多級(jí)緩存來(lái)完美提升性能。
到此這篇關(guān)于SpringBoot多級(jí)緩存實(shí)現(xiàn)方案總結(jié)的文章就介紹到這了,更多相關(guān)SpringBoot實(shí)現(xiàn)多級(jí)緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring security登錄成功后通過(guò)Principal獲取名返回空問(wèn)題
這篇文章主要介紹了spring security登錄成功后通過(guò)Principal獲取名返回空問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
JDK8時(shí)間相關(guān)類(lèi)超詳細(xì)總結(jié)(含多個(gè)實(shí)例)
jdk1.8的一些新特性簡(jiǎn)化了代碼的寫(xiě)法,減少了部分開(kāi)發(fā)量,下面這篇文章主要給大家介紹了關(guān)于JDK8時(shí)間相關(guān)類(lèi)超詳細(xì)總結(jié),文中包含了多個(gè)實(shí)例代碼,需要的朋友可以參考下2023-01-01
MyBatis?實(shí)現(xiàn)動(dòng)態(tài)排序的多表查詢(xún)
本文將展示如何在 Java 項(xiàng)目中結(jié)合 MyBatis 實(shí)現(xiàn)動(dòng)態(tài)排序,尤其是在涉及多表查詢(xún)的情況下,具有一定的參考價(jià)值,感興趣的可以了解一下2024-05-05
在springboot中對(duì)kafka進(jìn)行讀寫(xiě)的示例代碼
本篇文章主要介紹了在springboot中對(duì)kafka進(jìn)行讀寫(xiě)的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
SpringBoot整合WebService的實(shí)戰(zhàn)案例
WebService是一個(gè)SOA(面向服務(wù)的編程)的架構(gòu),它是不依賴(lài)于語(yǔ)言,平臺(tái)等,可以實(shí)現(xiàn)不同的語(yǔ)言間的相互調(diào)用,這篇文章主要給大家介紹了關(guān)于SpringBoot整合WebService的相關(guān)資料,需要的朋友可以參考下2024-07-07
SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴(lài)后的問(wèn)題解決
由于SpringBoot從底層逐漸引導(dǎo)開(kāi)發(fā)者書(shū)寫(xiě)規(guī)范的代碼,同時(shí)也是個(gè)憂(yōu)傷的消息,循環(huán)依賴(lài)的應(yīng)用場(chǎng)景實(shí)在是太廣泛了,所以SpringBoot 2.6.x不推薦使用循環(huán)依賴(lài),本文給大家說(shuō)下SpringBoot2.6.x默認(rèn)禁用循環(huán)依賴(lài)后的應(yīng)對(duì)策略,感興趣的朋友一起看看吧2022-02-02
java實(shí)現(xiàn)簡(jiǎn)單計(jì)算器
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單計(jì)算器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12

