最近关注了沅陵的莲花池,整个村子都是钓鱼佬,人均up主,基本都是沾亲带故的,错综复杂,恰逢正在学习Neo4j,可以借此机会将人物关系通过图谱的方式呈现出来。
人物创建
首先将人物关系建立出来,利用create来创建效率实在太低了 ,所以决定采用数据导入的方式。
去种田的向凹凸
小白兔的胡萝卜甜(冉甜)
向云朵
我是野农
画燕儿
向鹿鸣
路人阿丙X
大明星(冉蜜)
向偶然
荷塘星星
向尘俊
向星言
多肉葡萄肉多多
白叔
播音哥
三叔
丹宝
守山人阿亮
导入数据:
load csv from "file:///莲花池人物.csv" as line
create (:Lianhuachi {name: line[0]})
最终效果图:
MATCH (n:Lianhuachi) RETURN n LIMIT 25
人物关系创建
去种田的向凹凸,小白兔的胡萝卜甜(冉甜),夫妻
向云朵,小白兔的胡萝卜甜(冉甜),女儿
向云朵,去种田的向凹凸,女儿
我是野农,画燕儿,夫妻
向鹿鸣,画燕儿,儿子
向鹿鸣,我是野农,儿子
路人阿丙X,大明星(冉蜜),夫妻
向偶然,大明星(冉蜜),女儿
向偶然,路人阿丙X,女儿
荷塘星星,多肉葡萄肉多多,夫妻
向尘俊,荷塘星星,儿子
向星言,荷塘星星,女儿
向尘俊,多肉葡萄肉多多,儿子
向星言,多肉葡萄肉多多,女儿
三叔,荷塘星星,父亲
守山人阿亮,荷塘星星,表弟
导入数据:
load csv from "file:///莲花池人物关系.csv" as line
create (:LianhuachiRelation {from:line[0],relation:line[2],to:line[1]})
最终效果图:
MATCH (n:LianhuachiRelation) RETURN n LIMIT 25
查询人物关系
match (n:Lianhuachi {name: "向云朵"}),(m:LianhuachiRelation) where m.from='向云朵'
return n.name, m.relation, m.to
Read More ~
Java8 使用sun.tools.javadoc 读取注释信息
前言
项目需要整理所有依赖的外部Feign调用,整理成一份文档,类似于:
远程接口
调用位置
作用
xxx/zzz
top.imyzt.xxx#methodName
用作xxx
方案
因为项目过多,且依赖的外部服务接口众多,不想一个个去整理,故想直接读取所有的Feign接口,直接扫描出方法注释,将上述表格完成。
Java是编译型语言,当代码从.java编译成.class后,代码中的注释将会清空,所以说,项目打包之后,就无法再进行注释的解析了,所以反射等方案不可行,通过在Google搜索,发现了com.sun.tools.javadoc.Main工具类,可以直接读取文件的形式读取.java文件,然后解析其中的各类注释信息,使用起来也很简单,下面举个例子:
/**
* 类注释
* @author imyzt
* @date 2024/04/10
*/
public class Demo {
/**
* 方法注释
*/
public void demo() {
}
}
上面是一个简单的带注释的类,通过sun公司的工具类,可以直接读取:
public class Doclet {
public static Logger logger = LoggerFactory.getLogger(Doclet.class);
private static RootDoc rootDoc;
private final String clsFilePath;
public static boolean start(RootDoc root) {
rootDoc = root;
return true;
}
public Doclet(String clsFilePath) {
this.clsFilePath = clsFilePath;
}
public void exec() {
com.sun.tools.javadoc.Main.execute(
new String[]{"-doclet", Doclet.class.getName(),
"-docletpath", Doclet.class.getResource("/").getPath(),
"-encoding", "utf-8",
clsFilePath});
ClassDoc[] classes = rootDoc.classes();
if (classes == null || classes.length == 0) {
logger.warn(clsFilePath + " 无ClassDoc信息");
return;
}
ClassDoc classDoc = classes[0];
// 获取类的名称
System.err.println("类名:" + classDoc.name());
// 获取类的注释
String classComment = Reflect.on(classDoc).field("documentation").get().toString();
System.err.println("类注释:" + classComment);
// 获取属性名称和注释
for (FieldDoc field : classDoc.fields(false)) {
System.err.printf("属性名:%s, 属性类型:%s, 注释:%s%n", field.name(), field.type().typeName(), field.commentText());
}
for (MethodDoc method : classDoc.methods(false)) {
System.err.printf("方法名:%s, 方法返回类型:%s, 注释:%s%n", method.name(), method.returnType().typeName(), method.commentText());
}
}
}
最终的运行效果:
public class CommentReader {
public static void main(String[] args) {
Doclet doclet = new Doclet("/Users/.../java-reader-classormethod-comment/src/main/java/top/imyzt/learing/readercomment/Demo.java");
doclet.exec();
// 正在构造 Javadoc 信息...
// 类名:Demo
// 类注释: 类注释
// @author imyzt
// @date 2024/04/10
//
// 方法名:demo, 方法返回类型:void, 注释:方法注释
}
}
通过这个工具类,可以快速的读取,解析各类注释信息,进行资料的整理。
工具类还有很多其他的API,网上资料也很多,这里只是记录做一个备忘,具体的使用时进行Google搜索即可。
示例代码
java-reader-classormethod-comment
Read More ~
Neo4j 使用
图数据模型:与关系型数据库使用表格存储数据不同,图数据库通过节点(Node)和关系(Relationship)来表示数据和它们之间的联系。
节点:代表实体,如人、地点、物品等。
关系:定义节点之间的连接,可以有方向和属性。
属性:节点和关系的附加信息,如人的姓名、年龄等。
CREATE (person1:Person {name: 'Alice', age: 30})
CREATE (person2:Person {name: 'Bob', age: 25})
CREATE (person1)-[:KNOWS]->(person2)
person1/person2:节点
Person:标签
name/age:属性
KNOWS:关系
导入数据
通过GPT,生成《西游记》的人物关系图和人物名单,案例数据如下:
将文件放置于Neo4j的Home/import目录下,然后执行导入命令:
load csv from "file:///西游记.csv" as line
create (:xiyouRelation {from:line[1], relation:line[3],to:line[0]})
load csv from "file:///人物.csv" as line
create (:person {name:line[0]})
执行match (person) return person,查看数据:
创建人物和关系
创建人物和关系
创建人物
create (:student {name: '小明'}),(:student {name : '小红'}),(:student {name: '小李'})
创建人物关系,并且返回人物关系
match (n:student {name: '小明'}),(m:student {name: '小红'})
create (n)-[r:同学]->(m) return n.name,type(r),m.name
显示人物关系
MATCH p=()-[r:`同学`]->() RETURN p LIMIT 25
删除标签
match (n:LianhuachiPerson)
detach delete n
Read More ~
花市
-- 2024-03-24 摄于荷兰花卉小镇
Read More ~
业务逻辑编排错误 & TTL浅拷贝导致参数丢失问题
前言
在DDD项目中,为了方便参数的传递,通常会使用ThreadLocal来保存一个对象来实现对参数的跨方法传递,避免通过形参的形式传递。在内部项目中,有一个项目使用的是 alibaba开源的 transmittable-thread-local来存储参数,新建了一个上下文对象(AbilityContext.java),使用HashMap来临时存储和获取参数。
AbilityContext 示例
public class AbilityContext {
private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>();
private AbilityContext() {
}
/**
* 初始化上下文
*/
public static void initContext() {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
CONTEXT.set(new HashMap<>(8));
} else {
CONTEXT.get().clear();
}
}
/**
* 清除上下文
*/
public static void clearContext() {
CONTEXT.remove();
}
public static Map<String, Object> getInnerMap() {
return CONTEXT.get();
}
/**
* 获取上下文内容
*/
public static <T> T getValue(String key) {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
return null;
}
return (T) con.get(key);
}
/**
* 设置上下文参数
*/
public static void putValue(String key, Object value) {
Map<String, Object> con = CONTEXT.get();
if (con == null) {
CONTEXT.set(new HashMap<>(8));
con = CONTEXT.get();
}
con.put(key, value);
}
}
项目情况介绍
通常来说,DDD项目的基本流程是由interface->application,中间封装一层来集中处理上下文的初始化和清空动作,如下图:
在正常情况下,上述流程可以正确的完成参数的写入和获取,但是,在项目运行过程中遇到了一个bug,正常写入参数后,偶现性(低频)获取值为NULL,导致程序出错,示例代码如下(隐去业务代码,重新写的伪代码):
其中demo()方法为当时复现的方法
demo2()为伪代码,是业务代码中调用了另一个application,假设其逻辑和demo()方法一致的业务代码。
@Slf4j
public class AlibabaTtlWrongUsageExampleApplication {
public static void main(String[] args) {
demo(i);
}
private static void demo(int idx) {
// 初始化
AbilityContext.initContext();
// 赋业务值
AbilityContext.putValue("main", "mainValue");
// 这里简化了代码,实际上经过了很多层业务代码调用后才出现了此方法
ThreadUtil.execute(() -> {
execute->demo2();
});
// do something
// 主线程再次获取业务值(偶现为null)
String value = AbilityContext.getValue("main");
if (Objects.isNull(value)) {
log.warn("lastGetNullValue, idx={}", idx);
}
}
}
上述代码运行设置了一个key=main,值为mainValue。在下方AbilityContext.getValue("main")偶现获取==NULL。
展开分析
当时在分析的开始有推测是业务代码中参数被重新赋值为NULL,但通过对后续业务代码逐行查看,并没有找到重新赋值的逻辑。
在深入业务代码分析的过程中,发现主流程中有一个异步方法调用(ThreadUtil.execute()),再次调用了另一个领域服务(这是不符合DDD规范的!),而领域服务的入口都会AbilityContext.initContext()的逻辑,通过这个线索 ,继续展开了深入分析。
编码者的初衷可能是想到异步线程已经脱离了当前线程,再次调用 initContext()方法是初始化了一个新的对象上下文,但是由于项目使用的是 alibaba TTL,能够实现跨线程的传递,所以在子线程中依旧能拿到父线程的HashMap。并且TTL默认是使用的浅拷贝对象。由于initContext()中,调用了HashMap.clear()方法,相当于将父线程的HashMap给清空了!。
通过比对父子线程的hashCode值确定为同一对象
// 主线程获取hashCode
final int hashCode = AbilityContext.getInnerMap().hashCode();
ThreadUtil.execute(() -> {
// 子线程对比hashCode
log.info("{}, ThreadUtil hashCode={}", idx, AbilityContext.getInnerMap().hashCode() == hashCode);
// 子线程再次初始化(错误的根源)
AbilityContext.initContext();
// do something
});
14:42:28.198 [pool-1-thread-26] INFO top.imyzt.learning.caseanalysis.ttl.AlibabaTtlWrongUsageExampleApplication -- 25, ThreadUtil hashCode=true
持续分析
有了上述的线索,基本把问题原因找到了,但是为什么是偶现的呢?
因为使用了异步线程,而线程的调度由操作系统的线程调度算法来决定,并不是一定保证顺序的,所以只要当操作系统优先调度异步线程,那么HashMap就被清空了,如果主线程优先往下走,那么就能够获取到完整的HashMap。
后记
至此,问题分析就告一段落了,整个过程中涉及到 TTL值的父子线程传递、对象浅拷贝、线程的调度,还涉及到了DDD的不规范逻辑编排,整个分析下来花费了一上午的时间,收获还是很大的。
transmittable-thread-local
TransmittableThreadLocal的传递只有浅拷贝吗?
线程的优先级
我将源代码上传了GitHub,如果你想在本地调试运行上述案例,可以下载到本地调试,有问题可以评论区沟通。
Read More ~
使用Redis实现分布式锁的坑
分布式锁的关键在于对单一资源的竞争。获得资源的实例将继续执行,其余实例要么退出(互斥锁),要么等待(阻塞锁)。
实现分布式锁的方案有很多,既可以直接使用MySQL作为分布式锁(例如xxl-job),也可以利用ZooKeeper、Redis等。
在基于Spring Cloud的业务系统中,一般都会引入Redis作为分布式缓存中间件,因此更多的人会选择使用Redis来实现分布式锁。本文将介绍使用Redis作为分布式锁时常见的问题和解决方法。
1. 没有使用原子操作指令
错误写法
Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
stringRedisTemplate.expire(lockKey, Duration.ofSeconds(expireTime));
if (!tryLock) {
return;
}
上述操作通常出现在新手阶段,在写入锁对象时,没有考虑到原子性问题。在Redis中有提供SET NX PX指令,支持在设置锁的同时指定过期时间,并且支持原子性判断key是否已存在。
NX 和 PX 是 Redis 命令中用于设置 key 的两个选项。
NX: 当指定 NX 选项时,只有在 key 不存在的情况下才会设置 key 的值。如果 key 已经存在,则不进行任何操作。
PX: PX 选项用于设置 key 的过期时间(以毫秒为单位)。例如,PX 10000 表示在 10 秒后将 key 设置为过期状态。
正确写法:
Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
2. 释放了别人的锁
错误写法
try {
Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
if (!tryLock) {
return;
}
// do something
} finally {
stringRedisTemplate.delete(lockKey);
}
在加锁的过程中,没有设定唯一值作为Value存储到Redis中,在释放时,不判断直接对锁进行释放。其二,将获取锁的代码放在了try代码块中。
在上述代码中存在两个问题:
不该执行到finlly代码块:A请求获得了锁正在执行业务代码,而B请求没有获得锁,但是因为获取锁的代码在try代码块中,导致finally一定会执行,B请求就会将A请求的锁释放,而如果A请求依旧未执行完毕,此时C请求过来时,则C请求错误的拿到了锁。
不该删除别人的锁:在删除锁时,应该判断自己是否是上锁人,由于多次执行Redis指令不具备原子性,所以一般是交由LUA脚本来实现的。
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('del', KEYS[1])
else
return 0
end
正确写法
提前将LUA脚本载入到Redis服务端
script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua")));
获取和释放锁示例
Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS);
if (!tryLock) {
return;
}
try {
// do something
} finally {
ArrayList<String> keys = new ArrayList<>();
keys.add(context.getLockKey());
stringRedisTemplate.execute(this.script, keys, context.getLockValue());
}
3. 事务未提交锁就释放了
错误代码
/**
* 事务内获取分布式锁
*/
@Transactional(rollbackFor = Exception.class)
public void saveUserWithDistributedLock(String name) {
String lockKey = "lock_key:" + name;
RedisLock.LockContext lockContext = redisLock.tryLock(lockKey, 10000L);
if (!lockContext.getTryLock()) {
// printLog("没拿到锁");
return;
}
printLog("拿到锁了" + lockKey);
try {
this.save(name);
} finally {
redisLock.release(lockContext);
printLog("释放锁了");
}
}
MySQL常规情况下是RR的隔离级别,只有等到事务提交数据才对其他事务可见,存在**“读视图”,在上述的代码中,A请求拿到了锁执行了业务代码,执行到redisLock.release时将锁释放了,但Spring的@Transactional依赖的是AOP,其需要等到方法执行完毕才会提交事务,在这个临界点,B请求可以正常拿到锁,但是A请求的事务还未提交,B请求的读视图**中还未查询到A请求提交的数据,最终造成了数据的不一致性。
正确代码
正确的情况是在另一个方法中获取到锁之后,再调用包含事务的业务代码。此时需要注意SpringAOP在本方法内代理失效的问题,通常需要新建一个Service来处理。
业务代码执行超过锁过期时间
错误代码
// Domain-Service
public void save(String name) {
String lockKey = "lock_key:" + name;
RedisLock.LockContext lockContext = redisLock.tryLock(lockKey, 10000L);
if (!lockContext.getTryLock()) {
printLog("没拿到锁");
return;
}
printLog("拿到锁了" + lockKey);
try {
userService.save(name);
} finally {
redisLock.release(lockContext);
printLog("释放锁了");
}
}
// UserService
@Transactional(rollbackFor = Exception.class)
public void save(String name) {
List<User> users = userRepository.findUsersByName(name);
if (CollUtil.isNotEmpty(users)) {
printLog("已经写入, 不再写入" + users);
return;
}
// 业务保存模拟执行很慢
TimeUnit.SECONDS.sleep(70);
}
上述代码中,锁对象只有10s的时间,但是业务代码执行却需要70s,A请求虽然拿到了锁,此时后续10秒其他请求均无法获取锁,但是从第11秒开始的请求将可以拿到锁,而此时A请求还未执行完毕,此时开始出现错误的获取锁,最终造成数据的不一致。
正确写法
参考Redisson的WatchDog机制,另外开辟线程每隔 10s 就给还未执行完毕的 Key 自动续期 30s,保证业务代码能够安全的执行完毕再自行释放锁对象。
示例代码:
// watch dog
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
if (!LOCK_CONTEXTS.isEmpty()) {
for (LockContext lockContext : LOCK_CONTEXTS) {
// 如果执行线程还未释放锁, 续期30s(模拟Redisson)
stringRedisTemplate.expire(lockContext.getLockKey(), Duration.ofSeconds(30));
Long expire = stringRedisTemplate.getExpire(lockContext.getLockKey());
log.info("WatchDog, expire 30s, lockKey={}, ttl={}", lockContext.getLockKey(), expire);
}
}
}, 0,
// 10秒检测一次
10, TimeUnit.SECONDS);
后记
分布式锁的错误还有很多,本篇主要是自己在工作过程中遇到的一些坑,着重介绍新手阶段在编写分布式锁时遇到的比较基础的问题,后面有空再进行其他场景的逐个介绍。
本文参考:聊聊redis分布式锁的8大坑
本文代码:redis-lua-distributed-lock
Read More ~
错用HashedWheelTimer导致的OOM问题
事件中心在私有化环境下,只要server一启动过几秒就oom,查看日志是 Failed to create a thread: retVal -1073741830, errno 11。
异常堆栈:
Caused by: java.lang.OutOfMemoryError: Failed to create a thread: retVal -1073741830, errno 11
at java.lang.Thread.startImpl(Native Method)
at java.lang.Thread.start(Thread.java:993)
at io.netty.util.HashedWheelTimer.start(HashedWheelTimer.java:366)
at io.netty.util.HashedWheelTimer.newTimeout(HashedWheelTimer.java:447)
at 业务调用代码省略
在标品环境下没有问题,在其他KA客户上也没有问题
通过对日志的分析,最终发现是事件中心的延迟消息代码存在缺陷,使用了Netty的HashedWheelTimer,但是语法存在问题,理论上应该是new一个HashedWheelTimer来处理所有时间延迟,但是错用程每次new一个新的HashedWheelTimer,HashedWheelTimer内部每次都会new一个新的线程来处理做调度,一个线程占用1MB,最终内存资源被耗尽。
io.netty.util.HashedWheelTimer#HashedWheelTimer源码:
public HashedWheelTimer(
ThreadFactory threadFactory,
long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection,
long maxPendingTimeouts, Executor taskExecutor) {
checkNotNull(threadFactory, "threadFactory");
checkNotNull(unit, "unit");
checkPositive(tickDuration, "tickDuration");
checkPositive(ticksPerWheel, "ticksPerWheel");
this.taskExecutor = checkNotNull(taskExecutor, "taskExecutor");
// Normalize ticksPerWheel to power of two and initialize the wheel.
wheel = createWheel(ticksPerWheel);
mask = wheel.length - 1;
// Convert tickDuration to nanos.
long duration = unit.toNanos(tickDuration);
// Prevent overflow.
if (duration >= Long.MAX_VALUE / wheel.length) {
throw new IllegalArgumentException(String.format(
"tickDuration: %d (expected: 0 < tickDuration in nanos < %d",
tickDuration, Long.MAX_VALUE / wheel.length));
}
if (duration < MILLISECOND_NANOS) {
logger.warn("Configured tickDuration {} smaller then {}, using 1ms.",
tickDuration, MILLISECOND_NANOS);
this.tickDuration = MILLISECOND_NANOS;
} else {
this.tickDuration = duration;
}
// 每次都new一个线程来处理
workerThread = threadFactory.newThread(worker);
leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null;
this.maxPendingTimeouts = maxPendingTimeouts;
if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT &&
WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) {
reportTooManyInstances();
}
}
因为标品和其他KA使用的是阿里云RocketMQ,此客户使用的是自建的开源版RocketMQ,开源RocketMQ是没有自定义时长的延迟消息的,所以我们自己实现了一套时间轮来实现任意时长的延迟消息,当小于60s的延迟消息会丢入我们的时间轮来处理延迟投递,当时此客户的环境中有大量的60s内的延迟消息,导致一启动就会崩溃。
不过在RocketMQ5.0也支持任意时长了。
Read More ~
CircuitBreak导致ThreadLocal参数丢失问题
背景
使用OpenFeign时,通常会实现RequestInterceptor接口来自定义FeignConfiguration,OpenFeign暴露了feign.RequestTemplate信息,给到我们在发送请求前自定义参数信息的扩展点。
在分布式系统中,通常会将本服务的信息(UserInfo、RequestId)透传至下游服务,从而实现分布式链路追踪等功能,对于像用户信息等,在Web系统中通常使用 ThreadLocal 来存储信息,在自定义的FeignConfiguration中获取ThreadLocal再塞入到feign.RequestTemplate中,实现向下游服务的传递,示例:
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String userId = SubjectContext.get().getUserId();
if (null != attributes) {
HttpServletRequest request = attributes.getRequest();
template.header("token", request.getHeader("TOKEN"));
template.header("userId", userId);
}
}
}
简单的Context示例:
public class SubjectContext {
protected static ThreadLocal<UserInfo> subjectContext = new ThreadLocal();
public static void remove() {
subjectContext.remove();
}
public static void set(UserInfo uerInfo) {
subjectContext.set(uerInfo);
}
public static UserInfo get() {
return (UserInfo)subjectContext.get();
}
}
出现错误
上述代码在常规情况下,是能够按照预期执行的。
但是最近项目引入了CircuitBreaker作为服务熔断的断路器之后,上述代码在执行到SubjectContext.get()时,会抛出空指针,拿不到用户信息。
通过分析CircuitBreaker的源码,最终定位到代码出现在Resilience4JCircuitBreaker内部,在Resilience4JCircuitBreaker中有一个public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback)方法,方法入参的toRun就是封装过的我们定义的Feign接口,其包装过程在FeignCircuitBreakerInvocationHandler#asSupplier代码中,如下:
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
return () -> {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
// 执行我们的真正方法
return dispatch.get(method).invoke(args);
}
catch (RuntimeException throwable) {
throw throwable;
}
catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
Spring Cloud CircuitBreaker Resilience4j 提供了两种实现:
使用 Semaphores 的 SemaphoreBulkhead。
一个 FixedThreadPoolBulkhead,它使用一个有界队列和一个固定的线程池。
默认情况下,Spring Cloud CircuitBreaker Resilience4j 使用 FixedThreadPoolBulkhead。要修改默认行为以使用 SemaphoreBulkhead,请将属性 spring.cloud.circuitbreaker.resilience4j.enableSemaphoreDefaultBulkhead 设为 true。
正是由于上述原因,默认将我们的FeignConfiguration提交给了线程池,由于我们使用的是ThreadLocal导致线程本地变量没有向子线程传递,在执行FeignConfiguration时子线程无法拿到Context信息,最终导致程序的报错。
解决办法
通过分析源码我们发现,执行任务的线程池Resilience4JCircuitBreaker#executorService是由外部传递过来进行初始化的,调用方在Resilience4JCircuitBreakerFactory#create(java.lang.String, java.lang.String, java.util.concurrent.ExecutorService)
在Resilience4JCircuitBreakerFactory中发现,是由本实例在create方法被调用时传入的本类的成员变量,即:
private ExecutorService executorService = Executors.newCachedThreadPool();
private ConcurrentHashMap<String, ExecutorService> executorServices = new ConcurrentHashMap<>();
而我们在没有定义自定义Feign Group时,默认使用的就是executorService,在本类中有一个Resilience4JCircuitBreakerFactory#configureExecutorService方法专门保留了外部传入自定义线程池的扩展,我们可以自己实现创建一个支持传递Context到子线程的线程池,即可将参数向下传递,比如像这样:
@Configurable
@AllArgsConstructor
public class CircuitBreakerConfiguration implements ApplicationRunner {
private final Resilience4JCircuitBreakerFactory factory;
@Override
public void run(ApplicationArguments args) throws Exception {
ContextThreadPoolExecutor contextThreadPoolExecutor =
new ContextThreadPoolExecutor(2, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024));
// **change ThreadPoolExecutor**
factory.configureExecutorService(contextThreadPoolExecutor);
}
public static class ContextThreadPoolExecutor extends ThreadPoolExecutor {
public ContextThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public void execute(Runnable command) {
super.execute(wrap(command));
}
private static Runnable wrap(Runnable runnable) {
**SubjectContext context = SubjectContext.getContext();**
return () -> {
// 将参数向下传递
**SubjectContext.setContext(context);**
try {
runnable.run();
} finally {
**SubjectContext.clear();**
}
};
}
}
}
后记
上述的方案只解决了没有自定义Group的情况,官方在自定义Group的情况下是没有保留扩展位的,所以给官方提了一个MR并且已成功合并到主分支,如下:
Customizable groupExecutorService #180
Read More ~
IDEA 常用插件
编码类
Generate All Getter And Setter 如其名
AceJump 快速将光标导航到编辑器中可见的任何位置,ctrl+,然后输入字符,进行高亮定位
GsonFormat 将JSONObject格式的String 解析成实体。
Lombok 针对模型对象精简代码
MybatisCodeHelperPro 通过方法名来生成sql,全自动提示,自动检测
工具类
any-rule 可以搜索正则表达式
CamelCase 变量名转驼峰
Jrebel 快速实现热部署,跳过了构建和部署的过程,可以省去大量的部署用的时间。
jrebel-mybatisplus-extension 热部署中mybatis的扩展,他的功能就是配合 Jrebel 一起使用,修改 mybatis 的 mapper.xml 文件不用重启项目
CodeGlance 显示代码地图插件,进行语法突出显示
RestfulToolKit 可以搜索接口
POJO TO JSON 将pojo类转换为json
Translation 翻译插件
Maven类
Maven Helper 依赖冲突分析利器
Maven Dependency Helper Maven中央仓库搜索
规范类
P3C 编码规范的一个插件,高亮提示违规代码
SonarLint 打开文件的时候自动进行代码审查
提效类
Key Promoter X 在IDEA里面使用鼠标的时候,如果这个鼠标操作是能够用快捷键替代,此插件会进行提示
POJO TO JSON 将pojo类转换为json
装修类
Nyan Progress Bar 漂亮的进度条
Rainbow Brackets 配对括号相同颜色,并且实现选中区域代码高亮的功能
Read More ~
IDEA 2023 非常用配置
配置文件全部使用UTF-8
Editor-File Encodings,将所有内容均设置为UTF-8。
Create UTF-8 files 选择 with NO BOM,避免在创建文件前3个字节来标志为UTF-8文件。
参数提示
默认情况下,是没有参数提示的,需要手动开启。
Editor-General-Code Completion-Parameter Info
注释默认在当前缩进
默认情况下,自动补全注释是在最前面,可以通过:
Editor-Code Style-Java(其它语言位置类似)-Code Generation-Comment Code按如下图配置进行处理。
如果配置的内容,在新项目不生效,则注意此处
除了字体样式字体大小外,其它的配置IDEA官方认为,每个项目在创建时,可能都需要一些不同的配置,所以有很多内容都是跨项目不生效的(Maven、文件格式等),需要在此处配置(点进去和Settings一模一样,但是对应的是Next Project(新项目)的配置)
Read More ~