【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.FilterType; import org.springframework.stereotype.Controller; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; /** * 参考: * https://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.tmpdir")); 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"; @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}; } @ComponentScan(value = PACKAGE_PATH, excludeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class), // 避免扫描到加了注解(@Configuration)的子容器 @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ServletConfig.class)}) static class AppConfig { } @ComponentScan(value = PACKAGE_PATH) 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); 。。。 }
菜瓜:这样容器就可以用了吗?
水稻:是的,这样就可以直接在浏览器上面访问http://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拦截器链也是日常开发中会用到的功能,顺便熟悉一下请求的执行过程
是谁来自江河湖海,却囿于昼夜厨房与爱