精通Spring Boot 3 : 3. Spring Boot 网络开发 (2)
ztj100 2025-01-02 20:33 17 浏览 0 评论
Spring Web 注解控制器
Spring MVC 提供了基于注解的编程方式,并包含两个实用的注解:@Controller 和 @RestController。这些注解用于处理请求映射、请求输入、异常处理等多种功能。
@Controller 注解与 Model、ModelAndView 类以及 View 接口一起使用时,您可以访问 Session 属性和可在 HTML 页面中使用的对象。使用此功能时,您需要返回视图的名称,Spring Web 将负责解析和渲染 HTML 页面。请参见以下代码片段:
// more ...
@Controller
public class MyRetroBoardController {
private RetroBoardService retroBoardService;
@GetMapping("/retros")
public String handle(Model model) {
model.addAttribute("retros", retroBoardService.findAll());
return "listRetros";
}
// more ...
}
前面的代码片段将 MyRetroBoardController 标记为一个网络控制器(使用 @Controller 注解)。当我们访问 "/retros" 这个端点时,它会创建一个必要的 Model 类,我们可以在其中添加一个属性(在这里是 retros 属性,值为所有的 retros),然后返回 HTML 页面的名称(listRetros,该页面位于 src/main/resources/templates/listRetros.html),这个页面将负责渲染并在页面中使用 retros 数据。Spring Web 会知道如何解析页面的位置以及如何通过模板引擎进行渲染。 如果您需要以特定值(与视图不同)进行响应,则必须在返回类型(方法中声明)上添加 @ResponseBody 注解,以便 Spring Web 能够使用 HTTP 消息转换器进行正确的响应。
@RestController 注解是一个类标记(它是 @Controller 的组合注解),可以直接写入响应体(因此不需要 @ResponseBody 注解)。我们将在整本书中广泛使用这个注解,因为我们将在后端更多地使用 Spring Boot。
Spring Web 技术还包括 @RequestMapping 注解,它可以用作类或方法的标记,因为它在将请求映射到控制器时非常有用。@RequestMapping 的一个优点是可以访问请求参数、头信息和媒体类型。虽然 @RequestMapping 可以用于每个方法,但有时可以使用一些快捷方式,例如 @GetMapping、@PostMapping、@PutMapping、@DeleteMapping 等等。这些快捷方式在列表 3-12 中有详细描述。
现在是时候编写我们的网络控制器了。请创建一个名为 web 的包和一个名为 RetroBoardController 的类,如清单 3-12 所示。
package com.apress.myretro.web;
import com.apress.myretro.board.Card;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.service.RetroBoardService;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@AllArgsConstructor
@RestController
@RequestMapping("/retros")
public class RetroBoardController {
private RetroBoardService retroBoardService;
@GetMapping
public ResponseEntity<Iterable<RetroBoard>> getAllRetroBoards(){
return ResponseEntity.ok(retroBoardService.findAll());
}
@PostMapping
public ResponseEntity<RetroBoard> saveRetroBoard(@Valid @RequestBody RetroBoard retroBoard){
RetroBoard result = retroBoardService.save(retroBoard);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{uuid}")
.buildAndExpand(result.getId().toString())
.toUri();
return ResponseEntity.created(location).body(result);
}
@GetMapping("/{uuid}")
public ResponseEntity<RetroBoard> findRetroBoardById(@PathVariable UUID uuid){
return ResponseEntity.ok(retroBoardService.findById(uuid));
}
@GetMapping("/{uuid}/cards")
public ResponseEntity<Iterable<Card>> getAllCardsFromBoard(@PathVariable UUID uuid){
return ResponseEntity.ok(retroBoardService.findAllCardsFromRetroBoard(uuid));
}
@PutMapping("/{uuid}/cards")
public ResponseEntity<Card> addCardToRetroBoard(@PathVariable UUID uuid,@Valid @RequestBody Card card){
Card result = retroBoardService.addCardToRetroBoard(uuid,card);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{uuid}/cards/{uuidCard}")
.buildAndExpand(uuid.toString(),result.getId().toString())
.toUri();
return ResponseEntity.created(location).body(result);
}
@GetMapping("/{uuid}/cards/{uuidCard}")
public ResponseEntity<Card> getCardFromRetroBoard(@PathVariable UUID uuid,@PathVariable UUID uuidCard){
return ResponseEntity.ok(retroBoardService.findCardByUUIDFromRetroBoard(uuid,uuidCard));
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{uuid}/cards/{uuidCard}")
public void deleteCardFromRetroBoard(@PathVariable UUID uuid,@PathVariable UUID uuidCard){
retroBoardService.removeCardFromRetroBoard(uuid,uuidCard);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, Object> response = new HashMap<>();
response.put("msg","There is an error");
response.put("code",HttpStatus.BAD_REQUEST.value());
response.put("time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
response.put("errors",errors);
return response;
}
}
列表 3-12 源代码:src/main/java/com/apress/myretro/web/RetroBoardController.java
让我们来分析一下 RetroBoardController 类
- @AllArgsConstructor:这个来自 Lombok 库的注解会使用字段作为参数来创建构造函数。在这种情况下,它将使用 RetroBoardService 作为参数。Spring 会通过这个构造函数注入 RetroBoardService bean,因此您可以在这个控制器类中访问它。
- 我们通过@RestController 注解来标识这个类。该注解会直接使用所有声明的方法将内容写入响应体。
- @RequestMapping:此注解将该类标记为响应任何请求的类,使用正确的 HTTP 方法,并将以/retros 端点作为其他路径的基础。
- @GetMapping:该注解用于标记多个方法,这些方法将通过 HTTP GET 方法响应/retros 端点。这是@RequestMapping(method = RequestMethod.GET)的简化写法,后者提供了更多可用参数。如果你查看 findRetroBoardById 方法,会发现@GetMapping 使用了值"/{uuid}";这是一个映射到 URL 模式的路径变量。在这种情况下,使用了 PathPattern,允许使用如下匹配模式:
- "/retros/docu?ent.doc": 仅在路径中匹配一个字符。
- "/retros/*.jpg": 在路径中匹配任意数量的字符。
- "/retros/**": 匹配多个路径部分。
- "/retros/{project}/versions": 匹配路径段并将其作为变量捕获。
- "/retros/{project:[a-z]+}/versions": 使用正则表达式匹配并捕获一个变量。
- 你可以在方法中使用类似这样的代码:@GetMapping("/{product:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")。正如你所看到的,你可以选择如何访问你的接口,这同样适用于每个@RequestMapping 及其快捷方式。
- ResponseEntity:这个类是从 HttpEntity(一个泛型类)扩展而来的,表示一个 HTTP 请求或响应实体,包含头部和主体。如果你查看代码,会发现类型各不相同。在 Spring Boot 或 Spring Web 应用程序中,使用 ResponseEntity 类是常见的做法之一。默认情况下,Spring Boot 会使用 HTTP JSON 消息转换器进行响应,因此你可以始终期待收到 JSON 格式的响应。当然,你也可以覆盖这个默认设置,以其他格式进行响应,例如 XML。
- @PostMapping:这是一个用于响应 HTTP POST 请求的注解,它会在 HTTP 数据包中发送主体(数据)。这与@RequestMapping(method = RequestMethod.POST)是相同的。对于这种请求,通常需要发送数据,因此您可能需要使用@RequestBody 注解。
- @RequestBody:这个注解会查看网络请求的主体,并尝试将其(通过 HttpMessageConverter)绑定到带有此注解的实例。在这个控制器中,我们有两个实例:一个用于 RetroBoard,另一个用于 Card 类。您可以使用 @Valid 注解来对数据进行验证。
- @Valid:此注解用于标记一个参数,以便进行级联验证,检查该参数是否根据使用的约束(如@NotNull、@NotEmpty、@NotBlank 等)通过验证。所有这些注解都在 RetroBoard 和 Card 类中,并属于 Jakarta 库。在后台,当进行网络请求时,将执行验证,并抛出一个我们可以捕获的异常。
- @PathVariable:这个注解用于将路径与在@RequestMapping 中声明的路径或使用的快捷方式进行绑定。例如,在 findRetroBoardById 方法中,路径/retros/{uuid}的值被绑定到 UUID 实例。因此,我们可以通过类似的方式来访问它:
- 访问 http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2
- 9dc9b71b-a07e-418b-b972-40225449aff2 将被设置为 UUID 实例变量。路径名称 ({uuid}) 必须与函数中声明的参数名称一致。
- ServletUriComponentsBuilder:这是一个辅助类,用于创建 URI 并扩展到包含键和值的基本路径。在此情况下,该类在 saveRetroBoard 方法中使用,负责创建 Header 中所需的位置,并通过 ResponseEntity.create()方法调用将其设置为响应的一部分。
- @PutMapping:该注解用于响应 HTTP PUT 方法请求,通常会将 HTTP Body 包含在请求中。在我们的例子中,它结合了特定路径(带有 URL 模式)和 HTTP Body(@RequestBody);请查看 addCardToRetroBoard 方法,它还包含了一些使用@Valid 注解的验证。
- @ResponseStatus:该注解用于自定义在特定控制器方法或异常处理程序中返回的 HTTP 状态码。有时,无论操作是什么,我们都可以返回特定的 HTTP 状态码。在这种情况下,在 deleteCardFromRetroBoard 方法中,我们返回 HttpStatus.NO_CONTENT 状态(204 代码)。
- @DeleteMapping:该注解用于处理 HTTP DELETE 请求方法。在我们的代码中,我们还通过@PathVariable 声明了一个 URL 路径。
- @ExceptionHandler:该注解用于定义处理控制器方法执行过程中抛出的特定异常的方法(或在@ControllerAdvice 类中进行全局异常处理)。当发生与@ExceptionHandler 方法参数中声明的异常类型匹配的异常时,Spring MVC 会调用该方法来处理异常并生成相应的响应。有时,我们需要以特定的方式回应应用程序中发生的任何错误或异常。默认情况下,Spring 会返回由嵌入式 Tomcat 服务器抛出的“服务器内部错误”(或其他错误),并且不会提供太多关于发生了什么的详细信息。 为了避免这种情况,我们可以捕获这些类型的错误,例如当我们没有有效数据(来自 RetroBoard 或 Card)时发生的错误,并解释发生了什么。我们可以注释一个处理此问题的方法。在我们的代码中,有一个 handleValidationExceptions 方法。我们的@ExceptionHandler 将 MethodArgumentNotValidException 类声明为参数,这意味着只有在验证过程中抛出此错误时,它才会被触发。
- 方法参数无效异常:当进行@Valid 级联验证时,会抛出此异常。handleValidationExceptions 方法将生成包含错误信息的响应,并返回一个将被转换为 JSON 格式的 Map,这样更容易理解发生了什么。
我们刚刚实现了一个基于 Spring Boot 的 Web 应用程序。在您继续进行测试之前,请再仔细查看一下 RetroBoardController 类,关注每一个细节。
测试我的怀旧应用
为了测试我们的应用程序,我们首先要测试 RetroBoardService 类,这是应用程序的核心。因此,在测试文件夹中,请用列表 3-13 中的代码替换 MyretroApplicationTests 的内容。
package com.apress.myretro;
import com.apress.myretro.board.Card;
import com.apress.myretro.board.CardType;
import com.apress.myretro.board.RetroBoard;
import com.apress.myretro.exception.CardNotFoundException;
import com.apress.myretro.exception.RetroBoardNotFoundException;
import com.apress.myretro.service.RetroBoardService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Collection;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
@SpringBootTest
class MyretroApplicationTests {
@Autowired
RetroBoardService service;
UUID retroBoardUUID = UUID.fromString("9DC9B71B-A07E-418B-B972-40225449AFF2");
UUID cardUUID = UUID.fromString("BB2A80A5-A0F5-4180-A6DC-80C84BC014C9");
UUID mehCardUUID = UUID.fromString("775A3905-D6BE-49AB-A3C4-EBE287B51539");
@Test
void saveRetroBoardTest(){
RetroBoard retroBoard = service.save(RetroBoard.builder().name("Gathering 2023").build());
assertThat(retroBoard).isNotNull();
assertThat(retroBoard.getId()).isNotNull();
}
@Test
void findAllRetroBoardsTest(){
Iterable<RetroBoard> retroBoards = service.findAll();
assertThat(retroBoards).isNotNull();
assertThat(retroBoards).isNotEmpty();
}
@Test
void cardsRetroBoardNotFoundTest() {
assertThatThrownBy(() -> {
service.findAllCardsFromRetroBoard(UUID.randomUUID());
}).isInstanceOf(RetroBoardNotFoundException.class);
}
@Test
void findRetroBoardTest(){
RetroBoard retroBoard = service.findById(retroBoardUUID);
assertThat(retroBoard).isNotNull();
assertThat(retroBoard.getName()).isEqualTo("Spring Boot 3.0 Meeting");
assertThat(retroBoard.getId()).isEqualTo(retroBoardUUID);
}
@Test
void findCardsInRetroBoardTest(){
RetroBoard retroBoard = service.findById(retroBoardUUID);
assertThat(retroBoard).isNotNull();
assertThat(retroBoard.getCards()).isNotEmpty();
}
@Test
void addCardToRetroBoardTest(){
Card card = service.addCardToRetroBoard(retroBoardUUID, Card.builder()
.comment("Amazing session")
.cardType(CardType.HAPPY)
.build());
assertThat(card).isNotNull();
assertThat(card.getId()).isNotNull();
RetroBoard retroBoard = service.findById(retroBoardUUID);
assertThat(retroBoard).isNotNull();
assertThat(retroBoard.getCards()).isNotEmpty();
}
@Test
void findAllCardsFromRetroBoardTest() {
Iterable<Card> cardList = service.findAllCardsFromRetroBoard(retroBoardUUID);
assertThat(cardList).isNotNull();
assertThat(((Collection) cardList).size()).isGreaterThan(3);
}
@Test
void removeCardsFromRetroBoardTest(){
service.removeCardFromRetroBoard(retroBoardUUID,cardUUID);
RetroBoard retroBoard = service.findById(retroBoardUUID);
assertThat(retroBoard).isNotNull();
assertThat(retroBoard.getCards()).isNotEmpty();
assertThat(retroBoard.getCards()).hasSizeLessThan(4);
}
@Test
void findCardByIdInRetroBoardTesT(){
Card card = service.findCardByUUIDFromRetroBoard(retroBoardUUID,mehCardUUID);
assertThat(card).isNotNull();
assertThat(card.getId()).isEqualTo(mehCardUUID);
}
@Test
void notFoundCardInRetroBoardTest(){
assertThatThrownBy(() -> {
service.findCardByUUIDFromRetroBoard(retroBoardUUID,UUID.randomUUID());
}).isInstanceOf(CardNotFoundException.class);
}
}
列表 3-13 源代码:src/test/java/com/apress/myretro/MyRetroApplicationTests.java
正如你所看到的,MyretroApplicationTests 类仅测试服务。在这个测试中,我们使用了 AssertJ 库中的一些简单断言。为了验证 My Retro App 是否返回正确的异常,我们可以使用 assertThatThrownBy 方法。你可以通过你的 IDE 或运行以下命令来执行这些测试:
./gradlew clean test
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
> Task :compileJava
> Task :test
MyretroApplicationTests > saveRetroBoardTest() PASSED
MyretroApplicationTests > findAllRetroBoardsTest() PASSED
MyretroApplicationTests > findRetroBoardTest() PASSED
MyretroApplicationTests > removeCardsFromRetroBoardTest() PASSED
MyretroApplicationTests > cardsRetroBoardNotFoundTest() PASSED
MyretroApplicationTests > notFoundCardInRetroBoardTest() PASSED
MyretroApplicationTests > findCardsInRetroBoardTest() PASSED
MyretroApplicationTests > addCardToRetroBoardTest() PASSED
MyretroApplicationTests > findCardByIdInRetroBoardTesT() PASSED
MyretroApplicationTests > findAllCardsFromRetroBoardTest() PASSED
BUILD SUCCESSFUL in 10s
5 actionable tasks: 5 executed
现在,我们真正想做的是测试我们的网络 API,对吗?有许多工具可以帮助我们实现这一点,比如 PostMan (https://www.postman.com) 和 Insomnia (https://insomnia.rest)。但我想向你介绍一个名为 REST Client 的工具,它可以直接帮助我们进行 HTTP 请求。这个工具有开源版本和付费版本,风格一致。付费版本包含在 IntelliJ IDEA 企业版中,而如果你使用 VS Code,则需要从插件选项卡安装 REST Client (v0.25.x),这个插件的作者是 Huachao Mao(见图 3-3)。
我将演示如何使用 VS Code REST Client 插件来测试 My Retro App。请下载插件并创建 src/http 文件夹,然后添加一个名为 myretro.http 的文件,内容如列表 3-14 所示。
### Get All Retro Boards
GET http://localhost:8080/retros
Content-Type: application/json
### Get Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2
Content-Type: application/json
### Get All Cards from Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards
Content-Type: application/json
### Get Single Card from Retro Board
GET http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards/bb2a80a5-a0f5-4180-a6dc-80c84bc014c9
Content-Type: application/json
### Create a Retro Board
POST http://localhost:8080/retros
Content-Type: application/json
{
"name": "Spring Boot Conference"
}
### Add Card to Retro
PUT http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards
Content-Type: application/json
{
"comment": "We are back in business",
"cardType": "HAPPY"
}
### Delete Card from Retro
DELETE http://localhost:8080/retros/9dc9b71b-a07e-418b-b972-40225449aff2/cards/bb2a80a5-a0f5-4180-a6dc-80c84bc014c9
Content-Type: application/json
文件 3-14src/http/myretro.http
列表 3-14 展示了实现我的复古应用所需的所有调用。在每个调用的顶部,您应该启用“发送请求”链接;点击后,您将在另一个窗口中查看响应。请尝试每个调用。
使用这种类型的客户端非常简单。这个插件的功能远超这段简要介绍。如果你想了解更多关于如何使用它的信息,请访问 https://github.com/Huachao/vscode-restclient。
相关推荐
- 前端案例·程序员的浪漫:流星雨背景
-
如果文章对你有收获,还请不要吝啬【点赞收藏评论】三连哦,你的鼓励将是我成长助力之一!谢谢!(1)方式1:简单版本【1】先看实现效果...
- UI样式iPod classic的HTML本地音乐播放器框架
-
PS:音量可以鼠标点击按住在音量图标边的轮盘上下拖拽滑动音量大小中心按钮可以更改播放器为白色...
- JavaScript 强制回流问题及优化方案
-
JavaScript代码在运行过程中可能会强制触发浏览器的回流(Reflow)...
- Ai 编辑器 Cursor 零基础教程:推箱子小游戏实战演练
-
最近Ai火的同时,Ai编辑器Cursor同样火了一把。今天我们就白漂一下Cursor,使用免费版本搞一个零基础教程...
- 19年前司机被沉尸水库!凶手落网,竟已是身家千万的大老板
-
]|\[sS])*"|'(?:[^\']|\[sS])*'|[^)}]+)s*)/g,l=window.testenv_reshost||window.__moon_host||"res.wx.qq...
- 全民健身网络热度调查“居家健身”成为第一网络热词
-
]|\[sS])*"|'(?:[^\']|\[sS])*'|[^)}]+)s*)/g,l=window.testenv_reshost||window.__moon_host||"res.wx.qq...
- 取代JavaScript库的10个现代Web API及详细实施代码
-
为什么浏览器内置的API你还在用某个臃肿的Javascript库呢?用内置的API有什么好处呢?Web平台经历了巨大演进,引入了强大的原生API,不再需要臃肿的JavaScript库。现代浏览器现已支...
- 前端文件下载的N种姿势:从简单到高级
-
文件下载是web开发里一个非常常见的功能,无论是下载用户生成的数据、图片、文档还是应用程序包。前端开发者有多种方式来实现这一需求,每种方式都有其适用场景和优缺点。介绍下几种比较常用的文件下载方法。...
- JavaScript 性能优化方法(js前端性能优化)
-
JavaScript性能优化方法减少DOM操作频繁的DOM操作会导致浏览器重绘和回流,影响性能。使用文档片段(DocumentFragment)或虚拟DOM技术减少直接操作。...
- DOM节点的创建、插入、删除、查找、替换
-
在前端开发中,js与html联系最紧密的莫过于对DOM的操作了,本文为大家分享一些DOM节点的基本操作。一、创建DOM节点使用的命令是varoDiv=document.createElement...
- 前端里的拖拖拽拽(拖拽式前端框架)
-
最近在项目中使用了react-dnd,一个基于HTML5的拖拽库,“拖拽能力”丰富了前端的交互方式,基于拖拽能力,会扩展各种各样的拖拽反馈效果,因此有必要学习了解,最好的学习方式就是实操!...
- 大模型实战:Flask+H5三件套实现大模型基础聊天界面
-
本文使用Flask和H5三件套(HTML+JS+CSS)实现大模型聊天应用的基本方式话不多说,先贴上实现效果:流式输出:思考输出:聊天界面模型设置:模型设置会话切换:前言大模型的聊天应用从功能...
- SSE前端(sse前端数据)
-
<!DOCTYPEhtml><htmllang="zh-CN"><head>...
- 课堂点名总尴尬?试试 DeepSeek,或能实现点名自由!(附教程)
-
2025年2月26日"你有没有经历过这样的场景?老师拿着花名册扫视全班:'今天我们来点名...'那一刻心跳加速,默念:'别点我!'但现在,我要...
- 我会在每个项目中复制这10个JS代码片段
-
你是否也有这种感觉:在搭建新项目时,你会想:"这个函数我是不是已经写过了...在某个地方?"是的——我也是。所以在开发了数十个React、Node和全栈应用后,我不再重复造轮子。我创建...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)