基于 Spring Boot 的 Web 三大核心交互案例精讲
在 Spring Boot Web 开发中,最常见、最核心的三种前后端交互方式是:
- 表单提交(同步提交 / POST)
传统网页最经典的方式(目前仍大量存在于后台管理系统、企业内部系统) - Ajax / Fetch + JSON(前后端分离主流方式)
现代 Web 应用(Vue/React/Angular + Spring Boot REST API)的事实标准 - 文件上传 & 下载(multipart/form-data + ResponseEntity)
几乎每个中后台系统都会涉及的功能
下面用三个典型、实用的案例,把三种交互方式的核心写法、常见坑和最佳实践一次性讲清楚。
案例 1:表单提交(传统同步方式)
场景:用户注册 / 登录 / 修改个人信息(传统 Thymeleaf 或 JSP 页面)
后端 Controller 写法
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/register")
public String registerPage(Model model) {
model.addAttribute("user", new UserForm());
return "user/register"; // thymeleaf 模板
}
@PostMapping("/register")
public String register(@ModelAttribute UserForm user,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
// 校验
if (bindingResult.hasErrors()) {
return "user/register";
}
// 业务逻辑(保存用户、加密密码等)
// userService.register(user);
redirectAttributes.addFlashAttribute("message", "注册成功,请登录");
return "redirect:/user/login";
}
}
前端(Thymeleaf 示例)
<form th:action="@{/user/register}" th:object="${user}" method="post">
<div>
<label>用户名:</label>
<input type="text" th:field="*{username}" required />
<span th:errors="*{username}" class="error"></span>
</div>
<div>
<label>密码:</label>
<input type="password" th:field="*{password}" required />
</div>
<button type="submit">注册</button>
</form>
关键点与常见坑
- 使用
@ModelAttribute+BindingResult做 JSR-303 校验 - 表单提交成功后用重定向(PRG 模式)防止重复提交
- 错误回显靠
BindingResult和 thymeleaf 的th:errors - 密码等敏感字段不要直接回显
案例 2:前后端分离 – RESTful JSON 接口(@RestController + @RequestBody)
场景:Vue/React 前端通过 axios/fetch 调用后端 API
后端代码
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserApiController {
private final UserService userService;
@PostMapping
public ResponseEntity<ApiResult<UserVO>> createUser(
@Valid @RequestBody UserCreateDTO dto,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest()
.body(ApiResult.error("参数校验失败", bindingResult));
}
UserVO vo = userService.createUser(dto);
return ResponseEntity.ok(ApiResult.success(vo));
}
@GetMapping("/{id}")
public ResponseEntity<ApiResult<UserVO>> getUser(@PathVariable Long id) {
UserVO vo = userService.findById(id);
return ResponseEntity.ok(ApiResult.success(vo));
}
@PutMapping("/{id}")
public ResponseEntity<ApiResult<Void>> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateDTO dto) {
userService.updateUser(id, dto);
return ResponseEntity.ok(ApiResult.success());
}
}
统一响应结构(推荐做法)
@Data
public class ApiResult<T> {
private boolean success;
private T data;
private String message;
private String code;
public static <T> ApiResult<T> success(T data) {
ApiResult<T> result = new ApiResult<>();
result.success = true;
result.data = data;
return result;
}
public static ApiResult<?> error(String msg, BindingResult errors) {
// ... 收集 field error
}
}
前端(axios 示例)
axios.post('/api/v1/users', {
username: 'zhangsan',
password: '123456',
email: 'test@example.com'
})
.then(res => {
if (res.data.success) {
message.success('注册成功')
}
})
.catch(err => {
if (err.response?.data?.message) {
message.error(err.response.data.message)
}
})
常见坑与最佳实践
- 统一使用
@RestController而不是@Controller - 校验失败统一返回 400 + 详细错误信息
- 使用
@Valid+BindingResult或MethodArgumentNotValidException全局处理 - 返回
ResponseEntity而不是直接对象,更灵活控制状态码 - 前端要处理
4xx、5xx不同的错误提示
案例 3:文件上传 & 下载(multipart + ResponseEntity)
文件上传(单文件 & 多文件)
@RestController
@RequestMapping("/api/files")
public class FileController {
@PostMapping("/upload")
public ApiResult<FileUploadVO> uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam(required = false) String bizType) {
if (file.isEmpty()) {
return ApiResult.error("文件不能为空");
}
String originalFilename = file.getOriginalFilename();
String extension = FilenameUtils.getExtension(originalFilename);
// 推荐:使用 UUID + 扩展名
String newFileName = UUID.randomUUID() + "." + extension;
Path dest = Paths.get("/uploads/", newFileName);
Files.createDirectories(dest.getParent());
file.transferTo(dest.toFile());
return ApiResult.success(new FileUploadVO(newFileName, "/files/" + newFileName));
}
// 多文件上传
@PostMapping("/batch-upload")
public ApiResult<List<FileUploadVO>> uploadMultiple(
@RequestPart("files") List<MultipartFile> files) {
// 类似处理...
}
}
文件下载(推荐 ResponseEntity 方式)
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) throws IOException {
Path filePath = Paths.get("/uploads/", fileName).normalize();
if (!Files.exists(filePath)) {
return ResponseEntity.notFound().build();
}
Resource resource = new InputStreamResource(Files.newInputStream(filePath));
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + URLEncoder.encode(fileName, StandardCharsets.UTF_8) + "\"")
.body(resource);
}
前端下载(两种常用方式)
// 方式1:直接 a 标签(简单文件)
<a :href="downloadUrl" download>下载</a>
// 方式2:通过 axios(可处理 token、进度)
axios.get('/api/files/download/xxx.pdf', {
responseType: 'blob'
}).then(response => {
const url = window.URL.createObjectURL(new Blob([response.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', 'filename.pdf')
document.body.appendChild(link)
link.click()
link.remove()
})
总结:三大交互方式对比
| 交互方式 | 前端技术 | 后端注解核心 | 典型场景 | 现代主流度(2025-2026) |
|---|---|---|---|---|
| 表单同步提交 | form + submit | @Controller + @PostMapping | 后台管理系统、管理端 | ★★☆☆☆ |
| RESTful JSON | axios/fetch | @RestController + @RequestBody | 前后端分离、B端、C 端 API | ★★★★★ |
| 文件上传/下载 | FormData / input | @RequestPart + MultipartFile | 头像、附件、导入导出 | ★★★★☆ |
你目前项目里用的是哪种交互方式占比最高?
或者你正在做的某个具体功能(比如富文本上传图片、分片上传、断点续传、Excel 导入导出等)遇到了痛点?可以继续细聊~