文件上传

下面的所以包都是支持jakarta的依赖,Spring Boot 3 已经完全抛弃javax,如何当前的版本还有javax的依赖,请升级Spring Boot 版本

编写MultipartAutoConfiguration

import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.Servlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.support.StandardServletMultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;

/**
 * @Title: MultipartAutoConfiguration
 * @Author David
 * @Package com.work.spring.springbootwork.config
 * @Date 2024/9/15 下午4:38
 * @description:
 */
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
    private final MultipartProperties multipartProperties;

    public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
        this.multipartProperties = multipartProperties;
    }

    @Bean
    @ConditionalOnMissingBean(MultipartConfigElement.class) // 移除了 CommonsMultipartResolver.class
    public MultipartConfigElement multipartConfigElement() {
        return this.multipartProperties.createMultipartConfig();
    }

    @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
    @ConditionalOnMissingBean(MultipartResolver.class)
    public StandardServletMultipartResolver multipartResolver() {
        StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
        multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
        return multipartResolver;
    }
}

编写FileUploadController

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

/**
 * @Title: FileUploadController
 * @Author David
 * @Package com.work.spring.springbootwork.controller
 * @Date 2024/9/15 下午4:54
 */
@RestController
public class FileUploadController {

    // 上传文件保存目录
    private static final String UPLOADED_FOLDER = System.getProperty("user.dir") + "/upload/";

    @PostMapping("/api/upload")
    public Map<String, Object> singleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {
        Map<String, Object> response = new HashMap<>();

        if (file.isEmpty()) {
            response.put("status", "error");
            response.put("message", "文件为空,请选择你的文件上传");
            return response;
        }

        saveFile(file);
        response.put("status", "success");
        response.put("message", "上传文件 " + file.getOriginalFilename() + " 成功");
        response.put("url", "/api/download/" + file.getOriginalFilename());

        return response;
    }

    // 保存上传的文件
    private void saveFile(MultipartFile file) throws IOException {
        Path path = Paths.get(UPLOADED_FOLDER+file.getOriginalFilename());

        // 如果目录不存在,则创建目录
        if (!Files.exists(path.getParent())) {
            Files.createDirectories(path.getParent());
        }

        // 保存文件
        file.transferTo(path);
    }

    // 下载文件的接口
    @GetMapping("/api/download/{filename:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        try {
            Path filePath = Paths.get(UPLOADED_FOLDER).resolve(filename).normalize();
            Resource resource = new UrlResource(filePath.toUri());

            if (resource.exists() && resource.isReadable()) {
                // 设置响应头,提示浏览器下载文件
                return ResponseEntity.ok()
                        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                        .body(resource);
            } else {
                return ResponseEntity.status(404).body(null);
            }
        } catch (MalformedURLException e) {
            return ResponseEntity.status(500).body(null);
        }
    }
}

浅谈MultipartAutoConfiguration中的细节

- 注解@ConditionalOnClass

我们先谈一个重要的概念,类路径,对月类路径你是怎么理解的,我取一个大家比较熟悉的语言Java和大家浅聊一下,在你启动SpringBoot项目的时候,你是否有思考过它的运行过程,它从@SpringBootApplication注解的启动类中进入程序,然后向什么方向发展呢?

不考虑类加载时机的化,这个程序的运行过程就像一棵由依赖组成的树,这棵树的根就是启动类。

20240903220327.png

在我们调用服务就像从根进入指定节点一样,在到达这个节点前,我们需要经过其他节点,经过其他节点时,我们便经过了类路径,那些箭头就是所谓的类路径,是一个服务所需要的或者是依赖


@ConditionalOnClassSpring Boot 中的一个条件注解,它的作用是根据类路径中是否存在指定的类,决定是否加载当前的配置类或 @Bean 定义。只有当指定的类存在于类路径时,配置才会被加载。这是 Spring Boot 自动配置的重要机制之一,它使得配置在合适的环境下才会生效,从而避免了不必要的配置加载。

  • 主要功能

    • 条件加载配置类或 @Bean:@ConditionalOnClass 只有在类路径中存在指定的类时,才会加载所标注的配置类或 @Bean 方法。 提高模块化和兼容性:通过这个注解,Spring Boot 可以在需要时加载相关功能模块,而不会在不需要时加载。它允许不同的库根据类路径中的可用类进行自定义自动配置。
  • 示例

@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })

- 注解@ConditionalOnProperty

@ConditionalOnProperty 是 Spring Boot 提供的一个非常强大的注解,用于根据配置文件中的属性值来决定是否启用某个 @Configuration 类或 @Bean。它允许你通过 Spring 配置文件(如 application.propertiesapplication.yml)中的属性值,动态控制配置的加载。这使得应用程序更加灵活,可以根据不同的环境或需求启用或禁用特定的功能。

  • 主要作用

@ConditionalOnProperty 的主要作用是:根据配置文件中的某个属性的值来决定是否启用某个 Spring 配置类或 @Bean

例如,你可以在 application.properties 中定义某个属性,当它为 true 时启用某个功能模块;为 false 或者没有定义时则不启用。

  • 参数详解
  1. prefix

    • 这是配置属性的前缀,通常用于结构化组织配置。例如,如果你有多个相关的配置项,前缀可以帮助你将它们组合在一起。例如 spring.servlet.multipart 是文件上传相关的配置项的前缀。

    示例:

    @ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled")
    
  2. name

    • 这是具体要检查的配置属性的名称。你可以通过这个参数指定一个或多个属性名称。

    示例:

    @ConditionalOnProperty(name = "feature.enabled")
    // 配置文件中 "feature.enabled" 时才启用文件上传配置
    
  3. havingValue

    • 这是指定的属性值,只有当属性的值等于 havingValue 时,条件才会成立,配置才会生效。如果没有指定 havingValue,则只检查属性是否存在。

    示例:

    @ConditionalOnProperty(name = "feature.mode", havingValue = "advanced")
    // 配置文件中 "feature.mode=advanced" 时才启用
    
  4. matchIfMissing

    • 当属性缺失时的行为。如果设置为 true,即使属性没有出现在配置文件中,配置类也会被加载。如果设置为 false,则只有当属性存在并且满足条件时,配置类才会加载。

    示例:

    @ConditionalOnProperty(name = "feature.enabled", matchIfMissing = true)
    // 在没有配置的时候默认启用
    

- 其余注解粗讲

  1. @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET):确保该配置类仅在 Servlet 类型的 Web 应用中生效,比如基于 Spring MVC 的应用。如果是响应式 Web 应用,则不会加载这个配置。

  2. @EnableConfigurationProperties(MultipartProperties.class):启用 MultipartProperties 类的属性绑定功能,允许从配置文件中加载与文件上传相关的属性(如文件大小、临时存储路径等)。

  3. @ConditionalOnMissingBean(MultipartConfigElement.class):确保只有在没有其他 MultipartConfigElement Bean 定义时,才创建一个默认的 MultipartConfigElement Bean。这种机制常用于 Spring Boot 的自动配置中,以避免覆盖用户的自定义配置。