文章目录
- Spring MVC(中)
- RESTFul风格设计
- RESTFul风格概述
- RESTFul风格特点
- RESTFul风格设计规范
- RESTFul风格好处
- RESTFul风格实战
- 需求分析
- RESTFul风格接口设计
- 后台接口实现
- 基于RESTFul风格练习(前后端分离模式)
- 案例功能和接口分析
- 功能预览
- 接口分析
- 工程项目准备
- 前端项目搭建
- 后端项目搭建
- 后台增删改查实现
- 项目根路径设计
- SpringMVC解决跨域问题
- 业务实现
Spring MVC(中)
RESTFul风格设计
RESTFul风格概述
RESTful(Representational State Transfer)是一种软件架构风格,用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量级的通信协议,广泛应用于现代的Web服务开发。
通过遵循 RESTful 架构的设计原则,可以构建出易于理解、可扩展、松耦合和可重用的 Web 服务。RESTful API 的特点是简单、清晰,并且易于使用和理解,它们使用标准的 HTTP 方法和状态码进行通信,不需要额外的协议和中间件。
RESTful 架构通常用于构建 Web API,提供数据的传输和操作。它可以用于各种应用场景,包括客户端-服务器应用、单页应用(SPA)、移动应用程序和微服务架构等。
总而言之,RESTful 是一种基于 HTTP 和标准化的设计原则的软件架构风格,用于设计和实现可靠、可扩展和易于集成的 Web 服务和应用程序!
学习RESTful设计原则可以帮助我们更好去设计HTTP协议的API接口!!
RESTFul风格特点
- 每一个URI代表1种资源(URI 是名词);
- 客户端使用GET、POST、PUT、DELETE 4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
- 资源的表现形式是XML或者JSON;
- 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。
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协议请求方式
路径设计从原来携带动标识,改成名词,对应资源的唯一标识即可!
RESTFul风格好处
-
含蓄,安全
使用问号键值对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用 REST 风格携带数据不再需要明显的暴露数据的名称。
-
风格统一
URL 地址整体格式统一,从前到后始终都使用斜杠划分各个单词,用简单一致的格式表达语义。
-
无状态
在调用一个接口(访问、操作资源)的时候,可以不用考虑上下文,不用考虑当前状态,极大的降低了系统设计的复杂度。
-
严谨,规范
严格按照 HTTP1.1 协议中定义的请求方式本身的语义进行操作。
-
简洁,优雅
过去做增删改查操作需要设计4个不同的URL,现在一个就够了。
-
丰富的语义
通过 URL 地址就可以知道资源之间的关系。它能够把一句话中的很多单词用斜杠连起来,反过来说就是可以在 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 |
RESTFul风格实战
需求分析
- 数据结构: User {id 唯一标识,name 用户名,age 用户年龄}
- 功能分析
- 用户数据分页展示功能(条件:page 页数 默认1,size 每页数量 默认 10)
- 保存用户功能
- 根据用户id查询用户详情功能
- 根据用户id更新用户数据功能
- 根据用户id删除用户数据功能
- 多条件模糊查询用户功能(条件:keyword 模糊关键字,page 页数 默认1,size 每页数量 默认 10)
RESTFul风格接口设计
接口设计
功能 | 接口和请求方式 | 请求参数 | 返回值 |
---|---|---|---|
分页查询 | GET /user | page=1&size=10 param | { 响应数据 } |
用户添加 | 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 和请求体来传递参数。
后台接口实现
准备用户实体类:
package com.gj.pojo;/*** projectName: com.gj.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;}@Overridepublic String toString() {return "User{" +"id=" + id +", name='" + name + '\'' +", age=" + age +'}';}
}
准备用户Controller:
/*** projectName: com.gj.controller** description: 用户模块的控制器*/
@RequestMapping("user")
@RestController
public class UserController {/*** 模拟分页查询业务接口*/@GetMappingpublic 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'}";}/*** 模拟用户保存业务接口*/@PostMappingpublic 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'}";}/*** 模拟用户更新业务接口*/@PutMappingpublic 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'}";}
}
基于RESTFul风格练习(前后端分离模式)
案例功能和接口分析
功能预览
接口分析
学习计划查询
/*
需求说明查询全部数据页数据
请求urischedule
请求方式 get
响应的json{"code":200,"flag":true,"data":[{id:1,title:'学习java',completed:true},{id:2,title:'学习html',completed:true},{id:3,title:'学习css',completed:true},{id:4,title:'学习js',completed:true},{id:5,title:'学习vue',completed:true}]}
*/
学习计划删除
/*
需求说明根据id删除日程
请求urischedule/{id}
请求方式 delete
响应的json{"code":200,"flag":true,"data":null}
*/
学习计划保存
/*
需求说明增加日程
请求urischedule
请求方式 post
请求体中的JSON{title: '',completed: false}
响应的json{"code":200,"flag":true,"data":null}
*/
学习计划修改
/*
需求说明根据id修改数据
请求urischedule
请求方式 put
请求体中的JSON{id: 1,title: '',completed: false}
响应的json{"code":200,"flag":true,"data":null}
*/
工程项目准备
前端项目搭建
安装node和npm
-
node和npm简介
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,可以使 JavaScript 运行在服务器端。
NPM全称Node Package Manager,是Node.js包管理工具,是全球最大的模块生态系统,里面所有的模块都是开源免费的;也是Node.js的包管理工具!
-
node和npm安装
安装Nodejs,自动安装npm包管理工具!
-
打开官网https://nodejs.org/en下载对应操作系统的 LTS 版本。(资料中已有node安装包)
-
双击安装包进行安装,安装过程中遵循默认选项即可(或者参照https://www.runoob.com/nodejs/nodejs-install-setup.html )。安装完成后,可以在命令行终端输入
node -v
和npm -v
查看 Node.js 和 npm 的版本号。
-
-
npm镜像和版本升级
- 配置阿里镜像
npm config set registry https://registry.npmmirror.com
- 查看镜像配置结果
npm config get registry
- 升级npm版本
npm install -g npm@9.6.6
-
使用npm安装项目依赖
使用cmd黑窗口,进入前端工程项目文件夹下(切记:进入到package.json同层文件夹下,运行一下命令)
npm i
-
启动前端程序
npm run dev
后端项目搭建
数据库怎么办?使用HashMap模拟,所以不涉及和MyBatis、Spring的整合!
搭建后台项目
pom.xml
<properties><spring.version>6.0.6</spring.version><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties><dependencies><!-- springioc相关依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version></dependency><!-- web相关依赖 --><!-- 在 pom.xml 中引入 Jakarta EE Web API 的依赖 --><!--在 Spring Web MVC 6 中,Servlet API 迁移到了 Jakarta EE API,因此在配置 DispatcherServlet 时需要使用Jakarta EE 提供的相应类库和命名空间。错误信息 “‘org.springframework.web.servlet.DispatcherServlet’is not assignable to ‘javax.servlet.Servlet,jakarta.servlet.Servlet’” 表明你使用了旧版本的Servlet API,没有更新到 Jakarta EE 规范。--><dependency><groupId>jakarta.platform</groupId><artifactId>jakarta.jakartaee-web-api</artifactId><version>9.1.0</version><scope>provided</scope></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.15.0</version></dependency><!-- springwebmvc相关依赖 --><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>${spring.version}</version></dependency></dependencies>
准备实体类
/*** projectName: com.gj.pojo** description: 任务实体类*/
public class Schedule {private Integer id;private String title;private Boolean completed;public Schedule() {}public Schedule(Integer id, String title, Boolean completed) {this.id = id;this.title = title;this.completed = completed;}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public Boolean getCompleted() {return completed;}public void setCompleted(Boolean completed) {this.completed = completed;}@Overridepublic String toString() {return "Schedule{" +"id=" + id +", title='" + title + '\'' +", completed=" + completed +'}';}
}
准备R结果包装类
/*** projectName: com.gj.utils** description: 返回结果类*/
public class R {private int code = 200; //200成功状态码private boolean flag = true; //返回状态private Object data; //返回具体数据public static R ok(Object data){R r = new R();r.data = data;return r;}public static R fail(Object data){R r = new R();r.code = 500; //错误码r.flag = false; //错误状态r.data = data;return r;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public boolean isFlag() {return flag;}public void setFlag(boolean flag) {this.flag = flag;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}
}
准备业务类
业务接口
/*** projectName: com.gj.service** description: schedule业务接口*/public interface ScheduleService {/*** 返回全部学习计划* @return*/List<Schedule> getAll();/*** 保存学习计划* @param schedule*/void saveSchedule(Schedule schedule);/*** 更新学习计划* @param schedule*/void updateSchedule(Schedule schedule);/*** 移除学习计划* @param*/void removeById(Integer id);}
业务实现
/*** projectName: com.gj.service.impl** description:*/
@Service
public class ScheduleServiceImpl implements ScheduleService {//准备假数据private static Map<Integer,Schedule> scheduleMap;private static int maxId = 5;static {scheduleMap = new HashMap<>();Schedule schedule = null;schedule = new Schedule(1, "学习Java", false);scheduleMap.put(1, schedule);schedule = new Schedule(2, "学习H5", true);scheduleMap.put(2, schedule);schedule = new Schedule(3, "学习Css", false);scheduleMap.put(3, schedule);schedule = new Schedule(4, "学习JavaScript", false);scheduleMap.put(4, schedule);schedule = new Schedule(5, "学习Spring", true);scheduleMap.put(5, schedule);} /*** 返回全部学习计划** @return*/@Overridepublic List<Schedule> getAll() {return new ArrayList<>(scheduleMap.values());}/*** 保存学习计划** @param schedule*/@Overridepublic void saveSchedule(Schedule schedule) {maxId++;schedule.setId(maxId);scheduleMap.put(maxId,schedule);}/*** 更新学习计划** @param schedule*/@Overridepublic void updateSchedule(Schedule schedule) {scheduleMap.put(schedule.getId(),schedule);}/*** 移除学习计划** @param id*/@Overridepublic void removeById(Integer id) {scheduleMap.remove(id);}}
准备spring-mvc.配置文件
位置:resources/spring-mvc.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:mvc="http://www.springframework.org/schema/mvc"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"><!-- 扫描controller对应的包,将handler加入到ioc--><context:component-scan base-package="com.gj.controller,com.gj.service" /><!--注意: 导入mvc命名空间!mvc:annotation-driven 是一个整合标签他会导入handlerMapping和handlerAdapter他会导入json数据格式转化器等等!--><mvc:annotation-driven /><!-- viewResolver 不需要配置,因为我们不需要查找逻辑视图!!! --><!-- 加入这个配置,SpringMVC 就会在遇到没有 @RequestMapping 的请求时放它过去 --><!-- 所谓放它过去就是让这个请求去找它原本要访问的资源 --><mvc:default-servlet-handler/>
</beans>
准备 web.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 --><servlet><servlet-name>DispatcherServlet</servlet-name><!-- DispatcherServlet的全类名 --><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!-- 通过初始化参数指定SpringMVC配置文件位置 --><init-param><!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 --><param-name>contextConfigLocation</param-name><!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 --><param-value>classpath:spring-mvc.xml</param-value></init-param><!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 --><!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 --><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>DispatcherServlet</servlet-name><!-- 对DispatcherServlet来说,url-pattern有两种方式配置 --><!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 --><!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 --><url-pattern>/</url-pattern></servlet-mapping></web-app>
后台增删改查实现
项目根路径设计
因为前端项目设置了后台访问的项目根路径为 /rest
我们后台项目也对应的设置:
SpringMVC解决跨域问题
假设我们有一个网站 http://example.com
,现在需要跨域请求另外一个网站 http://api.example.com
中的数据。浏览器就会因为安全问题,拒绝客户端访问请求!
跨域问题是指在浏览器中发起跨域请求被浏览器拦截的问题。在同一个源域(同一协议、主机、端口),浏览器允许 JavaScript 发起跨域请求;在不同的源域下,浏览器对发起的异域请求会做出不同的限制。
常见的跨域问题的场景有:
- 访问不同的子域名;
- 访问不同的端口号;
- 访问不同的协议(http、https);
- 访问不同的域名;
基于CORS方式,解决跨域思路:
CORS(Cross-Origin Resource Sharing)是 W3C 制定的一种跨域解决方案,它给出了跨域请求和响应的标准。服务器端代码需要在响应头中设置 Access-Control-Allow-Origin,并指定访问来源域名名或 * 通配符,表示允许的跨域请求。浏览器可以根据响应头信息,判断是否允许该请求。
SpringMVC基于CORS思路解决跨域方案:
-
@CrossOrigin注解
@CrossOrigin
注释在带注释的【控制器方法】 / 【控制器类】上启用跨源请求@RestController @RequestMapping("/account") public class AccountController {@CrossOrigin@GetMapping("/{id}")public Account retrieve(@PathVariable Long id) {// ...}@DeleteMapping("/{id}")public void remove(@PathVariable Long id) {// ...} }
默认情况下,
@CrossOrigin
允许:- All origins.
- All headers.
- All HTTP methods to which the controller method is mapped.
注解核心设置属性讲解:
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface CrossOrigin {/*** 设置哪些客户端地址可以跨域访问! 格式为: 协议://主机地址:端口号* @return*/@AliasFor("origins")String[] value() default {};@AliasFor("value")String[] origins() default {};/*** 设置哪些客户端的[自定义请求头]可以跨域访问!*/String[] allowedHeaders() default {};/*** 设置哪些服务端的自定义响应头,可以被客户端读取!*/String[] exposedHeaders() default {};/***设置哪些请求方法,可以跨域方式! */RequestMethod[] methods() default {};/*** 值为 true 或者 false* 客户端是否可以携带cookie!*/String allowCredentials() default ""; }
-
xml全局跨域配置
<mvc:cors><mvc:mapping path="/**"allowed-origins="*"allowed-methods="GET, PUT"allowed-headers="header1, header2, header3"exposed-headers="header1, header2" allow-credentials="true"/><mvc:mapping path="/**"allowed-origins="https://domain1.com" /></mvc:cors>
业务实现
查询业务
/*** projectName: com.gj.controller** description: 学习计划controller*/
@CrossOrigin
/*@CrossOrigin 注释在带注释的控制器方法上启用跨源请求默认情况下, @CrossOrigin 允许:All origins 任何请求主机地址All headers 任何请求头All HTTP methods to which the controller method is mapped. 任何请求方式!可以设置:@CrossOrigin(origins = "https://domain2.com") 指定允许跨域请求的主机地址*/
@RequestMapping("schedule")
@RestController
public class ScheduleController
{@Autowiredprivate ScheduleService scheduleService;@GetMappingpublic R showList(){List<Schedule> list = scheduleService.getAll();return R.ok(list);}
}
修改业务
@PutMapping
public R changeSchedule(@RequestBody Schedule schedule){scheduleService.updateSchedule(schedule);return R.ok(null);
}
删除业务
@DeleteMapping("/{id}")
public R removeSchedule(@PathVariable Integer id){scheduleService.removeById(id);return R.ok(null);
}
保存业务
@PostMapping
public R saveSchedule(@RequestBody Schedule schedule){scheduleService.saveSchedule(schedule);return R.ok(null);
}