海创软件组-初始化Spring MVC

这两周写的确实不少,不过AOP和IOC都已经是一个体系了,拆开不好,只有MVC还在初始阶段,所以就用这篇了

前言

Spring MVC一开始就定义为一个较为松散的组合,展示给用户的视图(VIEW),控制器返回的数据模型(Model),定位视图的视图解析器(ViewResolver)和处理适配器(HandlerAdapter)等内容都是独立的。换句话说,通过Spring MVC很容易把后台的数据转化为各种类型的数据,以满足移动互联网数据多样化的要求。例如Spring MVC可以十分方便的转换目前最常用的JSON数据集,也可以转换为PDF,EXCEL和XML等。加之Spring MVC是基于Spring基础框架派生出的Web框架,所以他天然就可以十分方便的整合到Spring框架中。
基于这个趋势,Spring MVC已经成为当前最主流的web开发架构。学习Spring MVC,首先是学习其基于MVC分层的思想。

1. Spring MVC框架的设计

当今MVC(Model-View-Controller)框架已经盛行,他不单单应用于Java的开发,也广泛应用于其他系统的开发,甚至近年来对于移动互联网前端开发也是如此。MVC巨大的成功在于他的理念,所以有必要先认识一下MVC框架。
为了基于读者清晰的认识,先画出Spring MVC的示意图
在这里插入图片描述
其中带有阿拉伯数字的说明,是MVC框架运行的流程。处理器请求会先到达控制器(Controller),控制器的作用是进行请求分发,这样他会根据请求的内容去访问模型层(Model),数据主要从数据库和NoSQL中来,而对于数据库而言往往还存在着事务的机制,为了适应这种变化,设计者会把模型层在分为两层,即服务层(service)和数据访问层(DAO)。当控制器获取由模型层返回的数据后,就会把数据渲染到视图中,这样就能够展现给用户了。
当然这只是一个粗狂的说明下面还有许多细节需要完善。例如,如何接受请求参数,如何选择控制器,如何定位视图以及视图类型等问题都需要我们进一步的阐释。

2. Spring MVC流程

尽管在Spring Boot开发中我们可以通过快速的通过配置实现Spring MVC的开发,但是为了解决实际的问题,我们还是很有必要了解Spring MVC的运行流程和组件,否则很难理解Spring Boot自动为我们生成了什么,配置了什么,这些有什么作用。流程和组件是Spring MVC的核心,Spring 的流程是围绕着DispatcherServlet展开的,所以Spring MVC中DispatcherServlet就是其最重要的内容。在DispatcherServlet的基础上,还存在其他组件,掌握流程和组件就是Spring MVC开发的基础。

首先画出Spring MVC的流程和组件,如图所示
在这里插入图片描述
上图十分重要他是Spring MVC运行的全流程,其中图中的阿拉伯数字是其执行流程,这是Spring MVC开发的基础。严格地说,Spring MVC处理请发并非一定需要经过全流程,有时候一些流程并不存在。例如,在我们加入@ResponseBody时,是没有经过视图解析器和视图渲染的。关于这部分内容,我们后面在讨论,这里先看一个简单的实例,对这个流程做进一步的论述。
首先,Web服务器启动的过程中,如果在Spring Boot机制下启用Spring MVC,他就开始初始化一些重要的组件,如DispatcherServlet(前端控制器,分发控制器),HandlerAdapter的实现类RequestMappingHandlerAdapter等组件对象。关于这些组件的初始化,我们可以看到spring-webmvc-xxx.jar包的属性文件DispatcherServlet.properties,他定义的对象都在Spring MVC开始时就初始化,并且存放在Spring IoC容器中,代码如下。

//找到他也挺不容易的
# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
#国际化解析器
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
#主题解析器
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
#HandlerMappring实例
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping
#处理器适配器
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
	org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter,\
	org.springframework.web.servlet.function.support.HandlerFunctionAdapter

#处理器异常解析器
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
	org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
#策略视图名称转换器,当你没有返回视图逻辑名称时,通过他可以生成默认的视图名称。
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
#视图解析器
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
#FlashMap管理器。不常用,不再讨论
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

在上面代码中,中文是我加入的注释,这些组件会在Spring MVC得到初始化,所以我们并不需要太多的配置就能够开发Spring MVC程序。尤其是在Spring Boot中,更是如此,我们可以通过Spring Boot的配置来定制这些组件的初始化。下面我们一边开发一边谈他的运行流程。
其次是开发控制器(Controller),代码如下:

package cn.hctech2006.boot.bootmvc.controller;

import cn.hctech2006.boot.bootmvc.bean.SysRole;
import cn.hctech2006.boot.bootmvc.service.SysRoleService;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/role")
public class RoleController {
    @Autowired
    private SysRoleService sysRoleService;

    @RequestMapping("details")
    public ModelAndView details(Long id){
        //访问模型层得到数据
        SysRole sysRole = sysRoleService.findById(id);
        //模型和视图
        ModelAndView mv = new ModelAndView();
        //定义模型视图
        mv.setViewName("role/details");
        //加入数据模型
        mv.addObject("sysRole",sysRole);
        //返回模型和数据
        return mv;
    }
}

这里的注解@Controller表明这是一个控制器,然后@RequestMapering代表请求路径和控制器(或者其他方法)的映射关系,他会在Web服务器启动Spring MVC时,就被扫描到HandlerMappring的机制中存储,之后用户发起请求被DispatcherServlet拦截之后,通过URL和其他条件,在HandlerMapping机制中能够找到对应的控制器(或者方法)进行响应。只是通过HandlerMapping返回的是一个HanlderExecutionChain对象,这个对象的源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web.servlet;

import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;

public class HandlerExecutionChain {
//日志
    private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);
   //处理器
    private final Object handler;
    //拦截器数组
    @Nullable
    private HandlerInterceptor[] interceptors;
    //拦截器列表
    @Nullable
    private List<HandlerInterceptor> interceptorList;
    //拦截器当前下标
    private int interceptorIndex;

   -----------------

从源码中可以看出,HandlerExecutionChain对象包含了一个处理器(handler)。这里的处理器是对控制器(controller)的包装,因为我们的控制器方法可能存在参数,那么处理器就可以读入HTTP和上下文相关参数,然后传递给控制器方法。而在控制器执行完毕返回后,处理器又可以通过配置信息对控制器的返回结果进行处理。从这段描述中可以看出,处理器包含了控制器方法的逻辑,此外还有处理器的拦截器(interceptor),这样就能够通过拦截处理器进一步增强处理器的功能。

得到处理器(handler)之后,还需要去运行,但是我们有普通的http请求,也有按照BeanName的请求,甚至还有WebSocket的请求,所以它还需要一个适配器去运行HandlerExecutionChain对象包含的处理器,这就是HanlderAdaptor接口定义的实现类。在代码中我们可以看到在Spring MVC中最常用的HandlerAdaptor接口的实现类就是HttpRequestHandlerAdaptor。通过请求的类型,DispatcherServlet就可以找到他来执行Web请求的HandlerExecutionChain对象包含的内容,这样就能够执行我们的处理器(handler)了。只是HandlerAdaptor运行HandlerExecutionChain这步比较复杂,我们在那时不做讨论,放到后面再谈。

---我们不使用JSP文件了,因为对于Spring Boot太不友好了,使用thymeleaf模板引擎

在处理器调用控制器的时候,他先通过模型层得到数据,再放入视图模型中,最后将返回模型和视图对象(ModelAndView)对象,这里设置控制器设置的视图名称为"role/details",这样就走到了视图解析器中(ViewResolver)去解析视图逻辑名称了。
在DspatcherServlet.properties中可以看到InternalResourceViewResolver这个视图即诶器的初始化,可以在application.yml中进行配置,代码清单如下:

  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html

通过这样的机制们就可以在Spring Boot的机制下定制InternalResourceViewResolver这个视图解析器的初始化,也就是在返回视图名称之后,他会以前缀,后缀以及视图名称组成全路径定位视图。例如,在控制器返回的是“role/details”,那么他就会找到 classpath:/template/role/details.html作为视图(view)。严格的说,这一步并不是必需的,因为有些视图并不需要逻辑名称,在不需要的时候,就不再需要视图解析器工作了。关于这一点,我们呢后面会再给出一个例子说明。

视图解析器定位到视图之后,视图的作用就是将数据模型(Model)渲染,这样就可以响应用户的请求。这一步就是视图将数据渲染(View)出来,用来展示给用户查看。按照我们呢控制器的返回就是classpath:/template/role/details.html作为我们的视图。我们看看他的代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>权限列表</title>
</head>
<body>
    <table border="1">
        <tr>
            <td>角色编号</td>
            <td>角色名称</td>
        </tr>
        <tr>
            <td th:text="${sysRole.id}"></td>
            <td th:text="${sysRole.name}"></td>
        </tr>
    </table>
</body>
</html>

注意,我们控制器里面绑定数据模型的时候,属性名称为sysRole,而属性为SyRole对象,所以就有了${sysRole.id}代表SysRole对象的id属性,,,,,,,这样就能够将数据模型的数据渲染到JSP视图上来展示用户详情给请求。

接着我们修改Spring Boot的启动文件,如下代码所示

@SpringBootApplication(scanBasePackages = "cn.hctech2006.boot.bootmvc")
@MapperScan(basePackages = {"cn.hctech2006.boot.bootmvc.mappper"},annotationClass = Repository.class)
public class BootMvcApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootMvcApplication.class, args);
    }

}

这样就可以运行他得到如下日志

在这里插入图片描述
我们通过请求http://localhost:8243/role/details?id=8以及HandlerMapping的匹配机制就可以找到处理器提供服务。而这个处理器则包含我们开发的控制器,那么进入这个控制器之后,他就执行控制器的逻辑,通过模型和视图(ModelAndView)绑定了数据模型,而且把视图名称修改为"role/details",随后返回。
模型和视图(ModelAndView)返回后,视图名称为"role/details",而我们定义的视图解析器(InternalResourceViewResolver)的前缀为classpath:/template/,且后置为.html,这样他便能够映射为classpath:/template/role/details.html,进而找到html文件作为视图,这就是视图解析器的作用。然后将数据模型渲染到视图中,这样就能够看到下图的效果了
在这里插入图片描述

因为Spring MVC流程和组件的重要性,所以为了让读者有着更好的认识,再次画出实例在Spring MVC里运行的流程图如下:
在这里插入图片描述
上图中的阿拉伯数字可以看出器运行流程,从而更好的知道Spring MVC运行的过程。但是有时候,我们可能需要的只是JSON数据集,因为目前前后端分离的趋势,使用JSON已经成为了主流的方式,正如我们之前使用的@ResponseBoay,他会采用处理器内部的机制。本节暂时不讨论处理器内部的机制,而是先用MappringJackson2JsonView转换出JSON,代码清单如下:

@Controller
@RequestMapping("/role")
public class RoleController {
    @Autowired
    private SysRoleService sysRoleService;

    @RequestMapping("details")
    public ModelAndView details(Long id){
        //访问模型层得到数据
        SysRole sysRole = sysRoleService.findById(id);
        //模型和视图
        ModelAndView mv = new ModelAndView();
        //生成JSON视图
        MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
        //定义模型视图
        mv.setView(jsonView);
        //加入数据模型
        mv.addObject("sysRole",sysRole);
        //返回模型和数据
        return mv;
    }
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("11121");
        return "details";
    }
}

可以看到,在控制器的方法中模型和视图(ModelAndView)中会捆绑JOSN视图(MappingJackson2JsonView)和数据模型(SysRole对象),然后返回,其结果也会变成JSON,只是需要注意的是这部与我们使用JSP作为视图是不一样的,在设置视图名的代码中,他会根据视图解析器(InternalREsourceViewRsolver)的解析找到html,然后渲染数据到视图中,从而展示最后的将诶过,而这里的JSON视图是没有视图解析器的定位视图的,因为他不是一个逻辑视图,只是需要将数据模型(SysRole)对象转换为JSOn而已。我们可以看下图所示的流程图
在这里插入图片描述
从流程图中,我们可以看到并没有视图解析器,那是因为MappingJackson2JsonView是一个非逻辑视图。他并不需要视图解析器进行定位,他的作用只是将数据模型渲染为JSON数据集来响应请求。可见Spring MVC中并不是每一个步骤搜是必须的。而是根据特别的需要会有不同的流程。也许更为让你关注的@REsponseBody则是一个在处理器内部机制转换的,后面我们会在学习处理器内部机制的谈到他。

3. 定制Spring MVC初始化

正如Spring Boot所称诺的一样,他会尽可能的配置Spring,对于Spring MVC也是如此,但是无论如何这些配置都可能满足不了我们的需要,需要进一步的对Spring MVC定制
在Servlet3.0规范中,web.xml不再是个必须的配置文件。,为了适应这个规范,Spring MVC从3.1版本开始也进行支持,也就是我们已经不再需要通过任何XML配置文件去配置Spring MVC的运行环境,正如Spring Boot宗旨,消除了XML繁复配置。为了支持对Spring MVC的配置,Spring提供了接口WebMvcConfigurer,这是一个基于Java 8的接口,所以大部分方法是default类型的,但是他们都是空实现,这样开发者只需要实现这个接口,重写需要自定义的方法即可,这样就很方便进行开发了。在Spring Boot中,自定义是通过配置类WebMvcAutoCOnfiguration定义的,他又一个静态的内部类WebMvcAutoConfigurationAdapter,通过他SPring Boot就自动配置了Spring MVC的初始化,他们之间的关系如下所示
在这里插入图片描述
在WebMvcAutoConfigurationAdapter类中,他会读入Spring配置Spring MVC的属性来初始化对应组件,这样便能够在一定程度上实现自定义。不过应该首先明确可以配置那些内容:一下代码是Spring Boot关于Spring MVC可以配置的内容:
在这里插入图片描述
这些配置项将会被Spring Boot的机制读入,然后使用WebMvcAutoConfigurationAdapter去定制初始化。一般而言,我们只需要配置少数的选项就可以嫩狗使得Spring MVC工作了。
对于这些选项,这个时候我们还可以参考上图,实现接口WebMvcConfigurer加入自己定义的方法就可以了,毕竟这个接口是Java8的接口,器本身已经提供了default方法,对其定义的方法实现了空实现。

4. Spring MVC实例

这一节将展示一个实例,以让读者更加熟悉Spring的开发。应该说Spring Boot中开发Spring MVC还是比较容易的,正如之前我们的例子,Spring MVC的开发核心是控制器的开发,控制器的开发又分为这么几个步骤,首先是定义请求分发,让Spring MVC能够产生HandlerMapping,其次接受请求获取参数,再其次是处理业务逻辑获取数据模型,最后绑定视图和数据模型。视图将数据模型渲染则是定义视图定义的问题,不属于控制器开发的步骤。
下面我们演示一个角色列表查询的界面。假设可以通过角色名称进行查询,但是一开始进入页面需要载入所有数据展示给用户来看,这里分成两种常用的场景,一种是刚进入页面时,易班那来说是不允许存在一步请求的,因为一步请求会造成数据的刷新,对用户不友好,另一种是进入页面后的查询,这是可以考虑使用Ajax异步请求,只是刷新数据而不刷新页面,这才是良好的UI设计体验。

4.1 开发控制器

    @RequestMapping("/table")
    public ModelAndView table(String name){
        SysRole sysRole = new SysRole();
        sysRole.setName(name);
        //访问模型层得到数据
        List<SysRole> sysRoles = sysRoleService.findAll(sysRole);
        //模型和视图
        ModelAndView mv = new ModelAndView();
        //定义模型视图
        mv.setViewName("role/table");
        //加入数据模型
        mv.addObject("sysRoles", sysRoles);
        //返回模型和视图
        return mv;
    }

这里我们先开发控制器。开发控制器首先是指定请求分发,这个任务交给@RequestMapping来完成。这个注解可以标注类或者方法。当一个类被标注的时候,所有关于他的请求,都需要在@RequestMapping定义的URL下。这个注解还可任意标注方法,当这个方法被标注后,也可以定义部分URL,这样就可以让URL找到对应的路径。配置了扫描路径之后,SpringMVC扫描机制就可以将其扫描,并且装载为HandlerMapping,以备后用。
这里的控制器存在一个方法,这个方法的任务是进入页面的时候,首先查询所有的用户,这是一个没有条件的查询,当他查询出所有的用户之后,创建模型和视图(ModelAndView),接着指定视图名称为’role/table’,然后将查询出的用户列表绑定到模型和视图中,最后返回模型和视图。这里我们沿用thymeleaf的视图解析器,这样在SpringMVC的机制中就会通过视图解析器找到classpath:/template/role/table.html作为视图,然后把数据模型渲染出来。
4.2 视图和视图渲染
上一节我们已经开发了控制器,接着我们需要将控制器返回的视图渲染出来以达到展示给请求者的目的。这里是用EasyUI作为页面端界面,下面看代码清单中的代码。
太难了,改了一下午

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>用户列表</title>
    <link th:rel="stylesheet" type="text/css"
        href="/themes/default/easyui.css">

    <link rel="stylesheet" type="text/css"
        href="/themes/icon.css">
    <link rel="stylesheet" type="text/css"
          href="/demo/demo.css">
    <script type="text/javascript" src="/jquery.min.js"></script>
    <script type="text/javascript" src="/jquery.easyui.min.js"></script>

    <script type="text/javascript">
        //定义事件方法
        function onSearch() {
            //定义请求路径
            var opts = $("#dg").datagrid("options");
            opts.url="./list";
            //获取查询参数
            var name=$("#name").val();
            //组织参数
            var params = {};
            if(name != null && name.trim() != ''){
                params.name = name;
            }
            //重新载入表格数据
            $("#dg").datagrid('load', params);
        }
    </script>

</head>
<body>
<div style="margin: 20px 0;"></div>

        <div class="easyui-layout" style="width: 100%; height: 350px">
            <div data-options="region:'north'" style="height: 50px">
                <form id="searchForm" method="post">
                    <table>
                        <tr>
                            <td>角色名称</td>
                            <td><input id="name" name="name"
                                       class="easyui-textbox"
                                       data-options="prompt:'输入角色名称。。。'"
                                       style="width: 100%; height: 32px"
                            ></td>
                            <td><a href="#" class="easyui-linkbutton"
                                   data-options="iconCls:'icon-search'"
                                   style="width: 80px"
                                   onclick="onSearch()">查询</a>
                            </td>
                        </tr>
                    </table>
                </form>
            </div>
            <div data-options="region:'centor',title:'用户列表',iconCls:'icon-ok'" style="height: 350px">
                <table id="dg" class="easyui-datagrid",
                       data-options="border:false,singleSelect:true,
                   fit:true,fitColumns:true">
                    <thead>
                    <tr>
                        <th data-options="field:'id'" width="80">编号</th>
                        <th data-options="field:'name'" width="100">角色名称</th>
                    </tr>
                    </thead>
                    <tbody>
                    <!--使用forEache渲染数据-->
                    <tr th:each="sysRole:${sysRoles}">
                        <td th:text="${sysRole.id}"></td>
                        <td th:text="${sysRole.name}"></td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
</body>
</html>

---前端的代码真是折磨人---
这里使用了EasyUI以及他的控件DataGrid(数据网络),通过thymeleaf的each标签进行循环将控制器返回的用户列表渲染到这张html中,所以在刚刚进入页面的时候,就可以展示用户列表,如图所示,因为这里采用先取数据在渲染的方式,所以在刚刚进入页面的时候。并不会出现Ajax的异步请求,这样有助于提高UI体验
在这里插入图片描述
继续看代码清单的list方法。首先他标注了@ResponseBody.这样,Spring MVC就知道最终需要把返回的结果转化为JSON。然后是获取参数,这里是用注解@RequestParam,通过指定参数名称使得http请求的参数和方法的参数进行绑定,只是这个注解的默认规则是参数不能为空。为了克服这个问题,代码将其属性required设置为false即可,其意义在于允许参数为空,这样就可以测试这个请求了。
在这里插入图片描述
从上图中可以看出,带你即查询按钮之后,就会执行Ajax请求,把数据取回来,用来显示在DataGrid控件中。这样就可以在互联网系统中,第一次进入一个新的界面,就可以无限的刷新显示数据,而在查询等操作中使用Ajax,这样就可以提高用户的体验。
还是有一点感悟的,觉得这个一个表格需要使用多个方法,后端就可以通过一个方法,设置为传递的参数不同,对应传递的数据不同,好像大家可以互相方便啊。

发布了180 篇原创文章 · 获赞 114 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43404791/article/details/105604165