使用Spring MVC构建Web应用程序
3.1 Java Web应用程序架构
3.1.2 Model 2或MVC架构
典型的Model 2架构(即MVC):

Model 2架构明确划分了模型、视图与控制器之间的职责:
- 模型:表示要用于生成视图的数据。
 - 视图:使用模型来渲染屏幕。
 - 控制器:控制流并获取浏览器提出的请求,填充模型以及重定向到视图,示例包括上图中的Servlet1和Servlet2。
 
3.1.3 Model 2前端控制器架构
在基本版本的Model 2架构中,浏览器提出的请求直接由不同的servlet(或控制器)处理。
在新的Model 2前端控制器(Front Controller)架构中,所有请求注入一个控制器中,它称为前端控制器。(在前端控制器中可以统一处理一些授权认证等):

通常,前端控制器负责的一些工作如下:
- 决定由哪个控制器执行请求。
 - 决定渲染哪个视图。
 - 提供资源来添加更多通用功能。
 - Spring MVC使用配有前端控制器的MVC模式。前端控制器称为DispatcherServlet
 
3.2 基本流
创建Spring MVC控制器
 @Controller
 public class BasicController {
   @RequestMapping(value = "/welcome")
   @ResponseBody
   public String welcome() {
     return "Welcome to Spring MVC";
   }
 }
@ResponseBody 在这个特定的上下文中,welcome方法返回的文本会作为响应内容发送给浏览器。
@Controller
public class BasicViewController {
  @RequestMapping(value = "/welcome-view")
  public String welcome() {
    return "welcome";
   }
}
此方法上没有使用@RequestBody注解。因此,Spring MVC会尝试将返回的字符串welcome与视图进行匹配。
3.2.7 流6——在上一个流中添加验证功能
Spring MVC全面集成了Bean Validation API。JSR 303和JSR 349为Bean Validation API(分别为版本1.0和1.1)定义了规范,Hibernate Validator充当引用实现。
下面是在Controller中增加Bean校验的示例代码:
Step 1. 添加依赖
首先将Hibernate Validator添加到项目pom.xml中:
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>5.0.2.Final</version>
</dependency>
Step 2. 使用
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Controller
@RequestMapping("/bean-valid")
public class BeanValidController {
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    @ResponseBody
    public String exec(@Valid @RequestBody User user, BindingResult result) {
        if (result.hasErrors()) {
            String errors = result.getFieldErrors()
                    .stream()
                    .map(error -> String.format("%s: %s", error.getField(), error.getDefaultMessage()))
                    .collect(Collectors.joining("\n"));
            return errors;
        }
        return "验证通过";
    }
    @Getter
    @Setter
    public static class User {
        @Size(min = 6, max = 10, message = "长度必须位于6-10")
        private String userName;
        @NotNull(message = "密码不能为空")
        private String password;
        @NotNull(message = "两次密码不匹配")
        private String password2;
        @Min(value = 18, message = "最小18岁")
        private int age;
        @AssertTrue(message = "密码不匹配")
        private boolean isValid() {
            return Objects.equals(this.password, this.password2);
        }
    }
}
Step 3. 测试
curl --request POST 'http://localhost:8080/bean-valid/test' \
--header 'Content-Type: application/json' \
--data-raw '{
    "userName": "AlanWei",
    "password": "123456",
    "password2": "123456",
    "age": 18
}'
使用Bean Validation可以执行的其他验证功能如下。
@NotNull:不得为null。@Size(min =5, max = 50):最多50个字符,最少5个字符。@Past:应为过去日期。@Future:应为将来日期。@Pattern:应与提供的正则表达式相匹配。@Max:字段的最大值。@Min:字段的最小值。
使用@AssertTrue注解可以实现更加复杂的自定义验证.
Hibernate Validator 也可以在方法里手动执行校验, 参考Hibernate Validator 7.0.1.Final - Jakarta Bean Validation Reference Implementation: Reference Guide:
import org.junit.BeforeClass;
import org.junit.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import static org.junit.Assert.assertEquals;
public class HibernateValidatorTest {
    private static Validator validator;
    @BeforeClass
    public static void setUpValidator() {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();
    }
    @Test
    public void validate() {
        BeanValidController.User user = new BeanValidController.User();
        user.setUserName("AlanWei");
        user.setPassword("123456");
        user.setPassword2("123456");
        user.setAge(10);
        Set<ConstraintViolation<BeanValidController.User>> violations = validator.validate(user);
        assertEquals(1, violations.size()); // => 年龄校验失败
    }
}
3.3 Spring MVC概述
3.3.2 工作机制
Spring MVC框架中的关键组件如下图所示:

以下面返回视图的代码为例,URL为http://localhost:8080/welcome-model-view:
@Controller
public class BasicModelViewController {
  @RequestMapping(value = "/welcome-model-view")
   public ModelAndView welcome(ModelMap model) {
     model.put("name", "XYZ");
     return new ModelAndView("welcome-model-view", model);
   }
}
- 浏览器向特定URL提出请求。
DispatcherServlet是处理所有请求的前端控制器,因此,它收到该请求。 - DispatcherServlet将分析URI(此例中为*/welcome-model-view*),并需要确定处理该URI所需的相应控制器。为帮助找到正确的控制器,它与处理程序(即Controller)映射进行交互。
 - 处理程序映射返回特定处理程序方法(此例中为
BasicModelViewController中的welcome方法)来处理请求。 - DispatcherServlet调用特定处理程序方法(
public ModelAndView welcome(ModelMap model))。 - 处理程序方法返回模型和视图。本例中将返回
ModelAndView对象。 - DispatcherServlet有逻辑视图名称(来自
ModelAndView。本例中为welcome-model-view)。它因为需要了解如何确定物理视图名称,所以检查是否有任何可用的视图解析器,结果找到配置的视图解析器(org.springframework.web.servlet.view.InternalResourceViewResolver),并调用此视图解析器,提供逻辑视图名称(本例中为welcome-model-view)作为输入。 - 视图解析器执行逻辑,将逻辑视图名称映射为物理视图名称。本例中
welcome-model-view被转换为/WEB-INF/views/welcome-model-view.jsp。 - DispatcherServlet执行视图,还使模型对视图可用。
 - 视图返回要送回给DispatcherServlet的内容。
 - DispatcherServlet将响应返回给浏览器。
 
3.4 Spring MVC背后的重要概念
3.4.1 RequestMapping
RequestMapping 用于将URI映射为控制器或控制器方法, 可以在类或方法级别执行此操作:
@Controller
@RequestMapping("/user")
public class UserController {
  @RequestMapping(value = "/show-page" , method =
   RequestMethod.GET)
   public String showPage() {
     /* 一些代码 */
   }
}
使用了RequestMapping注解的方法所支持的入参类型:
| 参数类型/注解 | 用途 | 
|---|---|
| java.util.Map / org.springframework. ui.Model / org.springframework.ui.ModelMap | 充当模型(MVC),将作为传送给视图的值的容器 | 
命令或表单对象(自定义类, 比如上述使用的 User 类) | 用于将请求参数绑定到bean并支持验证 | 
| org.springframework.validation.Errors / org.springframework.validation.BindingResult | 命令或表单对象的验证结果(表单对象应为前一个方法参数) | 
| @PreDestroy | 可以对任何Spring bean使用@PreDestroy注解,来提供一个在销毁前调用的方法, 只会在从容器中删除bean时调用此方法。此方法可用于释放任何由bean保留的资源 | 
| @RequestParam | 此注解用于访问特定HTTP请求参数 | 
| @RequestHeader | 此注解用于访问特定HTTP请求标头 | 
| @SessionAttribute | 此注解用于访问HTTP会话中的属性 | 
| @RequestAttribute | 此注解用于访问特定HTTP请求属性 | 
| @PathVariable | 此注解用于访问URI模板/owner/{ownerId}中的变量,我们将在讨论微服务时详细介绍此注解 | 
使用了RequestMapping注解的方法所支持的返回类型:
| 返回类型 | 结果 | 
|---|---|
| ModelAndView | 对象包括对模型和视图名称的引用 | 
| Model | 仅返回模型, 使用DefaultRequestToViewNameTranslator确定视图名称 | 
| Map | 用于公开模型的简单映射 | 
| View | 具有隐式定义模型的视图 | 
| String | 引用视图名称 | 
使用RequestMapping注解的方法支持多种返回类型,不管是那种返回类型都要解决两个问题:
- 什么是视图
 - 视图需要哪个模型
 
如果视图未显式定义为返回类型的一部分,则进行隐式定义:
- 隐式充实模型: 如果模型是返回类型的一部分,那么它由命令对象(包括命令对象的验证结果)来充实。此外,还会在模型中添加使用
@ModelAttribute注解的方法的调用结果。 - 隐式确定视图: 如果返回类型中不包括视图名称,将使用
DefaultRequestToViewNameTranslator确定该名称。默认情况下,DefaultRequestToViewNameTranslator会删除URI中的前导和尾部反斜杠以及文件扩展名;例如,display.html将变成display。 
3.4.2 视图解析
Spring MVC提供了多种视图选项:
- 集成JSP、Freemarker。
 - 多种视图解析策略,其中一些策略如下:
XmlViewResolver:视图解析基于外部XML配置。ResourceBundleViewResolver:视图解析基于属性文件。UrlBasedViewResolver:将逻辑视图名称直接映射为URL。ContentNegotiatingViewResolver:根据Accept请求标头委任给其他视图解析器。
 - 支持用明确定义的优先次序链接视图解析器。
 - 使用内容协商(Content Negotiation)直接生成XML、JSON和Atom。
 
3.4.3 处理程序(Controller)映射和拦截器
参考Spring MVC HandlerInterceptor 简介
Spring MVC 高级功能
3.5.1 异常处理
Controller Advice可用于实现所有控制器通用的异常处理:
@ControllerAdvice
public class ExceptionController {
  private Log logger =
  LogFactory.getLog(ExceptionController.class);
  @ExceptionHandler(value = Exception.class)
  public ModelAndView handleException
  (HttpServletRequest request, Exception ex) {
     logger.error("Request " + request.getRequestURL()
     + " Threw an Exception", ex);
     ModelAndView mav = new ModelAndView();
     mav.addObject("exception", ex);
     mav.addObject("url", request.getRequestURL());
     mav.setViewName("common/spring-mvc-error");
     return mav;
  }
}
上述代码返回的是个视图, 或者直接返回原始文本内容:
@ControllerAdvice
public class ExceptionControllerAdvice {
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public String handleException(HttpServletRequest request, Exception ex) {
        return request.getRequestURL() + " => " + ex.getMessage() + "\n"; // 这个字符串会直接输出到响应内容
    }
}
通过实现使用@ExceptionHandler(value = Exception.class)注解可以指定处理特定异常.