依賴準備
<!-- SmartUpload (舊項目常用,新項目建議 Apache Commons FileUpload / Servlet 3.0 Part) -->
<dependency>
<groupId>com.jspsmart</groupId>
<artifactId>smartupload</artifactId>
<version>1.1</version>
</dependency>
<!-- JavaMail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
1. 文件上傳:Controller/Servlet 處理
@WebServlet("/tool/upload")
@MultipartConfig(maxFileSize = 10 * 1024 * 1024, maxRequestSize = 50 * 1024 * 1024) // Servlet 3.0 原生支持
public class UploadServlet extends HttpServlet {
private static final String BASE_UPLOAD_DIR = "/data/uploads";
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String email = null;
boolean success = true;
String dataId = String.valueOf(System.currentTimeMillis());
Path dataDir = Paths.get(BASE_UPLOAD_DIR, dataId, "data");
Files.createDirectories(dataDir);
// 解析 multipart 請求
for (Part part : req.getParts()) {
if (part.getSize() == 0) continue;
String fileName = Paths.get(part.getSubmittedFileName()).getFileName().toString(); // 防路徑遍歷
if (part.getName().equals("file")) { // 文件域
Path target = dataDir.resolve(fileName);
try (InputStream in = part.getInputStream()) {
Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
}
} else if (part.getName().equals("email")) { // 普通表單域
email = new String(part.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}
req.setAttribute("success", success);
req.setAttribute("email", email);
req.getRequestDispatcher("/view/result.jsp").forward(req, resp);
}
}
2. 郵件發送服務:支持附件、HTML 正文、異步發送
@Component
public class EmailService {
@Value("${mail.host:smtp.163.com}") private String host;
@Value("${mail.username}") private String username;
@Value("${mail.password}") private String password;
@Value("${mail.from}") private String from;
public void sendWithAttachments(String to, String subject, String htmlContent, List<File> attachments) {
try {
Properties props = new Properties();
props.put("mail.smtp.host", host);
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.port", "465");
props.put("mail.smtp.socketFactory.port", "465");
props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
Session session = Session.getInstance(props, new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipients(Message.RecipientType.TO, to);
message.setSubject(subject, "UTF-8");
// 構建 Multipart
Multipart multipart = new MimeMultipart();
// HTML 正文
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlContent, "text/html;charset=UTF-8");
multipart.addBodyPart(htmlPart);
// 附件
if (attachments != null) {
for (File file : attachments) {
MimeBodyPart attachPart = new MimeBodyPart();
attachPart.attachFile(file);
// 解決中文附件名亂碼
attachPart.setFileName(MimeUtility.encodeText(file.getName(), "UTF-8", "B"));
multipart.addBodyPart(attachPart);
}
}
message.setContent(multipart);
Transport.send(message);
} catch (MessagingException | IOException | UnsupportedEncodingException e) {
throw new RuntimeException("郵件發送失敗", e);
}
}
// 異步發送版本
@Async("emailExecutor")
public CompletableFuture<Void> sendAsync(String to, String subject, String html, List<File> files) {
sendWithAttachments(to, subject, html, files);
return CompletableFuture.completedFuture(null);
}
}
3. 前端頁面:表單上傳 + 進度提示
<form id="uploadForm" action="/tool/upload" method="post" enctype="multipart/form-data">
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-control" required />
</div>
<div class="form-group">
<label>文件</label>
<input type="file" name="file" class="form-control" required />
</div>
<button type="submit" class="btn btn-primary">上傳並發送郵件</button>
</form>
<script>
$('#uploadForm').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
$.ajax({
url: '/tool/upload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
xhr: function() {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener('progress', function(evt) {
if (evt.lengthComputable) {
var percent = Math.round(evt.loaded / evt.total * 100);
$('#progressBar').css('width', percent + '%').text(percent + '%');
}
}, false);
return xhr;
},
success: function(resp) {
alert('上傳成功,郵件發送中...');
},
error: function() { alert('上傳失敗'); }
});
});
</script>
關鍵點總結
| 模塊 | 關鍵點 |
| 上傳解析 | Servlet 3.0 @MultipartConfig + req.getParts() 零依賴、標準化;舊項目用 Commons FileUpload |
| 文件名安全 | Paths.get(submittedFileName).getFileName() 防止 ../../etc/passwd 路徑遍歷 |
| 大文件 | 流式複製 Files.copy(in, target),不載入內存;超大文件需分片上傳 |
| 郵件編碼 | MimeUtility.encodeText(filename, "UTF-8", "B") 解決中文附件名亂碼 |
| SSL/TLS | 465 端口 + SSLSocketFactory,或 587 + STARTTLS |
| 異步化 | @Async + 線程池,避免阻塞 HTTP 響應 |
現代替代建議
- 上傳:Spring MVC
MultipartFile、MinIO/S3 對象存儲、分片上傳庫 (Uppy、Plupload)。
- 郵件:Spring Boot
JavaMailSender 封裝、Thymeleaf 模板引擎渲染 HTML、隊列異步 (RabbitMQ/Kafka) + 重試補償。