簡介
所有企業應用程式共有三個功能:文件上傳/下載、電子郵件通知以及透過 WebSocket 進行即時通訊。本文指導每個功能的生產就緒實施。
1. 文件上傳
1.1 配置
# application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
app:
upload:
dir: ./uploads
allowed-types: image/jpeg,image/png,image/webp,application/pdf
1.2 上傳控制器
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private final FileStorageService fileStorageService;
public FileController(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@PostMapping("/upload")
public ResponseEntity<FileResponse> uploadFile(
@RequestParam("file") MultipartFile file) {
FileResponse response = fileStorageService.store(file);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@PostMapping("/upload-multiple")
public ResponseEntity<List<FileResponse>> uploadMultiple(
@RequestParam("files") List<MultipartFile> files) {
List<FileResponse> responses = files.stream()
.map(fileStorageService::store)
.toList();
return ResponseEntity.status(HttpStatus.CREATED).body(responses);
}
}
1.3 檔案儲存服務
@Service
public class FileStorageService {
private final Path uploadDir;
private final List<String> allowedTypes;
public FileStorageService(
@Value("${app.upload.dir}") String uploadDir,
@Value("${app.upload.allowed-types}") List<String> allowedTypes) {
this.uploadDir = Path.of(uploadDir).toAbsolutePath().normalize();
this.allowedTypes = allowedTypes;
try {
Files.createDirectories(this.uploadDir);
} catch (IOException e) {
throw new RuntimeException("Cannot create upload directory", e);
}
}
public FileResponse store(MultipartFile file) {
// Validate
if (file.isEmpty()) {
throw new BadRequestException("File is empty");
}
if (!allowedTypes.contains(file.getContentType())) {
throw new BadRequestException("File type not allowed: "
+ file.getContentType());
}
// Generate unique filename
String extension = StringUtils.getFilenameExtension(
file.getOriginalFilename());
String fileName = UUID.randomUUID() + "." + extension;
// Prevent path traversal
Path targetPath = uploadDir.resolve(fileName).normalize();
if (!targetPath.startsWith(uploadDir)) {
throw new BadRequestException("Invalid file path");
}
try {
Files.copy(file.getInputStream(), targetPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to store file", e);
}
return new FileResponse(fileName, file.getContentType(), file.getSize(),
"/api/v1/files/download/" + fileName);
}
}
2. 檔案下載
@GetMapping("/download/{fileName}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
Path filePath = uploadDir.resolve(fileName).normalize();
if (!filePath.startsWith(uploadDir)) {
throw new BadRequestException("Invalid file path");
}
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
throw new ResourceNotFoundException("File", "name", fileName);
}
String contentType = Files.probeContentType(filePath);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(
contentType != null ? contentType : "application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
3. 使用 Spring Mail 傳送電子郵件
3.1 配置
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail.smtp.auth: true
mail.smtp.starttls.enable: true
3.2 電子郵件服務
@Service
public class EmailService {
private final JavaMailSender mailSender;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
// Text email
public void sendSimpleEmail(String to, String subject, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(text);
message.setFrom("[email protected]");
mailSender.send(message);
}
// HTML email
public void sendHtmlEmail(String to, String subject,
String htmlContent) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlContent, true);
helper.setFrom("[email protected]");
mailSender.send(message);
}
// Email với attachment
public void sendEmailWithAttachment(String to, String subject,
String text, Path attachment)
throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text);
helper.addAttachment(attachment.getFileName().toString(),
new FileSystemResource(attachment));
mailSender.send(message);
}
}
4.WebSocket 和 STOMP
4.1 WebSocket 配置
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("http://localhost:3000")
.withSockJS();
}
}
4.2 WebSocket 控制器
@Controller
public class ChatController {
@MessageMapping("/chat.send")
@SendTo("/topic/messages")
public ChatMessage sendMessage(ChatMessage message) {
message.setTimestamp(Instant.now());
return message;
}
@MessageMapping("/chat.private")
@SendToUser("/queue/private")
public ChatMessage sendPrivateMessage(
@Payload ChatMessage message,
Principal principal) {
message.setSender(principal.getName());
return message;
}
}
4.3 伺服器推播通知
@Service
public class NotificationPushService {
private final SimpMessagingTemplate messagingTemplate;
public NotificationPushService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// Broadcast tới tất cả subscriber
public void broadcastNotification(NotificationMessage notification) {
messagingTemplate.convertAndSend("/topic/notifications", notification);
}
// Gửi tới user cụ thể
public void sendToUser(String username, NotificationMessage notification) {
messagingTemplate.convertAndSendToUser(
username, "/queue/notifications", notification);
}
}
總結
- 檔案上傳與驗證(類型、大小)、路徑遍歷預防和唯一檔案名稱生成
- Spring Mail 支援文字、HTML 和附件電子郵件 — 與 @Async 結合以實現非阻塞傳送
- WebSocket + STOMP 用於即時通訊:廣播(主題)、私人訊息(佇列)、伺服器推播通知
練習
- 實作檔案上傳API:支援圖片(JPEG、PNG、WebP),最大5MB。驗證內容類型並建立縮圖
- 建立一個電子郵件服務,在使用者註冊時發送帶有 HTML 範本的歡迎電子郵件。使用@Async不阻塞 3.使用WebSocket實現簡單的聊天室:使用者加入房間、發送訊息、接收即時訊息