问题出现
项目中进行国际化适配,系统需要支持多语言转化。系统根据请求头中的header的值判使用哪种语言。
但是在新线程中以异步操作是拿不到request对象的。
// 注意:实例伪代码,不符合编码规范!
@RequestMapping(value = "/queryInfo", method = RequestMethod.POST)
public Result<?> batchIssueInvoice(@RequestBody UserInfo userInfo) {
// 主线程中能够拿到request对象,request不为null
//HttpServletRequest request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String l=getLanguage();
// 新启动一个线程,在子线程中拿不到request对象
new Thread(() -> {
// 此时,request==null
methotest1(userInfo);
methotest2(userInfo);
// request为null,此时无法拿到header中的值,系统无法正常判断语言类型。会默认返回中文!不符合其他语言环境的要求!
}).start();
}
// 获取语言环境的方法
public static String getLanguage(){
HttpServletRequest request;
request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader("language")==null?"cn":request.getHeader("language");
}
public void methotest1(UserInfo userInfo){
getLanguage()
//此处需要获取语言标识进行其他操作
}
public void methotest2(UserInfo userInfo){
getLanguage()
//此处需要获取语言标识进行其他操作
}
解决方案一
将request中的对象作为参数传入子线程中
// 注意:实例伪代码,不符合编码规范!
@RequestMapping(value = "/queryInfo", method = RequestMethod.POST)
public Result<?> batchIssueInvoice(@RequestBody UserInfo userInfo) {
// 主线程中能够拿到request对象,request不为null
// HttpServletRequest request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String l=getLanguage();
// 新启动一个线程,在子线程中拿不到request对象
new Thread(() -> {
// 此时,request==null
methotest1(userInfo,l);
methotest2(userInfo,l);
// request为null,此时无法拿到header中的值,系统无法正常判断语言类型。会默认返回中文!不符合其他语言环境的要求!
}).start();
}
// 获取语言环境的方法
public static String getLanguage(){
HttpServletRequest request;
request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader("language")==null?"cn":request.getHeader("language");
}
public void methotest1(UserInfo userInfo,String language){
//此处需要获取语言标识进行其他操作
}
public void methotest2(UserInfo userInfo,String language){
//此处需要获取语言标识进行其他操作
}
这样虽然能够解决问题,但是在实际的场景中,很多方法中会用到语言标识,如果按照这种方式进行改造需要改造所有这些方法,工作量非常大,并且比较low。
解决方案二
在开启线程之前,将request的信息设置为子线程共享
// 在开启线程之前,将request的信息设置为子线程共享
ServletRequestAttributes ser = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(ser, true);
// 注意:实例伪代码,不符合编码规范!
@RequestMapping(value = "/queryInfo", method = RequestMethod.POST)
public Result<?> batchIssueInvoice(@RequestBody UserInfo userInfo) {
// 主线程中能够拿到request对象,request不为null
String l=getLanguage();
// 在开启线程之前,将request的信息设置为子线程共享
ServletRequestAttributes ser = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(ser, true);
new Thread(() -> {
// 此时,request==null
methotest1(userInfo);
methotest2(userInfo);
// request为null,此时无法拿到header中的值,系统无法正常判断语言类型。会默认返回中文!不符合其他语言环境的要求!
}).start();
}
// 获取语言环境的方法
public static String getLanguage(){
HttpServletRequest request;
request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader("language")==null?"cn":request.getHeader("language");
}
public void methotest1(UserInfo userInfo){
getLanguage()
//此处需要获取语言标识进行其他操作
}
public void methotest2(UserInfo userInfo){
getLanguage()
//此处需要获取语言标识进行其他操作
}
这种方式当主线程没有结束的时候子线程确实可以拿到request对象。但是问题又来了,系统中有一些异步操作,主线程是优先结束于异步线程的。这种解决方案当主线程结束后,异步线程同样会出现拿不到request对象的情况。
解决方案三
用全局静态变量缓存request中所需要的数据
用拦截器拦截请求后,将request中需要的数据缓存到全局静态变量中。需要的时候从静态变量中获取,但是静态两边会出现线程安全问题!
主角登场
既然想到了静态变量的线程安全问题,当然要用Thread Local来解决这个问题!
首先我们再来回忆一下Thread Local的相关知识!
ThreadLocal是一个内部线程存储类,可以在指定线程内存储数据,并且存储的数据只能被指定线程所访问。所以ThreadLocal是线程安全的。
Thread Local的使用其实很简单
public static final ThreadLocal<T> threadLocal=new ThreadLocal<>();
threadLocal.set();
threadLocal.get();
// 源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
我们可以看出Thread Local的底层实现使用了一个ThreadLocalMap,当我们使用set方法时实际上可以看作将当前线程作为Key,我们存入的值作为Value存入ThreadLocalMap中的,当我们get的时候也是从ThreadLocalMap中取值。
解决问题
了解了ThreadLocal的特性之后,采用ThreadLocal缓存Request中所需要的值。
public static final ThreadLocal<String> threadLocal=new ThreadLocal<>();
public static String getLanguage(){
HttpServletRequest request;
try {
request= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}catch (Exception e){
return threadLocal.get()==null?"cn":threadLocal.get();
}
if (null==request||null==request.getHeader("language")){
return threadLocal.get()==null?"cn":threadLocal.get();
}
return request.getHeader("language");
}
// 注意:实例伪代码,不符合编码规范!
@RequestMapping(value = "/queryInfo", method = RequestMethod.POST)
public Result<?> batchIssueInvoice(@RequestBody UserInfo userInfo) {
// 主线程中能够拿到request对象,request不为null
String l=getLanguage();
new Thread(() -> {
// 将request的信息存入子线程
threadLocal.set(l)
methotest1(userInfo);
methotest2(userInfo);
// request为null,此时无法拿到header中的值,系统无法正常判断语言类型。会默认返回中文!不符合其他语言环境的要求!
}).start();
}
这样在子线程无论什么时候需要获取request中的信息都可以获取到了,也保证了线程安全性。