012 Spring boot 上下文context

在上篇 011 Spring Boot 在拦截器中操作数据库 中,我们在拦截器中解析了 token 并进行了数据库的操作来验证用户是否存在。

此外,还有这样一种需求:我们已在拦截器中通过连接数据库,获取了用户信息,我们能否把用户信息共享给 controller、service 等其他层来使用?

因为在每个层里面都可能会用到该用户信息,如果每次用的时候,都去操作数据库,这未免有些资源的浪费,所以既然在拦截器中已做了查询用户信息的这种,那这个数据是否可以共享给其他模块使用?

为了实现这种操作,我们应用到了 spring boot 的上下文:

1. 上下文简介

顾名思义,上下文是存放了当前进程/线程当前栈的数据。

SpringBoot在响应一个请求时,会创建一个线程,而实现上下文的管理,可以通过创建线程本地变量来实现,这样的话,多个请求的响应之间就不会发生上下文的冲突。
管理的线程变量的方式有很多,比较常见的有:

synchronized(锁)
ThreadLocal
这里我们使用的是ThreadLocal,因为synchronized(锁)是采用时间换空间的方式,多个线程(请求响应)之间共用一份变量,当一个线程访问时,其他线程进行等待,这样不仅会造成多线程的效率下降,而且一个线程改变了变量的值,其他线程读取到的就是改变后的值。
而ThreadLocal是为每一个线程提供了变量副本,每个线程使用自己的副本,无论对变量进行什么操作,都不会影响其他线程的变量。
所以我们的上下文管理的实现方式就是,将上下文注册到ThreadLocal中,通过我们的工具类进行赋值和读取。

关于 ThreadLocal

2. 上下文的应用

2.1 新建上下文接口类

package org.example.frame.context;

import java.io.Serializable;
import java.util.Map;

public interface IBaseContext<T> extends Serializable {
    T getCurrentUser();

    void setCurrentUser(T currentUser);

    Map<Object, Object> getProperties();
    public void setProperties(Map<Object, Object> properties);
}

2.2 实现上下文接口

package org.example.frame.controller;

import org.example.entity.User;
import org.example.frame.context.IBaseContext;

import java.util.HashMap;
import java.util.Map;

public class JDZContext implements IBaseContext<User> {
    private User currentUser;
    private Map<Object, Object> properties = new HashMap<>();

    public User getCurrentUser() {
        return currentUser;
    }

    public void setCurrentUser(User currentUser) {
        this.currentUser = currentUser;
    }

    public Map<Object, Object> getProperties() {
        return properties;
    }

    public void setProperties(Map<Object, Object> properties) {
        this.properties = properties;
    }
}

2.3 新建上下文的管理

package org.example.frame.context;

public class JDZBaseContextHolder {
    private static final ThreadLocal<JDZContext> contextHolder = new ThreadLocal<>();

    public JDZBaseContextHolder() {

    }

    public static void setContext(JDZContext context) {
        contextHolder.set(context);
    }

    public static JDZContext getContext() {
        JDZContext obj = contextHolder.get();
        if(obj == null) {
            obj = new JDZContext();
            setContext(obj);
        }
        return obj;
    }
}

2.4 调用

这时就可以在拦截器中把获取的用户信息放入到上下文环境中:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    logger.info("==========preHandle================");

    // 从 http header 中获取 token
    String token = request.getHeader("x-token");
    if(token == null) { // 判断是否有 token 如果没有则抛出异常,终止运行
    throw new JDZException(RtCode.ERR_NEED_TOKEN);
    }

    // 解析 token
    TokenBuilder tokenBuilder = new TokenBuilder();
    RtCode rtCode = tokenBuilder.parse(token);
    if(rtCode != RtCode.SUCCESS) { // token 解析失败
    throw new JDZException(rtCode);
    }
    // token 解析成功,继续往下执行

    User user = userService.getInfo(tokenBuilder.getUid());
    if(user == null) { // 用户不在数据库中
    throw new JDZException(RtCode.ERR_USER_NOT_EXIST);
    }

    // 放入上下文
    JDZContext context = new JDZContext();
    context.setCurrentUser(user);
    JDZBaseContextHolder.setContext(context);

    return true;
}

为了避免在使用 ThreadLocal 时内存泄露,我们还需要手动释放该变量:

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    logger.info("==========afterCompletion================");
    HandlerInterceptor.super.afterCompletion(request, response, handler, ex);

    // 删除 ThreadLocal 内容
    JDZBaseContextHolder.removeContext();
}

在 controller 类中获取上下文用户信息:

@RequestMapping("/user/info")
public User info(Integer uid) {
    JDZContext jdzContext = JDZBaseContextHolder.getContext();
    User user = jdzContext.getCurrentUser();
    System.out.println(user);
    return userService.getInfo(user.getUid()); // uid
}

源码:

《012 Spring boot 上下文context》有1条评论

发表评论