WebApplicationContext 中特殊的 bean 类型(一)--- 请求/异常处理

1 前言

其实 Spring 的基本思想就是“万物都是 bean”,那么为了满足 spring 工程的需要,spring 中有一些默认的 bean 选项,它们用于处理请求,渲染视图等。比如上一篇文章就用过的 viewResolver 的配置。当然,servlet 也允许你配置使用不同特定的 bean,但是,如果你没有配置,spring 将会按照默认的 bean 进行配置。本章将会详细说明文档中列出的 bean 的配置以及具体的使用例子,所讲述的 bean 类型包括:(使用版本:Spring-webmvc 4.3.18.RELEASE)

2 HandlerAdapter 和 HandlerMapping 解析

2.1 前期准备

本章节将基于文档实践(一)的代码进行后续的操作,因此我们使用了单个 ContextConfig 来配置工程 Context 对象,也就是 root-context.xml 文件。另一方面,为了实现 HandlerMapping 在 xml 配置的功能,我们关掉了

1
<mvc:annotation-driven/>

的功能,使得 @Controller 注解下的类不再会被自动配置并且做 url 的映射,现在再去试一下 localhost:8080/hello.do 的话,已经是 404 Not Found 了。之后再进行后续的实践过程。

这里 HandlerMapping 和 HandlerAdapter 一起讲是因为,HandlerMapping 需要 HandlerAdapter 的支持才能正常运行。HandlerMapping 用于将请求的 url 映射到对应的 controller 上面,如果没有进行配置的话,@Controller 注解即为 HandlerMapping,上一篇的 ExampleController 即有着和上述相似的功能。值得注意的是,Spring MVC 4.0 之后主推 Annotation Driven,也就是注解驱动模式下的工程,因此,对应的 adapter 已经标记为 deprecated,不推荐使用,这里只做帮助理解使用。

2.2 HandlerAdapter

由于工程中的 Controller 都是用注解配置的,因此,在 DispatcherServlet 根据 bean 的配置信息(root-context.xml,我们用 Context 对象来配置 bean 的信息)知道了自己所需要调用的 controller 之后,他需要根据注解来提取其他的所需要的信息。这时候就需要 HandlerAdapter 来做这些解析的事情。

然而,目前的 Spring MVC 的配置都基于注解,因此,HandlerAdapter 也退居幕后,@Controller 注解包含了其中逻辑,在 Annotation-driven 被我们关掉的场景下,也只要做好 HandlerMapping,就可以成功地映射你想要的 url

2.3 HandlerMapping

HandlerMapping 本质还是一个 Bean,他在 Spring MVC 装配完成之后,执行着将 URL 的请求转发到对应的 Controller 执行后续视图,数据等返回的工作。因此,在配置 HandlerMapping Bean 的时候,需要配置 property 的 mappings 字段,并且在 字段下面指定对应的请求映射。具体代码如下:

1
2
3
4
5
6
7
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/handler-mapping.do">handlerMappingController</prop>
</props>
</property>
</bean>

2.4 HandlerAdapter 和 HandlerMapping 的测试

为了同步一下,目前 root-context.xml (Spring Context 对象配置文件) 的配置加入了 HandlerMapping 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<context:component-scan base-package="com.test.myapp.example"/>

<!--注册一个用于 handlerMapping 的 bean 用于检测 handlerMapping 效果-->
<bean id="handlerMappingController" class="com.test.myapp.example.handlermapping.HandlerMappingController"/>

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/handler-mapping.do">handlerMappingController</prop>
</props>
</property>
</bean>

<!--<bean id="simpleHandler" class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>-->
<!--<mvc:annotation-driven/>-->

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/views/" p:suffix=".jsp" p:order="1">
</bean>

</beans>

并且新增了 HandlerMappingController.java 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
* Usage: 测试 handler mapping 的有效性
* @author: srfan
* Date: 10/26/18 4:11 PM
*/
@Controller
public class HandlerMappingController {

@RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
public String helloWorld() {
return "handler_mapping_hello";
}
}

我们看到,HandlerMapping 下面配置了 /handler-mapping.do 的映射。因此,在运行工程之后,输入 localhost:8080/handler-mapping.do,就可以看到对应的 handler_mapping_hello.jsp 上的前端视图返回。

3 HandlerExceptionResolver 解析

HandlerExceptionResolver 是工程中用于捕获特定 Exception 的 Bean,可以提前设定自己需要捕获并且定向的 Exception,并且交由 HandlerExceptionResolver 映射到特定的视图页上面。 目前常用的方法有:

  • 实现 HandlerExceptionResolver 接口
  • 在方法上使用 @ExceptionHandler 注解

3.1 实现 HandlerExceptionResolver 接口

HandlerExceptionResolver 接口只有一个待实现的方法

1
ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4);

为了工程上面比较直观简便的实现,我们只需要做最简单的实现:拿到 Exception 的具体类,并且返回对应的 error 的视图,并且记录下 Exception 的 message,显示在视图页面上面。因此我们的工序如下:

3.1.1 实现一个自定义的 Exception: MyCustomException

1
2
3
4
5
6
7
package com.test.myapp.example.handlermapping;

public class MyCustomException extends RuntimeException {
public MyCustomException(String msg) {
super(msg);
}
}

这个 Exception 类很简单,只是把 message 放进 Exception 中,无需赘述,主要是要让 ExceptionResolver 捕获该 Exception。

3.1.2 实现 HandlerExceptionResolver 接口:ExceptionResolver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class ExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) {
if (e instanceof MyCustomException) {
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("msg", e.getMessage());
return modelAndView;
}
return null;
}
}

我们使用 ExceptionResolver 实现了 resolveException 方法,并且会解析 MyCustomException 并且在 ModelAndView 对象加入一个变量,并且返回名为 “error” 的 jsp 视图。我们也可以在 error.jsp 上显示这个 msg 字段的信息。

3.1.3 HandlerMappingController 添加两个会抛出 Exception 的接口

为了对照效果,我们实现两个接口,一个会抛出 MyCustomException,另一个则会抛出普通的 IllegalArgumentException,而我们需要捕获的则是 MyCustomException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.test.myapp.example.handlermapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HandlerMappingController {

@RequestMapping(value="/handler-mapping.do", method = RequestMethod.GET)
public String helloWorld() {
return "handler_mapping_hello";
}

@RequestMapping(value="/custom-exception.do", method = RequestMethod.GET)
public String throwException() {
throw new MyCustomException("oh, you got custom exception message~!");
}

@RequestMapping(value="/argument-exception.do", method = RequestMethod.GET)
public String throwArgumentException() {
throw new IllegalArgumentException("oh, you got argument exception message~!");
}
}

3.1.4 视图文件 error.jsp 配置

视图文件 error.jsp 比较简单,只要体现 msg 字段即可:

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Ooooops, you meet MyCustomException</title>
</head>
<body>
<h1>${msg}</h1>
</body>
</html>

3.1.5 测试

运行工程后,在浏览器分别输入:

3.2 使用 @ExceptionHandler 注解

另一种方法是使用 @ExceptionHandler 的注解,该注解用于 method 的签名上面,我们可以实现一个 Controller 的基类并让实际接收 url 请求的 Controller 继承该基类。值得注意的是,这个方法实现的 ExceptionResolver 只会在该 Controller 内部有效,而来自其他 Controller 类的 Exception 则无法得到解析。具体代码步骤如下:

3.2.1 设置自定义 Exception: CustomExceptionForAnnotation

我们为这一次测试也设置了自定义的 Exception 类,实现方法也很简单,可以自定义 Exception 中的信息:

1
2
3
4
5
6
7
package com.test.myapp.example.exceptionresolver;

public class CustomExceptionForAnnotation extends RuntimeException {
public CustomExceptionForAnnotation(String msg) {
super(msg);
}
}

3.2.2 实现有 @ExceptionHandler 注解的 Controller 基类

我们的 Controller 基类需要 Resolve CustomExceptionForAnnotation,需要用 @ExceptionHandler(CustomExceptionForAnnotation.class) 进行配置,具体方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.test.myapp.example.exceptionresolver;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;

public abstract class BaseExceptionResolver {
@ExceptionHandler({CustomExceptionForAnnotation.class})
public ModelAndView handleCustomException(CustomExceptionForAnnotation ex) {
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("msg", ex.getMessage());
return modelAndView;
}
}

可以看到,该类中所含有的方法仅会解析 CustomExceptionForAnnotation 类,并且将其重新导向 error.jsp 视图,最后输出对应的 message 信息到前端。

3.2.3 实现两个 Controller 类

为了使测试结果有对照性,我们实现了两个 Controller 类,一个继承自 BaseExceptionResolver,另一个则没有。理论上说,继承了 BaseExceptionResolver 的 Controller 将可以解析上面的 Exception,而另一个则不能。具体的配置方法如下:

  • 继承了 BaseExceptionResolver 的 Controller 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.test.myapp.example.exceptionresolver;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;

    @Controller
    public class MyExceptionController extends BaseExceptionResolver {

    @RequestMapping("exception-for-annotation.do")
    public void exceptionForAnnotation() {
    throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message");
    }
    }
  • 未继承 BaseExceptionResolver:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.test.myapp.example.exceptionresolver;

    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;

    @Controller
    public class MyExceptionOutsideController {

    @RequestMapping("exception-for-annotation-outside.do")
    public void exceptionForAnnotation() {
    throw new CustomExceptionForAnnotation("Oooops, you get CustomExceptionForAnnotation message");
    }
    }

3.2.4 测试

我们仍然使用了 error.jsp 视图来做最后的测试工作,我们看到 BaseExceptionResolver 在捕获异常后,仍然会输出 error.jsp 的视图。我们将会请求两个具体 Controller 类的 url,观察是否会有我们想要的视图的输出:

  • localhost:8080/exception-for-annotation.do: 成功输出了我们放入 CustomExceptionForAnnotation 的信息。
  • localhost:8080/exception-for-annotation-outside.do: 页面输出了 500 的错误信息,并且带上了 Exception 中的信息,因为其没有继承 BaseExceptionResolver,因此也没有对应的 Exception 解析器了。

4 小结

本章主要讲述了 HandlerMapping 和 HandlerExceptionResolver 的具体实现代码,一个是处理正常的 url 请求的映射工具,而另一个则是专门处理工程在运行过程中出现 Exception 的处理方法。下一次我将继续介绍后面这几个特殊 Bean 的用法。