六脈神劍-我在公司造了六個(gè)輪子
前言
相信很多開發(fā)都會有自己造輪子的想法,畢竟只有提效了才能創(chuàng)造更多的摸魚空間。我呢,不巧就是高效的選手,也有幸自己設(shè)計(jì)并開發(fā)了好多輪子,并成功推廣給整個(gè)團(tuán)隊(duì)使用。如果有看過我前面文章的讀者朋友,肯定了解博主的工作情況,本職是一線業(yè)務(wù)搬磚黨,所以輪子都是我閑暇和周末時(shí)間自己慢慢積累起來。后來實(shí)在是太好用了,我就開始推廣,人人提效,人人如龍。但這東西吧,啥都挺好,就有一點(diǎn)不好,自從大家開發(fā)效率都提升了之后,領(lǐng)導(dǎo)給的開發(fā)工時(shí)更短了,淦。不說這些難過的事了,其實(shí)就我個(gè)人而言,組件也是我學(xué)習(xí)成長過程中的見證者,從一開始的磕磕絆絆到現(xiàn)在的信手拈來,回看當(dāng)年的提交記錄時(shí),依舊覺得很有意思。
本文不僅有所有組件的包結(jié)構(gòu)簡析,還有對核心功能的精講,更有我特別整理的版本更新記錄,而且我還特別把提交時(shí)間給撈出來了。更新記錄撈出來呢,主要也是想讓讀者從變更的過程中,去了解我在造輪子過程中遇到的問題,一些掙扎和選型的過程。當(dāng)然有一些之前提到過的組件,我偷懶啦,放了之前文章的鏈接,不然這一萬多字的文章裝不下了。寫完全篇后發(fā)現(xiàn)沒有放我小倉庫的連接,撈一下gitee.com/cloudswzy/g…,給需要的讀者們,里面有下面組件的部分功能抽取。
Tool-Box(工具箱)
包結(jié)構(gòu)簡析
├─annotation-注解
│ IdempotencyCheck.java-冪等性校驗(yàn),帶參數(shù)
│ JasyptField.java-加密字段,標(biāo)記字段用
│ JasyptMethod.java-標(biāo)記方法加密還是解密
│ LimitMethod.java-限流器
├─aop
│ IdempotencyCheckHandler.java-冪等性校驗(yàn)切面
│ JasyptHandler.java-數(shù)據(jù)加密切面
│ LimitHandler.java-基于漏斗思想的限流器
├─api
│ GateWayApi.java--對外接口請求
├─common
│ CheckUrlConstant.java--各個(gè)環(huán)境的接口請求鏈接常量
│ JasyptConstant.java--加密解密標(biāo)識常量
├─config
│ SpringDataRedisConfig.java--SpringDataRedis配置類,包含jedis配置、spring-cache配置、redisTemplate配置--2.3.4版本廢棄
│ CaffeineConfig.java--本地緩存caffeine通用配置
│ MyRedissonConfig.java--Redisson配置--2.3.4版本廢棄
│ ThreadPoolConfig.java--線程池配置
│ ToolApplicationContextInitializer.java--啟動后檢查參數(shù)
│ ToolAutoConfiguration.java--統(tǒng)一注冊BEAN
│ RedisPlusConfig.java--統(tǒng)一Redis入口配置類--2.3.4版本整合
├─exception
│ ToolException.java-工具箱異常
├─pojo
│ ├─message--郵件及消息通知用
│ │ EmailAttachmentParams.java
│ │ EmailBodyDTO.java
│ │ NoticeWechatDTO.java
│ └─user--用戶信息提取
│ UserHrDTO.java
│ UserInfoDTO.java
├─properties--自定義spring配置參數(shù)提醒
│ ToolProperties.java
├─service
│ DateMybatisHandler.java--Mybatis擴(kuò)展,用于日期字段增加時(shí)分秒
│ HrTool.java--OA信息查詢
│ JasyptMybatisHandler.java--Mybatis擴(kuò)展,整合Jasypt用于字段脫敏
│ LuaTool.java--redis的lua腳本工具
│ MessageTool.java--消息通知類
│ SpringTool.java--spring工具類 方便在非spring管理環(huán)境中獲取bean
│ CachePlusTool.java--二級緩存工具類
└─util
MapUtil.java--Map自用工具類,用于切分Map支持多線程
核心功能點(diǎn)
緩存(Redis和Caffeine)
關(guān)聯(lián)類SpringDataRedisConfig,CaffeineConfig,MyRedissonConfig
xml復(fù)制代碼<dependency> ?? ?<groupId>org.redisson</groupId> ?? ?<artifactId>redisson</artifactId> ?? ?<version>3.21.0</version> </dependency> <dependency> ?? ?<groupId>org.springframework.boot</groupId> ?? ?<artifactId>spring-boot-starter-data-redis</artifactId> ?? ?<version>2.1.18.RELEASE</version> ?? ?<exclusions> ?? ? ? ?<exclusion> ?? ? ? ? ? ?<groupId>ch.qos.logback</groupId> ?? ? ? ? ? ?<artifactId>logback-classic</artifactId> ?? ? ? ?</exclusion> ?? ? ? ?<exclusion> ?? ? ? ? ? ?<groupId>org.slf4j</groupId> ?? ? ? ? ? ?<artifactId>jul-to-slf4j</artifactId> ?? ? ? ?</exclusion> ?? ? ? ?<exclusion> ?? ? ? ? ? ?<groupId>org.springframework.boot</groupId> ?? ? ? ? ? ?<artifactId>spring-boot-starter-logging</artifactId> ?? ? ? ?</exclusion> ?? ? ? ?<exclusion> ?? ? ? ? ? ?<artifactId>lettuce-core</artifactId> ?? ? ? ? ? ?<groupId>io.lettuce</groupId> ?? ? ? ?</exclusion> ?? ?</exclusions> </dependency> <dependency> ?? ?<groupId>redis.clients</groupId> ?? ?<artifactId>jedis</artifactId> ?? ?<version>4.3.2</version> </dependency> <!-- ? ? ? ?不可升級,3.x以上最低jdk11--> <dependency> ?? ?<groupId>com.github.ben-manes.caffeine</groupId> ?? ?<artifactId>caffeine</artifactId> ?? ?<version>2.9.3</version> </dependency>
關(guān)于依賴,說明一下情況,公司的框架提供的Spring Boot版本是2.1.X版本,spring-boot-starter-data-redis在2.X版本是默認(rèn)使用lettuce,當(dāng)然也是因?yàn)閘ettuce擁有比jedis更優(yōu)異的性能。為什么這里排除了呢?原因是低版本下,lettuce存在斷連問題,阿里云-通過客戶端程序連接Redis,上面這篇文章關(guān)于客戶端的推薦里面,理由寫得很清楚了,就不細(xì)說了。但是我個(gè)人推薦引入Redisson,這是我目前用過最好用的Redis客戶端。
ini復(fù)制代碼import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.xx.tool.exception.ToolException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.util.StringUtils; import redis.clients.jedis.JedisPoolConfig; ?import java.time.Duration; import java.util.Arrays; ?/** ?* @Classname SpringDataRedisConfig ?* @Date 2021/3/25 17:53 ?* @Author WangZY ?* @Description SpringDataRedis配置類,包含jedis配置、spring-cache配置、redisTemplate配置 ?*/ @Configuration public class SpringDataRedisConfig { ?? ?@Autowired ?? ?private ConfigurableEnvironment config; ? ? ?/** ?? ? * 定義Jedis客戶端,集群和單點(diǎn)同時(shí)存在時(shí)優(yōu)先集群配置 ?? ? */ ?? ?@Bean ?? ?public JedisConnectionFactory redisConnectionFactory() { ?? ? ? ?String redisHost = config.getProperty("spring.redis.host"); ?? ? ? ?String redisPort = config.getProperty("spring.redis.port"); ?? ? ? ?String cluster = config.getProperty("spring.redis.cluster.nodes"); ?? ? ? ?String redisPassword = config.getProperty("spring.redis.password"); ?? ? ? ?JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); ?? ? ? ?// 默認(rèn)阻塞等待時(shí)間為無限長,源碼DEFAULT_MAX_WAIT_MILLIS = -1L ?? ? ? ?// 最大連接數(shù), 根據(jù)業(yè)務(wù)需要設(shè)置,不能超過實(shí)例規(guī)格規(guī)定的最大連接數(shù)。 ?? ? ? ?jedisPoolConfig.setMaxTotal(100); ?? ? ? ?// 最大空閑連接數(shù), 根據(jù)業(yè)務(wù)需要設(shè)置,不能超過實(shí)例規(guī)格規(guī)定的最大連接數(shù)。 ?? ? ? ?jedisPoolConfig.setMaxIdle(60); ?? ? ? ?// 關(guān)閉 testOn[Borrow|Return],防止產(chǎn)生額外的PING。 ?? ? ? ?jedisPoolConfig.setTestOnBorrow(false); ?? ? ? ?jedisPoolConfig.setTestOnReturn(false); ?? ? ? ?JedisClientConfiguration jedisClientConfiguration = JedisClientConfiguration.builder().usePooling() ?? ? ? ? ? ? ? ?.poolConfig(jedisPoolConfig).build(); ?? ? ? ?if (StringUtils.hasText(cluster)) { ?? ? ? ? ? ?// 集群模式 ?? ? ? ? ? ?String[] split = cluster.split(","); ?? ? ? ? ? ?RedisClusterConfiguration clusterServers = new RedisClusterConfiguration(Arrays.asList(split)); ?? ? ? ? ? ?if (StringUtils.hasText(redisPassword)) { ?? ? ? ? ? ? ? ?clusterServers.setPassword(redisPassword); ?? ? ? ? ? ?} ?? ? ? ? ? ?return new JedisConnectionFactory(clusterServers, jedisClientConfiguration); ?? ? ? ?} else if (StringUtils.hasText(redisHost) && StringUtils.hasText(redisPort)) { ?? ? ? ? ? ?// 單機(jī)模式 ?? ? ? ? ? ?RedisStandaloneConfiguration singleServer = new RedisStandaloneConfiguration(redisHost, Integer.parseInt(redisPort)); ?? ? ? ? ? ?if (StringUtils.hasText(redisPassword)) { ?? ? ? ? ? ? ? ?singleServer.setPassword(redisPassword); ?? ? ? ? ? ?} ?? ? ? ? ? ?return new JedisConnectionFactory(singleServer, jedisClientConfiguration); ?? ? ? ?} else { ?? ? ? ? ? ?throw new ToolException("spring.redis.host及port或spring.redis.cluster" + ?? ? ? ? ? ? ? ? ? ?".nodes必填,否則不可使用RedisTool以及Redisson"); ?? ? ? ?} ?? ?} ? ? ?/** ?? ? * 配置Spring-Cache內(nèi)部使用Redis,配置序列化和過期時(shí)間 ?? ? */ ?? ?@Bean ?? ?public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { ?? ? ? ?RedisSerializer<String> redisSerializer = new StringRedisSerializer(); ?? ? ? ?Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer ?? ? ? ? ? ? ? ?= new Jackson2JsonRedisSerializer<>(Object.class); ?? ? ? ?ObjectMapper om = new ObjectMapper(); ?? ? ? ?// 防止在序列化的過程中丟失對象的屬性 ?? ? ? ?om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); ?? ? ? ?// 開啟實(shí)體類和json的類型轉(zhuǎn)換,該處兼容老版本依賴,不得修改 ?? ? ? ?om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); ?? ? ? ?jackson2JsonRedisSerializer.setObjectMapper(om); ?? ? ? ?// 配置序列化(解決亂碼的問題) ?? ? ? ?RedisCacheConfiguration config = RedisCacheConfiguration. ?? ? ? ? ? ? ? ?defaultCacheConfig() ?? ? ? ? ? ? ? ?.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) ?? ? ? ? ? ? ? ?.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) ?? ? ? ? ? ? ? ?.disableCachingNullValues()// 不緩存空值 ?? ? ? ? ? ? ? ?.entryTtl(Duration.ofMinutes(30));//30分鐘不過期 ?? ? ? ?return RedisCacheManager ?? ? ? ? ? ? ? ?.builder(connectionFactory) ?? ? ? ? ? ? ? ?.cacheDefaults(config) ?? ? ? ? ? ? ? ?.build(); ?? ?} ? ? ?/** ?? ? * @Author WangZY ?? ? * @Date 2021/3/25 17:55 ?? ? * @Description 如果配置了KeyGenerator ,在進(jìn)行緩存的時(shí)候如果不指定key的話,最后會把生成的key緩存起來, ?? ? * 如果同時(shí)配置了KeyGenerator和key則優(yōu)先使用key。 ?? ? **/ ?? ?@Bean ?? ?public KeyGenerator keyGenerator() { ?? ? ? ?return (target, method, params) -> { ?? ? ? ? ? ?StringBuilder key = new StringBuilder(); ?? ? ? ? ? ?key.append(target.getClass().getSimpleName()).append("#").append(method.getName()).append("("); ?? ? ? ? ? ?for (Object args : params) { ?? ? ? ? ? ? ? ?key.append(args).append(","); ?? ? ? ? ? ?} ?? ? ? ? ? ?key.deleteCharAt(key.length() - 1); ?? ? ? ? ? ?key.append(")"); ?? ? ? ? ? ?return key.toString(); ?? ? ? ?}; ?? ?} ? ? ?/** ?? ? * @Author WangZY ?? ? * @Date 2021/7/2 11:50 ?? ? * @Description springboot 2.2以下版本用,配置redis序列化 ?? ? **/ ?? ?@Bean ?? ?public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { ?? ? ? ?RedisTemplate<String, Object> template = new RedisTemplate<>(); ?? ? ? ?template.setConnectionFactory(factory); ?? ? ? ?Jackson2JsonRedisSerializer json = new Jackson2JsonRedisSerializer(Object.class); ?? ? ? ?ObjectMapper mapper = new ObjectMapper(); ?? ? ? ?mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); ?? ? ? ?mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); ?? ? ? ?json.setObjectMapper(mapper); ?? ? ? ?//注意編碼類型 ?? ? ? ?template.setKeySerializer(new StringRedisSerializer()); ?? ? ? ?template.setValueSerializer(json); ?? ? ? ?template.setHashKeySerializer(new StringRedisSerializer()); ?? ? ? ?template.setHashValueSerializer(json); ?? ? ? ?template.afterPropertiesSet(); ?? ? ? ?return template; ?? ?} }
SpringDataRedisConfig的配置文件里面,對Jedis做了一個(gè)簡單的配置,設(shè)置了最大連接數(shù),阻塞等待時(shí)間默認(rèn)無限長就不用配置了,除此之外對集群和單點(diǎn)的配置做了下封裝。Spring-Cache也屬于常用,由于其默認(rèn)實(shí)現(xiàn)是依賴于本地緩存Caffeine,所以還是替換一下,并且重寫了keyGenerator,讓默認(rèn)生成的key具有可讀性。Spring-Cache和RedisTemplate的序列化配置相同,key采用String是為了在圖形化工具查詢時(shí)方便找到對應(yīng)的key,value采用Jackson序列化是為了壓縮數(shù)據(jù)同時(shí)也是官方推薦。
ini復(fù)制代碼import com.xx.tool.exception.ToolException; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.ClusterServersConfig; import org.redisson.config.Config; import org.redisson.config.SingleServerConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; ?import java.util.ArrayList; import java.util.List; ?/** ?* @Classname MyRedissonConfig ?* @Date 2021/6/4 14:04 ?* @Author WangZY ?* @Description Redisson配置 ?*/ @Configuration public class MyRedissonConfig { ?? ?@Autowired ?? ?private ConfigurableEnvironment config; ? ? ?/** ?? ? * 對 Redisson 的使用都是通過 RedissonClient 對象 ?? ? */ ?? ?@Bean(destroyMethod = "shutdown") // 服務(wù)停止后調(diào)用 shutdown 方法。 ?? ?public RedissonClient redisson() { ?? ? ? ?String redisHost = config.getProperty("spring.redis.host"); ?? ? ? ?String redisPort = config.getProperty("spring.redis.port"); ?? ? ? ?String cluster = config.getProperty("spring.redis.cluster.nodes"); ?? ? ? ?String redisPassword = config.getProperty("spring.redis.password"); ?? ? ? ?Config config = new Config(); ?? ? ? ?//使用String序列化時(shí)會出現(xiàn)RBucket<Integer>轉(zhuǎn)換異常 ?? ? ? ?//config.setCodec(new StringCodec()); ?? ? ? ?if (ObjectUtils.isEmpty(redisHost) && ObjectUtils.isEmpty(cluster)) { ?? ? ? ? ? ?throw new ToolException("spring.redis.host及port或spring.redis.cluster" + ?? ? ? ? ? ? ? ? ? ?".nodes必填,否則不可使用RedisTool以及Redisson"); ?? ? ? ?} else { ?? ? ? ? ? ?if (StringUtils.hasText(cluster)) { ?? ? ? ? ? ? ? ?// 集群模式 ?? ? ? ? ? ? ? ?String[] split = cluster.split(","); ?? ? ? ? ? ? ? ?List<String> servers = new ArrayList<>(); ?? ? ? ? ? ? ? ?for (String s : split) { ?? ? ? ? ? ? ? ? ? ?servers.add("redis://" + s); ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?ClusterServersConfig clusterServers = config.useClusterServers(); ?? ? ? ? ? ? ? ?clusterServers.addNodeAddress(servers.toArray(new String[split.length])); ?? ? ? ? ? ? ? ?if (StringUtils.hasText(redisPassword)) { ?? ? ? ? ? ? ? ? ? ?clusterServers.setPassword(redisPassword); ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?//修改命令超時(shí)時(shí)間為40s,默認(rèn)3s ?? ? ? ? ? ? ? ?clusterServers.setTimeout(40000); ?? ? ? ? ? ? ? ?//修改連接超時(shí)時(shí)間為50s,默認(rèn)10s ?? ? ? ? ? ? ? ?clusterServers.setConnectTimeout(50000); ?? ? ? ? ? ?} else { ?? ? ? ? ? ? ? ?// 單機(jī)模式 ?? ? ? ? ? ? ? ?SingleServerConfig singleServer = config.useSingleServer(); ?? ? ? ? ? ? ? ?singleServer.setAddress("redis://" + redisHost + ":" + redisPort); ?? ? ? ? ? ? ? ?if (StringUtils.hasText(redisPassword)) { ?? ? ? ? ? ? ? ? ? ?singleServer.setPassword(redisPassword); ?? ? ? ? ? ? ? ?} ?? ? ? ? ? ? ? ?singleServer.setTimeout(40000); ?? ? ? ? ? ? ? ?singleServer.setConnectTimeout(50000); ?? ? ? ? ? ?} ?? ? ? ?} ?? ? ? ?return Redisson.create(config); ?? ?} }
Redisson沒啥好說的,太香了,redisson官方中文文檔,中文文檔更新慢而且有錯(cuò)誤,建議看英文的。這里配置很簡單,主要是針對集群和單點(diǎn)還有超時(shí)時(shí)間做了封裝,重點(diǎn)是學(xué)會怎么玩Redisson,下面給出分布式鎖和緩存場景的代碼案例。低版本下的SpringDataRedis我是真的不推薦使用,之前我也封裝過RedisTemplate,但是后來發(fā)現(xiàn)Redisson性能更強(qiáng),功能更豐富,所以直接轉(zhuǎn)用Redisson,組件中也沒有提供RedisTemplate的封裝。
csharp復(fù)制代碼@Autowired private RedissonClient redissonClient; ?//分布式鎖 public void xxx(){ ?? ?RLock lock = redissonClient.getLock("鎖名"); ?? ?boolean locked = lock.isLocked(); ?? ? ? ?if (locked) { ?? ? ? ?//被鎖了 ?? ? ? ?}else{ ?? ? ? ? ? ? try { ?? ? ? ? ? ? ? ? lock.lock(); ?? ? ? ? ? ? ? ? //鎖后的業(yè)務(wù)邏輯 ?? ? ? ? ? ?} finally { ?? ? ? ? ? ? ? ? lock.unlock(); ?? ? ? ? ? ?} ?? ? ? ?} } //緩存應(yīng)用場景 public BigDecimal getIntervalQty(int itemId, Date startDate, Date endDate) { ?? ?String cacheKey = "dashboard:intervalQty:" + itemId + "-" + startDate + "-" + endDate; ?? ?RBucket<BigDecimal> bucket = redissonClient.getBucket(cacheKey); ?? ?BigDecimal cacheValue = null; ?? ?try { ?? ? ? ? ? ?//更新避免Redis報(bào)錯(cuò)版本 ?? ? ? ? ? ?cacheValue = bucket.get(); ?? ? ? ?} catch (Exception e) { ?? ? ? ? ? ?log.error("redis連接異常", e); ?? ? ? ?} ?? ?if (cacheValue != null) { ?? ? ? ?return cacheValue; ?? ?} else { ?? ? ? ?BigDecimal intervalQty = erpInfoMapper.getIntervalQty(itemId, startDate, endDate); ?? ? ? ?BigDecimal res = Optional.ofNullable(intervalQty).orElse(BigDecimal.valueOf(0)).setScale(2, ?? ? ? ? ? ? ? ?RoundingMode.HALF_UP); ?? ? ? ?bucket.set(res, 16, TimeUnit.HOURS); ?? ? ? ?return res; ?? ?} }
我是幾個(gè)月前發(fā)現(xiàn)設(shè)置String序列化方式時(shí),使用RBucket<>進(jìn)行泛型轉(zhuǎn)換會報(bào)類型轉(zhuǎn)換錯(cuò)誤的異常。官方在3.18.0版本才修復(fù)了這個(gè)問題,不過我推薦沒有圖形客戶端可視化需求的使用默認(rèn)編碼即可,有更高的壓縮率,并且目前使用沒有出現(xiàn)過轉(zhuǎn)換異常。
當(dāng)下Redis可視化工具最推薦官方的RedisInsight-v2,純免費(fèi)、好用還持續(xù)更新,除此之外推薦使用Another Redis Desktop Manager。
本地緩存之王Caffeine,哈哈,不知道從哪看的了,反正就是牛。我參考官網(wǎng)WIKI的例子做了一個(gè)簡單的封裝吧,提供了一個(gè)能應(yīng)付常見場景的實(shí)例可以直接使用,我個(gè)人更推薦根據(jù)實(shí)際場景自己新建實(shí)例。默認(rèn)提供一個(gè)最多元素為10000,初始元素為1000,過期時(shí)間設(shè)置為16小時(shí)的緩存實(shí)例,使用方法如下。更多操作看官方文檔,Population zh CN · ben-manes/caffeine Wiki。
typescript復(fù)制代碼@Autowired @Qualifier("commonCaffeine") private Cache<String, Object> caffeine; ?Object countryObj = caffeine.getIfPresent("country"); if (Objects.isNull(countryObj)) { ?? ?//緩存沒有,從數(shù)據(jù)庫獲取并填入緩存 ?? ?caffeine.put("country", country); ?? ?return country; } else { //緩存有,直接強(qiáng)制轉(zhuǎn)換后返回 ?? ?return (Map<String, String>) countryObj; } ?import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; ?import java.time.Duration; ?/** ?* @author WangZY ?* @classname CaffeineConfig ?* @date 2022/5/31 16:37 ?* @description 本地緩存caffeine通用配置 ?*/ @Configuration public class CaffeineConfig { ?? ?@Bean ?? ?public Cache<String, Object> commonCaffeine() { ?? ? ? ?return Caffeine.newBuilder() ?? ? ? ? ? ? ? ?//初始大小 ?? ? ? ? ? ? ? ?.initialCapacity(1000) ?? ? ? ? ? ? ? ?//PS:expireAfterWrite和expireAfterAccess同時(shí)存在時(shí),以expireAfterWrite為準(zhǔn)。 ?? ? ? ? ? ? ? ?//最后一次寫操作后經(jīng)過指定時(shí)間過期 // ? ? ? ? ? ? ? ?.expireAfterWrite(Duration.ofMinutes(30)) ?? ? ? ? ? ? ? ?//最后一次讀或?qū)懖僮骱蠼?jīng)過指定時(shí)間過期 ?? ? ? ? ? ? ? ?.expireAfterAccess(Duration.ofHours(16)) ?? ? ? ? ? ? ? ?// 最大數(shù)量,默認(rèn)基于緩存內(nèi)的元素個(gè)數(shù)進(jìn)行驅(qū)逐 ?? ? ? ? ? ? ? ?.maximumSize(10000) ?? ? ? ? ? ? ? ?//打開數(shù)據(jù)收集功能 ?hitRate(): 查詢緩存的命中率 evictionCount(): 被驅(qū)逐的緩存數(shù)量 averageLoadPenalty(): 新值被載入的平均耗時(shí) // ? ? ? ? ? ? ? ?.recordStats() ?? ? ? ? ? ? ? ?.build(); //// 查找一個(gè)緩存元素, 沒有查找到的時(shí)候返回null // ? ? ? ?Object obj = cache.getIfPresent(key); //// 查找緩存,如果緩存不存在則生成緩存元素, ?如果無法生成則返回null // ? ? ? ?obj = cache.get(key, k -> createExpensiveGraph(key)); //// 添加或者更新一個(gè)緩存元素 // ? ? ? ?cache.put(key, graph); //// 移除一個(gè)緩存元素 // ? ? ? ?cache.invalidate(key); //// 批量失效key // ? ? ? ?cache.invalidateAll(keys) //// 失效所有的key // ? ? ? ?cache.invalidateAll() ?? ?} }
Redis客戶端整合
Redis斷連從框架層面該如何搶救?,變更的原因見這篇文章,變更后版本號為2.3.4,文章歷史記錄保留。
Redis工具
基于漏斗思想的限流器
關(guān)聯(lián)類LimitMethod,LimitHandler,LuaTool
java復(fù)制代碼import com.xx.framework.base.config.BaseEnvironmentConfigration; import com.xx.tool.annotation.LimitMethod; import com.xx.tool.exception.ToolException; import com.xx.tool.service.LuaTool; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; ?/** ?* @Author WangZY ?* @Date 2022/2/21 17:21 ?* @Description 基于漏斗思想的限流器 ?**/ @Aspect @Component @Slf4j public class LimitHandler { ? ? ?@Autowired ?? ?private LuaTool luaTool; ?? ?@Autowired ?? ?private BaseEnvironmentConfigration baseEnv; ? ? ?@Pointcut("@annotation(com.ruijie.tool.annotation.LimitMethod)") ?? ?public void pointCut() { ?? ?} ? ? ?@Around("pointCut()") ?? ?public Object around(ProceedingJoinPoint joinPoint) throws Throwable { ?? ? ? ?MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); ?? ? ? ?LimitMethod limitMethod = methodSignature.getMethod().getAnnotation(LimitMethod.class); ?? ? ? ?int limit = limitMethod.limit(); ?? ? ? ?String application = baseEnv.getProperty("spring.application.name"); ?? ? ? ?String methodName = methodSignature.getName(); ?? ? ? ?//當(dāng)沒有自定義key時(shí),給一個(gè)有可讀性的默認(rèn)值 ?? ? ? ?String key = ""; ?? ? ? ?if (ObjectUtils.isEmpty(application)) { ?? ? ? ? ? ?throw new ToolException("當(dāng)前項(xiàng)目必須擁有spring.application.name才能使用限流器"); ?? ? ? ?} else { ?? ? ? ? ? ?key = application + ":limit:" + methodName; ?? ? ? ?} ?? ? ? ?long judgeLimit = luaTool.judgeLimit(key, limit); ?? ? ? ?if (judgeLimit == -1) { ?? ? ? ? ? ?throw new ToolException("系統(tǒng)同時(shí)允許執(zhí)行最多" + limit + "次當(dāng)前方法"); ?? ? ? ?} else { ?? ? ? ? ? ?log.info(methodSignature.getDeclaringTypeName() + "." + methodName + "在系統(tǒng)中允許同時(shí)執(zhí)行" + limit + ?? ? ? ? ? ? ? ? ? ?"次當(dāng)前方法,當(dāng)前執(zhí)行中的有" + judgeLimit + "個(gè)"); ?? ? ? ? ? ?Object[] objects = joinPoint.getArgs(); ?? ? ? ? ? ?return joinPoint.proceed(objects); ?? ? ? ?} ?? ?} ? ? ?/** ?? ? * spring4/springboot1: ?? ? * 正常:@Around-@Before-method-@Around-@After-@AfterReturning ?? ? * 異常:@Around-@Before-@After-@AfterThrowing ?? ? * spring5/springboot2: ?? ? * 正常:@Around-@Before-method-@AfterReturning-@After-@Around ?? ? * 異常:@Around-@Before-@AfterThrowing-@After ?? ? */ ?? ?@After("pointCut()") ?? ?public void after(JoinPoint joinPoint) { ?? ? ? ?MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); ?? ? ? ?LimitMethod limitMethod = methodSignature.getMethod().getAnnotation(LimitMethod.class); ?? ? ? ?int limit = limitMethod.limit(); ?? ? ? ?String application = baseEnv.getProperty("spring.application.name"); ?? ? ? ?String methodName = methodSignature.getName(); ?? ? ? ?if (StringUtils.hasText(application)) { ?? ? ? ? ? ?String key = application + ":limit:" + methodName; ?? ? ? ? ? ?long nowCount = luaTool.returnCount(key); ?? ? ? ? ? ?log.info(methodSignature.getDeclaringTypeName() + "." + methodName + "在系統(tǒng)中允許同時(shí)執(zhí)行最多" + limit + ?? ? ? ? ? ? ? ? ? ?"次當(dāng)前方法,執(zhí)行完畢后返還次數(shù),現(xiàn)仍執(zhí)行中的有" + nowCount + "個(gè)"); ?? ? ? ?} ?? ?} }
整個(gè)限流器以漏斗思想為基礎(chǔ)構(gòu)建,也就是說,我只限制最大值,不過和時(shí)間窗口算法有區(qū)別的一點(diǎn)是,多了歸還次數(shù)的動作,這里把他放在@After,確保無論如何都會執(zhí)行。為了保證易用性,會生成Redis的默認(rèn)key,我的選擇是用application(應(yīng)用名) + ":limit:" + methodName(方法名),達(dá)到了key不重復(fù)和易讀的目標(biāo)。
ini復(fù)制代碼/** ?? ? * 限流器-漏斗算法思想 ?? ? * ?? ? * @param key ? 被限流的key ?? ? * @param limit 限制次數(shù) ?? ? * @return 當(dāng)前時(shí)間范圍內(nèi)正在執(zhí)行的線程數(shù) ?? ? */ ?? ?public long judgeLimit(String key, int limit) { ?? ? ? ?RScript script = redissonClient.getScript(new LongCodec()); ?? ? ? ?return script.eval(RScript.Mode.READ_WRITE, ?? ? ? ? ? ? ? ?"local count = redis.call('get', KEYS[1]);" + ?? ? ? ? ? ? ? ? ? ? ? ?"if count then " + ?? ? ? ? ? ? ? ? ? ? ? ?"if count>=ARGV[1] then " + ?? ? ? ? ? ? ? ? ? ? ? ?"count=-1 " + ?? ? ? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ? ? ?"redis.call('incr',KEYS[1]);" + ?? ? ? ? ? ? ? ? ? ? ? ?"end; " + ?? ? ? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ? ? ?"count = 1;" + ?? ? ? ? ? ? ? ? ? ? ? ?"redis.call('set', KEYS[1],count);" + ?? ? ? ? ? ? ? ? ? ? ? ?"end;" + ?? ? ? ? ? ? ? ? ? ? ? ?"redis.call('expire',KEYS[1],ARGV[2]);" + ?? ? ? ? ? ? ? ? ? ? ? ?"return count;", ?? ? ? ? ? ? ? ?RScript.ReturnType.INTEGER, Collections.singletonList(key), limit, 600); ?? ?} ? ? ?/** ?? ? * 歸還次數(shù)-漏斗算法思想 ?? ? * ?? ? * @param key 被限流的key ?? ? * @return 正在執(zhí)行的線程數(shù) ?? ? */ ?? ?public long returnCount(String key) { ?? ? ? ?RScript script = redissonClient.getScript(new LongCodec()); ?? ? ? ?return script.eval(RScript.Mode.READ_WRITE, ?? ? ? ? ? ? ? ?"local count = tonumber(redis.call('get', KEYS[1]));" + ?? ? ? ? ? ? ? ? ? ? ? ?"if count then " + ?? ? ? ? ? ? ? ? ? ? ? ?"if count>0 then " + ?? ? ? ? ? ? ? ? ? ? ? ?"count=count-1;" + ?? ? ? ? ? ? ? ? ? ? ? ?"redis.call('set', KEYS[1],count);" + ?? ? ? ? ? ? ? ? ? ? ? ?"redis.call('expire',KEYS[1],ARGV[1]); " + ?? ? ? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ? ? ?"count = 0;" + ?? ? ? ? ? ? ? ? ? ? ? ?"end; " + ?? ? ? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ? ? ?"count = 0;" + ?? ? ? ? ? ? ? ? ? ? ? ?"end;" + ?? ? ? ? ? ? ? ? ? ? ? ?"return count;", ?? ? ? ? ? ? ? ?RScript.ReturnType.INTEGER, Collections.singletonList(key), 600); ?? ?}
核心就是Lua腳本,推薦使用的原因如下,感興趣的話可以自學(xué)一下,上面阿里云的文章里也有案例可以參考,包括Redisson的源碼中也有大量參考案例。
減少網(wǎng)絡(luò)開銷??梢詫⒍鄠€(gè)請求通過腳本的形式一次發(fā)送,減少網(wǎng)絡(luò)時(shí)延。使用lua腳本執(zhí)行以上操作時(shí),比redis普通操作快80%左右
原子操作。Redis會將整個(gè)腳本作為一個(gè)整體執(zhí)行,中間不會被其他請求插入。因此在腳本運(yùn)行過程中無需擔(dān)心會出現(xiàn)競態(tài)條件,無需使用事務(wù)。
復(fù)用??蛻舳税l(fā)送的腳本會永久存在redis中,這樣其他客戶端可以復(fù)用這一腳本,而不需要使用代碼完成相同的邏輯。
說一下我寫的腳本邏輯,首先獲取當(dāng)前key對應(yīng)的值count,如果count不為null的情況下,再判斷是否大于limit,如果大于說明超過漏斗最大值,將count設(shè)置為-1,標(biāo)記為超過限制。如果小于limit,則將count值自增1.如果count為null,說明第一次進(jìn)入,設(shè)置count為1。最后再刷新key的有效期并返回count值,用于切面邏輯判斷。歸還邏輯和進(jìn)入邏輯相同,反向思考即可。
總結(jié)一下,限流器基于Lua+AOP,切點(diǎn)是@LimitMethod,注解參數(shù)是同時(shí)運(yùn)行次數(shù),使用場景是前后端的接口。@Around運(yùn)行實(shí)際方法前進(jìn)行限流(使用次數(shù)自增),@After后返還使用次數(shù)。作用是限制同時(shí)運(yùn)行線程數(shù),只有限流沒有降級處理,超過的拋出異常中斷方法。
讀者提問:腳本最后一行失效時(shí)間重置的意圖是啥?
換個(gè)相反的角度來看,如果去掉了重置失效時(shí)間的代碼,是不是會存在一點(diǎn)問題?比如剛好進(jìn)入限流后,此時(shí)流量為N,方法還沒有運(yùn)行完畢,這個(gè)key失效了。那么按照代碼邏輯來看,生成一個(gè)新的key就從0開始,但是明明之前我還有N個(gè)流量沒有執(zhí)行完畢,也就是表面上看key的結(jié)果是最新的1,但實(shí)際上是1+N,這樣流量就不準(zhǔn)了。所以我這重置了下超時(shí)時(shí)間,確保方法在超時(shí)時(shí)間內(nèi)運(yùn)行完畢能順利歸還,保證流量數(shù)更新正確。
冪等性校驗(yàn)器
ini復(fù)制代碼import com.alibaba.fastjson.JSON; import com.x.framework.base.RequestContext; import com.xx.framework.base.config.BaseEnvironmentConfigration; import com.xx.tool.annotation.IdempotencyCheck; import com.xx.tool.exception.ToolException; import com.xx.tool.service.LuaTool; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.multipart.MultipartFile; ?import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; ?/** ?* @Author WangZY ?* @Date 2022/2/21 17:21 ?* @Description 冪等性校驗(yàn)切面 ?**/ @Aspect @Component @Slf4j public class IdempotencyCheckHandler { ? ? ?@Autowired ?? ?private LuaTool luaTool; ?? ?@Autowired ?? ?private BaseEnvironmentConfigration baseEnv; ? ? ?@Pointcut("@annotation(com.ruijie.tool.annotation.IdempotencyCheck)") ?? ?public void pointCut() { ?? ?} ? ? ?@Around("pointCut()") ?? ?public Object around(ProceedingJoinPoint joinPoint) throws Throwable { ?? ? ? ?MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); ?? ? ? ?Object[] objects = joinPoint.getArgs(); ?? ? ? ?IdempotencyCheck check = methodSignature.getMethod().getAnnotation(IdempotencyCheck.class); ?? ? ? ?int checkTime = check.checkTime(); ?? ? ? ?String checkKey = check.checkKey(); ?? ? ? ?String application = baseEnv.getProperty("spring.application.name"); ?? ? ? ?String methodName = methodSignature.getName(); ?? ? ? ?String key = ""; ?? ? ? ?if (ObjectUtils.isEmpty(application)) { ?? ? ? ? ? ?throw new ToolException("當(dāng)前項(xiàng)目必須擁有spring.application.name才能使用冪等性校驗(yàn)器"); ?? ? ? ?} else { ?? ? ? ? ? ?key = application + ":" + methodName + ":"; ?? ? ? ?} ?? ? ? ?if (ObjectUtils.isEmpty(checkKey)) { ?? ? ? ? ? ?String userId = RequestContext.getCurrentContext().getUserId(); ?? ? ? ? ? ?String digest = DigestUtils.md5DigestAsHex(JSON.toJSONBytes(getRequestParams(joinPoint))); ?? ? ? ? ? ?key = key + userId + ":" + digest; ?? ? ? ?} else { ?? ? ? ? ? ?key = key + checkKey; ?? ? ? ?} ?? ? ? ?long checkRes = luaTool.idempotencyCheck(key, checkTime); ?? ? ? ?if (checkRes == -1) { ?? ? ? ? ? ?log.info("冪等性校驗(yàn)已開啟,當(dāng)前Key為{}", key); ?? ? ? ?} else { ?? ? ? ? ? ?throw new ToolException("防重校驗(yàn)已開啟,當(dāng)前方法禁止在" + checkTime + "秒內(nèi)重復(fù)提交"); ?? ? ? ?} ?? ? ? ?return joinPoint.proceed(objects); ?? ?} ? ? ?/*** ?? ? * @Author WangZY ?? ? * @Date 2020/4/16 18:56 ?? ? * @Description 獲取入?yún)??? ? */ ?? ?private String getRequestParams(ProceedingJoinPoint proceedingJoinPoint) { ?? ? ? ?Map<String, Object> requestParams = new HashMap<>(16); ?? ? ? ?//參數(shù)名 ?? ? ? ?String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames(); ?? ? ? ?//參數(shù)值 ?? ? ? ?Object[] paramValues = proceedingJoinPoint.getArgs(); ?? ? ? ?for (int i = 0; i < paramNames.length; i++) { ?? ? ? ? ? ?Object value = paramValues[i]; ?? ? ? ? ? ?//如果是文件對象 ?? ? ? ? ? ?if (value instanceof MultipartFile) { ?? ? ? ? ? ? ? ?MultipartFile file = (MultipartFile) value; ?? ? ? ? ? ? ? ?//獲取文件名 ?? ? ? ? ? ? ? ?value = file.getOriginalFilename(); ?? ? ? ? ? ? ? ?requestParams.put(paramNames[i], value); ?? ? ? ? ? ?} else if (value instanceof HttpServletRequest) { ?? ? ? ? ? ? ? ?requestParams.put(paramNames[i], "參數(shù)類型為HttpServletRequest"); ?? ? ? ? ? ?} else if (value instanceof HttpServletResponse) { ?? ? ? ? ? ? ? ?requestParams.put(paramNames[i], "參數(shù)類型為HttpServletResponse"); ?? ? ? ? ? ?} else { ?? ? ? ? ? ? ? ?requestParams.put(paramNames[i], value); ?? ? ? ? ? ?} ?? ? ? ?} ?? ? ? ?return JSON.toJSONString(requestParams); ?? ?} } ?/** ?* @author WangZY ?* @date 2022/4/25 17:41 ?* @description 冪等性校驗(yàn) ?**/ public long idempotencyCheck(String key, int expireTime) { ?? ?RScript script = redissonClient.getScript(new LongCodec()); ?? ?return script.eval(RScript.Mode.READ_WRITE, ?? ? ? ? ? ?"local exist = redis.call('get', KEYS[1]);" + ?? ? ? ? ? ? ? ? ? ?"if not exist then " + ?? ? ? ? ? ? ? ? ? ?"redis.call('set', KEYS[1], ARGV[1]);" + ?? ? ? ? ? ? ? ? ? ?"redis.call('expire',KEYS[1],ARGV[1]);" + ?? ? ? ? ? ? ? ? ? ?"exist = -1;" + ?? ? ? ? ? ? ? ? ? ?"end;" + ?? ? ? ? ? ? ? ? ? ?"return exist;", ?? ? ? ? ? ?RScript.ReturnType.INTEGER, Collections.singletonList(key), expireTime); }
冪等性校驗(yàn)器基于Lua和AOP,切點(diǎn)是@IdempotencyCheck,注解參數(shù)是單次冪等性校驗(yàn)有效時(shí)間和冪等性校驗(yàn)Key,使用場景是前后端的接口。通知部分只有@Around,Key值默認(rèn)默認(rèn)為應(yīng)用名(spring.application.name):當(dāng)前方法名:當(dāng)前登錄人ID(沒有SSO就是null):入?yún)⒌膍d5值,如果checkKey不為空就會替換入?yún)⒑彤?dāng)前登錄人--->應(yīng)用名:當(dāng)前方法名:checkKey。作用是在checkTime時(shí)間內(nèi)相同checkKey只能運(yùn)行一次。
Lua腳本的寫法因?yàn)闆]有加減,所以比限流器簡單。這里還有個(gè)要點(diǎn)就是為了保證key值長度可控,將參數(shù)用MD5加密,對一些特殊的入?yún)⒁惨獑为?dú)做處理。
發(fā)號器
ini復(fù)制代碼/** ?* 單號按照keyPrefix+yyyyMMdd+4位流水號的格式生成 ?* ?* @param keyPrefix 流水號前綴標(biāo)識--用作redis key名 ?* @return 單號 ?*/ public String generateOrder(String keyPrefix) { ?? ?RScript script = redissonClient.getScript(new LongCodec()); ?? ?long between = ChronoUnit.SECONDS.between(LocalDateTime.now(), LocalDateTime.of(LocalDate.now(), ?? ? ? ? ? ?LocalTime.MAX)); ?? ?Long eval = script.eval(RScript.Mode.READ_WRITE, ?? ? ? ? ? ?"local sequence = redis.call('get', KEYS[1]);" + ?? ? ? ? ? ? ? ? ? ?"if sequence then " + ?? ? ? ? ? ? ? ? ? ?"if sequence>ARGV[1] then " + ?? ? ? ? ? ? ? ? ? ?"sequence = 0 " + ?? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ?"sequence = sequence+1;" + ?? ? ? ? ? ? ? ? ? ?"end;" + ?? ? ? ? ? ? ? ? ? ?"else " + ?? ? ? ? ? ? ? ? ? ?"sequence = 1;" + ?? ? ? ? ? ? ? ? ? ?"end;" + ?? ? ? ? ? ? ? ? ? ?"redis.call('set', KEYS[1], sequence);" + ?? ? ? ? ? ? ? ? ? ?"redis.call('expire',KEYS[1],ARGV[2]);" + ?? ? ? ? ? ? ? ? ? ?"return sequence;", ?? ? ? ? ? ?RScript.ReturnType.INTEGER, Collections.singletonList(keyPrefix), 9999, between); ?? ?DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); ?? ?String dateNow = LocalDate.now().format(formatter); ?? ?int len = String.valueOf(eval).length(); ?? ?StringBuilder res = new StringBuilder(); ?? ?for (int i = 0; i < 4 - len; i++) { ?? ? ? ?res.append("0"); ?? ?} ?? ?res.append(eval); ?? ?return keyPrefix + dateNow + res; }
發(fā)號器邏輯很簡單,單號按照keyPrefix+yyyyMMdd+4位流水號的格式生成。Redis獲取當(dāng)前keyPrefix對應(yīng)的key,如果沒有則返回1,如果存在,判斷是否大于9999,如果大于返回錯(cuò)誤,如果小于就將value+1,并且設(shè)置過期時(shí)間直到今天結(jié)束。
加密解密
關(guān)聯(lián)類JasyptField,JasyptMethod,JasyptHandler,JasyptConstant,JasyptMybatisHandler
提供注解JasyptField用于對象屬性以及方法參數(shù)。提供注解JasyptMethod用于注解在方法上。此加密方式由切面方式實(shí)現(xiàn),使用時(shí)請務(wù)必注意切面使用禁忌。
使用案例
less復(fù)制代碼public class UserVO { ?? ?private String userId; ?? ?private String userName; ?? ?@JasyptField ?? ?private String password; } ?@PostMapping("test111") @JasyptMethod(type = JasyptConstant.ENCRYPT) public void test111(@RequestBody UserVO loginUser) { ?? ?System.out.println(loginUser.toString()); ?? ?LoginUser user = new LoginUser(); ?? ?user.setUserId(loginUser.getUserId()); ?? ?user.setUserName(loginUser.getUserName()); ?? ?user.setPassword(loginUser.getPassword()); ?? ?loginUserService.save(user); } ?@GetMapping("test222") @JasyptMethod(type = JasyptConstant.DECRYPT) public UserVO test222(@RequestParam(value = "userId") String userId) { ?? ?LoginUser one = loginUserService.lambdaQuery().eq(LoginUser::getUserId, userId).one(); ?? ?UserVO user = new UserVO(); ?? ?user.setUserId(one.getUserId()); ?? ?user.setUserName(one.getUserName()); ?? ?user.setPassword(one.getPassword()); ?? ?return user; } ?@GetMapping("test333") @JasyptMethod(type = JasyptConstant.ENCRYPT) public void test111(@JasyptField @RequestParam(value = "userId") String userId) { ?? ?LoginUser user = new LoginUser(); ?? ?user.setUserName(userId); ?? ?loginUserService.save(user); } ?配置文件 # jasypt加密配置 jasypt.encryptor.password=wzy
效果如下
為什么選擇jasypt這個(gè)框架呢?是之前看到有人推薦,加上可玩性不錯(cuò),配置文件、代碼等場景都能用上,整合也方便就直接用了。這個(gè)切面換成別的加密解密也是一樣的玩法,用這個(gè)主要是還附贈配置文件加密的方法。除以上用法,還擴(kuò)展了Mybatis,這里對String類型做了脫敏處理,當(dāng)然用別的解密方式也可以的。
Mybatis擴(kuò)展使用
使用時(shí),如果是mybatis-plus,務(wù)必在表映射實(shí)體類上增加注解@TableName(autoResultMap = true),在對應(yīng)字段上加 typeHandler = JasyptMybatisHandler.class
java復(fù)制代碼import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import org.jasypt.encryption.StringEncryptor; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; ?import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; ?/** ?* @Author WangZY ?* @Date 2021/9/15 11:15 ?* @Description Mybatis擴(kuò)展,整合Jasypt用于字段脫敏 ?**/ @Component public class JasyptMybatisHandler implements TypeHandler<String> { ? ? ?/** ?? ? * mybatis-plus需在表實(shí)體類上加 @TableName(autoResultMap = true) ?? ? * 屬性字段上需加入 @TableField(value = "item_cost", typeHandler = JasyptMybatisHandler.class) ?? ? */ ?? ?private final StringEncryptor encryptor; ? ? ?public JasyptMybatisHandler(StringEncryptor encryptor) { ?? ? ? ?this.encryptor = encryptor; ?? ?} ? ? ?@Override ?? ?public void setParameter(PreparedStatement preparedStatement, int i, String s, JdbcType jdbcType) throws SQLException { ?? ? ? ?if (StringUtils.isEmpty(s)) { ?? ? ? ? ? ?preparedStatement.setString(i, ""); ?? ? ? ?} else { ?? ? ? ? ? ?preparedStatement.setString(i, encryptor.encrypt(s.trim())); ?? ? ? ?} ?? ?} ? ? ?@Override ?? ?public String getResult(ResultSet resultSet, String s) throws SQLException { ?? ? ? ?if (StringUtils.isEmpty(resultSet.getString(s))) { ?? ? ? ? ? ?return resultSet.getString(s); ?? ? ? ?} else { ?? ? ? ? ? ?return encryptor.decrypt(resultSet.getString(s).trim()); ?? ? ? ?} ?? ?} ? ? ?@Override ?? ?public String getResult(ResultSet resultSet, int i) throws SQLException { ?? ? ? ?if (StringUtils.isEmpty(resultSet.getString(i))) { ?? ? ? ? ? ?return resultSet.getString(i); ?? ? ? ?} else { ?? ? ? ? ? ?return encryptor.decrypt(resultSet.getString(i).trim()); ?? ? ? ?} ?? ?} ? ? ?@Override ?? ?public String getResult(CallableStatement callableStatement, int i) throws SQLException { ?? ? ? ?if (StringUtils.isEmpty(callableStatement.getString(i))) { ?? ? ? ? ? ?return callableStatement.getString(i); ?? ? ? ?} else { ?? ? ? ? ? ?return encryptor.decrypt(callableStatement.getString(i).trim()); ?? ? ? ?} ?? ?} }
線程池
scss復(fù)制代碼import com.xx.tool.properties.ToolProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; ?import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ThreadPoolExecutor; ?/** ?* @Author WangZY ?* @Date 2020/2/13 15:51 ?* @Description 線程池配置 ?*/ @EnableConfigurationProperties({ToolProperties.class}) @Configuration public class ThreadPoolConfig { ? ? ?@Autowired ?? ?private ToolProperties prop; ? ? ?/** ?? ? * 默認(rèn)CPU密集型--所有參數(shù)均需要在壓測下不斷調(diào)整,根據(jù)實(shí)際的任務(wù)消耗時(shí)間來設(shè)置參數(shù) ?? ? * CPU密集型指的是高并發(fā),相對短時(shí)間的計(jì)算型任務(wù),這種會占用CPU執(zhí)行計(jì)算處理 ?? ? * 因此核心線程數(shù)設(shè)置為CPU核數(shù)+1,減少線程的上下文切換,同時(shí)做個(gè)大的隊(duì)列,避免任務(wù)被飽和策略拒絕。 ?? ? */ ?? ?@Bean("cpuDenseExecutor") ?? ?public ThreadPoolTaskExecutor cpuDense() { ?? ? ? ?ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ?? ? ? ?//獲取邏輯可用CPU數(shù) ?? ? ? ?int logicCpus = Runtime.getRuntime().availableProcessors(); ?? ? ? ?if (prop.getPoolCpuNumber() != null) { ?? ? ? ? ? ?//如果是核心業(yè)務(wù),需要?;钭銐虻木€程數(shù)隨時(shí)支持運(yùn)行,提高響應(yīng)速度,因此設(shè)置核心線程數(shù)為壓測后的理論最優(yōu)值 ?? ? ? ? ? ?executor.setCorePoolSize(prop.getPoolCpuNumber() + 1); ?? ? ? ? ? ?//設(shè)置和核心線程數(shù)一致,用隊(duì)列控制任務(wù)總數(shù) ?? ? ? ? ? ?executor.setMaxPoolSize(prop.getPoolCpuNumber() + 1); ?? ? ? ? ? ?//Spring默認(rèn)使用LinkedBlockingQueue ?? ? ? ? ? ?executor.setQueueCapacity(prop.getPoolCpuNumber() * 30); ?? ? ? ?} else { ?? ? ? ? ? ?executor.setCorePoolSize(logicCpus + 1); ?? ? ? ? ? ?executor.setMaxPoolSize(logicCpus + 1); ?? ? ? ? ? ?executor.setQueueCapacity(logicCpus * 30); ?? ? ? ?} ?? ? ? ?//默認(rèn)60秒,維持不變 ?? ? ? ?executor.setKeepAliveSeconds(60); ?? ? ? ?//使用自定義前綴,方便問題排查 ?? ? ? ?executor.setThreadNamePrefix(prop.getPoolName()); ?? ? ? ?//默認(rèn)拒絕策略,拋異常 ?? ? ? ?executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); ?? ? ? ?executor.initialize(); ?? ? ? ?return executor; ?? ?} ? ? ?/** ?? ? * 默認(rèn)io密集型 ?? ? * IO密集型指的是有大量IO操作,比如遠(yuǎn)程調(diào)用、連接數(shù)據(jù)庫 ?? ? * 因?yàn)镮O操作不占用CPU,所以設(shè)置核心線程數(shù)為CPU核數(shù)的兩倍,保證CPU不閑下來,隊(duì)列相應(yīng)調(diào)小一些。 ?? ? */ ?? ?@Bean("ioDenseExecutor") ?? ?public ThreadPoolTaskExecutor ioDense() { ?? ? ? ?ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ?? ? ? ?int logicCpus = Runtime.getRuntime().availableProcessors(); ?? ? ? ?if (prop.getPoolCpuNumber() != null) { ?? ? ? ? ? ?executor.setCorePoolSize(prop.getPoolCpuNumber() * 2); ?? ? ? ? ? ?executor.setMaxPoolSize(prop.getPoolCpuNumber() * 2); ?? ? ? ? ? ?executor.setQueueCapacity(prop.getPoolCpuNumber() * 10); ?? ? ? ?} else { ?? ? ? ? ? ?executor.setCorePoolSize(logicCpus * 2); ?? ? ? ? ? ?executor.setMaxPoolSize(logicCpus * 2); ?? ? ? ? ? ?executor.setQueueCapacity(logicCpus * 10); ?? ? ? ?} ?? ? ? ?executor.setKeepAliveSeconds(60); ?? ? ? ?executor.setThreadNamePrefix(prop.getPoolName()); ?? ? ? ?executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); ?? ? ? ?executor.initialize(); ?? ? ? ?return executor; ?? ?} ? ? ?@Bean("cpuForkJoinPool") ?? ?public ForkJoinPool cpuForkJoinPool() { ?? ? ? ?int logicCpus = Runtime.getRuntime().availableProcessors(); ?? ? ? ?return new ForkJoinPool(logicCpus + 1); ?? ?} ? ? ?@Bean("ioForkJoinPool") ?? ?public ForkJoinPool ioForkJoinPool() { ?? ? ? ?int logicCpus = Runtime.getRuntime().availableProcessors(); ?? ? ? ?return new ForkJoinPool(logicCpus * 2); ?? ?} }
線程池對傳統(tǒng)的ThreadPoolTaskExecutor和新銳的ForkJoinPool提供了常見的CPU和IO密集型的通用解。核心線程數(shù)和最大線程數(shù)設(shè)置為一致,通過隊(duì)列控制任務(wù)總數(shù),這是基于我對目前項(xiàng)目使用情況的一個(gè)經(jīng)驗(yàn)值判斷。如果是非核心業(yè)務(wù),不需要?;钸@么多核心線程數(shù),可以設(shè)置的小一些,最大線程數(shù)設(shè)置成壓測最優(yōu)結(jié)果即可。
更新記錄
版本號發(fā)布時(shí)間更新記錄0.62021/6/21 14:13初始化組件,增加Hr信息查詢、消息通知、Redis、Spring工具0.72021/6/21 18:39增加Redisson配置類0.82021/6/22 14:15優(yōu)化包結(jié)構(gòu),遷移maven倉庫坐標(biāo)0.92021/6/22 15:09增加說明文檔1.02021/7/2 11:51增加Redis配置類,配置Spring Data Redis1.22021/7/15 11:25Hr信息查詢增加新方法1.2.52021/8/3 18:361.增加加密解密切面2.增加啟動校驗(yàn)參數(shù)類1.32021/8/4 10:31加密解密切面BUG FIXED1.4.02021/8/10 10:14Redisson配置類增加Redis-Cluster集群支持1.4.52021/9/14 16:03增加Excel模塊相關(guān)類1.5.02021/9/14 16:51增加@Valid快速失敗機(jī)制1.6.02021/9/15 15:041.加密解密切面支持更多入?yún)?,BUG FIXED2.增加脫敏用Mybatis擴(kuò)展1.6.82021/9/17 11:29增加主站用待辦模塊相關(guān)類1.6.92021/10/27 13:19脫敏用Mybatis擴(kuò)展BUG FIXED1.7.02021/10/28 20:43更新郵件發(fā)送人判斷,優(yōu)化消息通知工具1.7.12021/11/15 10:07待辦參數(shù)移除強(qiáng)制校驗(yàn)1.7.22021/11/23 14:08郵件發(fā)送增加附件支持1.7.52021/12/9 11:081.待辦及Excel模塊遷移至組件Business-Common2.增加spring-cache配置redis3.ToolException繼承AbstractRJBusinessException,能被全局異常監(jiān)聽2.0.02022/1/7 11:22完全去除業(yè)務(wù)部分,遷移至組件Business-Common2.0.22022/1/13 15:44增加統(tǒng)一注冊類ToolAutoConfiguration2.0.52022/3/14 15:11消息通知工具使用resttemplate默認(rèn)編碼格式不支持中文問題解決2.0.62022/3/24 23:49Redisson編碼更換String,方便圖形可視化2.0.72022/3/30 14:22Redisson及Mybatis依賴版本升級2.0.82022/4/12 11:57增加線程池配置2.0.92022/4/15 18:25增加漏桶算法限流器2.1.02022/4/18 14:29漏桶算法限流器優(yōu)化,切面順序調(diào)整2.1.12022/4/26 9:56新增冪等性校驗(yàn)工具2.1.22022/4/26 16:13冪等性校驗(yàn)支持文件、IO流等特殊參數(shù)2.1.32022/4/29 14:231.移除redisTool,推薦使用Redisson2.修改單號生成器BUG2.1.42022/5/18 11:291.修復(fù)了自2.1.0版本以來的限流器BUG2.優(yōu)化了緩存配置類的過時(shí)代碼2.1.62022/5/24 17:44配合架構(gòu)組升級新網(wǎng)關(guān)2.1.72022/6/8 14:01增加Caffeine配置2.1.82022/7/12 10:191.回歸fastjson1,避免fastjson2版本兼容性BUG2.forkjoinpool臨時(shí)參數(shù)2.1.92022/7/27 13:59優(yōu)化消息通知工具,增加發(fā)送人參數(shù)2.2.02022/8/25 9:241.增加ForkJoinPool類型的線程池默認(rèn)配置2.線程池參數(shù)增加配置化支持2.2.22022/9/19 17:08修改Redisson編碼為默認(rèn)編碼,String編碼不支持RBucket的泛型(Redisson3.18.0已修復(fù)該問題)2.2.32022/9/21 19:06調(diào)大Redisson命令及連接超時(shí)參數(shù)2.2.42022/9/27 11:52消息通知工具BUG FIXED,避免空指針2.2.52022/12/16 18:46增加工具類Map切分2.2.82022/12/18 13:19增加Mybatis擴(kuò)展,日期轉(zhuǎn)換處理器2.2.92023/2/10 22:30Redisson及Lombok依賴版本升級2.3.02023/5/6 10:26重寫Redis配置類,增加SpringDataRedisConfig2.3.12023/5/7 19:051.線程池參數(shù)調(diào)整2.優(yōu)化注釋2.3.32023/7/24 18:511.redis加載新增開關(guān)2.新增二級緩存工具類2.3.42023/5/7 19:05優(yōu)化Redis開關(guān)配置體驗(yàn)