本篇《Spring Boot 4 学习从入门到大神》邮件通知、文件上传下载是几乎所有后台系统的刚需。本文将系统讲解 Spring Boot 4 邮件整合、模板邮件、文件上传下载、大小与格式限制,并延伸到大文件分片上传、断点续传与 MinIO 对象存储,帮你覆盖从简单到复杂的真实业务场景。
一、邮件发送:从简单到模板化
1️⃣ 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!-- 模板邮件(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2️⃣ 邮箱配置(application.yml)
以 QQ 邮箱 为例(其他邮箱类似):
spring:
mail:
host: smtp.qq.com
port: 587
username: 123456@qq.com
password: xxxx # 邮箱授权码,不是登录密码
protocol: smtp
default-encoding: UTF-8
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
ssl:
enable: false
📌 授权码获取:
- QQ 邮箱 → 设置 → 账号 → 开启 POP3/SMTP → 获取授权码
3️⃣ 发送简单文本邮件
@Service
public class MailService {
@Autowired
private JavaMailSender mailSender;
@Value("${spring.mail.username}")
private String from;
public void sendSimpleMail(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
mailSender.send(message);
}
}
mailService.sendSimpleMail(
"test@test.com",
"测试邮件",
"这是一封来自 Spring Boot 4 的测试邮件"
);
4️⃣ 发送 HTML 邮件
public void sendHtmlMail(String to, String subject, String htmlContent) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlContent, true); // true = HTML
mailSender.send(message);
}
String html = "<h1>欢迎注册</h1><p>请点击链接激活账号:<a href='#'>激活</a></p>";
sendHtmlMail("test@test.com", "账号激活", html);
5️⃣ 发送带附件的邮件
public void sendAttachmentMail(String to, String subject, String content, String filePath)
throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
FileSystemResource file = new FileSystemResource(new File(filePath));
helper.addAttachment("附件.pdf", file);
mailSender.send(message);
}
6️⃣ 模板邮件(Thymeleaf,生产推荐 ⭐)
模板:templates/mail/welcome.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h1>欢迎您,<span th:text="${username}"></span>!</h1>
<p>您的注册邮箱是:<span th:text="${email}"></span></p>
<p>请点击以下链接激活账号:</p>
<a th:href="${activeUrl}">激活账号</a>
</body>
</html>
发送模板邮件
@Service
public class MailService {
@Autowired
private JavaMailSender mailSender;
@Autowired
private SpringTemplateEngine templateEngine;
public void sendTemplateMail(String to, String username, String email, String activeUrl)
throws MessagingException {
Context context = new Context();
context.setVariable("username", username);
context.setVariable("email", email);
context.setVariable("activeUrl", activeUrl);
String content = templateEngine.process("mail/welcome", context);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject("账号激活邮件");
helper.setText(content, true);
mailSender.send(message);
}
}
✅ 模板邮件优点:
- 样式可控
- 逻辑与视图分离
- 支持复杂业务数据
7️⃣ 异步发送邮件(强烈推荐)
@Async("taskExecutor")
public CompletableFuture<Void> sendTemplateMailAsync(...) throws MessagingException {
sendTemplateMail(...);
return CompletableFuture.completedFuture(null);
}
📌 避免邮件发送阻塞主流程(如注册接口)。
二、文件上传:从基础到安全
1️⃣ 基础文件上传(MultipartFile)
@RestController
@RequestMapping("/files")
public class FileController {
@PostMapping("/upload")
public Result<?> upload(@RequestParam("file") MultipartFile file) throws IOException {
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID() + suffix;
String uploadDir = System.getProperty("user.dir") + "/uploads/";
File dest = new File(uploadDir + fileName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
file.transferTo(dest);
return Result.success(Map.of(
"fileName", fileName,
"originalName", originalFilename,
"size", file.getSize()
));
}
}
2️⃣ 文件上传配置(大小限制)
spring:
servlet:
multipart:
enabled: true
max-file-size: 50MB # 单文件大小
max-request-size: 100MB # 单次请求总大小
file-size-threshold: 2MB # 超过则写入磁盘
3️⃣ 文件类型校验(安全必做 ⭐)
private static final List<String> ALLOWED_TYPES =
Arrays.asList("jpg", "jpeg", "png", "pdf", "doc", "docx");
private boolean isAllowed(String filename) {
String suffix = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
return ALLOWED_TYPES.contains(suffix);
}
✅ 永远不要相信前端传的文件名和 MIME 类型。
4️⃣ 多文件上传
@PostMapping("/upload/multiple")
public Result<?> uploadMultiple(@RequestParam("files") MultipartFile[] files) throws IOException {
List<String> fileNames = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
// 保存文件...
fileNames.add(file.getOriginalFilename());
}
}
return Result.success(fileNames);
}
三、文件下载与断点续传
1️⃣ 基础文件下载
@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
File file = new File("uploads/test.pdf");
response.setContentType("application/pdf");
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
try (InputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
2️⃣ 断点续传(Range 支持,大文件必备 ⭐)
@GetMapping("/download/range")
public void downloadWithRange(HttpServletRequest request, HttpServletResponse response)
throws IOException {
File file = new File("uploads/bigfile.zip");
long fileLength = file.length();
long start = 0;
long end = fileLength - 1;
String range = request.getHeader("Range");
if (range != null && range.startsWith("bytes=")) {
String[] ranges = range.substring(6).split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
}
}
long contentLength = end - start + 1;
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setContentType("application/octet-stream");
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileLength);
response.setHeader("Accept-Ranges", "bytes");
response.setContentLengthLong(contentLength);
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream out = response.getOutputStream()) {
raf.seek(start);
byte[] buffer = new byte[1024];
long need = contentLength;
int len;
while (need > 0 &&
(len = raf.read(buffer, 0, (int) Math.min(buffer.length, need))) != -1) {
out.write(buffer, 0, len);
need -= len;
}
}
}
✅ 支持浏览器/下载工具断点续传。
四、大文件分片上传(生产级方案)
1️⃣ 分片上传流程
前端:
1. 计算文件 MD5
2. 将文件切成 5MB 分片
3. 依次上传分片(带 index)
4. 所有分片上传完成后,通知后端合并
后端:
1. 接收分片,按 MD5 + index 存储
2. 记录分片上传状态
3. 合并分片,校验 MD5
2️⃣ 分片上传接口
@PostMapping("/upload/chunk")
public Result<?> uploadChunk(
@RequestParam("file") MultipartFile chunk,
@RequestParam("md5") String md5,
@RequestParam("index") int index,
@RequestParam("total") int total) throws IOException {
String chunkDir = "uploads/chunks/" + md5 + "/";
File dir = new File(chunkDir);
if (!dir.exists()) {
dir.mkdirs();
}
chunk.transferTo(new File(chunkDir + index));
return Result.success("分片 " + index + " 上传成功");
}
3️⃣ 合并分片
@PostMapping("/upload/merge")
public Result<?> mergeChunks(@RequestParam("md5") String md5,
@RequestParam("fileName") String fileName) throws IOException {
String chunkDir = "uploads/chunks/" + md5 + "/";
File dir = new File(chunkDir);
File[] chunks = dir.listFiles();
if (chunks == null) {
return Result.error("分片不存在");
}
Arrays.sort(chunks, Comparator.comparingInt(f -> Integer.parseInt(f.getName())));
String mergePath = "uploads/" + fileName;
try (FileOutputStream fos = new FileOutputStream(mergePath)) {
byte[] buffer = new byte[1024];
for (File chunk : chunks) {
try (FileInputStream fis = new FileInputStream(chunk)) {
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
}
}
// 删除分片
for (File chunk : chunks) {
chunk.delete();
}
dir.delete();
return Result.success("文件合并完成");
}
五、对象存储:MinIO 实战(企业级推荐 ⭐)
1️⃣ 为什么不用本地磁盘?
|
本地磁盘 |
对象存储 |
|---|---|
|
容量有限 |
无限扩容 |
|
单机故障 |
高可用 |
|
难做 CDN |
天然支持 CDN |
|
难迁移 |
云厂商通用 |
✅ 生产环境:本地 + 对象存储(MinIO / OSS / COS / S3)
2️⃣ 引入 MinIO
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.9</version>
</dependency>
3️⃣ 配置 MinIO
minio:
endpoint: http://localhost:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: test-bucket
4️⃣ MinIO 配置类
@Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
5️⃣ 上传文件到 MinIO
@Service
public class MinioService {
@Autowired
private MinioClient minioClient;
@Value("${minio.bucket-name}")
private String bucketName;
public String upload(MultipartFile file) throws Exception {
String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
return endpoint + "/" + bucketName + "/" + fileName;
}
}
六、Spring Boot 4 新变化
- 文件上传与虚拟线程结合,高并发下吞吐更高
MultipartFile与 AOT 编译兼容性更好- 邮件发送支持更严格的 TLS 配置
七、常见坑位总结
|
坑 |
解决 |
|---|---|
|
邮件发送阻塞接口 |
|
|
邮件被当成垃圾邮件 |
配置 SPF / DKIM |
|
文件上传大小超限 |
配置 |
|
恶意文件上传 |
校验后缀 + 内容类型 |
|
大文件上传失败 |
分片上传 + 断点续传 |
|
本地磁盘满了 |
使用 MinIO / OSS |
|
文件下载中文乱码 |
|
八、本篇总结
- 邮件发送:简单 → HTML → 模板 → 异步
- 文件上传必须做:大小限制 + 类型校验
- 大文件:分片上传 + 断点续传
- 生产环境文件存储:优先对象存储(MinIO)
- 下载接口支持 Range,提升用户体验
- 所有耗时操作(邮件 / 文件处理)异步化
&spm=1001.2101.3001.5002&articleId=166107367&d=1&t=3&u=d56a16d480d342139a3c554ed74291fe)
339

被折叠的 条评论
为什么被折叠?



