1

在编码一段时间之后,再重新看之前学过的框架,发现有新的认知,像Spring,我就重新学习了一把:

这次重学的是Spring MVC,以前学习Spring MVC的时候是在B站看的视频,确实不错,让我快速入门了,但是我还是觉得不是很系统,感觉知识点支离破碎,我希望用我的方式将这些方式串联起来。在学习本篇之前,需要有Java EE和Spring Framework的基础,如果没有Java EE的基础,可以参看JavaWeb视频教程,如果没有Spring Framework的基础,请参看我上面的文章,如果觉得四篇有点多,可以先只看 欢迎光临Spring时代(一) 上柱国IOC列传,如果可以的话,还是建议先看完上面四篇文章来看这四篇文章。

原生Servlet时代的问题

简单的说Servlet是Java中的一个接口,位于javax.servlet下。我们来看一下Servlet上的注释:

A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients,usually across HTTP, the HyperText Transfer Protocol.
Servlet是一个运行在Web 服务器中的一个小Java程序,Servlet接收和处理Web 客户端的请求,通常用于Http协议。
我们通常用的是它的实现类HttpServlet,然后重写doGet和doPost方法,像下面这样:
WebServlet(name ="/servlet")
public class ServletDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getParameterNames();
        req.getParameter("username");

    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理post请求
        super.doPost(req, resp);
    }
}

它有哪些痛点呢? 第一个就是取参数的问题,取参数只有这一种方式,参数一多就有点让人心烦了,不是那么的符合面向对象的思想,程序员除了关心业务逻辑的,还要关注如何获取参数,的确HttpServletRequest里面放置的参数很多了,很全很全:

那getParameterNames去哪里了? HttpServletRequest 是一个接口,getParameterNames从ServletRequest继承而来。

能否让程序员更加专注于业务逻辑一点呢,让取参数变的更简单一些,比如我想用学生类对象接收,又比如我需要的参数只有一个Long类型的id,接收参数的时候能否就直接让用Long id来接收呢。这是我们提出的第一个问题,取参数能否变的简单一些。除了取参数,在Ajax技术已经普遍应用的今天,能否让我响应前端的Ajax请求在简单一些呢? 在原生Servlet,我们处理Ajax,我们在响应中的设置编码,设置响应的类型,然后通过一些Java的JSON库将我们的对象转成JSON形式,发送给前端,大致上要这么做:

Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    Student student = new Student(13, "223", null);
    Gson gson = new Gson();
    resp.setContentType("application/json;charset=utf-8");
    resp.setCharacterEncoding("UTF-8");
    System.out.println(req.getMethod());
    System.out.println("处理get请求");
    PrintWriter writer = resp.getWriter();
    writer.println(gson.toJson(student));
}

这是我们提出的第二个问题。还有文件上传下载,原生Servlet的文件名乱码问题,异常的统一处理,系统出了错直接把出错日志返回到网页上?用户一脸懵逼,这是发生了什么? 我们能否统一的处理一下异常呢? 比如服务端出了问题,就直接给个提示,服务端发生了错误,请联系系统工程师。这样是不是更友好一些呢。

总结一下

我们这里再总结一下,我们使用原生Servlet 进行Web编程所遇到的痛点:

  • 处理参数麻烦,不符合面向对象原则
  • 处理Ajax麻烦,而且有些流程是通用的,但是还要重复编写
  • 上传和下载文件中文名乱码
  • 没有提供统一的异常处理机制

这四点让我们在关注于业务的逻辑的同时,也要在取参数上花上一番功夫,这些问题是使用原生Servlet普遍都会遇到的,
很自然我们在碰到这些问题之后会对原生的Servlet进行扩展,那么既然大家都会用到,那么就会有人或社区针对上面的问题,提出解决方案,一般我们称这种类型的框架为Web 框架,在Java Web领域比较常用的有:

  • Spring MVC
Spring 公司出品,其实人家叫Spring Web MVC,不过我们更习惯叫它Spring MVC,性能强大,可以和Spring Framework无缝集成。
  • Apache Struts 2.x
Apache 出品,不过现在来看没有Spring MVC性能高,Spring MVC使用更为广泛
  • Apache Tapestry 5.x
Apache 出品,这个倒是头一次见,我也是在重新学习Spring MVC的时候,去翻官方文档才看到的这个框架,查了一下,发现资料不多。这里作为了解吧。

本篇我们介绍的就是Spring MVC,各位同学在学习的时候,一定注意体会思想,技术发展的是很快的,如果疲于学习各种框架,你会发现淹没在技术的海洋中,那什么是通往彼岸的船呢?那就是思想,解决问题,可能思想是相同的,但是具体实现上可能会有一些小差异,这也就是学一通百。这也是我在文章开头会花很大的力气,介绍这种技术出现的原因以及思想,介绍框架的使用很容易,但是介绍思想相对来说就更难一些。

这里我还想顺带聊一下如何学习知识,一般来如果官方文档写的不错的话,我就会直接去看官方文档,因为相对来说官方文档更为权威,也是一手资料。我在重学Spring MVC,去翻了一下Spring MVC的官方文档,发现写的还是不错的,有兴致的同学也可以翻翻官方文档。

  1. 点击链接https://spring.io/projects/sp... 进入Spring官网。

准备工作

本篇我们还是使用maven来搭建项目,如果不懂maven还想下jar包,在 欢迎光临Spring时代(一) 上柱国IOC列传一文中我也介绍了下载jar包的方法,这里不再赘述,欢迎光临Spring时代(一) 上柱国IOC列传一文所需要的依赖我们还要接着用,新需要的依赖如下:

    <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

先用起来

上面介绍的Java Web领域的Web框架解决上面我们提出的问题就是通过扩展原生Servlet来做的,那如何使用Spring MVC提供的一系列扩展Servlet呢? 那么首先就要让Tomcat启动的时候知道,也就是在web.xml进行配置:

  <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <!--初始dispatcherServlet需要的参数contextConfigLocation,DispatcherServlet有这个属性-->
        <init-param>
            <!--加载配置文件,指明配置文件的位置-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <!--元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。-->
        <load-on-startup>1</load-on-startup>
    </servlet>
 <servlet-mapping>
        <servlet-name>springDispatcherServlet</servlet-name>
         <!--    /:  拦截一切请求   -->
        <!--  /user:  拦截所有以/user开头的请求  -->
        <!--  /user/abc.do: 只拦截该请求   -->
        <!--  .action:  只拦截.action的请求  -->
        <url-pattern>/</url-pattern>
 </servlet-mapping>

我们看下contextConfigLocation在DispatcherServlet哪里,首先到DispatcherServlet去看:

发现没找到,我们去它的父类找:

上面我们初始化DispatcherServlet的时候指明了配置文件的位置,那么配置文件中要配置什么呢? 除了数据库连接池之类的,我们回忆一下使用原生Servelt+jsp进行编程的时候,我们还能控制页面的跳转,虽然在前后端分离时代,这个页面的跳转被前端仔拿走了,但是Spring MVC还是提供了这样的功能,Spring中称之为视图解析器, 所以还要在配置文件中将这个配置文件中加进来,也就是纳入到IOC容器管辖。写到这里似乎有点Spring 整合Spring MVC的味道了,但是在Java Web领域Spring是管理对象中心,不管用什么框架你都得将核心对象纳入到IOC容器中,以便我们优雅的使用。所以在开篇我们强调学习本篇要有Spring的基础。
在配置文件中需要配置的:

  <!--配置扫描器-->
    <context:component-scan base-package="org.example"/>
    <!--配置视图解析器-->
    <bean id = "internalResourceViewResolver" class = "org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name = "prefix" value = "view/"></property>
        <property name = "suffix" value = ".jsp"></property>
    </bean>

学过Spring的人会知道配置扫描器的作用,将加上了Spring提供注解的类(@Controller、@Service、@Repository、@Component)加入到IOC容器中,那配置的这个视图解析器是做什么的呢? 该类就用来控制页面的跳转,那这个前缀是什么意思呢? 假如Spring MVC认为你返回的是一个视图(也就是页面)的话,如下面:

// 该注解将该类变为处理HTTP请求的类
Controller
public class StudentController {
    // 将处理请求方式为GET,URI为servlet的请求
    @RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet() {
        System.out.println("hello world");
        return "success";
    }
     // 将处理请求方式为post,URI为servlet的请求
    @RequestMapping(value = "/servlet", method = RequestMethod.POST)
    public String testPost() {
        System.out.println("hello world");
        return "success";
    }
}

返回值是String,Spring MVC会认为你要跳转到view/success.jsp页面。下面我们在webapp下建一个名为success.jsp。

接下来我们测试一下:

梳理一下

我们要使用Spring MVC提供的扩展Servlet,首先需要在web.xml配置文件让Spring MVC的DispatcherServlet接管我们的所有请求,为了做到这一点,我们需要让Tomcat在启动的时候就初始化DispatcherServlet。

当一个类上拥有@Controller注解,则该类在Spring MVC框架中就被视为一个能够处理请求的类,该类中拥有@RequestMapping的方法,将能够处理HTTP请求。@RequestMapping中的value属性用于指定URI,假如HTTP请求的URI和该方法上@RequestMapping的value属性吻合,那么请求将进入该方法。 @RequestMapping中的method用于限制请求方式,method值要求是RequestMethod类型,RequestMethod是Method。

由于在Spring MVC在设计之初,前后端分离时代还没到来,那个时候Java还能控制页面的跳转,所以Spring MVC用internalResourceViewResolver做视图解析。但是本篇并不会介绍多少有关页面跳转,静态资源处理的东西,这些东西已经不再适合这个前后端分离时代。

简化取参数的方式

似乎Spring MVC下处理请求的类只是变的简介了一些,并没有让我们感受到取参数的简化啊。我们接下来就通过几个注解来体会一下Spring MVC下优雅的取参数方式。

直接接对象

Spring MVC可以将前端的参数直接变为一个对象,在参数名能对应的情况下,首先我们准备一个简单的对象:

// 为了节省篇幅,这里引入了Lombok插件,通过这些注解就可以产生构造函数,get、set方法、重写toString方法
Data 
Getter 
Setter
@ToString 
public class User {
    private String userName;
    private String password;
}

@Controller
public class StudentController {

@RequestMapping(value = "/obj", method = RequestMethod.GET)
public String testOBj(User user) {
    System.out.println(user);
    return "success";
}

}
测试一下:

测试结果

@RequestParam

@Controller
public class StudentController {
    @RequestMapping(value = "/servlet", method = RequestMethod.GET)
    public String testGet(@RequestParam(value = "name") String userName, @RequestParam(value = "password") String password) {
        System.out.println("username:" + userName);
        System.out.println("password:" + password);
        return "success";
    }
}

假如HTTP请求的参数名和方法的参数名相同,在请求进入该方法的时候,Spring MVC的请求方法的方法参数可以直接接收。
假如HTTP的请求和Controller中的方法形参不相同,也就是不同名,可通过@RequestParam作为桥梁,通过value属性将请求参数映射到指定参数变量上,但是形参上有@RequestParam,则默认该形参必须有,否则会报错。
我们测试一下:

可以通过@RequestParam的required属性设置为false,取消要求该参数必须有的设定。默认情况@RequestParam要求的参数必须要有。

@RequestHeader和@CookieValue

  • @RequestHeader: 完成请求头(header)数据到处理器功能处理方法参数上的绑定
  • @CookieValue: 完成Cookie数据到处理器功能处理方法的方法参数上的绑定

我们首先看下请求头里有什么:

然后再捋一捋Session和Cookie之间的关系:
由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识别具体的用户,这个机制就是Session。这个机制就是Session。典型的场景比如购物车,当你电脑下单按钮时,由于HTTP协议的无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识特定的用户,这个Session是保存在服务端的。
服务端在识别到特定的用户的时候,就需要Cookie登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用Cookie来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在Cookie里面记录一个Session Id,以后每次请求把这个回话ID发送到服务器,我就知道你是谁了。
然后我们看下Cookie中有什么:

示例:

@Controller
public class StudentController {
    @RequestMapping(value = "info", method = RequestMethod.GET)
    public void test(@RequestHeader("User-Agent") String useAgent, @CookieValue("JSESSIONID") String jsesionId) {
        System.out.println(useAgent);
        System.out.println(jsesionId);
    }
}

@RequestBody

我们知道get请求,请求参数放置在URI之后,没有请求体,post请求的请求参数可以放在URI之后,也可以放在请求体中。
上面我们已经可以看到,假如请求参数是get,且参数名和请求方法中形参的属性名能对的上,那Spring MVC就会将请求参数帮我们封装为一个对象,那对于post请求呢? 我将请求参数放置在请求体中,我们就需要通过@RequestBody就可以将post请求发送的json格式的数据,转为Java中的对象。

简化文件上传

原生Servlet的上传

前台代码:

 <form action="upload" method = "post" enctype = "multipart/form-data">
       用户名:<input type = "text" name = "userName"/>
       密码:<input type = "text" name = "password"/>
       文件:<input type = "file" name = "pic">
       <input type = "submit" value = "提交">
   </form>

在原生Servlet时代,我们通过Apache提供的组件做文件上传,后台的逻辑大致是这样的:

 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 统一编码
        req.setCharacterEncoding("utf-8");
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("text/html;charset=UTF-8");
        boolean isMultipartContent = ServletFileUpload.isMultipartContent(req);
        // 表单中有上传文件,method中必须有enctype这个属性
        if (isMultipartContent) {
            FileItemFactory fileItemFactory = new DiskFileItemFactory();
            ServletFileUpload servletFileUpload = new ServletFileUpload(fileItemFactory);
            List<FileItem> items = servletFileUpload.parseRequest(req);
            for (FileItem item : items) {
                if (item.isFormField()){
                    // 处理表单的非文件字段
                }else{
                    // 将上传文件写入到文件服务器中
                    item.write(new File("服务器的路径/"));
                }
            }
        }
    }

Spring MVC下的使用Apache组件进行文件上传

  • 使用Apache组件的上传就变成了这样

首先在配置文件中,我们要配置文件解析器:

 <bean id = "commonsMultipartResolver" class = "org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <property name="maxUploadSize">
            <!--单位是B 1048576 = 1024 * 1024 -->
            <value>1048576</value>
        </property>
    </bean>
 // 使用MultipartFile当形参,SpringMVC会自动将表单中的文件对象注入到该参数中
 // SpringMVC也会尝试将表单的非文件对象放入User对象中
 // post请求我们一般习惯性将文件对象放进请求体中
 @RequestMapping(value = "mvcUpload" , method = RequestMethod.POST)
  public void testUpload1(@RequestBody  User u , MultipartFile pic){

 }

忘了讲,这两个Apache组件的依赖如下:

<dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.5</version>
        </dependency>

Servlet3 实现文件上传

首先在web.xml中配置上传的位置:

 <servlet>
        <servlet-name>springDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--初始dispatcherServlet需要的参数contextConfigLocation,DispatcherServlet有这个属性-->
        <init-param>
        <!--加载配置文件,指明配置文件的位置-->
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:applicationContext.xml</param-value>
        </init-param>
        <!--元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。-->
        <load-on-startup>1</load-on-startup>
        <multipart-config>
            <location>c:temp</location>
            <max-file-size>1048576</max-file-size>
            <!--单个数据大小-->
            <file-size-threshold>10240</file-size-threshold>
        </multipart-config>
    </servlet>

这里强调一下,要注意Servlet的版本:

然后在IOC容器中加入:

   <bean  id = "multipartResolver" class = "org.springframework.web.multipart.support.StandardServletMultipartResolver">
    </bean>

后端代码不变。

简化文件下载

这里其实感觉没有简化多少,用原生的Servlet也不是那么麻烦。Spring MVC这里又给我们提供了一种选择而已。
下面是示例:

 @RequestMapping(value = "testUpload")
    public void downloadFile(Long id, HttpServletResponse resp, HttpServletRequest request) throws IOException {
        // 根据id去查文件的位置
        resp.setContentType("application/x-msdownload");
        String userAgent = request.getHeader("User-Agent");
        if (userAgent.contains("IE")) {
            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("文件名", StandardCharsets.UTF_8.name()));
        } else {
            resp.setHeader("Content-Disposition", "attachment;filename=" + new String("文件名".getBytes("UTF-8"), StandardCharsets.ISO_8859_1.name()));
        }
        Files.copy(Paths.get("文件夹", "文件名"), resp.getOutputStream());
    }

    @RequestMapping(value = "testUpload2")
    public ResponseEntity<byte[]> testUpload(Long id, HttpServletResponse resp, HttpServletRequest request) throws IOException {
        // 根据id去查文件的位置
        resp.setContentType("application/x-msdownload");
        String userAgent = request.getHeader("User-Agent");
        HttpHeaders httpHeaders = new HttpHeaders();
        if (userAgent.contains("IE")) {
            resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("文件名", StandardCharsets.UTF_8.name()));
        } else {
            resp.setHeader("Content-Disposition", "attachment;filename=" + new String("文件名".getBytes("UTF-8"), StandardCharsets.ISO_8859_1.name()));
        }
        Files.copy(Paths.get("服务器上的文件夹", "文件名"), resp.getOutputStream());
        httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        byte[] array = FileUtils.readFileToByteArray(new File("文件夹", "文件名"));
        return new ResponseEntity<byte[]>(array,httpHeaders, HttpStatus.CREATED);
    }

对Restful风格的支持

什么是Restful风格

Restfu是一种软件风格,严格意义上Restfu是一种编码风格,简单的说,通常情况我们设计的后端接口可以对应四种操作,即增删查改,这也是服务端工程师经常被称作CRUD仔的原因,那么怎么让增删查改和请求方式绑定在一起呢? 对Restful风格不熟悉的朋友可能会说,不是只有两种请求方式吗? 怎么对应四种操作呢? 事实上HTTP协议规定的请求方式可不止有四种,有人说是十四种,但是我在火狐开发者文档上只查到八种,但是浏览器只支持两种,所以我们用原生Servlet进行编程的时候,只重写了HttpServlet的doGet和doPost方法。 这八种请求方式有四种我们可以拎出来看看:

  • DELETE
  • GET
  • POST
  • PUT

粗略的说,Restful风格中规定DELETE代表删除某个资源,GET代表向服务端获取某个资源,POST代表向服务器新增一个资源,PUT代表向服务器请求更新一个资源。这样的设计理念,就不用开发者在设计接口的时候,在接口中加入动词来标识这个接口要完成的动作,这样更有利于维护,贯彻单一设计职责,后续的开发人员从接口就能看出来这个请求是做什么的,这也是Restful风格盛行的原因。

但是我在翻阅火狐的开发者文档的时候,发现文档在介绍浏览器对八种请求方式都是支持的:

但是我在网上翻了许多资料,都是说浏览器只支持GET和POST的请求,仔细想了想,这也许跟浏览器的版本有关系吧。不同版本支持的HTTP协议是不同的,HTTP1.1之前,请求方式只有GET和POST两种,HTTP1.1协议新增了五种请求方式:

  • OPTIONS
  • HEAD
  • PUT
  • DELETE
  • TRACE

浏览器支持是支持,但是客户端发起请求的时候,怎么让服务端知道你用的是PUT和DELETE请求的,通用的做法就是在表单代码中增加隐藏属性,把POST请求包装成PUT请求。
另外顺便提一句,火狐浏览器的开发者文档写的不错,有介绍HTTP协议的,想学HTTP协议,找不到权威的参考资料的可以去看下,文末的参考资料有链接。

对Restful的支持

  • @ResponseBody
在如今的前后端分离时代,跳转页面已经被前端控制,前后端通信的方式也是通过Ajax实现局部刷新,但是在SpringMVC还是有跳转视图的概念,那怎么告知Spring MVC,我不要跳转页面,我就是给前端页面发送了数据呢? 就是通过在请求方法上加@ResponseBody来完成的。像下面这样:
  • @RestController
如果你觉得每个方法加@ResponseBody比较方法,可以在类上加上这个注解,加上
  • @GetMapping@PostMapping@PutMapping@DeleteMapping
见名知义,支持不同的请求方式,用法和@RequestMapping一样。将这五个注解统称为请求注解。
  • @PathVariable
服务端使用请求注解后,可以类似{参数名}站位符传参,此时需要通过@PathVariable注解可以将URL中站位符参数绑定到控制器处理方法的形参中。

此时客户端的请求地址如: http://localhost:8080/studySpringFrameWork_war_exploded/delete/1
服务端代码:

@RequestMapping(value = "delete/{id}", method = RequestMethod.GET)
public String getInfo(@PathVariable(value = "id") String id) {
    System.out.println("id:" + id);
    return id;
}

{参数值} 和 @PathVariable(value = "id") 要维持一致。

统一的异常处理

简介

Spring MVC处理异常有三种方式:
(1) 使用Spring MVC 提供的简单异常处理器 SimpleMappingExceptionResolver
(2) 现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器
(3) 使用@ExceptionHandler + @ControllerAdvice注解实现异常处理
最顶层的异常处理就是HandlerExceptionResolver,继承结构图如下:

一般我们常用的是第一种和第三种处理方式。

第一种处理方式

第一种方式具体的实操就是在配置文件中配置,要对哪些异常进行处理,跳转到哪些页面,像下面这样:

   <bean id="simpleMappingExceptionResolver"  class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!-- 定义默认的异常处理页面,当该异常类型发生时跳转到error页面-->
        <property name="defaultErrorView" value="error"/>
        <!--定义异常处理页面用来获取异常信息的变量名,默认名为exception-->
        <property name="exceptionAttribute" value="ex"/>
        <!--定义需要特殊处理的异常,有两种配置方式 1种是value标签方式  2.props标签方式  还是配置类更好,无需关心这些细节-->
        <property name="exceptionMappings">
            <!-- value标签多个异常用逗号隔开-->
<!--            <value>-->
<!--                java.lang.ArithmeticException=error,-->
<!--                java.lang.NullPointerException=exception-->
<!--            </value>-->
            <!-- props标签,每个prop标签的key代表处理一种异常,prop标签中代表具体要跳转的页面 -->
            <props>
                <prop key="java.lang.ArithmeticException">
                    error
                </prop>
            </props>
        </property>
    </bean>

第三种处理方式

// 这个注解会处理所有方法的异常
@ControllerAdvice
public class ExceptionHandlerControllerAdvice {
    // 该注解中声明要处理哪些异常
    @ExceptionHandler({Exception.class})
    public  String error(Exception ex , Model model){
        // 向请求域中放入异常信息
        model.addAttribute("errorMsg",ex.getMessage());
        // 跳转至error页面
        return "error";
    }
}

拦截器

简介

在用户的请求到达请求方法之前会进入拦截器,我们可以在拦截器里面做一些通用操作,比如鉴权,判断用户登录之类的。折让我想到了AOP,也可以做到在方法之前、方法执行之后执行。那拦截器和AOP有什么区别呢? 某种意义上拦截器可以算在AOP里面,但是拦截器又不能算在AOP里面,我们简单的讲一下拦截器的原理,用户的请求在到达DispacherServlet之后,由DispacherServlet分发请求,DispacherServlet会查看该请求是否被拦截器拦截,如果被拦截,那么先将该请求交给拦截器,如下图所示:

而AOP则是通过动态代理来做的,在运行的时候生成需要增强方法的类的子类来做到的。拦截器算是在整个Spring MVC执行流程的一环,只能拦URL,AOP则更为细致。执行顺序上,拦截器先于AOP执行。

怎么用?

实现org.springframework.web.servlet.HandlerInterceptor接口。我们先大致的看一下这个方法:

preHandle在请求到处理器之前执行,postHandle在处理请求的方法执行完毕之后,视图渲染之前执行。afterCompletion视图渲染之后执行。这里的视图是什么意思呢? 也就是Spring MVC的V视图层,页面渲染完就是页面形成完毕
然后在配置文件或配置类中配置拦截器即可。
首先实现HandlerInterceptor接口:

public class MyHandlerInterceptor implements HandlerInterceptor {
    /**
     * 返回true代表将请求交给真正的控制器
     * 返回false代表将请求放给下一个拦截器,如果找不到下一个拦截器,
     * 则该请求不会到达真正的拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("在方法执行之前执行................");
        return true;
    }
    /**
     * 在处理器方法执行之后执行
     * ModelAndView中存放方法返回的数据和要跳转的视图
     * 方法未成功执行,不会触发此方法
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("在方法执行之后执行................");
    }

    /**
     * 在页面渲染之后获取
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 打印方法的异常信息
        ex.printStackTrace();
        System.out.println("在视图渲染完毕执行之后执行................");
    }
}

配置文件中配置:

  <mvc:interceptors>
            <mvc:interceptor>
<!--                /*: 只能拦截一级路径-->
<!--                /**: 可以拦截一级或多级路径-->
                <mvc:mapping path = "/**"/>
                <mvc:exclude-mapping path="login"/>
                <bean class="org.example.mvc.MyHandlerInterceptor"></bean>
            </mvc:interceptor>
    </mvc:interceptors>

补充介绍

Spring MVC的M(model)、V(view)、C(Controller),上面我们似乎就将关注点放在C上了,也就是接收参数,处理参数了。其实M也讲了,就是用@ResponseBody向前端返回JSON格式的数据,还有V我们这里也并没有细讲,原因在于这些并不常用了,服务端已经不能控制住页面的跳转了,关于返回数据也主要是向前端返回JSON格式的数据。上面似乎有提到过Model,这是一个在跳转对应页面后取数据的类,在Controller取完数据之后,将数据放入Model,然后指明跳转到哪个页面,然后在对应的页面取出Model中存储的数据,Model是Spring MVC提供给我们的类,除此之外还有ModelAndView,既能存放数据又能存放视图,当返回类型是这个的时候,SpringMVC会ModelAndView取视图跳转,然后我们就可以在对应的页面取数据了。但是这些并不常用,所以着墨不多,本文重点关注的都是在前后端分离时代,高频常用的,其实这里本来想介绍Spring MVC统一处理乱码的,就是在web.xml中配置一个过滤器:

<filter>
        <filter-name>encoding</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>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

但是不知道咋会事,也许是我用的Spring MVC版本太新了,这个乱码问题就被解决了。如果有同学在编码的时候发现有中文乱码问题的话,可以在web.xml把上面的配置加一下。

写在最后

每次重学框架都会有新的认知,一不小心又把篇幅拉的太大了,总结一下本文讲了什么,本文主要讲了Spring MVC的高频使用点,Spring MVC对原生的Servlet进行了增强,为了使用Spring MVC提供的增强的Servlet,我们需要让Spring MVC的DispatcherServlet接管所有的请求,这也就是在Tomcar的配置文件web.xml配置在启动的时候就初始化DispatcherServlet,由于Spring MVC刚设计之初,服务端还能控制页面跳转,所以我们还要在配置文件中配置视图解析器,也就是InternalResourceViewResolver。然后我们就能体会到Spring MVC提供给我们的遍历,简化取参数的方式让我们更专注于业务逻辑,统一的异常处理让我们的页面不至于直接把错误信息直接输出到页面上,简单的获取前端文件的方式,拦截器可以让我们统一进行鉴权,以及对RESTful风格的友好支持。希望对大家会有所帮助。

参考资料


北冥有只鱼
147 声望35 粉丝