2023/5/12追加更新,文章补漏、私货分享
前语
相信很多开发都会有自己造轮子的想法,究竟只要提效了才干创造更多的摸鱼空间。我呢,不巧便是高效的选手,也有幸自己规划并开发了很多轮子,并成功推行给整个团队运用。假如有看过我前面文章的读者朋友,肯定了解博主的作业状况,本职是一线业务搬砖党,所以轮子都是我闲暇和周末时刻自己慢慢堆集起来。后来实在是太好用了,我就开端推行,人人提效,人人如龙。但这东西吧,啥都挺好,就有一点不好,自从大家开发功率都提高了之后,领导给的开发工时更短了,淦。不说这些难过的事了,其实就我个人而言,组件也是我学习成长进程中的见证者,从一开端的磕磕绊绊到现在的信手拈来,回看当年的提交记载时,依旧觉得很有意思。
本文不只要一切组件的包结构简析,还有对中心功用的精讲,更有我特别收拾的版别更新记载,而且我还特别把提交时刻给捞出来了。更新记载捞出来呢,首要也是想让读者从改变的进程中,去了解我在造轮子进程中遇到的问题,一些挣扎和选型的进程。当然有一些之前提到过的组件,我偷懒啦,放了之前文章的链接,不然这一万多字的文章装不下了。写彻底篇后发现没有放我小仓库的衔接,捞一下gitee.com/cloudswzy/g…,给需求的读者们,里边有下面组件的部分功用抽取。
Tool-Box(东西箱)
包结构简析
├─annotation-注解
│ IdempotencyCheck.java-幂等性校验,带参数
│ JasyptField.java-加密字段,符号字段用
│ JasyptMethod.java-符号办法加密仍是解密
│ LimitMethod.java-限流器
├─aop
│ IdempotencyCheckHandler.java-幂等性校验切面
│ JasyptHandler.java-数据加密切面
│ LimitHandler.java-依据漏斗思维的限流器
├─api
│ GateWayApi.java–对外接口恳求
├─common
│ CheckUrlConstant.java–各个环境的接口恳求链接常量
│ JasyptConstant.java–加密解密标识常量
├─config
│ SpringDataRedisConfig.java–SpringDataRedis装备类,包括jedis装备、spring-cache装备、redisTemplate装备
│ CaffeineConfig.java–本地缓存caffeine通用装备
│ MyRedissonConfig.java–Redisson装备
│ ThreadPoolConfig.java–线程池装备
│ ToolApplicationContextInitializer.java–发动后查看参数
│ ToolAutoConfiguration.java–一起注册BEAN
├─exception
│ ToolException.java-东西箱反常
├─pojo
│ ├─message–邮件及音讯告诉用
│ │ EmailAttachmentParams.java
│ │ EmailBodyDTO.java
│ │ NoticeWechatDTO.java
│ └─user–用户信息提取
│ UserHrDTO.java
│ UserInfoDTO.java
├─properties–自定义spring装备参数提示
│ ToolProperties.java
├─service
│ DateMybatisHandler.java–Mybatis扩展,用于日期字段添加时分秒
│ HrTool.java–OA信息查询
│ JasyptMybatisHandler.java–Mybatis扩展,整合Jasypt用于字段脱敏
│ LuaTool.java–redis的lua脚本东西
│ MessageTool.java–音讯告诉类
│ SpringTool.java–spring东西类 便利在非spring办理环境中获取bean
└─util
│ MapUtil.java–Map自用东西类,用于切分Map支撑多线程
中心功用点
缓存(Redis和Caffeine)
相关类SpringDataRedisConfig,CaffeineConfig,MyRedissonConfig
<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>
关于依靠,阐明一下状况,公司的结构供给的Spring Boot版别是2.1.X版别,spring-boot-starter-data-redis在2.X版别是默许运用lettuce,当然也是由于lettuce具有比jedis更优异的功用。为什么这儿扫除了呢?原因是低版别下,lettuce存在断连问题,阿里云-通过客户端程序衔接Redis,上面这篇文章关于客户端的引荐里边,理由写得很清楚了,就不细说了。可是我个人引荐引入Redisson,这是我现在用过最好用的Redis客户端。
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客户端,集群和单点一起存在时优先集群装备
*/
@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();
// 默许堵塞等待时刻为无限长,源码DEFAULT_MAX_WAIT_MILLIS = -1L
// 最大衔接数, 依据业务需求设置,不能超越实例规格规则的最大衔接数。
jedisPoolConfig.setMaxTotal(100);
// 最大闲暇衔接数, 依据业务需求设置,不能超越实例规格规则的最大衔接数。
jedisPoolConfig.setMaxIdle(60);
// 关闭 testOn[Borrow|Return],防止产生额定的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)) {
// 单机形式
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内部运用Redis,装备序列化和过期时刻
*/
@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);
// 敞开实体类和json的类型转化,该处兼容老版别依靠,不得修正
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 ,在进行缓存的时分假如不指定key的话,最终会把生成的key缓存起来,
* 假如一起装备了KeyGenerator和key则优先运用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做了一个简略的装备,设置了最大衔接数,堵塞等待时刻默许无限长就不用装备了,除此之外对集群和单点的装备做了下封装。Spring-Cache也归于常用,由于其默许完成是依靠于本地缓存Caffeine,所以仍是替换一下,而且重写了keyGenerator,让默许生成的key具有可读性。Spring-Cache和RedisTemplate的序列化装备相同,key选用String是为了在图形化东西查询时便利找到对应的key,value选用Jackson序列化是为了压缩数据一起也是官方引荐。
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") // 服务中止后调用 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序列化时会呈现RBucket<Integer>转化反常
//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);
}
//修正命令超时时刻为40s,默许3s
clusterServers.setTimeout(40000);
//修正衔接超时时刻为50s,默许10s
clusterServers.setConnectTimeout(50000);
} else {
// 单机形式
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官方中文文档,中文文档更新慢而且有过错,主张看英文的。这儿装备很简略,首要是针对集群和单点还有超时时刻做了封装,重点是学会怎样玩Redisson,下面给出分布式锁和缓存场景的代码事例。低版别下的SpringDataRedis我是真的不引荐运用,之前我也封装过RedisTemplate,可是后来发现Redisson功用更强,功用更丰富,所以直接转用Redisson,组件中也没有供给RedisTemplate的封装。
@Autowired
private RedissonClient redissonClient;
//分布式锁
public void xxx(){
RLock lock = redissonClient.getLock("锁名");
boolean locked = lock.isLocked();
if (locked) {
//被锁了
}else{
try {
lock.lock();
//锁后的业务逻辑
} finally {
lock.unlock();
}
}
}
//缓存运用场景
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报错版别
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;
}
}
我是几个月前发现设置String序列化办法时,运用RBucket<>进行泛型转化会报类型转化过错的反常。官方在3.18.0版别才修正了这个问题,不过我引荐没有图形客户端可视化需求的运用默许编码即可,有更高的压缩率,而且现在运用没有呈现过转化反常。
当下Redis可视化东西最引荐官方的RedisInsight-v2,纯免费、好用还持续更新,除此之外引荐运用Another Redis Desktop Manager。

本地缓存之王Caffeine,哈哈,不知道从哪看的了,反正便是牛。我参阅官网WIKI的例子做了一个简略的封装吧,供给了一个能敷衍常见场景的实例能够直接运用,我个人更引荐依据实践场景自己新建实例。默许供给一个最多元素为10000,初始元素为1000,过期时刻设置为16小时的缓存实例,运用办法如下。更多操作看官方文档,Population zh CN ben-manes/caffeine Wiki。
@Autowired
@Qualifier("commonCaffeine")
private Cache<String, Object> caffeine;
Object countryObj = caffeine.getIfPresent("country");
if (Objects.isNull(countryObj)) {
//缓存没有,从数据库获取并填入缓存
caffeine.put("country", country);
return country;
} else {
//缓存有,直接强制转化后回来
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一起存在时,以expireAfterWrite为准。
//最终一次写操作后通过指守时刻过期
// .expireAfterWrite(Duration.ofMinutes(30))
//最终一次读或写操作后通过指守时刻过期
.expireAfterAccess(Duration.ofHours(16))
// 最大数量,默许依据缓存内的元素个数进行驱逐
.maximumSize(10000)
//打开数据搜集功用 hitRate(): 查询缓存的命中率 evictionCount(): 被驱逐的缓存数量 averageLoadPenalty(): 新值被载入的平均耗时
// .recordStats()
.build();
//// 查找一个缓存元素, 没有查找到的时分回来null
// Object obj = cache.getIfPresent(key);
//// 查找缓存,假如缓存不存在则生成缓存元素, 假如无法生成则回来null
// obj = cache.get(key, k -> createExpensiveGraph(key));
//// 添加或者更新一个缓存元素
// cache.put(key, graph);
//// 移除一个缓存元素
// cache.invalidate(key);
//// 批量失效key
// cache.invalidateAll(keys)
//// 失效一切的key
// cache.invalidateAll()
}
}
Redis东西
依据漏斗思维的限流器
相关类LimitMethod,LimitHandler,LuaTool
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();
//当没有自定义key时,给一个有可读性的默许值
String key = "";
if (ObjectUtils.isEmpty(application)) {
throw new ToolException("当时项目有必要具有spring.application.name才干运用限流器");
} else {
key = application + ":limit:" + methodName;
}
long judgeLimit = luaTool.judgeLimit(key, limit);
if (judgeLimit == -1) {
throw new ToolException("体系一起答应履行最多" + limit + "次当时办法");
} else {
log.info(methodSignature.getDeclaringTypeName() + "." + methodName + "在体系中答应一起履行" + limit +
"次当时办法,当时履行中的有" + judgeLimit + "个");
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 + "在体系中答应一起履行最多" + limit +
"次当时办法,履行结束后返还次数,现仍履行中的有" + nowCount + "个");
}
}
}
整个限流器以漏斗思维为基础构建,也便是说,我只约束最大值,不过和时刻窗口算法有差异的一点是,多了偿还次数的动作,这儿把他放在@After,确保无论如何都会履行。为了确保易用性,会生成Redis的默许key,我的挑选是用application(运用名) + “:limit:” + methodName(办法名),达到了key不重复和易读的方针。
/**
* 限流器-漏斗算法思维
*
* @param key 被限流的key
* @param limit 约束次数
* @return 当时时刻范围内正在履行的线程数
*/
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);
}
/**
* 偿还次数-漏斗算法思维
*
* @param key 被限流的key
* @return 正在履行的线程数
*/
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脚本,引荐运用的原因如下,感兴趣的话能够自学一下,上面阿里云的文章里也有事例能够参阅,包括Redisson的源码中也有很多参阅事例。
- 削减网络开销。能够将多个恳求通过脚本的形式一次发送,削减网络时延。运用lua脚本履行以上操作时,比redis普通操作快80%左右
- 原子操作。Redis会将整个脚本作为一个全体履行,中心不会被其他恳求刺进。因而在脚本运转进程中无需忧虑会呈现竞态条件,无需运用业务。
- 复用。客户端发送的脚本会永久存在redis中,这样其他客户端能够复用这一脚本,而不需求运用代码完结相同的逻辑。
说一下我写的脚本逻辑,首先获取当时key对应的值count,假如count不为null的状况下,再判别是否大于limit,假如大于阐明超越漏斗最大值,将count设置为-1,符号为超越约束。假如小于limit,则将count值自增1.假如count为null,阐明第一次进入,设置count为1。最终再刷新key的有用期并回来count值,用于切面逻辑判别。偿还逻辑和进入逻辑相同,反向考虑即可。
总结一下,限流器依据Lua+AOP,切点是@LimitMethod,注解参数是一起运转次数,运用场景是前后端的接口。@Around运转实践办法前进行限流(运用次数自增),@After后返还运用次数。作用是约束一起运转线程数,只要限流没有降级处理,超越的抛出反常中断办法。
读者提问:脚本最终一行失效时刻重置的意图是啥?
换个相反的视点来看,假如去掉了重置失效时刻的代码,是不是会存在一点问题?比方刚好进入限流后,此时流量为N,办法还没有运转结束,这个key失效了。那么依照代码逻辑来看,生成一个新的key就从0开端,可是明明之前我还有N个流量没有履行结束,也便是表面上看key的成果是最新的1,但实践上是1+N,这样流量就禁绝了。所以我这重置了下超时时刻,确保办法在超时时刻内运转结束能顺畅偿还,确保流量数更新正确。
幂等性校验器
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 幂等性校验切面
**/
@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("当时项目有必要具有spring.application.name才干运用幂等性校验器");
} 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("幂等性校验已敞开,当时Key为{}", key);
} else {
throw new ToolException("防重校验已敞开,当时办法制止在" + checkTime + "秒内重复提交");
}
return joinPoint.proceed(objects);
}
/***
* @Author WangZY
* @Date 2020/4/16 18:56
* @Description 获取入参
*/
private String getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {
Map<String, Object> requestParams = new HashMap<>(16);
//参数名
String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames();
//参数值
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], "参数类型为HttpServletRequest");
} else if (value instanceof HttpServletResponse) {
requestParams.put(paramNames[i], "参数类型为HttpServletResponse");
} else {
requestParams.put(paramNames[i], value);
}
}
return JSON.toJSONString(requestParams);
}
}
/**
* @author WangZY
* @date 2022/4/25 17:41
* @description 幂等性校验
**/
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);
}
幂等性校验器依据Lua和AOP,切点是@IdempotencyCheck,注解参数是单次幂等性校验有用时刻和幂等性校验Key,运用场景是前后端的接口。告诉部分只要@Around,Key值默许默许为运用名(spring.application.name):当时办法名:当时登录人ID(没有SSO便是null):入参的md5值,假如checkKey不为空就会替换入参和当时登录人—>运用名:当时办法名:checkKey。作用是在checkTime时刻内相同checkKey只能运转一次。
Lua脚本的写法由于没有加减,所以比限流器简略。这儿还有个要点便是为了确保key值长度可控,将参数用MD5加密,对一些特别的入参也要独自做处理。
发号器
/**
* 单号依照keyPrefix+yyyyMMdd+4位流水号的格式生成
*
* @param keyPrefix 流水号前缀标识--用作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;
}
发号器逻辑很简略,单号依照keyPrefix+yyyyMMdd+4位流水号的格式生成。Redis获取当时keyPrefix对应的key,假如没有则回来1,假如存在,判别是否大于9999,假如大于回来过错,假如小于就将value+1,而且设置过期时刻直到今日结束。
加密解密
相关类JasyptField,JasyptMethod,JasyptHandler,JasyptConstant,JasyptMybatisHandler
供给注解JasyptField用于方针特点以及办法参数。供给注解JasyptMethod用于注解在办法上。此加密办法由切面办法完成,运用时请务必留意切面运用忌讳。
运用事例
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这个结构呢?是之前看到有人引荐,加上可玩性不错,装备文件、代码等场景都能用上,整合也便利就直接用了。这个切面换成别的加密解密也是相同的玩法,用这个首要是还附赠装备文件加密的办法。除以上用法,还扩展了Mybatis,这儿对String类型做了脱敏处理,当然用别的解密办法也能够的。
Mybatis扩展运用
运用时,假如是mybatis-plus,务必在表映射实体类上添加注解@TableName(autoResultMap = true),在对应字段上加 typeHandler = JasyptMybatisHandler.class


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扩展,整合Jasypt用于字段脱敏
**/
@Component
public class JasyptMybatisHandler implements TypeHandler<String> {
/**
* mybatis-plus需在表实体类上加 @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());
}
}
}
线程池
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;
/**
* 默许CPU密集型--一切参数均需求在压测下不断调整,依据实践的使命消耗时刻来设置参数
* CPU密集型指的是高并发,相对短时刻的核算型使命,这种会占用CPU履行核算处理
* 因而中心线程数设置为CPU核数+1,削减线程的上下文切换,一起做个大的行列,防止使命被饱和战略回绝。
*/
@Bean("cpuDenseExecutor")
public ThreadPoolTaskExecutor cpuDense() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//获取逻辑可用CPU数
int logicCpus = Runtime.getRuntime().availableProcessors();
if (prop.getPoolCpuNumber() != null) {
//假如是中心业务,需求保活满足的线程数随时支撑运转,提高响应速度,因而设置中心线程数为压测后的理论最优值
executor.setCorePoolSize(prop.getPoolCpuNumber() + 1);
//设置和中心线程数一起,用行列操控使命总数
executor.setMaxPoolSize(prop.getPoolCpuNumber() + 1);
//Spring默许运用LinkedBlockingQueue
executor.setQueueCapacity(prop.getPoolCpuNumber() * 30);
} else {
executor.setCorePoolSize(logicCpus + 1);
executor.setMaxPoolSize(logicCpus + 1);
executor.setQueueCapacity(logicCpus * 30);
}
//默许60秒,维持不变
executor.setKeepAliveSeconds(60);
//运用自定义前缀,便利问题排查
executor.setThreadNamePrefix(prop.getPoolName());
//默许回绝战略,抛反常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
/**
* 默许io密集型
* IO密集型指的是有很多IO操作,比方长途调用、衔接数据库
* 由于IO操作不占用CPU,所以设置中心线程数为CPU核数的两倍,确保CPU不闲下来,行列相应调小一些。
*/
@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);
}
}
线程池对传统的ThreadPoolTaskExecutor和新锐的ForkJoinPool供给了常见的CPU和IO密集型的通用解。中心线程数和最大线程数设置为一起,通过行列操控使命总数,这是依据我对现在项目运用状况的一个经验值判别。假如对错中心业务,不需求保活这么多中心线程数,能够设置的小一些,最大线程数设置成压测最优成果即可。
更新记载
版别号 | 发布时刻 | 更新记载 |
---|---|---|
0.6 | 2021/6/21 14:13 | 初始化组件,添加Hr信息查询、音讯告诉、Redis、Spring东西 |
0.7 | 2021/6/21 18:39 | 添加Redisson装备类 |
0.8 | 2021/6/22 14:15 | 优化包结构,搬迁maven仓库坐标 |
0.9 | 2021/6/22 15:09 | 添加阐明文档 |
1.0 | 2021/7/2 11:51 | 添加Redis装备类,装备Spring Data Redis |
1.2 | 2021/7/15 11:25 | Hr信息查询添加新办法 |
1.2.5 | 2021/8/3 18:36 | 1.添加加密解密切面2.添加发动校验参数类 |
1.3 | 2021/8/4 10:31 | 加密解密切面BUG FIXED |
1.4.0 | 2021/8/10 10:14 | Redisson装备类添加Redis-Cluster集群支撑 |
1.4.5 | 2021/9/14 16:03 | 添加Excel模块相关类 |
1.5.0 | 2021/9/14 16:51 | 添加@Valid快速失利机制 |
1.6.0 | 2021/9/15 15:04 | 1.加密解密切面支撑更多入参,BUG FIXED2.添加脱敏用Mybatis扩展 |
1.6.8 | 2021/9/17 11:29 | 添加主站用待办模块相关类 |
1.6.9 | 2021/10/27 13:19 | 脱敏用Mybatis扩展BUG FIXED |
1.7.0 | 2021/10/28 20:43 | 更新邮件发送人判别,优化音讯告诉东西 |
1.7.1 | 2021/11/15 10:07 | 待办参数移除强制校验 |
1.7.2 | 2021/11/23 14:08 | 邮件发送添加附件支撑 |
1.7.5 | 2021/12/9 11:08 | 1.待办及Excel模块搬迁至组件Business-Common2.添加spring-cache装备redis3.ToolException承继AbstractRJBusinessException,能被大局反常监听 |
2.0.0 | 2022/1/7 11:22 | 彻底去除业务部分,搬迁至组件Business-Common |
2.0.2 | 2022/1/13 15:44 | 添加一起注册类ToolAutoConfiguration |
2.0.5 | 2022/3/14 15:11 | 音讯告诉东西运用resttemplate默许编码格式不支撑中文问题处理 |
2.0.6 | 2022/3/24 23:49 | Redisson编码替换String,便利图形可视化 |
2.0.7 | 2022/3/30 14:22 | Redisson及Mybatis依靠版别晋级 |
2.0.8 | 2022/4/12 11:57 | 添加线程池装备 |
2.0.9 | 2022/4/15 18:25 | 添加漏桶算法限流器 |
2.1.0 | 2022/4/18 14:29 | 漏桶算法限流器优化,切面次序调整 |
2.1.1 | 2022/4/26 9:56 | 新增幂等性校验东西 |
2.1.2 | 2022/4/26 16:13 | 幂等性校验支撑文件、IO流等特别参数 |
2.1.3 | 2022/4/29 14:23 | 1.移除redisTool,引荐运用Redisson2.修正单号生成器BUG |
2.1.4 | 2022/5/18 11:29 | 1.修正了自2.1.0版别以来的限流器BUG2.优化了缓存装备类的过时代码 |
2.1.6 | 2022/5/24 17:44 | 合作架构组晋级新网关 |
2.1.7 | 2022/6/8 14:01 | 添加Caffeine装备 |
2.1.8 | 2022/7/12 10:19 | 1.回归fastjson1,防止fastjson2版别兼容性BUG2.forkjoinpool临时参数 |
2.1.9 | 2022/7/27 13:59 | 优化音讯告诉东西,添加发送人参数 |
2.2.0 | 2022/8/25 9:24 | 1.添加ForkJoinPool类型的线程池默许装备2.线程池参数添加装备化支撑 |
2.2.2 | 2022/9/19 17:08 | 修正Redisson编码为默许编码,String编码不支撑RBucket的泛型(Redisson3.18.0已修正该问题) |
2.2.3 | 2022/9/21 19:06 | 调大Redisson命令及衔接超时参数 |
2.2.4 | 2022/9/27 11:52 | 音讯告诉东西BUG FIXED,防止空指针 |
2.2.5 | 2022/12/16 18:46 | 添加东西类Map切分 |
2.2.8 | 2022/12/18 13:19 | 添加Mybatis扩展,日期转化处理器 |
2.2.9 | 2023/2/10 22:30 | Redisson及Lombok依靠版别晋级 |
2.3.0 | 2023/5/6 10:26 | 重写Redis装备类,添加SpringDataRedisConfig |
2.3.1 | 2023/5/7 19:05 | 1.线程池参数调整2.优化注释 |
Business-Common(业务包)
包结构简析
├─annotation
│ ExcelFieldValid.java–数据输入校验注解
├─config
│ BusinessAutoConfiguration.java–一起注册BEAN
│ BusinessBeanConfig.java–删去公司结构包中的大局反常监听类
│ ExcelFieldValidator.java–Excel参数校验-Validator扩展
│ MybatisPlusConfig.java–支撑Mybatis-Plus分页方言装备
│ MyBatisPlusObjectHandler.java–MyBatisPlus用填充字段规则
│ ValidatorConfig.java–Valid装备快速失利形式
├─constant
│ ExcelFieldConstant.java–Excel模块用常量
│ TaskConstant.java–待办模块用常量
├─excel
│ ExcelListener.java–通用Easy Excel监听,在原版基础上魔改强化
│ ExcelTool.java–超级威力无敌全能Excel东西,整合主子站、文件服务器、Easy Excel
│ NoModelExcelListener.java–无模板Excel监听类
├─exception
│ PtmException.java–供给项目一起的自定义反常
│ PtmExceptionHandler.java–大局反常监听
├─pojo
│ ├─dto
│ │ CommonProperties.java–业务包可装备参数
│ ├─excel
│ │ ExcelAddDTO.java–主站文件列表新增接口入参
│ │ ExcelAnalyzeResDTO.java–Excel解析成果类
│ │ ExcelUpdateDTO.java–主站文件列表更新接口入参
│ │ ExcelUploadResDTO.java–文件服务器回来成果类
│ └─task
│ ForwardTaskDTO.java–主站转办接口入参
│ RecallTaskDTO.java–主站撤回待办接口入参
│ TaskApproveDTO.java–主站批阅待办接口入参
│ TaskReceiveDTO.java–主站生成待办接口入参
├─properties
│ RewriteOriginTrackedPropertiesLoader.java–修正Spring装备文件读取默许编码
│ RewritePropertiesPropertySourceLoader.java–修正Spring装备文件读取默许编码
├─remote
│ ExternalApi.java–对外调用
└─util
│ JacksonJsonUtil.java–Jackson东西类
│ ModelConverterUtils.java–模型转化东西
│ UploadFileServerUtil.java–文件服务器交互东西类
中心功用
Excel模块
后端思维-如何规划一个操作和办理Excel的业务模块,具体状况参阅以上文章,六千字精解,不再赘述。
1.0.4重大版别更新
处理BUG—获取文件流失利 java.io.FileNotFoundException: /data/ptm/tmp/upload_0e7e1e62_8df3_4d2a_ae2f_86be3a0c08c6_00000000.tmp (No such file or directory) at java.io.FileInputStream.open0(Native Method)
该BUG原因是Spring上传文件时,异步操作时主线程关闭IO流,Tomcat删去缓存的上传文件,导致子线程操作文件实例时找不到。当时已修正该问题,并做了新的优化,包括运用缓冲流加快文件读取、删去本地临时文件释放空间。
反常体系
反常体系首要是为了供给友爱提示、依据不同过错码转向不同处理场景、优化Controller层。

优化后如上,需求有一个类似于RemoteResult的类,包括状态码,音讯,回来值,假如你有更多的内容需求输出那就扩展这个类。反常首要是用到三个类。
- 业务反常类PtmException,供给项目一起的自定义反常,包括过错码,过错信息,默许过错码是10001。
- 抽象反常类AbstractException,这个类的首要作用是供给一个反常的父类,便利扩展,一切业务反常类PtmException比方强制承继该类
- 大局反常监听类PtmExceptionHandler,在这个类里边去监听不同的过错,依据不同的过错来进行对应的处理
import com.xx.framework.common.RemoteResult;
import com.xx.framework.exception.exception.AbstractRJBusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author WangZY
* @Date 2020/12/24 10:40
* @Description 大局反常监听
**/
@RestControllerAdvice
@Slf4j
public class PtmExceptionHandler {
@ExceptionHandler(Exception.class)
public RemoteResult<String> handleException(HttpServletRequest request, Exception e) {
log.error("大局监听反常捕获,办法={}", request.getRequestURI(), e);
return new RemoteResult<>("10001", "内部过错,请联系办理员处理");
}
@ExceptionHandler(AbstractRJBusinessException.class)
public RemoteResult<String> handleBusinessException(HttpServletRequest request, AbstractRJBusinessException e) {
log.error("大局监听业务反常捕获,办法={}", request.getRequestURI(), e);
String errCode = e.getErrCode();
if ("10003".equals(errCode)) {
return new RemoteResult<>(errCode, "用户未授权,行将跳转登录地址", e.getErrMsg());
} else {
return new RemoteResult<>(errCode, e.getErrMsg());
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public RemoteResult<String> methodArgumentNotValidExceptionHandler(HttpServletRequest request,
MethodArgumentNotValidException e) {
log.error("大局监听Spring-Valid反常捕获,办法={}", request.getRequestURI(), e);
// 从反常方针中拿到ObjectError方针
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
String err = allErrors.stream().map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
// 然后提取过错提示信息进行回来
return new RemoteResult<>("10001", err);
}
}
强制Spring读取装备文件运用UTF-8
重写装备类RewritePropertiesPropertySourceLoader,固定UTF-8编码,防止中文读取乱码。spring.factories里为org.springframework.boot.env.PropertySourceLoader接口供给一个新的完成类,而且运用@Order调高优先级。
移除三方包中指定Bean
该办法不行移除装备类,也便是@Configuran注解的类。
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.stereotype.Component;
/**
* @Classname RegistryBeanFactoryConfig
* @Date 2021/12/6 18:39
* @Author WangZY
* @Description 删去base包部分数据
*/
@Component
public class BusinessBeanConfig implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (registry.containsBeanDefinition("rJGlobalDefaultExceptionHandler")) {
registry.removeBeanDefinition("rJGlobalDefaultExceptionHandler");
}
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
更新记载
版别号 | 发布时刻 | 更新记载 |
---|---|---|
0.1.0 | 2021/12/7 10:53 | 初始化组件包,添加一起注册类BusinessAutoConfiguration |
0.2.0 | 2021/12/7 16:21 | 搬迁Tool-Box中的业务部分 |
0.3.0 | 2021/12/9 11:05 | 初版 该版可用1.整合依靠-部分结构中Base包及Http包,Easy Excel,文件服务器,Mybatis-Plus2.搬迁Tool-Box的Excel模块,开发Excel Tool,该东西更新中,现在完好的功用已有异步导出3.Mybatis-Plus分页插件装备4.禁用结构大局反常监听类,整合为PtmExceptionHandler,一切反常一起为PtmException,自定义反常有必要承继AbstractRJBusinessException5.禁用部分结构日志切面,运用自定义恳求回来结构,添加注解IgnoreLog可不输出入参和出参日志,添加日志切面类RequestLogAspect |
0.5.0 | 2021/12/15 17:25 | 1.部分结构更新2.7.12.新增装备文件参数指引3.ExcelTool添加通用导入办法commonImportExcel,整合主子站交互以及文件服务器交互逻辑,支撑对File以及MultipartFile类型的解析 |
0.5.5 | 2021/12/22 9:44 | 引入RestTemplate |
0.5.6 | 2021/12/23 15:14 | RestTemplate整合部分网关校验参数 |
0.6.0 | 2022/1/7 11:31 | 新增文件及待办相关业务组件 |
0.6.4 | 2022/1/13 16:40 | RequestLogAspect及ExcelTool的BUG FIXED |
0.6.5 | 2022/1/27 16:45 | RequestLogAspect日志切面优化 |
0.6.6 | 2022/2/9 17:16 | 移除日志切面模块,搬迁至Log-Transfer组件 |
0.6.7 | 2022/2/14 17:21 | 1.resttemplate装备完善,添加部分要求header2.无法传递Date字段的格式化问题处理 |
0.7.0 | 2022/3/1 10:13 | ExcelTool优化文件名生成逻辑,更具可读性且更便利 |
0.8.0 | 2022/3/30 14:28 | 晋级Mybatis-Plus大版别,高版别存在不兼容问题,需各业务体系挑选性更新 |
0.8.2 | 2022/5/18 11:51 | 1.ExcelTool新增同步导出办法2.MP分页插件方言类型支撑Spring装备文件参数装备 |
0.8.4 | 2022/6/8 14:59 | 魔改强化Easy Excel默许读取类,并添加解析反常信息的搜集 |
0.8.6 | 2022/6/20 9:40 | 1.ExcelTool创立文件默许导出BUG FIXED2.ExcelTool新增创立人信息入参 |
0.8.8 | 2022/6/20 16:34 | ExcelTool搜集仓库信息需求截取,优化日志输出 |
0.8.9 | 2022/6/21 19:57 | 业务支撑-待办新增字段 |
0.9.0 | 2022/6/23 16:56 | ExcelTool添加新办法,支撑动态导入及导出场景 |
0.9.1 | 2022/6/24 14:44 | 1.添加读取表头及搜集功用2.供给表头校验参数,校验是否与预期一起-支撑业务 |
0.9.3 | 2022/7/5 15:05 | ExcelTool优化过错展示,供给多种途径的过错信息输出 |
0.9.4 | 2022/7/21 10:50 | ExcelTool的BUG FIXED |
0.9.6 | 2022/7/27 14:06 | 1.截取反常信息BUG FIXED2.资源释放优化,try-with-resources3.FastJson版别序列化兼容问题处理 boolean isXXX |
0.9.8 | 2022/8/1 17:17 | 加载Spring装备文件强制运用UTF-8,处理中文乱码问题 |
0.9.9 | 2022/8/19 16:41 | 下调Excel解析失利日志等级为warn |
1.0.1 | 2022/9/14 15:50 | 添加Mybatis-Plus自动填充装备类 |
1.0.2 | 2022/10/21 15:27 | ExcelListener监听类BUG FIXED |
1.0.3 | 2022/11/18 13:58 | 添加模型转化东西类 |
1.0.4 | 2022/12/16 17:04 | 1.异步读取文件,文件丢掉BUG修正2.运用缓冲流优化文件读取速度3.优化通用导入办法,修正回来结构4.删去导入和导出时的本地文件,释放空间 |
1.0.5 | 2023/2/9 15:49 | 更新Mybatis-Plus和Easy Excel依靠版别至最新版 |
SSO-ZERO(单点登录)
包结构简析
├─api
│ ScmApi.java–门户网站SCM长途调用
├─common
│ CheckUrlConstant.java–各个环境的URL
│ LoginConstant.java–SSO一起常量,用于主子站同享变量
├─config
│ RJSsoConfigurerAdapter.java–SSO拦截器注入,供给途径扫除
│ SSOProperties.java–SSO-ZERO用参数
├─exception
│ SsoAppCode.java–部分原始SSO留传
│ SSOException.java–反常
├─hanlder
│ SsoProcessHandler.java–SSO中心处理类
├─model
│ LocalUserInfo.java–留下扩展,参数受部分大结构约束
├─pojo
│ └─dto
│ MenuVO.java–菜单信息
│ RoleCacheDTO.java–人物缓存信息
│ UserGroupCacheDTO.java–用户组缓存信息
├─spi
│ RuiJieSsoVerifyProcessor.java–SPI扩展接口,留下放出,现在仅有自己开发规划维护
└─utils
│ CookieUtils.java–Cookie东西类
│ CurrentUserUtil.java–供给给开发同事的SSO信息简易获取东西类
组件简述

后端思维-单点登录组件的规划与考虑,同样是一个我规划并开发的,缺了认证的单点登录模块,很惋惜受限于公司架构,不是认证授权鉴权三位一体的完好版。在已有认证的状况下,做了一个主站-组件构成的授权鉴权模块,由于是内网,安全方面做的比较粗糙。在功用上我是依照shiro去规划的,比方注解操控权限。文章是好文章,记载了六次迭代的改变点和我的考虑,最终总结的时分还列举我对这个单点登录组件的一些感想,可是组件没有做到很完善,仍是有点惋惜。
更新记载
版别号 | 发布时刻 | 更新记载 |
---|---|---|
1.0.0 | 2021/5/10 21:15 | 初始化组件,不行用 |
1.1.0 | 2021/5/11 10:48 | 兼容公司OA登录,优化冗余代码 |
1.1.2 | 2021/5/11 16:53 | 去除校验模仿登录模块,格式化代码 |
1.1.3 | 2021/5/11 18:50 | 去除无用缓存模块 |
1.1.6 | 2021/5/13 14:49 | 兼容E渠道登录 |
1.1.9 | 2021/5/26 15:15 | 1.对接主站权限模块2.优化日志输出 |
1.2.0 | 2021/5/27 11:44 | 整合主站优化授权模块 |
1.2.2 | 2021/6/3 11:36 | 添加缓存,提高鉴权及授权速度 |
1.2.4 | 2021/6/21 17:39 | 判别仅有逻辑从部分原有的userid变为整合主站后的userid+uid,防止多个认证源呈现userid一起的状况 |
1.2.5 | 2021/8/3 11:09 | 业务支撑-为子体系添加扩展信息 |
1.2.7 | 2021/9/9 15:36 | 添加体系链接装备,用以本地调试 |
1.2.8 | 2021/10/27 14:04 | 代码优化,老版SSO封版 |
1.4.0 | 2021/10/28 13:58 | 合作公司战略,认证办法替换为其他部分自研认证体系SID,第一次整合结束 |
1.4.1 | 2021/11/8 10:10 | 1.晋级SID版别后,移除无用登录校验2.整合用户信息及前端菜单、权限、组等信息接口,合二为一,削减长途调用次数,加快鉴权3.添加SSO日志,打印具体鉴权及授权进程和反常信息定位日志4.删去冗余代码,精简代码 |
1.4.2 | 2021/11/12 17:44 | 1.一切用户鉴权操作融合成一个接口,再次削减校验次数2.新增SID版别下测验环境的Cookie,并做好正式测验的阻隔3.优化包结构,简化代码 |
1.4.4 | 2021/12/7 10:48 | 1.新增渠道用户登录校验2.日志BUG,API部分优化 |
1.4.6 | 2022/1/4 15:15 | E渠道账号中心空格数据导致URL解析失利的BUG FIXED |
1.4.7 | 2022/1/7 13:25 | 新增Refresh Token续约机制 |
1.4.8 | 2022/1/13 16:34 | 过错码定制化,与前端合作完善SID版别单点登录模块 |
1.4.9 | 2022/1/18 11:42 | 搬迁部分新网关 |
1.5.0 | 2022/2/17 16:14 | 新网关存在兼容问题,紧急回撤老网关并参加白名单 |
1.5.1 | 2022/3/30 22:42 | 新网关已稳定,从头迁回 |
1.5.3 | 2022/5/23 17:27 | 新增获取用户信息东西类 |
1.5.4 | 2022/6/20 17:15 | 1.持续优化代码2.添加对新老网关的兼容 |
1.5.9 | 2022/8/22 14:57 | 兼容部分通用鉴权授权体系 |
1.6.1 | 2022/8/23 16:18 | 兼容部分鉴权授权体系引发的BUG FIXED |
1.6.3 | 2022/8/23 19:18 | 优化ThreadLocal运用部分的代码 |
1.6.4 | 2022/8/25 20:34 | SID版别添加测验版别已阻隔版别环境 |
1.6.5 | 2022/10/8 10:48 | 添加具体过错日志,便利问题定位 |
1.6.9 | 2023/5/6 17:36 | 老网关容易呈现反常,搬迁一切接口转为新网关 |
Log-Transfer(日志传输)
包结构简述
├─annotation
│ IgnoreLog.java–切面扫除该日志
│ LogCollector.java–前史留传,第一版日志搜集体系用注解
├─aop
│ LogTransferHandler.java–优化后日志切面,仍保留第一版日志搜集体系用注解,为后续行为日志搜集埋下伏笔
├─config
│ KafkaConsumerConfig.java–多种顾客装备
│ KafkaProducerConfig.java–多种生产者装备
│ LogApplicationContextInitializer.java–发动后查看参数
│ LogAutoConfiguration.java–一起注册BEAN
│ LogBeanConfig.java–删去部分结构中默许日志切面
│ Snowflake.java– hutool雪花ID单机版,自用魔改简化版
├─constants
│ LogConstants.java–日志常量
├─exception
│ TransferException.java–日志反常
├─pojo
│ LogProviderDTO.java–日志搜集体系用信息搜集类
├─properties
│ TransferProperties.java–Spring装备文件可装备参数
└─util
│ AddressUtils.java–获取IP归属地
│ IpUtils.java–获取IP办法
组件简述

Filebeat+Kafka+数据处理服务+Elasticsearch+Kibana+Skywalking日志搜集体系,该组件服务于我自己规划并开发的完好日志搜集体系,并供给Kafka生产者和顾客的模板装备,后边是本文介绍。
一个由我独立规划并开发的,完好的日志搜集体系,到今日成功运转了一年半了,接入了团队的三四十个大小项目,成功抢了架构组的活,装了个大大的逼。文章具体描绘了三次完好的迭代进程,为什么需求迭代?我做了什么优化?这一阶段我是怎样想的?以上大家最关心的问题,我都做出了回答。毫无疑问,这是我做过最疯狂的操作,难度系数拉满。后续更新的时分追加了一些扩大日志,以及部分装备的优化。对我来说,真的是一次很有应战,也很长常识的阅历,我至今难以想象我是如何用下班和周末时刻,自己捣鼓出来这么一套庞大的东西,真TM离谱。

音讯积压问题难?思路代码优化细节全公开–1550阅览37赞42保藏,一起组件为本文的Kafka装备供给了代码支撑,后边是该文介绍。
我很奇怪,这篇纯纯的实战文真的是榨干了我,花了很多的时刻来测验和佐证我的结论。有音讯积压问题的具体处理思路和伪代码,还对Kafka的生产者顾客装备的优化给出了解说。我在整个进程中遇到的问题也有具体的记载和处理方案。数据算是一般般吧,不过我会持续尽力的,带来更好的文章。
更新记载
版别号 | 发布时刻 | 更新记载 |
---|---|---|
1.0.0 | 2022/1/19 18:35 | 初始化包结构 |
1.0.2 | 2022/1/20 17:37 | 开发日志切面LogTransferHandler |
1.0.6 | 2022/2/9 17:14 | 1.扫除部分结构中的日志切面2.完善日志切面投入运用 |
1.0.8 | 2022/2/16 18:42 | 1.供给日志搜集注解2.添加不自动搜集日志的挑选 |
1.0.9 | 2022/2/18 10:12 | 优化日志切面,放过健康检测接口,MDC添加自定义参数 |
1.1.1 | 2022/2/25 17:32 | 一起注册Bean类 |
1.1.2 | 2022/4/11 11:20 | 供给Kafka生产者和顾客的多种模板装备,高并发低时延次序性等等 |
1.1.3 | 2022/4/13 15:03 | 削减日志组件的强制装备,供给默许装备 |
1.1.4 | 2022/4/14 22:27 | Kafka参数供给Spring装备文件参数装备,并添加@Primary,防止引入时未指定Bean导致的报错 |
1.1.5 | 2022/4/24 14:10 | 依据业务状况优化Kafka装备的参数 |
1.1.6 | 2022/6/20 16:56 | 优化日志切面,处理日志打印延后的问题 |
1.1.7 | 2022/7/14 14:24 | 应实践业务状况添加手动提交顾客的装备 |
1.1.8 | 2023/2/28 22:29 | 适配项目调整顾客参数 |
1.1.9 | 2023/3/9 22:57 | 添加订单交付核算项目专用测验顾客,并调整参数适配项目 |
1.2.6 | 2023/5/8 17:40 | 1.Kafka生产者装备最大音讯大小和恳求超时时刻调大,防止大音讯发送失利2.Kafka顾客装备添加单通道消费 |
Feign-Seata(Seata包)
包结构简析
├─config
│ FeignInterceptor.java–Open Feign植入Seata用XID
│ SeataAutoConfiguration.java–一起注册BEAN
├─filter
│ SeataFilter.java–Seata过滤器–也能够用拦截器完成
└─interceptor
│ SeataHandlerInterceptor.java–Seata官方包捞出来的拦截器完成版别
│ SeataHandlerInterceptorConfiguration.java–拦截器注册类
组件简析

分布式业务Seata-1.5.2运用全道路指北,三千字文章全面介绍了部署、server端装备、client端装备、组件封装,还记载了我在运用Seata时遇到的问题,最终聊了聊我对分布式业务的看法,欢迎来看嗷!版更记载就没必要发了,写完之后就没什么变动了,便是一个普通的依据Seata做本地化封装的组件。即使更新也仅仅适配新版别Seata,依照官方文档进行修正。
Timer-Common(Elastic-Job包)
包结构简析
├─config
│ ElasticJobConfiguration.java–守时调度装备类
│ TimerAutoConfiguration.java–一起注册BEAN
└─properties
│ TimerProperties.java–守时调度可装备参数
组件简析
import com.xx.framework.base.config.BaseEnvironmentConfigration;
import com.xx.timer.properties.TimerProperties;
import org.apache.commons.lang3.StringUtils;
import org.apache.shardingsphere.elasticjob.reg.base.CoordinatorRegistryCenter;
import org.apache.shardingsphere.elasticjob.reg.zookeeper.ZookeeperConfiguration;
import org.apache.shardingsphere.elasticjob.reg.zookeeper.ZookeeperRegistryCenter;
import org.apache.shardingsphere.elasticjob.tracing.api.TracingConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 守时调度装备列
*/
@EnableConfigurationProperties({TimerProperties.class})
@Configuration
public class ElasticJobConfiguration {
@Autowired
private TimerProperties prop;
@Autowired
private BaseEnvironmentConfigration env;
/**
* 初始化装备
*/
/**
* 当ruijie.timer.start为true时初始化bean,假如没有该项特点则默许值为true
*/
@Bean(initMethod = "init")
@ConditionalOnProperty(value = "ruijie.timer.start", havingValue = "true", matchIfMissing = true)
public CoordinatorRegistryCenter zkRegCenter() {
String zkServerList = prop.getZkServerList();
String zkNamespace = prop.getZkNamespace();
String currentEnv = env.getCurrentEnv();
if (StringUtils.isBlank(zkServerList)) {
if ("pro".equalsIgnoreCase(currentEnv)) {
zkServerList = "";
} else {
zkServerList = "";
}
}
if (StringUtils.isBlank(zkNamespace)) {
if ("pro".equalsIgnoreCase(currentEnv)) {
zkNamespace = "elastic-job-pro";
} else {
zkNamespace = "elastic-job-uat";
}
}
ZookeeperConfiguration zkConfig = new ZookeeperConfiguration(zkServerList, zkNamespace);
zkConfig.setConnectionTimeoutMilliseconds(100000);
zkConfig.setSessionTimeoutMilliseconds(100000);
zkConfig.setMaxRetries(3);
zkConfig.setMaxSleepTimeMilliseconds(60000);
zkConfig.setBaseSleepTimeMilliseconds(30000);
return new ZookeeperRegistryCenter(zkConfig);
}
@Bean
@ConditionalOnProperty(value = "ruijie.timer.start", havingValue = "true", matchIfMissing = true)
public TracingConfiguration<DataSource> tracingConfiguration() {
String dbUrl = prop.getDbUrl();
String dbDriverClassName = prop.getDbDriverClassName();
String dbUserName = prop.getDbUserName();
String dbPassword = prop.getDbPassword();
String currentEnv = env.getCurrentEnv();
if (StringUtils.isBlank(dbUrl)) {
if ("pro".equalsIgnoreCase(currentEnv)) {
dbUrl = "";
dbDriverClassName = "org.postgresql.Driver";
dbUserName = "";
dbPassword = "";
} else {
dbUrl = "";
dbDriverClassName = "org.postgresql.Driver";
dbUserName = "";
dbPassword = "";
}
}
DataSource source = DataSourceBuilder.create()
.url(dbUrl).driverClassName(dbDriverClassName)
.username(dbUserName).password(dbPassword).build();
return new TracingConfiguration<>("RDB", source);
}
}
组件中心便是上面这个装备类,简略封装了一下Elastic-Job必要的参数,为下面这个项目中运用的守时类,供给必要的装备。
import org.apache.shardingsphere.elasticjob.api.JobConfiguration;
import org.apache.shardingsphere.elasticjob.lite.api.bootstrap.impl.ScheduleJobBootstrap;
import org.apache.shardingsphere.elasticjob.reg.base.CoordinatorRegistryCenter;
import org.apache.shardingsphere.elasticjob.tracing.api.TracingConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @Classname JobConfig
* @Date 2022/3/29 17:52
* @Author WangZY
* @Description 守时使命装备类
*/
@Component
public class JobConfig {
public static final String time_zone = "GMT+08:00";
//注入TracingConfiguration和CoordinatorRegistryCenter两个必要的装备类,固定装备
@Autowired
private TracingConfiguration tracingConfiguration;
@Autowired
private CoordinatorRegistryCenter zkRegCenter;
@Autowired
private ErpBudgetSchedule erpBudgetSchedule;
//守时使命详情装备,一般只用改newBuilder里,这儿是使命仅有ID,分片给1就行,假如需求多个分片一起参加运算则给多个。
//cron表达式自己写,描绘改一下,其他不用动了
private JobConfiguration createErpBudgetSchedule() {
JobConfiguration job = JobConfiguration
.newBuilder("Dashboard-ErpBudgetSchedule", 1)
.cron("0 0 1 ? * *").description("资金办理")
.overwrite(true).jobErrorHandlerType("LOG").timeZone(time_zone).build();
job.getExtraConfigurations().add(tracingConfiguration);
return job;
}
private JobConfiguration createErpBudgetSchedule() {
JobConfiguration job = JobConfiguration
.newBuilder("Dashboard-ErpBudgetSchedule", 1)
.cron("0 0 1 ? * *").description("资金办理")
.overwrite(true).jobErrorHandlerType("LOG").timeZone(time_zone).build();
job.getExtraConfigurations().add(tracingConfiguration);
return job;
}
@PostConstruct
public void runSchedule() {
//这儿固定写法,只用改第二和第三个参数即可
new ScheduleJobBootstrap(zkRegCenter, erpBudgetSchedule, createErpBudgetSchedule()).schedule();
new ScheduleJobBootstrap(zkRegCenter, analisisReportSchedule, analysisReportScheduleTask()).schedule();
.......
}
}
写在最终
就在昨日也便是周天的时分呢,发生了一件对我来说特别有意义的功德,哈哈,所以,我很高兴!在长时刻的激动和高兴之后呢,决议临时加更一篇文章,来平复我的心境,当然我并不是有什么大病,非得写文章来冷静。和朋友打了两把游戏,聊了一会儿,最终仍是亢奋,没办法,得写点东西。写完日记之后,复盘了下这件功德,觉得正好要写东西吧,那就写篇文章。正好领导让我收拾我做的轮子,究竟就我一人开发,还开发了这么多轮子,没人知道怎样玩了,我一请假出问题就全白给。所以这篇文章应运而出,家人们,有功德发生,这文后语不写了,我先溜了!
功德告一段落,心境美滋滋,有点小紧张,哈哈,不过还算顺畅。接着随意写写,但如同也没啥写的,那就重申一遍我的人生信条,我要让这苦楚压抑的世界开放幸福快乐之花,向美好的世界献上祝福!!!
2023/5/12追加更新
- Caffeine添加遗漏的装备文件
- 追加封面图,云吸猫
读者朋友给我发了他拿我文档中的代码去问ChatGpt的截图,我懵了,真的泰裤辣!早知道ChatGpt精干这个,我自己写啥注释啊!牛逼的,真是长见识了。


最近有读者和朋友跟我提到过焦虑,我也焦虑,可是焦虑也没用啊,是吧,假如还想干这行,就多学习。不是说有多卷,作业仍是不少的,吃饭是没问题的,保持学习的劲头。一起呢,有一些解压的喜好那是最好,我自己的话便是游戏和写博客,还会偶尔记记日记。
我不会劝你放下焦虑,更等待你有勇气面临可能到来的窘境,诸君共勉!
最终小小的给自己引荐一波文章,由于本文现在是后端热榜第一,综合榜第二,阅览量来到了4K,保藏数来到了160,理论上这样的数据在后端这个板块暂时是没有增加趋势了,那么我小推一下自己的文章应该不算引流吧,哈哈。算是给偶然点进来的读者们一个小小的惊喜!
写技术博客的这一年,有个人的成长也有与别人思维的碰撞,博主的个人介绍,一些对人生、作业、情感的考虑和内省,期望给焦虑的你带来一丝安慰。
如何发掘项目中的亮点(多方向带事例),现在我最强也是全网独一份的文章,彻底原创,帮我冲一下保藏吧,立刻就能够上保藏榜了,谢谢大家!
最终的最终,仍是不由得想提起之前半场开香槟的蠢事,哈哈,这儿给自己简略记一笔,下次别这么上头了。