簡介
本文介紹解決SpringBoot子線程無法獲得HttpServletRequest的attribute的問題。
在SpringBoot請求中,如果建立了子線程擷取request的attribute,會無法擷取到。比如:我想記錄日志,将日志放到了request的attribute中:request.setAttribute("logContent", "日志内容"),然後建立子線程去擷取logContent,然後進行處理。
問題複現
代碼
package com.knife.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
@RestController
public class HelloController {
@GetMapping("/test")
public String test() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
request.setAttribute("logContent", "日志内容:" + LocalDateTime.now());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
System.out.println("子線程運作。擷取到的attribute為:"
+ request.getAttribute("logContent"));
}
});
thread.start();
return "test success";
}
}
結果(getRequestAttributes傳回null)
Exception in thread "Thread-131" java.lang.NullPointerException
at com.knife.controller.HelloController$1.run(HelloController.java:26)
at java.lang.Thread.run(Thread.java:748)
解決方案
建立子線程之前調用如下代碼:
RequestContextHolder.setRequestAttributes(servletRequestAttributes, true);
代碼
package com.knife.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
@RestController
public class HelloController {
@GetMapping("/test")
public String test() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
request.setAttribute("logContent", "日志内容:" + LocalDateTime.now());
RequestContextHolder.setRequestAttributes(servletRequestAttributes, true);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
System.out.println("子線程運作。擷取到的attribute為:"
+ request.getAttribute("logContent"));
}
});
thread.start();
return "test success";
}
}
結果(可以擷取到屬性)
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:43:18.463
線程安全性
上邊的解決方案是否有線程安全問題呢?我在子線程裡延時5秒模拟長時間請求,然後多次請求,看結果。如果值都是不一樣的話,應該是線程安全的。(本處我無法百分百确定,實際開發中不太建議這麼寫。實際開發中建議将日志内容作為線程的參數傳入,與attribute剝離開)。
代碼
package com.knife.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
@RestController
public class HelloController {
@GetMapping("/test")
public String test() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
request.setAttribute("logContent", "日志内容:" + LocalDateTime.now());
RequestContextHolder.setRequestAttributes(servletRequestAttributes, true);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ServletRequestAttributes servletRequestAttributes =
(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
System.out.println("子線程運作。擷取到的attribute為:"
+ request.getAttribute("logContent"));
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
return "test success";
}
}
結果(日志内容是新的,應該是線程安全的)
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:42.242
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:42.799
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:43.359
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:43.877
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:44.392
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:44.804
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:45.247
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:45.610
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:46.010
子線程運作。擷取到的attribute為:日志内容:2022-09-03T16:46:46.433