SSM速通3

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/123id=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-TypeAccept

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 包含两个核心部分:

  1. Headers :HTTP 请求/响应头,类型为 MultiValueMap<String, String>(支持多值头)。
  2. Body :HTTP 请求/响应体,类型为泛型 T(可以是任意 Java 对象,如 UserList<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 架构写一个可以完成增删改查的员工系统,该员工系统可执行的操作见核心概念的举例中所示

①、脚手架创建项目模块
  1. 创建Maven项目
  2. 选中依赖:Lombok、web、jdbc、MySQL driver驱动(当然这些依赖也可以后面自己手动添加)
②、准备数据库
  1. 创建数据库

  2. 创建表并插入数据

    以下给出示例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);
  3. 连接数据库

    在application.properties中配置数据库即可

    yaml 复制代码
    spring.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:443https://example.com/api 是同源

https://example.comhttp://example.com 不同源(协议不同)

https://example.comhttps://api.example.com 不同源(子域不同)

https://example.comhttps://example.com:8080 不同源(端口不同)

最简单的解决方法:在controller层添加@CrossOrigin注解,允许该类所有方法跨域,当然也可以只在某个方法上添加这个注解

复杂的跨域会发2次请求:

第一次发送预检请求,检查你是否允许跨域

第二次才是发送真正的http请求,去执行你的方法

相关推荐
一起养小猫2 小时前
Flutter for OpenHarmony 实战:番茄钟应用完整开发指南
开发语言·jvm·数据库·flutter·信息可视化·harmonyos
独自破碎E2 小时前
总持续时间可被 60 整除的歌曲
java·开发语言
Python+JAVA+大数据2 小时前
TCP_IP协议栈深度解析
java·网络·python·网络协议·tcp/ip·计算机网络·三次握手
丶小鱼丶2 小时前
Java基础之【多线程】
java
一起养小猫2 小时前
Flutter for OpenHarmony 实战:数据持久化方案深度解析
网络·jvm·数据库·flutter·游戏·harmonyos
u0109272712 小时前
使用XGBoost赢得Kaggle比赛
jvm·数据库·python
东东5162 小时前
基于vue的电商购物网站vue +ssm
java·前端·javascript·vue.js·毕业设计·毕设
她说..3 小时前
策略模式+工厂模式实现审批流(面试问答版)
java·后端·spring·面试·springboot·策略模式·javaee
鹿角片ljp3 小时前
力扣9.回文数-转字符双指针和反转数字
java·数据结构·算法