欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringMVC之五:Controller

程序员文章站 2022-07-14 11:58:22
...

@Controller将一个bean标注为控制器,@RequestMapping标注一个控制器方法为url处理器。这大概是我们在日常开发工作中接触得最多的两个关键字了。本章介绍Controller类,尤其是@RequestMapping标注的处理器方法背后的工作原理。这部分介绍的知识,都是大家在日常工作中大概率能用上的知识,非常值得我们花点时间把它搞清楚。

本章的示例代码:SpringMVCSample的子工程Controller。

Controller

@Contoller是@Component的组合注解,因此被@Controller标注的类,能够被@ComponentScan扫描并初始化为一个bean。@Controller标注的bean,会被RequestMappingHandlerMapping扫描,将@RequestMapping标注的成员方法扫描出来,记录下url到bean方法之间的映射关系。

一个典型的Controller是这样定义的:

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

@RequestMapping标准的方法支持多种的参数和返回值形式,上面这个方法接受一个Model参数,处理逻辑往model里面插入数据,String类型的返回值代表视图View的名字,交给ViewResolver来解析。完美地展示了M+V+C分离的设计思想。Model和View的讨论我们放到后面的章节。

@RequestMapping匹配

前面章节我们介绍了HandlerMapping这个bean类型,以及它的具体实现RequestMappingHandlerMapping。@RequestMapping注解声明了path和controller方法之间的映射关系,这个注解会被RequestMappingHandlerMapping扫描到,后者会将这个映射关系缓存起来。

限定HttpMethod

RequestMapping可以限定映射接受的HttpMethod, 比如,@RequestMapping(value="/hello",method=RequestMethod.GET)限定这个映射只接受HTTP GET,method属性可以设定为多个值。Spring定义了一些快捷的注解来支持特定HttpMethod的映射关系:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

URL匹配

RequestMapping使用的url匹配规则是AntPathMatcher:

  • ?匹配单个字符,分隔符除外;
  • *匹配多个字符,分隔符除外;
  • **匹配一个或多个路径段;

PathVariable(路径变量)

路径变量是指抓取url某个路径段内的字符串,作为一个变量给Controller方法使用:

@GetMapping("/owners/{ownerId}/pets/is{petId}")
public Pet findPet(@PathVariable("ownerId") Long ownerId, @PathVariable("petId") Long petId) {
    // ...
}

@GetMapping的url设定中,{}其实起了两个作用,一是这一段url的匹配规则类似一个*符号;二是将运行时匹配的这一段字符串作为一个具名变量注入到处理器方法。

可以对PathVariable附加一个正则表达式来约束它能匹配的字符串:

@GetMapping("/owners/{ownerId:kfc\d+}")
public Pet findPet(@PathVariable("ownerId") Long ownerId) {
    // ...
}

如果处理器方法参数列表里声明的PathVariable不是String,那么需要进行类型转换,上一个文章系列:Spring基础里介绍的类型转换机制就派上了用场。

Pattern优先级

由于RequestMapping里面可以包含通配符,那么可能出现的情况是多个mapping规则可以匹配同一个输入的url,此时需要有一种判定机制。AntPathMatcher.getPatternComparator(String path)生成基于具体url的一个比较器,用来比较不同匹配规则的相对优先级。

这个比较器的具体细节比较复杂,总体原则是更具体的匹配模式胜出:

  • 有更多PathVariable的模式更具体;
  • ?通配符比*更具体,*比**更具体;
  • /**是一个特殊的模式,具备最低的优先级。

后缀匹配

默认配置下,Spring MVC匹配任意的路径扩展名,它的意思是,@RequestMapping("/person")能够匹配/person.*,比如/person.pdf,person.json和persion.xml等。

这条规则的初衷是,用扩展名来表达客户端期望的content-type,上面的示例表示客户端分别期望服务端返回pdf、json和xml格式的数据。不过目前这种方式已经不被推荐,它在复杂情形下导致模糊性,比如在使用路径变量的时候(@RequestMapping("/{person}")),变量person的值是否应该包含扩展名呢?

目前的推荐做法是客户端添加明确的HTTP HEADER字段:Accept,来表明接受的内容格式。可以关闭路径扩展名匹配:

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.setUseSuffixPatternMatch(false);
    }
}

限定请求Content-Type

@RequestMapping可以通过consumes属性来限定http请求内容的Content-Type:

@PostMapping(path = "/pets", consumes = "application/json") 
public void addPet(@RequestBody Pet pet) {
    // ...
}

上面的配置,意味着这个处理器只匹配内容格式是application/json的Http请求。属性值还可以添加逻辑操作符,比如consumes = "!application/json",接受非application/json的Http请求。

限定响应Content-Type

@RequestMapping可以通过produces属性来匹配Http请求的头部字段Accept:

@GetMapping(path = "/pets/{petId}", produces = "application/json") 
public Pet getPet(@PathVariable String petId) {
    // ...
}

上面的配置,意味着这个http request必须要接受application/json类型的响应,才会匹配。这个属性同样支持添加逻辑操作符。

参数字段匹配

@RequestMapping可以通过params属性来匹配参数值:

@GetMapping(path = "/pets/{petId}", param="name=cat") 
public void findCat(@PathVariable String petId) {
    // ...
}

上面的配置,意味着这个请求,必须携带参数name,且值为cat。还可以配置为!name表示不能有这个参数。

Header字段匹配

@RequestMapping可以通过params属性来匹配参数值:

@GetMapping(path = "/pets/{petId}", headers="name=cat") 
public void findCat(@PathVariable String petId) {
    // ...
}

上面的配置,意味着这个请求,必须携带Header name,且值为cat。其规则与参数字段匹配一致。

HTTP方法:HEAD, OPTIONS

@RequestMapping自动支持HTTP的HEAD和OPTIONS方法,不需要显示配置。也即,无论是@RequestMapping(method=HttpMethod.GET)还是@GetMapping都会支持。

@RequestMapping放在Controller类上

@RequestMapping还可以放在Controller类上,就像这样:

@Controller
@RequestMapping(value="hello", method="GET")
public class HelloController {
	//...
}

最终,类上面的@RequestMapping配置和方法上的@RequestMapping配置会结合起来,每个属性结合的方式不一致。比如url属性是将两者拼接起来,而其他属性,基本都是后者覆盖前者。

Controller方法的参数

@RequestMapping标注的控制器方法,可以有非常灵活的签名形式,支持各种类型的参数类型和返回值类型。

控制器方法的参数和返回值有两种工作模式:

  1. 特定Java类型的参数或返回值,比如HttpServletRequest,有其固定的含义;
  2. 通过注解,比如@RequestParam,给参数或返回值赋予特定的含义;

下面介绍一些常见的参数形式,并不覆盖全部Spring MVC支持的类型,有些特殊用途的参数形式(比如model相关的),放到相关话题里面介绍。

Request和Response类型

当我们需要在直接访问httprequest和httpresponse对象时,可选用的类型有以下几种:

  • ServletRequest和ServletResponse
  • HttpServletRequest和HttpServletResponse,原始的servlet类型;
  • MultipartHttpServletRequest,对于multi-part请求可使用该类型;
  • WebRequest或NativeWebRequest,spring包装的request类型,当不想依赖sevlet类型时;

典型的使用方式:

@GetMapping("url") 
public void pets(HttpServletRequest request, HttpServletResponse response) {
    // ...
}

通过request参数我们几乎可以获取http请求任何信息,而通过response则可以直接写入返回数据。这是最直接也最原始的处理请求的方式,代码会比较繁琐,也与框架层紧密耦合,要尽量避免。

注:request和response可单独使用,也可其他参数类型混合使用。

HttpEntity类型

将参数类型声明为HttpEntity<B>,可以访问请求的header,并且自动把http body转换为实体类型(B)。

@GetMapping("url") 
public void entity(HttpEntity<String> httpEntity) {
	//httpEntity.getHeaders()
   //httpEntity.getBody()
}

HttpSession类型

使用HttpSession类型的参数,可以直接访问和操纵当前Session的属性:

@GetMapping("url") 
public void entity(HttpSession session) {
   //session.getSessionAttribute()
}

@RequestParam

@RequestParam可以提取HttpRequest的参数,包括query参数和表单字段,典型的用法如下:

@RequestMapping("/pets") 
public void pets(@RequestParam("name") String name) {
   // ...
}

可以声明@RequestParam(required=false)来表明参数是可选的,否则缺少对应参数会抛出异常。

如果@RequestParam修饰的变量是Map<String, String>MultiValueMap<String, String>类型,那么该map对象会被request全部参数值填充。

@RequestHeader

提取HttpRequest的头部字段,用法和@RequestParam有很大的相似性。

@CookieValue

提取HttpRequest携带的cookie字段,用法和@RequestParam有很大的相似性。

@MatrixVariable

RFC 3986定义了在URL路径段里面携带key&value的方式,这些key&value被称之为MatrixVariable,或路径参数(Path Parameters)。

//GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
    // petId == 42
    // q == 11
}

上面的代码示例了路径参数的使用,如果多个段有路径参数,那么需要区分一下:

// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {
    // q1 == 11
    // q2 == 22
}

还可以将参数类型声明为Map<String, String>MultiValueMap<String, String>,来包含一个路径段所有的参数。另外路径参数必须附在类路径变量上,对于固定的路径段,不可以添加路径参数。

@RequestAttribute

抓取HttRequest上的属性值,这些属性值被http处理环节中涉及的模块注入进入的,比如Filter和拦截器(Interceptor)。
默认情况下到底有哪些attribute可以使用,并没有文档说明,我们可以通过代码来探查一下:

@RequestMapping("requestAttribute")
public String requestAttribute(HttpServletRequest request) {
    Enumeration itr =  request.getAttributeNames();
    List<String> names = new ArrayList<>();
    while (itr.hasMoreElements()) {
        names.add(itr.nextElement().toString());
    }
    return names.stream().reduce((a,b)->a+"<br/>"+b).get();
}

结果发现Spring MVC注入到HttpRequest的attribute真不少,大都是request上下文相关的一些信息和bean对象。这些属性名大都作为常量定义在DispatcherServlet里面。

注:还可以通过RequestContextUtils来访问HttpRequest的上下文属性。

@ModelAttribute

提取Model属性:

@RequestMapping("url")
public String setModel(@ModelAttribute Pet pet) {
    return "petView";
}

在该场景下,Model属性的来源,按优先级如下:

1、Pet构造函数所需参数能在httpRequest参数里找到

比如Pet有构造函数接受name,weight参数,而http请求里面,恰好有这些参数,那么就会调用此构造函数来创建参数:

@ConstructorProperties({"name", "weight"})
public Pet(String name, int weight) {
    this.name = name;
    this.weight = weight;
}

浏览器输入http://localhost:8080/normal?name=Monkey&weight=9&来测试这种情况。

注:如果有多个构造函数都适用,那么选一个参数最多的。

2、当前的Model里有指定的属性

此时绑定该Model属性到参数值,至于此时Model里面的Pet属性从何而来,我们后面会讲。

3、使用Pet的默认构造函数

如果Pet有默认构造函数,使用它来创建参数,否则抛出异常。

普通类型参数

handler方法里面声明任意普通类型的参数(这里“普通类型”指既不是的特定Java类型,形参上也没有相关注解):

@RequestMapping("normal")
public void entity(String text, Pet pet) {
	//
}

上面这种参数签名,对于text这种简单类型,Spring MVC会从httpRequest里面去取“text”参数来赋值,没有的话就是null;而对Pet类型,处理方式和@ModelAttribute一样。

实际项目中,明确指出参数的来源才是上上之选。

设置HttpResponse

总体上说,我们有3种方式来设定返回給客户端的响应数据:

  • 通过字节流写入,这是最原始的方式;
  • 返回可转换为HttpBody的数据对象;
  • 通过Mode和View来渲染;

字节流写入

示例代码如下:

@RequestMapping("write")
public void write(HttpServletResponse response) {
    response.getWriter().write("");
} 

实际上,上面所说的3种方式,最终都是通过HttpServletResponse的输入流写入数据,而HttpServletResponse的输出流只允许写入一次。因此上面的方法只能声明为void,或返回null,否则可能会抛出异常。

注:实际上可以通过HttpServletResponse.reset来重新写入

返回HttpBody对象

又可以进一步分成几种方式:

@ReponseBody

该注解可以放在Controller方法上,也可以放在Controller类上,它表明方法返回值应该直接序列化成http body返回给客户端:

@GetMapping(path = "pets") 
@ReponseBody
public Pet pets() {
    // ...
}

HttpEntity<B>或ResponseEntity<B>

和@ReponseBody语义差不多,可以更加细致地设置返回的Header:

@GetMapping(path = "pets") 
public HttpEntity<Pet> pets() {
    // ...
}

HttpHeaders

表示Response只包含header,不包含body:

@GetMapping(path = “pets”)
public HttpHeaders pets() {
// …
}

Mode和View渲染

凡不属于前两种方式的HttpResponse机制,都会落入这种方式。

View的确定

我们大概有几种方式可以来指定渲染HttpResponse的View:

  • 返回一个字符串类型,Spring MVC认为这是一个View Name,由视图解析器(ViewResolver)来解析;
  • 返回一个View对象;
  • 返回ModelAndView,包含View(或View Name)和Model;

如果没有指明View,Spring MVC依据当前的request自动生成一个view name,具体由一个类型为RequestToViewNameTranslator的bean来执行,它默认是把request的URI字符串当做View Name。

我们可以自定义一个RequestToViewNameTranslator来修改默认行为:

@Bean(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)
public RequestToViewNameTranslator requestToViewNameTranslator() {
    return new RequestToViewNameTranslator() {
    	  //取URI的最后一段作为View Name
        @Override
        public String getViewName(HttpServletRequest request) throws Exception {
            String[] paths = request.getRequestURI().split("/");
            return paths[paths.length - 1];
        }
    };
}

至于从View Name到View的解析机制,后面有单独的章节来介绍。

Model属性

渲染View使用的Model,包含一个或多个属性,所谓属性就是普通的java POJO类或基本类型。Spring MVC在执行handler的过程中,会用一个容器来存放所有的model属性,这个容器类型是ModelAndViewContainer。

我们一般不直接接触ModelAndViewContainer,但是需要明白两件事:

  • ModelAndViewContainer在handler执行之前就已经被创建和初始化,并且可能已经包含了一些model属性;
  • handler所创建的model属性,在handler返回之后会进入这个容器;

预置Model属性

ModelAndViewContainer初始化发生在handler方法执行之前,它预置的model属性有两个来源:

1、当前Controller被@ModelAttribute标注的方法的返回值。

@Controller
public class ModelController {
    @ModelAttribute
    public Pet commonPet() {
        return new Pet("Tigger",300);
    }
}

上面这个代码段中,ModelController内的任何handler方法被调用之前,commonPet方法会被调用一次,返回值插入ModelAndViewContainer。

注意:@ModelAttribute不能和@RequestParam标注同一方法,前者会失效。

2、当前HttpSession的部分session属性

这里有两个规则:

第一个规则由@SessionAttributes指定,@SessionAttributes标注Controller,它指定了model属性的名字或类型。ModelAndViewContainer初始化时,如果当前HttpSession存在,它所包含的与@SessionAttributes匹配的session属性,会插入ModelAndViewContainer。

第二个规则由handler的@ModelAttribute参数指定,如果当前Session存在,它所包含的与@ModelAttribute参数匹配的session属性,会插入ModelAndViewContainer。

3、再看handler方法的@ModelAttribute参数

前面说过,@ModelAttribute可以注解handler方法参数,以绑定当前Model属;至此,该注解的工作原理应该讲明白了吧。

添加Model属性

handler方法可以通过以下途径添加Model属性:

  • 方法返回java.util.Map或org.springframework.ui.Model,map里面的所有对象都会成为Model的一部分;
  • 方法声明java.util.Map或org.springframework.ui.Model参数,map里面的所有对象都会成为Model的一部分;
  • @ModelAttribute注解的方法参数,方法结束后,该参数值会作为Model属性;
  • 方法返回一个非基本类型,会成为model属性;

Model属性,是一个具名的对象,以map形式添加的model属性,key是名字,value是属性值;如果我们不指定名字,那么Spring MVC依据类型自动生成一个名字,就和自动生成Bean Name类似。

如果通过多个途径,指定了同一name的model属性,那么会互相覆盖,探讨这种优先级并没有太大意义,我们应该尽量避免。

View渲染

最后我们看看org.springframework.web.servlet.View这个接口的定义:

public interface View {
	default String getContentType();
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response);

render方法的参数包括:原始request请求,model属性集合,以及用于写入渲染结果的HttpServletResponse。

示例代码里面,包含一个简单的渲染View实现,将Pet对象渲染成一段Html:

public class PetView implements View {
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        PrintWriter writer = response.getWriter();
        List<Pet> petList = new ArrayList<>();
        model.forEach((k, v) -> {
            if (v instanceof Pet) {
                petList.add((Pet) v);
            }
        });
        writer.write("<html><head>PetView</head><body><table border=\"1\"><tr><th>Name</th><th>Weight</th></tr>");
        petList.forEach(v -> {
            writer.write(String.format(" <tr>\n" +
                    "    <td>%s</td>\n" +
                    "    <td>%d</td>\n" +
                    "  </tr>\n", v.getName(), v.getWeight()));
        });
        writer.write("</table></body></html");
    }
    @Override
    public String getContentType() {
        return "text/html";
    }
}

HttpSession

HttpSession代表单个客户端连续的http请求组成的会话,HttpSession是会话的抽象,可以存储一些跨多个请求的数据。Spring MVC只有在必要时才创建HttpSession,“必要时”是指HttpSesison被使用时,比如处理器方法声明了HttpSession参数。

操作HttpSession

最直接的向HttpSession注入数据的方式,是在Controller方法声明HttpSession参数:

@RequestMapping("setSession")
public String setSession(HttpSession session, Model model) {
	//session.setAttribute
	//session.getAttribute
}

@SessionAttribute

@SessionAttribute标注控制器方法参数,提取HttpSession上的现有属性:

@RequestMapping("getSession")
public String getSession(@SessionAttribute("pet") Pet pet) {
	//
}

@SessionAttributes

前面介绍过@SessionAttributes可以筛选HttpSession的属性,填充ModelAndViewContainer。它还有另外一个作用:一旦某个方法产生了匹配的model属性,会自动成为HttpSession属性。

下面这个例子,setPet返回了Pet类型的model属性,刚好与SessionAttributes指定的类型匹配,因此存入HttpSession;再访问getPet,@ModelAttribute注释的参数能绑定该属性。

@Controller
@SessionAttributes(types = {Pet.class})
public class ModelController {

    @RequestMapping("setPet")
    public Pet setPet(HttpSession session) {
        return new Pet("BlueBird", 9);
    }
    @RequestMapping("getPet")
    public Pet getPet(@ModelAttribute Pet pet) {
        return pet;
    }
}

重定向机制

Redirect

实现重定向的方法大体有两种,第一种是返回一个特定格式的View Name:rediret:**,第二种是返回RediectView。

//不能添加@ResponseBody
@RequestMapping("from")
public String redirect() {
    return "redirect:/redirect/to";
}
//或者
@RequestMapping("from")
public View redirect() {
	new RedirectView("/redirect/to")
}

重定向路径如果是以/开头,代表相对ServletContext的路径,否则代表相对当前Controller的路径。

重定向路径变量

路径变量对于重定向直接可用:

@PostMapping("/files/{path}")
public String upload(...) {
    // ...
    return "redirect:dir/{path}";
}

重定向参数

要么手动拼接参到目标url上,要么通过RedirectAttribute参数来做:

@RequestMapping("from")
public View  redirect(RedirectAttributes redirectAttributes, Model model) {
    redirectAttributes.addAttribute("name", "Cat");
    redirectAttributes.addAttribute("weight", "10");
    return new RedirectView("to");
}

@RequestMapping("to")
public Pet redirected(@RequestParam("name") String name, @ModelAttribute("weight") int weight) {
    return new Pet(name,weight);
}

但是这种方式的重定向会导致这些参数出现在客户端浏览器的url的Query参数部分。如果不期望暴露参数给客户端,可以使用Flash Attributes。

Flash Attributes

顾名思义,Flash表示闪存,它在HttpRequest相关的一块内存放一些属性,给其他request来用。存放flash属性的抽象结构叫做FlashMap,对每个HttRequest,有一个输入FlashMap存放获得的flash属性,有一个输出FlashMap存放产生的flash属性,Spring MVC只有在必要时才会创建对应的FalshMap。全局的FlashMapManager管理着这些数据对象。

不过用户代码不需要直接使用FlashMap,使用RedirectAttributes可以写入输出Flash属性;输入Flash属性则会自动进入Model,用@ModelAttribute即可提取。

@RequestMapping("from")
public View  redirect(RedirectAttributes redirectAttributes, Model model) {
    redirectAttributes.addFlashAttribute("name", "Cat");
    redirectAttributes.addFlashAttribute("weight", "10");
    return new RedirectView("to");
}

@RequestMapping("to")
public Pet redirected(@RequestParam("name") String name, @ModelAttribute("weight") int weight) {
    return new Pet(name,weight);
}

值得注意的是,Flash属性的使用存在并发风险,因为它暂存在内存中,有可能被其他请求拿走。RedirectView会基于redirect的目标url和query参数给输出FlashMap生成一个指纹,随后匹配的request才能拿到该FlashMap。

WebDataBinder

在Spring MVC提取http request的参数并绑定到Controller方法时,会使用一个叫做WebDataBinder的工具类。
WebDataBinder做以下几件事:

  • 绑定request参数到controller方法参数;
  • 完成上一步所需的参数转换;
  • 将model对象序列化为字符串,用来渲染html。

WebDataBinder在执行类型转换时,仍然使用了Spring的类型转换机制,不过我们可以对某Controller进行局部定制:

@RestController
public class BinderController {
    @InitBinder
    public void init(WebDataBinder dataBinder) {
        dataBinder.addCustomFormatter(new PetFormatter());
    }
}

上面代码工作的内部机理是,Spring在执行每个Controller方法前,会创建一个WebDataBinder,并调用@InitBinder方法来初始化。

Exception

如果一个Controller的处理器方法在执行过程中抛出异常,默认情况下,Spring MVC会把这个Exception作为一个model属性,并转发到/error这个URI(在前面介绍HandlerMapping时候,我们已经知道Spring MVC内部有这个url映射)。

最终的结果展示一个返回这样一个Html页面给客户端:

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Nov 20 11:31:55 CST 2019
There was an unexpected error (type=Internal Server Error, status=500).
No message available

如果我们想要自定义异常处理逻辑,就要用到HandlerExceptionResolver。

HandlerExceptionResolver

HandlerExceptionResolver的接口定义如下,它依据异常及其上下文信息,返回一个ModelAndView,渲染给客户端。

public interface HandlerExceptionResolver {
	@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}

Spring MVC内置了以下HandlerExceptionResolver

  • SimpleMappingExceptionResolver: 将异常类型向View Name映射;
  • DefaultHandlerExceptionResolver: 给特定异常类型赋予一个特定的HttpStatus状态码,一般异常返回500;
  • ResponseStatusExceptionResolver:支持将@ResponseStatus添加到异常类型上,来指定HttpStatus状态码;
  • ExceptionHandlerExceptionResolver:支持Controller上@ExceptionHandler注解的异常处理器方法。

配置HandlerExceptionResolver

还是通过WebMvcConfigurer.

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
	     //
    }
    
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty(ExceptionResolved.class.getName(), "ExceptionView");
        resolver.setExceptionMappings(properties);
        resolvers.add(resolver);
    }
}

有两个接口可以用来做这件事,实现configureHandlerExceptionResolvers接口,意味着我们要完全控制系统内有哪些HandlerExceptionResolver;extendHandlerExceptionResolvers在系统默认的基础上添加自定义的HandlerExceptionResolver。推荐使用后者,否则很多默认功能会失效,比如@ExceptionHandler注解。

@ExceptionHandler

这是实际项目中最常用的Controller异常处理手段,它标注一个Controller方法:

@Controller 
public class ControllerClass {
	@ExceptionHandler(ExceptionUnResolved.class)
	public HttpEntity<String> exceptionHandler(Exception e) {
		return new HttpEntity<>("exceptionHandler "+e.getClass().getSimpleName());
	}
}

完全可以将@ExceptionHandler类比为@RequestMapping,只不过前者映射Exception,后者映射URI。@ExceptionHandler注解参数表明该handler匹配的异常,不仅匹配Exception本身,还可以匹配Exception.getCause()。

@ExceptionHandler方法支持的参数除了Exception之外,和@RequestMapping方法差别也不大,返回值的语义也类似。

@ControllerAdvice

@ControllerAdvice可类比为Controller的切面,它将一组Controller的公共功能聚合起来,一般用来支持@ExceptionHandler, @InitBinder, 和 @ModelAttribute这三项配置。

@ControllerAdvice参数可以通过包名、类名、注解来过滤生效的Controller。

@ControllerAdvice(assignableTypes = NormalController.class)
public class AdviceController {

    @ExceptionHandler
    @ResponseBody
    public String exceptionHandler(Exception e) {
        return "AdviceController.exceptionHandler cache:" + e.getClass().getSimpleName();
    }
}

上面通过@ControllerAdvice来集中处理Controller异常,并声明仅对NormalController及其子类生效。

总结

本章的内容非常多,涵盖了Spring MVC Controller工作的各个环节,从URI匹配,方法参数解析、Response的产生,最后到异常处理。我们无法也没有必要记住这些环节涉及的所有技术细节,了解它的工作原理即可;这样在发生问题时,能够有针对性地去诊断,而不是无脑百度。

关于Http请求的Response部分,涉及ViewResolver和HttpMessageConvter,这两个技术点比较复杂,后续开辟专门章节来讨论它。