四、RESTFul风格设计和实战
4.1 RESTFul风格概述
4.1.1 RESTFul风格简介
RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量级的通信协议,广泛应用于现代的Web服务开发。
通过遵循 RESTful 架构的设计原则,可以构建出易于理解、可扩展、松耦合和可重用的 Web 服务。RESTful API 的特点是简单、清晰,并且易于使用和理解,它们使用标准的 HTTP 方法和状态码进行通信,不需要额外的协议和中间件。
总而言之,RESTful 是一种基于 HTTP 和标准化的设计原则的软件架构风格,用于设计和实现可靠、可扩展和易于集成的 Web 服务和应用程序!
学习RESTful设计原则可以帮助我们更好去设计HTTP协议的API接口!!
4.1.2 RESTFul风格特点
-
每一个URI代表1种资源(URI 是名词);
-
客户端使用GET、POST、PUT、DELETE 4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
-
资源的表现形式是XML或者JSON;
-
客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。
4.1.3 RESTFul风格设计规范
-
HTTP协议请求方式要求
REST 风格主张在项目设计、开发过程中,具体的操作符合HTTP协议定义的请求方式的语义。
操作 请求方式 查询操作 GET 保存操作 POST 删除操作 DELETE 更新操作 PUT -
URL路径风格要求
REST风格下每个资源都应该有一个唯一的标识符,例如一个 URI(统一资源标识符)或者一个 URL(统一资源定位符)。资源的标识符应该能明确地说明该资源的信息,同时也应该是可被理解和解释的!
使用URL+请求方式确定具体的动作,他也是一种标准的HTTP协议请求!
操作 传统风格 REST 风格 保存 /CRUD/saveEmp URL 地址:/CRUD/emp 请求方式:POST 删除 /CRUD/removeEmp?empId=2 URL 地址:/CRUD/emp/2 请求方式:DELETE 更新 /CRUD/updateEmp URL 地址:/CRUD/emp 请求方式:PUT 查询 /CRUD/editEmp?empId=2 URL 地址:/CRUD/emp/2 请求方式:GET
-
总结
根据接口的具体动作,选择具体的HTTP协议请求方式
路径设计从原来携带动标识,改成名词,对应资源的唯一标识即可!
4.1.4 RESTFul风格好处
-
含蓄,安全
使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。
-
风格统一
URL 地址整体格式统一,从前到后始终都使用斜杠划分各个单词,用简单一致的格式表达语义。
-
无状态
在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。
-
严谨,规范
严格按照 HTTP1.1 协议中定义的请求方式本身的语义进行操作。
-
简洁,优雅
过去做增删改查操作需要设计4个不同的URL,现在一个就够了。
操作 传统风格 REST 风格 保存 /CRUD/saveEmp URL 地址:/CRUD/emp 请求方式:POST 删除 /CRUD/removeEmp?empId=2 URL 地址:/CRUD/emp/2 请求方式:DELETE 更新 /CRUD/updateEmp URL 地址:/CRUD/emp 请求方式:PUT 查询 /CRUD/editEmp?empId=2 URL 地址:/CRUD/emp/2 请求方式:GET -
丰富的语义
通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 URL 地址中用一句话来充分表达语义。
http://localhost:8080/shop http://localhost:8080/shop/product http://localhost:8080/shop/product/cellPhone http://localhost:8080/shop/product/cellPhone/iPhone
4.2 RESTFul风格实战
4.2.1 需求分析
-
数据结构: User {id 唯一标识,name 用户名,age 用户年龄}
-
功能分析
-
用户数据分页展示功能(条件:page 页数 默认1,size 每页数量 默认 10)
-
保存用户功能
-
根据用户id查询用户详情功能
-
根据用户id更新用户数据功能
-
根据用户id删除用户数据功能
-
多条件模糊查询用户功能(条件:keyword 模糊关键字,page 页数 默认1,size 每页数量 默认 10)
-
4.2.2 RESTFul风格接口设计
-
接口设计
功能 接口和请求方式 请求参数 返回值 分页查询 GET /user page=1&size=10 { 响应数据 } 用户添加 POST /user { user 数据 } {响应数据} 用户详情 GET /user/1 路径参数 {响应数据} 用户更新 PUT /user { user 更新数据} {响应数据} 用户删除 DELETE /user/1 路径参数 {响应数据} 条件模糊 GET /user/search page=1&size=10&keywork=关键字 {响应数据} -
问题讨论
为什么查询用户详情,就使用路径传递参数,多条件模糊查询,就使用请求参数传递?
误区:restful风格下,不是所有请求参数都是路径传递!可以使用其他方式传递!
在 RESTful API 的设计中,路径和请求参数和请求体都是用来向服务器传递信息的方式。
-
对于查询用户详情,使用路径传递参数是因为这是一个单一资源的查询,即查询一条用户记录。使用路径参数可以明确指定所请求的资源,便于服务器定位并返回对应的资源,也符合 RESTful 风格的要求。
-
而对于多条件模糊查询,使用请求参数传递参数是因为这是一个资源集合的查询,即查询多条用户记录。使用请求参数可以通过组合不同参数来限制查询结果,路径参数的组合和排列可能会很多,不如使用请求参数更加灵活和简洁。 此外,还有一些通用的原则可以遵循:
-
路径参数应该用于指定资源的唯一标识或者 ID,而请求参数应该用于指定查询条件或者操作参数。
-
请求参数应该限制在 10 个以内,过多的请求参数可能导致接口难以维护和使用。
-
对于敏感信息,最好使用 POST 和请求体来传递参数。
-
4.2.3 后台接口实现
准备用户实体类:
package com.atguigu.pojo;
/**
* projectName: com.atguigu.pojo
* 用户实体类
*/
public class User {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
准备用户Controller:
/**
* projectName: com.atguigu.controller
*
* description: 用户模块的控制器
*/
@RequestMapping("user")
@RestController
public class UserController {
/**
* 模拟分页查询业务接口
*/
@GetMapping
public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
@RequestParam(name = "size",required = false,defaultValue = "10")int size){
System.out.println("page = " + page + ", size = " + size);
System.out.println("分页查询业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户保存业务接口
*/
@PostMapping
public Object saveUser(@RequestBody User user){
System.out.println("user = " + user);
System.out.println("用户保存业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户详情业务接口
*/
@PostMapping("/{id}")
public Object detailUser(@PathVariable Integer id){
System.out.println("id = " + id);
System.out.println("用户详情业务!");
return "{'status':'ok'}";
}
/**
* 模拟用户更新业务接口
*/
@PutMapping
public Object updateUser(@RequestBody User user){
System.out.println("user = " + user);
System.out.println("用户更新业务!");
return "{'status':'ok'}";
}
/**
* 模拟条件分页查询业务接口
*/
@GetMapping("search")
public Object queryPage(@RequestParam(name = "page",required = false,defaultValue = "1")int page,
@RequestParam(name = "size",required = false,defaultValue = "10")int size,
@RequestParam(name = "keyword",required= false)String keyword){
System.out.println("page = " + page + ", size = " + size + ", keyword = " + keyword);
System.out.println("条件分页查询业务!");
return "{'status':'ok'}";
}
}
五、SpringMVC其他扩展
5.1 全局异常处理机制
5.1.1 异常处理两种方式
开发过程中是不可避免地会出现各种异常情况的,例如网络连接异常、数据格式异常、空指针异常等等。异常的出现可能导致程序的运行出现问题,甚至直接导致程序崩溃。因此,在开发过程中,合理处理异常、避免异常产生、以及对异常进行有效的调试是非常重要的。
对于异常的处理,一般分为两种方式:
-
编程式异常处理:是指在代码中显式地编写处理异常的逻辑。它通常涉及到对异常类型的检测及其处理,例如使用 try-catch 块来捕获异常,然后在 catch 块中编写特定的处理代码,或者在 finally 块中执行一些清理操作。在编程式异常处理中,开发人员需要显式地进行异常处理,异常处理代码混杂在业务代码中,导致代码可读性较差。
-
声明式异常处理:则是将异常处理的逻辑从具体的业务逻辑中分离出来,通过配置等方式进行统一的管理和处理。在声明式异常处理中,开发人员只需要为方法或类标注相应的注解(如
@Throws
或@ExceptionHandler
),就可以处理特定类型的异常。相较于编程式异常处理,声明式异常处理可以使代码更加简洁、易于维护和扩展。
站在宏观角度来看待声明式事务处理:
整个项目从架构这个层面设计的异常处理的统一机制和规范。
一个项目中会包含很多个模块,各个模块需要分工完成。如果张三负责的模块按照 A 方案处理异常,李四负责的模块按照 B 方案处理异常......各个模块处理异常的思路、代码、命名细节都不一样,那么就会让整个项目非常混乱。
使用声明式异常处理,可以统一项目处理异常思路,项目更加清晰明了!
5.1.2 基于注解异常声明异常处理
-
声明异常处理控制器类
异常处理控制类,统一定义异常处理handler方法!
/** * projectName: com.atguigu.execptionhandler * * description: 全局异常处理器,内部可以定义异常处理Handler! */ /** * @RestControllerAdvice = @ControllerAdvice + @ResponseBody * @ControllerAdvice 代表当前类的异常处理controller! */ @RestControllerAdvice public class GlobalExceptionHandler { }
-
声明异常处理hander方法
异常处理handler方法和普通的handler方法参数接收和响应都一致!
只不过异常处理handler方法要映射异常,发生对应的异常会调用!
普通的handler方法要使用@RequestMapping注解映射路径,发生对应的路径调用!
/** * 异常处理handler * @ExceptionHandler(HttpMessageNotReadableException.class) * 该注解标记异常处理Handler,并且指定发生异常调用该方法! * * * @param e 获取异常对象! * @return 返回handler处理结果! */ @ExceptionHandler(HttpMessageNotReadableException.class) public Object handlerJsonDateException(HttpMessageNotReadableException e){ return null; } /** * 当发生空指针异常会触发此方法! * @param e * @return */ @ExceptionHandler(NullPointerException.class) public Object handlerNullException(NullPointerException e){ return null; } /** * 所有异常都会触发此方法!但是如果有具体的异常处理Handler! * 具体异常处理Handler优先级更高! * 例如: 发生NullPointerException异常! * 会触发handlerNullException方法,不会触发handlerException方法! * @param e * @return */ @ExceptionHandler(Exception.class) public Object handlerException(Exception e){ return null; }
-
配置文件扫描控制器类配置
确保异常处理控制类被扫描
<!-- 扫描controller对应的包,将handler加入到ioc--> @ComponentScan(basePackages = {"com.atguigu.controller", "com.atguigu.exceptionhandler"})
5.2 拦截器使用
5.2.1 拦截器概念
拦截器和过滤器解决问题
-
生活中
为了提高乘车效率,在乘客进入站台前统一检票
-
程序中
在程序中,使用拦截器在请求到达具体 handler 方法前,统一执行检测
拦截器 Springmvc VS 过滤器 javaWeb:
-
相似点
-
拦截:必须先把请求拦住,才能执行后续操作
-
过滤:拦截器或过滤器存在的意义就是对请求进行统一处理
-
放行:对请求执行了必要操作后,放请求过去,让它访问原本想要访问的资源
-
-
不同点
-
工作平台不同
-
过滤器工作在 Servlet 容器中
-
拦截器工作在 SpringMVC 的基础上
-
-
拦截的范围
-
过滤器:能够拦截到的最大范围是整个 Web 应用
-
拦截器:能够拦截到的最大范围是整个 SpringMVC 负责的请求
-
-
IOC 容器支持
-
过滤器:想得到 IOC 容器需要调用专门的工具方法,是间接的
-
拦截器:它自己就在 IOC 容器中,所以可以直接从 IOC 容器中装配组件,也就是可以直接得到 IOC 容器的支持
-
-
选择:
功能需要如果用 SpringMVC 的拦截器能够实现,就不使用过滤器。
5.2.2 拦截器使用
-
创建拦截器类
public class Process01Interceptor implements HandlerInterceptor { // if( ! preHandler()){return;} // 在处理请求的目标 handler 方法前执行 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("request = " + request + ", response = " + response + ", handler = " + handler); System.out.println("Process01Interceptor.preHandle"); // 返回true:放行 // 返回false:不放行 return true; } // 在目标 handler 方法之后,handler报错不执行! @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", modelAndView = " + modelAndView); System.out.println("Process01Interceptor.postHandle"); } // 渲染视图之后执行(最后),一定执行! @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("request = " + request + ", response = " + response + ", handler = " + handler + ", ex = " + ex); System.out.println("Process01Interceptor.afterCompletion"); } }
-
修改配置类添加拦截器
@EnableWebMvc //json数据处理,必须使用此注解,因为他会加入json处理器 @Configuration @ComponentScan(basePackages = {"com.atguigu.controller","com.atguigu.exceptionhandler"}) //TODO: 进行controller扫描 //WebMvcConfigurer springMvc进行组件配置的规范,配置组件,提供各种方法! 前期可以实现 public class SpringMvcConfig implements WebMvcConfigurer { //配置jsp对应的视图解析器 @Override public void configureViewResolvers(ViewResolverRegistry registry) { //快速配置jsp模板语言对应的 registry.jsp("/WEB-INF/views/",".jsp"); } //开启静态资源处理 <mvc:default-servlet-handler/> @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //将拦截器添加到Springmvc环境,默认拦截所有Springmvc分发的请求 registry.addInterceptor(new Process01Interceptor()); } }
```
-
配置详解
-
默认拦截全部
@Override public void addInterceptors(InterceptorRegistry registry) { //将拦截器添加到Springmvc环境,默认拦截所有Springmvc分发的请求 registry.addInterceptor(new Process01Interceptor()); }
-
精准配置
@Override public void addInterceptors(InterceptorRegistry registry) { //将拦截器添加到Springmvc环境,默认拦截所有Springmvc分发的请求 registry.addInterceptor(new Process01Interceptor()); //精准匹配,设置拦截器处理指定请求 路径可以设置一个或者多个,为项目下路径即可 //addPathPatterns("/common/request/one") 添加拦截路径 //也支持 /* 和 /** 模糊路径。 * 任意一层字符串 ** 任意层 任意字符串 registry.addInterceptor(new Process01Interceptor()).addPathPatterns("/common/request/one","/common/request/tow"); }
-
排除配置
//添加拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //将拦截器添加到Springmvc环境,默认拦截所有Springmvc分发的请求 registry.addInterceptor(new Process01Interceptor()); //精准匹配,设置拦截器处理指定请求 路径可以设置一个或者多个,为项目下路径即可 //addPathPatterns("/common/request/one") 添加拦截路径 registry.addInterceptor(new Process01Interceptor()).addPathPatterns("/common/request/one","/common/request/tow"); //排除匹配,排除应该在匹配的范围内排除 //addPathPatterns("/common/request/one") 添加拦截路径 //excludePathPatterns("/common/request/tow"); 排除路径,排除应该在拦截的范围内 registry.addInterceptor(new Process01Interceptor()) .addPathPatterns("/common/request/one","/common/request/tow") .excludePathPatterns("/common/request/tow"); }
-
-
多个拦截器执行顺序
-
preHandle() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置顺序调用各个 preHandle() 方法。
-
postHandle() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置相反的顺序调用各个 postHandle() 方法。
-
afterCompletion() 方法:SpringMVC 会把所有拦截器收集到一起,然后按照配置相反的顺序调用各个 afterCompletion() 方法。
-
5.3 参数校验
在 Web 应用三层架构体系中,表述层负责接收浏览器提交的数据,业务逻辑层负责数据的处理。为了能够让业务逻辑层基于正确的数据进行处理,我们需要在表述层对数据进行检查,将错误的数据隔绝在业务逻辑层之外。
-
校验概述
JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 标准中。JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证。
注解 规则 @Null 标注值必须为 null @NotNull 标注值不可为 null @AssertTrue 标注值必须为 true @AssertFalse 标注值必须为 false @Min(value) 标注值必须大于或等于 value @Max(value) 标注值必须小于或等于 value @DecimalMin(value) 标注值必须大于或等于 value @DecimalMax(value) 标注值必须小于或等于 value @Size(max,min) 标注值大小必须在 max 和 min 限定的范围内 @Digits(integer,fratction) 标注值值必须是一个数字,且必须在可接受的范围内 @Past 标注值只能用于日期型,且必须是过去的日期 @Future 标注值只能用于日期型,且必须是将来的日期 @Pattern(value) 标注值必须符合指定的正则表达式 JSR 303 只是一套标准,需要提供其实现才可以使用。Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解: 注解 规则 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -------------------- @Email 标注值必须是格式正确的 Email 地址 @Length 标注值字符串大小必须在指定的范围内 @NotEmpty 标注值字符串不能是空字符串 @Range 标注值必须在指定的范围内 Spring 4.0 版本已经拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在SpringMVC 中,可直接通过注解驱动 @EnableWebMvc 的方式进行数据校验。Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在Spring容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean中。Spring本身并没有提供JSR 303的实现,所以必须将JSR 303的实现者的jar包放到类路径下。 配置 @EnableWebMvc后,SpringMVC 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Validated 注解即可让 SpringMVC 在完成数据绑定后执行数据校验的工作。 -
操作演示
-
导入依赖
<!-- 校验注解 --> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>9.1.0</version> <scope>provided</scope> </dependency> <!-- 校验注解实现--> <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.0.Final</version> </dependency> <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator-annotation-processor --> <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator-annotation-processor</artifactId> <version>8.0.0.Final</version> </dependency>
-
应用校验注解
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Min; import org.hibernate.validator.constraints.Length; /** * projectName: com.atguigu.pojo */ public class User { //age 1 <= age < = 150 @Min(10) private int age; //name 3 <= name.length <= 6 @Length(min = 3,max = 10) private String name; //email 邮箱格式 @Email private String email; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
-
handler标记和绑定错误收集
@RestController @RequestMapping("user") public class UserController { /** * @Validated 代表应用校验注解! 必须添加! */ @PostMapping("save") public Object save(@Validated @RequestBody User user, //在实体类参数和 BindingResult 之间不能有任何其他参数, BindingResult可以接受错误信息,避免信息抛出! BindingResult result){ //判断是否有信息绑定错误! 有可以自行处理! if (result.hasErrors()){ System.out.println("错误"); String errorMsg = result.getFieldError().toString(); return errorMsg; } //没有,正常处理业务即可 System.out.println("正常"); return user; } }
-
-
易混总结
@NotNull、@NotEmpty、@NotBlank 都是用于在数据校验中检查字段值是否为空的注解,但是它们的用法和校验规则有所不同。
-
@NotNull (包装类型不为null)
@NotNull 注解是 JSR 303 规范中定义的注解,当被标注的字段值为 null 时,会认为校验失败而抛出异常。该注解不能用于字符串类型的校验,若要对字符串进行校验,应该使用 @NotBlank 或 @NotEmpty 注解。
-
@NotEmpty (集合类型长度大于0)
@NotEmpty 注解同样是 JSR 303 规范中定义的注解,对于 CharSequence、Collection、Map 或者数组对象类型的属性进行校验,校验时会检查该属性是否为 Null 或者 size()==0,如果是的话就会校验失败。但是对于其他类型的属性,该注解无效。需要注意的是只校验空格前后的字符串,如果该字符串中间只有空格,不会被认为是空字符串,校验不会失败。
-
@NotBlank (字符串,不为null,切不为" "字符串)
@NotBlank 注解是 Hibernate Validator 附加的注解,对于字符串类型的属性进行校验,校验时会检查该属性是否为 Null 或 "" 或者只包含空格,如果是的话就会校验失败。需要注意的是,@NotBlank 注解只能用于字符串类型的校验。 总之,这三种注解都是用于校验字段值是否为空的注解,但是其校验规则和用法有所不同。在进行数据校验时,需要根据具体情况选择合适的注解进行校验。
-
六、SpringMVC总结
核心点 | 掌握目标 |
---|---|
springmvc框架 | 主要作用、核心组件、调用流程 |
简化参数接收 | 路径设计、参数接收、请求头接收、cookie接收 |
简化数据响应 | 模板页面、转发和重定向、JSON数据、静态资源 |
restful风格设计 | 主要作用、具体规范、请求方式和请求参数选择 |
功能扩展 | 全局异常处理、拦截器、参数校验注解 |