1、需求与问题复现
需求:a方法(a线程)里异步执行b方法(b线程),无需等待b执行完(因为b业务复杂,执行太慢,无需等待),a就结束,返回结果给前端。b方法里涉及调用其他微服务,需要用到http请求头信息做鉴权之类的,所以需要设置请求头,将a线程的请求头放到b线程中。
问题:报错了,如下图!因为a线程一结束,requestAttributes就失效了,b线程里获取attribute就会报错。
java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Cannot ask for request attribute - request is not active anymore!
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357) ~[?:1.8.0_252]
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1908) ~[?:1.8.0_252]
Caused by: java.lang.IllegalStateException: Cannot ask for request attribute - request is not active anymore!
at org.springframework.web.context.request.ServletRequestAttributes.getAttribute(ServletRequestAttributes.java:149) ~[spring-web-5.3.4.jar:5.3.4]
代码复现下问题。a方法里异步调用b方法,b方法获取attribute报错。
public void a() {
// 获取requestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 异步执行b方法
CompletableFuture.runAsync(() -> {
try {
b(requestAttributes);
} finally {
RequestContextHolder.resetRequestAttributes();
}
});
System.out.println("主线程:a结束");
}
private void b(RequestAttributes requestAttributes) {
System.out.println("b方法");
// 假设有复杂逻辑,运行个2s
try {
System.out.println("异步:睡2s");
Thread.sleep(2000);
System.out.println("异步:醒了");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 模拟b方法中设置requestAttributes、获取requestAttributes
try {
RequestContextHolder.setRequestAttributes(requestAttributes); // 不报错
System.out.println("异步:requestAttributes保存了——"+requestAttributes);
ServletRequestAttributes re = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();// 不报错
System.out.println("异步:获取到的requestAttributes: "+ re);
HttpServletRequest request = re.getRequest(); // 不报错
System.out.println("异步:获取到的request: "+ request);
String header = request.getHeader("userId"); // 不报错
System.out.println("异步:获取到的Header: "+ header);
Object attribute = re.getAttribute("gray", 0); // 报错
System.out.println("异步:获取到的attribute: "+ attribute);
RequestContextHolder.resetRequestAttributes(); // 不报错
System.out.println("异步:清空requestAttributes: "+ header);
} catch (Exception e) {
System.out.println("出异常了:" + e);
throw new RuntimeException(e);
}
}
运行结果如下:
插播其他小知识-----------------------------------------------
header和attribute的区别:
header是客户端信息。记录http请求信息(请求url、get\post、Cookie等),服务端一般不能修改。
attribute是服务端信息。用于服务器之间存储、传递数据,服务器端可以增删改attribute。
回到正轨-----------------------------------------------
发现当a主线程退出时,b中获取attribute会报错,提示request不是激活状态。这是因为源码里有这么一层判断:
插播其他小知识-----------------------------------------------
上图源码146行中的scope有两个值,如下图。request范围和session范围。
request和session的区别:
request是一次http请求。生命周期是请求发起到请求响应
session是一次会话,对应多次http请求。生命周期是用户与web应用程序的一次交互,通常是用户访问网站到用户关闭浏览器。
如何判断请求是在同一个session里面呢?
http是无状态的。所以session是服务端这边的管理机制。web服务器(如Tomcat、Jetty、LLS等)都会引入会话的概念来记性状态管理。
会话id!!!在http请求头中加入JSESSIONID来记录会话id,如果请求是同一个会话的请求,他们的会话id是一样的。web服务器端一般是通过Cookie或url重写的方式,把会话id给到浏览器客户端,这样客户端下次可以把这个会话id再传回服务端。
回到正轨-----------------------------------------------
2、解决办法
2.1、范围扩大,request范围会报错,但是session范围不会报错。
跳过147行的检查。
re.getAttribute("gray", 0)会报错,但re.getAttribute("gray", 1)不会报错。所以设置attribute的时候,范围选择1,这样获取的时候范围就是1了。
2.2、自己重写getAttribute方法。
这里有两个概念Request和ServletRequestAttributes。后者是对前者的封装,所以先有前者,再有后者。前者是tomcat的,后者是spring的。
2.2.1、继承Request类,来自定义Request类。我这叫HttpServletRequestCopy。
注意:这里需要自己记录下成员变量attributes。当然你也可以记录header,但header也可以不记录,因为header用源代码的getHeader方法获取没有报错。
注意!!!这里需要重写getAttribute方法。这样就可以不用走147行的判断,就不会报错了!!
import org.apache.catalina.connector.Connector;
import org.apache.catalina.connector.Request;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class HttpServletRequestCopy extends Request {
private Map<String, String> header = new HashMap<>();
private Map<String, Object> attributes = new HashMap<>();
/**
* Create a new Request object associated with the given Connector.
*
* @param connector The Connector with which this Request object will always
* be associated. In normal usage this must be non-null. In
* some test scenarios, it may be possible to use a null
* Connector without triggering an NPE.
*/
public HttpServletRequestCopy(Connector connector) {
super(connector);
}
public HttpServletRequestCopy(Map<String, String> header, Map<String, Object> attribute) {
super(null);
if (header != null) {
this.header = header;
}
if (attribute != null) {
this.attributes = attribute;
}
}
@Override
public String getHeader(String name) {
return header.get(name);
}
@Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(header.keySet());
}
@Override
public Object getAttribute(String name) {
return attributes.get(name);
}
}
2.2.2、继承ServletRequestAttributes类来自定义类。我这叫RequestAttributesCopy
import org.springframework.web.context.request.ServletRequestAttributes;
public class RequestAttributesCopy extends ServletRequestAttributes {
public RequestAttributesCopy(HttpServletRequestCopy request) {
super(request);
}
}
2.2.3、开整!
下面就是在a方法中将Spring的requestAttributes复制到自己的RequestAttributesCopy中,这样传递的时候就传递自己的RequestAttributesCopy,获取attribute的时候也是从自己的RequestAttributesCopy中获取。
public void a() {
// 获取requestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 将requestAttributes的header、attribute信息复制到自己的httpServletRequestCopy中
HttpServletRequestCopy httpServletRequestCopy = copyToRequest(((ServletRequestAttributes) requestAttributes).getRequest());
// 生成自己的requestAttributesCopy
RequestAttributesCopy requestAttributesCopy = new RequestAttributesCopy(httpServletRequestCopy);
// 异步执行b方法
CompletableFuture.runAsync(() -> {
try {
// b(requestAttributes);
b(requestAttributesCopy); // b方法什么都不用改,入参传入自己的requestAttributesCopy,他是继承自RequestAttributes
} finally {
RequestContextHolder.resetRequestAttributes();
}
});
System.out.println("主线程:a结束");
}
public HttpServletRequestCopy copyToRequest(HttpServletRequest request) {
// 存储/复制header
Map<String, String> headerMap = new HashMap();
Enumeration<String> headers = request.getHeaderNames();
if (headers != null) {
while (headers.hasMoreElements()) {
String header = headers.nextElement();
headerMap.put(header, request.getHeader(header));
}
}
// 存储/复制attribute
Map<String, Object> attributeMap = new HashMap();
Enumeration<String> attributeNames = request.getAttributeNames();
if (attributeNames != null) {
while (attributeNames.hasMoreElements()) {
String attributeName = attributeNames.nextElement();
attributeMap.put(attributeName, request.getAttribute(attributeName));
}
}
return new HttpServletRequestCopy(headerMap, attributeMap);
}
运行结果如下,这样就不会报错了。这里是null是因为gray这个key,我没有设值,设下值就好了
在这里不得不感慨下继承的伟大,不愧是java的三大特性之一。第三方变成是针对抽象编程,然后我们继承下这个抽象类,重写具体的实现,就能用走第三方的整体逻辑,然后局部逻辑走自己的具体实现。妙哉~