项目中一些RPC接口需要获取到用户信息才会允许调用,而当我们再一次请求中如果需要调A接口和B接口时,这两个接口业务并没有相关联,且耗时长,首选使用异步来完成,这时候就需要传递threadlocal中的登录信息。看了网上有用框架的,也有借助中间件的。最后想到了一个比较笨的方法,不使用第三方工具来实现这个功能,如果有需要改进的希望大家提出意见。
1.子线程中传递threadlocal的信息.
所谓子线程即直接在当前线程中new Thread.
Thread subThread = new Thread();
这种情况下传递threadlocal是很容易实现的,InheritableThreadLocal和Exchanger
都可以实现,这里就不去讲使用方法了,大家可以自行去研究,但是这两个类在线程池中使用时就会出现问题。接下来讲讲springbooot项目中如何使得线程池中的线程能获取Http请求线程中threadlocal.
2.springbooot+线程池传递threadlocal
(1)先看目录结构:
(2)LoginConfig:登录拦截配置类。这里是拦截所有请求
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Autowired
private LoginInerceptor loginInerceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInerceptor).addPathPatterns()
.addPathPatterns("/**");
}
}
(3)ThreadPoolConfig:注入一个MyThreadPoolExcutor,这是一个自定义线程池,后面会贴代码.
/**
* 注入一个自定义线程池
*/
@Configuration
public class ThreadPoolConfig {
@Bean(name = "myThreadPoolExcutor")
public ThreadPoolExecutor getPoll(){
return new MyThreadPoolExcutor(5,5,60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5));
}
}
(4)LoginController:一个接收测试请求的Controller
@RestController
public class LoginController {
@Autowired
private IProduct product;
@RequestMapping("/dosomething")
public String asyncSearch(){
return product.search();
}
}
(5)LoginInerceptor:用户拦截请求,获取请求中的cookie信息,并且吧信息存入ThreadLocal中.
@Component
public class LoginInerceptor implements HandlerInterceptor {
/**
* 登录拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Cookie[] cookies = request.getCookies();
User user = new User();
if(cookies != null){
for (int i = 0; i <cookies.length ; i++) {
Cookie cookie = cookies[i];
String name = cookie.getName();
if(name != null && name.equals("username"))
user.setUsername(cookie.getValue());
if(name != null && name.equals("token"))
user.setToken(cookie.getValue());
}
}
//把登录信息放入ThreadLocal中
LoginUtil.putInfo(user);
return true;
}
(6)User:用户信息的封装,这里只加了两个字段.
/**
* 用户信息
*/
public class User {
private String username;
private String token;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + token + '\'' +
'}';
}
}
(7)IProduct:这个代码就不贴了。就一个简单的测试方法
(8)IProductImpl:模拟业务处理方法,注入了自定义的线程池,并且在向线程池提交异步任务。
@Service
public class IProductImpl implements IProduct {
@Resource
private MyThreadPoolExcutor myThreadPoolExcutor;
//此时线程为执行http请求的线程,当前线程已经在拦截时存放了登录信息。
//现在模拟提交一个异步任务,并且让线程池的异步线程也拥有当前登录信息。
@Override
public String search() {
//从当前http线程中获取登录信息
User info = LoginUtil.getInfo();
UserTask userTask = new UserTask();
userTask.setUser(info);
//提交线程池任务
myThreadPoolExcutor.execute(userTask);
return "请求完成";
}
}
(9)UserTask:这个类是用于线程池提交任务的,并且这里面加入了user字段,用于保存登录用户信息,这个字段很重要
public class UserTask implements Runnable{
//存放当前会话的用户信息
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public void run() {
//模拟执行业代码
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(10)LoginUtil:工具类,用于获取threadlocal信息
public class LoginUtil {
final static ThreadLocal<User> logoinInfo = new ThreadLocal();
public static void putInfo(User user){
logoinInfo.set(user);
}
public static User getInfo(){
return logoinInfo.get();
}
public static void moveInfo(){
logoinInfo.remove();
}
}
(11)MyThreadPoolExcutor:自定义线程池。这里面最重要的一个方法beforeExecute。这个线程池执行任务前的一个回调函数。
/**
* 自定义线程池
*/
public class MyThreadPoolExcutor extends ThreadPoolExecutor {
public MyThreadPoolExcutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
/**
* 线程池执行前会调用此方法
* @param r
* @param t
*/
@Override
public void beforeExecute(Thread t, Runnable r) {
//这个任务就是我们传入的userTask任务,其中有个字段user就保存了用户登录信息
if(r instanceof UserTask){
UserTask userTask = (UserTask) r;
User user = userTask.getUser();
System.out.println("线程池的线程名字:"+t.getName());
System.out.println("提交任务中用户信息:"+user.toString());
}
}
}
放一段ThreadPoolExcutor中的源码:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//提交任务前会执行此方法
beforeExecute(wt, task);
Throwable thrown = null;
try {
//开始执行向线程池提交的任务
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
//执行完任务后调用这个方法
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
/*Method invoked prior to executing the given Runnable in the given thread. This method is invoked by thread t that will execute task r, and may be used to re-initialize ThreadLocals, or to perform logging.
This implementation does nothing, but may be customized in subclasses. Note: To properly nest multiple overridings, subclasses should generally invoke super.beforeExecute at the end of this method.
Params:
t – the thread that will run task r
r – the task that will be executed
*/
protected void beforeExecute(Thread t, Runnable r) { }
可以发现这个方法在线程池中是一个空方法,t就是线程中执行任务的线程,r就是我们传入的任务.
(12)结果验证
利用postman发起请求,并在请求头中加入请求cookie.
控制台打印:
到此可以发现我们在线程池的线程中以userTask作为载体获取到了用户的登录信息。