一、文件上传下载
1. 概述
| 功能 | 说明 |
|---|---|
| 文件上传 | 前端将文件发送到后端 |
| 文件下载 | 后端将文件发送到前端 |
| 文件存储 | 本地存储或云存储(OSS) |
2. 文件上传配置
application.properties:
# 文件上传配置
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
# 文件存储路径
app.file.upload-dir=uploads
3. 文件上传 API
FileController.java
package com.example.myapp.controller;
import com.example.myapp.dto.FileUploadResponse;
import com.example.myapp.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/files")
public class FileController {
@Autowired
private FileService fileService;
// 上传单个文件
@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(
@RequestParam("file") MultipartFile file
) {
FileUploadResponse response = fileService.uploadFile(file);
return ResponseEntity.ok(response);
}
// 上传多个文件
@PostMapping("/upload/multiple")
public ResponseEntity<?> uploadFiles(
@RequestParam("files") MultipartFile[] files
) {
return ResponseEntity.ok(fileService.uploadFiles(files));
}
// 下载文件
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
Resource resource = fileService.downloadFile(filename);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + resource.getFilename() + "\""
)
.body(resource);
}
// 删除文件
@DeleteMapping("/{filename}")
public ResponseEntity<Void> deleteFile(@PathVariable String filename) {
fileService.deleteFile(filename);
return ResponseEntity.noContent().build();
}
}
FileService.java
package com.example.myapp.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
@Service
public class FileService {
@Value("${app.file.upload-dir}")
private String uploadDir;
private Path getUploadPath() {
return Paths.get(uploadDir).toAbsolutePath().normalize();
}
// 上传单个文件
public FileUploadResponse uploadFile(MultipartFile file) {
// 验证文件
if (file.isEmpty()) {
throw new RuntimeException("文件不能为空");
}
// 创建上传目录
Path uploadPath = getUploadPath();
if (!Files.exists(uploadPath)) {
try {
Files.createDirectories(uploadPath);
} catch (IOException e) {
throw new RuntimeException("创建上传目录失败", e);
}
}
// 生成文件名(避免文件名冲突)
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf("."));
String newFilename = System.currentTimeMillis() + fileExtension;
// 保存文件
Path targetLocation = uploadPath.resolve(newFilename);
try {
Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
// 返回文件信息
return new FileUploadResponse(
newFilename,
originalFilename,
file.getContentType(),
file.getSize(),
"/api/files/download/" + newFilename
);
}
// 上传多个文件
public List<FileUploadResponse> uploadFiles(MultipartFile[] files) {
List<FileUploadResponse> responses = new ArrayList<>();
for (MultipartFile file : files) {
responses.add(uploadFile(file));
}
return responses;
}
// 下载文件
public Resource downloadFile(String filename) {
try {
Path filePath = getUploadPath().resolve(filename).normalize();
Resource resource = new UrlResource(filePath.toUri());
if (!resource.exists()) {
throw new RuntimeException("文件不存在");
}
return resource;
} catch (Exception e) {
throw new RuntimeException("文件下载失败", e);
}
}
// 删除文件
public void deleteFile(String filename) {
try {
Path filePath = getUploadPath().resolve(filename).normalize();
Files.deleteIfExists(filePath);
} catch (IOException e) {
throw new RuntimeException("文件删除失败", e);
}
}
}
FileUploadResponse.java
package com.example.myapp.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class FileUploadResponse {
private String filename; // 文件名
private String originalName; // 原始文件名
private String contentType; // 文件类型
private Long size; // 文件大小
private String downloadUrl; // 下载地址
}
4. 前端上传示例
React 示例
import { useState } from 'react';
import axios from 'axios';
function FileUpload() {
const [file, setFile] = useState(null);
const [uploadProgress, setUploadProgress] = useState(0);
const handleFileChange = (e) => {
setFile(e.target.files[0]);
};
const handleUpload = async () => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
const response = await axios.post(
'http://localhost:8080/api/files/upload',
formData,
{
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setUploadProgress(progress);
}
}
);
console.log('上传成功:', response.data);
alert('上传成功!');
} catch (error) {
console.error('上传失败:', error);
alert('上传失败!');
}
};
return (
<div>
<h2>文件上传</h2>
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload}>上传</button>
{uploadProgress > 0 && <div>上传进度:{uploadProgress}%</div>}
</div>
);
}
export default FileUpload;
5. API 测试
上传文件
请求:
POST http://localhost:8080/api/files/upload
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="test.jpg"
Content-Type: image/jpeg
[二进制文件内容]
------WebKitFormBoundary--
响应:
{
"filename": "1711465200000.jpg",
"originalName": "test.jpg",
"contentType": "image/jpeg",
"size": 102400,
"downloadUrl": "/api/files/download/1711465200000.jpg"
}
下载文件
请求:
GET http://localhost:8080/api/files/download/1711465200000.jpg
响应:
[二进制文件内容]
二、定时任务
1. 概述
| 类型 | 说明 |
|---|---|
| 固定延迟 | 任务完成后延迟指定时间再次执行 |
| 固定速率 | 固定间隔时间执行 |
| Cron 表达式 | 按照时间规则执行 |
2. 启用定时任务
启动类添加注解:
@SpringBootApplication
@EnableScheduling // 启用定时任务
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
3. 定时任务示例
ScheduledTask.java
package com.example.myapp.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class ScheduledTask {
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 固定延迟:任务完成后延迟 5 秒再次执行
@Scheduled(fixedDelay = 5000)
public void fixedDelayTask() {
System.out.println("固定延迟任务执行时间: " +
LocalDateTime.now().format(formatter));
}
// 固定速率:每 5 秒执行一次
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
System.out.println("固定速率任务执行时间: " +
LocalDateTime.now().format(formatter));
}
// Cron 表达式:每天凌晨 2 点执行
@Scheduled(cron = "0 0 2 * * ?")
public void cronTask() {
System.out.println("Cron 任务执行时间: " +
LocalDateTime.now().format(formatter));
}
// Cron 表达式:每分钟执行
@Scheduled(cron = "0 * * * * ?")
public void cronTaskEveryMinute() {
System.out.println("每分钟任务执行时间: " +
LocalDateTime.now().format(formatter));
}
// Cron 表达式:每 10 秒执行
@Scheduled(cron = "*/10 * * * * ?")
public void cronTaskEvery10Seconds() {
System.out.println("每 10 秒任务执行时间: " +
LocalDateTime.now().format(formatter));
}
}
4. Cron 表达式详解
格式
秒 分 时 日 月 周
示例
| Cron 表达式 | 说明 |
|---|---|
0 0 2 * * ? | 每天凌晨 2 点执行 |
0 */5 * * * ? | 每 5 分钟执行 |
0 0 12 * * ? | 每天中午 12 点执行 |
0 0 12 ? * MON | 每周一中午 12 点执行 |
0 0 0 1 * ? | 每月 1 号凌晨执行 |
0 0 8 ? * MON-FRI | 工作日上午 8 点执行 |
0 0 0 * * 6,7 | 每周末凌晨执行 |
5. 动态配置定时任务
ScheduledTaskConfig.java
package com.example.myapp.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@Configuration
public class ScheduledTaskConfig implements SchedulingConfigurer {
@Value("${app.scheduling.cron}")
private String cron;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> {
System.out.println("动态定时任务执行: " + LocalDateTime.now());
}, cron);
}
}
application.properties:
app.scheduling.cron=*/10 * * * * ?
6. 异步任务
AsyncConfig.java
package com.example.myapp.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync // 启用异步任务
public class AsyncConfig {
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 核心线程数
executor.setMaxPoolSize(10); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
AsyncService.java
package com.example.myapp.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
// 异步执行任务
@Async
public void asyncTask() {
System.out.println("异步任务开始: " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步任务结束: " + Thread.currentThread().getName());
}
// 异步执行任务(带返回值)
@Async
public java.util.concurrent.Future<String> asyncTaskWithResult() {
System.out.println("异步任务(带返回值): " + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new java.util.concurrent.AsyncResult<>("任务完成");
}
}
三、消息队列(RabbitMQ)
1. 概述
| 组件 | 说明 |
|---|---|
| 生产者 | 发送消息 |
| 消费者 | 接收消息 |
| 队列 | 存储消息 |
| 交换机 | 路由消息 |
2. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3. RabbitMQ 配置
application.properties:
# RabbitMQ 配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 消息确认
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
4. 配置类
RabbitMQConfig.java
package com.example.myapp.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
// 队列
public static final String QUEUE_NAME = "my_queue";
// 交换机
public static final String EXCHANGE_NAME = "my_exchange";
// 路由键
public static final String ROUTING_KEY = "my_routing_key";
// 声明队列
@Bean
public Queue queue() {
return new Queue(QUEUE_NAME, true);
}
// 声明交换机
@Bean
public TopicExchange exchange() {
return new TopicExchange(EXCHANGE_NAME);
}
// 绑定队列和交换机
@Bean
public Binding binding(Queue queue, TopicExchange exchange) {
return BindingBuilder.bind(queue)
.to(exchange)
.with(ROUTING_KEY);
}
}
5. 消息生产者
MessageProducer.java
package com.example.myapp.producer;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MessageProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
// 发送消息
public void sendMessage(String message) {
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_NAME,
RabbitMQConfig.ROUTING_KEY,
message
);
System.out.println("发送消息: " + message);
}
}
6. 消息消费者
MessageConsumer.java
package com.example.myapp.consumer;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class MessageConsumer {
// 监听队列
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void receiveMessage(String message) {
System.out.println("接收消息: " + message);
}
}
7. 消息对象
UserMessage.java
package com.example.myapp.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserMessage {
private Long id;
private String name;
private String email;
}
发送对象消息
@Component
public class MessageProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
// 发送对象消息
public void sendUserMessage(UserMessage userMessage) {
rabbitTemplate.convertAndSend(
RabbitMQConfig.EXCHANGE_NAME,
RabbitMQConfig.ROUTING_KEY,
userMessage
);
System.out.println("发送用户消息: " + userMessage);
}
}
接收对象消息
@Component
public class MessageConsumer {
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void receiveUserMessage(UserMessage userMessage) {
System.out.println("接收用户消息: " + userMessage);
}
}
8. 测试消息队列
@SpringBootTest
public class MessageQueueTest {
@Autowired
private MessageProducer messageProducer;
@Test
public void testSendMessage() {
messageProducer.sendMessage("Hello, RabbitMQ!");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Test
public void testSendUserMessage() {
UserMessage userMessage = new UserMessage(1L, "张三", "zhangsan@example.com");
messageProducer.sendUserMessage(userMessage);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
四、总结
| 功能 | 说明 |
|---|---|
| 文件上传 | 使用 MultipartFile 处理文件上传 |
| 文件下载 | 使用 Resource 返回文件 |
| 定时任务 | 使用 @Scheduled 注解 |
| Cron 表达式 | 秒 分 时 日 月 周 |
| 异步任务 | 使用 @Async 注解 |
| 消息队列 | RabbitMQ 实现消息收发 |