前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring MVC起源篇--01

Spring MVC起源篇--01

作者头像
大忽悠爱学习
发布2022-07-12 16:00:14
3830
发布2022-07-12 16:00:14
举报
文章被收录于专栏:c++与qt学习c++与qt学习

MVC起源

Servlet独行天下的时代

Servlet是java平台第一个用于Web开发的技术,但是在初期使用Servlet的时候,很多开发人员,都将Servlet写的臃肿难以维护,为什么呢?

因为开发人员为了偷懒,将所有逻辑都混杂与一个Servlet之中,从而形成了神奇的Servlet.

代码语言:javascript
复制
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String name = request.getParameter("name");
        String pwd = request.getParameter("pwd");

        try {
            Connection connection = getConn();
            Statement statement = connection.createStatement();
            //数据库访问操作
        } catch (SQLException e) {
            e.printStackTrace();
        }
        
        
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + name + "</h1>");
        out.println("</body></html>");
    }
...
}

代码可维护性差先不提,单单是数据访问逻辑,业务处理逻辑,视图渲染逻辑混杂在一起,就是严重的耦合错误。

因此,如果想要对上面的代码进行解耦合,那么首先就是将三者逻辑剥离开来,

  • 业务处理逻辑和数据访问逻辑剥离出来很简单

通过Service层完成业务处理逻辑,业务处理过程中通过Dao层完成数据访问逻辑,然后Service层返回的数据交给Servlet进行渲染。

看上去好像非常美好,但是视图渲染逻辑依然和Servlet纠缠在了一起,这个怎么解耦呢?

  • 如果每个Servlet类中都存放着一堆HTML代码,你觉得合适吗?

繁盛一时的JSP时代

为了能够将视图渲染逻辑从Servlet中剥离出来,就有了JSP的诞生,虽然JSP本质也是通过视图解释器最终还是会翻译成一个Servlet,然后通过一堆out.printfln输出,但是这个过程被模板化了,开发人员不需要关心。

  • Servlet向相关域中放入属性
代码语言:javascript
复制
@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
      request.setAttribute("name","DHY");
      request.setAttribute("age","18");
      request.getRequestDispatcher("index.jsp").forward(request,response);
    }
}
  • JSP中取出相关域中的属性,然后展示 (具体是模板解释器在解释的时候,会去查询相关域中保存的属性,然后进行解释替换)
代码语言:javascript
复制
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<body>
<H1>我的名字叫做: ${name}</H1>
</body>
</html>

因为JSP中可以内嵌JAVA代码,因此在JSP繁盛时期,大量开发人员将本该属于Service或者Dao层的逻辑,全部一股脑塞入了JSP中,因此原本为了剥离视图渲染逻辑的JSP,由变得耦合起来。

出现上面的情况,不是因为JSP的解决思路出现了错误,而是当时开发者没有严格地界定JSP的基本使命,这才有了后来的MVC时代。

还有问题就是因为大部分开发人员都是采用一个web请求对应一个servlet的方式,所以在项目请求数非常多的情况下,就需要超级多的Servlet类被创建,类越多,越难维护,因此如何解决这个问题,也十分棘手。


Servlet与JSP的联盟

为了区分各个组件的职责,避免乱堆乱放,后来推出了JSP Model标准的诞生。

JSP Model引入了JavaBean,通过JavaBean对相关业务逻辑进行封装,完成初步关注点的分离。

并且由于一开始没有严格限制JSP的职责,导致JSP还是会被乱用,因此后来又规定JSP只能用于视图逻辑渲染,不能在其中增加一堆业务逻辑代码。

此时的JSP Model架构图如下:

在改进后的JSP Model架构中,由Servlet负责管理流程控制,由JSP负责视图渲染,由JavaBean封装业务逻辑并负责与数据层进行交互。

可以看出,此时的JSP Model模型已经初步具备了MVC模式的样子,但是与MVC又有些许不同,下面我们先来回顾一下MVC。


MVC(Model-View-Controller 模型–视图–控制器) 在当今JAVA界尤其是Web开发领域,已经非常成熟了。

MVC中有以下几个组件:

  • 控制器负责接收视图发送的请求并进行处理,它会根据请求条件通知模型进行应用程序状态的更新,之后选择合适的视图展示给用户。
  • 模型通常封装了应用的逻辑以及数据状态,当控制器通知模型进行状态更新的时候,模型封装的相应逻辑将被调用。执行完成后,模型通常会通过事件机制通知状态更新完毕,从而视图可以显示最新数据状态。
  • 视图是面向用户的接口,当用户通过视图发起某种请求的时候,视图将这些请求转发给控制器进行处理,处理流程经控制器和模型之后,最终视图将接收到模型的状态更新通知,然后视图将结合模型数据,更新自身的显示。

但是,最初意义上的MVC模式,在视图与模型间的数据同步工作是采用从模型PUSH到视图的形式完成的。而对于Web应用来说,局限于所用的协议和使用场景,无法实现从模型PUSH数据到视图这样的功能。

所以,我们只能对MVC中的组件的最初作用定义做出调整,由控制器与模型进行交互,在原来的通知模型更新应用程序状态的基础上,还要获取模型更新的结果数据,然后将更新的模型数据一并转发给视图。

也就是说,我们现在改由控制器从模型中PULL数据给视图,这种意义上的MVC称为Web MVC,也就是现在大多说WEB开发框架所用的架构模式。


虽然JSP Model改进后的模型已经非常接近与Web MVC框架了,但是还是存在问题。

从JSP Model的架构图上可以看到,Servlet是作为控制器角色存在的,但是,该架构并没有说明,具体应用程序是只需要一个控制器,还是需要多个控制器,这就造成了如下两种情况:

  • Web应用程序中使用多个Servlet控制器,即一个Servlet对应一个Web请求的处理,这是最初开发人员使用最多的模式。但是这个模式存在很大的问题,因为随着Web请求的增多,我们需要在Web.xml将每一个Servlet映射的URL记录下来,这会导致Web.xml非常臃肿。并且由于Web请求的处理流程将各自分散管理,没有一种集中管理方式,因此不利于整个系统规定开发和维护。
  • Web应用程序中使用单一的Servle作为集中控制器。现在,所有的Web处理请求全部由单一的Servlet控制器处理,但是这个时候还是存在下面几个问题。
    • 因为所有的Web请求都映射到了单一的Servlet控制器中处理,所有,我们需要自己在这个控制器中,对每个请求的URL进行分析,然后判断处理流程的流向。显然,这部分逻辑如果写死了,那么就不方便调整,如果不写死,就需要在此基础上写一套请求映射管理和请求派发逻辑。

正如我们所看到的那样,制约JSP Model2发展的,就是将流程控制等通用相关逻辑进行硬编码的实践方式,这直接导致了JSP Model2架构的不可重用性。 每次启动新的Web应用程序开发,又需要从头编写Servlet控制器的URL分析,以及流程控制等Web层通用逻辑。

这自然就促使我们去除架构中控制逻辑的硬编码,并尽可能的复用Web应用程序开发过程中的一些通用逻辑。


数英雄人物,还看今朝

Web框架存在的意义在于,他们为Web应用程序的开发提供了一套可重复利用的基础设施,这样开发人员只需要关注特定与每个应用程序的逻辑开发工作,而不需要每次都重复哪些可以统一处理的通用逻辑。

当前Web开发框架有如下两种类型:

  • 请求驱动的Web框架,又称为request/response框架,顾名思义,这种框架是基于Serlvet的请求/响应处理模型构建而成的。这种类型的开发框架大都以Web MVC模式为指导,在JSP Model架构基础上进化而来。比如: struts框架,spring mvc框架等。
  • 事件驱动的Web框架,又被称为基于组件的Web开发框架,这种框架采用与Swing等GUI开发框架类似的思想,将视图组件化,由视图中的相应组件触发事件,进而驱动整个处理流程。

这里重点讲解请求驱动框架

对于请求驱动的Web框架来说,他们是基于JSP Model演化而来,那么他们是如何解决JSP Model在实践过程中的问题呢?

首先,对于大多数请求驱动的Web框架而言,他们更倾向于使用单一Servlet作为控制器的实践方式,并且这些框架通常会结合Front Controller和Page Controller模式,对单一Servlet控制器做进一步的改进,对原先过于耦合的各种控制器逻辑进行逐步分离。

具体来说就是原来的单一Servlet作为整个应用程序的Front Controller,该Servlet接收到具体的Web处理请求之后,会参照映射信息,将待处理的Web请求转发给次一级的控制器来处理。

在控制器Servlet接收到Web请求后,他会对Web请求的URL进行分析,然后根据分析结果,并通过相关配置信息,将当前Web请求转发给次一级的控制器类进行处理。

现在,作为Front Controller的Servlet和次级控制器类共同组成了整个应用程序的控制器。

原先单一的控制器Servlet通过将流程控制信息外部化,并分离具体的Web请求处理逻辑给次级控制器类进行处理的方式。

瘦身为灵活而可复用的担当Front Controller的Servlet,有了这样的关注点分离之后,我们就可以提高整个Web应用中控制器逻辑的可复用性。

其实Spring MVC就是上面思想一步步演化而来,如果稍微研究过的小伙伴,很快就可以对应上Spring mvc中相关组件的作用


Spring MVC初探

Spring MVC也是通过Front Controller和Page Controller的概念来分离流程控制逻辑与具体Web请求处理逻辑。

org.springframework.web.servlet.DispatcherServlet就是Spring MVC框架中的Front Controller,它负责接收并处理所有的Web请求,只不过针对具体的处理逻辑,它会委派给它的下一级控制器去实现,即org.springframework.web.servlet.mvc.Controller,也对应着Page Controller的角色定义。

再来探究DispathcherServlet之前,不妨先思考一下Servlet通常都会做什么工作。

  • 获取请求信息,比如请求的路径,各种参数值等
  • 根据请求信息,调用具体的服务对象处理具体的Web请求
  • 处理完后,将要在视图中显示的模型数据通过request进行传递,最后通过RequestDispathcer选择具体的JSP视图并显示。

DispathcerServlet所作的工作和上面说的没有什么不同,唯一的改变就是DispathcerServlet将各项工作细化并分离给了较为独立的角色来完成。


DispathcerServlet的处理流程可以简单概括如下:

  • HandlerMapping—Web请求的处理协调人

既然DispathcerServlet是整个框架的FrontController,当将它注册到Web.xml时,就注定了它要服务于规定的一组Web请求的命运,而不是一个单独的Web请求,如果要将DispathcerServlet注册到web.xml中,通常配置形式如下:

代码语言:javascript
复制
    <servlet>
        <servlet-name>dispathcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispathcerServlet</servlet-class>
        <load-on-startup>2</load-on-startup> 
    </servlet>
    
    <servlet-mapping>
        <servlet-name>dispathcher</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

因为DispathcerServlet会拦截所有的请求,而自己负责处理Web请求和具体的处理类之间的映射匹配关系。

具体的处理方式或者说策略可能多种多样,例如:

  • 例如: "掐头去尾"的处理方式,将Web请求的URL路径去除前面的上下文路径(context path)和最后的扩展名,去最终剩下的路径信息,作为匹配的结果。比如,如下代码将最终以resource作为匹配结果。
代码语言:javascript
复制
http://www.hhh.com/app/resouce.html
  • 以Web请求的URL中存在的某个参数的值作为匹配的标准。比如,当发现请求的路径中存在controller这个参数的话,其值就会被作为匹配结果用来调用具体的处理类,对于如下所示的URL:
代码语言:javascript
复制
http://www.hhh.com/app/resouce.html?controller=hhController

匹配的结果就是hhController

  • 以cookie或者session中的某些信息作为匹配标准,比如,针对某个客户的Web请求,全部转发给一个处理类进行处理。

或者结合Ruby On Rails的理念,我们在开发中规定一些惯例或者说约定,然后以这些惯例或者约定来解析Web请求的URL路径信息,以获取具体的处理类匹配。

可见,如果把映射匹配的逻辑写死在DispatcherServlet中,是无效有效扩展的,而且匹配的方式也可能随着需求而变化。所以,Spring MVC为了能够灵活地处理映射的匹配,引入了HandlerMapping专门管理Web请求到具体的处理类之间的映射关系。

在Web请求叨叨DispathcerServlet之后,Dispatcher将寻找具体的HandlerMapping实例,以获取对应当前Web请求的具体处理类,即Controller.


  • Controller (Web请求具体处理者)

Controller是对应DispatcherServlet的次级控制器,它本身实现了对应某个Web请求的处理逻辑。在我们所使用的HandlerMapping查找到当前Web请求对应哪个Controller的具体实例之后,DispatcherServlet即可获得HandlerMapping返回的结果,并调用Controller的处理方法来处理当前的Web请求。

Controller的处理方法执行完后,将返回一个ModelAndView实例,该对象包含如下两部分信息:

  • 视图的逻辑名称(或者具体的视图实例),DispatcherServlet将根据该视图的逻辑名称来决定为用户显示哪个视图。
  • 模型数据。视图渲染过程中需要将这些模型数据并入视图的显示中。

有了ModelAndView所包含的视图与模型二者信息后,DispatcherServlet就可以进行视图渲染的工作了。


  • ViewResolver和View(视图独立战争的领导者)

对于一个Web框架而言,视图渲染技术一般存在多个,例如Velocity,Freemarker等通用的模板引擎,并且他们不依赖于request对象来传递模型数据,甚至我们也不需要依赖DispatcherServlet来输出最终的视图。

鉴于视图技术存在多种选择,Spring提出了一套基于ViewResolver和View接口的Web视图处理抽象层,来屏蔽Web框在使用不同的Web技术时的差异性。

那么,大家思考一个问题: Spring MVC是如何以统一的方式,将相同的模型数据纳入不同的视图形式并显示的呢?

原生的Servlet自身就提供了两种基本的视图输出方式,即HTML文本输出和二进制内容输出,即

代码语言:javascript
复制
PrintWriter writer=response.getWriter();
ServletOutputStream out= response.getOutputStream();

在HttpServletResponse可以同时支持文本形式和二进制形式的视图输出的前提下,我们只要在最终将视图数据通过HttpServletResponse输出之前,借助于不同的视图技术API,并结合模型数据和相应的模板文件,就能生成最终的视图结果,如下所示:

  • 获取模型(Model)数据
  • 获取视图模板文件(比如: *.JSP, *.VM , *.FM , *.XLS等)
  • 结合视图模板和模型数据,使用相应的视图技术API生成最终视图结果
  • 完成

这样,不管最终生成的视图如何,我们都可以用同样的方式输出他们,但唯一的问题在于,我们不可能将每个视图的生成代码都纳入DispatcherServlet的职权范围。

大家思考一下,如果同时存在多个实现子类时,通常会怎么解决这个问题

  • Interface ----> 统一上层接口,底层无论使用什么视图技术,对我们而言都是透明的

Spring MVC通过引入View接口定义,来统一抽象视图的生产策略。

之后,DispatcherServlet只需要根据Spring Controller处理完毕后通过ModelAndView返回的逻辑视图名称查找到具体的View实现,然后委派该具体的View实现类来根据模型数据,输出具体的视图内容即可。

Spring的View抽象策略如下图所示:

现在,视图模板与模型数据的合并逻辑,以及合并后的视图结果的输出逻辑,全部封装到了相应的View实现类中。

DispatcherServlet只需要根据ModelAndView返回的信息,选择具体的View实现类做最终的具体工作即可。

不过,与HandlerMapping帮助DispatcherServlet查找具体的Controller处理Web请求类似,DispatcherServlet通过ViewResolver来处理逻辑视图名与具体的View实例之间的映射关系。

ViewResolver将根据ModelAndView中的逻辑视图名查找相应的View实现类,然后将查找的结果返回DispatcherServlet。

DispatcherServlet最终会将ModelAndView中的模型数据交给返回的View来处理最终的视图渲染工作。


最后简单来看一下DispatcherServlet的工作流程:


实践出真知

上面讲的都是理论,下面来看一下实际使用时,我们需要做什么

一个基于Spring MVC框架的Web应用,依然是一个遵循Servlet规范的Web应用程序,自然,它也就拥有一个遵循Servlet规范的Web应用程序应有的目录结构和相应的部署描述符文件。

不过,Spring MVC框架会额外增加两个配置文件。

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>dhy</display-name>

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

   <servlet>
       <servlet-name>controller</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <load-on-startup>2</load-on-startup>
   </servlet>

    <servlet-mapping>
        <servlet-name>controller</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    
</web-app>

loadOnStartUp>0时,表示对应Servlet在tomcat启动时,就进行初始化,而不是等到第一次访问时,采去创建

web.xml属于tomcat的配置文件,它是整个Web应用程序的部署描述符文件,我们可以在该文件中配置监听器,过滤器,servelt等。

我们可以看到,对于Spring MVC来说,其在web.xml中注册了一个ContextLoaderListener监听器和DispatcherServelt全局单一控制器。


ContextLoaderListener与/WEB-INF/applicationContext.xml

上面我们在Web.xml中注册了一个ContextLoaderListener,它负责为整个Web应用程序加载顶层的WebApplicationContext(ROOT WebApplicationContext).

该顶层WebApplicationContext主要用于提供应用所使用的中间层服务。我们使用的数据源(DataSource),数据访问对象(DAO),服务器对象(Service)等,都在WebApplicationContext中注册。

可以将其看做ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext,只不过,WebApplicationContext专门用于Web环境下,在这种容器中,我们可以注册scope类型为request或者session的bean。

ContextLoaderListener加载的WebApplicationContext的默认配置文件路径为/WEB-INF/application.xml,这也就是为什么下图中/WEB-INF/目录下存在一个application.xml配置文件的原因。

该文件符合通用的Spring IOC容器配置文件格式。但是,实际开发中,不管是出于团队并行开发效率的考虑,还是处于管理的因素,很少使用单一配置文件的方式。一般会按照应用程序的层次进行配置文件的分割,要么会按照系统功能模块进行配置文件的切割。

当存在多个分割后的配置文件的时候,ContextLoaderListener的默认加载行为将成为我们的制约。这时,我们可以通过在web.xml中指定名称为contextConfigLocation的配置参数来打破默认行为的制约。

代码语言:javascript
复制
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/application.xml,/WEB-INF/applicationContext-module.xml</param-value>
    </context-param>

< param-value >中通过逗号或者空格来分割多个配置文件路径,甚至使用ANT类型的路径表达式。


ContextLoaderListener之外的选择

如果你不得不继续使用Servlet 2.2的Web容器,或者那些并不支持ServletContextListener特性的Servlet 2.3容器,那么不得不放弃ContextLoaderListener,转而使用ContextLoaderServlet加载顶层的WebApplicationContext.

ContextLoaderServlet完成与ContextLoaderListener相同的工作,需要注意的是,我们需要调整load-on-startup的值,让它在当前Web应用程序中使用的其他Servlet之前启动。

我们可以通过下面的配置方式让ContextLoaderServlet工作:

代码语言:javascript
复制
    <servlet>
        <servlet-name>contextLoader</servlet-name>
        <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

load-on-startup数字越小,优先级越大

ContextLoaderServlet和ContextLoaderListener加载相应路径下的容器配置文件,并在构建完成相应的顶层WebApplicationContext之后,将该顶层WebApplicationContext绑定到ServletContext,如果想要获取绑定到ServletContext的WebApplicationContext,我们可以通过WebApplicationContextUtils类,这样就不需要知道顶层WebApplicationContext绑定到ServletContext的时候使用的key是什么。

例如:

代码语言:javascript
复制
//getServletContext()方法来自父类GenericServlet中ServletConfig属性,ServletConfig属性再父类GenericServlet的init方法中被赋值
WebApplicationContext wac=WebApplicationContextUtils.getWebAplicationContext(getServletContext());
//或者--严格模式,如果获取不到wac,就抛出异常
WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
 wac.getBean("serviceBeanName");

其实是因为StandardWrapper继承了ServletConfig接口,所以在wrapper初始化每个servlet,调用其init方法时,会将当前wrapper的外观对象传入(为了隐藏部分接口)

从零开始手写Tomcat的教程11节----StandardWrapper

通过ContextLoaderListener或者ContextLoaderServlet将wac绑定到ServletContext,任何类型的Web应用程序,只要能够获取ServletContext的引用,就能获取并使用该WebApplicationContext。


ServletContext是啥(Tomcat知识点巩固)
  • 先看下面这两张图片,回顾一下Tomcat的大体架构

没读过tomcat源码也没关系,大概弄清楚是怎么肥事就可以了

一个Host对应一个域名,一般我们访问的Http URL都是 www.dhy.com/dhy/xpy/queryOne , 域名后面跟着的就是资源名。

Host下面管理的子容器集合是Context,一般一个Web应用程序只有一个Context,一个Context会映射到一个资源目录,如下所示:

Host也可以映射到一个目录,如果严格按照层级关系的话,context对应的目录,应该是Host目录下的一个子目录,当然tomcat并没有限制死

代码语言:javascript
复制
      <Host name="www.dhy.com"  appBase="dhy"
            unpackWARs="true" autoDeploy="true">
        <context path="/xpy" docBase="xpy" ></context>
      </Host>

context标签中的path其实就是对应我们平常设置的context-path,即表示要访问当前context对应的Web应用程序,域名后面紧跟着的第一级资源路径必须为/xpy, 如果访问www.dhy.com/xpy/user.html, 再没有特别指定user.html位于那个具体路径下的时候,默认会去xpy目录下面寻找。

一个Context下面可以有很多个Servlet,其实上面的ServletContext就可以理解为是站在Context层面的一个资源集合,那么该资源集合会被该Context下面所有的servlet所共享。


口说无凭,下面以tomcat 4源码为例,我们大概看看ServletContext的模样:

  • ServletContext
代码语言:javascript
复制
//ServletContext 是servlet规范的一个接口,tomcat的具体实现类是ApplicationContext 
public class ApplicationContext implements ServletContext {
    /**
     * 可以向ServletContext中存放属性,给该context下面所有的servlets共享
     */
    private HashMap attributes = new HashMap();


    /**
     *和上面不同之处在于,该集合中的属性,是只读的
     */
    private HashMap readOnlyAttributes = new HashMap();


    /**
     * 当前servletContext关联的Context对象
     */
    private StandardContext context = null;


    /**
     * 为了向程序员隐藏ApplicationContext类中某些接口,这里使用了外观模式---我们程序员在开发中拿到的其实是外观对象
     */
    private ServletContext facade = new org.apache.catalina.core.ApplicationContextFacade(this);


    /**
     * 存放初始化参数
     */
    private HashMap parameters = null;

    //ApplicationContext的构造函数也可以说明,一个ApplicationContext必须与一个Context对象相关联才行
    public ApplicationContext(String basePath, StandardContext context) {
        super();
        this.context = context;
        this.basePath = basePath;
    }
    ...
}
  • StanardContext类中也有获取ServletContext 的方法
代码语言:javascript
复制
    /**
     * Return the servlet context for which this Context is a facade.
     */
    public ServletContext getServletContext() {

        if (context == null)
            context = new org.apache.catalina.core.ApplicationContext(getBasePath(), this);
        return (context);

    }

Dispathcher与xxx-servlet.xml

Dispathcer使用了一个外部化的配置文件,来配置Spring MVC框架在处理Web请求过程中涉及到的各个组件,包括HandlerMapping定义,Controller定义,ViewResolver定义等。

该外部化的配置文件存在的默认路径也是/WEB-INF/,名称需要参照web.xml中定义的DispathcerServlet的< servlet-name >来决定。

例如: 我们当前定义的DispathcerServlet对应的< servlet-name>为controller,那么,默认的配置文件即对应/WEB-INF/controller-servlet.xml,也就是说,DispathcerServlet对应的默认配置文件名称,将在< servlet-name >的值的基础加上后缀-servlet.xml。

< servlet-name > -servlet.xml与/WEB-INF/applicationContext.xml相同,也符合Spring IOC容器配置文件格式。

只不过,相对于/WEB-INF/applicationContext.xml来说,< servlet-name >-servlet.xml有自己的职责。其主要负责配置基于Spring MVC框架的Web应用程序使用的各种Web组件。

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    
    <bean name="/infoList.do" class="...InfoListController"/>
</beans>

DispathcerServlet启动之后将加载该配置文件,并构建相应的WebApplicationContext,该WebApplicationContext将之前通过ContextLoaderListener加载的顶层WebApplicationContext(ROOT WebApplicationContext)作为父容器。

这样,如果需要< servlet-name > -servlet.xml中注册的各种面向Web层的组件,也可以注入来自顶层WebApplicationContext的依赖了。

即二者是父子容器的关系。

  • 顶层ROOT的wac和特定于DispathcerServlet的wac之间的逻辑依赖关系。

因为随着Web应用程序的开发,单一的< servlet-name >-servlet.xml配置文件会变得越来越臃肿,因此DispathcerServlet的contextConfigLocation初始化参数可以帮助我们切割配置文件。

代码语言:javascript
复制
   <servlet>
       <servlet-name>controller</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <init-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>WEB-INF/contriller-servlet.xml,/WEB-INF/module-servlet.xml,...</param-value>
       </init-param>
       <load-on-startup>2</load-on-startup>
   </servlet>

多个配置文件,用逗号或者空格分割即可。


小结

ContextLoaderListener负责加载顶层的WebApplicationContext,该顶层的WebApplicationContext将/WEB-INF/applicationContext.xml作为其默认配置文件的路径。

顶层的WebApplicationContext提供的IOC容器主要负责Dao层,Service层,一些domain对象的bean管理。

而对于DispatcherServlet来说,其会去加载< servlet-name > -servlet.xml配置文件并构建对应的WebApplicationContext,并将顶层WebApplicationContext作为其父容器。

由DispatcherServlet构建出的子wac提供的IOC主要负责对controller层相关bean进行管理。

因为子容器可以注入父容器中的bean,因此controller层可以注入service层的bean,而在父子容器的限制下,service层无法注入controller层的bean TIPS: Springboot提供的是一个IOC容器,不存在父子容器之说,不要搞混了哦!


小案例

就实现一个点击a标签,发送请求,经过处理后,跳转到指定页面的功能吧。

从浏览器中点击某个a标签后,Web请求将被发送到DispathcerServlet进行处理。DispathcerServlet将寻求相应的HandlerMapping对Web请求进行分析,然后调用匹配结果对应的Controller实现,具体到当前场景就是我们要实现的HelloController。

HelloController处理完毕将视图名称hello和模型数据一并返回,然后DispathcerServlet借助于相应的ViewResolver,根据返回的视图名称选择相应的视图并显示,这就是整个流程。

下面开始实现:

  • 配置基础装备,在web.xml中配置DispathcerServlet和ContextLoaderListener。还可以额外添加相应的Filter和ServletContextListener以及处理字符编码和Log4j初始化等配置内容。
代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <display-name>dhy</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/application.xml,/WEB-INF/applicationContext-module.xml</param-value>
    </context-param>

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

    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <servlet-name>controller</servlet-name>
    </filter-mapping>

   <servlet>
       <servlet-name>controller</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <load-on-startup>2</load-on-startup>
   </servlet>


    <servlet-mapping>
        <servlet-name>controller</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>


    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>
  • 添加applicationContext.xml配置文件和servletName-servlet.xml配置文件
  • 开发独立的业务逻辑。对于一个设计良好的Web应用程序来说,Web层依赖于业务层对象,但业务层却不应该对Web层有任何依赖。Web层只应该看做是公开业务逻辑的一种视角或者交互方式,这样实现的话,业务层完全可以独立设计并实现,而不需要关心最终通过什么手段将服务公开给用户。
代码语言:javascript
复制
public class HelloService {
    public String hello(){
        return "hello";
    }
}

整个Web应用的中间层服务支持,默认都是通过/WEB-INF/applicationContext.xml注册(即ROOT wac)。所以,我们要将HelloService的实现增加到Web应用程序顶层容器的配置文件中。

代码语言:javascript
复制
    <bean id="helloService" class="com.example.service.HelloService"/>

真实开发中,我们还需要添加数据源和一些其他bean


  • 添加HandlerMapping,DispathcerServlet在接收到Web请求后,将寻求相应HandlerMapping进行Web请求到具体的Controller实现的匹配。所以,我们需要提供一个HandlerMapping的实现,Spring MVC默认提供了多个HandlerMapping实现,我们暂且使用BeanNameUrlHandlerMapping,它将根据URL与Controller的bean定义的名称进行匹配。

HandlerMapping要添加到servletName-servlet.xml配置文件中去

代码语言:javascript
复制
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

Spring MVC在没有配置任何HandlerMapping的情况下,默认使用BeanNameUrlHandlerMapping。

BeanNameUrlHandlerMapping的匹配规则如下: http://host:port/hhh/hhh.do -----> 去当前容器内勋在名为/hhh.do的Controller定义。


  • 实现对应的Controller并添加到配置文件,在Spring mvc起初使用的时候,是通过扩展AbstractController 的方式来实现具体的Controller的。
代码语言:javascript
复制
@Data
public class HelloController extends AbstractController {
    private HelloService helloService;
    private String viewName;
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        String hello = helloService.hello();
        ModelAndView modelAndView = new ModelAndView(viewName);
        modelAndView.addObject("hello",hello);
        return modelAndView;
    }
}

将controller注册到servletname-servlet.xml配置文件中去

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
   
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
    
    <bean name="/hhh.do" class="com.example.controller.HelloController">
        <property name="helloService" ref="helloService"/>
        <property name="viewName" value="hello"/>
    </bean>
</beans>

helloService的依赖注入来自父容器。

BeanNameUrlHandlerMapping要求Controller的bean定义名称以/开头。


  • 添加ViewResolver. 因为上面controller返回的视图名hello,DispatcherServlet并不知道这个逻辑视图名对应的视图实现,因此需要一个视图名解析器。

Spring mvc为ViewResolver提供了多种实现,我们使用JST/JSTL作为视图技术:

代码语言:javascript
复制
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

prefix和suffix属性规定了拼接的前后缀,即我们返回的视图名为hello,会拼接成/WEB-INF/jsp/hello.jsp,即会去拼接后的目录下面寻找对应的文件。


  • 实现hello.jsp
代码语言:javascript
复制
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<body>
<h1>${hello}</h1>
</body>
</html>

  • 测试

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-07-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Spring MVC起源篇--01
  • MVC起源
    • Servlet独行天下的时代
      • 繁盛一时的JSP时代
        • Servlet与JSP的联盟
          • 数英雄人物,还看今朝
          • Spring MVC初探
            • 实践出真知
              • ContextLoaderListener与/WEB-INF/applicationContext.xml
              • Dispathcher与xxx-servlet.xml
              • 小结
              • 小案例
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档