關於 Spring 父子容器的三個問題

關於 Spring 父子容器的三個問題

前言

  對 Spring 父容器和子容器做了一個案例的測試。對於已有的問題進行了一個好的測試。

正文

  我先把本項目的Web啓動類,以及一些基本配置發上來。關於如何構建一個Web項目,可以參數我的這篇文章《純Java啓動Web(無配置web.xml)》

WebApp.java(啓動類)

package vip.wulang.start;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import vip.wulang.config.ChildContext;
import vip.wulang.config.ParentContext;

/**
 * @author CoolerWu on 2018/11/18.
 * @version 1.0
 */
public class WebApp extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ParentContext.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{ChildContext.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

ParentContext.java(父容器也就是 Spring 容器)

package vip.wulang.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

/**
 * @author CoolerWu on 2018/11/18.
 * @version 1.0
 */
@Configuration
@ComponentScan(
        value = "vip.wulang",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = EnableWebMvc.class)
)
public class ParentContext implements ApplicationContextAware {

    private static ApplicationContext parentContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.parentContext = applicationContext;
    }

    public static ApplicationContext getParentContext() {
        return parentContext;
    }
}

ChildContext.java(子容器也就是 Spring MVC 容器)

package vip.wulang.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import vip.wulang.controller.UserController;

/**
 * @author CoolerWu on 2018/11/18.
 * @version 1.0
 */
@Configuration
@EnableWebMvc
@ComponentScan("vip.wulang.controller")
public class ChildContext extends WebMvcConfigurerAdapter implements ApplicationContextAware {

    private static ApplicationContext childContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.childContext = applicationContext;
    }

    public static ApplicationContext getChildContext() {
        return childContext;
    }

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);
        return resolver;
    }

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    
}

UserController.java(Controller類,用於測試)

package vip.wulang.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import static vip.wulang.config.ParentContext.getParentContext;
import static vip.wulang.config.ChildContext.getChildContext;

@Controller
public class UserController {

}

問題一:Spring 父容器和子容器掃描同一個類時,子容器還會創建一個新的bean實例嗎?

  其實這個掃描跟在 Java 配置類裏面寫一個方法並帶有 @Bean 註解是一個道理,倘若 ParentContext 和 ChildContext 都有一個 bean 實例,那肯定不一樣。我們使用 @ComponentScan 註解來掃描同一個包,來看看具體結果,現在 UserController 類添加如下代碼:

	@RequestMapping("/")
	@ResponseBody
	public String getAllUser() {
		System.out.println(getParentContext()); // 父容器
		System.out.println(getChildContext()); // 子容器
		System.out.println(getChildContext().getParent() == getParentContext()); // 驗證是否屬於父子關係
		System.out.println(getParentContext().getBean("userController") == getChildContext().getBean("userController")); // 驗證兩個容器的 userController 是否不同
		return "ok";
	}

Root WebApplicationContext: startup date [Sun Nov 18 15:24:57 CST 2018]; root of context hierarchy

WebApplicationContext for namespace ‘dispatcher-servlet’: startup date [Sun Nov 18 15:24:57 CST 2018]; parent: Root WebApplicationContext

true

false

  看見結果,就知道了。總結:父容器和子容器同時掃描一個類會產生不同實例,並且倘若子容器沒有該實例,可以從父容器裏面獲取。

問題二:一個類被父容器和子容器同時掃描,並且它也實現了 ApplicationContextAware 這個接口,那麼注入的是哪個接口呢,或者說會報錯呢?

  話不多說,進行測試,我還是在原來的基礎上修改了 UserController 一個類而已,代碼如下:

package vip.wulang.controller;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import static vip.wulang.config.ParentContext.getParentContext;
import static vip.wulang.config.ChildContext.getChildContext;

@Controller
public class UserController implements ApplicationContextAware {
	private ApplicationContext applicationContext;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@RequestMapping("/")
	@ResponseBody
	public String getAllUser() {
		System.out.println(applicationContext == getParentContext()); // 是否是父容器
		System.out.println(applicationContext == getChildContext()); // 是否是子容器
		return "ok";
	}

}

false

true

  你以爲就是子容器注入成功了?是的沒錯,它確實是注入成功了,但是父容器也是被注入過,只不過子容器是最後被注入的。稍微修改 UserController 類的方法 setApplicationContext 代碼:

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;

        // 如果是父容器,輸出
		if (applicationContext == getParentContext()) {
			System.out.println("Parent is coming...");
		}
        // 如果是子容器,輸出
		if (applicationContext == getChildContext()) {
			System.out.println("Child is coming...");
		}
	}

  在控制檯上尋找,你會發現:

Parent is coming…

Child is coming…

  原來 Spring 注入 ApplicationContext 類,是按照一定順序注入的,先注入父容器,再注入子容器,最後注入的才能決定真正的 ApplicationContext 的引用類型。

問題三:Spring與SpringMVC的容器衝突的原因到底在那裏?

  我們先把 ChildContext 類的 @ComponentScan(“vip.wulang.controller”) 註釋掉,啓動服務器,會出現 404,表示沒有找到。那我們先把 ChildContext 類的 @ComponentScan(“vip.wulang.controller”) 的註釋去掉,然後把 ParentContext 類的 @ComponentScan 註解中掃描到 vip.wulang.controller 這個包給排除掉,如下:

// ParentContext 類
@ComponentScan(
        value = "vip.wulang",
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = EnableWebMvc.class),
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
        }
)

  啓動服務器,我們會發現該服務器可以正常運作並且返回了JSON “ok”,通過Debug,我們找到了 AbstractHandlerMethodMapping 類,該類的 initHandlerMethods() 方法的作用是掃描ApplicationContext中的bean,檢測和註冊處理程序方法,該方法的源代碼如下:

	protected void initHandlerMethods() {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for request mappings in application context: " + getApplicationContext());
		}
		String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));

		for (String beanName : beanNames) {
			if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
				Class<?> beanType = null;
				try {
					beanType = getApplicationContext().getType(beanName);
				}
				catch (Throwable ex) {
					// An unresolvable bean type, probably from a lazy bean - let's ignore it.
					if (logger.isDebugEnabled()) {
						logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
					}
				}
                // 見下面 RequestMappingHandlerMapping 類的部分源代碼
				if (beanType != null && isHandler(beanType)) {
					detectHandlerMethods(beanName);
				}
			}
		}
		handlerMethodsInitialized(getHandlerMethods());
	}

  RequestMappingHandlerMapping 類的 isHandler() 方法的作用是判斷是否含有 Controller 或者 RequestMapping 註解,該方法的源代碼如下:

	@Override
	protected boolean isHandler(Class<?> beanType) {
		return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
				AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
	}

  我們會發現,Spring MVC 容器只在該容器中查找 bean 實例,而沒有去查找父容器,這就是關鍵所在。根據官方建議我們就可以很好把不同類型的Bean分配到不同的容器中進行管理。所以沒必要讓父容器也掃描 @Controller 註解,可以進行剔除。

結束語

  遇到每一個知識點,都應該自己去發散思維,把自己不懂得都給用代碼測試出來,只有親眼所見才能印象深刻。
  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章