Chuyển đến nội dung chính

第 15 課:文件上傳/下載、電子郵件和 WebSocket

帶驗證的分段文件上傳。文件下載流。使用 Spring Mail 和 Thymeleaf 範本發送電子郵件。與 WebSocket 和 STOMP 進行即時通訊。

💻 程式設計 — 第 14 課 第 15 課:文件上傳/下載、電子郵件和 WebSockets

Spring Boot 4:從基礎到高級

第 4 部分:進階功能

亞洲開發網

簡介

所有企業應用程式共有三個功能:文件上傳/下載、電子郵件通知以及透過 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 用於即時通訊:廣播(主題)、私人訊息(佇列)、伺服器推播通知

練習

  1. 實作檔案上傳API:支援圖片(JPEG、PNG、WebP),最大5MB。驗證內容類型並建立縮圖
  2. 建立一個電子郵件服務,在使用者註冊時發送帶有 HTML 範本的歡迎電子郵件。使用@Async不阻塞 3.使用WebSocket實現簡單的聊天室:使用者加入房間、發送訊息、接收即時訊息