SSM速通3
五、SpringMVC
1、web应用架构模式
1.1、前后端不分离
在前后端不分离的模式下,前端页面的生成和后端业务逻辑的处理是在同一个项目中完成的。后端代码会直接生成HTML页面,并将页面发送给浏览器。
特点:
- 耦合性强:前端代码(HTML/CSS/JS)和后端代码(如Java/PHP/Python)混合在同一个项目中,通常由后端框架(如Django、Spring MVC、Laravel)直接渲染页面。
- 服务端渲染(SSR):后端生成完整的HTML页面并返回给浏览器,前端仅负责展示,逻辑处理(如路由、数据获取)由后端控制。
- 典型技术:JSP、Thymeleaf、PHP模板引擎(如Blade)、Django模板等。
优点:
- 开发简单:适合小型项目,前后端协作直接。
- SEO友好:服务端渲染的HTML内容容易被搜索引擎抓取。
缺点:
- 维护困难:前后端代码混杂,修改需求时可能互相影响。
- 性能瓶颈:页面每次请求需后端重新渲染,增加服务器负载。
- 扩展性差:难以适应多端(如移动端、桌面端)需求。
1.2、前后端分离
前后端分离是一种现代的软件开发架构模式。在这种模式下,前端和后端是两个独立的项目。
- 前端:主要负责用户界面(UI)和用户体验(UX)。它使用HTML、CSS和JavaScript等技术构建,运行在浏览器端。例如,一个电商网站的前端页面,包括商品展示页面、购物车页面、用户登录注册页面等,这些页面的布局、样式和交互逻辑都由前端代码来实现。
- 后端:主要负责处理业务逻辑、数据存储和数据交互等。后端通常运行在服务器上,使用如Java、Python、Node.js等语言开发。它通过API(应用程序编程接口)与前端进行通信。例如,当用户在前端页面点击"提交订单"按钮时,前端会通过API向后端发送订单信息,后端接收到订单信息后,会处理订单的存储、库存更新等业务逻辑。
特点:
- 职责解耦:
- 前端:独立工程(如React/Vue/Angular),负责UI渲染、交互和路由,通过API与后端通信。
- 后端:仅提供数据接口(RESTful API/GraphQL),返回JSON/XML格式数据,不涉及UI逻辑。
- 客户端渲染(CSR):浏览器加载前端框架后,通过Ajax/Fetch动态获取数据并渲染页面。
优点:
- 灵活性高:前后端可独立开发、测试和部署,适合敏捷开发。
- 多端兼容:同一套API可服务Web、移动App、桌面应用等。
- 性能优化:前端可缓存数据,减少服务器压力。
缺点:
- SEO挑战:CSR页面初始HTML为空,需额外技术(如SSR、预渲染)优化。
- 复杂度提升:需管理跨域、接口文档、状态管理(如Redux/Vuex)等问题。
1.3、前后端分离与不分离对比
| 对比维度 | 前后端分离 | 前后端不分离 |
|---|---|---|
| 开发模式 | 前端、后端独立开发,通过接口对接 | 前端、后端耦合在一个项目中 |
| 技术栈 | 前后端技术栈独立,可自由选择 | 前后端技术栈统一,难以独立更换 |
| 开发效率 | 大型项目中并行开发,效率高 | 小型项目快,大型项目容易混乱 |
| 部署方式 | 前后端分开部署,前端可放CDN | 一起部署,通常后端渲染页面 |
| 通信方式 | 通过API(如REST、GraphQL)通信 | 后端直接渲染页面,通信成本低 |
| 性能 | 首次加载可能慢,后续交互快 | 每次请求可能都重新加载页面 |
| 可维护性 | 结构清晰,易维护 | 代码耦合高,维护困难 |
| 团队协作 | 前后端团队可独立协作 | 需要更紧密的协作,容易互相影响 |
| 适用场景 | 中大型项目、移动端+Web多端支持 | 小型项目、传统Web站点 |
总结一下:
- 前后端不分离是传统模式,当前端发出请求,后端返回的是带数据的完整渲染过的页面
- 前后端分离是现代常用模式,当前端发出请求,后端只返回数据
1.4、补充:序列化和反序列化
序列化(Serialization)和反序列化(Deserialization) 是计算机中用于数据存储、传输和交换的核心概念,主要解决 对象与字节流之间的转换问题。
①、定义
-
序列化 :将 内存中的对象 转换为可存储或传输的 字节流(如JSON、二进制等)。
-
反序列化 :将 字节流 还原为内存中的 对象。
②、核心作用
- 持久化存储:将对象保存到文件或数据库(如Redis缓存)。
- 网络传输:跨进程/网络传递数据(如HTTP API、RPC调用)。
- 深拷贝:通过序列化实现对象的深度复制。
③、常见序列化格式
| 格式 | 特点 | 应用场景 |
|---|---|---|
| JSON | 文本、易读、跨语言支持 | Web API、配置文件 |
| XML | 标签结构、冗长 | 旧式SOAP协议、企业系统 |
| Protocol Buffers | 二进制、高效、需预定义Schema | 微服务通信(gRPC) |
| MessagePack | 二进制、比JSON更紧凑 | 高性能数据传输 |
| Pickle | Python专用、支持复杂对象 | Python进程间通信 |
简单理解:
- 对象------>字符串:序列化,前端的对象会序列化为字符串传回给后端
- 字符串------>对象:反序列化,后端的数据在传到前端会被反序列化为对象
而这其中的字符串最常用的就是JSON格式
2、Controller层的注解、请求、响应
要进行controller层的使用,即请求方面的使用,必须用 spring-boot-starter-web :因为Controller注解的实现依赖Spring MVC,而该模块由 spring-boot-starter-web 提供。
2.1、URL路由匹配通配符
| 通配符 | 作用 | 示例 | 匹配路径 |
|---|---|---|---|
* |
单层路径任意字符 | /api/*/info |
/api/user/info, /api/order/info |
** |
多层路径任意字符(含子路径) | /static/** |
/static/css/style.css, /static/js/app.js |
? |
匹配单个字符 | /file-?.txt |
/file-a.txt, /file-1.txt |
{var} |
路径变量(需框架支持) | /user/{id} |
/user/123 → id=123 |
注意:当使用**时,必须放在最后,表示其后包含任意多层路径字符
当多个匹配符均可对一个地址进行匹配,则按照匹配的精确度为顺序:完全匹配 >? > * > **
Spring MVC示例:
java
@GetMapping("/files/*.txt") // 匹配/files/abc.txt,不匹配/files/sub/abc.txt
public String getFile() { ... }
@GetMapping("/resources/**") // 匹配/resources/...任意子路径
public String serveResource() { ... }
@RequestMapping("/jjpp")
public String test1() { ... }
@RequestMapping("/jjpp?")
public String test1() { ... }
@RequestMapping("/jjpp*")
public String test1() { ... }
@RequestMapping("/jjpp/*")
public String test1() { ... }
@RequestMapping("/jjpp/**")
public String test1() { ... }
// 默认8080端口没有被占用
// 当请求网址路径为 localhost:8080/jjpp 时执行 test1
// 当请求网址路径为 localhost:8080/jjppa 时执行 test2
// 当请求网址路径为 localhost:8080/jjpp/a 时执行 test3
// 当请求网址路径为 localhost:8080/jjpp/a/b 时执行 test4
2.2、核心注解
① 、类级别注解
| 注解 | 作用 |
|---|---|
@Controller |
定义该类为控制器,返回视图名称(配合模板引擎如Thymeleaf)。 |
@RestController |
@Controller + @ResponseBody,直接返回JSON/XML数据(RESTful API)。 |
@RequestMapping |
通用请求映射,可定义类的基础路径(如/api)。 |
示例:
java
@RestController
@RequestMapping("/api/users")
// 之后在网页请求 localhost:8080/api/users 网址即可实现以下类中的方法,相当于模块化分组
// 即以下类的基础路径是 /api/users
// 当然如果你的8080端口被占用了,可以在application.properties中修改请求端口即可
public class UserController {
// 方法级注解...
@RequestMapping("/test1") // 在以上的基础路径之上再添加 /test1 即可使用 method1 方法
public void method1(){...}
}
// 以下是上面代码的另一种表示,二者等同
@Controller
@RequestMapping("/api/users")
public class UserController {
// 方法级注解...
@@ResponseBody // 将方法返回值直接写入HTTP响应体
@RequestMapping("/test1")
public void method1(){...}
}
②、方法级别注解(HTTP方法映射)
方法级别的注解其对应的是请求的方式,对应的注解必须要和请求的方法对应否则会请求失败
| 注解 | 等效简写 | 说明 |
|---|---|---|
@GetMapping |
@RequestMapping(method=GET) |
处理GET请求(查询数据)。 |
@PostMapping |
@RequestMapping(method=POST) |
处理POST请求(新增数据)。 |
@PutMapping |
@RequestMapping(method=PUT) |
处理PUT请求(全量更新数据)。 |
@PatchMapping |
@RequestMapping(method=PATCH) |
处理PATCH请求(部分更新数据)。 |
@DeleteMapping |
@RequestMapping(method=DELETE) |
处理DELETE请求(删除数据)。 |
@RequestMapping |
通用请求映射,可定方法的路径(如/api)。 |
可见@RequestMapping既可以作为类级别的注解,也可以作为方法级别的注解,当其作为类级别的注解,代表的是以下类的基础路径,当其作为方法级别的注解,代表的是以下方法的额外的请求路径(需在原有的基础路径上再添加额外路径)
示例:
java
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.save(user);
}
③、参数绑定注解
| 注解 | 作用 |
|---|---|
@PathVariable |
从URL路径中获取变量(如/users/{id})。 |
@RequestParam |
从URL查询参数中获取值(如/users?name=Alice),可设置默认值/非必需。 |
@RequestBody |
将请求体(如JSON)绑定到对象(通常用于POST/PUT)。 |
@RequestHeader |
获取HTTP请求头中的值。 |
@CookieValue |
获取Cookie中的值。 |
@ModelAttribute |
绑定表单数据或查询参数到对象(常用于传统Web开发)。 |
示例:
java
@GetMapping("/search")
public List<User> searchUsers(
@RequestParam(required = false, defaultValue = "") String keyword,
@RequestHeader("User-Agent") String userAgent) {
return userService.search(keyword);
}
④、响应处理注解
| 注解 | 作用 |
|---|---|
@ResponseBody |
将方法返回值直接写入HTTP响应体(如返回JSON)。 |
@ResponseStatus |
自定义HTTP响应状态码(如@ResponseStatus(HttpStatus.CREATED))。 |
@CrossOrigin |
允许跨域请求(可指定来源、方法等)。 |
示例:
java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@RequestBody User user) {
return userService.save(user);
}
@CrossOrigin(origins = "https://example.com")
@GetMapping("/public")
public String publicData() {
return "允许跨域访问的数据";
}
⑤、其他实用注解
| 注解 | 作用 |
|---|---|
@Valid / @Validated |
触发参数校验(配合JSR-303注解如@NotNull)。 |
@ExceptionHandler |
处理控制器内的特定异常(局部异常处理)。 |
@InitBinder |
自定义请求参数绑定逻辑(如日期格式转换)。 |
示例:
java
@PostMapping
public User createUser(@Valid @RequestBody User user) {
// 自动校验User对象的@NotNull等注解
return userService.save(user);
}
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
return new ErrorResponse(ex.getMessage());
}
⑥、组合注解(Spring Boot)
| 注解 | 作用 |
|---|---|
@RestControllerAdvice |
全局异常处理 + 响应封装(结合@ExceptionHandler)。 @ExceptionHandler)。 |
@RequestMapping + @ResponseBody |
早期RESTful写法(现多用@RestController替代)。 |
- 基础映射 :
@Controller、@RestController、@RequestMapping及其简化版(如@GetMapping)。 - 参数处理 :
@PathVariable、@RequestParam、@RequestBody。 - 高级功能 :校验(
@Valid)、异常处理(@ExceptionHandler)、跨域(@CrossOrigin跨域(@CrossOrigin)。
2.3、请求限定
在Spring框架中,请求限定(Request Mapping Constraints)是指通过注解或配置对HTTP请求的访问条件进行精细化控制,例如限制请求方法、参数、头部等。
①、HTTP方法限定
方法限定包含两个方法,二者是等效的,请求方法和方法级别的注解一一对应
Ⅰ、方法级别注解
Ⅱ、@RequestMapping中的method
java
@PostMapping("/create") // 仅允许POST请求
public String create() { ... }
// 等效的@RequestMapping写法,即以下写法等同于上方的@PostMapping,仅允许POST请求
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String create() { ... }
@GetMapping("/create") // 仅允许Get请求
public String create() { ... }
// 等效的@RequestMapping写法,即以下写法等同于上方的@GetMapping,仅允许Get请求
@RequestMapping(value = "/create", method = RequestMethod.Get)
public String create() { ... }
②、请求参数限定
Ⅰ、必传参数
要求请求必须包含指定参数(params):
java
@GetMapping(path = "/user", params = "id") // 必须带?id=xxx,例:/user?id=666
public String getUser(@RequestParam String id) { ... }
// 多参数条件
@GetMapping(path = "/search", params = {"id","keyword=123","page!=1"})
// 必须带?id=xxx&keyword=123&page不等于1或者直接不写page都可以
// 例:/search?id=1&keyword=123&page=0 或 /search?id=1&keyword=123
public String search(@RequestParam String keyword, @RequestParam int page) { ... }
Ⅱ、参数值匹配
限定参数值必须符合特定规则:
java
// 要求type参数值必须为"admin"或"guest"
@GetMapping(path = "/access", params = "type=admin || type=guest")
public String checkAccess() { ... }
③、请求头限定
Ⅰ、必传请求头
要求请求必须包含指定头部(headers):
java
@PostMapping(path = "/auth", headers = "X-Token") // 必须携带X-Token头部,X-Token后的内容任意
public String auth() { ... }
// 多头部条件
@GetMapping(headers = {"Content-Type=application/json", "Accept-Language=en-US"})
// 严格匹配请求头中是否显式包含Content-Type: application/json,必须携带Accept-Language且其后内容为en-US
public String getResource() { ... }
Ⅱ、内容类型限定
限制请求的Content-Type或Accept:
java
@PostMapping(path = "/submit", consumes = "application/json") // 只接收JSON请求体
public String submit(@RequestBody User user) { ... }
@GetMapping(path = "/data", produces = "application/json") // 只返回JSON响应
public User getData() { ... }
④、路径变量正则限定
通过正则表达式约束路径变量的格式:
java
// id必须是数字
@GetMapping("/user/{id:\\d+}")
public String getUserById(@PathVariable String id) { ... }
// 更复杂的正则(如日期格式)
@GetMapping("/report/{date:\\d{4}-\\d{2}-\\d{2}}")
public String getReportByDate(@PathVariable String date) { ... }
⑤、组合条件示例
综合使用多种限定条件:
java
@RestController
@RequestMapping("/api")
public class AdvancedController {
// 条件:POST + JSON内容类型 + 必须带X-API-Key头部 + 参数action=update
@PostMapping(
path = "/operation",
consumes = "application/json",
headers = "X-API-Key",
params = "action=update"
)
public String complexOperation(@RequestBody Data data) {
return "Operation Success";
}
}
⑥、动态条件(编程式限定)
通过@RequestMapping + @Conditional或拦截器实现更灵活的控制:
java
@GetMapping("/dynamic")
public String dynamicPath(HttpServletRequest request) {
if (!request.getHeader("User-Agent").contains("Chrome")) {
throw new UnsupportedOperationException("Only Chrome is allowed");
}
return "Dynamic Access";
}
可用来发送请求的常用Postman,简易的请求可直接使用idea自带的http 客户端

2.4、http
HTTP(HyperText Transfer Protocol,超文本传输协议)是用于传输超媒体文档(如 HTML)的应用层协议,它是万维网(WWW)数据通信的基础。以下是 HTTP 请求的核心概念和组成部分:
①、HTTP 请求的基本结构
一个 HTTP 请求由以下部分组成:
-
请求行(Request Line) 包含:
方法+URL+HTTP版本示例:GET /index.html HTTP/1.1
而其中的URL携带大量数据,包含你的传输协议、主机/域名、Port端口、请求的Path路径、查询参数、片段锚点(配合前端使用,一般不发给服务器)等
http协议端口不写,其端口默认是80端口
https协议不写,其默认端口是443
-
请求头(Headers) 键值对形式,传递额外信息(如客户端类型、支持的编码等)。 示例:
Host: www.example.com User-Agent: Mozilla/5.0 Accept: text/html -
空行分隔头部和请求体。
-
**请求体(Body,可选)**POST、PUT 等方法需要发送数据时使用。示例(JSON 数据):
{"username": "test", "password": "123"}
在请求体中传送数据相比于在请求行的URL中直接传输其安全性有了很大提升,打个比方:在URL中传输相当于直接将信息写在信封上,在请求体中进行数据的传输相当于写在信纸上再包装好放进信封里
②、常见 HTTP 方法
| 方法 | 描述 | 是否包含请求体 |
|---|---|---|
GET |
获取资源(如网页、图片) | 通常无 |
POST |
提交数据(如表单、文件上传) | 有 |
PUT |
更新服务器上的资源(全量替换) | 有 |
DELETE |
删除资源 | 通常无 |
PATCH |
部分更新资源 | 有 |
HEAD |
只获取响应头,不返回响应体(用于检查资源) | 无 |
③、常见 HTTP 请求头
| 头部字段 | 作用 |
|---|---|
Host |
目标服务器的域名(HTTP/1.1 必需) |
User-Agent |
客户端标识(如浏览器、爬虫) |
Content-Type |
请求体的格式(如 application/json) |
Accept |
客户端能处理的响应类型(如 text/html) |
Authorization |
认证信息(如 Bearer token) |
Cookie |
发送服务器设置的 Cookie |
2.5、请求处理(实验练习)
①、实验一
java
// 实验1:使用普通变量接收请求参数
@Controller
public class HelloController {
@RequestMapping("/test1")
public String test1(
String username,
String password,
String phone,
boolean agreement){
System.out.println(username);
System.out.println(password);
System.out.println(phone);
System.out.println(agreement);
return "hello ";
}
}
// 之后访问:http://localhost:8080/test1?username=zhangsan&password=123&phone=123456&agreement=true
// 观察是否会打印你的访问路径中的请求参数

注意在使用普通变量接收请求参数时,我们的普通变量名必须和请求参数的变量名相同,否则接收不到
②、实验二
java
// 实验1:使用@RequestParam接收请求参数
@Controller
public class HelloController {
@RequestMapping("/test2")
public String test2(
// @RequestParam 的参数包含
@RequestParam("username") String name, // 相当于将 username 获取到的请求参数赋给 name
// 将 password 获取到的请求参数赋给 pwd,同时如果请求参数中为设置此变量的值,默认是123456
@RequestParam(name = "password", defaultValue = "123456") String pwd,
// 将 phone 获取到的请求参数赋给 phonenumber,同时允许参数不存在(此时参数值为 null)
@RequestParam(name = "phone", required = false) String phonenumber,
// 如果没有额外设置 required 的值,则默认是 true,默认是必须传值
@RequestParam("agreement") boolean okornot){
System.out.println(name);
System.out.println(pwd);
System.out.println(phonenumber);
System.out.println(okornot);
return "hello ";
}
}
// 之后访问:http://localhost:8080/test2?username=hhhhh&agreement=false
// 观察是否会打印你的访问路径中的请求参数

@RequestParam 支持多个属性,用于控制参数的行为:
| 属性 | 作用 | 示例 |
|---|---|---|
name / value |
指定请求参数的名称 | @RequestParam("id") |
required |
是否必须(默认 true) |
@RequestParam(required = false) |
defaultValue |
默认值(当参数未传时生效) |
@RequestParam 无论你的请求参数是在请求体中还是请求头的url?之后都可以通过此注解获取
③、实验三
java
@Controller // 定义此层为 controller 层
public class HelloController {
@RequestMapping("/test3")
public String test3(Person person) {
System.out.println(person);
return "hello";
}
}
java
public class Person {
// 通过该 Person 封装类,简化请求中的参数调用
// 注意其中的参数必须要和请求参数名相同
// 不可使用 @RequestParam,该注解是只能在参数中使用
private String username;
private String password = "123456"; // 要想实现默认值效果直接为其中属性赋值即可
private String phone;
private boolean agreement;
// 注意:这里我使用手写get和set方法是因为我的Lombok的@Data不知道为啥失效了,如果没有失效的直接@Data即可
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getPhone() {
return phone;
}
public boolean isAgreement() {
return agreement;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setAgreement(boolean agreement) {
this.agreement = agreement;
}
@Override
public String toString() {
return "Person [username=" + this.getUsername() + ", password=" + this.getPassword()+ ", phone=" + this.getPhone()+ ", agreement=" + this.isAgreement()+ "]";
}
}

此实验就是通过将我们的多个参数封装在一个类中以方便使用,该方法就是PoJo封装
当然我们类中可以使用另一个封装类,比如你定义一个Person类,其中有个属性是Address,这个Address是你定义的另一个类,如此套娃就是级联封装
④、实验四
java
@Controller // 定义此层为 controller 层
public class HelloController {
@RequestMapping("/test4")
public String test4(
@RequestHeader("host") String host,
@RequestHeader("User-Agent") String ua) {
System.out.println(host);
System.out.println(ua);
return "hello";
}
}


当然@RequestHeader 支持多个属性,用于控制参数的行为:
| 属性 | 作用 | 示例 |
|---|---|---|
name / value |
指定请求参数的名称 | @RequestHeader("id") |
required |
是否必须(默认 true) |
@RequestHeader(required = false) |
defaultValue |
默认值(当参数未传时生效) |
通过这个方法可以获取请求头的信息
⑤、实验五
java
@Controller // 定义此层为 controller 层
public class HelloController {
@RequestMapping("/test5")
public String test5(@CookieValue("t") String tt) {
System.out.println(tt);
return "hello";
}
}

通过该注解可查看你的cookie值
⑥、实验六
如果前端使用的json格式,则可以使用@RequestBody接收并反序列化为对象,如果前端是正常的key=value格式的,就正常使用对象即可
java
@Controller // 定义此层为 controller 层
public class HelloController {
@RequestMapping("/test6")
public String test6(@RequestBody Person person) {
System.out.println(person);
return "hello";
}
}

⑦、实验七
MultipartFile 是 Spring 框架中用于处理文件上传的接口。当你在 Spring MVC 中接收文件上传时,通常会使用这个接口。
MultipartFile 的主要方法
String getOriginalFilename(): 获取原始文件名String getContentType(): 获取文件内容类型boolean isEmpty(): 判断文件是否为空long getSize(): 获取文件大小(字节)byte[] getBytes(): 获取文件内容的字节数组InputStream getInputStream(): 获取文件输入流void transferTo(File dest): 将文件保存到指定位置
⑧、实验八
HttpEntity 包含两个核心部分:
- Headers :HTTP 请求/响应头,类型为
MultiValueMap<String, String>(支持多值头)。 - Body :HTTP 请求/响应体,类型为泛型
T(可以是任意 Java 对象,如User、List<String>等)。
可直接使用getHeaders和getBody方法直接获取你的请求头和请求体
2.6、响应处理
①、实验一
java
@RestController
@Controller // 定义此层为 controller 层
public class HelloController {
@RequestMapping("/test0")
public Person test7() {
Person person = new Person();
person.setUsername("zs");
person.setPassword("zs");
person.setPhone("1111");
person.setAgreement(true);
return person;
}
}

②、实验二
ResponseEntity(响应实体)
- 用途:表示服务器返回的 HTTP 响应(包含响应头、响应体、HTTP 状态码)。
- 扩展功能 :相比
HttpEntity,额外包含 HTTP 状态码(Status Code)。 - 典型场景 :控制器方法返回响应结果(替代
@ResponseBody,更灵活地控制状态码和响应头)。
示例:控制器返回带状态码和响应头的响应
Java
@PostMapping("/api/users")
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.save(user); // 保存用户
// 构建响应头
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.set("Location", "/api/users/" + savedUser.getId()); // 资源位置
// 返回 ResponseEntity(状态码 201 Created + 响应头 + 响应体)
return new ResponseEntity<>(savedUser, responseHeaders, HttpStatus.CREATED);
}
简化写法 :使用 ResponseEntity 的静态工具方法(如 ok()、badRequest()、status()):
java
// 返回 200 OK + 响应体
return ResponseEntity.ok(savedUser);
// 返回 400 Bad Request + 错误消息
return ResponseEntity.badRequest().body("Invalid user data");
// 返回自定义状态码 + 响应头 + 响应体
return ResponseEntity.status(HttpStatus.CREATED)
.headers(responseHeaders)
.body(savedUser);
3、RestFul API
RESTful API 是一种基于 HTTP 协议的软件架构风格
3.1、核心概念
①、原则
RESTful API 遵循以下原则:
- 使用 HTTP 方法(GET, POST, PUT, DELETE 等)表示操作
- 无状态通信
- 资源通过 URI 标识
- 通常使用 JSON 或 XML 格式传输数据
简单来讲:其认为万物皆资源,你的URL请求地址应该用资源开头,通过不同的http请求方法来表示对该资源的操作
②、举例
以下给出一个例子:
RESTful API以前,接口可能是这样的:
/getEmployee?id=1:查询员工/addEmployee?name=zhangsan&age=18:新增员工/updateEmployee?id=1&age=20:修改员工/deleteEmployee?id=1:删除员工/getEmployeeList:获取所有员工
以员工的增删改查为例,设计的 RESTful API 如下(下面会有完整的代码演示):
| URI | 请求方式 | 请求体 | 作用 | 返回数据 |
|---|---|---|---|---|
/employee/{id} |
GET | 无 | 查询某个员工 | Employee JSON |
/employee |
POST | employee JSON | 新增某个员工 | 成功或失败状态 |
/employee/{id} |
PUT | employee JSON | 修改某个员工 | 成功或失败状态 |
/employee/{id} |
DELETE | 无 | 删除某个员工 | 成功或失败状态 |
/employees |
GET | 无 / 查询条件 | 查询所有员工 | ListJSON |
/employees/page |
GET | 无 / 分页条件 | 查询所有员工 | 分页数据 JSON |
③、概念区分
注意此处的API和我们java中的API接口有部分区别,此处的API指的是我们Web应用暴露出来给他人访问的请求路径,他人通过访问该请求路径可以获取我们已经定义好的功能,比如返回什么数据等等
要想访问他人的功能,我们有以下两种方式:
- API:通过调用他人的API,访问他们暴露出来的请求路径
- SDK:导入jar包
我们写controller层就是为前端匹配多个接口,前端发请求,后端根据请求写API接口返回数据
3.2、写一个员工系统
通过以 RestFul API 架构写一个可以完成增删改查的员工系统,该员工系统可执行的操作见核心概念的举例中所示
①、脚手架创建项目模块
- 创建Maven项目
- 选中依赖:Lombok、web、jdbc、MySQL driver驱动(当然这些依赖也可以后面自己手动添加)
②、准备数据库
-
创建数据库
-
创建表并插入数据
以下给出示例SQL语句:
sql-- 创建 employee 表 CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT CHECK (age BETWEEN 18 AND 65), email VARCHAR(100) UNIQUE, gender VARCHAR(4), address VARCHAR(200), salary DECIMAL(12,2) DEFAULT 0.00 ); -- 插入 3 条示例数据 INSERT INTO employee (name, age, email, gender, address, salary) VALUES ('张三', 28, 'alice@example.com', '男', '纽约', 8000.00), ('阿丽斯', 35, 'bob@example.com', '女', '东京', 1000.00), ('梅林', 23, 'carol@example.com', '女', '北京', 7500.00); -
连接数据库
在application.properties中配置数据库即可
yamlspring.datasource.url=jdbc:mysql://localhost:3306/ssmtest spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver注意此处的数据库的驱动,原来的是com.mysql.jdbc.Driver几乎不怎么用了,现在已经改为最新的com.mysql.cj.jdbc.Driver
③、创建包并编写业务
Ⅰ、entity
本包存放的对象是和数据库一一对应的
java
package org.example.spring04mvczsgc.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer id;
private String name;
private Integer age;
private String email;
private String gender;
private String address;
private BigDecimal salary;
}
Ⅱ、dao
本包存放的是对数据库操作相关的接口和对应的实现类
以下给出的实现类中只实现部分的简单功能,其他功能应和实际业务对标
java
package org.example.spring04mvczsgc.dao;
import org.example.spring04mvczsgc.entity.Employee;
import java.util.List;
public interface EmployeeDao {
// 根据员工 id 查询员工
Employee getEmployeeById(int id);
// 新增员工
void addEmployee(Employee employee);
// 修改某个员工
void updateEmployee(Employee employee, int id);
// 根据员工 id 删除员工
void deleteEmployee(int id);
// 查询所有员工
List<Employee> getAllEmployees();
}
java
package org.example.spring04mvczsgc.dao.impl;
import org.example.spring04mvczsgc.dao.EmployeeDao;
import org.example.spring04mvczsgc.entity.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public class EmployeeDaoImpl implements EmployeeDao {
@Autowired
private JdbcTemplate jdbcTemplate;
// JDBC 中的 queryForObject方法:
// queryForObject后的参数可以是:
// 1. sql+要转换的类+可变参数
// queryForObject("select * from book where id = ?", new BeanPropertyRowMapper<>(Book.class),id);
// 2. sql+可变参数的数组+要转换的类
// queryForObject("select * from book where id = ?", new Object[]{id},new BeanPropertyRowMapper<>(Book.class));
@Override
public Employee getEmployeeById(int id) {
String sql = "select * from employee where id=?";
// 注意使用的是字符串 sql ,而不是使用 "sql",这样就变成你的 SQL 语句就是 sql 字符串了
return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Employee.class), id);
}
@Override
public void addEmployee(Employee employee){
String sql = "insert into employee values(?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql,
employee.getId(),
employee.getName(),
employee.getAge(),
employee.getEmail(),
employee.getGender(),
employee.getAddress(),
employee.getSalary()
);
System.out.println("新增成功");
}
@Override
public void updateEmployee(Employee employee, int id){
String sql = "Update employee set id = ?, name = ?, age = ?, email = ?, gender = ?, address = ?, salary = ? where id = ?";
jdbcTemplate.update(sql,
employee.getId(),
employee.getName(),
employee.getAge(),
employee.getEmail(),
employee.getGender(),
employee.getAddress(),
employee.getSalary(),
id
);
System.out.println("修改成功");
}
@Override
public void deleteEmployee(int id){
String sql = "delete from employee where id=?";
jdbcTemplate.update(sql, id);
System.out.println("删除成功");
}
@Override
public List<Employee> getAllEmployees() {
String sql = "SELECT * FROM employee";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
Employee e = new Employee();
e.setId(rs.getInt("id"));
e.setName(rs.getString("name"));
e.setAge(rs.getInt("age"));
e.setEmail(rs.getString("email"));
e.setGender(rs.getString("gender"));
e.setAddress(rs.getString("address"));
e.setSalary(rs.getBigDecimal("salary"));
return e;
});
}
}
Ⅲ、service
本包存放的是服务业务的接口和对应的实现类,本质是对dao层的再度包装,相当于dao层的一个静态代理对象,在这一层可以补充dao层(只进行对数据库的操作)的辅助业务,比如拦截、验证、排除等等
以下给出的实现类中只实现部分的简单功能,其他功能应和实际业务对标
java
package org.example.spring04mvczsgc.service;
import org.example.spring04mvczsgc.entity.Employee;
import java.util.List;
public interface EmployeeService {
// 根据员工 id 查询员工
Employee getEmployeeById(int id);
// 新增员工
void addEmployee(Employee employee);
// 修改某个员工
void updateEmployee(Employee employee, int id);
// 根据员工 id 删除员工
void deleteEmployee(int id);
// 查询所有员工
List<Employee> getAllEmployees();
}
java
package org.example.spring04mvczsgc.service.impl;
import org.example.spring04mvczsgc.dao.EmployeeDao;
import org.example.spring04mvczsgc.entity.Employee;
import org.example.spring04mvczsgc.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // 本质是对 dao 层的再度包装,补充验证、拦截等功能
// 即我们的 controller 层是调用 service 层的方法,service 层再调用 dao 层的方法并且补充功能,最后返回结果给 controller 层
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeDao employeeDao;
@Override
public Employee getEmployeeById(int id) {
if(id <= 0){
throw new IllegalArgumentException("id 不能小于等于 0");
}
// 调用 dao 层的方法查询员工
Employee employee = employeeDao.getEmployeeById(id);
// 如果员工不存在,抛出异常
if(employee == null){
throw new IllegalArgumentException("id 对应的员工不存在");
}
return employee;
}
@Override
public void addEmployee(Employee employee) {
// 验证员工信息是否完整
// (在我们的建表语句中是使用 not null 约束 name,unique 约束 email,所以这里需要验证)
// 当然如果后来业务要求我们的邮箱有命名规范等等,我们可以在这里添加正则表达式等等来进行验证
if(employee.getName() == null || employee.getEmail() == null){
throw new IllegalArgumentException("员工信息不完整,必须填写姓名和邮箱");
}
// (在我们的建表语句中是要求员工年龄在 18 到 65 岁之间,所以这里需要验证)
if(employee.getAge() < 18 || employee.getAge() > 65){
throw new IllegalArgumentException("员工年龄必须在 18 到 65 岁之间");
}
// 当以上约束均满足,调用 dao 层的方法新增员工
employeeDao.addEmployee(employee);
}
@Override
public void updateEmployee(Employee employee, int id) {
// 验证员工 id 是否存在
if(id <= 0){
throw new IllegalArgumentException("id 不能小于等于 0");
}
// 调用 dao 层的方法查询员工,如果员工不存在,抛出异常
if(employeeDao.getEmployeeById(id) == null){
throw new IllegalArgumentException("id 对应的员工不存在");
}
// 验证员工信息是否完整
// (在我们的建表语句中是使用 not null 约束 name,unique 约束 email,所以这里需要对要更新的员工信息进行验证)
// 当然如果后来业务要求我们的邮箱有命名规范等等,我们可以在这里添加正则表达式等等来进行验证
if(employee.getName() == null || employee.getEmail() == null){
throw new IllegalArgumentException("员工信息不完整,必须填写姓名和邮箱");
}
// (在我们的建表语句中是要求员工年龄在 18 到 65 岁之间,所以这里需要对要更新的员工年龄进行验证)
if(employee.getAge() < 18 || employee.getAge() > 65){
throw new IllegalArgumentException("员工年龄必须在 18 到 65 岁之间");
}
// 当以上约束均满足,调用 dao 层的方法修改员工
employeeDao.updateEmployee(employee, id);
}
@Override
public void deleteEmployee(int id) {
// 验证员工 id 是否存在
if(id <= 0){
throw new IllegalArgumentException("id 不能小于等于 0");
}
// 调用 dao 层的方法查询员工,如果员工不存在,抛出异常
if(employeeDao.getEmployeeById(id) == null){
throw new IllegalArgumentException("id 对应的员工不存在");
}
// 当以上约束均满足,调用 dao 层的方法删除员工
employeeDao.deleteEmployee(id);
}
@Override
public List<Employee> getAllEmployees() {
return employeeDao.getAllEmployees();
}
}
Ⅳ、controller
本包实现的是和前端请求的接口匹配
java
package org.example.spring04mvczsgc.controller;
import org.example.spring04mvczsgc.entity.Employee;
import org.example.spring04mvczsgc.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employee/{id}")
// 当然也可以写成 @RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)
// 只有当请求方法为 GET 时,才会调用该方法
public Employee getEmployeeById(@PathVariable int id) {
return employeeService.getEmployeeById(id);
}
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public String addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
return "ok";
}
@PutMapping("/employee/{id}")
// 只有当请求方法为 PUT 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
// 同时,其路径变量 id 也会被自动映射到 id 参数中
public String updateEmployee(@RequestBody Employee employee, @PathVariable int id) {
employeeService.updateEmployee(employee,id);
return "ok";
}
@DeleteMapping("/employee/{id}")
// 当然也可以写成 @RequestMapping(value = "/employee/{id}",method = RequestMethod.DELETE)
// 只有当请求方法为 DELETE 时,才会调用该方法
public String deleteEmployeeById(@PathVariable int id) {
employeeService.deleteEmployee(id);
return "ok";
}
@GetMapping("/employee")
public String getAllEmployees() {
employeeService.getAllEmployees();
return "ok";
}
}
④、编写测试类
Ⅰ、dao测试类
java
package org.example.spring04mvczsgc;
import org.example.spring04mvczsgc.dao.EmployeeDao;
import org.example.spring04mvczsgc.entity.Employee;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
@SpringBootTest
public class EmployDaoTest {
@Autowired
EmployeeDao employeeDao;
@Test
public void test() {
System.out.println(employeeDao.getEmployeeById(1));
System.out.println(employeeDao.getEmployeeById(2));
System.out.println("原表情况:");
System.out.println(employeeDao.getAllEmployees());
employeeDao.addEmployee(new Employee(100, "Jack", 18, "123@hh.com", "男", "深圳", new BigDecimal("123.45")));
System.out.println("修改后表情况:");
System.out.println(employeeDao.getAllEmployees());
Employee employee1 = new Employee(66, "jieke", 44, "123@hh.com", "男", "深圳", new BigDecimal("123.45"));
employeeDao.updateEmployee(employee1, 100);
employeeDao.deleteEmployee(1);
}
}

Ⅱ、service测试类
这里只进行更新的操作测试
java
package org.example.spring04mvczsgc;
import org.example.spring04mvczsgc.entity.Employee;
import org.example.spring04mvczsgc.service.EmployeeService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.math.BigDecimal;
@SpringBootTest
public class EmployeeServiceTest {
@Autowired
EmployeeService employeeService;
@Test
public void test() {
Employee employee1 = new Employee(-1,"zsgc",20,"zsgc@zsgc.com","男","北京",new BigDecimal(5000));
Employee employee2 = new Employee(1,"zsgc",0,"zsgc@zsgc.com","男","北京",new BigDecimal(5000));
Employee employee3 = new Employee(1,"更新测试",33,"更新测试邮箱","男","北京",new BigDecimal(5000));
// 测试更新员工方法,其更新的员工 id 应大于 0
employeeService.updateEmployee(employee1,-1);
// 测试更新员工方法,其更新的员工 id 应在数据库内
employeeService.updateEmployee(employee1,100);
// 测试更新员工方法,其更新的员工 age 应该在 18~65 之间
employeeService.updateEmployee(employee2,1);
// 成功
employeeService.updateEmployee(employee3,1);
}
}


Ⅲ、controller测试类
以下只进行按id查询和更新的请求,其他请求自行测试


⑤、统一返回结果优化
除了以上基础的流程,我们还可以为我们的业务返回一个json格式的统一返回结果
该结果包含:
- code状态码
- message服务端给前端的提示信息(比如查询执行失败,返回给前端查询失败的提示id不存在等等)
- data服务器返回给前端的数据
除以上三种还可能有其他信息,具体根据前端业务设计
将该结果包装成为一个类,该类中的成员变量包含以上结果内容,再编写返回方法
java
package org.example.spring05bestpracitce.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R<T> {
private int code;
private String msg;
private T data;
// 以下成功失败的状态码具体根据前端业务设计
// 成功,但是返回空参
public static <T> R<T> ok() {
return new R<>(200,"success",null);
}
// 成功,返回数据
public static <T> R<T> ok(T data) {
return new R<>(200,"success",data);
}
public static <T> R<T> fail() {
return new R<>(400,"fail",null);
}
public static <T> R<T> error(String msg) {
return new R<>(500,msg,null);
}
}
之后将原service实现类中的return返回值改为R.ok/R.ok(...)/R.fail等等,最终规范返回格式都是json格式,如下:
java
// 只给出部分修改
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employee/{id}")
// 当然也可以写成 @RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)
// 只有当请求方法为 GET 时,才会调用该方法
public R getEmployeeById(@PathVariable int id) {
return R.ok(employeeService.getEmployeeById(id));
}
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public R addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
return R.ok();
}
}
⑥、跨域同源策略
跨域问题(CORS,Cross-Origin Resource Sharing)本质上是浏览器 的一种安全机制,叫做 同源策略(Same-Origin Policy) 。浏览器默认禁止"不同源"的网页之间互相访问资源 ,除非服务器明确告诉浏览器"我允许"。
两个 URL 必须同时满足以下三点才算同源:
| 组成部分 | 示例 |
|---|---|
| 协议(Protocol) | https |
| 域名(Host) | example.com |
| 端口(Port) | 443(默认) |
✅
https://example.com:443和https://example.com/api是同源❌
https://example.com和http://example.com不同源(协议不同)❌
https://example.com和https://api.example.com不同源(子域不同)❌
https://example.com和https://example.com:8080不同源(端口不同)
最简单的解决方法:在controller层添加@CrossOrigin注解,允许该类所有方法跨域,当然也可以只在某个方法上添加这个注解
复杂的跨域会发2次请求:
第一次发送预检请求,检查你是否允许跨域
第二次才是发送真正的http请求,去执行你的方法