SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用

摘要:本文主要介绍SpringBoot3版本的Http调用新方式,替换了传统的RestTemplate调用,改为了通过注解@HttpExchange + RestClient的方式,免去了繁琐的包装配置和工具类,就像写接口一样调用他人的接口,也可以用流式的写法更简单调用API

背景

以前服务间的远程调用,大家大多数采用的是FeignRestTemplate来调用;

  • Feign:已经进入了维护阶段了。
  • RestTemplateAPI调用繁琐。

所以SpringBoot3我们就使用一个更加简单的方式吧。

开始使用

pom.xml

maven依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

RestClientApplication

启动类

java 复制代码
@SpringBootApplication
public class RestClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(RestClientApplication.class, args);
    }
}

Dto

java 复制代码
@Data
public class ResponseMessage<T> {

    private int code;

    private String message;

    private T data;

    public static <T> ResponseMessage<T> success(T data){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setCode(200);
        responseMessage.setData(data);
        responseMessage.setMessage("success");
        return responseMessage;
    }

    public static <T> ResponseMessage<T> fail(String message){
        ResponseMessage<T> responseMessage = new ResponseMessage<>();
        responseMessage.setCode(500);
        responseMessage.setMessage(message);
        return responseMessage;
    }
}
java 复制代码
@Data
public class GoodsInfo {

    private Long id;

    private Integer number;

}

提供的服务GoodsController

java 复制代码
@Slf4j
@RestController
@RequestMapping(value = "goods")
public class GoodsController {

    @PostMapping("/create")
    public ResponseMessage<GoodsInfo> create(@RequestBody GoodsInfo goodsInfo) {
        log.info("创建商品");
        return ResponseMessage.success(goodsInfo);
    }

    @PostMapping("/create-form")
    public ResponseMessage<GoodsInfo> createForm(GoodsInfo goodsInfo) {
        log.info("创建商品");
        return ResponseMessage.success(goodsInfo);
    }

    @GetMapping("/info")
    public ResponseMessage<Long> info() {
        long l = System.currentTimeMillis();
        try{
            Thread.sleep(50);
        }catch (Exception ex) {

        }
        if(l%100 == 0) {
            return ResponseMessage.fail("异常");
        }
        return ResponseMessage.success(l);
    }

    @GetMapping("/detail/{id}")
    public ResponseMessage<Map> detail(@PathVariable(value = "id") Long id) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("id", id);
        resultMap.put("desc", "这是详情" + id);
        return ResponseMessage.success(resultMap);
    }
}

定义被调用的接口

GoodsClient接口

类似和以前的@Feign标注的接口

java 复制代码
public interface GoodsClient {

    @GetExchange("/goods/detail/{id}")
    ResponseMessage<Map> detail(@PathVariable(value = "id") Long id);

    @PostExchange(value = "/goods/upload", headers = {"Content-Type: multipart/form-data"})
    ResponseMessage<Map> upload(@RequestPart("file") MultipartFile file);

    @PostExchange(value = "/http-exchange/upload333")
    ResponseMessage<Map> upload333(@RequestPart("file") Resource file);

    @PostExchange("/goods/create")
    ResponseMessage<Map> create(@RequestBody GoodsInfo goodsInfo, @RequestParam("userId") String userId);

    @PostExchange("/goods/create-form")
    ResponseMessage<Map> createForm(@RequestParam Map<String, Object> goodsInfo, @RequestParam("userId") String userId);

}

LogExecChainHandler日志拦处理器

java 复制代码
@Slf4j
public class LogExecChainHandler implements ExecChainHandler {

    private static final String MULTIPART_CONTENT_TYPE = "multipart/form-data";
    private static final String APPLICATION_JSON = "application/json";
    private static final String TEXT_JSON = "text/json";
    private static final String CONTENT_TYPE_HEADER = "Content-Type";

    @Override
    public ClassicHttpResponse execute(ClassicHttpRequest request, ExecChain.Scope scope, ExecChain chain)
            throws IOException, HttpException {
        long startTime = System.currentTimeMillis();

        logRequest(request);

        try {
            ClassicHttpResponse response = chain.proceed(request, scope);
            long duration = System.currentTimeMillis() - startTime;
            logResponse(response, duration);
            return response;
        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            logError(request, duration, e);
            throw e;
        }
    }

    /**
     * 记录HTTP请求日志
     * @param request HTTP请求
     */
    private void logRequest(ClassicHttpRequest request) {
        try {
            String method = request.getMethod();
            String uri = request.getUri().toString();
            String headers = formatHeaders(request.getHeaders());
            String entityInfo = extractRequestEntityInfo(request);

            log.info("\n┌───── HTTP REQUEST ─────────────────────────────\n" +
                            "│ Method: {}\n" +
                            "│ URI: {}\n" +
                            "│ Headers: {}\n" +
                            "│ Body: {}\n" +
                            "└────────────────────────────────────────────────",
                    method, uri, headers, entityInfo);
        } catch (Exception e) {
            log.warn("记录请求日志时出错: {}", e.getMessage());
        }
    }

    /**
     * 提取请求实体信息
     * @param request HTTP请求
     * @return 请求体信息
     */
    private String extractRequestEntityInfo(ClassicHttpRequest request) {
        try {
            String contentType = getContentType(request.getHeaders());
            if (isMultipartFormData(contentType)) {
                return "[文件流请求体]";
            }
            return readJsonRequestBody(request);
        } catch (Exception e) {
            return "[读取请求体失败: " + e.getMessage() + "]";
        }
    }

    /**
     * 读取JSON请求体
     * @param request HTTP请求
     * @return JSON请求体内容
     */
    private String readJsonRequestBody(ClassicHttpRequest request) {
        try {
            HttpEntity entity = request.getEntity();
            if (entity == null) {
                return "[无请求体]";
            }

            byte[] content = entityToBytes(entity);
            if (content.length == 0) {
                return "[空JSON请求体]";
            }
            return new String(content, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.debug("读取JSON请求体失败: {}", e.getMessage());
            return "[读取JSON请求体失败]";
        }
    }

    /**
     * 记录HTTP响应日志
     * @param response HTTP响应
     * @param duration 请求耗时
     */
    private void logResponse(ClassicHttpResponse response, long duration) {
        try {
            String statusLine = response.getCode() + " " + response.getReasonPhrase();
            String headers = formatHeaders(response.getHeaders());
            String entityInfo = extractResponseEntityInfo(response);

            log.info("\n┌───── HTTP RESPONSE ────────────────────────────\n" +
                            "│ Status: {}\n" +
                            "│ Headers: {}\n" +
                            "│ Body: {}\n" +
                            "│ Duration: {}ms\n" +
                            "└────────────────────────────────────────────────",
                    statusLine, headers, entityInfo, duration);
        } catch (Exception e) {
            log.warn("记录响应日志时出错: {}", e.getMessage());
        }
    }

    /**
     * 提取响应实体信息
     * @param response HTTP响应
     * @return 响应体信息
     */
    private String extractResponseEntityInfo(ClassicHttpResponse response) {
        try {
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                return "[无响应体]";
            }

            if (isJsonResponse(entity, response.getHeaders())) {
                BufferedHttpEntity bufferedHttpEntity = new BufferedHttpEntity(entity);
                response.setEntity(bufferedHttpEntity);
                return readJsonResponseBody(bufferedHttpEntity);
            } else{
                return "[非JSON响应体]";
            }
        } catch (Exception e) {
            return "[读取响应体失败: " + e.getMessage() + "]";
        }
    }

    /**
     * 读取JSON响应体
     * @param entity HTTP实体
     * @return JSON响应体内容
     */
    private String readJsonResponseBody(HttpEntity entity) {
        try {
            byte[] content = entityToBytes(entity);
            if (content.length == 0) {
                return "[空JSON响应体]";
            }
            return new String(content, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.debug("读取JSON响应体失败: {}", e.getMessage());
            return "[读取JSON响应体失败]";
        }
    }

    /**
     * 记录错误日志
     * @param request HTTP请求
     * @param duration 请求耗时
     * @param error 异常信息
     */
    private void logError(ClassicHttpRequest request, long duration, Exception error) {
        try {
            String method = request.getMethod();
            String uri = request.getUri().toString();
            log.error("\n┌───── HTTP ERROR ───────────────────────────────\n" +
                            "│ Method: {}\n" +
                            "│ URI: {}\n" +
                            "│ Duration: {}ms\n" +
                            "│ Error: {}\n" +
                            "└────────────────────────────────────────────────",
                    method, uri, duration, error.getMessage());
        } catch (Exception e) {
            log.warn("记录错误日志时出错: {}", e.getMessage());
        }
    }

    /**
     * 获取Content-Type头部信息
     * @param headers 头部数组
     * @return Content-Type值
     */
    private String getContentType(Header[] headers) {
        return Arrays.stream(headers)
                .filter(h -> CONTENT_TYPE_HEADER.equalsIgnoreCase(h.getName()))
                .map(Header::getValue)
                .findFirst()
                .orElse(null);
    }

    /**
     * 格式化头部信息
     * @param headers 头部数组
     * @return 格式化后的头部信息
     */
    private String formatHeaders(Header[] headers) {
        if (headers == null || headers.length == 0) {
            return "";
        }
        return Arrays.stream(headers)
                .map(header -> header.getName() + ": " + header.getValue())
                .collect(Collectors.joining("; "));
    }

    /**
     * 判断是否为multipart/form-data类型
     * @param contentType Content-Type值
     * @return 是否为multipart/form-data类型
     */
    private boolean isMultipartFormData(String contentType) {
        return contentType != null && contentType.contains(MULTIPART_CONTENT_TYPE);
    }

    /**
     * 判断是否为JSON响应
     * @param entity HTTP实体
     * @param headers 头部信息
     * @return 是否为JSON响应
     */
    private boolean isJsonResponse(HttpEntity entity, Header[] headers) {
        String contentType = Optional.ofNullable(entity.getContentType())
                .orElse(getContentType(headers));

        return contentType != null &&
                (contentType.contains(APPLICATION_JSON) || contentType.contains(TEXT_JSON));
    }

    /**
     * 将HTTP实体转换为字节数组
     * @param entity HTTP实体
     * @return 字节数组
     * @throws IOException IO异常
     */
    private byte[] entityToBytes(HttpEntity entity) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        entity.writeTo(baos);
        return baos.toByteArray();
    }
}

ClientConfig注册接口为Spring管理的Bean

java 复制代码
@Slf4j
@Configuration
public class ClientConfig {

    @Bean
    public GoodsClient goodsClient() {
        String baseUrl = "http://localhost:8080";
        CloseableHttpClient httpClient = HttpClients.custom().addExecInterceptorFirst("log", new LogExecChainHandler()).build();
        RestClient restClient = RestClient.builder()
                .baseUrl(baseUrl)
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient) {{
                    setConnectTimeout(5000);
                    setReadTimeout(300000);
                }})
                .build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(GoodsClient.class);
    }

}
  • setConnectTimeout:连接超时时间
  • setReadTimeout:读取数据超时时间

HttpExchangeController测试入口

java 复制代码
@RestController
@RequestMapping(value = "http-exchange")
public class HttpExchangeController {

    @Autowired
    private GoodsClient goodsClient;

    @GetMapping(value = "create")
    public Map create() {
        GoodsInfo goodsInfo = new GoodsInfo();
        goodsInfo.setId(10L);
        goodsInfo.setNumber(2);
        return goodsClient.create(goodsInfo, "1000").getData();
    }

    @GetMapping(value = "create-form")
    public Map createForm() {
        GoodsInfo goodsInfo = new GoodsInfo();
        goodsInfo.setId(10L);
        goodsInfo.setNumber(2);
        return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData();
    }

    @GetMapping(value = "detail")
    public Map detail(Long id) {
        return goodsClient.detail(id).getData();
    }

    @GetMapping(value = "upload")
    public Map upload(Long id) throws Exception {
        File file = new File("C:\Users\xxx.zip");
        MultipartFile multipartFile = new MockMultipartFile(
                "file",  // 对应 @RequestPart("file")
                new FileInputStream(file)
        );
        return goodsClient.upload(multipartFile).getData();
    }

    @GetMapping(value = "upload2")
    public String upload2(Long id) throws Exception {
        Path path = Paths.get("C:\Users\xxx.zip");
        Resource fileResource = new FileSystemResource(path.toFile());
        goodsClient.upload333(fileResource);
         return "success";
    }

    @PostMapping("/upload333")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("文件为空");
        }

        try {
            // 获取文件名(注意:前端可能重命名,建议自定义文件名)
            String fileName = file.getOriginalFilename();
            // 保存路径(可根据需求修改,如 /data/uploads)
            String savePath = "./uploads/" + fileName;
            File saveDir = new File("./uploads");
            if (!saveDir.exists()) {
                saveDir.mkdirs(); // 创建目录
            }

            // 保存文件到本地
            file.transferTo(new File(savePath));

            return ResponseEntity.ok("文件上传成功,保存路径:" + savePath);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.internalServerError().body("文件上传失败:" + e.getMessage());
        }
    }

}

重点备注说明

GoodsClient定义的接口的参数必须要有参数相关的注解标注,不然会报错

文件上传接口如何定义

  • 推荐使用:@RequestPart("file") Resource file 这个类,这样文件就是流式传输,不是一次性加载到内存防止OOM
  • 不推荐使用:@RequestPart("file") MultipartFile file 这个类,它会一次性加载文件所有内容到内存,大文件很容易堆内存溢出。

参数不固定的接口定义

  • 推荐使用 @RequestParam Map<String, Object> mapParam:这样就能传输不固定的参数

其他接口

就像你写SpringBoot接口一样定义就好了。

为什么不用RestClient默认的拦截器配置

java 复制代码
@FunctionalInterface
public interface ClientHttpRequestInterceptor {

    ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
          throws IOException;

}

这个会把所有的内容加载到内存中,上传文件就会导致内存溢出,所以我就用了自定义的HttpClient

增强自定义注解使用@EnableRestClient

这个注解就像使用@EnableFeignClient一样

RestClient 注解

配置后扫描到就注册为Spring IOC管理的Bean

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RestClient {

    /**
     * 服务地址
     * @return
     */
    String url();

    /**
     * 连接超时时间(毫秒)
     * @return
     */
    int connectTimeout() default 5000;

    /**
     * 读取超时(毫秒)
     * @return
     */
    int readTimeout() default 10000;

}

EnableRestClient 启动注解

开启需要扫描的包目录

less 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RestClientRegistrar.class) // 导入注册器
public @interface EnableRestClient {

    /**
     * 扫描 @RestClient 接口的基础包(多个包用逗号分隔)
     */
    String[] scanPackages() default {};

}

RestClientFactoryBean 工厂类,提供类实例

提供具体的注册Bean对象到Spring管理中

java 复制代码
public class RestClientFactoryBean<T> implements FactoryBean<T> {

    private final Class<T> interfaceClass;
    private final String baseUrl;
    private final int connectTimeout;
    private final int readTimeout;

    public RestClientFactoryBean(Class<T> interfaceClass, String baseUrl, int connectTimeout, int readTimeout) {
        this.interfaceClass = interfaceClass;
        this.baseUrl = baseUrl;
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
    }

    @Override
    public T getObject() {
        CloseableHttpClient httpClient = HttpClients.custom().addExecInterceptorFirst("log", new LogExecChainHandler()).build();
        org.springframework.web.client.RestClient restClient = RestClient.builder()
                .baseUrl(baseUrl)
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient) {{
                    setConnectTimeout(connectTimeout);
                    setReadTimeout(readTimeout);
                }})
                .build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(interfaceClass);
    }

    @Override
    public Class<?> getObjectType() {
        return interfaceClass;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }

}

RestClientRegistrar 扫描注册逻辑实现

负责扫描指定包下的@RestClient接口,并为每个接口注册Bean定义。

java 复制代码
public class RestClientRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 1. 获取 @EnableRestClient 注解的扫描包配置
        String[] scanPackages = getScanPackages(importingClassMetadata);

        // 2. 扫描所有标记了 @RestClient 的接口
        ClassPathScanningCandidateComponentProvider scanner = createScanner();
        scanner.addIncludeFilter(new AnnotationTypeFilter(RestClient.class));

        // 3. 遍历扫描结果,注册Bean定义
        for (String basePackage : scanPackages) {
            Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
            for (BeanDefinition candidate : candidates) {
                if (candidate instanceof AnnotatedBeanDefinition) {
                    registerRestClientBean(registry, (AnnotatedBeanDefinition) candidate);
                }
            }
        }
    }

    /**
     * 提取扫描包路径(优先使用 @EnableRestClient 指定的 scanPackages)
     */
    private String[] getScanPackages(AnnotationMetadata importingClassMetadata) {
        // 从 @EnableRestClient 中获取 scanPackages
        String[] annotationScanPackages = (String[]) importingClassMetadata.getAnnotationAttributes(
                EnableRestClient.class.getName()
        ).get("scanPackages");

        // 过滤空包,若用户未指定则使用导入类的包
        Set<String> validPackages = new HashSet<>();
        for (String pkg : annotationScanPackages) {
            if (StringUtils.hasText(pkg)) {
                validPackages.add(pkg.trim());
            }
        }
        if (validPackages.isEmpty()) {
            String defaultPackage = ClassUtils.getPackageName(importingClassMetadata.getClassName());
            validPackages.add(defaultPackage);
        }
        return validPackages.toArray(new String[0]);
    }

    /**
     * 创建支持 @RestClient 注解的扫描器
     */
    private ClassPathScanningCandidateComponentProvider createScanner() {
        return new ClassPathScanningCandidateComponentProvider(false) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                // 仅扫描接口类型的Bean
                return beanDefinition.getMetadata().isInterface();
            }
        };
    }

    /**
     * 为 @RestClient 接口注册Bean定义(使用FactoryBean生成代理)
     */
    private void registerRestClientBean(BeanDefinitionRegistry registry, AnnotatedBeanDefinition beanDefinition) {
        AnnotationMetadata metadata = beanDefinition.getMetadata();
        String className = metadata.getClassName();

        // 获取 @RestClient 注解属性
        Map<String, Object> restClientAttributes = metadata.getAnnotationAttributes(RestClient.class.getName());
        String url = (String) restClientAttributes.get("url");
        int connectTimeout = (int) restClientAttributes.get("connectTimeout");
        int readTimeout = (int) restClientAttributes.get("readTimeout");

        // 校验URL必填
        if (!StringUtils.hasText(url)) {
            throw new IllegalArgumentException("@RestClient.url() must not be empty");
        }

        // 创建Bean定义(使用FactoryBean)
        GenericBeanDefinition definition = new GenericBeanDefinition();
        definition.setBeanClass(RestClientFactoryBean.class);
        definition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);

        // 设置构造参数:接口类、基础URL、配置类
        definition.getConstructorArgumentValues().addGenericArgumentValue(className);
        definition.getConstructorArgumentValues().addGenericArgumentValue(url);
        definition.getConstructorArgumentValues().addGenericArgumentValue(connectTimeout);
        definition.getConstructorArgumentValues().addGenericArgumentValue(readTimeout);

        // 注册Bean(Bean名称默认使用接口全限定名的小写开头)
        String beanName = generateBeanName(metadata);
        registry.registerBeanDefinition(beanName, definition);
    }

    /**
     * 生成Bean名称(格式:restClient.接口名)
     */
    private String generateBeanName(AnnotationMetadata metadata) {
        String interfaceName = metadata.getClassName().substring(metadata.getClassName().lastIndexOf('.') + 1);
        return "restClient." + interfaceName.toLowerCase();
    }
}

GoodsClient实际的接口类

less 复制代码
@RestClient(url = "http://localhost:8080")
public interface GoodsClient {

    @GetExchange("/goods/detail/{id}")
    ResponseMessage<Map> detail(@PathVariable(value = "id") Long id);

    @PostExchange(value = "/goods/upload", headers = {"Content-Type: multipart/form-data"})
    ResponseMessage<Map> upload(@RequestPart("file") MultipartFile file);

    @PostExchange(value = "/http-exchange/upload333")
    ResponseMessage<Map> upload333(@RequestPart("file") Resource file);

    @PostExchange("/goods/create")
    ResponseMessage<Map> create(@RequestBody GoodsInfo goodsInfo, @RequestParam("userId") String userId);

    @PostExchange("/goods/create-form")
    ResponseMessage<Map> createForm(@RequestParam Map<String, Object> goodsInfo, @RequestParam("userId") String userId);

}

HttpExchangeController使用案例

和正常的Spring对象注入一样。

java 复制代码
@RestController
@RequestMapping(value = "http-exchange")
public class HttpExchangeController {

    @Autowired
    private GoodsClient goodsClient;

    @GetMapping(value = "create")
    public Map create() {
        GoodsInfo goodsInfo = new GoodsInfo();
        goodsInfo.setId(10L);
        goodsInfo.setNumber(2);
        return goodsClient.create(goodsInfo, "1000").getData();
    }

    @GetMapping(value = "create-form")
    public Map createForm() {
        GoodsInfo goodsInfo = new GoodsInfo();
        goodsInfo.setId(10L);
        goodsInfo.setNumber(2);
        return goodsClient.createForm(BeanUtil.beanToMap(goodsInfo), "1000").getData();
    }

    @GetMapping(value = "detail")
    public Map detail(Long id) {
        return goodsClient.detail(id).getData();
    }

    @GetMapping(value = "upload")
    public Map upload(Long id) throws Exception {
        File file = new File("C:\Users\xxx.zip");
        MultipartFile multipartFile = new MockMultipartFile(
                "file",  // 对应 @RequestPart("file")
                new FileInputStream(file)
        );
        return goodsClient.upload(multipartFile).getData();
    }

    @GetMapping(value = "upload2")
    public String upload2(Long id) throws Exception {
        Path path = Paths.get("C:\Users\xxx.zip");
        Resource fileResource = new FileSystemResource(path.toFile());
        goodsClient.upload333(fileResource);
         return "success";
    }

    @PostMapping("/upload333")
    public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("文件为空");
        }

        try {
            // 获取文件名(注意:前端可能重命名,建议自定义文件名)
            String fileName = file.getOriginalFilename();
            // 保存路径(可根据需求修改,如 /data/uploads)
            String savePath = "./uploads/" + fileName;
            File saveDir = new File("./uploads");
            if (!saveDir.exists()) {
                saveDir.mkdirs(); // 创建目录
            }

            // 保存文件到本地
            file.transferTo(new File(savePath));

            return ResponseEntity.ok("文件上传成功,保存路径:" + savePath);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.internalServerError().body("文件上传失败:" + e.getMessage());
        }
    }

}
相关推荐
架构师沉默2 小时前
设计多租户 SaaS 系统,如何做到数据隔离 & 资源配额?
java·后端·架构
RoyLin2 小时前
TypeScript设计模式:适配器模式
前端·后端·node.js
该用户已不存在3 小时前
Mojo vs Python vs Rust: 2025年搞AI,该学哪个?
后端·python·rust
Moonbit3 小时前
MoonBit 正式加入 WebAssembly Component Model 官方文档 !
前端·后端·编程语言
Goland猫3 小时前
电商架构图
后端
Java中文社群3 小时前
重要:Java25正式发布(长期支持版)!
java·后端·面试
我是天龙_绍3 小时前
Whisper 通过 mp3输出中文
后端
zjjuejin3 小时前
Maven环境搭建
后端·maven
我是天龙_绍3 小时前
项目根目录有requirements.txt 如何安装
后端