10. Spring Boot 高级功能

4 阅读3分钟

一、文件上传下载

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 实现消息收发