Spring WebMvc基础-异常处理与拦截器

从 0 开始深入学习 Spring 专栏目录总览

WebMvc 的基础部分,还有两个比较重要的知识点,趁着热乎劲一起学了吧。

1. 统一异常处理【掌握】

现在我们写的代码中,有一个问题一直没有解决,就是当代码里报异常的时候,会跳转到 Tomcat 的 500 页面,而且里面的异常信息一大堆:

Spring WebMvc基础-异常处理与拦截器

究其原因,是因为之前在第 73 章写的 save 方法,有一个抛出运行时异常的动作。又因为我们没有再写别的 catch 去抓这个异常,最终导致这个异常抛到了 DispatcherServlet 的手中,而 DispatcherServlet 又无法解决,只好显示在页面了。

话又说回来,这页面可不能给用户看啊,体验不好就算了,我们背后的代码啥的都让人家知道了,这就不太美好。

所以,这个问题我们要解决,而解决的核心关键点在于,如何让 DispatcherServlet 能处理抛给它手里的异常

1.1 如何处理异常

先思考一下,如果我们要处理异常的话,可以使用哪些方法来处理。

1)对于 Controller 、Service 、Dao 层的每个方法,在可能预见到会有异常触发的位置使用 try-catch 进行处理。但这种写法太过于复杂太过于费劲,所以我们在开发中肯定不会这么干;

2)借助过滤器,捕获从 Controller 方法开始执行,直到方法执行完毕,这一整个过程中可能抛出的异常,并进行相应的处理。这种写法虽然在解决上好多了,但每一次处理异常的时候,都需要重新编码实现,可维护性不是特别高;

3)接着上面的思路进行延伸:如果能有一套机制,这个机制可以让我们针对指定的异常,编写对应的异常处理逻辑,最终把这些逻辑都汇总到一起,交给上面说的这个过滤器,那是不是当触发了某个特定异常的时候,这个异常处理的过滤器会自动执行我们编写好的异常处理逻辑。

可见,用最后一种方式,可扩展性也高,而且声明起来也简单。

1.2 @ExceptionHandler

既然我们自己都能分析出来,那 SpringWebMvc 这么优秀的框架自然也能想得到。SpringWebMvc 给我们提供了一个 @ExceptionHandler 的注解,用它可以声明式的捕获指定的异常。下面我们先来看一个简单的使用方式。

1.2.1 声明异常处理的普通类

回到最开始学习 SpringWebMvc 的那个状态,我们来编写一个最最简单的跳转页面的方法(但不要加任何注解):

public class RuntimeExceptionHandler {
    
    public String handleRuntimeException() {
        
        return "error";
    }
}

这方法够简单了吧,为啥说先不要加注解了呢,原因就在于,接下来要标注的注解跟之前不一样了。。。在整个类上,不再标注之前的 @Controller 或者 @RestController ,而是一个新的注解叫 @ControllerAdvice ;在要处理异常的方法上标注 @ExceptionHandler ,并指定 value 为要捕捉的异常类型:

@ControllerAdvice
public class RuntimeExceptionHandler {
    
    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException() {
        
        return "error";
    }
}

接下来,这个方法可以传哪些参数呢?HttpServletRequest 和 HttpServletResponse 咱就不说了,肯定能传,除此之外,最重要的就是传异常类型了。比方说这个类我要捕捉所有的 RuntimeException ,那方法的参数就可以带上 :

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(HttpServletRequest request, 
            HttpServletResponse response, RuntimeException e) {
        e.printStackTrace();
        return "error";
    }

之后里面的处理逻辑我们就可以自己写啦,比方说我们先在这里打印一下异常的栈信息,别的啥事不干。

1.2.2 编写error页面

上面的方法中返回了 error ,这个套路跟普通的 Controller 方法是一致的,所以返回 error 就相当于跳转到 error.jsp 页面。那这样我们就来一个最最简单的异常页面吧(不用花里胡哨的,费劲 ~ )

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>错误提示</title>
</head>
<body>
系统出现异常。
</body>
</html>

1.2.3 测试运行

直接重启 Tomcat ,来到用户信息的修改页面,在用户名的输入框中只写一个 z ,保存用户,发现这次不再展示 Tomcat 的报错页面了,而是跳转到了刚才我们定义的 error 页面:

Spring WebMvc基础-异常处理与拦截器

1.2.4 错误页面展示异常信息

如果想在跳转到错误页面时,带入异常的信息,也很简单,只需要在处理异常时,给 request 域中添加异常相关的信息即可,至于添加哪些,那就是你们自己在处理异常时怎么搞咯:

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(HttpServletRequest request, HttpServletResponse response, RuntimeException e) {
        e.printStackTrace();
        request.setAttribute("message", e.getMessage());
        return "error";
    }

相应的,记得在页面上加上异常信息的展示:

<body>
    系统出现异常。
    <c:if test="${message != null}">
        异常原因:${message}
    </c:if>
</body>

重启 Tomcat ,就可以看到修改之后的效果了。

1.3 多种异常处理共存

当然,在实际开发中不可能捕捉一个 RuntimeException ,甚至无脑捕捉 Exception 的,那这跟不处理没什么特别大的区别了。。。更多的写法,还是分别对不同的异常,分门别类的处理。为了演示通过不同的方式触发不同的异常,我们在 UserController 的 save 方法中,添加一个新的 if 分支,来抛出 IllegalArgumentException 的异常:

    @PostMapping("/save")
    public String save(@Validated(UserPasswordGroup.class) User user, BindingResult bindingResult) {
        if (StringUtils.isEmpty(user.getName())) {
            throw new IllegalArgumentException("User的name为空");
        }
        if (bindingResult.hasErrors()) {
            throw new RuntimeException("数据格式不正确!");
        }
        System.out.println(user);
        return "redirect:/user/list";
    }

相应的,就应该在 RuntimeExceptionHandler 中添加一个新的异常处理方法了。为了能在页面上体现出差异,所以我们在 handleIllegalArgumentException 方法的 setAttribute 异常信息添加一个标识性的提示前缀:

    @ExceptionHandler(RuntimeException.class)
    public String handleRuntimeException(HttpServletRequest request, RuntimeException e) {
        request.setAttribute("message", e.getMessage());
        return "error";
    }
    
    @ExceptionHandler(IllegalArgumentException.class)
    public String handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
        request.setAttribute("message", "[不合法的参数]" + e.getMessage());
        return "error";
    }

好,写完之后,重启 Tomcat ,通过测试触发不同的异常,可以发现触发的异常处理方法确实不同。

Spring WebMvc基础-异常处理与拦截器

而且除此之外,还能得出一个结论:更细粒度的异常处理,优先级高于粗粒度的异常处理

1.4 自定义异常与处理

除了使用现有的异常类之外,还有很多情况是我们项目中的自定义异常类。这种处理方式在本质上与前面的处理方式并无差别,小伙伴可以自行定义、练习。

1.5 @ControllerAdvice

上面的编写中,我们用到了一个 @ControllerAdvice 注解,下面咱来说说它的作用。

1.5.1 Controller的增强

看到 Advice ,各位应该能第一时间联想到 AOP 吧,增强是吧。所以 @ControllerAdvice 注解有增强 Controller 的意思。

但注意,标注上这个注解后,是它增强别的 Controller ,不是它需要被增强。

另外,请注意,@ControllerAdvice 并不是 @Controller ,它只是一个普通的 @Component :

@Component
public @interface ControllerAdvice { ... }

默认情况下,被 @ControllerAdvice 标注的类,会增强所有的 @Controller ,当然我们也可以通过声明 @ControllerAdvice 的 value / basePackages 来指定增强的包,也可以声明 assignableTypes 来单独点名指定要增强的 @Controller :

@ControllerAdvice(basePackages = "com.linkedbear.spring.withdao.controller")
@ControllerAdvice(assignableTypes = UserController.class)

1.5.2 配合其他注解

在 @ControllerAdvice 的文档注释上,第一段他就提出了与之配合的几个注解:

Specialization of @Component for classes that declare @ExceptionHandler, @InitBinder, or @ModelAttribute methods to be shared across multiple @Controller classes.

它的本质是一个 @Component ,它专门用于给多个标注了 @Controller 注解的类之间,共享标注有 @ExceptionHandler@InitBinder 或 @ModelAttribute 的方法。

可见,@ControllerAdvice 不止可以用 @ExceptionHandler 来增强 Controller 中的异常处理,还可以共享 @InitBinder 、@ModelAttribute 注解的方法,以实现增强。这些注解的配合使用,我们放在下一章中讲解。

2. 拦截器【熟悉】

拦截器是 SpringWebMvc 基础的最后一个知识点了。说到拦截器,小伙伴们很容易联想到过滤器,所以咱先就拦截器和过滤器,对比一下。

2.1 【面试题】拦截器和过滤器对比

首先我们要明白一点,拦截器是 SpringWebMvc 的概念,而过滤器是 Servlet 的概念。Servlet 、Filter 、Listener 共同称为 Servlet 三大组件,它们都需要依赖 Servlet 的 API 。而拦截器不同,拦截器是 SpringWebMvc 提出,它只是 SpringWebMvc 框架中的一个 API 设计罢了。所以我们可以总结出第一个区别:拦截器是框架的概念,而过滤器是 Servlet 的概念。

那既然两方的概念来源不同,那它们的拦截范围也就不一样了。过滤器 Filter 是在 web.xml 或者借助 Servlet 3.0 规范来注册,任何来自于 Servlet 容器(Tomcat)的请求都会走这些过滤器;拦截器既然是框架的概念,而 SpringWebMvc 的核心是一个 DispatcherServlet ,所以拦截器实际上是在 DispatcherServlet 接收到请求后才有机会工作的,对于 DispatcherServlet 没有接收到的请求,拦截器只能干瞪眼。所以接下来总结出的第二个区别:过滤器可以拦截几乎所有请求,而拦截器只能拦截到被 DispatcherServlet 接收处理的请求。

继续,来源不同还造成一个不同的现象:过滤器由 Servlet 容器创建,与 SpringFramework 的 IOC 没有任何关系,所以无法借助依赖注入,给过滤器中注入属性;而拦截器是被 SpringFramework 的 IOC 统一管理的,它就是一个一个的普通的 bean ,所以可以任意注入需要的 bean 。所以第三条区别:拦截器可以借助依赖注入获取所需要的 bean ,而过滤器无法使用正常手段获取。

如果深入到底层调用,可以发现,**过滤器的调用是一层一层的函数回调,而拦截器是 SpringWebMvc 在底层借助反射调用的。**由于这部分会涉及到源码,所以这里咱就不展开翻源码了,感兴趣的小伙伴可以回头自己看一下。

2.2 拦截器的拦截时机

不同于过滤器,SpringWebMvc 设计的拦截器,在拦截时机的切入更多,具体的核心接口是 HandlerInterceptor ,这里面定义了三个方法:

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {
	}

	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {
	}
}

可以发现,三个方法的切入时机由先到后依次是:

  • preHandle :在执行 Controller 的方法之前触发,可用于编码、权限校验拦截等
  • postHandle :在执行完 Controller 方法后,跳转页面 / 返回 json 数据之前触发
  • afterCompletion :在完全执行完 Controller 方法后触发,可用于异常处理、性能监控等

注意这里面 postHandle 和 afterCompletion 的区别,postHandle 方法的参数上有一个 ModelAndView ,证明该时机下还没有确定好数据的封装和视图的返回(页面跳转),此时是可以对数据和视图进行修改的;而到了下边的 afterCompletion 方法的参数上没有 ModelAndView 了,而是多出来一个 Exception ,这代表方法执行可能出现了异常,可以在此处进行异常处理。

具体的使用我们可以来实际的编码测试一下。

2.3 拦截器的简单使用

我们先来定义一个最简单的拦截器,先体会一下拦截器的工作机制。

2.3.1 定义拦截器

public class DemoInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("DemoInterceptor preHandle ......");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("DemoInterceptor postHandle ......");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("DemoInterceptor afterCompletion ......");
    }
}

够简单了吧!注意一点,preHandle 方法的返回值要为 true !如果设置为 false ,则 Controller 方法,以及下面的 postHandle 和 afterCompletion 方法都不会执行

2.3.2 配置拦截器

接下来,我们把定义好的拦截器,配置到 mvc 的 IOC 容器中。mvc 下有一个专门配置拦截器的标签,叫 <mvc:interceptors> :

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/department/**"/>
            <bean class="com.linkedbear.spring.withdao.interceptor.DemoInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

注意这个配置的套路,<mvc:mapping> 中配置的路径,如果要匹配路径及子路径的话,要用 /** 。然后下面的 <bean> 就是配置具体的拦截器了。

2.3.3 测试运行

重启 Tomcat ,浏览器访问 http://localhost:8080/spring-webmvc/department/list ,可以发现控制台上可以打印 DemoInterceptor 的日志:

DemoInterceptor preHandle ......
DemoInterceptor postHandle ......
DemoInterceptor afterCompletion ......

而访问 http://localhost:8080/spring-webmvc/user/list 时,拦截器不会向控制台打印任何信息,说明拦截器配置一切成功。

2.4 多个拦截器的执行机制

拦截器不像过滤器,过滤器的执行是一条单向的链,执行过去就完事了,但拦截器并不是这样。我们可以来声明两个不同的拦截器,来测试一下效果。

2.4.1 定义两个不同的拦截器

直接把上面的 DemoInterceptor 复制粘贴两份,并将其中的控制台打印添加 1 和 2 的后缀:

public class DemoInterceptor1 implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("DemoInterceptor1 preHandle ......");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("DemoInterceptor1 postHandle ......");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("DemoInterceptor1 afterCompletion ......");
    }
}

( DemoInterceptor2 就不粘贴出来了)

2.4.2 配置两个不同的拦截器

拦截器的配置是有先后顺序的,比方说像下面这样的配置,1 是在 2 前头的,反之亦然。

    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/department/**"/>
            <bean class="com.linkedbear.spring.withdao.interceptor.DemoInterceptor1"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/department/**"/>
            <bean class="com.linkedbear.spring.withdao.interceptor.DemoInterceptor2"/>
        </mvc:interceptor>
    </mvc:interceptors>

2.4.3 测试运行

重启 Tomcat ,此时两个拦截器的 preHandle 方法均返回 true ,代表两个拦截器都可以正常执行。

浏览器访问 http://localhost:8080/spring-webmvc/department/list ,控制台打印出这样的记录:

DemoInterceptor1 preHandle ......
DemoInterceptor2 preHandle ......
DemoInterceptor2 postHandle ......
DemoInterceptor1 postHandle ......
DemoInterceptor2 afterCompletion ......
DemoInterceptor1 afterCompletion ......

诶?虽然 preHandle 下 1 在 2 前头,但是 postHandle 和 afterCompletion 方法都是 2 先 1 后啊,这是为什么呢?

2.4.4 拦截器的执行机制

根据上面的测试结果,我们可以总结出一张这样的执行流程图:

Spring WebMvc基础-异常处理与拦截器

preHandle 方法是顺序执行,postHandle 和 afterCompletion 方法均是逆序执行。

2.4.5 测试preHandle

下面咱继续测试如果 preHandle 方法返回 false ,会有什么效果。

首先我们先把 DemoInterceptor1 的 preHandle 返回值改为 false ,重启 Tomcat 看效果:

嗯?页面咋变空白了?而且控制台也只打印了这一行:

DemoInterceptor1 preHandle ......

哎,刚才咋说的来着,preHandle 方法返回值为 false ,则 Controller 方法,以及下面的 postHandle 和 afterCompletion 方法都不会执行,所以接下来的 DemoInterceptor2 ,以及 Controller 方法都没有执行,相当于访问了一个返回值为 void 的 Controller 方法,那自然啥都没干,就给我们返回一个空白页咯。

接下来我们把 DemoInterceptor1 的 preHandle 返回值改为 true ,把 DemoInterceptor2 的 preHandle 返回值改为 false ,重新测试:

emmmm 果不其然,页面还是空白的,这已经在我们的预料之内了,但控制台打印了 3 行:

DemoInterceptor1 preHandle ......
DemoInterceptor2 preHandle ......
DemoInterceptor1 afterCompletion ......

咦,这就有点意思了。因为 DemoInterceptor2 在 DemoInterceptor1 之后,而且 DemoInterceptor1 的返回值为 true ,所以导致 DemoInterceptor2 返回 false 后,DemoInterceptor1 仍然能够调用 afterCompletion 成功,但 postHandle 两个都没有调用。

所以我们可以得出最终结论了:

  • 只有 preHandle 方法返回 true 时,afterCompletion 方法才会调用
  • 只有所有 preHandle 方法的返回值全部为 true 时,Controller 方法和 postHandle 方法才会调用

OK 到这里有关拦截器的知识也就说完了,这个东西我们没必要刻意的去记,会写会用就行。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring WebMvc基础-异常处理与拦截器