前言

在使用Spring和SpringMVC的老版本进行开发时,我们需要配置很多的xml文件,非常的繁琐,总是让用户自行选择配置也是非常不好的。基于约定大于配置的规定,Spring提供了很多注解帮助我们简化了大量的xml配置;但是在使用SpringMVC时,我们还会使用到WEB-INF/web.xml,但实际上我们是完全可以使用Java类来取代xml配置的,这也是后来SpringBoott的实现原理。本篇就来看看Spring是如何实现完全的零XML配置。

正文

先来看一下原始的web.xml配置:

  1. <!DOCTYPE web-app PUBLIC
  2. "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
  3. "http://java.sun.com/dtd/web-app_2_3.dtd" >
  4. <web-app>
  5. <context-param>
  6. <param-name>contextConfigLocation</param-name>
  7. <param-value>
  8. <!--加载spring配置-->
  9. classpath:spring.xml
  10. </param-value>
  11. </context-param>
  12. <context-param>
  13. <param-name>webAppRootKey</param-name>
  14. <param-value>ServicePlatform.root</param-value>
  15. </context-param>
  16. <listener>
  17. <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  18. <!--<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>-->
  19. </listener>
  20. <servlet>
  21. <servlet-name>spring-dispatcher</servlet-name>
  22. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  23. <init-param>
  24. <!--springmvc的配置文件-->
  25. <param-name>contextConfigLocation</param-name>
  26. <param-value>classpath:spring-dispatcher.xml</param-value>
  27. </init-param>
  28. <load-on-startup>0</load-on-startup>
  29. </servlet>
  30. <servlet-mapping>
  31. <servlet-name>spring-dispatcher</servlet-name>
  32. <url-pattern>/</url-pattern>
  33. </servlet-mapping>
  34. </web-app>

这里各个配置的作用简单说下,context-param是加载我们主的sping.xml配置,比如一些bean的配置和开启注解扫描等;listener是配置监听器,Tomcat启动会触发监听器调用;servlet则是配置我们自定义的Servlet实现,比如DispatcherServlet。还有其它很多配置就不一一说明了,在这里主要看到记住context-paramservlet配置,这是SpringIOC父子容器的体现。在之前的I文章中讲过IOC容器是以父子关系组织的,但估计大部分人都不能理解,除了看到复杂的继承体系,并没有看到父容器作用的体现,稍后来分析。

了解了配置,我们就需要思考如何替换掉这些繁琐的配置。实际上Tomcat提供了一个规范,有一个ServletContainerInitializer接口:

  1. public interface ServletContainerInitializer {
  2. void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
  3. }

Tomcat启动时会调用该接口实现类的onStartup方法,这个方法有两个参数,第二个不用说,主要是第一个参数什么?从哪里来?另外我们自定义的实现类又怎么让Tomcat调用呢?

首先解答最后一个问题,这里也是利用SPI来实现的,因此我们实现了该接口后,还需要在META-INF.services下配置。其次,这里传入的第一个参数也是我们自定义的扩展接口的实现类,我们可以通过我们自定义的接口实现很多需要在启动时做的事,比如加载Servlet,但是Tomcat又是怎么知道我们自定义的接口是哪个呢?这就需要用到@HandlesTypes注解,该注解就是标注在ServletContainerInitializer的实现类上,其值就是我们扩展的接口,这样Tomcat就知道需要传入哪个接口实现类到这个onStartup方法了。来看一个简单的实现:

  1. @HandlesTypes(LoadServlet.class)
  2. public class MyServletContainerInitializer implements ServletContainerInitializer {
  3. @Override
  4. public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
  5. Iterator var4;
  6. if (set != null) {
  7. var4 = set.iterator();
  8. while (var4.hasNext()) {
  9. Class<?> clazz = (Class<?>) var4.next();
  10. if (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()) && LoadServlet.class.isAssignableFrom(clazz)) {
  11. try {
  12. ((LoadServlet) clazz.newInstance()).loadOnstarp(servletContext);
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. }
  19. }
  20. }
  21. public interface LoadServlet {
  22. void loadOnstarp(ServletContext servletContext);
  23. }
  24. public class LoadServletImpl implements LoadServlet {
  25. @Override
  26. public void loadOnstarp(ServletContext servletContext) {
  27. ServletRegistration.Dynamic initServlet = servletContext.addServlet("initServlet", "org.springframework.web.servlet.DispatcherServlet");
  28. initServlet.setLoadOnStartup(1);
  29. initServlet.addMapping("/init");
  30. }
  31. }

这就是Tomcat给我们提供的规范,通过这个规范我们就能实现Spring的零xml配置启动,直接来看Spring是如何做的。

根据上面所说我们可以在spring-web工程下找到META-INF/services/javax.servlet.ServletContainerInitializer配置:

  1. @HandlesTypes(WebApplicationInitializer.class)
  2. public class SpringServletContainerInitializer implements ServletContainerInitializer {
  3. @Override
  4. public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
  5. throws ServletException {
  6. List<WebApplicationInitializer> initializers = new LinkedList<>();
  7. if (webAppInitializerClasses != null) {
  8. for (Class<?> waiClass : webAppInitializerClasses) {
  9. // Be defensive: Some servlet containers provide us with invalid classes,
  10. // no matter what @HandlesTypes says...
  11. if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
  12. WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
  13. try {
  14. initializers.add((WebApplicationInitializer)
  15. ReflectionUtils.accessibleConstructor(waiClass).newInstance());
  16. }
  17. catch (Throwable ex) {
  18. throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
  19. }
  20. }
  21. }
  22. }
  23. if (initializers.isEmpty()) {
  24. servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
  25. return;
  26. }
  27. servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
  28. AnnotationAwareOrderComparator.sort(initializers);
  29. for (WebApplicationInitializer initializer : initializers) {
  30. initializer.onStartup(servletContext);
  31. }
  32. }
  33. }

核心的实现就是WebApplicationInitializer,先看看其继承体系



AbstractReactiveWebInitializer不用管,主要看另外一边,但是都是抽象类,也就是说真的实例也是由我们自己实现,但需要我们实现什么呢?我们一般直接继承AbstractAnnotationConfigDispatcherServletInitializer类,有四个抽象方法需要我们实现:

  1. //父容器
  2. @Override
  3. protected Class<?>[] getRootConfigClasses() {
  4. return new Class<?>[]{SpringContainer.class};
  5. }
  6. //SpringMVC配置子容器
  7. @Override
  8. protected Class<?>[] getServletConfigClasses() {
  9. return new Class<?>[]{MvcContainer.class};
  10. }
  11. //获取DispatcherServlet的映射信息
  12. @Override
  13. protected String[] getServletMappings() {
  14. return new String[]{"/"};
  15. }
  16. // filter配置
  17. @Override
  18. protected Filter[] getServletFilters() {
  19. MyFilter myFilter = new MyFilter();
  20. CorsFilter corsFilter = new CorsFilter();
  21. return new Filter[]{myFilter,corsFilter};
  22. }

这里主要注意getRootConfigClassesgetServletConfigClasses方法,分别加载父、子容器:

  1. @ComponentScan(value = "com.dark",excludeFilters = {
  2. @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
  3. })
  4. public class SpringContainer {
  5. }
  6. @ComponentScan(value = "com.dark",includeFilters = {
  7. @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = {Controller.class})
  8. },useDefaultFilters = false)
  9. public class MvcContainer {
  10. }

看到这两个类上的注解应该不陌生了吧,父容器扫描装载了所有不带@Controller注解的类,子容器则相反,但需要对象时首先从当前容器中找,如果没有则从父容器中获取,为什么要这么设计呢?直接放到一个容器中不行么?先思考下, 稍后解答。

回到onStartup方法中,直接回调用到AbstractDispatcherServletInitializer类:

  1. public void onStartup(ServletContext servletContext) throws ServletException {
  2. super.onStartup(servletContext);
  3. //注册DispatcherServlet
  4. registerDispatcherServlet(servletContext);
  5. }

先是调用父类:

  1. public void onStartup(ServletContext servletContext) throws ServletException {
  2. registerContextLoaderListener(servletContext);
  3. }
  4. protected void registerContextLoaderListener(ServletContext servletContext) {
  5. //创建spring上下文,注册了SpringContainer
  6. WebApplicationContext rootAppContext = createRootApplicationContext();
  7. if (rootAppContext != null) {
  8. //创建监听器
  9. ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
  10. listener.setContextInitializers(getRootApplicationContextInitializers());
  11. servletContext.addListener(listener);
  12. }
  13. }

然后调用createRootApplicationContext创建父容器:

  1. protected WebApplicationContext createRootApplicationContext() {
  2. Class<?>[] configClasses = getRootConfigClasses();
  3. if (!ObjectUtils.isEmpty(configClasses)) {
  4. AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
  5. context.register(configClasses);
  6. return context;
  7. }
  8. else {
  9. return null;
  10. }
  11. }

可以看到就是创建了一个AnnotationConfigWebApplicationContext对象,并将我们的配置类SpringContainer注册了进去。接着创建Tomcat启动加载监听器ContextLoaderListener,该监听器有一个contextInitialized方法,会在Tomcat启动时调用。

  1. public void contextInitialized(ServletContextEvent event) {
  2. initWebApplicationContext(event.getServletContext());
  3. }
  4. */
  5. public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  6. long startTime = System.currentTimeMillis();
  7. try {
  8. // Store context in local instance variable, to guarantee that
  9. // it is available on ServletContext shutdown.
  10. if (this.context == null) {
  11. this.context = createWebApplicationContext(servletContext);
  12. }
  13. if (this.context instanceof ConfigurableWebApplicationContext) {
  14. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
  15. if (!cwac.isActive()) {
  16. // The context has not yet been refreshed -> provide services such as
  17. // setting the parent context, setting the application context id, etc
  18. if (cwac.getParent() == null) {
  19. // The context instance was injected without an explicit parent ->
  20. // determine parent for root web application context, if any.
  21. ApplicationContext parent = loadParentContext(servletContext);
  22. cwac.setParent(parent);
  23. }
  24. configureAndRefreshWebApplicationContext(cwac, servletContext);
  25. }
  26. }
  27. servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
  28. ClassLoader ccl = Thread.currentThread().getContextClassLoader();
  29. if (ccl == ContextLoader.class.getClassLoader()) {
  30. currentContext = this.context;
  31. }
  32. else if (ccl != null) {
  33. currentContextPerThread.put(ccl, this.context);
  34. }
  35. return this.context;
  36. }
  37. }

可以看到就是去初始化容器,这个和之前分析xml解析是一样的,主要注意这里封装了ServletContext对象,并将父容器设置到了该对象中。

父容器创建完成后自然就是子容器的创建,来到registerDispatcherServlet方法:

  1. protected void registerDispatcherServlet(ServletContext servletContext) {
  2. String servletName = getServletName();
  3. Assert.hasLength(servletName, "getServletName() must not return null or empty");
  4. //创建springmvc的上下文,注册了MvcContainer类
  5. WebApplicationContext servletAppContext = createServletApplicationContext();
  6. Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
  7. //创建DispatcherServlet
  8. FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
  9. Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
  10. dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
  11. ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
  12. if (registration == null) {
  13. throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
  14. "Check if there is another servlet registered under the same name.");
  15. }
  16. /*
  17. * 如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
  18. 如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,
  19. 值越小,servlet的优先级越高,就越先被加载
  20. * */
  21. registration.setLoadOnStartup(1);
  22. registration.addMapping(getServletMappings());
  23. registration.setAsyncSupported(isAsyncSupported());
  24. Filter[] filters = getServletFilters();
  25. if (!ObjectUtils.isEmpty(filters)) {
  26. for (Filter filter : filters) {
  27. registerServletFilter(servletContext, filter);
  28. }
  29. }
  30. customizeRegistration(registration);
  31. }
  32. protected WebApplicationContext createServletApplicationContext() {
  33. AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
  34. Class<?>[] configClasses = getServletConfigClasses();
  35. if (!ObjectUtils.isEmpty(configClasses)) {
  36. context.register(configClasses);
  37. }
  38. return context;
  39. }

这里也是创建了一个AnnotationConfigWebApplicationContext对象,不同的只是这里注册的配置类就是我们的Servlet配置了。然后创建了DispatcherServlet对象,并将上下文对象设置了进去。看到这你可能会疑惑,既然父子容器创建的都是相同类的对象,何来的父子容器之说?别急,这个在初始化该上文时就明白了。但是这里的初始化入口在哪呢?没有看到任何监听器的创建和调用。实际上这里的上下文对象初始化是在Servlet初始化时实现的,即init方法,直接来到HttpServletBeaninit方法(分析SpringMVC源码时讲过):

  1. public final void init() throws ServletException {
  2. ...省略
  3. // Let subclasses do whatever initialization they like.
  4. initServletBean();
  5. }
  6. protected final void initServletBean() throws ServletException {
  7. try {
  8. this.webApplicationContext = initWebApplicationContext();
  9. initFrameworkServlet();
  10. }
  11. }
  12. protected WebApplicationContext initWebApplicationContext() {
  13. //这里会从servletContext中获取到父容器,就是通过监听器加载的容器
  14. WebApplicationContext rootContext =
  15. WebApplicationContextUtils.getWebApplicationContext(getServletContext());
  16. WebApplicationContext wac = null;
  17. if (this.webApplicationContext != null) {
  18. // A context instance was injected at construction time -> use it
  19. wac = this.webApplicationContext;
  20. if (wac instanceof ConfigurableWebApplicationContext) {
  21. ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
  22. if (!cwac.isActive()) {
  23. if (cwac.getParent() == null) {
  24. cwac.setParent(rootContext);
  25. }
  26. //容器加载
  27. configureAndRefreshWebApplicationContext(cwac);
  28. }
  29. }
  30. }
  31. if (wac == null) {
  32. wac = findWebApplicationContext();
  33. }
  34. if (wac == null) {
  35. wac = createWebApplicationContext(rootContext);
  36. }
  37. if (!this.refreshEventReceived) {
  38. synchronized (this.onRefreshMonitor) {
  39. onRefresh(wac);
  40. }
  41. }
  42. if (this.publishContext) {
  43. // Publish the context as a servlet context attribute.
  44. String attrName = getServletContextAttributeName();
  45. getServletContext().setAttribute(attrName, wac);
  46. }
  47. return wac;
  48. }

看到这里想你也应该明白了,首先从ServletContext中拿到父容器,然后设置到当前容器的parent中,实现了父子容器的组织,而这样设计好处我想也是很清楚的,子容器目前装载的都是MVC的配置和Bean,简单点说就是Controller,父容器中都是Service,Controller是依赖于Service的,如果不构建这样的层级关系并优先实例化父容器,你怎么实现Controller层的依赖注入成功呢?

总结

本篇结合之前的文章,分析了SpringMVC零XML配置的实现原理,也补充了之前未分析到父子容器关系,让我们能从细节上更加全面的理解SpringIOC的实现原理,相信看完本篇对于SpringBoot的实现你也会有自己的想法。

这一次搞懂Spring Web零xml配置原理以及父子容器关系的更多相关文章

  1. 这一次搞懂Spring事务注解的解析

    前言 事务我们都知道是什么,而Spring事务就是在数据库之上利用AOP提供声明式事务和编程式事务帮助我们简化开发,解耦业务逻辑和系统逻辑.但是Spring事务原理是怎样?事务在方法间是如何传播的?为 ...

  2. 一张图搞懂Spring bean的完整生命周期

    一张图搞懂Spring bean的生命周期,从Spring容器启动到容器销毁bean的全过程,包括下面一系列的流程,了解这些流程对我们想在其中任何一个环节怎么操作bean的生成及修饰是非常有帮助的. ...

  3. SpringBoot零XML配置的Spring Boot Application

    Spring Boot 提供了一种统一的方式来管理应用的配置,允许开发人员使用属性properties文件.YAML 文件.环境变量和命令行参数来定义优先级不同的配置值.零XML配置的Spring B ...

  4. 模拟Springboot一:(零xml配置搭建SSM项目)

    在spring官网文档中无论是spring的基础文档,还是spring-mvc文档都推荐我们使用javaconfig的方式来搭建项目 间接说明 (优点:javaconfig配置>xml配置) 其 ...

  5. Spring和SpringMVC父子容器关系初窥

    一.背景 最近由于项目的包扫描出现了问题,在解决问题的过程中,偶然发现了Spring和SpringMVC是有父子容器关系的,而且正是因为这个才往往会出现包扫描的问题,我们在此来分析和理解Spring和 ...

  6. spring与springMVC的父子容器关系

    背景和概述 在spring与springMVC中通过IOC可以管理bean对象,有两个配置文件可以配置ioc spring的配置文件applicationContext.xmlspringMVC的配置 ...

  7. Spring IOC-基于XML配置的容器

    Spring IOC-基于XML配置的容器 我们先分析一下AbstractXmlApplicationContext这个容器的加载过程. AbstractXmlApplicationContext的老 ...

  8. SpringMVC与Spring的父子容器关系

    问题: 在整合框架的时候有人也许会产生一个问题:能不能只配置一个扫描包加载实现类的扫描驱动,即在根目录下扫描所有的注解(@Controller.@Service.@Repository.@Compne ...

  9. spring之pom.xml配置

    spring之pom.xml配置 <?xml version="1.0" encoding="UTF-8"?> <project xmlns= ...

随机推荐

  1. 在 Linux 系统中如何管理 systemd 服务

    在上一篇文章<Linux的运行等级与目标>中,我介绍过 Linux 用 systemd 来取代 init 作为系统的初始化进程.尽管这一改变引来了很多争议,但大多数发行版,包括 RedHa ...

  2. 需求:一个页面中需要用到多个字典数据。用于下拉选项,同时,需要将其保存为json格式。以便于key,value的相互转换。记录在实现过程中踩的坑

    本文涉及到的知识: Promise,all()的使用 js处理机制 reduce的用法 map的用法 同步异步 需求: 一个页面中需要用到多个字典数据.用于下拉选项,同时,需要将其保存为json格式. ...

  3. Cypress系列(3)- Cypress 的初次体验

    如果想从头学起Cypress,可以看下面的系列文章哦 https://www.cnblogs.com/poloyy/category/1768839.html 前言 这里的栗子项目时 Cypress ...

  4. spring boot 整合 poi 导出excel

    一. 第一种方式 1.首先从中央仓库中导入架包Poi3.14以及Poi-ooxml3.14. <dependency> <groupId>org.apache.poi</ ...

  5. Bitwarden_rs搭建

    最近LastPass网络极其不稳定,正好闲下来找到了Bitwarden_rs这个替代品,感觉不错,分享记录下部署过程. 一.Docker方式部署 #获取镜像 docker pull bitwarden ...

  6. 小谢第7问:js前端如何实现大文件分片上传、上传进度、终止上传以及删除服务器文件?

    文件上传一般有两种方式:文件流上传和base64方式上传,毫无疑问,当进行大文件上传时候,转为base64是不现实的,因此用formData方式结合文件流,直接上传到服务器 本文主要结合vue的来讲解 ...

  7. “造轮运动”之 ORM框架系列(一)~谈谈我在实际业务中的增删改查

    想想毕业已经快一年了,也就是大约两年以前,怀着满腔的热血正式跨入程序员的世界,那时候的自己想象着所热爱的技术生涯会是多么的丰富多彩,每天可以与大佬们坐在一起讨论解决各种牛逼的技术问题,喝着咖啡,翘着二 ...

  8. 使用锚点定位不改变url同时平滑的滑动到锚点位置,不会生硬的直接到锚点位置

    使用锚点定位不改变url同时平滑的滑动到锚点位置,不会生硬的直接到锚点位置 对前端来说锚点是一个很好用的技术,它能快速定位到预先埋好的位置. 但是美中不足的是它会改变请求地址url,当用户使用了锚点的 ...

  9. js基本语法和数据类型

    三种引入方式: 使用JavaScript:前缀构建执行JavaScript代码 使用<script></script>标签来包含JavaScript代码 <body> ...

  10. Vue中将网址、动态网址转为二维码

    1. 首先需要安装相关的依赖包 npm install qrcodejs2 --save 或者 npm install qrcode2 --save 这里选择第二种方式进行安装,如图: 2.templ ...