Skip to content
第 61 章 后端 ⏱ 8 分钟阅读

第 61 章:文件上传与 WebSocket ​

学习目标 ​

  • 用 Spring MVC 处理文件上传
  • 用 MinIO 存文件
  • 学会 WebSocket 推送

一、文件上传 ​

1.1 单文件 ​

java
@PostMapping("/upload")
public Result<String> upload(@RequestParam MultipartFile file) {
    if (file.isEmpty()) {
        throw new BusinessException(ErrorCode.BAD_REQUEST, "文件为空");
    }
    String fileName = UUID.randomUUID() + "." + FilenameUtils.getExtension(file.getOriginalFilename());
    File dest = new File("/data/uploads/" + fileName);
    file.transferTo(dest);
    return Result.ok(fileName);
}

配置大小:

yaml
spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

1.2 多文件 ​

java
@PostMapping("/uploads")
public Result<List<String>> upload(@RequestParam MultipartFile[] files) {
    List<String> names = new ArrayList<>();
    for (MultipartFile file : files) {
        // ... 同上
    }
    return Result.ok(names);
}

1.3 大文件(分片上传) ​

java
@PostMapping("/upload/chunk")
public Result<Void> uploadChunk(@RequestParam String md5,
                                @RequestParam Integer chunk,
                                @RequestParam Integer chunks,
                                @RequestParam MultipartFile file) throws IOException {
    String dir = "/data/chunks/" + md5;
    Files.createDirectories(Path.of(dir));
    file.transferTo(new File(dir + "/" + chunk));
    return Result.ok();
}

@PostMapping("/upload/merge")
public Result<String> merge(@RequestParam String md5, @RequestParam String fileName) throws IOException {
    File chunkDir = new File("/data/chunks/" + md5);
    File dest = new File("/data/uploads/" + fileName);
    try (OutputStream os = new FileOutputStream(dest)) {
        for (File chunk : chunkDir.listFiles().stream().sorted(Comparator.comparingInt(f -> Integer.parseInt(f.getName()))).toList()) {
            Files.copy(chunk.toPath(), os);
        }
    }
    return Result.ok(fileName);
}

⚠️ 坑 1:生产别用本地存储。多实例部署文件不共享,容器重启文件丢失。用 MinIO / OSS。

二、MinIO 存储 ​

2.1 集成 ​

xml
<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio-java</artifactId>
    <version>8.5.10</version>
</dependency>
yaml
minio:
  endpoint: http://localhost:9000
  access-key: minioadmin
  secret-key: minioadmin
  bucket: my-bucket

2.2 配置 ​

java
@Configuration
public class MinioConfig {

    @Bean
    public MinioClient minioClient(MinioProperties props) {
        return MinioClient.builder()
            .endpoint(props.getEndpoint())
            .credentials(props.getAccessKey(), props.getSecretKey())
            .build();
    }
}

2.3 上传 ​

java
@Service
@RequiredArgsConstructor
public class MinioService {

    private final MinioClient minioClient;

    public String upload(MultipartFile file) throws Exception {
        String fileName = UUID.randomUUID() + "-" + file.getOriginalFilename();
        minioClient.putObject(
            PutObjectArgs.builder()
                .bucket("my-bucket")
                .object(fileName)
                .stream(file.getInputStream(), file.getSize(), -1)
                .contentType(file.getContentType())
                .build()
        );
        return fileName;
    }

    public String getUrl(String fileName) {
        return minioClient.getPresignedObjectUrl(
            GetPresignedObjectUrlArgs.builder()
                .bucket("my-bucket")
                .object(fileName)
                .method(Method.GET)
                .expiry(7, TimeUnit.DAYS)
                .build()
        );
    }
}

三、WebSocket ​

场景:实时推送(订单状态、聊天、通知)。

3.1 集成 ​

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3.2 配置 ​

java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/ws")
                .setAllowedOrigins("*");
    }
}

3.3 Handler ​

java
@Component
public class MyHandler extends TextWebSocketHandler {
    private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        SESSIONS.put(session.getId(), session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        // 收到消息处理
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        SESSIONS.remove(session.getId());
    }

    public void sendToAll(String msg) throws IOException {
        for (WebSocketSession s : SESSIONS.values()) {
            s.sendMessage(new TextMessage(msg));
        }
    }
}

3.4 客户端 ​

js
const ws = new WebSocket('ws://localhost:8080/ws')
ws.onmessage = (event) => console.log('收到:', event.data)

3.5 STOMP(适合复杂场景) ​

java
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic", "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws-stomp").setAllowedOrigins("*").withSockJS();
    }
}
java
@Controller
public class NotifyController {
    @MessageMapping("/notify")
    @SendTo("/topic/notice")
    public String notify(String message) {
        return "服务端收到: " + message;
    }

    @Autowired private SimpMessagingTemplate template;

    public void pushToUser(Long userId, String msg) {
        template.convertAndSendToUser(String.valueOf(userId), "/queue/notify", msg);
    }
}

⚠️ 坑 2:WebSocket 鉴权靠 token 放在 URL 里:/ws?token=xxx,在拦截器里验证。

四、本章小结 ​

要点关键
文件上传MultipartFile,注意大小
存储用 MinIO / OSS,不用本地
WebSocket简单用 Handler,复杂用 STOMP
鉴权token 放 URL

动手练习 ​

  1. 写一个上传接口,文件存到 MinIO
  2. 写一个 WebSocket,客户端发消息,服务端广播给所有人

下一章:第 62 章:监控与健康检查 →

本站基于 VitePress 构建 · 由 StackHub 团队维护