尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot中用ThreadLocal存放用户信息:原理、实现与防坑指南

Spring Boot中用ThreadLocal存放用户信息:原理、实现与防坑指南 在Web后端开发里“当前请求是谁”这个问题几乎每个项目都会遇到。刚工作那会儿我做权限模块每次在Service层要拿当前登录用户ID都是Controller从Token里解析出来再一层层往下传。方法少还好方法一多每个方法都挂个userId参数代码丑得没法看还容易漏传。后来看到一个老项目用了ThreadLocal处理这个问题我才反应过来把用户信息放在当前线程的上下文里才是正解。Spring Boot项目里用ThreadLocal存放用户信息算得上Java Web开发中的经典操作了既能解耦参数传递又能保证线程隔离原理和使用都有不少细节值得掰开揉碎讲清楚。这篇文章适合刚接触ThreadLocal的初中级开发者也适合那些已经在用、但偶尔被“线程池取值不对”“内存泄漏”这类问题困扰的同行。我会从核心原理讲起然后给出一个能直接抄进项目里的Spring Boot完整实现最后把我在实际项目中踩过的坑和排查思路都整理出来希望能帮你少走弯路。1. 为什么选ThreadLocal存放用户信息1.1 先理解ThreadLocal到底做了什么ThreadLocal的官方定义是“提供线程局部变量”。这句话听起来抽象我用一个生活化的类比解释假设公司前台有个快递架每个工位对应一个专属小格子。快递员把包裹放进对应工位的格子里哪个员工来取只能取到自己格子里的那份别人格子里的东西你碰不到也看不到。ThreadLocal就是这个快递架每个线程就是不同的工位格子你往ThreadLocal里放数据相当于放进当前线程专属的格子里。从源码层面看ThreadLocal的核心机制是每个Thread对象内部维护了一个ThreadLocalMap这个Map的key是ThreadLocal实例的弱引用value是你存入的对象。当你想在代码里拿数据时通过当前的Thread对象找到它的ThreadLocalMap再用当前的ThreadLocal实例作为key去查value。因为每个线程都有自己的ThreadLocalMap所以天然做到了线程隔离不同线程之间存的数据互不干扰。我在最初学习的时候有个误区以为ThreadLocal是解决并发下数据混乱问题的。其实准确来说ThreadLocal解决的是“变量在不同线程间的隔离”问题而不是“多个线程同时访问同一个变量时的竞争”问题。这两者的本质区别在于如果你定义了一个static的HashMap多个线程往里写数据那需要加锁或者用ConcurrentHashMap但如果是每个线程在自己的ThreadLocal里读写根本不存在共享也就谈不上竞争。1.2 用户信息传递这个场景的特殊性在Spring Boot的Web应用中一次HTTP请求从进入DispatcherServlet到最后响应返回整个过程都由Tomcat的一个工作线程从头到尾执行。也就是说请求来了Tomcat线程池里分配一个线程X来处理Controller、Service、Mapper这些层全在这个线程X上执行Request处理完线程X归还给Tomcat线程池。这个“单线程贯穿请求全程”的特性正好是ThreadLocal最理想的使用场景。我们可以把当前登录用户的信息放到ThreadLocal里在后续的任何层级、任何地方只要还在这个线程上执行就能随时取出来用。不用再层层传递参数也不需要在每个方法签名上加一个多余的用户参数。和传统的传参方式相比ThreadLocal的优势非常明显去除冗余参数所有业务方法不需要额外挂userId或User对象参数代码更干净。任意层级访问工具类、AOP切面、自定义注解解析、MyBatis拦截器等位置都可以直接获取登录用户。自动线程隔离高并发下A请求的线程和B请求的线程各存各的互不覆盖。还有一个容易被忽略的价值它让“当前用户”这个概念变成了和“当前时间”“当前环境”类似的上下文信息不再散落在代码各处。对于大型项目来说这种基础能力的统一封装比每个业务模块自己维护一套用户传递机制维护成本要低得多。1.3 为什么不建议用ServletRequest attribute或参数传递有人说Spring MVC本来就可以通过RequestAttributes拿当前请求的信息为什么还要用ThreadLocal理论上你可以用RequestContextHolder.currentRequestAttributes()拿到当前的HttpServletRequest然后从attribute里取用户信息。但问题在于这个方案严重依赖Servlet API在单元测试、MQ消费、定时任务这类非Web环境下会直接抛异常。而且用Request对象存取数据本质上还是在和底层容器耦合。参数传递的问题更直接如果Controller的每个方法都加一个“User currentUser”参数接口签名会非常臃肿如果因为偷懒漏传了某个参数还会导致后续做用户数据隔离时出现越权漏洞。ThreadLocal的方式不改变方法签名用的时候取一下用完删掉规则统一了安全性反而更有保证。2. Spring Boot中存放用户信息的完整实现2.1 整体设计思路在Spring Boot里用ThreadLocal存放用户信息业界比较成熟的方案是“三个组件”配合使用职责清晰、环环相扣第一个是用户上下文持有器UserContext内部封装ThreadLocal的读写操作对外提供set、get、remove等静态方法。持有器把所有细节藏起来业务方拿用户信息时只需一行代码。第二个是用户信息解析器UserInterceptor实现HandlerInterceptor接口在preHandle里从请求Token中解析出当前用户放入UserContext在afterCompletion里调用UserContext.clear()确保线程使用完后清空数据防止复用线程时数据串号。第三个是Web配置类WebMvcConfig负责把拦截器注册到Spring MVC的拦截器链中并配置需要拦截或放行的URL路径。这样设计的好处是业务代码只依赖UserContext这个门面不依赖具体实现如果以后要把ThreadLocal换成别的方案只改UserContext的内部代码即可业务层完全无感知。2.2 定义UserContext工具类这个类就是整个方案对外暴露的唯一入口。第一次写这个类的朋友最容易犯的错是定义一个成员变量ThreadLocal然后在Service里new一个UserContext实例来调方法。这种做法是错的。因为Service实例大多是多线程共享的如果ThreadLocal是Service的成员变量它虽然内部按线程做了隔离但语义上还是怪怪的。标准做法是UserContext只提供静态方法ThreadLocal直接以private static final的形式定义在类内部。public class UserContext { private static final ThreadLocalLoginUser HOLDER new ThreadLocal(); public static void set(LoginUser loginUser) { HOLDER.set(loginUser); } public static LoginUser get() { return HOLDER.get(); } public static Long getUserId() { LoginUser loginUser HOLDER.get(); return loginUser ! null ? loginUser.getUserId() : null; } public static String getUsername() { LoginUser loginUser HOLDER.get(); return loginUser ! null ? loginUser.getUsername() : null; } public static void clear() { HOLDER.remove(); } }LoginUser就是一个普通的POJO包含userId、username、nickname、roles等业务需要的字段。有人图省事直接往ThreadLocal里塞HttpSession或HttpServletRequest我建议别这样一是让上下文和具体容器实现纠缠在一起二是对象太重没必要。关于命名我看到有项目叫UserThreadLocal、CurrentUserHolder、UserContext的都行。我习惯用UserContext因为它的语义更接近“当前用户的上下文环境”而且方便后续扩展——以后如果想在里面增加请求ID、租户ID只改这个类就行。2.3 拦截器入口解析出口清场拦截器是这个方案的执行引擎。它的工作分两步preHandle阶段请求进入Controller之前从Authorization头或Cookie里拿到Token解析Token得到用户信息调用UserContext.set()放入ThreadLocal。这个阶段的代码要尽量“轻”不要把复杂的业务逻辑写在里面以防其他请求进入时阻塞Tomcat工作线程。afterCompletion阶段请求处理完成、视图渲染完毕之后调用UserContext.clear()。这一步是重中之重使用ThreadLocal后必须清理。如果不清理Tomcat的工作线程在处理完A请求后会被归还到线程池线程对象没有销毁ThreadLocalMap还挂在线程上。下一个B请求如果碰巧复用了这个线程在preHandle里没解析出新的用户信息比如B请求的Token过期了get()时就会拿到A请求残留的用户数据造成数据串号。更严重的是线程池里的线程生命周期很长如果每次请求都往ThreadLocal里塞对象而不清理这些对象会一直被线程引用着GC永远回收不了最终引发内存泄漏。public class UserInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } LoginUser loginUser parseToken(token); if (loginUser ! null) { UserContext.set(loginUser); } return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.clear(); } private LoginUser parseToken(String token) { // 这里调用JWT解析或Redis查询返回LoginUser对象 // 解析失败返回null即可不一定在这里抛异常 return null; } }这里有一个细节如果你用了Spring Security JWT通常自己写一个OncePerRequestFilter那么只需要在Filter里解析完Token后调用UserContext.set()然后在finally块里清理即可不一定要用HandlerInterceptor。这两种方式没有绝对的优劣取决于项目的权限框架。用拦截器的好处是能精确控制哪些路径需要解析用户、哪些不需要用Filter的好处是生效时机更早连静态资源也能覆盖到。我这里给出的拦截器是Spring MVC项目最常见的做法。2.4 注册拦截器并配置放行规则光定义拦截器没用还得把它注册进MVC配置里。Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private UserInterceptor userInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(userInterceptor) .addPathPatterns(/**) .excludePathPatterns( /api/auth/login, /api/auth/register, /doc.html, /webjars/**, /favicon.ico ); } }配置里有几个心得addPathPatterns(/**)是拦截所有路径然后通过excludePathPatterns精确放行白名单。白名单包括登录注册接口、Swagger文档、静态资源等不需要用户身份的路径。如果在Spring Security体系中这里配置的是MVC层面拦截器Security的过滤链是更外层的拦截职责不同别搞混了。Security负责认证授权这里负责把认证结果搬运到ThreadLocal。excludePathPatterns的路径匹配规则是Ant通配符/api/auth/可以匹配/api/auth下的多级子路径注意这个和*的区别。完成上面三步一个最小可用的ThreadLocal传用户信息方案就跑通了。Controller层、Service层的代码直接调用UserContext.getUserId()即可方法签名一个多余的参数都不用加。2.5 在业务代码中使用UserContext写一个简单的获取“我的信息”接口来验证效果RestController RequestMapping(/api/user) public class UserController { Resource private UserService userService; GetMapping(/me) public ResultLoginUser me() { Long userId UserContext.getUserId(); // 这里直接通过userId查询用户信息不用Controller接收userId参数 return Result.success(userService.getUserById(userId)); } }Service层如果还需要用户ID做权限校验也可以随时取Service public class OrderService { public ListOrder listMyOrders() { Long userId UserContext.getUserId(); return orderMapper.selectByUserId(userId); } }看起来是不是干净了很多这就是ThreadLocal方案的日常体验。业务代码不需要感知用户信息是怎么传进来的反正只要请求是从登录状态发起的UserContext里就一定拿得到数据。3. ThreadLocal的底层机制与高频踩坑点3.1 ThreadLocalMap的弱引用设计很多人背过“ThreadLocal内存泄漏是因为Entry的key是弱引用”但只知其然不知其所以然。我尽量把这个机制讲清楚。先看数据结构Thread类内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals。ThreadLocalMap的每个Entry是Entry extends WeakReferenceThreadLocal?也就是说Entry的key是一个对ThreadLocal实例的弱引用。弱引用的特点是当JVM进行GC时如果某个对象只被弱引用指向没有任何强引用指向它那么这个对象就会被回收掉。放到这个场景里ThreadLocal实例通常被定义为static final所以它一直存在强引用不会被回收。但如果你把ThreadLocal定义成普通的成员变量甚至局部变量创建的匿名ThreadLocal那么当这个对象不再被业务代码持有时JVM GC时就会把ThreadLocal实例回收掉此时ThreadLocalMap里对应的Entry就变成了key为null的“脏Entry”。key都没了value自然取不出来了但value还被这个Entry强引用着如果线程一直存活比如Tomcat工作线程常驻线程池那value就永远无法被GC回收这就是内存泄漏的根源。所以结论有两个层面如果你的ThreadLocal是static final的key存在强引用不会出现key为null的情况但value依然需要在请求结束时主动remove。如果某处代码临时创建了ThreadLocal比如工具方法里直接new了一个用完还不手动remove那这个线程的ThreadLocalMap里就会残留脏Entry长期积累就会泄漏。这也是我反复强调“set之后务必remove”的原因——不是强迫症而是生产环境里真实的抖动和OOM警报教会我的。3.2 线程池场景下的数据串号ThreadLocal的线程隔离在线程池环境下会遇到一个陷阱。Tomcat的工作线程本身就是一个线程池线程在线程池中是复用的。假设请求A处理到一半当前线程X的ThreadLocal里存了用户A的信息然后请求A代码里向业务线程池提交了一个异步任务Task1Task1被线程池分配到线程Y执行。如果Task1内部尝试通过UserContext.getUserId()拿用户信息拿到的是什么答案是大概率是null甚至可能是上一个复用线程Y的请求残留下来的用户B的信息。为什么因为ThreadLocal是根据当前执行线程存取的Task1在Y线程上执行X线程里的用户A信息对Y来说是不可见的。Y线程被用了多少次、之前执行过哪些任务、有没有清理过ThreadLocal全是未知数所以你可能会取到很奇怪的数据。这个问题在面试中经常被问到真实项目中也不少见。常用的对策有三种在提交异步任务之前把需要传递的用户信息作为参数显式传给任务。简单但不优雅。使用InheritableThreadLocal替代ThreadLocal可以让新建的子线程自动继承父线程的副本值。但注意线程池复用线程时子线程创建一次继承只发生在第一次创建时后续复用不会更新。使用阿里巴巴开源的TransmittableThreadLocalTTL它在InheritableThreadLocal基础上增强了“任务提交时对线程池进行值传递”的能力是目前异步场景最标准的解法。我个人的建议是如果项目里异步任务比较多直接引入TTL依赖让UserContext内部的ThreadLocal替换为TTL的子类业务代码无需感知任何差异。如果异步场景很少就老老实实采用参数传递不要上来就上中间件。dependency groupIdcom.alibaba/groupId artifactIdtransmittable-thread-local/artifactId version2.14.5/version /dependency使用TTL时UserContext内部的持有器改成private static final TransmittableThreadLocalLoginUser HOLDER new TransmittableThreadLocal();然后在线程池提交任务时使用TtlRunnable.get(task)包装一下或者配合TtlExecutors.getTtlExecutorService()包装线程池。这里只是提个方向具体用法建议参考官方文档。3.3 父子线程与异步调用的传递细节这里再展开说一下InheritableThreadLocal和TransmittableThreadLocal的区别避免新手选错。InheritableThreadLocal是JDK自带的类继承自ThreadLocal。当主线程new了一个子线程时子线程构造过程中会把父线程的ThreadLocalMap中的可继承值复制到自己的ThreadLocalMap中。它的局限非常明显只在创建子线程的那一刻拷贝一次。如果父线程在处理过程中多次修改了ThreadLocal中的值子线程拿到的还是创建时那个旧值。对线程池基本无效。线程池里的线程是预先创建好的不是请求来了才创建的所以继承动作根本不是发生在业务请求的那个父线程上的。TransmittableThreadLocal则专门解决了线程池场景下的值传递问题。它的实现思路是在线程池任务提交时捕获当前线程的所有TTL值快照在任务真正执行前回放到执行线程上任务执行完后恢复原值。这样每次任务提交都能拿到最新的父线程数据线程池复用时也不会串号。如果你的项目里用了Async注解、消息队列消费、定时任务、链路追踪等强烈建议用TTL方案。这不是过度设计而是实际分布式系统里排查“用户信息突然消失”时会救命的方案。4. 基于Spring Boot的具体集成与扩展4.1 与Spring MVC拦截器链的协作细节把UserInterceptor放进MVC拦截器链路时要注意执行顺序。如果你在WebMvcConfig里还注册了其他拦截器比如日志拦截器、跨域拦截器、接口耗时统计拦截器它们的执行顺序会影响UserContext里有没有数据。HandlerInterceptor的执行顺序是按照注册顺序执行所有拦截器的preHandle然后进入Controller执行执行完后按照注册顺序的逆序执行所有拦截器的postHandle和afterCompletion。这意味着如果日志拦截器注册在UserInterceptor之前日志拦截器在preHandle里打日志时UserContext还没被set取不到用户信息。如果跨域拦截器注册在UserInterceptor之后并且提前return false了那么其他拦截器的afterCompletion可能不会执行UserContext可能得不到清理。这一点要特别注意最好在UserInterceptor自己的afterCompletion里做好清理不要依赖其他拦截器一定会执行。关于CB跨域配置这个和ThreadLocal本身没有直接关系但如果你项目中配置了CorsFilter注意它的优先级高于MVC拦截器请求先经过CorsFilter再进入拦截器不要被“跨域时预检请求OPTIONS也会被拦截器处理”这类问题恶心到。Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()).order(1); registry.addInterceptor(userInterceptor).order(2); registry.addInterceptor(new RateLimitInterceptor()).order(3); }Spring Boot支持用order()控制拦截器执行顺序数字越小越先执行。建议把UserInterceptor放在比较靠后的位置保证进入业务前提早set清理放在afterCompletion里确保最后执行清理。4.2 配合Spring Security的常见姿势如果你的项目用了Spring Security配置方式略有不同。常见架构是用JWT做无状态认证然后在Security的过滤链中加入一个自定义OncePerRequestFilterComponent public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token resolveToken(request); if (token ! null SecurityContextHolder.getContext().getAuthentication() null) { LoginUser loginUser parseToken(token); if (loginUser ! null) { UserContext.set(loginUser); } } filterChain.doFilter(request, response); } finally { UserContext.clear(); } } }注意我特意用了一个try-finally结构无论后面的Filter或Controller有没有抛异常finally块一定会执行UserContext.clear()这是比afterCompletion更保险的写法。因为Filter的scope比MVC拦截器更外层它能保证ThreadLocal一定被清理。Spring Security版本从5.x升级到6.x后配置方式有些变化比如WebSecurityConfigurerAdapter废弃改为SecurityFilterChain Bean但Filter的写法基本不受影响。4.3 从ThreadLocal到MDC日志链路中的用户信息ThreadLocal存用户信息这件事还能延伸到一个非常实用的场景日志链路追踪。如果每个线程的日志都能自动带上当前操作用户ID排查线上问题时一条用户操作的全链路日志就非常直观。SLF4J自带MDC机制本质也是基于ThreadLocal实现的。你可以在拦截器里把userId塞进MDCMDC.put(userId, String.valueOf(UserContext.getUserId()));然后在logback配置文件的pattern里加上%X{userId}比如pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} userId:%X{userId} - %msg%n/pattern这样所有日志打出来都会带上当前用户的ID。清理的时候除了UserContext.clear()别忘了MDC.remove(userId)。MDC不清理的后果和ThreadLocal不清理是一样的线程池日志全串号。我见过不少项目把用户信息、请求ID、链路追踪ID统一放在一个ThreadLocal持有的TraceContext对象中然后配合MDC输出日志两边共用同一个上下文数据源效果非常理想。4.4 防止ThreadLocal滥用的一点建议说回设计原则ThreadLocal虽然好用但不能乱用。几类不推荐塞进ThreadLocal的场景各位应该要有数大数据对象比如把整个数据库查询结果集合放到ThreadLocal里内存开销大且不易回收容易触发频繁GC。数据库连接、网络连接等有资源生命周期管理的对象这种对象应该由连接池管理而不是ThreadLocal。有状态且会被多个线程共享修改的服务类实例如果对象内部有可变状态不同线程同时访问还会出现可见性问题ThreadLocal解决不了这个。ThreadLocal适合存放的是“线程私有、跨方法传递、生命周期不超过线程工作周期”的数据。用户信息、请求ID、事务上下文、租户信息这类轻量级上下文数据是最佳候选。5. 常见问题与排查技巧实录5.1 问题速查表我把实际开发和线上排查中高频遇到的问题整理成一个速查表方便大家对照处理。现象可能原因解决办法Controller里能拿到用户Service里取到null异步线程导致上下文丢失或者Service层通过Async异步执行使用TTL或显式传参用户A的请求里取到了用户B的信息ThreadLocal未清理线程池复用了残留数据检查afterCompletion或finally块是否执行remove排查是否有拦截器提前return false设置完ThreadLocal后日志里打印出旧userIdMDC未清理或者同一过滤器链中先执行了其他过滤器塞入了MDC在每个过滤器的finally中统一清理MDC和UserContext线程池里执行任务时UserContext里有旧任务的用户信息提交任务时没有清理或者任务结束后没有remove使用TtlRunnable包装或任务开头set任务finally里remove应用内存持续增长且有大量ThreadLocalMap对象长期未调用remove或者创建了大量临时ThreadLocal对代码进行搜索找到所有ThreadLocal的set点确保对应try-finally中remove我明明set了但get还是null可能set和get不在同一个线程也可能set后线程被切换也可能是跨JVM场景打印当前线程名称确认执行线程是否一致排查异步、代理、熔断器导致的线程切换5.2 排查“取不到用户”的定位思路如果你遇到UserContext.get()返回null不要慌。我的排查顺序一般是先确认请求是否真的经过了拦截器。用debugger在preHandle里打一个断点看token是否解析成功set是否执行。不经过拦截器的路径比如404、静态资源、被其他过滤器提前return false都不会set。再确认读数据的线程和执行拦截器的线程是不是同一个。给UserContext的get方法临时加一行日志打印当前线程名再对比拦截器里set时的线程名。不一样就是线程切换了去看异步配置。最后检查有没有全局异常处理器直接捕获异常后重新提交线程池做补偿任务的情况。这种代码会“偷走”线程上下文排查起来最隐蔽要多搜搜。5.3 面试中怎么把ThreadLocal讲清楚ThreadLocal是Java面试高频题如果你正好在准备跳槽我建议按这个逻辑去讲先讲作用和适用场景线程隔离、上下文变量传递。再讲原理Thread类里有ThreadLocalMapkey是ThreadLocal弱引用value是对象。然后讲内存泄漏问题为什么弱引用还会泄漏因为value被强引用线程存活时value无法回收所以必须remove。最后结合Spring Boot项目讲应用拦截器set、afterCompletion清、异步场景的传递问题。如果能讲清楚“为什么JDK设计成弱引用”这个问题面试官会认为你是真的有深度。我的理解是弱引用设计是为了尽量降低“ThreadLocal实例无法被回收”的风险因为ThreadLocal如果是局部变量非static一旦业务代码执行完线程还活着的话强引用可以让ThreadLocal实例继续被引用而弱引用下ThreadLocal实例可以被GC回收从而让Entry失效虽然value还是泄漏但至少key一侧不会再膨胀。这个设计谈不上完美所以官方也建议每次用完手动remove。5.4 推荐一份稳健的代码模板最后分享一个我一直在用的“稳健版”UserContext模板。它在拦截器入口负责设置在finally里负责清理并且提供判断是否登录的方法适合直接嵌进项目public class UserContext { private static final ThreadLocalLoginUser HOLDER new ThreadLocal(); private UserContext() { } public static void set(LoginUser user) { HOLDER.set(user); } public static LoginUser get() { return HOLDER.get(); } public static boolean isLoggedIn() { return HOLDER.get() ! null; } public static Long getUserId() { LoginUser user HOLDER.get(); return user null ? null : user.getUserId(); } public static void clear() { HOLDER.remove(); } }对应拦截器的标准写法Component public class UserContextInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { LoginUser user resolveUserFromRequest(request); if (user ! null) { UserContext.set(user); } return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } private LoginUser resolveUserFromRequest(HttpServletRequest request) { // 伪代码解析token - 查用户信息 - 返回LoginUser return null; } }这套组合经过我多个项目的验证放在通用权限框架里能稳定工作配合日志MDC、权限注解、多租户过滤链路都没问题。如果你现在正好在做Spring Boot项目要处理“当前登录用户”这类上下文传递的需求我强烈建议你在项目早期就建立这个统一上下文机制。别等到Controller、Service、Feign、MQ满天飞了再改造那时候提出“所有方法都要改签名”光是说服同事就会耗掉大量精力。而如果一开始就把这个基础设施搭好不管项目后面怎么膨胀用户信息的传递路径始终是清晰且可控的。
返回列表