“AOP代理”遇到“雙上下文”

K太狼發表於2016-06-16

最近有小夥伴兒遇到了一個問題來諮詢我,問題大致如下:

他在Service層利用Aspect設定了一個Spring AOP代理,在單元測試以及在service層程式碼上新增代理的時候均沒有發現問題,但是在web服務中的controller層程式碼新增代理的時候卻不成功。

其程式碼大概如下:

@Component
public class CoreBusiness {
     public void doSomething() {
         System.out.println("I did something");
     }    
}

@Controller
public class CoreController {
     public void doSomething() {
         System.out.println("I did something");
     }    
}

@Component
@Aspect
public class CrossCuttingConcern {
      @Before("execution(* com.test.CoreBusiness.*(..)) || execution(* com.test.CoreController.*(..))")
       public void doCrossCutStuff(){
              System.out.println("Doing the cross cutting concern now");
        }
}

同時在Service層有如下的配置:

<aop:aspectj-autoproxy expose-proxy="true"/>

其實要弄清楚這個問題需要明白兩點:

1、雙上下文的概念

2、AOP Proxy的建立機制

我們先來看看什麼情況下我們會用到雙上下文

場景1:

現在假設我們有一個客戶端程式,

private static ApplicationContext context = new ClassPathXmlApplicationContext("client.xml");
context.getBean(name);

在上面的程式中,我們會通過一個上下文載入所有的bean,所以不會涉及到雙上下文的問題。

場景2:

接下來,我們需要開發一個web應用程式,並且使用Tomcat容器,在web.xml中我們新增了如下的配置:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

新增該配置後,web應用在啟動時會載入applicationContext.xml中定義的所有bean,這裡也不會涉及到雙上下文。

場景3:

我們在web應用程式中引入了Spring MVC框架,並在web.xml中進行了如下的配置,

<servlet>
    <servlet-name>springweb</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>   
</servlet>

<servlet-mapping>
    <servlet-name>springweb</servlet-name>
    <url-pattern>*.action</url-pattern>
</servlet-mapping>

如此一來,tomcat啟動的時候會將springweb-servlet.xml中定義的bean進行初始化。

這個場景下依然沒有雙上下文的介入。

場景4:

這一次我們希望解耦web框架與底層的業務邏輯框架,因此我們又對web.xml進行了如下的修改,

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
    <servlet-name>springweb</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

</servlet>

<servlet-mapping>
    <servlet-name>springweb</servlet-name>
    <url-pattern>*.action</url-pattern>
</servlet-mapping>

如此一來,就會出現雙上下文的情形。

因為tomcat啟動時,ContextLoaderListener會初始化所有在applicationContext.xml中定義的beans。

而FrameworkServlet會初始化springweb-servlet.xml中定義的beans。

這時就出現了兩個應用上下文,那麼這兩個上下文是什麼關係呢?

我們來看看FrameworkServlet是如何初始化servlet所需要的上下文的。

    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;
            if (wac instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent -> set
                        // the root application context (if any; may be null) as the parent
                        cwac.setParent(rootContext);
                    }
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
......
}

我們可以看到,initWebApplicationContext方法中的第一句就是獲取根上下文,有興趣的同學可以繼續跟蹤下去,你會發現這個根上下文就是通過ContextLoaderListener載入的應用上下文,到這裡我們可以知道這兩個上下文是父子的關係。

而且,根據Spring的官方文件,我們也可以獲悉到如下的資訊:

As detailed in Section 3.13, “Additional Capabilities of the ApplicationContext”, ApplicationContext instances in Spring can be scoped. In the Web MVC framework, each DispatcherServlet has its own WebApplicationContext, which inherits all the beans already defined in the root WebApplicationContext. These inherited beans can be overridden in the servlet-specific scope, and you can define new scope-specific beans local to a given servlet instance.

到這裡我們已經弄清楚了第一個問題,那麼既然所有的child applicationcontext都可以繼承root applicationcontext,為什麼AOP代理在Controller層不生效呢?

其實原因很簡單,因為Spring AOP Proxy是在上下文初始化的時候通過BeanPostProcessor這個擴充套件點建立的。而aspectj autoproxy的配置是放在Service層的,那麼也就是根上下文初始化的時候會建立相應的AOP proxy,當Controller層程式碼所在的servlet webapplicationcontext初始化時,AOP proxy已經建立完畢了。這時servlet webapplicationcontext並不會感知到根上下文中建立的AOP proxy。到這裡為止就出現了文中開始處提到問題。

問題的原因已經找到了,那麼我們該如何進行處理呢?還是像以往一樣,留給讀者自己思考吧。

相關文章