Spring Web MVC(详解中)
文章目录
- 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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
准备用户Controller:
/**
* projectName: com.gj.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'}";
}
}
基于RESTFul风格练习(前后端分离模式)
案例功能和接口分析
功能预览
接口分析
学习计划查询
/*
需求说明
查询全部数据页数据
请求uri
schedule
请求方式
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删除日程
请求uri
schedule/{id}
请求方式
delete
响应的json
{
"code":200,
"flag":true,
"data":null
}
*/
学习计划保存
/*
需求说明
增加日程
请求uri
schedule
请求方式
post
请求体中的JSON
{
title: '',
completed: false
}
响应的json
{
"code":200,
"flag":true,
"data":null
}
*/
学习计划修改
/*
需求说明
根据id修改数据
请求uri
schedule
请求方式
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;
}
@Override
public 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
*/
@Override
public List<Schedule> getAll() {
return new ArrayList<>(scheduleMap.values());
}
/**
* 保存学习计划
*
* @param schedule
*/
@Override
public void saveSchedule(Schedule schedule) {
maxId++;
schedule.setId(maxId);
scheduleMap.put(maxId,schedule);
}
/**
* 更新学习计划
*
* @param schedule
*/
@Override
public void updateSchedule(Schedule schedule) {
scheduleMap.put(schedule.getId(),schedule);
}
/**
* 移除学习计划
*
* @param id
*/
@Override
public 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
{
@Autowired
private ScheduleService scheduleService;
@GetMapping
public 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);
}