这是 JWT
认证条件下的 getCurrentLoginUser
代码实现,请分析性能:
@Override @ApiOperation("获取当前登录的用户") public User getCurrentLoginUser() { if (this.currentLoginUser != null) { return this.currentLoginUser; } else { // 获取认证数据并查询登陆用户 Claims claims = JwtUtils.getClaims(this.getHttpServletRequest()); Long userId = JwtUtils.getUserId(claims); return this.getAuthInterceptor().getUserById(userId); } }
在生产环境中, currentLoginUser
永远为 null
, if
不执行。
执行 else
内的解析 JWT
的代码,解析 userId
,再查询用户。
很明显,一次请求内,当 getCurrentLoginUser
被多次调用时,会重复解析 JWT
,就会产生性能问题。
解决重复解析 JWT
的唯一思路就是缓存,第一次解析完 userId
并查询出 user
后将这个 user
对象缓存。
因为并发请求时,每个请求分配一个线程管理 Socket
。
所以当前待解决的问题就变成了:如何设计一种缓存,使之各线程不影响,线程安全。
简单的设计如上,一个 Map
, Thread
作为 key
,用户缓存作为 value
。
ThreadLocal
是啥?没听说过是不是?其实这是 Java
里最基础的东西。我的 Java
到底学了个啥呀?
日常吐槽,我发现其他学校竟然讲 spring-boot
、 spring-cloud
、 ConcurrentHashMap
源码,人家上完专业课直接精通 kafka
。
一般人知道 HashMap
的阀值为什么是 8
吗?
对不起,这些人家老师都讲过。而《河北工业大学》的“大博士”,你讲个检查异常都讲错了,被学生指出之后还不改。你去学学 Java
再来讲课好吗?
最后的结果就是,我们辛辛苦苦准备了好几个月的东西,人家课上就精通完了。怪不得干不过人家,我们引以为傲的 spring-cloud
、 RPC
原来都属于课上的基础知识。
继续。
我们想用一个类似 Map<Thread, User>
这样的数据结构来设计缓存,其实 JDK
中早就为我们封装好了,即 ThreadLocal<T>
。
大家来看下面的示例代码:
ThreadLocal<String> local = new ThreadLocal<>(); Thread thread1 = new Thread(() -> { local.set("test1"); System.out.println("线程1 set 完毕"); try { Thread.sleep(100); } catch (InterruptedException e) { } System.out.println("线程1: " + local.get()); }); Thread thread2 = new Thread(() -> { local.set("test2"); System.out.println("线程2 set 完毕"); try { Thread.sleep(100); } catch (InterruptedException e) { } System.out.println("线程2: " + local.get()); }); thread1.start(); thread2.start();
运行结果如下:
main
线程创建 ThreadLocal
对象, thread1
、 thread2
操作的是同一个对象 local
, thread1
、 thread2
分别 set
数据,两线程再次从 local
中获取数据的时候,能够保证两者数据不冲突。
ThreadLocal<String> local = new ThreadLocal<>();
ThreadLocal
中的 set
方法实现如下:
获取当前线程,同时通过线程对象获取 ThreadLocalMap
。
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
set
内调用了 getMap
方法,看看 getMap
的内部实现:
返回线程对象内的 threadLocals
属性。
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
Thread
类中的成员属性 threadLocals
,默认为 null
。
接着看:获取到了 Map
之后,用当前的 ThreadLocal
对象作为 key
存储 value
进 Map
。
if (map != null) map.set(this, value); else createMap(t, value);
这样设计十分地精妙,每一个线程都有独立的 Map
存储,肯定能做到数据隔离且安全。且可方便地创建多个安全的 ThreadLocal
进行存储。
鉴于 ThreadLocal
的特性,我们可以设计一个 SecurityContext
以封装 ThreadLocal<User>
。
@Component public class SecurityContext { private ThreadLocal<User> local = new ThreadLocal<>(); public void set(User user) { local.set(user); } public User get() { return local.get(); } public void clear() { local.remove(); } }
写一个拦截器,思路如下:
在 pre
里解析 JWT
,并存到 SecurityContext
里。
在 post
里 clear
,防止线程池线程复用导致数据错误。
然后原来的 getCurrentLoginUser
方法直接从 SecurityContext
中 get
即可。
看到 SecurityContext
这个名称是不是很熟悉?
从 spring-security
中获取用户信息的方法如下,它怎么实现的呢?
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
点开 spring-security
源码,有三种策略: GlobalSecurityContextHolderStrategy
(即全局线程共享策略)、 ThreadLocalSecurityContextHolderStrategy
(本地策略)、 InheritableThreadLocalSecurityContextHolderStrategy
(可继承的本地策略)。
点开源码后发现,其实 spring-security
就是这么简单,内部也是用 ThreadLocal
实现的,只是此处存储的信息比较多,使用 ThreadLocal<SecurityContext>
。
写框架的也是程序员,只是他们的基础比我们好而已。
醉里且贪欢笑,要愁那得工夫。近来始觉古人书,信著全无是处。
昨夜松边醉倒,问松我醉何如。只疑松动要来扶,以手推松曰去!
——辛弃疾《西江月·遣兴》
教育什么时候能改革呀?我太渺小了,还有人听得见吗?