分析這個問題需要先了解一個東西:ExposeInvocationInterceptor
1.從官方得到以下相關信息:
可以獲知,當使用 AspectJ 時,spring 會添加一個連接器放到 advice 鏈的開頭。但是為何要放這個東西呢?它是干嘛的?
2.看源碼:
** * Interceptor that exposes the current {@link org.aopalliance.intercept.MethodInvocation} * as a thread-local object. We occasionally need to do this; for example, when a pointcut * (e.g. an AspectJ expression pointcut) needs to know the full invocation context. * * <p>Don't use this interceptor unless this is really necessary. Target objects should * not normally know about Spring AOP, as this creates a dependency on Spring API. * Target objects should be plain POJOs as far as possible. * * <p>If used, this interceptor will normally be the first in the interceptor chain. * * @author Rod Johnson * @author Juergen Hoeller */ @SuppressWarnings("serial") public class ExposeInvocationInterceptor implements MethodInterceptor, PriorityOrdered, Serializable { /** Singleton instance of this class */ public static final ExposeInvocationInterceptor INSTANCE = new ExposeInvocationInterceptor(); /** * Singleton advisor for this class. Use in preference to INSTANCE when using * Spring AOP, as it prevents the need to create a new Advisor to wrap the instance. */ public static final Advisor ADVISOR = new DefaultPointcutAdvisor(INSTANCE) { @Override public String toString() { return ExposeInvocationInterceptor.class.getName() +".ADVISOR"; } }; private static final ThreadLocal<MethodInvocation> invocation = new NamedThreadLocal<MethodInvocation>("Current AOP method invocation"); /** * Return the AOP Alliance MethodInvocation object associated with the current invocation. * @return the invocation object associated with the current invocation * @throws IllegalStateException if there is no AOP invocation in progress, * or if the ExposeInvocationInterceptor was not added to this interceptor chain */ public static MethodInvocation currentInvocation() throws IllegalStateException { MethodInvocation mi = invocation.get(); if (mi == null) // 錯誤就是下面拋出來的 throw new IllegalStateException( "No MethodInvocation found: Check that an AOP invocation is in progress, and that the " + "ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that " + "advices with order HIGHEST_PRECEDENCE will execute before ExposeInvocationInterceptor!"); return mi; } /** * Ensures that only the canonical instance can be created. */ private ExposeInvocationInterceptor() { } @Override public Object invoke(MethodInvocation mi) throws Throwable { MethodInvocation oldInvocation = invocation.get(); invocation.set(mi); // 可知其實這個攔截器的作用就是把 MethodInvacation 放到了線程變量 ThreadLocal 中,這樣子線程就可以通過線程變量輕松拿到 MethodInvacation 了 try { return mi.proceed(); } finally { invocation.set(oldInvocation); } } @Override public int getOrder() { return PriorityOrdered.HIGHEST_PRECEDENCE + 1; // 從這里可以知道,它確實是放到了攔截鏈開頭 } /** * Required to support serialization. Replaces with canonical instance * on deserialization, protecting Singleton pattern. * <p>Alternative to overriding the {@code equals} method. */ private Object readResolve() { return INSTANCE; } }
看到源碼上面的解釋大概是這樣的(純Google翻譯):
*暴露當前{@link org.aopalliance.intercept.MethodInvocation}的攔截器 *作為線程本地對象。 我們偶爾需要這樣做; 例如,切入點時 *(例如,AspectJ表達式切入點)需要知道完整的調用上下文。 * *除非確實有必要,否則不要使用此攔截器。 目標對象應該 *通常不了解Spring AOP,因為這會產生對Spring API的依賴。 *目標對象應盡可能是普通的POJO。 * *如果使用,這個攔截器通常是攔截鏈中的第一個。
通過以上的了解,我們基本知道 ExposeInvocationInterceptor 這個攔截器是干嘛的,為啥要放到攔截鏈開頭以及怎么讓它一定會在攔截鏈開頭的。但是還有這幾點有點迷糊:偶爾需要這樣做事咋樣做呢?調用上下文是咋樣調?
3.看栗子(來源標明出處:https://codeday.me/bug/20190410/930090.html,感謝作者的發文):
一般我們會有以下這種代碼:
@Around(...) public Object monitor(ProceedingJoinPoint pjp) throws Throwable { // some code Obj o = pjp.proceed(); // some code }
然后又會有下面這種代碼:
private static ExecutorService executor = Executors.newCachedThreadPool(); @Around(...) public Object monitor(ProceedingJoinPoint pjp) throws Throwable { Object obj = null; Callable<Object> task = new Callable<Object>() { public Object call() { return pjp.proceed(); } }; Future<Object> future = executor.submit(task); try { obj = future.get(timeout, TimeUnit.MILLISECONDS); } catch (TimeoutException ex) { ... } catch (InterruptedException e) { // we ignore this one... } catch (ExecutionException e) { throw e.getCause(); // rethrow any exception raised by the invoked method } finally { future.cancel(true); // may or may not desire this } return obj; }
一旦改成第二種代碼就要出問題了:
No MethodInvocation found: Check that an AOP invocation is in progress, and that the ExposeInvocationInterceptor is upfront in the interceptor chain. Specifically, note that advices with order HIGHEST
4.獲得解決方案
根據以上了解現在大致可以獲知問題就出在 pjp.proceed(),即在 ExposeInvocationInterceptor 這個攔截器之前去執行了 proceed(); 導致拿不到 MethodInvocation。這就是沒有保證 ExposeInvocationInterceptor 在攔截鏈開頭導致的,綜上所述只有保證了這個策略也就是Spring源碼里說的那樣就不會有問題,目前解決辦法就是通過@Order來保證攔截器的執行順序