在多租戶項目中如果要開啟一個子線程,那麼需要手動進行RequestAttributes的子線程共享。如果應用場景較少的話可能也不是特複雜,但是如果場景數量上來了,還是很容易忘記的,在測試的時候才會發現疏忽了這一塊。是以想了半天,決定抽取一個公共方法,用來執行這些特定的子線程。
既然要複用這類線程的執行方式,線程池是個不錯的選擇。這裡省略建立線程池的步驟,選擇直接使用spring内已經初始化好的線程池ThreadPoolTaskExecutor。下面寫一個工具類,通過線程池啟動子線程,實作下面幾個内容:
- 使用線程池啟動子線程前擷取目前的RequestAttributes
- 在子線程中開啟RequestAttributes的繼承
- 測試在子線程中能否拿到Request中的租戶資訊
@Component
public class AsyncExecutorUtil {
@Autowired
ThreadPoolTaskExecutor threadPoolTaskExecutor;
public void doMethodWithRequest() {
ServletRequestAttributes sra = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
threadPoolTaskExecutor.execute(()->{
RequestContextHolder.setRequestAttributes(sra, true);
System.out.println(sra.getRequest().getHeader("tenantId"));
});
}
}
複制代碼
使用postman進行測試,發現這樣做确實可以實作Request的傳遞,那麼下一個問題就來了,我怎麼把要執行的方法邏輯傳遞給這個線程呢?可能每次要實際執行的邏輯都不一樣,是以這裡使用函數式接口來傳遞具體方法的實作:
@FunctionalInterface
public interface FunctionInterface {
void doMethod();
}
複制代碼
修改線程池的執行方法,首先儲存目前RequestAttributes,在啟動的子線程中實作對Request的繼承,最後執行函數式接口的方法:
public void doMethodWithRequest(FunctionInterface functionInterface) {
ServletRequestAttributes sra = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
threadPoolTaskExecutor.execute(()->{
RequestContextHolder.setRequestAttributes(sra, true);
System.out.println(sra.getRequest().getHeader("tenantId"));
functionInterface.doMethod();
});
}
複制代碼
在web請求中,在函數式接口中實作實際執行的邏輯,這裡為了使結構更清楚一些沒有使用lambda表達式,如果使用lambda表達式可以使這一段代碼更加簡潔。之後使用上面定義的異步線程工具類在子線程中執行資料庫的查詢:
@RestController
public class TestController {
@Autowired
AsyncExecutorUtil executorUtil;
@GetMapping("users")
public void user() {
executorUtil.doMethodWithRequest(new FunctionInterface() {
@Override
public void doMethod() {
List<User> userList = userService.getUserList();
log.info(userList.toString());
}
});
}
}
複制代碼
檢視執行結果,可以正常執行:
[User(id=2, name=trunks, phone=13788886666, address=beijing, tenantId=2)]
複制代碼
到這為止,不知道大家是不是記得之前提過的一個場景,有些時候第三方的系統在調用我們的接口時可能無法攜帶租戶資訊,之後的所有資料庫查詢都需要我們使用重新手寫sql,并添加SqlParse的過濾。
舉個例子,我們系統中建立訂單,調用微信支付,在前端支付成功後微信會回調我們的接口。這個時候微信是肯定不會攜帶租戶的資訊的,按照之前的做法,我們就需要先根據回調資訊的訂單号先使用過濾過的sql語句查出這筆訂單的資訊,拿到訂單中包含的租戶id,在之後所有被過濾掉的手寫sql中手動拼接這個租戶id。
但是有了上面的結果 ,對我們執行這類的請求可以産生一些改變 。之前我們是向子線程傳遞真實的原始Request,但是目前的Request請求不滿足我們的需求,沒有包含租戶資訊,那麼重新建構一個符合我們需求的Request,并傳遞給子線程,那麼是不是就不用去進行sql的過濾和重寫了呢?
按照上面的步驟,先進行第一步,手寫一個過濾租戶的sql:
public interface OrderMapper extends BaseMapper<Order> {
@SqlParser(filter = true)
@Select("select * from `order` where order_number= #{orderNumber}")
Order selectWithoutTenant(String orderNumber);
}
複制代碼
根據這個請求,能夠查詢出訂單的全部資訊,這裡面就包含了租戶的id:
Order(id=3, orderNumber=6be2e3e10493454781a8c334275f126a, money=100.0, tenantId=3)
複制代碼
接下來重頭戲來了,既然拿到了租戶id,我們就來重新僞造一個Request,讓這個新的Request中攜帶租戶id,并使用這個Request執行後續的邏輯。
@AllArgsConstructor
public class FakeTenantRequest {
private String tenantId;
public ServletRequestAttributes getFakeRequest(){
HttpServletRequest request = new HttpServletRequest() {
@Override
public String getHeader(String name) {
if (name.equals("tenantId")){
return tenantId;
}
return null;
}
//...這裡省略了其他需要重寫的方法,不重要,可不用重寫
};
ServletRequestAttributes servletRequestAttributes=new ServletRequestAttributes(request);
return servletRequestAttributes;
}
}
複制代碼
構造一個HttpServletRequest的過程比較複雜,裡面需要重寫的方法非常多,好在我們暫時都用不上是以不用重寫,隻重寫對我們比較重要的getHeader方法即可。我們在構造方法中傳進來租戶id,并把這個租戶id放在Request的請求頭的tenantId字段,最終傳回RequestAttributes。
線上程池工具類中添加一個方法,在子線程中使用我們僞造的RequestAttributes:
public void doMethodWithFakeRequest(ServletRequestAttributes fakeRequest,
FunctionInterface functionInterface) {
threadPoolTaskExecutor.execute(() -> {
RequestContextHolder.setRequestAttributes(fakeRequest, true);
functionInterface.doMethod();
});
}
複制代碼
模拟回調請求,這時候在請求的Header中不需要攜帶任何租戶資訊:
@GetMapping("callback")
public void callBack(String orderNumber){
Order order = orderMapper.selectWithoutTenant(orderNumber);
log.info(order.toString());
FakeTenantRequest fakeTenantRequest=new FakeTenantRequest(order.getTenantId().toString());
executorUtil.doMethodWithFakeRequest(fakeTenantRequest.getFakeRequest(),new FunctionInterface() {
@Override
public void doMethod() {
List<User> userList = userService.getUserList();
log.info(userList.toString());
}
});
}
複制代碼
檢視執行結果:
- ==> Preparing: select * from `order` where order_number= ?
- ==> Parameters: 6be2e3e10493454781a8c334275f126a(String)
- <== Total: 1
- Order(id=3, orderNumber=6be2e3e10493454781a8c334275f126a, money=100.0, tenantId=3)
- ==> Preparing: SELECT id, name, phone, address, tenant_id FROM user WHERE (id IS NOT NULL) AND tenant_id = '3'
- ==> Parameters:
- <== Total: 1
- [User(id=1, name=hydra, phone=13699990000, address=qingdao, tenantId=3)]
複制代碼
在子線程中執行的sql會經過mybatis-plus的租戶過濾器,在sql中添加租戶id條件。這樣,就實作了通過僞造Request的方式極大程度的簡化了改造sql的過程。