java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot邮件发送与文件上传下载

SpringBoot4实现邮件发送与文件上传下载的常见业务场景实战教学

作者:gis开发之家

本文将系统讲解Spring Boot4邮件整合、模板邮件、文件上传下载、大小与格式限制的相关内容,并延伸到大文件分片上传、断点续传与 MinIO 对象存储,帮你覆盖从简单到复杂的真实业务场景,希望对大家有所帮助

本篇《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;
        }
    }
}

支持浏览器/下载工具断点续传。

四、大文件分片上传(生产级方案)

分片上传流程

前端:

后端:

分片上传接口

@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 新变化

七、常见坑位总结

坑解决
邮件发送阻塞接口@Async 异步发送
邮件被当成垃圾邮件配置 SPF / DKIM
文件上传大小超限配置 max-file-size
恶意文件上传校验后缀 + 内容类型
大文件上传失败分片上传 + 断点续传
本地磁盘满了使用 MinIO / OSS
文件下载中文乱码URLEncoder.encode

八、本篇总结

  1. 邮件发送:简单 → HTML → 模板 → 异步
  2. 文件上传必须做:大小限制 + 类型校验
  3. 大文件:分片上传 + 断点续传
  4. 生产环境文件存储:优先对象存储(MinIO)
  5. 下载接口支持 Range,提升用户体验
  6. 所有耗时操作(邮件 / 文件处理)异步化

到此这篇关于SpringBoot4实现邮件发送与文件上传下载的常见业务场景实战教学的文章就介绍到这了,更多相关SpringBoot邮件发送与文件上传下载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文