最近在做一个系统的全局日志拦截记录功能,有一个需要记录的IP地址的信息,我是从HttpServletRequest对象中获取的,但是我发现如果使用线程池以后,记录日志信息会报错,主要是获取不到HttpServletRequest对象。
下面使用代码简单演示一下问题和解决方法:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
package com.learn.util;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class GlobalWebUtils {
/**
* 获取HttpServletRequest对象
*
* @return
*/
public static HttpServletRequest getRequest() {
HttpServletRequest request = null;
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
request = ((ServletRequestAttributes) requestAttributes).getRequest();
}
return request;
}
}
@RestController
public class HelloController {
@GetMapping("/hello")
public void hello(String name) {
// 获取HttpServletRequest对象
HttpServletRequest request = GlobalWebUtils.getRequest();
if (request != null) {
String helloName = request.getParameter("name");
System.out.println("hello," + name);
} else {
System.out.println("获取不到request对象");
}
}
}
启动项目,访问:http://localhost:8080/hello?name=张三
测试结果:
hello,张三
下面改造一下hello方法,使用多线程的方法执行该代码:
@GetMapping("/hello")
public void hello(String name) {
new Thread(() -> {
// 获取HttpServletRequest对象
HttpServletRequest request = GlobalWebUtils.getRequest();
if (request != null) {
String helloName = request.getParameter("name");
System.out.println("hello," + name);
} else {
System.out.println("获取不到request对象");
}
}).start();
}
再次访问,测试结果:
获取不到request对象
可以看到如果使用多线程的话,就获取不到父线程中的HttpServletRequest对象了。
解决方法其实很简单,调用一下RequestContextHolder的setRequestAttributes方法就行了,代码如下:
@GetMapping("/hello")
public void hello(String name) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(requestAttributes,true);
new Thread(() -> {
// 获取HttpServletRequest对象
HttpServletRequest request = GlobalWebUtils.getRequest();
if (request != null) {
String helloName = request.getParameter("name");
System.out.println("hello," + name);
} else {
System.out.println("获取不到request对象");
}
}).start();
}
再次测试,测试结果:
hello,张三
首先看一下setRequestAttributes方法源码:
/**
* Bind the given RequestAttributes to the current thread.
* @param attributes the RequestAttributes to expose,
* or {@code null} to reset the thread-bound context
* @param inheritable whether to expose the RequestAttributes as inheritable
* for child threads (using an {@link InheritableThreadLocal})
*/
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
其实看到源码就茅塞顿开了,主要看一下requestAttributesHolder和inheritableRequestAttributesHolder的类型,就可以知道是怎么实现的了。
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
父类是ThreadLocal类型:
public class NamedThreadLocal<T> extends ThreadLocal<T>
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
父类是InheritableThreadLocal类型:
public class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T>
看到这里是不是就很清晰了呢,简单来说就是,调用setRequestAttributes方法以后就把原来放在ThreadLocal对象中的属性放到InheritableThreadLocal对象中了,这就是为什么子线程可以获取到HttpServletRequest 对象的原因。
如果还有不明白的地方,可以参考一下我的另外两篇博客:
ThreadLocal的理解与应用
InheritableThreadLocal的理解与应用