【SpringMVC】SpringMVC中统一异常处理

113 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1、本文内容

带大家掌握springmvc中统一异常处理的使用。

2、通常我们是如何处理异常的?

看一下下面的代码,每个方法中有一段try catch,用来对业务异常进行处理,不知道大家是否写过这种类似的代码,这种代码有什么问题么?

public class Demo {   
    public void m1() {      
        try {      
            //业务代码     
        } catch (Exception e) {   
            e.message();
        }  
    }    
    public void m2() {  
        try {    
            //业务代码    
        } catch (Exception e) { 
            e.message();
        }   
    }  
    ....
}

3、上面代码存在的问题

先来思考一个问题,当系统发生异常了,我们会怎么做?

为了方便排查错误,我们会在catch中将异常信息记录到日志文件中,变成了下面这样

public class Demo {   
    public void m1() {  
        try {     
            //业务代码    
        } catch (Exception e) {    
            //1、将错误信息记录到日志文件 
        }   
    }    
    
    public void m2() {     
        try {   
            //业务代码   
        } catch (Exception e) {
            //1、将错误信息记录到日志文件  
        } 
    } 
    ....
}

如果发生了严重的异常,我们需要第一时间让开发人员介入,怎么办呢?

我们可以在catch中可以发送短信给开发者,让其第一时间介入解决。

此时是不是又要改代码,catch又要添加代码了,此时要改很多很多代码,得重新测试一遍了,此时对于开发和测试都是一种灾难。

4、如何更好的解决这个问题?

采用aop的方式,将异常处理和业务代码进行分离,让框架拦截所有方法的执行,目标方法中不要在捕获异常了,直接将异常抛出去,由统一的地方进行进行处理。

此时异常处理和业务代码分离开了,没有耦合在一起了,此时如果需要调整异常的处理逻辑,会非常方便,只需要修改统一处理异常的代码,就ok了。

如果对spring 的 aop比较熟悉的,实现起来还是很容易的,只需要一个环绕拦截器就可以了,有兴趣的朋友可以去试试。

本文是springmvc的内容,而springmvc中提供了类似的方式来统一处理系统所有的异常,Controller中的所有方法都无需捕获异常,只需要做一些配置,springmvc框架就会自动捕获异常,对异常进行集中处理,下面咱们来看看这玩意是怎么玩的。

5、springmvc集中统一处理异常的使用步骤

下面通过一个案例来看一下具体如何使用的。

5.1、需求

写个登录方法,方法中验证用户和密码,验证失败的时候分别抛出对应的异常,成功了跳转到success.jsp页面,代码如下。

@Controller
public class UserController {  
    @RequestMapping("/login")  
    public ModelAndView login(
    @RequestParam("name") String name,         
    @RequestParam("pass") Integer pass) throws Exception {   
        //用户名必须为路人       
        if (!"路人".equals(name)) {         
            throw new NameException("用户名有误!");        
        }        
        
        //密码必须为666  
        if (Integer.valueOf(666).equals(pass)) {     
            throw new PassException("密码有误!");   
        }      
        
        ModelAndView modelAndView = new ModelAndView();  
        modelAndView.addObject("name", name);    
        modelAndView.setViewName("success");   
        return modelAndView;   
     }
}

 /** 
 * 姓名异常类 
 */
 public class NameException extends Exception { 
     public NameException(String message) {    
         super(message); 
     }
 }
 
 /** 
 * 密码异常类 
 */
 public class PassException extends Exception {  
     public PassException(String message) {  
         super(message); 
     }
}

需要实现的需求

从代码中来看,我们可以将异常分为3类:NameException、PassException、其他异常,咱们需要实现让springmvc来统一处理这3类异常,然后将错误信息输出到错误页面。

5.2、具体实现步骤:3个步骤

step1:创建全局异常处理类(非常关键)

这个步骤是重点,内部包含3个小的步骤。

第1步:创建一个普通的类,作为全局异常处理类

第2步:在类上添加@ControllerAdvice注解,从注解的名称包含了Advice可以看出,aop中我们接触过Advice(通知),用来对bean的功能进行增强,而这个注解是对Controller的功能进行增强,用来集中处理Controller的所有异常。

第3步:添加处理异常的方法,方法上需要加上@ExceptionHandler注解,这个注解有个value属性,用来指定匹配的异常类型,当springmvc捕获到控制器异常后,会和这个异常类型进行匹配,匹配成功了,将调用@ExceptionHandler标注的方法;如果未指定value的值,表示匹配所有类型的异常。

最终代码如下,类中添加了3个方法,分别用来处理3类异常,方法的每一行输出了一条日志,稍后可以通过这个验证效果,方法内部对错误进行了封装,然后跳转到错误页面(error.jsp)进行展示,稍后我们会通过不同的场景来验证效果。

package com.tengteng.springmvc.chat10.handle;
import com.tengteng.springmvc.chat10.exception.NameException;
import com.tengteng.springmvc.chat10.exception.PassException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

/** 
* 统一异常处理类 
*/
@ControllerAdvice
public class GlobalExceptionHandle {   

    /**     
    * 此方法用来处理 NameException 类型的异常,  
    * 当controller抛出NameException异常的时候,此方法会被调用   
    *    
    * @param e  
    * @return   
    */    
    @ExceptionHandler({NameException.class})   
    public ModelAndView doNameException(Exception e) {   
        System.out.println("-----doNameException-----");      
        ModelAndView modelAndView = new ModelAndView();    
        modelAndView.setViewName("error");       
        modelAndView.addObject("msg", "登录名有误!");    
        modelAndView.addObject("e", e);       
        return modelAndView;   
    }   
    
    /**    
    * 此方法用来处理 AgeException 类型的异常,  
    * 当controller抛出NameException异常的时候,此方法会被调用   
    *   
    * @param e    
    * @return   
    */    
    @ExceptionHandler({PassException.class})  
    public ModelAndView doPassException(Exception e) { 
        System.out.println("-----doPassException-----");   
        ModelAndView modelAndView = new ModelAndView();   
        modelAndView.setViewName("error");       
        modelAndView.addObject("msg", "密码有误!");  
        modelAndView.addObject("e", e);        
        return modelAndView;   
    }  
    
    /**  
    * 此方法用来处理任意异常(也就是上面2个方法不能够处理的异常都会被这个方法处理)  
    *   
    * @param e  
    * @return  
    */    
    @ExceptionHandler
    public ModelAndView doException(Exception e) {     
        System.out.println("-----doException-----");    
        ModelAndView modelAndView = new ModelAndView();   
        modelAndView.setViewName("error");    
        modelAndView.addObject("msg", "系统异常!");    
        modelAndView.addObject("e", e);       
        return modelAndView;   
    }

step2:创建springmvc配置文件

添加如下配置

<!-- 设置controller扫描路径 -->
<context:component-scan base-package="com.tengteng.springmvc.chat10.controller"/>

<!-- 设置全局异常处理扫描器路径 -->
<context:component-scan base-package="com.tengteng.springmvc.chat10.handle"/>

<!-- 添加mvc注解驱动 -->
<mvc:annotation-driven/>

-------------------------这几行是重点-----------------------------------

完整配置如下

<?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:mvc="http://www.springframework.org/schema/mvc"  
       xmlns:context="http://www.springframework.org/schema/context" 
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans.xsd   
       http://www.springframework.org/schema/mvc 
       https://www.springframework.org/schema/mvc/spring-mvc.xsd 
       http://www.springframework.org/schema/context 
       https://www.springframework.org/schema/context/spring-context.xsd"> 
       
       <!-- 设置controller扫描路径 -->   
       <context:component-scan base-package="com.tengteng.springmvc.chat10.controller"/>   
       
       <!-- 设置全局异常处理器扫描路径 -->  
       <context:component-scan base-package="com.tengteng.springmvc.chat10.handle"/>  
       
       <!-- 添加mvc注解驱动 -->   
       <mvc:annotation-driven/>   
       
       <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">   
           <property name="prefix" value="/WEB-INF/view/"/>    
           <property name="suffix" value=".jsp"/>  
       </bean></beans>

step3:创建几个页面

登录页面(index.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head></head>
    <body>
        <div>  
            <form method="post" action="${pageContext.request.contextPath}/login">  
                姓名:<input name="name"/><br/>   
                密码:<input name="pass"/><br/>       
                <input type="submit" value="登录">
            </form>
        </div>
    </body>
</html>
登录成功页面(/WEB-INF/view/success.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>  
        <title>Title</title>
    </head>
    <body>
        <h1>欢迎您,登录成功!</h1>
        <br/>当前用户:${requestScope.name}<br/>
    </body>
</html>
错误页面(error.jsp)
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>   
        <title>Title</title>
    </head>
    <body>
        <h1>登录失败!</h1>
        <br/>错误提示:${requestScope.msg}<br/>Exception.message:${e.message}
    </body>
</html>

5.3、验证效果(4种场景)

下面我们来分别验证正常情况、用户名错误的情况、密码错误的情况、密码为非数字的情况,然后注意idea控制台和浏览器的输出效果。

场景1:正常情况

9a300fe9-5bc6-40f6-85df-436c62827a34.png

9e359d12-b533-497d-ac57-65ab3cadce46.png

场景2:姓名错误的情况

控制台输出如下,说明进到了GlobalExceptionHandle#doNameException方法了

-----doNameException-----

页面输出

b2a55450-abdd-4151-96e4-fe11f5c54967.png

场景3:密码错误的情况

d5682926-8caf-453b-8c29-d92449ceb3f9.png

控制台输出如下,说明进到了GlobalExceptionHandle#doPassException方法了

-----doPassException-----

页面输出

9511322a-8fb2-4731-811f-e7d1ab18ce52.png

场景4:密码为非数字的情况

1c739dac-fab0-459f-84fd-f78ef4304593.png

控制台输出如下,说明进到了GlobalExceptionHandle#doException方法了

-----doException-----

页面输出如下,由于密码必须为数字,而我们输入的是abc,无法转换为数字,此时springmvc内部在进行参数转换的时候,会自动抛出异常,从下面的异常信息中也可以看出是类型转换错误

f483ba5f-c510-4ce0-b729-a73821cf82bf.png

6、总结

  • 本文详细介绍了springmvc集中统一异常处理的具体用法,大家消化一下,重点主要用到了2个注解@ControllerAdvice@ExceptionHandler
  • 目前多数系统都是前后端分离了,后端所有的接口都返回json格式的数据,大家可以看下这篇文章,这一篇文章返回的是json数据,如何优雅的写controller层