第五章 说说Web服务
基于浏览器的B/S结构应用十分流行。Spring Boot非常适合Web应用开发。可以使用嵌入式Tomcat、Jetty、Undertow或Netty创建一个自包含的HTTP服务器。一个Spring Boot的Web应用能够自己独立运行,不依赖需要安装的Tomcat,Jetty等。
Spring Boot可以创建两种类型的Web应用
- 基于Servlet体系的Spring Web MVC应用
 - 使用spring-boot-starter-webflux模块来构建响应式,非阻塞的Web应用程序
 
Spring WebFlux是单独一个体系的内容,其他课程来说。 当前文档讲解 Spring Web MVC。又被称为“Spring MVC”。Spring MVC是“model view controller”的框架。专注web应用开发。我们快速的创建控制器(Controller),接受来自浏览器或其他客户端的请求。并将业务代码的处理结果返回给请求方。
Spring MVC处理请求:

5.1 高效构建Web应用
创建Web应用,Lession12-quick-web。 依赖选择spring-web 包含了Spring MVC , Restful, Tomcat这些功能。再选择Thymeleaf(视图技术,代替jsp),Lombok依赖。包名 com.bjpowernode.quickweb。 项目结构:

5.1.1 html页面视图
step1: Maven依赖
spring-web starter
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 视图技术 Thymeleaf模板引擎 -->
<dependency>
  
<groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>step2: 创建Controller
在根包的下面,创建子包controller,并创建QuickController
@Controller
public class QuickController {
  @RequestMapping("/exam/quick")
  public String quick(Model model){
    //业务处理结果数据,放入到Model模型
    model.addAttribute("title", "Web开发");
    model.addAttribute("time", LocalDateTime.now());
    return "quick";
  }
}step3: 创建视图
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>视图</title>
</head><body>
  <!--格式化,排列数据,视图在浏览器显示给用户-->
  <h3>显示请求处理结果</h3>
  <p th:text="${title}"></p>
  <p th:text="${time}"></p>
</body>
</html>step4:代码编写完成,现在运行启动类,在浏览器访问exam/quick url地址

编写Spring MVC的应用分成三步:
- 编写请求页面(在浏览器直接模拟的请求)
 - 编写Controller
 - 编写视图页面
 
5.1.2 JSON视图
上面的例子以Html文件作为视图,可以编写复杂的交互的页面,CSS美化数据。除了带有页面的数据,还有一种只需要数据的视图。比如手机应用app,app的数据来自服务器应用处理结果。app内的数据显示和服务器无关,只需要数据就可以了。主流方式是服务器返回json格式数据给手机app应用。我们可以通过原始的HttpServletResponse应该数据给请求方。 借助Spring MVC能够无感知的处理json。
step1:创建Controller
@Data
public class User {
  private String name;
  private Integer age;
}
@Controller
public class JSONViewController {
  //HttpServletResponse
  @RequestMapping("/exam/json")
  public void exam1(HttpServletResponse response) throws IOException {
    String data="{\"name\":\"lisi\",\"age\":20}";
    response.getWriter().println(data);
  }
  //@ResponseBody
  @RequestMapping("/exam/json2")
  @ResponseBody public User exam2()  {
    User user = new User();
    user.setName("张三");
    user.setAge(22);
    return  user;
  }
}注意:从Spring6. Spring Boot3开始 javax包名称,修改为jakarta。
原来:
javax.servlet.http.HttpServletRequest;
修改后:
jakarta.servlet.http.HttpServletRequest;
step2:浏览器测试两个地址


构建前-后端分离项目经常采用这种方式。

5.1.3 给项目加favicon
什么是favicon.ico :
favicon.ico是网站的缩略标志,可以显示在浏览器标签、地址栏左边和收藏夹,是展示网站个性的logo标志。

我们自己的网站定制logo。首先找一个在线工具创建favicon.ico。比如https://quanxin.org/favicon , 用文字,图片生成我们需要的内容。生成的logo文件名称是favicon.ico
step1:将生成的favicon.ico拷贝项目的resources/ 或 resources/static/ 目录。
step2:在你的视图文件,加入对favicon.ico的引用。
在视图的<head>部分加入
<link rel="icon" href="../favicon.ico" type="image/x-icon"/>代码如下
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>视图</title>
  <link rel="icon" href="../favicon.ico" type="image/x-icon"/>
</head>
<body>测试:浏览器访问项目地址http://localhost:8080/favicon.ico
注意:
- 关闭缓存,浏览器清理缓存
 - 如果项目有过滤器,拦截器需要放行对favicon.ico的访问
 
5.2 Spring MVC
Spring MVC是非常著名的Web应用框架,现在的大多数Web项目都采用Spring MVC。它与Spring有着紧密的关系。是Spring 框架中的模块,专注Web应用,能够使用Spring提供的强大功能,IoC , Aop等等。
Spring MVC框架是底层是基于Servlet技术。遵循Servlet规范,Web组件Servlet,Filter,Listener在SpringMVC中都能使用。同时 Spring MVC也是基于MVC架构模式的,职责分离,每个组件只负责自己的功能,组件解耦。 学习Spring MVC首先具备Servlet的知识,关注MVC架构的M、V、C在 Spring MVC框架中的实现。掌握了这些就能熟练的开发Web应用。
Spring Boot的自动配置、按约定编程极大简化,提高了Web应用的开发效率
5.2.1 控制器Controller
控制器一种有Spring管理的Bean对象,赋予角色是“控制器”。作用是处理请求,接收浏览器发送过来的参数,将数据和视图应答给浏览器或者客户端app等。
控制器是一个普通的Bean,使用@Controller或者@RestController注释。 @Controller被声明为@Component。所以他就是一个Bean对象。源代码如下:

如何创建控制器对象?
创建普通Java类,其中定义public方法。类上注解@Controller或者@RestController。
控制器类中的方法作用:
Controller类中的方法处理对应uri的请求, 一个(或多个)uri请求交给一个方法处理。就是一个普通的方法。方法有参数,返回值。方法上面加入@RequestMapping,说明这个uri由这个方法处理。

5.2.1.1 匹配请求路径到控制器方法
SpringMVC支持多种策略,匹配请求路径到控制器方法。AntPathMatcher 、 PathPatternParser

从SpringBoot3推荐使用 PathPatternParser策略。比之前AntPathMatcher提示6-8倍吞吐量。
我们看一下PathPatternParser中有关uri的定义
通配符:
- ? : 一个字符
 - * : 0或多个字符。在一个路径段中匹配字符
 - **:匹配0个或多个路径段,相当于是所有
 - 正则表达式: 支持正则表达式
 
RESTFul的支持路径变量
{变量名}
{myname:[a-z]+}: 正则皮a-z的多个字面,路径变量名称“myname”。@PathVariable(“myname”)
{*myname}: 匹配多个路径一直到uri的结尾
例子:
@GetMapping("/file/t?st.html")
?匹配只有一个字符
( GET http://localhost:8080/file/test.html
( GET http://localhost:8080/file/teest.html
@GetMapping("/file/t?st.html")
public String path1(HttpServletRequest request){
  return "path请求="+request.getRequestURI();
}@GetMapping ("/images/*.gif")
*:匹配一个路径段中的0或多个字符
( GET http://localhost:8080/images/user.gif
( GET http://localhost:8080/images/cat.gif
( GET http://localhost:8080/images/.gif
( GET http://localhost:8080/images/gif/header.gif
( GET http://localhost:8080/images/dog.jpg
@GetMapping ("/images/*.gif")
public String path2(HttpServletRequest request){
  return "* 请求="+request.getRequestURI();
}@GetMapping ("/pic/**")
** 匹配0或多段路径, 匹配/pic开始的所有请求
( GET http://localhost:8080/pic/p1.gif
( GET http://localhost:8080/pic/20222/p1.gif
( GET http://localhost:8080/pic/user
( GET http://localhost:8080/pic/
@GetMapping ("/pic/**")
public String path3(HttpServletRequest request){
  return "/pic/**请求="+request.getRequestURI();
}
RESTFul
@GetMapping("/order/{*id}")
{*id} 匹配 /order开始的所有请求, id表示order后面直到路径末尾的所有内容。
      id自定义路径变量名称。与@PathVariable一样使用
( GET http://localhost:8080/order/1001
( GET http://localhost:8080/order/1001/2023-02-16
@GetMapping("/order/{*id}")
public String path4(@PathVariable("id") String orderId, HttpServletRequest request){
  return "/order/{*id}请求="+request.getRequestURI() + ",id="+orderId;
}
注意:@GetMapping("/order/{*id}/{*date}")无效的, {*..}后面不能在有匹配规则了
@GetMapping("/pages/{fname:\\w+}.log")
:\\w+ 正则匹配, xxx.log
( GET http://localhost:8080/pages/req.log
( GET http://localhost:8080/pages/111.log
( GET http://localhost:8080/pages/req.txt
( GET http://localhost:8080/pages/###.log
@GetMapping("/pages/{fname:\\w+}.log")
public String path5(@PathVariable("fname") String filename, HttpServletRequest request){
  return "/pages/{fname:\\w}.log请求="+request.getRequestURI() + ",filename="+filename;
}5.2.1.2 @RequestMapping
@RequestMapping:用于将web请求映射到控制器类的方法。此方法处理请求。可用在类上或方法上。 在类和方法同时组合使用。
重要的属性
- value:别名path 表示请求的uri, 在类和方法方法同时使用value,方法上的继承类上的value值。
 - method:请求方式,支持GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE。
 
值为:RequestMethod[] method() , RequestMethod是enum类型。
快捷注解
- @GetMapping: 表示get请求方式的@RequestMapping
 - @PostMapping:表示post请求方式的@RequestMapping
 - @PutMapping:表示put请求方式的@RequestMapping
 - @DeleteMapping: 表示delete请求方式的@RequestMapping
 
对于请求方式get,post,put,delete等能够HttpMethod表示,Spring Boot3之前他是enum,Spring Boot3他是class
public enum HttpMethod Spring Boot3之前他是enum
public final class HttpMethod Spring Boot3他是class
5.2.1.3 控制器方法参数类型与可用返回值类型
参数类型
类型  | 作用  | 
WebRequest, NativeWebRequest  | 访问请求参数,作用同ServletRequest,  | 
jakarta.servlet.ServletRequest, jakarta.servlet.ServletResponse  | Servlet API中的请求,应答  | 
jakarta.servlet.http.HttpSession  | Servlet API的会话  | 
jakarta.servlet.http.PushBuilder  | Servlet 4.0 规范中推送对象  | 
HttpMethod  | 请求方式  | 
java.io.InputStream, java.io.Reader  | IO中输入,读取request body  | 
java.io.OutputStream, java.io.Writer  | IO中输入,访问response body  | 
@PathVariable,@MatrixVariable,@RequestParam,@RequestHeader,@CookieValue,@RequestBody,@RequestPart,@ModelAttribute  | uri模板中的变量,访问矩阵,访问单个请求参数,访问请求header,访问cookie,读取请求 body, 文件上传, 访问model中属性  | 
Errors, BindingResult  | 错误和绑定结果  | 
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap  | 存储数据Map,Model,ModelMap  | 
其他参数  | String name, Integer , 自定义对象  | 
完整https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments  | |
返回值:
返回值类型  | 作用  | 
@ResponseBody  | 将response body属性序列化输出  | 
HttpEntity<B>, ResponseEntity<B>  | 包含http状态码和数据的实体  | 
String  | 实体名称或字符串数据  | 
自定义对象  | 如果有json库,序列化为json字符串  | 
ErrorResponse  | 错误对象  | 
ProblemDetail  | RFC7807,规范的错误应答对象  | 
void  | 无返回值  | 
ModelAndView  | 数据和视图  | 
java.util.Map, org.springframework.ui.Model  | 作为模型的数据  | 
...https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types  | |
5.2.1.4 接收请求参数
用户在浏览器中点击按钮时,会发送一个请求给服务器,其中包含让服务器程序需要做什么的参数。 这些参数发送给控制器方法。控制器方法的形参列表接受请求参数。
接受参数方式:
- 请求参数与形参一一对应,适用简单类型。形参可以有合适的数据类型,比如String,Integer ,int等。
 - 对象类型,控制器方法形参是对象,请求的多个参数名与属性名相对应。
 - @RequestParam注解,将查询参数,form表单数据解析到方法参数,解析multipart文件上传。
 - @RequestBody,接受前端传递的json格式参数。
 - HttpServletRequest 使用request对象接受参数, request.getParameter(“...”)
 - @RequestHeader ,从请求header中获取某项值
 
解析参数需要的值,SpringMVC 中专门有个接口来干这个事情,这个接口就是:HandlerMethodArgumentResolver,中文称呼:处理器方法参数解析器,说白了就是解析请求得到 Controller 方法的参数的值。
5.2.1.4.1 接收json
step1:创建控制器类
@Data
public class User {
  private String name;
  private Integer age;
}
@RestController
public class ParamController {
  @PostMapping("/param/json")
  public String getJsonData(@RequestBody User user){
    System.out.println("接收的json:"+user.toString());
    return "json转为User对象"+user.toString();
  }
}
IDEA Http Client测试:
POST http://localhost:8080/param/json
Content-Type: application/json
{"name":"lisi","age":22}
接收 json array
step1:创建控制器方法
@PostMapping("/param/jsonarray")
public String getJsonDataArray(@RequestBody List<User> users){
  System.out.println("接收的json array:"+users.toString());
  return "json转为List<User>对象"+users.toString();
}
测试:
POST http://localhost:8080/param/jsonarray
Content-Type: application/json
[
 {"name":"lisi","age":22},
 {"name":"zhangesan","age":26},
 {"name":"zhouli","age":30}
]
5.2.1.4.2 Reader-InputStream
Reader 或 InputStream 读取请求体的数据, 适合post请求体的各种数据。具有广泛性。
step1:创建新的控制器方法
@PostMapping("/param/json2")
public String getJsonData2(Reader in)  {
  StringBuffer content = new StringBuffer("");
  try(BufferedReader bin = new BufferedReader(in)){
      String line = null;
      while( (line=bin.readLine()) != null){
        content.append(line);
      }
  } catch (IOException e) {
     throw new RuntimeException(e);
  }
  return "读取请求体数据"+ content.toString();
}
IDEA Http Client测试:
POST http://localhost:8080/param/json2
Content-Type: application/json 可无
{"name":"lisi","age":26}
5.2.1.4.3 数组参数接收多个值
数组参数接收多个值 数组作为形参,接受多个参数值 ,请求格式 参数名=值1&参数名=值2...
@GetMapping("/param/vals")
public String getMultiVal(Integer [] id){
  List<Integer> idList = Arrays.stream(id).toList();
  return idList.toString();
}
测试请求:
GET http://localhost:8080/param/vals?id=11&id=12&id=13
GET http://localhost:8080/param/vals?id=11,12,13,14,15
都是成功的方式
5.2.1.5 验证参数
服务器端程序,Controller在方法接受了参数,这些参数是由用户提供的,使用之前必须校验参数是我们需要的吗,值是否在允许的范围内,是否符合业务的要求。比如年龄不能是负数,姓名不能是空字符串,email必须有@符号,phone国内的11位才可以。
验证参数
- 编写代码,手工验证,主要是if语句,switch等等。
 - Java Bean Validation : JSR-303是JAVA EE 6 中的一项子规范,叫做 Bean Validation, 是一个运行时的数据验证规范,为 JavaBean 验证定义了相应的元数据模型和API。
 
5.2.1.5.1 Java Bean Validation
Spring Boot使用Java Bean Validation验证域模型属性值是否符合预期,如果验证失败,立即返回错误信息。 Java Bean Validation将验证规则从controller,service集中到Bean对象。一个地方控制所有的验证。
Bean的属性上,加入JSR-303的注解,实现验证规则的定义。JSR-3-3是规范,hibernate-validator是实现。
JSR-303: https://beanvalidation.org/
hibernate-validator:https://hibernate.org/validator/ https://docs.jboss.org/hibernate/validator/4.2/reference/en-US/html/
5.2.1.5.2 JSR-303注解
JSR-303定义的常用注解:
注解  | 作用  | 
@Null  | 被注释的元素必须为 null  | 
@Null  | 被注释的元素必须不为 null  | 
@AssertTrue  | 被注释的元素必须为 true  | 
@AssertFalse  | 被注释的元素必须为 false  | 
@Min(value)  | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值  | 
@Max(value)  | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值  | 
@DecimalMin(value)  | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值  | 
@DecimalMax(value)  | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值  | 
@Size(max, min)  | 被注释的元素的大小必须在指定的范围内  | 
@Digits (integer, fraction)  | 被注释的元素必须是一个数字,其值必须在可接受的范围内  | 
@Past  | 被注释的元素必须是一个过去的日期  | 
@Future  | 被注释的元素必须是一个将来的日期  | 
@Pattern(value)  | 被注释的元素必须符合指定的正则表达式  | 
被注释的元素必须是电子邮箱地址  | |
@NotEmpty  | 被注释的字符串的必须非空  | 
Hibernate提供的部分注解
注解  | 作用  | 
@URL  | 被注释的字符为URL  | 
@Length  | 被注释的字符串的大小必须在指定的范围内  | 
@Range  | 被注释的元素必须在合适的范围内  | 
... 还有其他注解  | |
5.2.1.5.3 快速上手
验证Blog中的文章信息。用户提交文章给Controller, 控制器使用Java Object接收参数,给Bean添加约束注解,验证文章数据。
step1:添加Bean Validator Starter
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>step2:创建文章数据类,添加约束注解
@Data
public class ArticleVO {
//文章主键
private Integer id;
  @NotNull(message = "必须有作者")
  private Integer userId;
  //同一个属性可以指定多个注解
  @NotBlank(message = "文章必须有标题")
  //@Size中null 认为是有效值.所以需要@NotBlank
  @Size(min = 3,max = 30,message = "标题必须3个字以上")
  private String title;
  @NotBlank(message = "文章必须副标题")
  @Size(min = 8,max = 60,message = "副标题必须30个字以上")
  private String summary;
  @DecimalMin(value = "0",message = "已读最小是0")
  private Integer readCount;
  @Email(message = "邮箱格式不正确")
  private String email;
}step3: Controller使用Bean
@RestController
public class ArticleController {
  @PostMapping("/article/add")
  public Map<String,Object> addArticle(@Validated @RequestBody ArticleVO articleVo,
      BindingResult br){
    Map<String,Object> map = new HashMap<>();
    if( br.hasErrors() ){
      br.getFieldErrors().forEach( field->{
        map.put(field.getField(), field.getDefaultMessage());
      });
    }
    return map;
  }
}@Validated: Spring中的注解,支持JSR 303规范,还能对group验证。可以类,方法,参数上使用 BindingResult 绑定对象,错误Validator的绑定。
step4:测试数据
POST http://localhost:8080/article/add
Content-Type: application/json
{
  "userId": 216,
  "title": "云原生",
  "summary": "云原生SpringCloud,Linux",
  "readCount": 1,
  "email": "abc@163.com"
}5.2.1.5.4 分组校验
上面的AriticleVO用来新增文章, 新的文章主键id是系统生成的。现在要修改文章,比如修改某个文章的title,summary, readCount,email等。此时id必须有值,修改这个id的文章。 新增和修改操作对id有不同的要求约束要求。通过group来区分是否验证。
group是Class作为表示, java中包加类一定是唯一的, 这个标识没有其他实际意义
step1:添加group标志
@Data
public class ArticleVO {
  //新增组
  public static interface AddArticleGroup { };
  //编辑修改组
  public static interface EditArticleGroup { };
  //文章主键
  @NotNull(message = "文章ID不能为空", groups = { EditArticleGroup.class } )
  @Min(value = 1, message = "文章ID从1开始",
       groups = { EditArticleGroup.class } )
  private Integer id;
  @NotNull(message = "必须有作者",
           groups = {AddArticleGroup.class, EditArticleGroup.class})
  private Integer userId;
  //同一个属性可以指定多个注解
  @NotBlank(message = "文章必须有标题",
            groups = {AddArticleGroup.class, EditArticleGroup.class})
  //@Size中null 认为是有效值.所以需要@NotBlank
  @Size(min = 3, max = 30, message = "标题必须3个字以上",
      groups = {AddArticleGroup.class,EditArticleGroup.class})
  private String title;
  @NotBlank(message = "文章必须副标题",
           groups = {AddArticleGroup.class, EditArticleGroup.class})
  @Size(min = 8, max = 60, message = "副标题必须30个字以上",
       groups = {AddArticleGroup.class,EditArticleGroup.class})
  private String summary;
  @DecimalMin(value = "0", message = "已读最小是0",
              groups = {AddArticleGroup.class,EditArticleGroup.class})
  private Integer readCount;
  //可为null,有值必须符合邮箱要求
  @Email(message = "邮箱格式不正确",
         groups = {AddArticleGroup.class, EditArticleGroup.class})
  private String email;
}step2:修改Controller,不同方法增加group标志
@RestController
public class ArticleController {
  //新增文章
  @PostMapping("/article/add")
  public Map<String,Object> addArticle(@Validated(ArticleVO.AddArticleGroup.class) @RequestBody ArticleVO articleVo,
      BindingResult br){
    Map<String,Object> map = new HashMap<>();
    if( br.hasErrors() ){
      br.getFieldErrors().forEach( field->{
        map.put(field.getField(), field.getDefaultMessage());
      });
    }
    return map;
  }
  //修改文章
  @PostMapping("/article/edit")
  public Map<String,Object> editArticle(@Validated(ArticleVO.EditArticleGroup.class) @RequestBody ArticleVO articleVo,
      BindingResult br){
    Map<String,Object> map = new HashMap<>();
    if( br.hasErrors() ){
      br.getFieldErrors().forEach( field->{
        map.put(field.getField(), field.getDefaultMessage());
      });
    }
    return map;
  }
}step3:测试代码
POST http://localhost:8080/article/add
Content-Type: application/json
{
  "userId": 216,
  "title": "云原生",
  "summary": "云原生SpringCloud,Linux",
  "readCount": 0,
  "email": "abc@163.com"
}
POST http://localhost:8080/article/edit
Content-Type: application/json
{
  "id": 2201,
  "userId": 216,
  "title": "云原生",
  "summary": "云原生SpringCloud,Linux",
  "readCount": 0,
  "email": "abc@163.com"
}
5.2.1.5.5 ValidationAutoConfiguration
spring-boot-starter-validation 引入了jakarta.validation:jakarta.validation-api:3.0.2 约束接口,org.hibernate.validator:hibernate-validator:8.0.0.Final 约束注解的功能实现
ValidationAutoConfiguration 自动配置类,创建了LocalValidatorFactoryBean对象, 当有class路径中有hibernate.validator。 能够创建hiberate的约束验证实现对象。
@ConditionalOnResource(resources = "classpath:META-INF/services/jakarta.validation.spi.ValidationProvider")

5.2.2 模型Model
在许多实际项目需求中,后台要从控制层直接返回前端所需的数据,这时Model大家族就派上用场了。
Model模型的意思,Spring MVC中的“M”,用来传输数据。从控制层直接返回数据给前端,配置jsp,模板技术能够展现M中存储的数据。
Model简单理解就是给前端浏览器的数据,放在Model中,ModelAndView里的任意值,还有json格式的字符串等都是Model。
@Controller
public class QuickController {
  @RequestMapping("/exam/quick")
  public String quick(Model model){  //Map ,ModelMap等,一般配合带有页面的视图,html,jsp等。
    //业务处理结果数据,放入到Model模型
    model.addAttribute("title", "Web开发");
    model.addAttribute("time", LocalDateTime.now());
    return "quick";
  }
}
5.2.3 视图View
Spring MVC中的View(视图)用于展示数据的,视图技术的使用是可插拔的。无论您决定使用thymleaf、jsp还是其他技术,classpath有jar就能使用视图了。开发者主要就是配置更改。Spring Boot3不推荐FreeMarker、jsp这些了。页面的视图技术Thymeleaf , Groovy Templates。
org.springframework.web.servlet.View视图的接口,实现此接口的都是视图类,视图类作为Bean被Spring管理。当然这些不需要开发者编写代码。
ThymeleafView:使用thymeleaf视图技术时的,视图类。
InternalResourceView:表示jsp的视图类。
控制器方法返回值和视图有是关系的。
String:如果项目中有thymeleaf , 这个String表示xxx.html视图文件(/resources目录)
ModelAndView: View中就是表示视图。 
@ResponeBody , @RestController 适合前后端分离项目
String : 表示一个字符串数据
Object:如果有Jackson库,将Objet转为json字符串 
常用的返回值:
String
自定义Object
ResponseEntity
5.2.3.1 页面视图
Thymeleaf作为代替jsp的视图技术,可以编写页面,排列数据。
step1:创建Controller ,控制器方法返回ModelAndView
@Controller
public class ReturnController {
  @GetMapping("/hello")
  public ModelAndView hello(Model model) {
    ModelAndView mv = new ModelAndView();
    mv.addObject("name","李四");
    mv.addObject("age",20);
    mv.setViewName("hello");
    return mv;
  }
}step2:创建视图: 在resources/templates 创建hello.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
    <h3>视图文件</h3>
    姓名:<div th:text="${name}"></div> <br/>
    年龄:<div th:text="${age}"></div> <br/>
</body>
</html>hello.html

application.properties默认thymeleaf的设置
#前缀 视图文件存放位置
spring.thymeleaf.prefix=classpath:/templates/
#后缀 视图文件扩展名
spring.thymeleaf.suffix=.html
step3:测试,浏览器访问
http://localhost:8080/hello

5.2.3.2 JSON视图
自定义Object
step1:增加控制器方法
@GetMapping("/show/json")
@ResponseBody public User getUser(){
  User user = new User();
  user.setName("李四");
  user.setAge(20);
  return user;
}step2: 浏览器访问 http://localhost:8080/show/json

5.2.3.3 复杂JSON
在一个类中定义其他多个引用类型,或集合类型。构成复杂json
step1:
@Data
public class Role {
  //角色ID
  private Integer id;
  //角色名称
  private String roleName;
  //角色说明
  private String memo;
}
@Data
public class User {
  private String name;
  private Integer age;
  private Role role;
}
step2:增加控制器方法
@GetMapping("/show/json2")
@ResponseBody public User getUserRole(){
  User user = new User();
  user.setName("李四");
  user.setAge(20);
  Role role = new Role();
  role.setId(5892);
  role.setRoleName("操作员");
  role.setMemo("基本操作,读取数据,不能修改");
  user.setRole(role);
  return user;
}
step3:浏览器访问

5.2.3.4 ResponseEntity
ResponseEntity包含HttpStatus Code 和 应答数据的结合体。 因为有Http Code能表达标准的语义, 200成功, 404没有发现等。
step1: ResponseEntity做控制器方法返回值
@GetMapping("/show/json3")
ResponseEntity<User> getUserInfo(){
  User user = new User();
  user.setName("李四");
  user.setAge(20);
  Role role = new Role();
  role.setId(5892);
  role.setRoleName("操作员");
  role.setMemo("基本操作,读取数据,不能修改");
  user.setRole(role);
  ResponseEntity<User> response = new ResponseEntity<>(user, HttpStatus.OK);
  return response;
}
step2: 浏览器测试

其他创建ResponseEntity的方式
//ResponseEntity<User> response = new ResponseEntity<>(user, HttpStatus.OK);
//状态码:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/204
// 200 状态码
ResponseEntity<User> response = ResponseEntity.ok(user);
//HTTP 204 No Content 成功状态响应码,表示该请求已经成功了
Response Entity<User> response = ResponseEntity.noContent().build();
5.2.3.5 Map作为返回值
Map作为返回值是数据,能够自动转为json
step1:创建Map返回值的方法
@GetMapping("/map/json")
@ResponseBody public Map getMap(){
  Map<String,Object> map = new HashMap<>();
  map.put("id",1001);
  map.put("address","大兴区");
  map.put("city","北京");
  return map;
}
step2: 测试

5.3 SpringMVC请求流程
Spring MVC 框架是基于Servlet技术的。以请求为驱动,围绕Servlet设计的。 Spring MVC处理用户请求与访问一个Servlet是类似的,请求发送给Servlet,执行doService方法,最后响应结果给浏览器完成一次请求处理。
5.3.1 DispatcherServlet是一个Servlet
DispatcherServlet是核心对象,称为中央调度器(前端控制器Front Controller)。负责接收所有对Controller的请求,调用开发者的Controller处理业务逻辑,将Controller方法的返回值经过视图处理响应给浏览器。
DispatcherServlet作为SpringMVC中的C,职责:
- 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有DispatcherServlet控制。SpringMVC 对外的入口。可以看做门面设计模式。
 - 访问其他的控制器。 这些控制器处理业务逻辑
 - 创建合适的视图,将2中得到业务结果放到视图,响应给用户。
 - 解耦了其他组件,所有组件只与DispatcherServlet交互。彼此之间没有关联
 - 实现ApplictionContextAware, 每个DispatcherServlet都拥自己的WebApplicationContext,它继承了 ApplicationContext。WebApplicationContext包含了Web相关的Bean对象,比如开发人员注释@Controller的类,视图解析器,视图对象等等。 DispatcherServlet访问容器中Bean对象。
 - Servlet + Spring IoC 组合 
	
编辑 
DispatcherServlet继承关系图
5.3.2 Spring MVC的完整请求流程

- 红色DispatherServlet 是框架创建的核心对象(可配置它的属性 contextPath)
 - 蓝色的部分框架已经提供多个对象。开发人员可自定义,替换默认的对象。
 - 绿色的部分是开发人员自己创建的对象,控制器Conroller和视图对象。
 
流程说明:
- DispatcherServlet 接收到客户端发送的请求。判断是普通请求,上传文件的请求。
 - DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
 - HandleMapping 根据请求URI 找到对应的控制器,成HandlerExecutionChain读写。将此对象 返回给DispatcherServlet,做下一步处理。
 - DispatcherServlet 调用HanderAdapter 处理器适配器。这里是适配器设计模式,进行接口转换,将对一个接口 调用转换为其他方法。
 - HandlerAdapter 根据执行控制器方法,也就是开发人员写的Controller类中的方法,并返回一个ModeAndView
 - HandlerAdapter 返回ModeAndView 给DispatcherServlet
 - DispatcherServlet 调用HandlerExceptionResolver处理异常,有异常返回包含异常的ModelAndView
 - DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
 - ViewResolver 解析ModeAndView 并返回真正的View 给DispatcherServlet
 - DispatcherServlet 将得到的视图进行渲染,填充Model中数据到request域
 - 返回给客户端响应结果
 
5.4 SpringMVC自动配置
我们看一下SpringMVC有关的自动配置,Spring MVC自动配置会创建很多对象,重点的有:
- ContentNegotiatingViewResolver和BeanNameViewResolver bean
 - 支持提供静态资源,包括对WebJars的支持
 - 自动注册Converter、GenericConverter和Formatter bean。
 - 对HttpMessageConverters的支持
 - 自动注册MessageCodesResolver
 - 静态index.html支持。
 - 自动使用ConfigurableWebBindingInitializer bean
 
WebMvcAutoConfiguration是Spring MVC自动配置类,源代码如下:
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
  ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
   //.....
}
DispatcherServletAutoConfiguration.class 自动配置DispatcherServlet
WebMvcConfigurationSupport.class 配置SpringMVC的组件
ValidationAutoConfiguration.class: 配置JSR-303验证器
@ConditionalOnWebApplication(type = Type.SERVLET) :应用是基于SERVET的web应用时有效
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }):当项目有Servlet.class, DispatcherServlet.lcass时起作用
5.4.1 DispatcherServletAutoConfiguration.class
web.xml 在SpringMVC以xml文件配置DispatcherServlet,现在有自动配置完成。
<servlet>
  <servlet-name>dispatcher</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/dispatcher.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>dispatcher</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>
DispatcherServletAutoConfiguration自动配置DispatcherServlet。作用:
1.创建DispatcherServlet
@Bean创建DispatcherServlet对象,容器中的名称为dispatcherServlet。作为Servlet的url-pattern为“/”
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
 DispatcherServlet dispatcherServlet = new DispatcherServlet();
 ....
 return dispatcherServlet;
}2.将DispatchServlet注册成bean,放到Spring容器,设置load-on-startup = -1 。
3.创建MultipartResolver,用于上传文件
4.他的配置类WebMvcProperties.class, 前缀spring.mvc
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {  }
5.4.2 WebMvcConfigurationSupport
Spring MVC组件的配置类,Java Config方式创建 HandlerMappings接口的多个对象,HandlerAdapters接口多个对象, HandlerExceptionResolver相关多个对象 ,PathMatchConfigurer, ContentNegotiationManager,OptionalValidatorFactoryBean, HttpMessageConverters等这些实例。
HandlerMappings:
RequestMappingHandlerMapping
HandlerAdapter:  
RequestMappingHandlerAdapter
HandlerExceptionResolver:  
DefaultHandlerExceptionResolver,ExceptionHandlerExceptionResolver(处理@ExceptionHandler注解) 
通过以上自动配置, SpringMVC处理需要的DispatcherServlet对象,HandlerMappings,HandlerAdapters,HandlerExceptionResolver,以及无视图的HttpMessageConverters对象。
5.4.3 ServletWebServerFactoryAutoConfiguration
ServletWebServerFactoryAutoConfiguration 配置嵌入式Web服务器。
- EmbeddedTomcat
 - EmbeddedJetty
 - EmbeddedUndertow
 
Spring Boot检测classpath上存在的类,从而判断当前应用使用的是Tomcat/Jetty/Undertow中的哪一个Servlet Web服务器,从而决定定义相应的工厂组件。也就是Web服务器
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
}
配置类:ServerProperties.class ,配置web server服务器
application文件配置服务器,现在使用tomcat服务器
#服务器端口号
server.port=8001
#上下文访问路径
server.servlet.context-path=/api
#request,response字符编码
server.servlet.encoding.charset=utf-8
#强制 request,response设置charset字符编码
server.servlet.encoding.force=true
#日志路径
server.tomcat.accesslog.directory=D:/logs
#启用访问日志
server.tomcat.accesslog.enabled=true
#日志文件名前缀
server.tomcat.accesslog.prefix=access_log
#日志文件日期时间
server.tomcat.accesslog.file-date-format=.yyyy-MM-dd
#日志文件名称后缀
server.tomcat.accesslog.suffix=.log
#post请求内容最大值,默认2M
server.tomcat.max-http-form-post-size=2000000
#服务器最大连接数
server.tomcat.max-connections=8192更进一步
@DateTimeFormat 格式化日期,可以方法,参数,字段上使用。
示例:控制器方法接受日期参数
@GetMapping("/test/date")
@ResponseBody public String paramDate(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime date){
return "日期:" + date;
}
无需设置:spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss
测试:
http://localhost:8001/api/test/date?date=2002-10-02 11:22:19
5.5 Servlets, Filters, and Listeners
Web应用还会用到Servlet、Filter或Listener。这些对象能够作为Spring Bean注册到嵌入式的Tomcat中。ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean控制Servlet,Filter,Listener。 @Order或Ordered接口控制对象的先后顺序。
Servlet现在完全支持注解的使用方式,@WebServlet
新SpringBoot项目Lession13-ServletFilter, 构建工具Maven, 包名com.bjpowernode.web,依赖Spring Web、Lombok ,JDK19.
5.5.1 Servlets
5.5.1.1 @WebServlet使用Servlet
step1:创建Servlet
package com.bjpowernode.web;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(urlPatterns = "/helloServlet",name = "HelloServlet")
public class HelloServlet extends HttpServlet {
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setContentType("text/html;charset=utf-8");
    PrintWriter writer = resp.getWriter();
    writer.println("这是一个Spring Boot中的Servlet");
    writer.flush();
    writer.close();
  }
}
@ServletComponentScan用于扫描Servlet, Filter ,Listener对象。
step3: 测试

5.5.1.2 ServletRegistrationBean
能够编码方式控制Servlet,不需要注解
step1:创建Servlet,不需要注解
@Configuration
public class WebAppConfig {
  @Bean
  public ServletRegistrationBean addServlet(){
    ServletRegistrationBean registrationBean = new ServletRegistrationBean();
    registrationBean.setServlet(new LoginServlet());
    registrationBean.addUrlMappings("/user/login");
    registrationBean.setLoadOnStartup(1);
    return registrationBean;
  }
}
step2:测试

5.5.2 创建Filter
Filter对象使用频率比较高,比如记录日志,权限验证,敏感字符过滤等等。Web框架中包含内置的Filter,SpringMVC中也包含较多的内置Filter,比如CommonsRequestLoggingFilter,CorsFilter,FormContentFilter...
5.5.2.1 @WebFilter注解
@WebFilter创建Filter对象,使用方式同@WebServlet.
step1:创建过滤器
package com.bjpowernode.web;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
...
//jakarta.servlet.Filter
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain)
      throws IOException, ServletException {
    String requestURI = ((HttpServletRequest) request).getRequestURI();
    System.out.println("filter代码执行了,uri=" +requestURI );
chain.doFilter(request,response);
  }
}
注意SpringBoot3 包名的更改javax--jakarta
step2: 扫描包
@ServletComponentScan(basePackages = "com.bjpowernode.web")
@SpringBootApplication
public class Lession13ServletFilterApplication {
  public static void main(String[] args) {
    SpringApplication.run(Lession13ServletFilterApplication.class, args);
  }
}
step3: 浏览器访问测试
访问Servlet,测试Filter执行

控制台输出:

5.5.2.2 FilterRegistrationBean
FilterRegistrationBean与ServletRegistrationBean使用方式类似,无需注解。
注册Filter
@Bean
public FilterRegistrationBean addFilter(){
  FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
  filterRegistration.setFilter(new LogFilter());
  filterRegistration.addUrlPatterns("/*");
  return filterRegistration;
}
5.5.2.3 Filter排序
多个Filter对象如果要排序,有两种途径:
- 过滤器类名称,按字典顺序排列, AuthFilter - > LogFilter
 - FilterRegistrationBean登记Filter,设置order顺序,数值越小,先执行。
 
step1:创建两个Filter,使用之前的AuthFilter, LogFilter
去掉两个Filter上面的注解
public class AuthFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    String requestURI = ((HttpServletRequest) request).getRequestURI();
    System.out.println("AuthFilter代码执行了,uri=" +requestURI );
    chain.doFilter(request,response);
  }
}
public class LogFilter implements Filter {
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    String requestURI = ((HttpServletRequest) request).getRequestURI();
    System.out.println("LogFilter代码执行了,uri=" +requestURI );
    chain.doFilter(request,response);
  }
}
step2:创建配置类,登记Filter
@Configuration
public class WebAppConfig {
  @Bean
  public ServletRegistrationBean addServlet(){
    ServletRegistrationBean registrationBean = new ServletRegistrationBean();
    registrationBean.setServlet(new LoginServlet());
    registrationBean.addUrlMappings("/user/login");
    registrationBean.setLoadOnStartup(1);
    return registrationBean;
  }
  @Bean
  public FilterRegistrationBean addLogFilter(){
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    filterRegistration.setFilter(new LogFilter());
    filterRegistration.addUrlPatterns("/*");
    filterRegistration.setOrder(1);
    return filterRegistration;
  }
  @Bean
  public FilterRegistrationBean addAuthFilter(){
    FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
    filterRegistration.setFilter(new AuthFilter());
    filterRegistration.addUrlPatterns("/*");
    filterRegistration.setOrder(2);
    return filterRegistration;
  }
}
LogFilter.setOrder(1), AuthFilter.setOrder(2) ; LogFilter先执行。
step3:测试Filter,访问user/login Servlet, 查看控制台输出

5.5.2.4 使用框架中的Filter
Spring Boot中有许多已经定义好的Filter,这些Filter实现了一些功能,如果我们需要使用他们。可以像自己的Filter一样,通过FilterRegistrationBean注册Filter对象。
现在我们想记录每个请求的日志。CommonsRequestLoggingFilter就能完成简单的请求记录。
step1:登记CommonsRequestLoggingFilter
@Bean
public FilterRegistrationBean addOtherFilter(){
  FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
  //创建Filter对象
  CommonsRequestLoggingFilter commonLog = new CommonsRequestLoggingFilter();
  //包含请求uri
  commonLog.setIncludeQueryString(true);
  //登记Filter
  filterRegistration.setFilter(commonLog);
  //拦截所有地址
  filterRegistration.addUrlPatterns("/*");
  return filterRegistration;
}
step2:设置日志级别为debug
修改application.properties
logging.level.web=debug
step3:测试访问
浏览器访问 http://localhost:8080/user/login?name=lisi
查看控制台:

5.5.3 Listener
@WebListener用于注释监听器,监听器类必须实现下面的接口:
- jakarta.servlet.http.HttpSessionAttributeListener
 - jakarta.servlet.http.HttpSessionListener
 - jakarta.servlet.ServletContextAttributeListener
 - jakarta.servlet.ServletContextListener
 - jakarta.servlet.ServletRequestAttributeListener
 - jakarta.servlet.ServletRequestListener
 - jakarta.servlet.http.HttpSessionIdListener
 
另一种方式用ServletListenerRegistrationBean登记Listener对象。
创建监听器:
@WebListener("Listener的描述说明")
public class MySessionListener  implements HttpSessionListener {
  @Override
  public void sessionCreated(HttpSessionEvent se) {
    HttpSessionListener.super.sessionCreated(se);
  }
}
5.6 WebMvcConfigurer
WebMvcConfigurer作为配置类是,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,就是Spring MVC XML配置文件的JavaConfig(编码)实现方式。自定义Interceptor,ViewResolver,MessageConverter。WebMvcConfigurer就是JavaConfig形式的Spring MVC的配置文件
WebMvcConfigurer是一个接口,需要自定义某个对象,实现接口并覆盖某个方法。主要方法功能介绍一下:
public interface WebMvcConfigurer {
	//帮助配置HandlerMapping
	default void configurePathMatch(PathMatchConfigurer configurer) {
	}
        //处理内容协商
	default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
	}
	//异步请求
	default void configureAsyncSupport(AsyncSupportConfigurer configurer) {
	}
	
	//配置默认servlet
	default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
	}
	
    //配置内容转换器
	default void addFormatters(FormatterRegistry registry) {
	}
	//配置拦截器 
	default void addInterceptors(InterceptorRegistry registry) {
	}
	//处理静态资源
	default void addResourceHandlers(ResourceHandlerRegistry registry) {
	}
	//配置全局跨域
	default void addCorsMappings(CorsRegistry registry) {
	}
	//配置视图页面跳转
	default void addViewControllers(ViewControllerRegistry registry) {
	}
	//配置视图解析器
	default void configureViewResolvers(ViewResolverRegistry registry) {
	}
	//自定义参数解析器,处理请求参数
	default void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
	}
	//自定义控制器方法返回值处理器
	default void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
	}
	//配置HttpMessageConverters
	default void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
	}
	//配置HttpMessageConverters
	  default void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
	}
	//配置异常处理器
	 default void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
	}
	//扩展异常处理器
	default void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
	}
	//JSR303的自定义验证器
	default Validator getValidator() {
		return null;
	}
  
        //消息处理对象
	default MessageCodesResolver getMessageCodesResolver() {
		return null;
	}
}
创建新的项目:Lession14-WebMvcConfig,Maven构建工具,JDK19,依赖SpringWeb,Thymeleaf,Lombok。代码包名com.bjpowernode.mvc。
5.6.1 页面跳转控制器
Spring Boot中使用页面视图,比如Thymeleaf。要跳转显示某个页面,必须通过Controller对象。也就是我们需要创建一个Controller,转发到一个视图才行。 如果我们现在需要显示页面,可以无需这个Controller。addViewControllers() 完成从请求到视图跳转。
需求:访问/welcome 跳转到项目首页index.html(Thyemeleaf创建的对象)
项目代码结构:

step1: 创建视图,resources/templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head><body>
  <h3>项目首页,欢迎各位小伙伴</h3>
</body>
</html>
step2: 创建SpringMVC 配置类
@Configuration
public class MvcSettings implements WebMvcConfigurer {
// 跳转视图页面控制器
  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/welcome").setViewName("index");
  }
}
step3: 测试功能

5.6.2 数据格式化
Formatter<T>是数据转换接口,将一种数据类型转换为另一种数据类型。与Formatter<T>功能类型的还有Converter<S,T>。本节研究Formatter<T>接口。Formatter<T>只能将String类型转为其他数据数据类型。这点在Web应用适用更广。因为Web请求的所有参数都是String,我们需要把String转为Integer ,Long,Date 等等。
Spring中内置了一下Formatter<T>:
- DateFormatter : String和Date之间的解析与格式化
 - InetAddressFormatter :String和 InetAddress之间的解析与格式化
 - PercentStyleFormatter :String 和Number 之间的解析与格式化,带货币符合
 - NumberFormat :String 和Number 之间的解析与格式化
 
我在使用@ DateTimeFormat , @NumberFormat 注解时,就是通过Formatter<T>解析String类型到我们期望的Date或Number类型
Formatter<T>也是Spring的扩展点,我们处理特殊格式的请求数据时,能够自定义合适的Formatter<T>,将请求的String数据转为我们的某个对象,使用这个对象更方便我们的后续编码。
接口原型
public interface Formatter<T> extends Printer<T>, Parser<T> {
}Formatter<T>是一个组合接口,没有自己的方法。内容来自Printer<T>和Parser<T>
Printer<T>:将 T 类型转为String,格式化输出
Parser<T>:将String类型转为期望的T对象。
我们项目开发,可能面对多种类型的项目,复杂程度有简单,有难一些。特别是与硬件打交道的项目,数据的格式与一般的name: lisi, age:20不同。数据可能是一串“1111; 2222; 333,NF; 4; 561” 。
需求:将“1111;2222;333,NF;4;561”接受,代码中用DeviceInfo存储参数值。
step1:创建DeviceInfo数据类
@Data
public class DeviceInfo {
  private String item1;
  private String item2;
  private String item3;
  private String item4;
  private String item5;
}
step2:自定义Formatter
public class DeviceFormatter implements Formatter<DeviceInfo> {
  //将String数据,转为DeviceInfo
  @Override
  public DeviceInfo parse(String text, Locale locale) throws ParseException {
    DeviceInfo info = null;
    if (StringUtils.hasLength(text)) {
      String[] items = text.split(";");
      info = new DeviceInfo();
      info.setItem1(items[0]);
      info.setItem2(items[1]);
      info.setItem3(items[2]);
      info.setItem4(items[3]);
      info.setItem5(items[4]);
    }
    return info;
  }
  //将DeviceInfo转为String
  @Override
  public String print(DeviceInfo object, Locale locale) {
    StringJoiner joiner = new StringJoiner("#");
    joiner.add(object.getItem1()).add(object.getItem2());
    return joiner.toString();
  }
}
step3: 登记自定义的DeviceFormatter
addFormatters() 方法登记Formatter
@Configuration
public class MvcSettings implements WebMvcConfigurer {
  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/welcome").setViewName("index");
  }
  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new DeviceFormatter());
  }
  
}
step4: 新建Controller接受请求设备数据
@RestController
public class DeviceController {
  @PostMapping("/device/add")
  public String AddDevice(@RequestParam("device") DeviceInfo deviceInfo){
      return "接收到的设备数据:"+deviceInfo.toString();
  }
}step5:单元测试
POST http://localhost:8080/device/add
Content-Type: application/x-www-form-urlencoded
device=1111;2222;333,NF;4;5615.6.3 拦截器
HandlerInterceptor接口和它的实现类称为拦截器,是SpringMVC的一种对象。拦截器是Spring MVC框架的对象与Servlet无关。拦截器能够预先处理发给Controller的请求。可以决定请求是否被Controller处理。用户请求是先由DispatcherServlet接收后,在Controller之前执行的拦截器对象。
一个项目中有众多的拦截器:框架中预定义的拦截器, 自定义拦截器。下面我说说自定义拦截器的应用。 根据拦截器的特点,类似权限验证,记录日志,过滤字符,登录token处理都可以使用拦截器。
拦截器定义步骤:
- 声明类实现HandlerInterceptor接口,重写三个方法(需要那个重写那个)
 - 登记拦截器
 
5.7 文件上传
上传文件大家首先想到的就是Apache Commons FileUpload,这个库使用非常广泛。Spring Boot3版本中已经不能使用了。代替他的是Spring Boot中自己的文件上传实现。
Spring Boot上传文件现在变得非常简单。提供了封装好的处理上传文件的接口MultipartResolver,用于解析上传文件的请求,他的内部实现类StandardServletMultipartResolver。之前常用的CommonsMultipartResolver不可用了。CommonsMultipartResolver是使用Apache Commons FileUpload库时的处理类。
StandardServletMultipartResolver内部封装了读取POST其中体的请求数据,也就是文件内容。我们现在只需要在Controller的方法加入形参@RequestParam MultipartFile。 MultipartFile表示上传的文件,提供了方便的方法保存文件到磁盘。
MultipartFile API
方法  | 作用  | 
getName()  | 参数名称(upfile)  | 
getOriginalFilename()  | 上传文件原始名称  | 
isEmpty()  | 上传文件是否为空  | 
getSize()  | 上传的文件字节大小  | 
getInputStream()  | 文件的InputStream,可用于读取部件的内容  | 
transferTo(File dest)  | 保存上传文件到目标dest  | 
创建项目Lession15-UploadFile,Maven构建工具,JDK19。依赖选择 Spring Web, Lombok。包名称com.bjpowernode.upload。
需求:上传文件到服务器
5.7.1 MultipartResolver
step1:服务器创建目录存放上传后的文件
例如在 E:/upload
step2: 创建index.html作为上传后的显示页面
resources/static/index.html 
<html lang="en">
<body>
    <h3>项目首页,上传文件成功</h3>
</body>
</html>
step3:创建上传文件页面
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <h3>上传文件</h3>
  <form action="files" enctype="multipart/form-data" method="post">
    选择文件:<input type="file" name="upfile" > <br/>
    <input type="submit" value="上传文件">
  </form>
</body>
</html>
要求:
- enctype="multipart/form-data"
 - method="post"
 - <input type="file" name="upfile" > 表示一个上传文件,upfile 自定义上传文件参数名称
 
step4:创建Controller
@Controller
public class UploadFileController {
  @PostMapping("/upload")
  public String upload(@RequestParam("upfile") MultipartFile multipartFile){
    Map<String,Object> info = new HashMap<>();
    try {
      if( !multipartFile.isEmpty()){
        info.put("上传文件参数名",multipartFile.getName());
        info.put("内容类型",multipartFile.getContentType());
        var ext = "unknown";
        var  filename = multipartFile.getOriginalFilename();
        if(filename.indexOf(".") > 0){
           ext = filename.substring(filename.indexOf(".") + 1);
        }
        var newFileName = UUID.randomUUID().toString() + ext;
        var path = "E:/upload/" + newFileName;
        info.put("上传后文件名称", newFileName );
        multipartFile.transferTo(new File(path));
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    //防止 刷新,重复上传
    return "redirect:/index.html";
  }
}
step5:测试
浏览器访问http://localhost:8080/upload.html
文件上传,查看E:/upload目录上传的文件
Spring Boot默认单个文件最大支持1M,一次请求最大10M。改变默认值,需要application修改配置项
spring.servlet.multipart.max-file-size=800B
spring.servlet.multipart.max-request-size=5MB
spring.servlet.multipart.file-size-threshold=oKB 
file-size-threshold超过指定大小,直接写文件到磁盘,不在内存处理。
配置错误页面 
resources/static/error/5xx.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <h3>上传文件错误</h3>
</body>
</html>5.7.2 Servlet规范
Servlet3.0规范中,定义了jakarta.servlet.http.Part接口处理multipart/form-data POST请求中接收到表单数据。有了Part对象,其write()方法将上传文件保存到服务器本地磁盘目录。
在HttpServletRequest接口中引入的新方法:
- getParts():返回Part对象的集合
 - getPart(字符串名称):检索具有给定名称的单个Part对象。
 
Spring Boot 3使用的Servlet规范是基于5的,所以上传文件使用的就是Part接口。
StandardServletMultipartResolver对Part接口进行的封装,实现基于Servlet规范的文件上传。 
原生的Serlvet规范的文件上传
@Controller
public class UploadAction {
  @PostMapping("/files")
  public String upload(HttpServletRequest request){
    try {
      for (Part part : request.getParts()) {
        String fileName = extractFileName(part);
        part.write(fileName);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    } catch (ServletException e) {
      throw new RuntimeException(e);
    }
    return "redirect:/index.html";
  }
  private String extractFileName(Part part) {
    String contentDisp = part.getHeader("content-disposition");
    String[] items = contentDisp.split(";");
    for (String s : items) {
      if (s.trim().startsWith("filename")) {
        return s.substring(s.indexOf("=") + 2, s.length()-1);
      }
    }
    return "";
  }
}
上传文件包含header头content-disposition,类似下面的内容, 可获取文件原始名称。
form-data; name="dataFile"; filename="header.png"
application文件,可配置服务器存储文件位置,例如:
spring.servlet.multipart.location=E://files/
5.7.3 多文件上传
多文件上传,在接收文件参数部分有所改变 MultiPartFile [] files 。循环遍历数组解析每个上传的文件。
前端请求页面:
<html>
<body>
  <h3>上传文件</h3>
  <form action="files" enctype="multipart/form-data" method="post">
    选择文件1:<input type="file" name="upfile" > <br/>
选择文件2:<input type="file" name="upfile" > <br/>
    <input type="submit" value="上传文件">
  </form>
</body>
</html>5.8 全局异常处理
在Controller处理请求过程中发生了异常,DispatcherServlet将异常处理委托给异常处理器(处理异常的类)。实现HandlerExceptionResolver接口的都是异常处理类。
项目的异常一般集中处理,定义全局异常处理器。在结合框架提供的注解,诸如:@ExceptionHandler,@ControllerAdvice ,@RestControllerAdvice一起完成异常的处理。
@ControllerAdvice与@RestControllerAdvice区别在于:@RestControllerAdvice加了@RepsonseBody。
创建项目Lession16-ExceptionHandler,Maven构建工具,JDK19。依赖选择 Spring Web, Lombok, Thymeleaf。包名称com.bjpowernode.eh。
5.8.1 全局异常处理器
需求:应用计算两个数字相除,当用户被除数为0 ,发生异常。使用自定义异常处理器代替默认的异常处理程序。
step1:创建收入数字的页面
在static目录下创建input.html , static目录下的资源浏览器可以直接访问 
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <form action="divide" method="get">
    除   数:<input name="n1" /> <br/>
    被除数:<input name="n2" /> <br/>
    <input type="submit" value="计算">
  </form>
</body>
</html>step2:创建控制器,计算两个整数相除
@RestController
public class NumberController {
  @GetMapping("/divide")
  public String some(Integer n1,Integer n2){
    int result = n1 / n2;
    return "n1/n2=" + result;
  }
}step3:浏览器访问 input.html ,计算两个数字相除

编辑
显示默认错误页面

编辑
step4:创建自定义异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
  //用视图页面作为展示
  @ExceptionHandler({ArithmeticException.class})
  public String handleArithmeticException(ArithmeticException e, Model model){
    String error = e.getMessage();
    model.addAttribute("error",error);
    return "exp";
  }
  //不带视图,直接返回数据
  /*
@ExceptionHandler({ArithmeticException.class})
  @ResponseBody public Map<String,Object>   
handleArithmeticExceptionReturnData(ArithmeticException e){
    String error = e.getMessage();
    Map<String,Object> map = new HashMap<>();
    map.put("错误原因", e.getMessage());
    map.put("解决方法", "输入的被除数要>0");
    return map;  }*/
//其他异常
  @ExceptionHandler({Exception.class})
  @ResponseBody public Map<String,Object> handleRootException(Exception e){
    String error = e.getMessage();
    Map<String,Object> map = new HashMap<>();
    map.put("错误原因", e.getMessage());
    map.put("解决方法", "请稍候重试");
    return map;
  }
}step5:创建给用提示的页面
在resources/templates/ 创建 exp.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
    错误原因:<h3 th:text="${error}"></h3>
</body>
</html>在测试显示,提示页面

编辑
更进一步
建议在参数签名中尽可能具体异常类,以减少异常类型和原因异常之间不匹配的问题,考虑创建多个@ExceptionHandler方法的,每个方法通过其签名匹配单个特定的异常类型。最后增加一个根异常,考虑没有匹配的其他情况  | 
5.8.2 BeanValidator异常处理
使用JSR-303验证参数时,我们是在Controller方法,声明BindingResult对象获取校验结果。Controller的方法很多,每个方法都加入BindingResult处理检验参数比较繁琐。 校验参数失败抛出异常给框架,异常处理器能够捕获到 MethodArgumentNotValidException,它是BindException的子类。

编辑
BindException异常实现了BindingResult接口,异常类能够得到BindingResult对象,进一步获取JSR303校验的异常信息。
需求:全局处理JSR-303校验异常
step1:添加JSR-303依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>step2:创建Bean对象,属性加入JSR-303注解
@Data
public class OrderVO {
  @NotBlank(message = "订单名称不能为空")
  private String name;
  @NotNull(message = "商品数量必须有值")
  @Range(min = 1,max = 99,message = "一个订单商品数量在{min}-{max}")
  private Integer amount;
  @NotNull(message = "用户不能为空")
  @Min(value = 1,message = "从1开始")
  private Integer userId;
}step3:Controlller接收请求
@RestController
public class OrderController {
  @PostMapping("/order/new")
  public String createOrder(@Validated @RequestBody OrderVO orderVO){
    return orderVO.toString();
  }
}step4:创建异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler2 {
  //校验参数异常
  @ExceptionHandler({BindException.class})
  public Map<String,Object> handleJSR303Exception(BindException e){
    Map<String,Object> map = new HashMap<>();
    BindingResult result = e.getBindingResult();
    if (result.hasErrors()) {
      List<FieldError> errors = result.getFieldErrors();
      errors.forEach(field -> {
        map.put("错误["+field.getField()+"]原因",field.getDefaultMessage());
      });
    }
    return map;
  }
}step5:测试
POST http://localhost:8080/order/new
Content-Type: application/json
{
  "name": "每日订单",
  "amount": 0,
  "userId": 0
}显示:
{  
   "错误[userId]原因": "从1开始",  
   "错误[amount]原因": "一个订单商品数量在1-99"
}
5.8.3 ProblemDetail [SpringBoot 3]
一直依赖 Spring Boot默认的异常反馈内容比较单一,包含Http Status Code, 时间,异常信息。但具体异常原因没有体现。这次Spring Boot3 对错误信息增强了。
5.8.3.1 RFC 7807
RFC 7807(Problem Details for HTTP APIs): RFC 7807: Problem Details for HTTP APIs (rfc-editor.org)
RESTFul服务中通常需要在响应体中包含错误详情,Spring 框架支持”Problem Details“。定义了Http应答错误的处理细节,增强了响应错误的内容。包含标准和非标准的字段。同时支持json和xml两种格式。
基于Http协议的请求,可通过Http Status Code分析响应结果,200为成功, 4XX为客户端错误,500是服务器程序代码异常。 status code过于简单,不能进一步说明具体的错误原因和解决途径。比如 http status code 403, 但并不能说明 ”是什么导致了403“,以及如何解决问题。Http状态代码可帮助我们区分错误和成功状态,但没法区分得太细致。RFC 7807中对这些做了规范的定义。 ”
Problem Details“ 的JSON应答格式 .
{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/transactions/abc"
}”Problem Details“ 包含内容:

标准字段  | 描述  | 必须  | 
type  | 标识错误类型的 URI。在浏览器中加载这个 URI 应该转向这个错误的文档。 此字段可用于识别错误类。完善的系统可用type构建异常处理模块,默认为 about:blank  | 可认为是  | 
title  | 问题类型的简短、易读的摘要  | 否  | 
detail  | 错误信息详细描述,对title的进一步阐述  | 否  | 
instance  | 标识该特定故障实例的 URI。它可以作为发生的这个错误的 ID  | 否  | 
status  | 错误使用的 HTTP 状态代码。它必须与实际状态匹配  | 否  | 
除了以上字段,用户可以扩展字段。采用key:value格式。增强对问题的描述。
5.8.3.2 MediaType
RFC 7807 规范增加了两种媒体类型: `application/problem+json`或`application/problem+xml`。返回错误的 HTTP 响应应在其`Content-Type`响应标头中包含适当的内容类型,并且客户端可以检查该标头以确认格式.
5.8.3.3 Spring支持Problem Detail
Spring支持ProblemDetail
- ProblemDetail 类: 封装标准字段和扩展字段的简单对象
 - ErrorResponse :错误应答类,完整的RFC 7807错误响应的表示,包括status、headers和RFC 7807格式的ProblemDetail正文
 - ErrorResponseException :ErrorResponse接口一个实现,可以作为一个方便的基类。扩展自定义的错误处理类。
 - ResponseEntityExceptionHandler:它处理所有Spring MVC异常,与@ControllerAdvice一起使用。
 
以上类型作为异常处理器方法的返回值,框架将返回值格式化RFC 7807的字段。
ProblemDetail 作为
ProblemDetail:类方法,org.springframework.http.ProblemDetail 

ErrorResponse:接口,ErrorResponseException是他的实现类,包含应答错误的status ,header, ProblemDetail .
SpringMVC中异常处理方法(带有@ExceptionHandler)返回ProblemDetail ,ErrorResponse都会作为RFC 7807的规范处理。
5.8.3.4 自定义异常处理器ProblemDetail
需求:我们示例查询某个isbn的图书。 在application.yml中配置图书的初始数据。 用户访问一个api地址,查询某个isbn的图书, 查询不到抛出自定义异常BootNotFoundException。 自定义异常处理器捕获异常。ProblemDetail 作为应答结果。支持RFC 7807
创建新的SpringBoot项目Lession17-ProblemDetail,依赖选择Spring Web , lombok 。Maven构建工具,JDK19,包名com.bjpowernode 。
项目Maven依赖
<!--web依赖-->
 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <!--lombok依赖-->
 <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
 </dependency>step1:新建图书的Record(普通的POJO类都是可以的)
public record Book(String isbn,String name,String author) {
}step2:创建存储多本图书的容器类
@Setter
@Getter
@ConfigurationProperties(prefix = "product")
public class BookContainer {
  private List<Book> books;
}step3:application.yml配置图书基础数据
product:
  books:
    - isbn: B001
      name: java
      author: lisi
    - isbn: B002
      name: tomcat
      author: zhangsan
    - isbn: B003
      name: jvm
      author: zhouxing
      
server:
  servlet:
    context-path: /apiste4:新建自定义异常类
public class BookNotFoundException extends RuntimeException{
  public BookNotFoundException() {
    super();
  }
  public BookNotFoundException(String message) {
    super(message);
  }
}step5:新建控制器类
@RestController
public class BookController {
  @Resource
  private BookContainer bookContainer;
  @GetMapping("/book")
  Book getBook(String isbn) throws Exception {
    Optional<Book> book = bookContainer.getBooks().stream()
        .filter(el -> el.isbn().equals(isbn))
        .findFirst();
    if( book.isEmpty() ){
      throw new BookNotFoundException("isbn:"+ isbn + "->没有此图书");
    }
    return book.get();
  }
}step6:新建异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
  @ExceptionHandler(value = { BookNotFoundException.class })
  public ProblemDetail handleBookNotFoundException(BookNotFoundException ex){
    ProblemDetail problemDetail = 
        ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,ex.getMessage());
    problemDetail.setType(URI.create("/api/probs/not-found"));
    problemDetail.setTitle("图书异常");
    return problemDetail;
  }
}step7:测试接口
测试部分,使用IDEA自带的Http Client工具。点击@GetMapping左侧的图标

启动Http Client工具, 编写Http 请求。
IDEA默认生成的 一个临时文件用于编写,存储http请求url,header等

点击左侧的绿色箭头执行请求,当前请求isbn为B001 ,能够正常执行请求,获取的Book。
将isbn设置为B006,测试结果如下

5.8.3.5 扩展ProblemDetail
修改异常处理方法,增加ProblemDetail自定义字段,自定义字段以Map<String,Object>存储,调用setProperty(name,value)将自定义字段添加到ProblemDetail对象。
@ExceptionHandler(value = { BookNotFoundException.class })
  public ProblemDetail handleBookNotFoundException(BookNotFoundException ex){
    ProblemDetail problemDetail = 
        ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,ex.getMessage());
    problemDetail.setType(URI.create("/api/probs/not-found"));
    problemDetail.setTitle("图书异常");
    //增加自定义字段
    //时间戳
    problemDetail.setProperty("timestamp", Instant.now());
    //客服邮箱
    problemDetail.setProperty("客服邮箱", "sevice@bjpowernode.com");
    return problemDetail;
  }测试接口,isbn=B006, 应答返回结果如下:
{
  "type": "/api/probs/not-found",
  "title": "图书异常",
  "status": 404,
  "detail": "isbn:B006->没有此图书",
  "instance": "/api/book",
  "timestamp": "2023-01-14T12:10:55.304722900Z",
  "客服邮箱": "sevice@bjpowernode.com"
}5.8.3.6 ErrorResponse
Spring Boot识别ErrorResponse类型作为异常的应答结果。可以直接使用ErrorResponse作为异常处理方法的返回值,ErrorResponseException是ErrorResponse的基本实现类。
注释掉GlobalExceptionHandler#handleBookNotFoundException方法,增加下面的方法
@ExceptionHandler(value = { BookNotFoundException.class})
public ErrorResponse handleException(BookNotFoundException ex){
    ErrorResponse error = new ErrorResponseException(HttpStatus.NOT_FOUND,ex);
    return error;
}测试接口,isbn=B006, 应答返回结果如下:
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "instance": "/api/book"
}5.8.3.7 扩展ErrorResponseException
自定义异常可以扩展ErrorResponseException, SpringMVC将处理异常并以符合RFC 7807的格式返回错误响应。ResponseEntityExceptionHandler能够处理大部分SpringMVC的异常的, 其方法handleException()提供了对ErrorResponseException异常处理:
@ExceptionHandler({
  ...
  ErrorResponseException.class,
  ...
 })
@Nullable
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request)由此可以创建自定义异常类,继承ErrorResponseException,剩下的交给SpringMVC内部自己处理就好。 省去了自己的异常处理器,@ExceptionHandler。
step1:创建新的异常类继承ErrorResponseException
public class IsbnNotFoundException extends ErrorResponseException {
  public IsbnNotFoundException(HttpStatus status, String detail) {
    super(status,createProblemDetail(status,detail),null);
  }
  private static ProblemDetail createProblemDetail(HttpStatus status,String detail) {
    ProblemDetail problemDetail = ProblemDetail.forStatus(status);
    problemDetail.setType(URI.create("/api/probs/not-found"));
    problemDetail.setTitle("图书异常");
    problemDetail.setDetail(detail);
    //增加自定义字段
    problemDetail.setProperty("严重程度", "低");
    problemDetail.setProperty("客服邮箱", "sevice@bjpowernode.com");
    return  problemDetail;
  }
}









