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的应用分成三步:
- 编写请求页面(在浏览器直接模拟的请求)
- 编写Controller
- 编写视图页面
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
注意:
- 关闭缓存,浏览器清理缓存
- 如果项目有过滤器,拦截器需要放行对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) | 被注释的元素必须符合指定的正则表达式 |
被注释的元素必须是电子邮箱地址 | |
@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
|
视图文件
姓名:年龄:
| | --- |
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,职责:
- 是一个门面,接收请求,控制请求的处理过程。所有请求都必须有DispatcherServlet控制。SpringMVC
对外的入口。可以看做门面设计模式。
- 访问其他的控制器。 这些控制器处理业务逻辑
- 创建合适的视图,将2中得到业务结果放到视图,响应给用户。
- 解耦了其他组件,所有组件只与DispatcherServlet交互。彼此之间没有关联
- 实现ApplictionContextAware, 每个DispatcherServlet都拥自己的WebApplicationContext,它继承了
ApplicationContext。WebApplicationContext包含了Web相关的Bean对象,比如开发人员注释@Controller的类,视图解析器,视图对象等等。 DispatcherServlet访问容器中Bean对象。
- Servlet + Spring IoC 组合
DispatcherServlet继承关系图
5.3.2 Spring MVC的完整请求流程
- 红色DispatherServlet 是框架创建的核心对象(可配置它的属性 contextPath)
- 蓝色的部分框架已经提供多个对象。开发人员可自定义,替换默认的对象。
- 绿色的部分是开发人员自己创建的对象,控制器Conroller和视图对象。
流程说明:
- DispatcherServlet 接收到客户端发送的请求。判断是普通请求,上传文件的请求。
2、DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
3、HandleMapping 根据请求URI 找到对应的控制器以及拦截器,组装成HandlerExecutionChain读写。将此对象 返回给DispatcherServlet,做下一步处理。
- DispatcherServlet 调用HanderAdapter 处理器适配器。这里是适配器设计模式,进行接口转换,将对一个接口
调用转换为其他方法。
-
HandlerAdapter 根据执行控制器方法,也就是开发人员写的Controller类中的方法,并返回一个ModeAndView
-
HandlerAdapter 返回ModeAndView 给DispatcherServlet
-
DispatcherServlet 调用HandlerExceptionResolver处理异常,有异常返回包含异常的ModelAndView
-
DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
-
ViewResolver 解析ModeAndView 并返回真正的View 给DispatcherServlet
-
DispatcherServlet 将得到的视图进行渲染,填充Model中数据到request域
-
返回给客户端响应结果
5.4 SpringMVC自动配置
我们看一下SpringMVC有关的自动配置,Spring MVC自动配置会创建很多对象,重点的有:
- ContentNegotiatingViewResolver和BeanNameViewResolver bean
- 支持提供静态资源,包括对WebJars的支持
- 自动注册Converter、GenericConverter和Formatter bean。
- 对HttpMessageConverters的支持
- 自动注册MessageCodesResolver
- 静态index.html支持。
- 自动使用ConfigurableWebBindingInitializer bean
WebMvcAutoConfiguration是Spring MVC自动配置类,源代码如下:
| @AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered._HIGHEST_PRECEDENCE _+ 10) @ImportRuntimeHints(WebResourcesRuntimeHints.class)public class WebMvcAutoConfiguration { //.....
} |
---|
DispatcherServletAutoConfiguration.class 自动配置DispatcherServlet WebMvcConfigurationSupport.class 配置SpringMVC的组件 ValidationAutoConfiguration.class: 配置JSR-303验证器
@ConditionalOnWebApplication(type = Type.SERVLET) :应用是基于SERVET的web应用时有效 @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }):当项目有Servlet.class, DispatcherServlet.lcass时起作用
5.4.1 DispatcherServletAutoConfiguration.class
web.xml 在SpringMVC以xml文件配置DispatcherServlet,现在有自动配置完成。
| dispatcher org.springframework.web.servlet.DispatcherServlet contextConfigLocation /WEB-INF/spring/dispatcher.xml 1 dispatcher /
DispatcherServletAutoConfiguration自动配置DispatcherServlet。作用:
- 创建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;
} |
---|
- 将DispatchServlet注册成bean,放到Spring容器,设置load-on-startup = -1 。
- 创建MultipartResolver,用于上传文件
- 他的配置类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对象如果要排序,有两种途径:
- 过滤器类名称,按字典顺序排列, AuthFilter - > LogFilter
- FilterRegistrationBean登记Filter,设置order顺序,数值越小,先执行。
step1:创建两个Filter,使用之前的AuthFilter, LogFilter 去掉两个Filter上面的注解
| public class AuthFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String requestURI = ((HttpServletRequest) request).getRequestURI(); System.out.println("AuthFilter代码执行了,uri=" +requestURI ); chain.doFilter(request,response); } }
public class LogFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String requestURI = ((HttpServletRequest) request).getRequestURI(); System.out.println("LogFilter代码执行了,uri=" +requestURI ); chain.doFilter(request,response); }
} |
---|
step2:创建配置类,登记Filter
| @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
|
项目首页,欢迎各位小伙伴
| | --- |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处理都可以使用拦截器。
拦截器定义步骤:
- 声明类实现HandlerInterceptor接口,重写三个方法(需要那个重写那个)
- 登记拦截器
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:登记拦截器
| @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:创建上传文件页面
|
上传文件
| | --- |要求:
- 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
|
上传文件错误
| | --- |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 。循环遍历数组解析每个上传的文件。
前端请求页面:
|
上传文件
| | --- |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目录下的资源浏览器可以直接访问
|
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
|
在测试显示,提示页面
更进一步
建议在参数签名中尽可能具体异常类,以减少异常类型和原因异常之间不匹配的问题,考虑创建多个@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+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.****** 。
项目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"
} |
---|