[Spring] Set exposeProxy property on Advised to true

[Spring] Set exposeProxy property on Advised to true

問題描述

我在工作上有一個需求需要使用多執行緒完整多個工項,每一個工作都是獨立事務交易,所以我使用Java 8 CompletableFuture提供的方法實作Runnable,再利用Spring AOP代理機制處理事務交易,但當我發送Http Request到我的AP Server時,卻收到下述報錯訊息,從中可以得知發生此錯誤的原因有兩種可能,一、沒有打開代理Java @EnableAspectJAutoProxy(exposeProxy = true, proxyTargetClass = true),另一個可能是不是在同一條執行緒使用代理機制,後來我才意識到我使用了不同的執行緒代理是無法生效的。

1
java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Cannot find current proxy: Set exposeProxy property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

我就好奇為什麼AOP代理機制為不能跨執行緒,於是我去翻AopContext的source code,我才發現原來它是從ThreadLocal取得目前的代理類別,ThreadLocal本身特性就專屬於一個執行緒使用,其他的執行緒不能存取、修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class AopContext {
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");

private AopContext() {
}

public static Object currentProxy() throws IllegalStateException {
Object proxy = currentProxy.get();
if (proxy == null) {
throw new IllegalStateException(
"Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " +
"ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.");
}
return proxy;
}
}

範例

以下範例模擬我當時工作時發生的錯誤,在raiseEmployeeSalary()先到MongoDB撈所有員工的資料後,以多執行緒方式替員工加薪數額,由raiseSalary()完成,CompletableFuture.allOf意思是等所有員工加完薪水後,繼續往下作。如同上一節我所描述的,在透過代理機制去呼叫raiseSalary()就會報錯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class EmployeeService {
@Autowired
private EmployeeDao employeeDao;

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void raiseEmployeeSalary(RaiseSalaryBo raiseSalaryBo) throws ExecutionException, InterruptedException {
List<Employee> employees = employeeDao.listAllEmployees();

List<CompletableFuture<?>> tasks = new ArrayList<>();
for (Employee employee : employees) {
tasks.add(CompletableFuture.runAsync(()->((EmployeeService)AopContext.currentProxy()).raiseSalary(employee, raiseSalaryBo.getBonus())));
}

CompletableFuture.allOf(tasks.toArray(new CompletableFuture<?>[0])).get();
}

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void raiseSalary(Employee employee, BigDecimal bonus) {
employeeDao.raiseSalary(employee, bonus);
}
}

解決方法

解決方法十分簡單,如若真的需要切割出子事務交易,那就不能使多執行緒來處理,依據上述範例必須把CompletableFuture移除,程式才能正常執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class EmployeeService {
@Autowired
private EmployeeDao employeeDao;

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
public void raiseEmployeeSalary(RaiseSalaryBo raiseSalaryBo) {
List<Employee> employees = employeeDao.listAllEmployees();

for (Employee employee : employees) {
((EmployeeService) AopContext.currentProxy()).raiseSalary(employee, raiseSalaryBo.getBonus());
}
}

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void raiseSalary(Employee employee, BigDecimal bonus) {
employeeDao.raiseSalary(employee, bonus);
}
}

資料庫更新後的結果

Image