第 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: 100MB1.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-bucket2.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 |
动手练习
- 写一个上传接口,文件存到 MinIO
- 写一个 WebSocket,客户端发消息,服务端广播给所有人
下一章:第 62 章:监控与健康检查 →