本篇《spring boot 4 学习从入门到大神》邮件通知、文件上传下载是几乎所有后台系统的刚需。
本文将系统讲解 spring boot 4 邮件整合、模板邮件、文件上传下载、大小与格式限制,并延伸到大文件分片上传、断点续传与 minio 对象存储,帮你覆盖从简单到复杂的真实业务场景。
一、邮件发送:从简单到模板化
引入依赖
<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>邮箱配置(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 → 获取授权码
发送简单文本邮件
@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 的测试邮件"
);发送 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);发送带附件的邮件
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);
}模板邮件(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}" rel="external nofollow" >激活账号</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);
}
}模板邮件优点:
- 样式可控
- 逻辑与视图分离
- 支持复杂业务数据
异步发送邮件(强烈推荐)
@async("taskexecutor")
public completablefuture<void> sendtemplatemailasync(...) throws messagingexception {
sendtemplatemail(...);
return completablefuture.completedfuture(null);
}避免邮件发送阻塞主流程(如注册接口)。
二、文件上传:从基础到安全
基础文件上传(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()
));
}
}文件上传配置(大小限制)
spring:
servlet:
multipart:
enabled: true
max-file-size: 50mb # 单文件大小
max-request-size: 100mb # 单次请求总大小
file-size-threshold: 2mb # 超过则写入磁盘文件类型校验(安全必做 ⭐)
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 类型。
多文件上传
@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);
}三、文件下载与断点续传
基础文件下载
@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);
}
}
}断点续传(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;
}
}
}支持浏览器/下载工具断点续传。
四、大文件分片上传(生产级方案)
分片上传流程
前端:
- 计算文件 md5
- 将文件切成 5mb 分片
- 依次上传分片(带 index)
- 所有分片上传完成后,通知后端合并
后端:
- 接收分片,按 md5 + index 存储
- 记录分片上传状态
- 合并分片,校验 md5
分片上传接口
@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 + " 上传成功");
}合并分片
@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 实战(企业级推荐 ⭐)
为什么不用本地磁盘?
| 本地磁盘 | 对象存储 |
|---|---|
| 容量有限 | 无限扩容 |
| 单机故障 | 高可用 |
| 难做 cdn | 天然支持 cdn |
| 难迁移 | 云厂商通用 |
生产环境:本地 + 对象存储(minio / oss / cos / s3)
引入 minio
<dependency>
<groupid>io.minio</groupid>
<artifactid>minio</artifactid>
<version>8.5.9</version>
</dependency>配置 minio
minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: test-bucket
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();
}
}上传文件到 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 配置
七、常见坑位总结
| 坑 | 解决 |
|---|---|
| 邮件发送阻塞接口 | @async 异步发送 |
| 邮件被当成垃圾邮件 | 配置 spf / dkim |
| 文件上传大小超限 | 配置 max-file-size |
| 恶意文件上传 | 校验后缀 + 内容类型 |
| 大文件上传失败 | 分片上传 + 断点续传 |
| 本地磁盘满了 | 使用 minio / oss |
| 文件下载中文乱码 | urlencoder.encode |
八、本篇总结
- 邮件发送:简单 → html → 模板 → 异步
- 文件上传必须做:大小限制 + 类型校验
- 大文件:分片上传 + 断点续传
- 生产环境文件存储:优先对象存储(minio)
- 下载接口支持 range,提升用户体验
- 所有耗时操作(邮件 / 文件处理)异步化
到此这篇关于springboot4实现邮件发送与文件上传下载的常见业务场景实战教学的文章就介绍到这了,更多相关springboot邮件发送与文件上传下载内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论