javaEE&从servlet到SpringMVC

6 篇文章 0 订阅
4 篇文章 0 订阅

javaWeb的开发经历了多个阶段;
刚开始时,由于框架等不够成熟,主要使用serlvet,jsp等技术实现,如果需求较为简单,当然可以胜任,不过随着项目复杂度的增加,这种略显"混乱"的方式便有些力所不逮了。
之后SpringMVC的出现,使得开发可以将更多的工作量放在核心任务之上,虽然还是需要做一定的配置,但总的来说,已经很方便了。

这里对比JSP的开发方式,来看一下SpringMVC的代码逻辑。

出于易懂的初衷,一般使用很简单的代码来查看整个流程,暂时不考虑逻辑的合理性

什么是servlet编程?

在古老的servlet时代,后台都是用java代码将html数据一行行打印生成的,代码量极为庞大,这里以一个简单的servlet来看:

TestAServlet.java:

public class TestAServlet extends HttpServlet implements CommonI {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
        out.println("<html>");
        out.println("<head>");
        out.println("<title>Hello World!</title>");
        out.println("</head>");
        out.println("<body>");
        out.println("<h1>Hello World!</h1>");
        out.println("</body>");
        out.println("</html>");
        out.close();
        /*var a =req.getRequestDispatcher("/gc/abc");ing
        a.forward(req,resp );*/
    }
}

该类继承自 HttpServletCommonI 是自定义的用于获取 Logger 的接口,不必过多考虑,Servlet的继承结构图如下:

在这里插入图片描述

这也是Servlet经典继承关系图:

  • Servlet 接口定义了模版方法:

    public interface Servlet {
      void init(ServletConfig var1) throws ServletException;
    
      ServletConfig getServletConfig();
    
      void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    
      String getServletInfo();
    
      void destroy();
    }
    

    出于简单的原则,我们只看个大概就好:

    • init方法用于初始化操作,这个从方法名字也可以看得出来,一般整个生命周期内只调用一次。
    • destroy方法在容器销毁时才会调用。
    • service方法在每次有客户端请求到来时都会调用,用于处理主要的业务逻辑。
  • GenericServlet是个抽象类,主要是使用代理模式在类中保持一个ServletConfig对象,用于获取各种参数或者配置信息(还提供了log方法)

  • HttpServlet抽象类主要是根据Method的不同,将请求路由到不同的方法中:例如**将 GET 请求路由到 doGet 方法 **,至于其他功能,处于简单的情况考虑,都不重要

因此,我们在TestAServlet中重载了doGet方法,就可以正确的处理GET请求,然后按照一贯的做法,在web.xml中,我们配置一下拦截路径就可以了:

<!--servlet:TestAServlet 拦截-->
<servlet>
    <servlet-name>test</servlet-name>
    <servlet-class>com.knowledge.mnlin.znd.TestAServlet</servlet-class>

    <!-- 支持异步请求 -->
    <async-supported>true</async-supported>
</servlet>
<servlet-mapping>
    <servlet-name>test</servlet-name>
    <url-pattern>/servlet</url-pattern>
</servlet-mapping>

这也是之前Servlet开发的基本流程,此时在浏览器中访问:

http://localhost:8080/servlet

就可以获取返回信息:

<html>
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello World!</h1>
    </body>
</html>

从这里看到,html显示的所有内容,包括<html>一类的标签,都是通过 out.println() 直接写出来

但这种方式的弊端很容易发现,代码太过繁琐,可视性等都很差,如果再加上对于<style>或者<script>的处理,那更是不堪重负,对于简单请求还好,其他的就算了。

不过通过Servlet,更容易去把控web框架是如何处理请求信息的。

结合上面的例子,即便我们没有去查看tomcat的源码,也很容易反向推测出被成为Web容器的tomcat,在这个过程中的作用:监听某个端口;解析处理Http协议
解释的说:建立socket连接,因为http只是协议,无法直接监听某个端口(如8080),获取网络数据,因此需要在服务端建立socket-server,当有客户端发送请求时,其实是向 8080 端口 发送了一段数据,该端数据满足 http 协议,经过 tomcat 处理后,转化为可以使用的对象,然后在 servlet 中进行处理。

我们抛却其中可能复杂的部分,以简单的逻辑示图:

在这里插入图片描述

什么是JSP内置对象?

JSP有九大内置对象,这个做开发时,应该都有了解,但对于JSP文件生成的代码(class类),可能不会做太多的关注,这里以一个例子查看jsp到底是怎么被jvm使用的。

当前有一JSP文件:start.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<html>
<head>
    <title><%=basePath.substring(0, 1)%>
    </title>
</head>
<body>
静止的首页信息:<br><br>
<%=basePath%>
</body>
</html>

功能很简单:**获取请求的路径信息,并进行显示,比如请求路径为:

http://localhost:8080/start.jsp

那么,客户端将收到服务端返回的信息(假设路径能正确响应):

<html>
    <head>
        <title>h
    </title>
    </head>
    <body>
静止的首页信息:
        <br>
        <br>
http://localhost:8080/

    </body>
</html>

从上面的html代码可以看到,框架对于jsp的处理,就是将其中有效的以 <% .* %> 声明的部分,或者一些已定义好的标签,用真实的数据进行填充,然后将结果返回给客户端浏览器进行显示。

我们可以查看该jsp对应生成的java/class文件,该文件一般存放在tomcat目录下,类似这样:

E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.java
E:\apache-tomcat-9.0.12\work\Catalina\localhost\war_create\org\apache\jsp\start_jsp.class

即:

tomcat安装目录\work\Catalina\localhost\项目名\org\apache\jsp\*

先查看 java 文件:start_jsp.java

public final class start_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set<java.lang.String> getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set<java.lang.String> getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {
     // 主代码部分,下面会详细介绍
  }
}

这里粘贴出该类所有的代码方便解析。我们先从继承关系来看该jsp文件的结构

在这里插入图片描述

相比较于第一部分介绍Servlet时的 TestAServletJspPageHttpJspPageHttpJspBase所做的事情,只是提供了initdestroy生命周期时的模版方法,再就是将之前依据Method分开的处理逻辑,统一又交还给了_jspService方法。

_jspService方法里面的逻辑层次很清晰:

  • 首先判断请求的Method,jsp 只支持四种方式的请求:GET, HEAD, POST, OPTIONS,如果不是这四种,则直接抛出 405 错误码:METHOD_NOT_ALLOWED

     if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }
    
  • 然后是对于 JSP 内置对象的处理:

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;
    

    接下来是对于这些对象的赋值操作

      response.setContentType("text/html;charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;
    

    除了不经常使用的 exception 对象,剩余的八个都在这里定义好了:

    1. pageContext属于 JSP 加入的,可以进行一些数据的保存读取等等。具体的功能可以参照实现类:org.apache.jasper.runtime.PageContextImpl
    2. page 对应了该class类自身,也就是this
    3. session自不必说,项目开发中使用的很多,详细 创建过程可以参照源码:org.apache.catalina.connector.Requestorg.apache.catalina.connector.RequestFacade
    4. applicationconfig包含的信息比较多,详细功能可参照接口参数。
    5. requestresponse,使用的也很多,_jspService方法参数传入。
    6. out对象其实是从response获取到的输出流对象,具体代码可参照org.apache.jasper.runtime.JspWriterImpl
      this.out = this.response.getWriter();
      

因此,在JSP中,我们才可以直接去使用这些对象,而不用去显式的创建,更根本的说:JSP开发和Servlet开发并无区别,只是开发效率上有了很大的提升。

好了,简单剖析了一下 JSP 和 Servlet ,可以看到,javaEE中最为麻烦的部分,其实 tomcat 已经帮我们处理好了,对于简单的业务需求,完成起来并会特别耗费力气。

但其中路径拦截处理是在很麻烦,需要一直去配置,更麻烦的是,随着业务逻辑的复杂性提高以及需求的变更,使用jsp来进行开发,着实不太方便。

SpringMVC 到底做了什么?

使用SpringMVC进行开发时,会发现配置文件很少,对于web.xml,基本上不需要做什么大的配置,如果不进行国际化或者主题切换(已当前来看,这种需求前端可以自行完成),那么只需要完成两个文件的处理就好:

  1. web.xml 这个配置是不可缺少的,毕竟tomcat容器依据的就是此文件;在该文件中,我们只需要配置一个Servlet,然后拦截所有的网络请求即可;该Servlet即为org.springframework.web.servlet.DispatcherServlet
    <!--spring mvc 配置-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    
    <!-- Spring MVC配置文件的位置-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/contextConfigLocation.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    
  2. contextConfigLocation.xml,该文件用于配置 SpringMVC
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:p="http://www.springframework.org/schema/p"
          xmlns:context="http://www.springframework.org/schema/context"
          xmlns:mvc="http://www.springframework.org/schema/mvc"
          xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
    http://www.springframework.org/schema/mvc/spring-mvc.xsd">
       <mvc:annotation-driven/>
       <!--扫描固定注解内容-->
       <context:component-scan base-package="com.knowledge.mnlin" use-default-filters="true"/>
    </beans>
    

默认的配置信息很少,SpringMVC 本质就是单Servlet应用,在一个Servlet中完成所有的路由选择,请求处理等信息。

之后为了简写,使用 mvc 代替 SpringMVC

跟之前两部分一样,我们根据org.springframework.web.servlet.DispatcherServlet的继承结构,来简单说明 mvc 如何工作的,不过限于篇幅以及mvck框架结构的复杂性,只做大概性的说明。

org.springframework.web.servlet.DispatcherServlet继承关系图:

在这里插入图片描述

这里只看中间主线的继承关系,相比于Servlet开发模式,这里又多了三层:

  1. HttpServletBean 主要是把xml中配置的servlet参数信息设置到类的属性中。

  2. FrameworkServlet初始化了WebApplicationContext,WebApplicationContext类的功能可以从其注释得到:

    Interface to provide configuration for a web application. This is read-only while
    the application is running, but may be reloaded if the implementation supports this.
    

    FrameworkServlet还使用LocaleContextHolder保存了当前请求的Locale信息,使用RequestContextHolder保存了此次请求的request信息,并在本次请求处理结束后进行了还原。

  3. DispatcherServlet做的事情虽然比较多,但层次很清晰;

    1. 在容器init阶段,进行一些全局信息的初始化,主要是初始化 mvc 的几个内置对象

      protected void initStrategies(ApplicationContext context) {
      	//处理包含文件上传的请求
      	initMultipartResolver(context);
      	//处理Locale,对于一些需要做国际化等等的项目,可能需要进行环境处理
      	initLocaleResolver(context);
      	//处理 theme,同locale一样,暂时不考虑这个
      	initThemeResolver(context);
      	//handler-mapping,根据request来确定使用哪个处理器处理网络请求
      	initHandlerMappings(context);
      	//当确定了用来处理本地请求的处理器时,需要找到对应的adapter,adapter用于指定处理器如何处理该请求
      	initHandlerAdapters(context);
      	// 当处理器模块出错时,会调用这个配置的对象去获取默认的视图信息
      	initHandlerExceptionResolvers(context);
      	//当View层没有值时,通过该方法获取一个ViewName
      	initRequestToViewNameTranslator(context);
      	//view 层获取到值时,通过解析来获取真正的 渲染的视图对象
      	initViewResolvers(context);
      	//在 重定向 时,获取第一次请求的参数信息
      	initFlashMapManager(context);
      }
      

      这些内置对象在下面还会做出解释,这里先看一个大概。

    2. 在 请求到来时,每次都会调用doService方法,该方法会在真正处理请求前,将一些locale,flashMap,theme等信息添加到request对象中,但这些对象一般我们并不用,所以直接略过,直接查看“真正”的方法doDispatch

    3. doDispatch会先检查是否为文件上传请求,是的话调用initMultipartResolver方法配置的内置对象先处理一次;然后,根据initHandlerMappings配置的 一些 hanlder-mapping,找到适合的处理器:handler;然后根据initHandlerAdapters配置好的数据,找到适合该处理器的适配器:adapter;然后结合adapterhandler从本次request得到 一个ModelAndView,看这个名字就很清楚,包含了viewmodel两个模块,一个视图,一个数据,进行数据填充后就可以得到最终的返回数据(当然,这个填充数据的过程需要使用initViewResolvers配置好的view-resovler来获取可用代码处理的view层)。

    4. 正常的逻辑基本就像上面所述,当然如果中间出现了异常,或者说view层为null,那就需要使用到initHandlerExceptionResolvers以及initRequestToViewNameTranslator配置的内置对象了。

好了,目前为止,已经浅析了 servlet 与 mvc 模式以及部分源码,关于两者之间可使用的“内置对象”,也做了简单的说明,不过,仅仅从源码,我们无法开出springmvc 开发的简便之处,这里可以使用一个例子来进行说明:

@Controller
@RequestMapping(value = "/gc")
public class GoController implements EnvironmentAware, CommonI {
    /**
     * 处理GET类型的"/index"和”/ind”请求
     * <p>
     * 匹配 @RequestMapping 注解的是 RequestMappingHandlerMapping
     */
    @RequestMapping(value = {"/index", "/ind"}, method = {RequestMethod.GET})
    public String index(Model model) throws Exception {
        getL().info("get请求:访问index或者ind");
        model.addAttribute("msg", "Go Go Go!");
        return "index.jsp";
    }
}

相比于 servlet 的需要在 xml 中进行路径配置,mvc只需要使用注解就可以实现同样的需求,并且结构更为简单。

这些注解是由 mvc 框架自动解析配置的,无需进行“人工”干涉;之前 servlet 开发好像将访问路径拦截到了具体的HttpServlet子类,mvc 像是根据访问路径先拦截到具体的标注了@Controller的类,然后又拦截到了类中具体的方法(事实上并非这么简单,只是在理解上可能会比较方便一些);

如果查看 mvc 具体负责请求处理的类源码,会很复杂,如果有兴趣可以参照看透springmvc源代码分析与实践;框架可能会在更迭中有所升级,不过核心原理一般不会变更。这里仅以 debug 模式来查看一下,mvc 自带的 “内置对象” 是哪些类:

在这里插入图片描述

按照前面initStrategies方法中内置对象出现的顺序,在初始化之后,我们可以查看到各个内置对象的值(有些是列表,有些是单个对象):

  • multipart-resolver :mvc 框架内置默认没有处理文件上传,需要的话可以自己定义。
  • locale-resolver:语言环境处理,默认有实现,不过用的比较少。
  • theme-resolver:主题切换处理,默认有实现,用的较少
  • flash-map:重定向时使用,用session实现,知道使用就好

然后看剩余的几个:

在这里插入图片描述

  • request-to-view-name-translator:默认实现是DefaultRequestToViewNameTranslator类,当没有明确指定 view 时,会从 request 中取得请求路径,然后加上前后缀,这里前后缀都是空串:"",因此取值就是路径
    @Override
    public String getViewName(HttpServletRequest request) {
       String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
       return (this.prefix + transformPath(lookupPath) + this.suffix);
    }
    
  • view-resolver:当返回的 ModelAndView 中 view 是字符串时,我们需要解析对应的真正可使用的 view 层对象,view-resolver是个 list ,因此可以存储多个实现类,它们之间的优先级是根据实现的 Ordered 接口来的,如果前面的view-resolver不想进行解析,可以在resolveViewName中直接返回null,这样框架会调用第二个解析器去进行处理。这里有个默认的实现类:InternalResourceViewResolver,可以返回InternalResourceView ,该 view 对应的就是 jsp 类型的视图。

然后还剩下了最后三个对象,这是 mvc 框架最核心的部分,也是我们之前注解真正起作用的地方,这里会说明每个对象大概完成的功能,具体类的实现如果有兴趣可以参照源码进行比对:

  • handler-mappinglist类型,有三个元素,用于寻找请求对应的处理器(controller):

    1. RequestMappingHandlerMapping:根据请求,查找到@Controller注解的类中的某个处理器(也就是单个的方法),该处理器是HandlerMethod类型。
    2. BeanNameUrlHandlerMapping,根据 url 找到 bean,参照网上一张图片就知道其功效:
      在这里插入图片描述
    3. SimpleUrlHandlerMapping:与BeanNameUrlHandlerMapping差不多,不过是根据 key - value 结构 来查找 处理器的,同样使用一张图例进行说明:
      在这里插入图片描述
  • handler-adapterlist类型;之前找到了处理器,还需要知道如何使用该处理器去处理请求;系统同样默认了三个实例:

    1. RequestMappingHandlerAdatper:配合RequestMappingHandlerMapping,mvc 框架中最核心的类,规范如何调用处理器,在处理器调用前做的初始化操作,参数赋值等等。
    2. HttpRequestHandlerAdapter:处理实现了HttpRequestHandler接口的处理器,框架会自动调用其handleRequest方法处理本次请求。
    3. SimpleControllerHandlerAdapter,处理实现了Controller接口的处理器。
  • handler-exception-resolver :也是一个列表,这里默认实现有三个:

    1. ExceptionHandlerExceptionResolver:处理器为HandlerMethod类型时,如果出现异常,则使用该对象进行处理;该对象一般会选择合适的注解了@ExceptionHandler的方法。
    2. ResponseStatusExceptionResolver:从名字便可以看出,主要处理@ResponseStatus注解标注的方法产生的异常。
    3. DefaultHanlderExceptionResolver:主要处理一些通用异常,如“METHOD”不支持handler未找到等等。

ok,经过上面的分析,应该有了一个 mvc 大概的处理流程,现在我们不妨彻底的“自定义”一把,把上面的九大组件都自己实现一次,然后查看效果;当然,这些组件需要先到contextConfigLocation.xml中配置好;

在这里插入图片描述

自定义的组件都是以First***开头,如果组件是单个对象,则会被替换,如果是List列表,则会添加一条数据,这样,我们就可以针对整个系统流程进行拦截。

以上就是对 mvc 架构的一些浅析,当然,设计到具体处理器的环境,肯定不止于此,不过了解这些至少可以在有需求时进行部分的定制。


这里附上:GITHUB:mvc-demo,可以查看文中出现的项目代码部分。


注:文中部分图片引用自他人blog:
BeanNameUrlHandlerMappingSimpleUrlHandlerMapping解释图引用自:SpringMVC 配置式开发-BeanNameUrlHandlerMapping(七)

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值