未整理的文件
在登录拦截器中封装全局上下文,基于ThreadLocal实现线程级用户身份共享,减少重复查询
- 为什么要用 ThreadLocal? 不用它会有什么问题?(回答重点:线程隔离、减少参数透传)
- ThreadLocal 的底层原理是什么?(回答重点:Thread 内部的
ThreadLocalMap,Key 是弱引用)
ThreadLocal 为什么会内存泄露? 你在项目中是怎么处理的?
为什么 Key 要设计成弱引用? 如果是强引用会怎样?
如果 Key 是强引用,即使业务代码里已经把
ThreadLocal变量设为null(不再使用了),但因为当前线程的ThreadLocalMap还持有它的强引用,这个ThreadLocal对象就永远无法被回收,导致内存泄露。Value 是业务真正需要的数据。如果 Value 也是弱引用,可能你刚存进去,还没来得及用,就被 GC 回收了。
拦截器中
afterCompletion的作用是什么? 为什么必须在这里调用.remove()?
如果是异步编排(CompletableFuture)或开启了新线程,上下文还能拿到吗?( 阿里开源的
TransmittableThreadLocal)在高并发下,如果使用线程池,ThreadLocal 会有什么坑?(回答重点:线程复用导致数据错乱)
线程池中的线程是复用的。如果线程 A 完成了请求但没有清理
ThreadLocal,当请求 B 进来复用了这个线程时,ReqInfoContext.getReqInfo()拿到的可能是线程 A 的用户信息。
确认以下细节:
- 拦截器的注册顺序: 确认你的
LoginInterceptor是否在其他需要用户信息的拦截器之前执行。 - ThreadLocal 的封装: 是否定义了一个
UserHolder或BaseContext工具类?里面是否有remove()方法? - remove() 的调用时机: 确认
remove()是写在拦截器的afterCompletion(视图渲染后)还是postHandle(方法执行后)?(正确做法是afterCompletion,确保异常情况下也能释放)。 - 用户信息字段: 你存的是完整的
UserDTO还是仅仅一个UserId?(建议存精简后的 DTO,减少内存占用)。
“在项目中,很多业务模块(如订单、收藏、评论)都需要获取当前登录用户的信息。如果每次都从 Session 或 Redis 里查,或者在 Controller 到 Service 的每一层都手动传 userId 参数,代码会非常臃肿且增加数据库压力。所以我设计了一个登录拦截器+ThreadLocal的方案。”
“具体流程是:当请求到达后端,拦截器会先校验 Token。校验通过后,解析出用户信息,并存入自定义的 UserContext 容器中。
这样在整个请求的下游(Service、Mapper、甚至工具类)中,只需要通过 UserContext.getUser() 就能直接获取当前线程绑定的用户信息,实现了线程级隔离。”
“在使用过程中我也重点考虑了内存泄露问题。因为 Web 服务器通常使用线程池,线程是循环利用的。如果请求结束后不手动清理,该线程被复用时不仅会造成数据错乱,还会导致 Entry 对象无法被回收。
因此,我利用拦截器的 afterCompletion 回调,在请求结束后的最后阶段强制调用 threadLocal.remove(),确保及时清理内存。”
构建Caffeine+Redis 多级缓存体系,提升首页流媒体、专栏等高频热点数据的访问吞吐能力,首页响 应耗时降低至80ms以内
- 你是如何保证 Caffeine、Redis 和数据库三者数据一致的?(先删缓存还是后删?本地缓存怎么同步更新?)
- 如果某个节点修改了数据,其他服务器节点的本地缓存(Caffeine)如何失效?(回答重点:引入消息队列如 Redis Pub/Sub 或 RocketMQ 进行广播)。
为什么要用 Caffeine 而不是直接用 Guava 或 HashMap?
- HashMap 没有淘汰策略,会撑爆内存;
- Guava 采用的是 LRU(最近最少使用)算法,容易被突发性的‘冷数据扫库’污染。
- Caffeine 使用了 W-TinyLFU 算法:它不仅看数据是不是刚被访问(Recency),还会记录数据访问的频次(Frequency)。
简单说:它能精准识别出哪些是真正的‘热数据’,哪些是‘偶尔被看一次’的干扰数据。这让首页这种热点集中的场景,命中率比 Guava 高出一截。”(踢人的时候先踢最不热的数据)
你的多级缓存查询流程是怎样的?(本地 -> 分布式 -> DB)。
本地缓存设多大?过期策略是什么?(回答重点:根据热点数据量和内存监控来定)。
1 | /** |
什么是“缓存击穿”? 如果首页某个热点专栏突然失效,大量请求压向 DB 怎么办?(回答重点:逻辑过期、互斥锁/双检锁)。
如何防止本地缓存把内存撑爆?(
Hard Limit(硬限制): 通过
maximumSize或maximumWeight严格限制缓存个数/字节大小。一旦触达,立即触发淘汰。Soft Values(软引用): 配置 Caffeine 使用
softValues()。这意味着当 JVM 内存极其紧张、即将发生 OOM 之前,GC 会强行回收这些缓存对象来保命。
80ms 是怎么测出来的? 优化前是多少?
除了 F12 查看 Network 面板的响应时间,我还使用了 JMeter 进行压测,对比优化前后的 P99(99% 的请求耗时)。
优化前后的对比(建议数值):
优化前: 大约在 300ms - 500ms 左右。因为首页侧边栏、推荐位需要多次查询数据库,涉及多次网络 IO。
优化后: 稳定在 80ms 以内。因为侧边栏等高频数据直接命中本地内存,几乎是零 IO 开销。
Caffeine 配置类: 看一下你设置的 initialCapacity(初始容量)、maximumSize(最大容量)和 expireAfterWrite(过期时间)的具体数值。
查询逻辑代码: 确认是否使用了 Cache.get(key, k -> value) 这种自动回源的写法,还是手动写的 if(null) { ... }。
消息通知机制: 检查项目中是否有处理“缓存同步”的代码。如果没有,面试时要准备好“如果需要同步,我会怎么做”的方案。
序列化方式: Redis 存的是 JSON 还是 JDK 序列化?这直接影响到响应耗时。
“首页流媒体和专栏属于高频热点且读多写少的数据。单纯依赖 Redis 仍会有网络 IO 开销和反序列化耗时。为了极致提升吞吐量,我采用了 L1(本地缓存 Caffeine)+ L2(分布式缓存 Redis) 的架构。
这样,绝大部分请求在 JVM 进程内就能直接返回,避免了网络波动对响应耗时的影响。”
“在查询时,程序先尝试从 Caffeine 中读取。如果缺失,再穿透到 Redis。如果 Redis 也没有,则加锁查询数据库并双写回缓存。 为了保证‘80ms’的指标,我对首页数据做了精简(DTO 瘦身),并使用 Protobuf 或 Jackson 优化了序列化性能。通过 JMeter 压测,在高并发下响应耗时从原来的 300ms+ 稳定下降到了 80ms 以内。”
“多级缓存最大的挑战是数据一致性。在我的方案中,对于一致性要求极高的数据,我采用了 Redis Pub/Sub 机制:当某个实例更新了 DB,会向特定的 Channel 发送通知,其他所有服务实例收到通知后立即失效本地的 Caffeine 缓存,从而确保用户看到的是最新数据。”
项目亮点话术(其实这里是filter,我不知道怎么说)
在登录认证这块,我做了一个全局上下文封装。具体是把 JWT 校验前置到拦截器里,在请求进入 Controller 之前统一解析 token,提取用户 id,然后通过
ThreadLocal存入BaseContext。这样在后续 Controller、Service、持久层扩展逻辑里,都可以直接获取当前登录用户,不需要每个接口重复解析 token 或重复查用户信息。
这样做的价值主要有三个:
第一,统一身份获取入口,减少重复代码;
第二,基于线程隔离实现当前请求内的用户身份共享,调用链传递更方便;
第三,降低了业务层对HttpServletRequest的依赖,提高了代码可维护性。
当然这套方案的前提是同步请求链路,并且必须在请求结束后及时清理ThreadLocal,避免线程池复用带来的脏数据问题。
1)你为什么要在登录拦截器里封装全局上下文?
主要是为了解决“当前登录人信息在业务链路里重复获取”的问题。
如果不做这层封装,很多接口都要重复从请求头拿 token、解析 JWT、再提取 userId,甚至有的地方还会再查一次用户信息,这样会带来三类问题:
- 重复代码多,认证逻辑分散
- 业务层依赖请求对象,不够解耦
- 容易出现有的接口校验了,有的接口忘记校验,安全边界不一致
所以我把认证逻辑收敛到拦截器中,把“当前用户是谁”作为一个请求级上下文统一封装。后续代码只要关心业务,不需要关心用户身份是怎么拿到的。
你可以顺带结合项目举例:
比如地址簿查询接口里,直接通过
BaseContext.getCurrentId()获取当前用户 id,然后查询该用户的地址,而不是在 Controller 中再写一遍 token 解析逻辑。
2)你说“减少重复查询”,具体减少的是什么查询?
更准确地说,这里减少的是“重复的身份解析和用户获取动作”,不一定只是数据库查询。
在这个项目里,JWT 本身已经包含了用户标识,所以很多场景下不需要每次都去数据库查用户,只需要在拦截器里解析一次 token,把用户 id 放到上下文里,后续链路共享即可。
如果某些业务场景确实要拿完整用户信息,也可以在第一次查询后再做请求级缓存,避免同一次请求里多次查库。
所以这个优化点本质上是:统一身份解析 + 请求内共享 + 避免重复获取。
如果你直接说“减少数据库查询”,有些面试官会问你:
你这段代码里不是只把 userId 放到 ThreadLocal 了吗?哪里减少查库了?
所以你最好答得更精准:
“减少重复身份解析,必要时也能减少同一请求内的重复查库。”
3)为什么选拦截器,不选 Filter?
因为这个项目是基于 Spring MVC 的,我这里更关注的是 Controller 层方法调用前的身份校验和上下文注入,所以拦截器更合适。
HandlerInterceptor能拿到handler,可以判断是不是 Controller 方法,还能更方便地和 Spring MVC 的执行链结合。
Filter 更偏 Servlet 规范层,适合做编码、跨域、统一日志、最外层请求处理;
而登录鉴权这种和 Controller 访问强相关的逻辑,用拦截器可控性更好。
你可以补一句:
如果项目里需要做更统一、更底层的认证,比如网关层或 Spring Security 体系里,也可以放到 Filter 或 Security Filter Chain 中处理。
4)ThreadLocal 是什么?为什么它能实现线程级共享?
ThreadLocal不是在线程之间共享数据,而是给“每个线程单独存一份副本”。
当前请求在 Spring MVC 的同步处理模型下,通常会由一个工作线程从头执行到尾,所以我在拦截器里把用户 id 存到当前线程的ThreadLocal后,当前请求后续的 Controller、Service、DAO 执行过程中,都可以从这个线程上下文中拿到同一个用户 id。
它的本质是:
- 数据不是放在线程共享区
- 而是存在线程对象内部的
ThreadLocalMap里- key 是当前这个
ThreadLocal实例,value 是你放进去的数据
所以它适合保存请求级、线程级的上下文信息,比如当前用户、租户、traceId 等。
5)ThreadLocal 的底层原理是什么?
每个
Thread对象内部都有一个ThreadLocalMap。
当调用threadLocal.set(value)的时候,本质上是以当前ThreadLocal实例作为 key,把 value 存到当前线程的ThreadLocalMap中。
调用get()时,也是从当前线程自己的ThreadLocalMap里取值。
因为每个线程都有自己独立的 map,所以线程之间互不干扰。
另外ThreadLocalMap里的 key 是弱引用,如果ThreadLocal实例被回收了,但 value 还在,就可能产生 value 无法及时释放的问题,所以一般使用完一定要remove()。
如果时间紧,可以说:
ThreadLocal的核心不是“线程共享”,而是“线程隔离”。每个线程内部维护一份自己的变量副本。
6)为什么说它能减少业务代码耦合?
因为如果不这么做,业务层要拿当前用户,通常有两种写法:
- 一路传
userId参数- 或者直接依赖
HttpServletRequest
这两种都会让业务代码和认证细节耦合。
用BaseContext后,业务层只依赖“当前用户上下文”这个抽象,不关心 token 从哪来、怎么解析,这样职责分层会更清晰。
7)你这个方案有什么风险?
最大的风险是
ThreadLocal使用不当导致线程池复用时的脏数据问题和内存泄漏风险。
因为 Web 容器线程通常来自线程池,一个请求结束后线程不会销毁,而是复用给下一个请求。
如果上一个请求把用户 id 放进了ThreadLocal,但没有及时清理,下一个请求复用这个线程时,就有可能读到上一个用户的数据,造成严重的越权问题。
所以正确姿势是:
- 在请求进入时
set- 在请求结束时
remove- 通常放在拦截器的
afterCompletion或过滤器的 finally 中清理
8)为什么必须 remove(),只 set(null) 行不行?
最推荐的是
remove(),而不是简单set(null)。remove()会真正把当前线程ThreadLocalMap中对应的 entry 清掉;set(null)只是把 value 置空,但 entry 还在,长期运行下不够干净,也不利于避免内存问题。
所以规范做法是try/finally或请求完成回调里remove()。
9)如果接口里开启了异步线程,ThreadLocal 还有效吗?
默认情况下无效。
ThreadLocal只对当前线程可见,如果业务处理中切到了其他线程,比如线程池异步任务、@Async、CompletableFuture,新线程是拿不到原线程里的用户上下文的。
所以这套方案适用于同步请求链路。
如果要跨线程传递上下文,常见方案有:
- 显式传参,把 userId 往下传
- 使用
InheritableThreadLocal,但它对线程池场景并不可靠- 使用阿里 TTL(TransmittableThreadLocal)这类上下文传递方案
- 或者在异步任务提交时手动封装上下文
10)那为什么不用 InheritableThreadLocal?
InheritableThreadLocal只是在“创建子线程”时把父线程值拷贝过去。
但在线上项目里,异步任务大多是线程池复用线程,不是每次新建线程,所以它在实际线程池场景下并不能稳定解决上下文透传问题。
因此对于线程池异步场景,我更倾向于显式传参或者使用 TTL 这类专门的方案。
11)你为什么只存用户 id,不直接存整个用户对象?
我更倾向于只存最小必要信息,比如用户 id、员工 id、租户 id,而不是完整用户对象。
原因有三个:
- 用户 id 更轻量,减少线程上下文存储负担
- 降低对象过期风险,避免把旧的用户状态长期保存在上下文里
- 权责更清晰,需要完整用户信息时再按需查询或缓存
这也是为什么这个项目里
BaseContext存的是Long类型,而不是 User 实体。
12)你这个方案和把 userId 一层层传参相比,有什么优缺点?
优点是调用链更简洁,减少很多样板参数,业务代码可读性更高。
缺点是隐式依赖更强,也就是方法签名上看不出它依赖当前用户上下文,需要开发者知道这个约定。
所以我的理解是:
- 对于“全链路都天然依赖当前登录人”的 Web 请求场景,用
ThreadLocal很合适- 对于公共组件、异步任务、跨系统调用,还是显式传参更稳妥
14)如果 token 失效或者伪造,会怎么处理?
在拦截器里统一做 JWT 校验,校验失败直接返回 401,不进入业务层。
这样业务层默认拿到的都是已通过认证的身份信息。
同时 token 的签名校验依赖服务端密钥,攻击者即便伪造 payload,没有正确签名也无法通过校验。
15)这个方案算不算权限控制?
严格来说,这一层主要解决的是“认证”和“身份传递”,不完全等于“权限控制”。
它回答的是“你是谁”;
权限控制回答的是“你能做什么”。
认证通过后,还需要结合角色、菜单、资源权限做授权判断。
所以这套ThreadLocal上下文方案更偏基础设施,为后续权限判断提供统一的身份来源。
追问:你这个实现有没有可以优化的地方?
有两个点可以优化。
第一,
ThreadLocal清理时机。
当前BaseContext里已经封装了removeCurrentId(),但如果拦截器里只set不remove,在 Tomcat 线程池复用场景下会有上下文串用风险。生产环境里我会在afterCompletion()统一清理。第二,当前上下文只存了一个
Long id,对于简单项目够用;如果后续要支持多租户、角色、请求追踪等,我会把它升级成一个UserContext对象,但仍然只放必要字段,避免存过重对象。
在这个项目里,我主要落地了基于 Spring Cache + Caffeine 的本地缓存,用于首页和专栏相关的侧边栏等高频、低变更数据,减少重复查询;同时首页通过异步并行聚合文章列表、轮播图、侧边栏和用户信息,来提升整体响应性能。
Redis 在项目中也有使用,但更多承担会话校验和部分状态缓存职责,而不是完整意义上的首页多级缓存体系。
1)你这个项目里到底缓存了什么?
这个项目里明确做了 Caffeine 本地缓存,主要缓存的是首页侧边栏、专栏侧边栏、文章详情侧边栏这类高频访问、更新不频繁的数据。
比如SidebarServiceImpl里,queryHomeSidebarList()、queryColumnSidebarList()、queryArticleDetailSidebarList()都加了@Cacheable,缓存管理器是caffeineCacheManager。
所以这个项目的缓存重点不是整页 HTML 或首页文章流,而是这些稳定的推荐/展示型数据。
- Caffeine:用于本地热点数据缓存
- Redis:更多用于登录态/JWT session、计数和一些业务状态
并没有看到统一封装好的“先查 Caffeine、再查 Redis、最后查 DB”的完整多级缓存链路专门服务首页热点数据。
所以如果面试要讲项目真实情况,我会说这是“本地缓存 + Redis 能力并存”,而不是硬说成完整多级缓存体系。
3)为什么首页能更快?主要靠缓存吗?
这个项目里首页提速不只是靠缓存,更关键的是并行化聚合。
IndexRecommendHelper.buildIndexVo()里把首页数据拆成多个任务并发执行,包括文章列表、置顶文章、轮播图、侧边栏、登录用户信息。
其中侧边栏又叠加了 Caffeine 缓存,所以整体响应时间会下降。
也就是说,这里的优化思路更像是:
- 对稳定数据做本地缓存
- 对多个独立数据源做异步并行聚合
4)为什么选 Caffeine,不直接全放 Redis?
因为这里缓存的数据访问频率高、数据量不大、变化也不频繁,先放本地缓存命中最快,少一次网络 IO。
Caffeine 的优点是:
- JVM 内存命中快
- 接入 Spring Cache 简单
- 适合首页侧边栏、推荐位这类热点只读数据
Redis 更适合做跨实例共享、需要统一状态的一类缓存。
所以如果只有单机或小规模部署,Caffeine 对这类场景性价比很高。
5)Caffeine 配置是怎么做的?
项目里在
ForumCoreAutoConfig中定义了caffeineCacheManager,核心配置是:
- 写入后 5 分钟过期
- 初始容量 100
- 最大条数 200
然后通过 Spring 的@Cacheable和@CacheEvict来使用和失效。
6)缓存一致性怎么处理?
这个项目里主要有两种处理方式:
- 对文章详情侧边栏,在后台更新文章时通过
@CacheEvict删除对应缓存- 后台还提供了一个缓存刷新接口,可以手动清空 Caffeine 缓存
所以它采用的是比较轻量的缓存一致性方案:
- 低频更新场景下,优先用 TTL 兜底
- 关键写操作时主动失效
- 管理后台提供手动刷新能力
7)为什么只缓存侧边栏,不缓存首页文章列表?
因为侧边栏数据更新频率低、结构稳定、命中率高,更适合直接缓存。
首页文章列表和分类文章流的变化更频繁,和分页、分类、置顶、发布时间等因素相关,直接缓存整块数据会让 key 设计和失效策略复杂很多。
所以这个项目里先缓存收益更明确、维护成本更低的部分,是一个比较务实的做法。
8)如果让你继续优化成真正的多级缓存,你会怎么做?
如果要往真正的“Caffeine + Redis 多级缓存”演进,我会这样设计:
- L1:Caffeine,本地热点缓存,承接高频读
- L2:Redis,跨实例共享缓存
- L3:DB/服务查询
查询流程是:先本地,再 Redis,再回源;回源后回填两级缓存。
同时配套:- 不同业务 key 规范
- TTL + 随机过期时间,防止雪崩
- 热点 key 互斥加载,防止击穿
- 变更时删 Redis 并广播/消息通知清本地缓存
但这部分属于“我会怎么扩展”,不是当前项目里已经完整落地的部分。
9)你说首页 80ms,有数据支持吗?
如果严格按项目代码来讲,我不会把“80ms”说成一个确定结论,因为代码仓库里我没有看到明确的压测报告或监控数据。
我更倾向于说:
“通过本地缓存 + 首页并行聚合,能够显著降低首页接口的平均响应耗时。”
如果面试官追问具体数字,我会说明那需要结合压测环境、机器配置、数据量和缓存命中率来给出。
这句很重要,避免被追着问压测细节。
你可以直接背的“项目版回答”
这个项目里缓存优化是做过的,但更准确地说,是基于 Spring Cache + Caffeine 的本地缓存,而不是完整落地的 Caffeine + Redis 多级缓存体系。
具体缓存的重点在首页侧边栏、专栏侧边栏、文章详情侧边栏这类高频但低变更数据,通过@Cacheable提高命中率,减少重复查询;同时首页数据聚合用了异步并行,把文章列表、轮播图、侧边栏和用户信息并发拉取,所以首页响应性能提升不只是靠缓存。
Redis 在项目里也有使用,但更多是登录态、session/JWT 校验和一些状态型数据,不是专门作为首页热点数据的二级缓存。
如果要把这块包装成真正的多级缓存能力,还需要补充 Redis 回填、本地缓存失效广播、热点 key 保护这些机制。
通过Redis 的 zset 实现用户活跃度排行,并通过幂等防止重复添加活跃度,通过Spring的Event和 Listenter 来触发活跃度更新。
你可以直接背这一版,比较自然:
我们项目里用户活跃度排行是用 Redis 的 ZSet 做的。
设计上把userId作为 ZSet 的 member,把活跃分作为 score,然后分别维护了日榜和月榜两个 key,比如当天一个 key、当月一个 key。当用户发生访问、点赞、收藏、评论、发文章、关注这类行为时,服务层会先把这次行为抽象成一个
ActivityScoreBo,根据行为类型算出本次应该加多少分或减多少分,然后调用 Redis 的ZINCRBY去更新该用户在日榜和月榜中的分值。另外项目里不是直接裸写 ZSet,而是又用 Redis Hash 做了一层幂等控制。Hash 里会记录“这个用户今天这个动作有没有加过分”,比如某篇文章是否已经点赞加分过。这样可以避免重复点击、重复请求导致分数被重复累计。
查询排行榜时,就是从对应的 ZSet 里拿 TopN 用户和分数,再批量查用户的昵称头像等信息做组装。查询单个用户排名也是直接从 ZSet 里查 rank 和 score。
这个方案的优点是更新快、查询快、实时性强,很适合排行榜场景。
如果要继续优化的话,我会关注幂等逻辑的原子性问题,因为现在是先查 Hash 再写 Hash 再更新 ZSet,极端并发下可能有竞态,比较好的做法是用 Lua 脚本把这几个 Redis 操作做成原子执行。
如果问:为什么不用 MySQL 排行?
你答:
MySQL 更适合持久化和复杂查询,但不适合高频实时排序。活跃度这种持续变动的实时排行榜,用 Redis ZSet 的增量更新和按分值排序会更高效。
如果问:为什么还要 Hash,不直接 ZSet 就行?
你答:
ZSet 只解决“排序”和“累加分数”,不解决“同一个动作是否重复记分”的问题,所以需要额外用 Hash 做幂等控制。
如果问:为什么同时维护日榜和月榜,不临时聚合?
你答:
临时聚合成本高,而且会增加查询延迟。提前维护两套榜单,查询时可以直接返回,适合高并发读场景。
我去临时聚合是什么。
利用AOP切面监听点赞、评论等用户行为事件,异步将通知消息写入RabbitMQ消息队列,消费者服 务再将通知消息写入
坏了,这里感觉没有用aop,用的应该是监听事件吧。。。
- 为什么用 AOP 而不是直接在 Service 里写发送代码?(回答重点:解耦、非侵入性、代码复用)。
- 切面是怎么定义的?(用的是
@Around还是@AfterReturning?建议用AfterReturning,只有操作成功了才发通知)。 - 如果点赞成功了,但消息发送 RabbitMQ 失败了怎么办?(高频考点:分布式事务一致性)。
- 如何保证消息不丢失?(回答重点:生产者确认
confirm、交换机/队列持久化、消费者手动ACK)。 - 如果 RabbitMQ 挂了,点赞功能还能用吗?(回答重点:降级策略,异步发送不能影响主业务流)。
- 如何防止重复消费(幂等性)?(如果网络波动导致消息重发,用户会不会收到两条一样的通知?)。
- 为什么要异步?同步写数据库不行吗?(回答重点:点赞、评论属于高频操作,同步写通知库会拖慢主逻辑响应速度)。
- 消费者服务压力太大处理不过来怎么办?(回答重点:限流、削峰填谷、增加消费者节点)。
AOP 切面类: 找一下 @Pointcut 表达式,你是切在具体的注解上(如 @NotifyUpdate)还是切在特定的方法名上?
消息体格式: 存入 RabbitMQ 的是 JSON 字符串还是对象序列化?(建议是 JSON,跨语言且易排查)。
RabbitMQ 配置: 检查是否有 CachingConnectionFactory 的配置,是否开启了 publisher-confirm-type: correlated。
消费端幂等逻辑: 消费者写数据库前,有没有查一下这张通知表里是否已经存在相同的 business_id(如点赞记录 ID)?
“在开发点赞和评论功能时,我发现这些操作属于核心高频路径,而‘发送通知’属于次要逻辑。如果同步执行,会增加主业务的响应耗时。
此外,如果通知逻辑报错,不应该导致点赞失败。所以我决定通过 AOP 切面 将通知逻辑剥离,利用 RabbitMQ 实现异步化解耦。”
“我自定义了一个注解,标记在需要触发通知的方法上。切面类通过
@AfterReturning拦截这些请求,将用户信息、行为类型、目标 ID 封装成 JSON 消息,推送到 RabbitMQ 的交换机中。消费者服务独立运行,监听队列并根据消息类型异步写入
notification表。这样主业务线程在点赞完成后立即返回,响应耗时大幅降低。”“为了确保消息不丢失,我开启了 RabbitMQ 的生产者确认机制。如果 MQ 宕机发送失败,我会捕获异常并进行本地日志记录或尝试重试。
在消费端,我采用了手动 ACK 模式,确保消息真正写入数据库后才从队列移除。同时,为了防止重复通知,我在数据库层面设置了唯一索引或在代码中做了幂等校验,确保同一个行为不会产生重复通知。”
关于事务的“坑”:
- 风险:如果你在点赞事务还没提交(
commit)前就把消息发出去了,万一事务回滚了,用户却收到了通知,这就是数据不一致。 - 对策:你可以提到利用
TransactionSynchronizationManager.registerSynchronization,确保在事务成功提交后再发送 MQ 消息。
消息堆积怎么办?
- 提到你会设置消息的 TTL(过期时间),或者配置 死信队列(DLX) 来处理那些无法正常投递的异常消息。
为什么不用 Spring 内部的 ApplicationEvent?
- 回答:Spring Event 是进程内的,如果服务重启,内存里的事件就丢了;RabbitMQ 是独立的中间件,支持持久化和横向扩展,更适合分布式环境。
“如果你的消费者服务挂了 10 分钟,这期间产生的 1 万条点赞通知会消失吗?重启后会发生什么?” (答案:不会消失,消息会堆积在磁盘持久化的队列里,重启后消费者会以‘匀速’重新开始消费,这就是削峰填谷。)
基于WebSocket 实现前端和后端之间的长连接通信通道,并结合DeepSeek大模型的StreamAPI实现
流式响应返回,只要后端有新的内容到达,前端就即时将文本逐步拼接显示。
我利用 WebSocket 建立了前后端的双向长连接,主要为了解决大模型回复耗时较长、用户等待焦虑的问题。
具体的实现逻辑是:后端利用 WebClient 异步监听 DeepSeek 的响应流(SSE),并采用‘边收边发’的策略,将解析出的 delta content 立即通过 WebSocket Session 推送。
为了保证体验,我们还加入了无感 Token 刷新机制和异步线程池优化,确保即使在流式传输这种长生命周期请求中,用户的身份信息和系统稳定性也能得到保障。”
- Q:如果 WebSocket 突然断了,AI 还在后台跑,怎么处理?
- A:我们会通过
WebSocketHandler的afterConnectionClosed回调,第一时间清理该 Session 对应的后台任务和 API 请求流,防止资源浪费。
- A:我们会通过
- Q:流式传输对服务器内存压力大吗?
- A:因为我们用了
WebClient的响应式非阻塞模型,数据是‘流过’服务器而不是‘存在’服务器,所以对内存的占用极低。
- A:因为我们用了
“我设计这套方案的核心目标是高可用和一致性。
针对大文件,我们采用 MD5 校验 + MinIO 分片上传,利用 Redis Bitmap 的位运算特性实现极低开销的断点续传。
考虑到后续的 PDF 解析(Tika)和向量化(Embedding)是计算密集型任务,我引入了 Kafka 作为缓冲层。这样做不仅实现了文件上传与复杂处理逻辑的解耦,还利用 Kafka 的持久化机制保证了处理链路的可追溯和高容错。
即使在处理过程中某个节点宕机,由于分片状态记录在 Redis、原始文件在 MinIO、处理任务在 Kafka,系统可以实现无损的任务恢复。”
- Q:如果 Kafka 里的消息处理失败了,导致向量数据库里缺数据,你怎么办?
- A:我们会利用 Kafka 的死信队列(DLN)记录失败任务,并配合定时任务对比 MinIO 文件库与向量库的数量,实现最终一致性。
“我实现了一套 ‘关键词 + 语义’的双引擎搜索方案。
首先,利用 IK 分词器 处理中文文档的细粒度拆解,保证了对专业术语的搜索精度。 其次,集成 阿里 Embedding 模型,利用 ES 的 KNN 向量检索 实现对模糊语义的捕捉。
为了提升 RAG 的回答质量,我重点优化了检索召回率。通过在 ES 侧结合关键词过滤防止结果跑偏,并利用 BM25 算法进行重排序,将召回结果进行二次筛选。这种‘双路并行、加权融合’的方法,相比单一的向量检索,将知识库的回复准确率提升了约 30% 左右(这是一个参考数据,体现你的结果导向)。”
- Q:ES 的向量搜索和专门的向量数据库(如 Milvus)相比有什么优缺点?
- A:ES 的优势在于‘一站式解决方案’。它能极其方便地做‘混合查询’(在一个 DSL 语句里同时写 filter 和 knn),不需要跨数据库同步数据,维护成本低。对于我们这种百万级规模的知识库,ES 的性能完全足够。
- Q:切片(Chunking)大小你是怎么定的?
- A:我们通常设在 500-800 Token 左右,并设置 10% 的重叠度(Overlap),防止关键信息在切片处被切断。
当你在 new TypeReference<...>() 后面加上 {} 时,你实际上是写了一个继承自 TypeReference 的临时子类,只是这个子类没有名字。
匿名内部类方式:当你用 {} 创建子类时,Java 会把父类的泛型信息(即那个复杂的嵌套结构)保存在这个子类的 “字节码” 里。
Jackson 的骚操作:Jackson 进入这个匿名内部类,通过反射(Reflection)读取它父类的泛型参数,从而惊叹道:“喔!原来你要的是一个装满 Map 的 List 啊!”
这种技术在 Java 里有个专业名词:超级类型令牌 (Super Type Tokens)。
问题
关于AOP
如果你在技术派的源码中搜 MdcUtil 的话,可以在 ReqRecordFilter 中找得到,顾名思义,该类是对请求的一个过滤器,会在每个请求中加上全链路的 traceid。
项目中一整个MdcDot注解都被用来记录方法用时,那么reqrecord中的记录又是在干什么?他有一个.log文件的记录,同时还把这一次的请求时间输出了。
我想一个适用于普通方法,而req的这个是在filter里专门记录请求的时间,可能和那个方法不兼容?
还有一个问题,两个方法都用的mdcutil里的方法,这个traceid没有覆盖的可能吗?
1. MdcDot 与 ReqRecord 的区别?traceId 会不会互相覆盖?
你已经大致猜对了,但我帮你补充成一个完整逻辑:
1)MdcDot 用来记录方法执行耗时
- 这是一个 AOP 注解(作用在某个方法上)
- AOP 拦截方法 → 进入前记录时间 → 退出后输出日志
- 一般属于业务级别的方法耗时监控(精确到某个 service 方法)
例如:
UserService.getUserById() 花了 230ms
OrderService.create() 花了 1.2s这些属于应用内部的性能监控。
2)ReqRecord(你看到的 req 处理)是 Filter 层的请求级日志
这段逻辑在 Filter 或 Interceptor 里:
- 进入请求:建立 traceId、记录用户信息、记录路径、IP、UA
- 完成请求:统一输出一次完整请求日志(写入 .log)
这是每次 HTTP 请求的入口 / 出口监控。
也就是“这次 API 调用持续了多久”。例如:
GET /api/user/info cost=156ms
POST /order/create cost=1220ms该日志是更偏向运维监控、流量统计、接口质量监控。
3)两个都用 MdcUtil,会不会覆盖 traceId?
不会。
原因:
- traceId 是在 filter 入口时统一设置的
- AOP 方法执行时只会使用这个 traceId,不会重新生成
- MdcUtil.addTraceId() 基本格式就是:“如果 MDC 中没有 traceId 才生成”
所以一个请求有一个 traceId,所有方法耗时都共享它。
这个项目细分了好多类,他还有一个Dao,专门和service区分开来了。@Respository和@Service 的区别只是标记不一样吗
2. Repository 与 Service 的区别?什么时候分层?
不是标签不同而已。
Repository(DAO)职责
只负责与数据库交互:
- CRUD
- 自定义 SQL
- 查询分页
- 统计 SQL
Repository/mapper 层不包含任何业务逻辑。
Service 的职责
封装业务逻辑:
- 参数校验
- 领域逻辑组合
- 调用多个 DAO / RPC
- 事务控制
- 缓存控制
什么时候需要分?
当你有以下需求时必须分:
- 业务需要组合多个数据库表的操作
- 一个请求涉及多个操作(比如先查用户再查订单)
- 需要保证事务(@Transactional)
- 需要隐藏数据库细节(controller 不应该直接写 SQL)
你之前的项目没有 DAO 层,是因为业务太简单。
所以说,之前那个项目有service层用其他service的情况,这种情况其实应该使用dao吗?
用service CRUD 和Mapper CRUD 的区别在?本来serviceimpl这个继承的类里就要求mapper继承了basemapper的,那用哪个有说法吗?我想了想可能是因为:用service的肯定是controller 层 ,controller层不可能去调用mapper层的东西的,即使调用的service其实本质上还是mapper,而dao(service)层就可以做基本CURD做不到的事情?
3. 为什么 MyBatis-Plus 要提供 Service CRUD?和直接在 Service 调 Mapper 有什么区别?
你问的这个问题非常关键:
“既然 Controller 能调 Service,那为什么 Service 不自己写 CRUD,而要依赖 MP 提供的 ServiceImpl 的 CRUD?”我拆成 3 点,你看了就懂了。
① MyBatis-Plus 的 Service CRUD 是通用业务服务层的模板
MP 的设计理念:
- Mapper 层只负责数据库 CRUD(原子、低级别操作)
- Service 层需要在 Mapper 的基础上补充业务流程逻辑
- 绝大多数业务都会需要标准化的 CRUD(分页、按条件查询、批量删除……)
因此 MP 提供了 IService + ServiceImpl,目的是:
- 你不用每个项目都重复写:list、getById、removeById、save、update……
- 这些方法自动配合 MP 的 QueryWrapper、分页插件、自动填充等
- 代码统一规范、减少重复性工作
也就是说,它做的是“中间层简化”和“业务层能力增强”。
② 你完全可以不继承 IService,但你会多写大量重复代码
假设你不用 MyBatis-Plus 的 IService,你会写成这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class UserService {
@Autowired
private UserMapper mapper;
public User getById(Long id) {
return mapper.selectById(id);
}
public List<User> list() {
return mapper.selectList(null);
}
public void save(User u) {
mapper.insert(u);
}
public void update(User u) {
mapper.updateById(u);
}
public void delete(Long id) {
mapper.deleteById(id);
}
}这是一堆重复工作,而且不同开发者写出的接口风格可能完全不一致。
MP 帮你统一成:
1
2@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {}然后你可以直接用:
1
2
3
4userService.getById(id);
userService.list();
userService.save(entity);
userService.removeById(id);你几乎不用写额外代码,即可保证所有项目统一风格。
为什么这里要用@Param注释?不加不可以吗?这是mybtis就有的还是plus才有的?这样的话在yml设置里开启驼峰命名的意义在?
1
2
3
4
5
6
7
8
9
10public interface UserMapper extends BaseMapper<UserDO> {
/**
* 根据三方唯一id进行查询
*
* @param accountId
* @return
*/
UserDO getByThirdAccountId( String accountId);
}情况1:编译器保留了参数名(默认情况自 JDK8 后大多数 IDE 会开启)
MyBatis 可以直接识别:
1
UserDO getByThirdAccountId(String accountId);
此时可以写:
1
#{accountId}
无需 @Param。
情况2:项目没有启用参数名保留功能(某些环境未开启 -parameters)
MyBatis 拿到的参数名会变成:
1
arg0, arg1, arg2...
你的方法:
1
UserDO getByThirdAccountId(String accountId)
在运行期可能变成:
1
arg0
这时你 SQL 写:
1
#{accountId}
就会报错:找不到 accountId。
==> 因此为了防止环境不一致,团队规范通常建议:多参数和注解 SQL 要使用 @Param。
TableId(type = IdType.AUTO),在数据库本来就设置了自增的情况下,还用这个注解让他在插入的时候自增其实没什么意义吧?哦,等等,它的数据库都没有设置约束,约束全在mybatis里,这样的好处在?
5. @TableId(type = IdType.AUTO) 为何数据库不建约束?
MP 默认通过注解控制主键策略,数据库里不建约束有两个好处:
1)跨数据库兼容
MySQL, PostgreSQL, Oracle 都有不同的自增写法
MP 用注解帮你屏蔽差异。2)方便迁移(比如切换到雪花算法)
你只改 Java 注解,不需要改表结构。
项目越大越喜欢用 Java 控制,而非 DBA 控制。
为何这个是用的传统渲染页面?
```java
@Controller
public class IndexController extends BaseViewController {@Autowired private IndexRecommendHelper indexRecommendHelper; @GetMapping(path = {"/", "", "/index", "/login"}) public String index(Model model, HttpServletRequest request) { String activeTab = request.getParameter("category"); IndexVo vo = indexRecommendHelper.buildIndexVo(activeTab); model.addAttribute("vo", vo); return "views/home/index"; }}
1
2
3
4
5
6
7
8
9
> ## 为什么不直接在 Controller 调 Service?
>
> 这是你问得最**专业**的点。
>
> ### ❌ 反模式(Controller 直接堆 Service)
>ArticleService
TagService
UserService
RecommendService
…1
2
3
4
5
6
7
8
9
10
11
12
Controller 会变成:
- 巨大
- 难维护
- 逻辑混乱
- 改一处牵一堆
------
### ✅ 这里采用的是「Helper / Facade 层」Controller
↓
IndexRecommendHelper ← 聚合逻辑
↓
多个 Service1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
`IndexRecommendHelper` 的职责是:
> **“为某一个页面 / 场景,组装完整的业务数据”**
------
## 五、这样设计的好处(非常重要)
### 1️⃣ Controller 极度干净
Controller 只做 3 件事:
1. 接收请求
2. 调用场景服务
3. 返回视图
------
### 2️⃣ Service 保持“单一业务职责”
- ArticleService:文章
- TagService:标签
- UserService:用户
不污染 Service 层
------
### 3️⃣ 页面级逻辑可以复用
今天是:buildIndexVo()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
明天可能是:
- PC 首页
- 移动端首页
- 小程序首页
**Helper 可以复用,Controller 各自不同**
------
### 4️⃣ 非常适合「复杂页面」
首页通常是:
- 数据多
- 来源多
- 规则多
- 经常改
Helper / Facade 是最合理的位置。
------
## 六、你可以这样理解它的角色
| 层级 | 角色 |
| --------------- | ------------------ |
| Controller | HTTP + 路由 + 视图 |
| Helper / Facade | 页面级业务编排 |
| Service | 单一领域业务 |
| VO | 页面数据模型 |
------
## 七、一句话总结(面试级)
> 这段代码是一个 **MVC 页面 Controller**,
> 它不直接调用 Service,而是通过 Helper 聚合首页所需的业务数据,
> 最终将 VO 注入 Model 并返回视图路径。
> 这种设计降低了 Controller 复杂度,提高了页面逻辑的可维护性和复用性。
Spring MVC 的流程是:Controller
↓ 返回 “views/home/index”
ViewResolver
↓ 拼接前缀 + 后缀
模板引擎(Thymeleaf / JSP)
↓ 渲染 HTML
浏览器收到 HTML1
2
3
例如(常见配置):spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html1
2
3
那么:return “views/home/index”;
1
2
3
最终渲染的是:classpath:/templates/views/home/index.html
1
2
3
4
5
## 1️⃣ template.execute(...) —— 为什么要这么写?return template.execute(new RedisCallback
- >>() {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
### 这是在干什么?
- `template` 是 `RedisTemplate`
- `execute` 允许你:
- **直接拿到底层 RedisConnection**
- 使用 Redis 的原生命令(性能更好、功能更全)
### 为什么不用 `redisTemplate.opsForZSet()`?
因为:
- `zRangeWithScores` 在某些版本/泛型下不好用
- 直接用 `RedisConnection` **最稳定、最贴近 Redis 原生命令**
👉 **这是偏“底层工具类”的写法**
| 数据结构 | 核心能力 | 最典型用途 |
| -------- | ----------- | -------------- |
| String | 单值 / 计数 | 缓存、计数、锁 |
| List | 有序 | 队列、历史 |
| Set | 去重 | 白名单、点赞 |
| ZSet | 排序 | 排行榜 |
| Hash | 结构化 | 对象缓存 |
```java
@Test
public void testExecute() {
// 使用 execute 方法执行 Redis 命令
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
// 执行 Redis 命令,例如 set 和 get 命令
connection.set("itwanger".getBytes(), "沉默王二".getBytes());
byte[] value = connection.get("itwanger".getBytes());
String strValue = new String(value);
// 输出获取到的值
System.out.println(strValue);
return null;
}
});
}
why cafeine not redis
你这个问题问得非常深刻,直戳架构设计的核心。
通常我们认为“全局共享”的数据该放 Redis,“用户私有”的数据才放本地。但在这个 ChatGPT 场景下,作者使用 Caffeine(本地缓存) 而不是 Redis,其实是基于 “有状态的连接” 和 “内存对象” 这两个特殊原因。
1. 为什么不能放 Redis?(核心:Redis 存不下“活的”对象)
这是最关键的技术限制。
- Redis 存的是“死数据”:Redis 只能存字符串、数字、JSON。你可以把用户的聊天记录存进去。
- ChatGPT 对象是“活的”:在代码里,
ChatGPT和ChatGPTStream是复杂的 Java 对象。它们内部包含了:- 网络连接池(OkHttpClient)
- 当前的代理配置(Proxy)
- 底层的事件监听器(EventListener)
问题所在:你没法把一个正在运行的 OkHttpClient 连接序列化成 JSON 存进 Redis,然后再从 Redis 读出来恢复成一个“活”的连接。
所以,这些“工具类对象”必须保存在 JVM 进程的内存里,也就是 Caffeine。
2. 这里的 cacheStream 到底存了什么?
看这段代码:
Java
1 | public LoadingCache<ImmutablePair<Long, AISourceEnum>, ImmutablePair<ChatGPT, ChatGPTStream>> cacheStream; |
- Key:
用户ID + 模型类型 - Value:
ChatGPT 对象 + 流式请求对象
它的逻辑是:
当用户 A 发起提问时,系统先去 Caffeine 找:“我之前给用户 A 创建过那个带代理、带 API Key 的 Client 对象吗?”
- 如果有,直接复用这个对象发请求。
- 如果没有,当场
new一个,并根据用户 ID 绑定一个固定的代理,然后存入 Caffeine。
3. 本地缓存 vs Redis 的博弈
在这个特定的 AI 插件场景下,用本地缓存有几个好处:
| 维度 | 本地缓存 (Caffeine) | 分布式缓存 (Redis) |
|---|---|---|
| 存储内容 | Java 实例对象 (包含连接池等) | 序列化数据 (JSON/Byte) |
| 访问速度 | 极快(纳秒级,直接读内存) | 快(毫秒级,需要网络 IO) |
| 一致性 | 弱(A 服务器创建的对象 B 看不见) | 强(全集群共享) |
为什么这里不需要一致性?
即使你的论坛部署了 2 台服务器,用户第一题在 A 服务器问,第二题路由到了 B 服务器。
- 在 B 服务器上,Caffeine 找不到记录,会重新为该用户
new一个 Client。 - 虽然多了一次
new的开销,但对于 ChatGPT 这种长耗时的请求来说,这几毫秒的初始化开销根本不算什么。
4. 这里的“用户本地”是个误解
我们要区分两个概念:
- 用户的浏览器本地:存 Cookie, LocalStorage。
- 服务器的进程本地:这就是 Caffeine 所在的地方。
所以,这个缓存依然是在服务端的,只是它存在于运行程序的内存里,而不是独立的 Redis 数据库里。
总结
作者这么写,是因为他需要缓存的是 “已经配置好的、可以直接发请求的工具对象”。
- 为了性能:不用每次请求都重新解析配置、重新构建 HTTP 客户端。
- 为了“代理粘滞”:通过固定用户与 Client 对象的对应关系,确保同一个用户的连续对话 IP 相对稳定。
一句话总结:
Redis 用来存“聊了什么”(消息内容),而 Caffeine 用来存“怎么去聊”(带代理和 Key 的连接对象)。
你想看看这个 cacheStream 的过期时间设置吗?
代码里写了 expireAfterWrite(300, TimeUnit.SECONDS),这意味着如果一个用户 5 分钟没说话,这个带代理的连接对象就会被销毁,释放服务器内存。这种自动化管理也是选择 Caffeine 的原因之一。
3. 为什么“只能靠 Cookie”?
因为 Cookie 是 HTTP 协议的一部分,它是跟着请求一起飞到服务器的;而 localStorage 是 浏览器端 JS 对象,它只能静静地躺在浏览器里,等 JS 去唤醒它。
- 如果用 localStorage: 服务器在渲染页面时,根本不知道你登录了没。它只能先给你一个“未登录”的静态页面,等页面到了浏览器、JS 跑起来了,再闪烁一下变成“已登录”。用户体验会很差(也就是常说的“白屏”或“闪烁”)。
- 如果用 Cookie: 服务器在发货前就识别了你的身份。你打开网页的一瞬间,看到的就是完整的、带有你头像和名字的私人定制页面。

我草吓哭了
- 构建工具:后端(Maven、Gradle)、前端(Webpack、Vite)
- 单元测试:Junit
- 开发框架:SpringMVC、Spring、Spring Boot
- Web 服务器:Tomcat、Caddy、Nginx
- 微服务:Spring Cloud
- 数据层:JPA、MyBatis、MyBatis-Plus
- 模板引擎:thymeleaf
- 容器:Docker(镜像仓库服务 Harbor、图形化工具 Portainer)、k8s、Podman
- 分布式 RPC 框架:Dubbo
- 消息队列:Kafka(图形化工具 Eagle)、RocketMQ、RabbitMQ、Pulsar
- 持续集成:Jenkins、Drone
- 压力测试:Jmeter
- 数据库:MySQL(数据库中间件 Gaea、同步数据 canal、数据库迁移工具 Flyway)
- 缓存:Redis(增强模块 RedisMod、ORM 框架 RedisOM)
- nosql:MongoDB
- 对象存储服务:minio
- 日志:Log4j、Logback、SF4J、Log4j2
- 搜索引擎:ES
- 日志收集:ELK(日志采集器 Filebeat)、EFK(Fluentd)、LPG(Loki+Promtail+Grafana)
- 大数据:Spark、Hadoop、HBase、Hive、Storm、Flink
- 分布式应用程序协调:Zookeeper
- token 管理:jwt(nimbus-jose-jwt)
- 诊断工具:arthas
- 安全框架:Shiro、SpringSecurity
- 权限框架:Keycloak、Sa-Token
- JSON 处理:fastjson2、Jackson、Gson
- office 文档操作:EasyPoi、EasyExcel
- 文件预览:kkFileView
- 属性映射:mapStruct
- Java 硬件信息库:oshi
- Java 连接 SSH 服务器:ganymed
- 接口文档:Swagger-ui、Knife4j、Spring Doc、Torna、YApi
- 任务调度框架:Spring Task、Quartz、PowerJob、XXL-Job
- Git 服务:Gogs
- 低代码:LowCodeEngine、Yao、Erupt、magic-api
- API 网关:Gateway、Zuul、apisix
- 数据可视化(Business Intelligence,也就是 BI):DataEase、Metabase
- 项目文档:Hexo、VuePress
- 应用监控:SpringBoot Admin、Grafana、SkyWalking、Elastic APM
- 注解:lombok
- jdbc 连接池:Druid
- Java 工具包:hutool、Guava
- 数据检查:hibernate validator
- 代码生成器:Mybatis generator
- Web 自动化测试:selenium
- HTTP 客户端工具:Retrofit
- 脚手架:sa-plus
?