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的源码中也有很多参阅事例。

  1. 削减网络开销。能够将多个恳求通过脚本的形式一次发送,削减网络时延。运用lua脚本履行以上操作时,比redis普通操作快80%左右
  2. 原子操作。Redis会将整个脚本作为一个全体履行,中心不会被其他恳求刺进。因而在脚本运转进程中无需忧虑会呈现竞态条件,无需运用业务。
  3. 复用。客户端发送的脚本会永久存在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追加更新

  1. Caffeine添加遗漏的装备文件
  2. 追加封面图,云吸猫

读者朋友给我发了他拿我文档中的代码去问ChatGpt的截图,我懵了,真的泰裤辣!早知道ChatGpt精干这个,我自己写啥注释啊!牛逼的,真是长见识了。

六脉神剑-我在公司造了六个轮子
六脉神剑-我在公司造了六个轮子

最近有读者和朋友跟我提到过焦虑,我也焦虑,可是焦虑也没用啊,是吧,假如还想干这行,就多学习。不是说有多卷,作业仍是不少的,吃饭是没问题的,保持学习的劲头。一起呢,有一些解压的喜好那是最好,我自己的话便是游戏和写博客,还会偶尔记记日记。

我不会劝你放下焦虑,更等待你有勇气面临可能到来的窘境,诸君共勉!

最终小小的给自己引荐一波文章,由于本文现在是后端热榜第一,综合榜第二,阅览量来到了4K,保藏数来到了160,理论上这样的数据在后端这个板块暂时是没有增加趋势了,那么我小推一下自己的文章应该不算引流吧,哈哈。算是给偶然点进来的读者们一个小小的惊喜!

写技术博客的这一年,有个人的成长也有与别人思维的碰撞,博主的个人介绍,一些对人生、作业、情感的考虑和内省,期望给焦虑的你带来一丝安慰。

如何发掘项目中的亮点(多方向带事例),现在我最强也是全网独一份的文章,彻底原创,帮我冲一下保藏吧,立刻就能够上保藏榜了,谢谢大家!

最终的最终,仍是不由得想提起之前半场开香槟的蠢事,哈哈,这儿给自己简略记一笔,下次别这么上头了。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。