springboot3视频笔记⑤说说web服务

5 说说Web服务

****王鹤springboot3视频笔记 基于浏览器的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.****.quickweb。 项目结构:

5.1.1 html页面视图

step1: Maven依赖 spring-web starter

| org.springframework.boot spring-boot-starter-web

org.springframework.boot spring-boot-starter-thymeleaf

org.projectlombok lombok true | | --- |

step2: 创建Controller 在根包的下面,创建子包controller,并创建QuickController

| @Controllerpublic class QuickController {

@RequestMapping("/exam/quick") public String quick(Model model){ //业务处理结果数据,放入到Model模型 model.addAttribute("title", "Web开发"); model.addAttribute("time", LocalDateTime.now()); return "quick"; }

}

step3: 创建视图

|

视图

显示请求处理结果

| | --- |

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

| @Datapublic class User { private String name; private Integer age; }

@Controllerpublic 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的引用。 在视图的部分加入

代码如下

|

视图 **** | | --- |

测试:浏览器访问项目地址 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.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, ResponseEntity 包含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:创建控制器类

| @Datapublic class User { private String name; private Integer age; } @RestControllerpublic 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 users){ System.out.println("接收的json array:"+users.toString()); return "json转为List对象"+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 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

| org.springframework.boot spring-boot-starter-validation

step2:创建文章数据类,添加约束注解

| @Datapublic 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

| @RestControllerpublic 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": "**********"

}
5.2.1.5.4 分组校验

上面的AriticleVO用来新增文章, 新的文章主键id是系统生成的。现在要修改文章,比如修改某个文章的title,summary, readCount,email等。此时id必须有值,修改这个id的文章。 新增和修改操作对id有不同的要求约束要求。通过group来区分是否验证。 group是Class作为表示, java中包加类一定是唯一的, 这个标识没有其他实际意义

step1:添加group标志

| @Datapublic 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标志

| @RestControllerpublic 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": "**********" } POST http://localhost:8080/article/edit_Content-Type_: application/json

{ "id": 2201, "userId": 216, "title": "云原生", "summary": "云原生SpringCloud,Linux", "readCount": 0, "email": "**********"

}
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](https://blog.csdn.net/yongwa123/article/details/_blank)大家族就派上用场了。 Model模型的意思,Spring MVC中的“M”,用来传输数据。从控制层直接返回数据给前端,配置jsp,模板技术能够展现M中存储的数据。 Model简单理解就是给前端浏览器的数据,放在Model中,ModelAndView里的任意值,还有json格式的字符串等都是Model。

| @Controllerpublic 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

| @Controllerpublic 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

|

Title

视图文件

姓名:

年龄:

| | --- |

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:

| @Datapublic class Role { //角色ID private Integer id; //角色名称 private String roleName; //角色说明 private String memo;

} @Datapublic 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 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 response = new ResponseEntity<>(user, HttpStatus.OK); return response;

}

step2: 浏览器测试

其他创建ResponseEntity的方式

| //ResponseEntity response = new ResponseEntity<>(user, HttpStatus.OK);

//状态码:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/204 // 200 状态码ResponseEntity response = ResponseEntity.ok(user);

//HTTP 204 No Content 成功状态响应码,表示该请求已经成功了ResponseEntity 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

对外的入口。可以看做门面设计模式。

  1. 访问其他的控制器。 这些控制器处理业务逻辑
  2. 创建合适的视图,将2中得到业务结果放到视图,响应给用户。
  3. 解耦了其他组件,所有组件只与DispatcherServlet交互。彼此之间没有关联
  4. 实现ApplictionContextAware, 每个DispatcherServlet都拥自己的WebApplicationContext,它继承了

ApplicationContext。WebApplicationContext包含了Web相关的Bean对象,比如开发人员注释@Controller的类,视图解析器,视图对象等等。 DispatcherServlet访问容器中Bean对象。

  1. 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,做下一步处理。

  1. DispatcherServlet 调用HanderAdapter 处理器适配器。这里是适配器设计模式,进行接口转换,将对一个接口

调用转换为其他方法。

  1. HandlerAdapter 根据执行控制器方法,也就是开发人员写的Controller类中的方法,并返回一个ModeAndView

  2. HandlerAdapter 返回ModeAndView 给DispatcherServlet

  3. DispatcherServlet 调用HandlerExceptionResolver处理异常,有异常返回包含异常的ModelAndView

  4. DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView

  5. ViewResolver 解析ModeAndView 并返回真正的View 给DispatcherServlet

  6. DispatcherServlet 将得到的视图进行渲染,填充Model中数据到request域

  7. 返回给客户端响应结果

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,现在有自动配置完成。

| dispatcher org.springframework.web.servlet.DispatcherServlet contextConfigLocation /WEB-INF/spring/dispatcher.xml 1 dispatcher /

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;

}
  1. 将DispatchServlet注册成bean,放到Spring容器,设置load-on-startup = -1 。
  2. 创建MultipartResolver,用于上传文件
  3. 他的配置类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服务器

| #服务器端口号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请求内容最大值,默认2Mserver.tomcat.max-http-form-post-size=2000000#服务器最大连接数server.tomcat.max-connections=8192

application文件配置服务器,现在使用tomcat服务器

SpringMVC配置

| spring.mvc.servlet.path=/course#Servlet的加载顺序,越小创建时间越早spring.mvc.servlet.load-on-startup=0#时间格式,可以在接受请求参数使用spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss

//测试日期参数 @GetMapping("/param/date")@ResponseBody public String paramDate(LocalDateTime date){ return "日期:"+date; }

http://localhost:8001/api/course/param/date?date=2203-02-02 12:10:10

更进一步

| @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.*****.web,依赖Spring Web、Lombok ,JDK19.

5.5.1 Servlets

5.5.1.1 @WebServlet使用Servlet

step1:创建Servlet

| package com.*****.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(); }

}

step2: 扫描Servlet注解

| @ServletComponentScan(basePackages = "com.****.web") @SpringBootApplication public class Lession13ServletFilterApplication {

public static void main(String[] args) { SpringApplication.run(Lession13ServletFilterApplication.class, args); }

}

@ServletComponentScan用于扫描Servlet, Filter ,Listener对象。

step3: 测试

5.5.1.2 ServletRegistrationBean

能够编码方式控制Servlet,不需要注解 step1:创建Servlet,不需要注解

| @Configurationpublic 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.*****.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.*****.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

| @Beanpublic 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

| @Configurationpublic 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

| @Beanpublic 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.*****.mvc。

5.6.1 页面跳转控制器

Spring Boot中使用页面视图,比如Thymeleaf。要跳转显示某个页面,必须通过Controller对象。也就是我们需要创建一个Controller,转发到一个视图才行。 如果我们现在需要显示页面,可以无需这个Controller。addViewControllers() 完成从请求到视图跳转。 需求:访问/welcome 跳转到项目首页index.html(Thyemeleaf创建的对象)

项目代码结构:

step1: 创建视图,resources/templates/index.html

|

Title

项目首页,欢迎各位小伙伴

| | --- |

step2: 创建SpringMVC 配置类

| @Configuration public class MvcSettings implements WebMvcConfigurer { // 跳转视图页面控制器 @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/welcome").setViewName("index"); }

}

step3: 测试功能 浏览器访问 http://localhost:8080/welcome

5.6.2 数据格式化

Formatter是数据转换接口,将一种数据类型转换为另一种数据类型。与Formatter功能类型的还有Converter<S,T>。本节研究Formatter接口。Formatter只能将String类型转为其他数据数据类型。这点在Web应用适用更广。因为Web请求的所有参数都是String,我们需要把String转为Integer ,Long,Date 等等。

Spring中内置了一下Formatter:

  • DateFormatter : String和Date之间的解析与格式化
  • InetAddressFormatter :String和 InetAddress之间的解析与格式化
  • PercentStyleFormatter :String 和Number 之间的解析与格式化,带货币符合
  • NumberFormat :String 和Number 之间的解析与格式化

我在使用@ DateTimeFormat , @NumberFormat 注解时,就是通过Formatter解析String类型到我们期望的Date或Number类型

Formatter也是Spring的扩展点,我们处理特殊格式的请求数据时,能够自定义合适的Formatter,将请求的String数据转为我们的某个对象,使用这个对象更方便我们的后续编码。

接口原型

| public interface Formatter extends Printer, Parser {

}

Formatter是一个组合接口,没有自己的方法。内容来自Printer和Parser Printer:将 T 类型转为String,格式化输出 Parser:将String类型转为期望的T对象。

我们项目开发,可能面对多种类型的项目,复杂程度有简单,有难一些。特别是与硬件打交道的项目,数据的格式与一般的name: lisi, age:20不同。数据可能是一串“1111; 2222; 333,NF; 4; 561” 。 需求:将“1111;2222;333,NF;4;561”接受,代码中用DeviceInfo存储参数值。

step1:创建DeviceInfo数据类

| @Datapublic class DeviceInfo { private String item1; private String item2; private String item3; private String item4; private String item5;

}

step2:自定义Formatter

| public class DeviceFormatter implements Formatter {

//将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

| @Configurationpublic 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接受请求设备数据

| @RestControllerpublic 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")
     &#124;&#124; requestURI.startsWith("/article/edit")
     &#124;&#124; requestURI.startsWith("/article/remove"))) {
  return  false;
}
return true;

}

}

step3:登记拦截器

| @Configurationpublic 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 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

| @Overridepublic 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.*****.upload。

需求:上传文件到服务器

5.7.1 MultipartResolver

step1:服务器创建目录存放上传后的文件 例如在 E:/upload

step2: 创建index.html作为上传后的显示页面 resources/static/index.html

|

项目首页,上传文件成功

| | --- |

step3:创建上传文件页面

|

Title

上传文件

选择文件:
| | --- |

要求:

  • enctype="multipart/form-data"
  • method="post"
  • 表示一个上传文件,upfile 自定义上传文件参数名称

step4:创建Controller

| @Controllerpublic 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=800Bspring.servlet.multipart.max-request-size=5MBspring.servlet.multipart.file-size-threshold=0KB

file-size-threshold超过指定大小,直接写文件到磁盘,不在内存处理。

配置错误页面 resources/static/error/5xx.html

|

Title

上传文件错误

| | --- |

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规范的文件上传

| @Controllerpublic 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 。循环遍历数组解析每个上传的文件。

前端请求页面:

|

上传文件

选择文件1:
选择文件2:
| | --- |

5.8 全局异常处理

在Controller处理请求过程中发生了异常,DispatcherServlet将异常处理委托给异常处理器(处理异常的类)。实现HandlerExceptionResolver接口的都是异常处理类。 项目的异常一般集中处理,定义全局异常处理器。在结合框架提供的注解,诸如:@ExceptionHandler,@ControllerAdvice ,@RestControllerAdvice一起完成异常的处理。 @ControllerAdvice与@RestControllerAdvice区别在于:@RestControllerAdvice加了@RepsonseBody。

创建项目Lession16-ExceptionHandler,Maven构建工具,JDK19。依赖选择 Spring Web, Lombok, Thymeleaf。包名称com*****.eh。

5.8.1 全局异常处理器

需求:应用计算两个数字相除,当用户被除数为0 ,发生异常。使用自定义异常处理器代替默认的异常处理程序。

step1:创建收入数字的页面 在static目录下创建input.html , static目录下的资源浏览器可以直接访问

|

Title
除   数:
被除数:
| | --- |

step2:创建控制器,计算两个整数相除

| @RestControllerpublic class NumberController {

@GetMapping("/divide") public String some(Integer n1,Integer n2){ int result = n1 / n2; return "n1/n2=" + result; }

}

step3:浏览器访问 input.html ,计算两个数字相除 显示默认错误页面

step4:创建自定义异常处理器

| @ControllerAdvicepublic 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

|

Title 错误原因:

| | --- |

在测试显示,提示页面

更进一步

建议在参数签名中尽可能具体异常类,以减少异常类型和原因异常之间不匹配的问题,考虑创建多个@ExceptionHandler方法的,每个方法通过其签名匹配单个特定的异常类型。最后增加一个根异常,考虑没有匹配的其他情况

5.8.2 BeanValidator异常处理

使用JSR-303验证参数时,我们是在Controller方法,声明BindingResult对象获取校验结果。Controller的方法很多,每个方法都加入BindingResult处理检验参数比较繁琐。 校验参数失败抛出异常给框架,异常处理器能够捕获到 MethodArgumentNotValidException,它是BindException的子类。 BindException异常实现了BindingResult接口,异常类能够得到BindingResult对象,进一步获取JSR303校验的异常信息。

需求:全局处理JSR-303校验异常

step1:添加JSR-303依赖

| org.springframework.boot spring-boot-starter-validation

step2:创建Bean对象,属性加入JSR-303注解

| @Datapublic 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接收请求

| @RestControllerpublic class OrderController {

@PostMapping("/order/new") public String createOrder(@Validated @RequestBody OrderVO orderVO){ return orderVO.toString(); }

}

step4:创建异常处理器

| @RestControllerAdvicepublic 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+jsonapplication/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.****** 。

项目Maven依赖

| org.springframework.boot spring-boot-starter-web

org.projectlombok lombok | | --- |

step1:新建图书的Record(普通的POJO类都是可以的)

| public record Book(String isbn,String name,String author) {

}

step2:创建存储多本图书的容器类

| @Setter @Getter @ConfigurationProperties(prefix = "product") public class BookContainer { private List 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 = 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@*****.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@*****.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 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@*****.com"); return problemDetail; }

}

step2:抛出IsbnNotFoundException

| @GetMapping("/book") Book getBook(String isbn) throws Exception { Optional book = bookContainer.getBooks().stream() .filter(el -> el.isbn().equals(isbn)) .findFirst(); if( book.isEmpty() ){ //throw new BookNotFoundException("isbn:"+ isbn + "->没有此图书"); throw new IsbnNotFoundException(HttpStatus.NOT_FOUND,"isbn:"+ isbn + "->没有此图书"); } return book.get();

}

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@******.com"

}
全部评论

相关推荐

点赞 评论 收藏
分享
勤奋努力的椰子这就开摆:美团骑手在美团工作没毛病
投递美团等公司10个岗位
点赞 评论 收藏
分享
评论
点赞
1
分享
牛客网
牛客企业服务