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

レッスン 15: ファイルのアップロード/ダウンロード、電子メール、WebSocket

検証付きのマルチパート ファイルのアップロード。ファイルダウンロードストリーミング。 Spring Mail および Thymeleaf テンプレートを使用して電子メールを送信します。 WebSocketとSTOMPによるリアルタイム通信。

💻 プログラミング — レッスン 14 レッスン 15: ファイルのアップロード/ダウンロード、メール送信、 Webソケット

Spring Boot 4: 基本から上級まで

パート 4: 高度な機能

xdev.asia

はじめに

ファイルのアップロード/ダウンロード、電子メール通知、WebSocket を介したリアルタイム通信という 3 つの機能は、すべてのエンタープライズ アプリケーションに共通しています。この記事では、各機能の本番環境に対応した実装について説明します。


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 を使用してシンプルなチャット ルームを実装します。ユーザーがルームに参加し、メッセージを送信し、リアルタイム メッセージを受信します。