转载

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

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

  • HandlerMapping 和 HandlerAdapter
  • HandlerExceptionResolver
  • LocaleResolver & LocaleContextResolver
  • ThemeResolver
  • MultipartResolver

HandlerAdapter 和 HandlerMapping 解析

前期准备

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

<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,不推荐使用,这里只做帮助理解使用。

HandlerAdapter

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

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

HandlerMapping

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

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

HandlerAdapter 和 HandlerMapping 的测试

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

<?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 的配置:

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 上的前端视图返回。

HandlerExceptionResolver 解析

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

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

实现 HandlerExceptionResolver 接口

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

ModelAndView resolveException(HttpServletRequest var1, HttpServletResponse var2, Object var3, Exception var4);
复制代码

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

实现一个自定义的 Exception: MyCustomException

package com.test.myapp.example.handlermapping;

public class MyCustomException extends RuntimeException {
    public MyCustomException(String msg) {
        super(msg);
    }
}
复制代码

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

实现 HandlerExceptionResolver 接口:ExceptionResolver

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 字段的信息。

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

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

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~!");
    }
}
复制代码

视图文件 error.jsp 配置

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

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Ooooops, you meet MyCustomException</title>
</head>
<body>
    <h1>${msg}</h1>
</body>
</html>
复制代码

测试

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

  • http://localhost:8080/custom-exception.do:浏览器返回了 error.jsp 的视图并且输出了包裹在 MyCustomException 的信息,符合预期
  • http://localhost:8080/argument-exception.do:浏览器返回了 500 Internal Server Error,因为没有用于 IllegalArgumentException 的 resolver,因此返回了默认的视图。

使用 @ExceptionHandler 注解

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

设置自定义 Exception: CustomExceptionForAnnotation

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

package com.test.myapp.example.exceptionresolver;

public class CustomExceptionForAnnotation extends RuntimeException {
    public CustomExceptionForAnnotation(String msg) {
        super(msg);
    }
}
复制代码

实现有 @ExceptionHandler 注解的 Controller 基类

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

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 信息到前端。

实现两个 Controller 类

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

  • 继承了 BaseExceptionResolver 的 Controller 类 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: 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");
        }
    }
    复制代码

测试

我们仍然使用了 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 解析器了。

小结

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

原文  https://juejin.im/post/5bf56263f265da61241514a3
正文到此结束
Loading...