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) 导入数据 通过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
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 ~

使用highlightjs让Gridea支持代码高亮

Gridea的Lemon主题,默认不支持代码高亮,需要自己补充。目前开源的highlight.js正合适。记录下处理的过程 打开官网,挑选自己喜欢的风格 highlightjs.org/examples 找一个CDN托管网站,避免走自己的小油管,这里推荐bootcdn(这么多年了,还是如此坚挺) www.bootcdn.cn/highlight.js 将上述找到的代码,应用在自己主题下的post.ejs的 <head> 代码块中。 1. ~/Gridea/themes/gridea-theme-lemon,找到自己的主题目录,gridea-theme-lemon是我的Lemon主题目录。 2. 编辑post.ejs文件,vim templates/post.ejs 3. 在<head>中补充如下代码 <link href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.8.0/styles/此处选择自己喜欢的主题.min.css" rel="stylesheet"> <script src="https://cdn.bootcss.com/highlight.js/9.12.0/highlight.min.js"></script> <script>hljs.initHighlightingOnLoad();</script>
Read More ~