动力节点王鹤SpringBoot3学习笔记——第五章 说说Web服务

目录

第五章 说说Web服务

5.1 高效构建Web应用

5.1.1  html页面视图

5.1.2 JSON视图 

5.1.3  给项目加favicon

5.2 Spring MVC 

5.2.1 控制器Controller 

5.2.1.1  匹配请求路径到控制器方法

5.2.1.2 @RequestMapping

5.2.1.3 控制器方法参数类型与可用返回值类型 

5.2.1.4 接收请求参数

5.2.1.4.1 接收json 

5.2.1.4.2 Reader-InputStream

5.2.1.4.3 数组参数接收多个值

5.2.1.5 验证参数

5.2.1.5.1 Java Bean Validation 

5.2.1.5.2 JSR-303注解 

5.2.1.5.3 快速上手

5.2.1.5.4 分组校验

5.2.1.5.5 ValidationAutoConfiguration

5.2.2  模型Model

5.2.3 视图View

5.2.3.1 页面视图 

5.2.3.2  JSON视图

5.2.3.3  复杂JSON

5.2.3.4  ResponseEntity

5.2.3.5 Map作为返回值 

5.3  SpringMVC请求流程

5.3.1 DispatcherServlet是一个Servlet 

5.3.2 Spring MVC的完整请求流程 

5.4 SpringMVC自动配置 

5.4.1 DispatcherServletAutoConfiguration.class 

5.4.2  WebMvcConfigurationSupport

5.4.3  ServletWebServerFactoryAutoConfiguration 

5.5  Servlets, Filters, and Listeners 

5.5.1 Servlets 

5.5.1.1 @WebServlet使用Servlet

5.5.1.2 ServletRegistrationBean 

5.5.2  创建Filter

5.5.2.1 @WebFilter注解 

5.5.2.2  FilterRegistrationBean

5.5.2.3 Filter排序

5.5.2.4  使用框架中的Filter

5.5.3  Listener

5.6 WebMvcConfigurer

5.6.1 页面跳转控制器 

5.6.2 数据格式化

5.6.3 拦截器

5.6.3.1  一个拦截器 

5.6.3.2  多个拦截器

5.7  文件上传

5.7.1  MultipartResolver 

5.7.2  Servlet规范

5.7.3 多文件上传 

5.8 全局异常处理

5.8.1  全局异常处理器 

5.8.2  BeanValidator异常处理

5.8.3  ProblemDetail [SpringBoot 3] 

5.8.3.1  RFC 7807 

5.8.3.2  MediaType

5.8.3.3 Spring支持Problem Detail 

5.8.3.4  自定义异常处理器ProblemDetail 

5.8.3.5  扩展ProblemDetail 

5.8.3.6 ErrorResponse 

5.8.3.7  扩展ErrorResponseException


 

第五章 说说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的应用分成三步:

  1. 编写请求页面(在浏览器直接模拟的请求)
  2. 编写Controller
  3. 编写视图页面

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

注意:

  1. 关闭缓存,浏览器清理缓存
  2. 如果项目有过滤器,拦截器需要放行对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.ServletResponseServlet API中的请求,应答
jakarta.servlet.http.HttpSessionServlet API的会话
jakarta.servlet.http.PushBuilderServlet 4.0 规范中推送对象
HttpMethod请求方式
java.io.InputStream, java.io.ReaderIO中输入,读取request body
java.io.OutputStream, java.io.WriterIO中输入,访问response body
@PathVariable,@MatrixVariable,@RequestParam,@RequestHeader,@CookieValue,@RequestBody,@RequestPart,@ModelAttributeuri模板中的变量,访问矩阵,访问单个请求参数,访问请求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错误对象
ProblemDetailRFC7807,规范的错误应答对象
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/  ,最新3.0版本,2020年10.

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)被注释的元素必须符合指定的正则表达式
@Email被注释的元素必须是电子邮箱地址
@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,职责: 

  1. 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有DispatcherServlet控制。SpringMVC 对外的入口。可以看做门面设计模式。
  2. 访问其他的控制器。 这些控制器处理业务逻辑
  3. 创建合适的视图,将2中得到业务结果放到视图,响应给用户。
  4. 解耦了其他组件,所有组件只与DispatcherServlet交互。彼此之间没有关联
  5. 实现ApplictionContextAware, 每个DispatcherServlet都拥自己的WebApplicationContext,它继承了   ApplicationContext。WebApplicationContext包含了Web相关的Bean对象,比如开发人员注释@Controller的类,视图解析器,视图对象等等。 DispatcherServlet访问容器中Bean对象。
  6. Servlet + Spring IoC 组合 

 

 DispatcherServlet继承关系图

5.3.2 Spring MVC的完整请求流程 

  1. 红色DispatherServlet 是框架创建的核心对象(可配置它的属性 contextPath)
  2. 蓝色的部分框架已经提供多个对象。开发人员可自定义,替换默认的对象。
  3. 绿色的部分是开发人员自己创建的对象,控制器Conroller和视图对象。

流程说明: 

  1. DispatcherServlet 接收到客户端发送的请求。判断是普通请求,上传文件的请求。
  2. DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
  3. HandleMapping 根据请求URI 找到对应的控制器以及拦截器,组装成HandlerExecutionChain读写。将此对象 返回给DispatcherServlet,做下一步处理。
  4. DispatcherServlet 调用HanderAdapter 处理器适配器。这里是适配器设计模式,进行接口转换,将对一个接口 调用转换为其他方法。
  5. HandlerAdapter 根据执行控制器方法,也就是开发人员写的Controller类中的方法,并返回一个ModeAndView 
  6. HandlerAdapter 返回ModeAndView 给DispatcherServlet
  7. DispatcherServlet 调用HandlerExceptionResolver处理异常,有异常返回包含异常的ModelAndView
  8. DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
  9. ViewResolver 解析ModeAndView 并返回真正的View 给DispatcherServlet
  10. DispatcherServlet 将得到的视图进行渲染,填充Model中数据到request域
  11. 返回给客户端响应结果

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对象如果要排序,有两种途径:

  1. 过滤器类名称,按字典顺序排列, AuthFilter - > LogFilter
  2. 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;561

5.6.3 拦截器

HandlerInterceptor接口和它的实现类称为拦截器,是SpringMVC的一种对象。拦截器是Spring MVC框架的对象与Servlet无关。拦截器能够预先处理发给Controller的请求。可以决定请求是否被Controller处理。用户请求是先由DispatcherServlet接收后,在Controller之前执行的拦截器对象。

一个项目中有众多的拦截器:框架中预定义的拦截器, 自定义拦截器。下面我说说自定义拦截器的应用。 根据拦截器的特点,类似权限验证,记录日志,过滤字符,登录token处理都可以使用拦截器。 

拦截器定义步骤:

  1. 声明类实现HandlerInterceptor接口,重写三个方法(需要那个重写那个)
  2. 登记拦截器 

5.6.3.1  一个拦截器 

需求:zhangsan操作员用户,只能查看文章,不能修改,删除。

step1:创建文章的Controller 

@RestController
public class ArticleController {

  @PostMapping("/article/add")
  public String addArticle(){
    return "发布新的文章";
  }

  @PostMapping("/article/edit")
  public String editArticle(){
    return "修改文章";
  }

  @GetMapping("/article/query")
  public String query(){
    return "查询文章";
  }

}

step2:创建有关权限拦截器

public class AuthInterceptor implements HandlerInterceptor {

  private final String COMMON_USER="zhangsan";
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {

    System.out.println("=====AuthInterceptor权限拦截器=====");
    //获取登录的用户
    String loginUser = request.getParameter("loginUser");
    //获取操作的url
    String requestURI = request.getRequestURI();


    if( COMMON_USER.equals(loginUser) &&
       (requestURI.startsWith("/article/add")
         || requestURI.startsWith("/article/edit")
         || requestURI.startsWith("/article/remove"))) {
      return  false;
    }
    return true;
  }
}

step3:登记拦截器

@Configuration
public class MvcSettings implements WebMvcConfigurer {
  //...
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    AuthInterceptor authInterceptor= new AuthInterceptor();
    registry.addInterceptor(authInterceptor)
        .addPathPatterns("/article/**")  //拦截article开始的所有请求
        .excludePathPatterns("/article/query"); //排除/article/query请求
  }
}

step4:测试拦截器

POST http://localhost:8080/article/add
Content-Type: application/x-www-form-urlencoded

loginUser=zhangsan&title=Vue3&summary=Vue从基础到精通

###
POST http://localhost:8080/article/add
Content-Type: application/x-www-form-urlencoded

loginUser=lisi&title=Vue3&summary=Vue从基础到精通

5.6.3.2  多个拦截器

增加一个验证登录用户的拦截器,只有zhangsan,lisi,admin能够登录系统。其他用户不可以。 两个拦截器登录的拦截器先执行,权限拦截器后执行,order()方法设置顺序,整数值越小,先执行。 step1:创建登录拦截器 

public class LoginInterceptor implements HandlerInterceptor {

  private List<String> permitUser= new ArrayList();

  public LoginInterceptor() {
    permitUser = Arrays.asList("zhangsan","lisi","admin");
  }

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {

    System.out.println("=====LoginInterceptor登录拦截器=====");
    //获取登录的用户
    String loginUser = request.getParameter("loginUser");
    if( StringUtils.hasText(loginUser) && permitUser.contains(loginUser) ) {
      return true;
    }
    return false;

  }
}

step2:登记拦截器,设置顺序order

@Override
public void addInterceptors(InterceptorRegistry registry) {

  AuthInterceptor authInterceptor= new AuthInterceptor();
  registry.addInterceptor(authInterceptor)
      .order(2)
      .addPathPatterns("/article/**")  //拦截article开始的所有请求
      .excludePathPatterns("/article/query"); //排除/article/query请求

  LoginInterceptor loginInterceptor = new LoginInterceptor();
  registry.addInterceptor(loginInterceptor)
      .order(1)
      .addPathPatterns("/**") //拦截所有请求
      .excludePathPatterns("/article/query"); //排除/article/query请求

}

step3:测试拦截器

POST http://localhost:8080/article/add
Content-Type: application/x-www-form-urlencoded

loginUser=lisi&title=Vue3&summary=Vue从基础到精通

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=0KB 

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">
    除&nbsp;&nbsp;&nbsp;数:<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: /api

ste4:新建自定义异常类

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;
  }
}

step3:启动RFC 7807支持

修改application.yml,增加配置

spring:
  mvc:
    problemdetails:
      enabled: true

step4:测试接口

测试接口,isbn=B006, 应答返回结果如下:

{
  "type": "/api/probs/not-found",
  "title": "图书异常",
  "status": 404,
  "detail": "isbn:B006->没有此图书",
  "instance": "/api/book",
  "严重程度": "低",
  "客服邮箱": "sevice@bjpowernode.com"
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/6069.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

HashMap底层数据结构

TreeMap TreeMap的底层是红黑树&#xff0c;是自平衡的二叉查找树。 在查找元素时会从左子树或右子树查找&#xff0c;和元素一个一个进行比较&#xff0c;对于大数量的查找的场景TreeMap不适合&#xff08;HashMap解决了这个问题&#xff09;。 TreeMap的好处&#xff0c;是…

隐私计算 FATE - 多分类神经网络算法测试

一、说明 本文分享基于 Fate 使用 横向联邦 神经网络算法 对 多分类 的数据进行 模型训练&#xff0c;并使用该模型对数据进行 多分类预测。 二分类算法&#xff1a;是指待预测的 label 标签的取值只有两种&#xff1b;直白来讲就是每个实例的可能类别只有两种 (0 或者 1)&…

两个数组的交集(力扣刷题)

给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/intersection-of-two-arrays 说…

人大女王金融硕士——站在一个更高的起点,拓宽自己的眼界

俗话说&#xff1a;“视野所及&#xff0c;心之所止”。做任何事情&#xff0c;最重要的是眼光。眼界不一样&#xff0c;就会有不一样的人生。站得更高才能看得更远&#xff0c;看得更远才能收获更多。人民大学与加拿大女王大学金融硕士项目为我们提供在职读研平台&#xff0c;…

Python机器学习:最大熵模型

信息论里&#xff0c;熵是可以度量随机变量的不确定性的&#xff0c;已经证明的&#xff1a;当随机变量呈均匀分布的时候&#xff0c;熵值最大&#xff0c;一个有序的系统有着较小的熵值&#xff0c;无序系统的熵值则较大。 机器学习里面&#xff0c;最大熵原理假设&#xff1…

【HAL库】HAL库STM32cubemx快速使用

文章目录整体框图一、基础工程1 新建工程2 配置RCC3 配置SYS4 工程设置5 生成代码6 keil设置下载&复位二、必备外设1 目录规范2 LED2 RTC3 USART4 KEY三、其他外设1 OLED&#xff08;模拟IIC、模拟SPI&#xff09;2 BH1750光强检测3 MQ2烟雾检测3 MQ4甲醛检测4 DHT11温湿度…

基于蓄电池进行调峰和频率调节研究【超线性增益的联合优化】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。⛳座右铭&#…

第04章_运算符

第04章_运算符 &#x1f3e0;个人主页&#xff1a;shark-Gao &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是shark-Gao&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f389;目前状况&#xff1a;23届毕业生&#xff0c;目前在某公…

该不该放弃嵌入式,单片机这条路?

本文几乎浓缩了我从业10几年的精华&#xff0c;内容涵盖我转行、打工、创业的经历。 建议从头到尾不要错过一字一句&#xff0c;因为字里行间的经验之谈&#xff0c;或许能成为你人生重要转折点。 全文3700多字&#xff0c;写了6个多小时&#xff0c;如果你赶时间&#xff0c;建…

【17】核心易中期刊推荐——深度学习 | 遥感图像处理

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

【学会这几个VSCode插件,让你的Python代码更优秀】

VSCode&#xff08;Visual Studio Code&#xff09;是由微软研发的一款免费、开源的跨平台文本&#xff08;代码&#xff09;编辑器&#xff0c;一般主要用于轻量级的编程代码工作&#xff0c;就非常适合Python&#xff0c;同时在前端开发方面也有举足轻重的地位。但如果想用于…

蓝桥杯集训·每日一题Week3

Trie AcWing 835. Trie字符串统计&#xff08;算法基础课&#xff09; 思路&#xff1a; Trie是一种高效地存储和查找字符串集合的数据结构,适用于字符串不太复杂的情况。其形状是一个以0为根节点的树&#xff0c;查询和插入的效率都比较高&#xff0c;有插入和查询两种操作。…

制造业的寒冬真的要来了吗?

制造业的寒冬真的要来了吗&#xff1f;其实当前&#xff0c;我国制造业发展水平是处于全球第三阵列&#xff0c;排名第四的&#xff1a; 但能处第三序列靠前&#xff0c;还是因为“规模发展”起了重要支撑——依靠规模拉动发展。所以如果从“质量效益”、“结构优化”、“持续发…

【AI探索】我问了ChatGPT几个终极问题

终于尝试了一把ChatGPT的强大之处&#xff0c;问了一下关心的几个问题&#xff1a; chatGPT现在在思考吗&#xff1f;有没有什么你感兴趣的问题&#xff1f; 你认为AI会对人类产生哪些方面的影响&#xff1f; 你对人类所涉及到的学科有了解吗&#xff1f;你认为在哪些方面与人类…

JetPack Compose之Modifier修饰符

前言 在Compose中&#xff0c;每一个组件都是带有Compose注解的函数&#xff0c;被称为Composable。Compose已经预置了很多的Compose UI组件&#xff0c;这些组件都是基于Material Design规范设计的&#xff0c;例如Button&#xff0c;TextField&#xff0c;TopAPPBar等。在布…

IOC、AOP、和javca面试题

一、 1、控制反转&#xff08;IOC&#xff09; 将创建管理对象的工作交给容器来做。在容器初始化&#xff08;或在某个时间节点&#xff09;通过反射机制创建好对象&#xff0c;在使用时直接从容器中获取。 控制反转&#xff1a;将对象的控制权反过来交给容器管理。 IOC实现…

既然有http 请求,为什么还要用rpc调用?

先弄明白什么是RPC。 RPC&#xff08;Remote Procedure Call&#xff09;—远程过程调用&#xff0c;它是一种通过网络从远程计算机程序上请求服务&#xff0c;而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在&#xff0c;如TCP或UDP&#xff0c;为通信程序之…

【面试】Java并发编程面试题

文章目录基础知识为什么要使用并发编程多线程应用场景并发编程有什么缺点并发编程三个必要因素是什么&#xff1f;在 Java 程序中怎么保证多线程的运行安全&#xff1f;并行和并发有什么区别&#xff1f;什么是多线程多线程的好处多线程的劣势&#xff1a;线程和进程区别什么是…

基于java+ssm+vue病人跟踪治疗信息管理系统的搭建及源码

源码获取方式见文末 一.需求简介 病人治疗信息管理系统采用B/S模式&#xff0c;实现安全、快捷、高效的病人跟踪治疗信息管理。传统手工管理模式效率低下&#xff0c;已无法满足病人需求。 信息化时代的到来&#xff0c;使得开发病人跟踪治疗信息管理系统成为必然。 本系统采…

Linux 串口RS232/485/GPS 驱动实验(移植minicom)

目录Linux 下UART 驱动框架I.MX6U UART 驱动分析硬件原理图分析RS232 驱动编写移植minicomRS232 驱动测试RS232 连接设置minicom 设置RS232 收发测试RS485 测试RS485 连接设置RS485 收发测试GPS 测试GPS 连接设置GPS 数据接收测试串口是很常用的一个外设&#xff0c;在Linux 下…