【Spring】內嵌Tomcat&去Xml&調試Mvc
菜瓜:今天聽到個名詞「父子容器」,百度了一下,感覺概念有點空洞,這是什麼核武器?
水稻:你說的是SpringMvc和Spring吧,其實只是一個概念而已,用來將兩個容器做隔離,起到解耦的作用,其中子容器可以拿到父容器的bean,父容器拿不到子容器的。但是SpringBoot出來之後這個概念基本就被淡化掉,沒有太大意義,SpringBoot中只有一個容器了。
菜瓜:能不能給個demo?
水稻:可以。由於現在SpringBoot已經大行其道,Mvc你可能接觸的少,甚至沒接觸過。
- 早些年啟動一個Mvc項目費老鼻子勁了,要配置各種Xml文件(Web.xml,spring.xml,spring-dispather.xml),然後開發完的項目要打成War包發到Tomcat容器中
- 現在可以直接引入Tomcat包,用main方法直接調起。為了調試方便,我就演示一個Pom引入Tomcat的例子
- ①啟動類
-
package com.vip.qc.mvc; import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; import org.apache.catalina.startup.Tomcat; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Controller; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; /** * 參考: * //docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-servlet * <p> * 嵌入tomcat,由Tomcat發起對Spring容器的初始化調用過程 * <p> * - 啟動過程 * * - Servlet規範,Servlet容器在啟動之後會SPI載入META-INF/services目錄下的實現類並調用其onStartup方法 * * - Spring遵循規範實現了ServletContainerInitializer介面。該介面在執行時會收集WebApplicationInitializer介面實現類並循環調用其onStartup方法 * * - 其中AbstractDispatcherServletInitializer * * * - 將spring上下文放入ContextLoaderListener監聽器,該監聽會發起對refresh方法的調用 * * * - 註冊dispatcherServlet,後續會由tomcat調用HttpServletBean的init方法,完成子容器的refresh調用 * * * * @author QuCheng on 2020/6/28. */ public class SpringWebStart { public static void main(String[] args) { Tomcat tomcat = new Tomcat(); try {
// 此處需要取一個目錄 Context context = tomcat.addContext("/", System.getProperty("java.io.tmp")); context.addLifecycleListener((LifecycleListener) Class.forName(tomcat.getHost().getConfigClass()).newInstance()); tomcat.setPort(8081); tomcat.start(); tomcat.getServer().await(); } catch (LifecycleException | ClassNotFoundException | IllegalAccessException | InstantiationException e) { e.printStackTrace(); } } static class MyWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { private final static String PACKAGE_PATH = "com.vip.qc.mvc.controller"; private final static String PACKAGE_PATH_CHILD = "com.vip.qc.mvc.service"; @Override protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected Class<?>[] getRootConfigClasses() { // spring 父容器 return new Class[]{AppConfig.class}; } @Override protected Class<?>[] getServletConfigClasses() { // servlet 子容器 return new Class[]{ServletConfig.class}; } @Configuration @ComponentScan(value = PACKAGE_PATH_CHILD, excludeFilters = @ComponentScan.Filter(classes = Controller.class)) static class AppConfig { } @Configuration @ComponentScan(value = PACKAGE_PATH, includeFilters = @ComponentScan.Filter(classes = Controller.class)) static class ServletConfig { } } } - ②Controller&Service
-
package com.vip.qc.mvc.controller; import com.vip.qc.mvc.service.ServiceChild; 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 javax.annotation.Resource; /** * @author QuCheng on 2020/6/28. */ @Controller public class ControllerT implements ApplicationContextAware { @Resource private ServiceChild child; @RequestMapping("/hello") @ResponseBody public String containter() { child.getParent(); System.out.println("parentContainer"); return "containter"; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("子容器" + applicationContext); System.out.println("子容器中獲取父容器bean" + applicationContext.getBean(ServiceChild.class)); } } package com.vip.qc.mvc.service; import com.vip.qc.mvc.controller.ControllerT; import org.springframework.beans.BeansException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Service; /** * @author QuCheng on 2020/6/28. */ @Service public class ServiceChild implements ApplicationContextAware { // @Resource private ControllerT controllerT; public void getParent() { System.out.println(controllerT); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println("父容器" + applicationContext); try { System.out.println("父容器中獲取子容器bean" + applicationContext.getBean(ControllerT.class)); } catch (NoSuchBeanDefinitionException e) { System.out.println("找不到子容器的bean"); } } }
// 調用SpringWebStart的main方法啟動-會有如下列印
父容器Root WebApplicationContext, started on Sun Jun 28 22:03:52 CST 2020
找不到子容器的bean
子容器WebApplicationContext for namespace 'dispatcher-servlet', started on Sun Jun 28 22:03:58 CST 2020, parent: Root WebApplicationContext
子容器中獲取父容器beancom.vip.qc.mvc.service.ServiceChild@4acfc43a - Demo比較簡單,不過也能反映父子容器的關係
菜瓜:嗯,效果看到了,能不能講一下啟動過程
水稻:稍等,我去下載源碼。上面程式碼演示中已經提前說明了,父子容器的載入是Tomcat依據Servlet規範發起調用完成的
- spring-web源碼包的/META-INF中能找到SPI的實際載入類SpringServletContainerInitializer#onStartup()方法會搜集實現WebApplicationInitializer介面的類,並調用其onStartup方法
- 上面MyWebApplicationInitializer啟動類是WebApplicationInitializer的子類,未實現onStartup,實際調用的是其抽象父類AbstractDispatcherServletInitializer的方法。跟進去
-
@Override public void onStartup(ServletContext servletContext) throws ServletException { //① 創建Spring父容器上下文-對象放入ContextLoadListener,後續調起完成初始化, super.onStartup(servletContext); //② 創建DispatcherServlet對象,後續會由tomcat調用其init方法,完成子容器的初始化工作 registerDispatcherServlet(servletContext); } // ①進來 protected void registerContextLoaderListener(ServletContext servletContext) { // 此處會回調我們啟動類的getRootConfigClasses()方法 - 父容器配置 WebApplicationContext rootAppContext = createRootApplicationContext(); if (rootAppContext != null) { ContextLoaderListener listener = new ContextLoaderListener(rootAppContext); istener.setContextInitializers(getRootApplicationContextInitializers()); servletContext.addListener(listener); } else { logger.debug("No ContextLoaderListener registered, as " + "createRootApplicationContext() did not return an application context"); } } // ②進來 protected void registerDispatcherServlet(ServletContext servletContext) { 。。。 // 此處會回調我們啟動類的getServletConfigClasses()方法 - 子容器配置 WebApplicationContext servletAppContext = createServletApplicationContext(); 。。。 // 初始化的dispatcherServlet,會加入Tomcat容器中-後續調用 // FrameworkServlet#initServletBean()會完成上下文初始化工作 FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); 。。。 }
菜瓜:這樣容器就可以用了嗎?
水稻:是的,這樣就可以直接在瀏覽器上面訪問//localhost:8081/hello,不過這是一個最簡陋的web項目
菜瓜:懂了,最簡陋是什麼意思
水稻:如果我們想加一些常見的Web功能,譬如說攔截器,過濾器啥的。可以通過@EnableWebMvc註解自定義一些功能
-
package com.vip.qc.mvc; import com.vip.qc.mvc.interceptor.MyInterceptor1; import com.vip.qc.mvc.interceptor.MyInterceptor2; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; /** * @author QuCheng on 2020/6/28. */ @Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { @Resource private MyInterceptor1 interceptor1; @Resource private MyInterceptor2 interceptor2; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor1).addPathPatterns("/interceptor/**"); registry.addInterceptor(interceptor2).addPathPatterns("/interceptor/**"); } } package com.vip.qc.mvc.interceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author QuCheng on 2020/6/28. */ @Configuration public class MyInterceptor1 implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("嘻嘻 我是攔截器1 pre"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("嘻嘻 我是攔截器1 post"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("嘻嘻 我是攔截器1 after"); } } package com.vip.qc.mvc.interceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author QuCheng on 2020/6/28. */ @Configuration public class MyInterceptor2 implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("嘻嘻 我是攔截器2 pre"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("嘻嘻 我是攔截器2 post"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("嘻嘻 我是攔截器2 after"); } }
菜瓜:我知道,這裡還有個Mvc請求調用流程和這個攔截器有關。而且這個攔截器不是MethodInterceptor(切面)
水稻:沒錯,說到這裡順便複習一下Mvc的請求過程
- 請求最開始都是通過Tomcat容器轉發過來的,調用鏈:HttpServlet#service() -> FrameworkServlet#processRequest() -> DispatcherServlet#doDispather()
-
1 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { 2 。。。 3 processedRequest = checkMultipart(request); 4 multipartRequestParsed = (processedRequest != request); 5 // 1.返回一個持有methodHandler(按照URL匹配得出的被調用bean對象以及目標方法)調用鏈(攔截器鏈)對象 6 mappedHandler = getHandler(processedRequest); 7 。。。 8 // 2.按照我們現在寫程式碼的方式,只會用到HandlerMethod,其他三種基本不會用 9 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 10 。。。 11 // 3.前置過濾器 - 順序調用 12 if (!mappedHandler.applyPreHandle(processedRequest, response)) { 13 return; 14 } 15 // 4.Actually invoke the handler. 16 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 17 。。。 18 applyDefaultViewName(processedRequest, mv); 19 // 5.後置過濾器 - 逆序調用 20 mappedHandler.applyPostHandle(processedRequest, response, mv); 21 。。。 22 // 6.處理試圖 - 內部render 23 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); 24 } 25 catch (Exception ex) { 26 // 異常處理 27 triggerAfterCompletion(processedRequest, response, mappedHandler, ex); 28 } 29 // 異常處理 30 catch (Throwable err) { 31 triggerAfterCompletion(processedRequest, response, mappedHandler, 32 new NestedServletException("Handler processing failed", err)); 33 } 34 。。。
菜瓜:這個之前看過不少,百度一大堆,不過還是源碼親切
總結:
- 目前基本互聯網項目都是SpringBoot起手了,再難遇到SpringMvc的項目,不過熟悉該流程有利於我們更加深刻的理解Ioc容器
- Mvc攔截器鏈也是日常開發中會用到的功能,順便熟悉一下請求的執行過程