使用Neo4j建立莲花池人物关系图(核心人物,简版)

最近关注了沅陵的莲花池,整个村子都是钓鱼佬,人均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,妻子,大明星(冉蜜) 大明星(冉蜜),女儿,向偶然 路人阿丙X,女儿,向偶然 多肉葡萄肉多多,丈夫,荷塘星星 荷塘星星,妻子,多肉葡萄肉多多 荷塘星星,儿子,向尘俊 多肉葡萄肉多多,儿子,向尘俊 荷塘星星,女儿,向星言 多肉葡萄肉多多,女儿,向星言 向星言,亲哥,向尘俊 向尘俊,亲妹,向星言 荷塘星星,父亲,三叔 三叔,儿子,荷塘星星 荷塘星星,表哥,守山人阿亮 守山人阿亮,表弟,荷塘星星 我是野农,亲弟,路人阿丙X 路人阿丙X,亲哥,我是野农 我是野农,钓友,去种田的向凹凸 去种田的向凹凸,钓友,我是野农 去种田的向凹凸,同村,荷塘星星 我是野农,钓友,荷塘星星 荷塘星星,钓友,我是野农 去种田的向凹凸,同村,荷塘星星 导入数据: load csv from "file:///莲花池人物关系.csv" as line create (n:LianhuachiRelation {from: line[0], relation: line[1], to: line[2]}) return n.from, n.relation, n.to 最终效果: MATCH (n:LianhuachiRelation) RETURN n.from, n.relation, n.to 借助人物关系标签,创建人物关系 match (f:Lianhuachi),(r:LianhuachiRelation),(t:Lianhuachi) where f.name=r.from and t.name=r.to create (f)-[rr:莲花池人物关系 {relation: r.relation}]->(t) return f.name, rr.relation, t.name 最终效果: MATCH p=()-[r:莲花池人物关系]->() RETURN p LIMIT 25 查询人物关系 match (n:Lianhuachi {name: "向云朵"}),(m:LianhuachiRelation) where m.from='向云朵' return n.name, m.relation, m.to 在现有的基础上,补充人物关系 match (n:Lianhuachi{name:"大明星(冉蜜)"}),(m:Lianhuachi{name:"小白兔的胡萝卜甜(冉甜)"}) create (n)-[r:`莲花池人物关系`{relation:"亲姐"}]->(m) match (n:Lianhuachi{name:"大明星(冉蜜)"}),(m:Lianhuachi{name:"小白兔的胡萝卜甜(冉甜)"}) create (m)-[r:`莲花池人物关系`{relation:"亲妹"}]->(n) 删除重建人物关系 MATCH (n:Lianhuachi {name:"去种田的向凹凸"})-[r:`莲花池人物关系` {relation: "钓友"}]->(m:Lianhuachi {name:"我是野农"}) delete r MATCH (n:Lianhuachi {name:"我是野农"})-[r:`莲花池人物关系` {relation: "钓友"}]->(m:Lianhuachi {name:"去种田的向凹凸"}) delete r MATCH (n:Lianhuachi {name:"去种田的向凹凸"}),(m:Lianhuachi {name:"我是野农"}) create (n)-[r2:`莲花池人物关系` {relation: "表哥"}]->(m) create (m)-[r3:`莲花池人物关系` {relation: "表弟"}]->(n) return r2.relation,r3.relation
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 删除关系 命令: match (n)-[r:西游人物关系]-(s) delete r 结果: Deleted 18 relationships, completed after 6 ms.
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 ~