飞道的博客

文件上传oss,并查询上传进度(SpringBoot+Redis+Oss+Swagger3)

354人阅读  评论(0)

诉求

       将文件上传到oss,并实时监听上传进度,并将进度进行存储。实现这个功能的由来是有可能上传的文件较大,并不能在调用上传接口得到文件上传成功或者失败的回应

技术选型

  • SpringBoot 2.4.0:选用SpringBoot可以进行快速开发迭代,社区支持力度较大,搜索问题较为方便
  • Redis:使用Redis当作文件进度的缓存,并设置过期时间
  • Oss:选取Aliyun Oss作为文件存储管理器
  • Swagger3:使用Swagger3可以让后端开发更便捷的在页面上操作接口,方便了接口之间的操作

pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
        <java.encoding>UTF-8</java.encoding>
        <slf4j.version>1.7.30</slf4j.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!--   集成swagger2代     -->
<!--        <dependency>-->
<!--            <groupId>io.springfox</groupId>-->
<!--            <artifactId>springfox-swagger2</artifactId>-->
<!--            <version>3.0.0</version>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>io.springfox</groupId>-->
<!--            <artifactId>springfox-swagger-ui</artifactId>-->
<!--            <version>3.0.0</version>-->
<!--        </dependency>-->

 <!--   集成swagger3代     -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>




        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.15.0</version>
        </dependency>

        <!-- 引入日志管理相关依赖-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.14.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <target>${java.version}</target>
                        <source>${java.version}</source>
                        <encoding>${java.encoding}</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.6</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-release-plugin</artifactId>
                    <configuration>
                        <arguments>-Prelease</arguments>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <version>2.1</version>
                    <configuration>
                        <attach>true</attach>
                    </configuration>
                    <executions>
                        <execution>
                            <phase>compile</phase>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                        </execution>
                    </executions>

                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>


 

项目结构

文件树

.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── springboot-test.iml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── demo
│   │   │               ├── DemoApplication.java
│   │   │               ├── ProgressInfo.java
│   │   │               ├── ServletInitializer.java
│   │   │               ├── component
│   │   │               │   └── OssComponent.java
│   │   │               ├── config
│   │   │               │   ├── CorsFilter.java
│   │   │               │   └── SwaggerConfig.java
│   │   │               ├── controller
│   │   │               │   └── FileController.java
│   │   │               └── service
│   │   │                   └── FileService.java
│   │   └── resources
│   │       ├── application.properties
│   │       ├── application.yaml
│   │       ├── static
│   │       │   └── styles.css
│   │       └── templates
│   │           └── index.html
│   └── test
│       └── java
│           └── com
│               └── example
│                   └── demo
│                       └── DemoApplicationTests.java
└── target
    ├── classes
    │   ├── application.properties
    │   ├── application.yaml
    │   ├── com
    │   │   └── example
    │   │       └── demo
    │   │           ├── DemoApplication.class
    │   │           ├── ProgressInfo.class
    │   │           ├── ServletInitializer.class
    │   │           ├── component
    │   │           │   ├── OssComponent$1.class
    │   │           │   ├── OssComponent$PutObjectProgressListener.class
    │   │           │   └── OssComponent.class
    │   │           ├── config
    │   │           │   ├── CorsFilter.class
    │   │           │   ├── SwaggerConfig$1.class
    │   │           │   └── SwaggerConfig.class
    │   │           ├── controller
    │   │           │   └── FileController.class
    │   │           └── service
    │   │               └── FileService.class
    │   ├── static
    │   │   └── styles.css
    │   └── templates
    │       └── index.html
    ├── generated-sources
    │   └── annotations
    ├── generated-test-sources
    │   └── test-annotations
    └── test-classes
        └── com
            └── example
                └── demo
                    └── DemoApplicationTests.class

37 directories, 34 files
fanlongfeideMacBook-Pro:springboot-test dasouche$ tree
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── springboot-test.iml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── example
│   │   │           └── demo
│   │   │               ├── DemoApplication.java
│   │   │               ├── ServletInitializer.java
│   │   │               ├── component
│   │   │               │   └── OssComponent.java
│   │   │               ├── config
│   │   │               │   ├── CorsFilter.java
│   │   │               │   └── SwaggerConfig.java
│   │   │               ├── controller
│   │   │               │   └── FileController.java
│   │   │               └── service
│   │   │                   └── FileService.java
│   │   └── resources
│   │       ├── application.properties
│   │       ├── application.yaml
│   │       ├── static
│   │       └── templates
│   └── test
│       └── java
│           └── com
│               └── example
│                   └── demo
│                       └── DemoApplicationTests.java
└── target
    ├── classes
    │   ├── application.properties
    │   ├── application.yaml
    │   ├── com
    │   │   └── example
    │   │       └── demo
    │   │           ├── DemoApplication.class
    │   │           ├── ServletInitializer.class
    │   │           ├── component
    │   │           │   ├── OssComponent$1.class
    │   │           │   ├── OssComponent$PutObjectProgressListener.class
    │   │           │   └── OssComponent.class
    │   │           ├── config
    │   │           │   ├── CorsFilter.class
    │   │           │   ├── SwaggerConfig$1.class
    │   │           │   └── SwaggerConfig.class
    │   │           ├── controller
    │   │           │   └── FileController.class
    │   │           └── service
    │   │               └── FileService.class
    │   ├── static
    │   └── templates
    ├── generated-sources
    │   └── annotations
    ├── generated-test-sources
    │   └── test-annotations
    └── test-classes
        └── com
            └── example
                └── demo
                    └── DemoApplicationTests.class


 

图示结构

代码实现

配置相关

配置文件yaml

spring:
  web:
    resources:
      #设置静态文件访问路径,用于直接访问html文件
      static-locations: classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/,classpath:/templates/
  thymeleaf:
    prefix:  /templates/**
    suffix: .html
    cache: false

  #redis配置
  redis:
    host: xxx
    port: xxx
    password: xxx
    timeout: 30000
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: -1ms



  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER

server:
  port: 8080

aliyun:
  OSS_ENDPOINT: http://oss-cn-hangzhou.aliyuncs.com
  ACCESS_ID: xxx
  ACCESS_KEY: xxx
  bucket: xxx

 

Swagger3配置

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.ArrayList;
import java.util.List;

/**
 * @author 
 * @date 2023年01月17日 16:00
 */
@Configuration
@EnableOpenApi
public class SwaggerConfig {
   

    @Bean
    public Docket createRestApi() {
   
        return new Docket(DocumentationType.OAS_30) // v2 不同
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.example.demo")) // 设置扫描路径
                .build();
    }

    
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
   
        return new WebMvcConfigurer() {
   
            @Override
            public void addResourceHandlers(ResourceHandlerRegistry registry) {
   
                registry.addResourceHandler("swagger-ui.html")
                        .addResourceLocations("classpath:/META-INF/resources/");
                registry.addResourceHandler("/webjars/**")
                        .addResourceLocations("classpath:/META-INF/resources/webjars/");
            }
        };
    }

}


 

跨域问题配置

package com.example.demo.config;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 
 * @date 2023年01月17日 14:46
 */
@Component
public class CorsFilter implements Filter {
   

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
   
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Content-Length, X-Requested-With");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
   
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
   
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
   
    }

    @Override
    public void destroy() {
   
    }
}



 

oss相关

package com.example.demo.component;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.event.ProgressEvent;
import com.aliyun.oss.event.ProgressEventType;
import com.aliyun.oss.event.ProgressListener;
import com.aliyun.oss.model.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.*;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static com.aliyun.oss.internal.OSSConstants.URL_ENCODING;

/**
 * @author 
 * @date 2023年01月17日 15:11
 */
@Component
@Slf4j
public class OssComponent implements InitializingBean, DisposableBean {
   

    @Value("${aliyun.OSS_ENDPOINT}")
    private String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
    @Value("${aliyun.ACCESS_ID}")
    private String accessKeyId = "xxx";
    @Value("${aliyun.ACCESS_KEY}")
    private String accessKeySecret = "xxx";
    @Value("${aliyun.bucket}")
    private String bucket = "xxx";

    @Resource
    private RedisTemplate<String, Long> redisTemplate;

    private OSS ossClient;

    //设置缓存失效时间:1天
    private static final TimeUnit TIME_UNIT = TimeUnit.DAYS;
    private static final Integer EXPIRE = 1;

    public String upload(File file, String fileName) throws Exception {
   

        String requestId = null;
        String etag = null;

        try{
   
            //用于标识上传文件,用于获取进度时使用
            requestId = UUID.randomUUID().toString();
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, "process-test/" + fileName, file);

            //添加进度条Listener,用于进度条更新
            putObjectRequest.withProgressListener(new PutObjectProgressListener(requestId, redisTemplate));

            //文件
            PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest);

            if(StringUtils.isBlank((etag = putObjectResult.getETag()))){
   
                throw new RuntimeException("上传失败!");
            }
            return requestId;
        }catch (Exception e){
   
            log.error("upload error ! requestId : {} etag : {} fileName : {}  " , requestId , etag , fileName , e);
            return null;
        }
    }

    public Integer batchDel(List<String> fileNames) {
   

        String requestId = null;

        try{
   

            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

            DeleteObjectsRequest deleteObjectsRequest = new DeleteObjectsRequest(bucket).withKeys(fileNames).withEncodingType(URL_ENCODING);
            DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(deleteObjectsRequest);
            if(deleteObjectsResult == null){
   
                return 0;
            }

            requestId = deleteObjectsResult.getRequestId();

            List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
            if(deletedObjects == null){
   
                return 0;
            }

            return deletedObjects.size();
        }catch (Exception e){
   
            log.error("upload error ! requestId : {} fileName : {}  " , requestId , fileNames , e);
            return null;
        }

    }


    @Override
    public void afterPropertiesSet() throws Exception {
   
        ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }

    @Override
    public void destroy() throws Exception {
   
        ossClient.shutdown();
    }


    public static class PutObjectProgressListener implements ProgressListener {
   
        private String requestId;
        private long bytesWritten = 0;
        private long totalBytes = -1;
        private boolean succeed = false;
        private RedisTemplate redisTemplate;


        public PutObjectProgressListener(String requestId, RedisTemplate redisTemplate) {
   
            this.requestId = requestId;
            this.redisTemplate = redisTemplate;
            this.redisTemplate.opsForValue().set(requestId + "_total", totalBytes);
            this.redisTemplate.opsForValue().set(requestId + "_uploaded", bytesWritten);
        }

        public PutObjectProgressListener() {
   
        }

        @Override
        public void progressChanged(ProgressEvent progressEvent) {
   

            long bytes = progressEvent.getBytes();
            ProgressEventType eventType = progressEvent.getEventType();
            switch (eventType) {
   
                case TRANSFER_STARTED_EVENT:
                    System.out.println("Start to upload......");
                    break;
                case REQUEST_CONTENT_LENGTH_EVENT:
                    this.totalBytes = bytes;
                    this.redisTemplate.opsForValue().set(requestId + "_total", totalBytes, EXPIRE, TIME_UNIT);
//                    this.totalBytes = bytes;
//                    System.out.println(this.totalBytes + " bytes in total will be uploaded to OSS");
                    break;
                case REQUEST_BYTE_TRANSFER_EVENT:
                    this.bytesWritten += bytes;
                    redisTemplate.opsForValue().set(requestId + "_uploaded", bytesWritten, EXPIRE, TIME_UNIT);
//                    this.bytesWritten += bytes;
//                    if (this.totalBytes != -1) {
   
//                        int percent = (int)(this.bytesWritten * 100.0 / this.totalBytes);
//                        System.out.println(bytes + " bytes have been written at this time, upload progress: " + percent + "%(" + this.bytesWritten + "/" + this.totalBytes + ")");
//                    } else {
   
//                        System.out.println(bytes + " bytes have been written at this time, upload ratio: unknown" + "(" + this.bytesWritten + "/...)");
//                    }
                    break;
                case TRANSFER_COMPLETED_EVENT:
                    this.succeed = true;
                    System.out.println("Succeed to upload, " + this.bytesWritten + " bytes have been transferred in total");
                    break;
                case TRANSFER_FAILED_EVENT:
                    System.out.println("Failed to upload, " + this.bytesWritten + " bytes have been transferred");
                    break;
                default:
                    break;
            }
        }
    }


    public static void main(String[] args) {
   
        String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
        String accessKeyId = "xxx";
        String accessKeySecret = "xxx";
        String bucketName = "xxx";
//
        String key = "process-test/object-get-progress-sample";

        OSS client = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
   
            File fh = createSampleFile();

            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, fh);
            putObjectRequest.<PutObjectRequest>withProgressListener(new PutObjectProgressListener());

            // 带进度条的上传
            PutObjectResult putObjectResult = client.putObject(putObjectRequest);
            String requestId = putObjectResult.getRequestId();

            System.out.println("requestId:" + requestId);

            // 带进度条的下载
//            client.getObject(new GetObjectRequest(bucketName, key).
//                    <GetObjectRequest>withProgressListener(new GetObjectProgressListener()), fh);

        } catch (Exception e) {
   
            e.printStackTrace();
        }
    }


    /**
     * Create a temp file with about 50MB.
     *
     */
    private static File createSampleFile() throws IOException {
   
        File file = File.createTempFile("oss-java-sdk-", ".txt");
        file.deleteOnExit();

        Writer writer = new OutputStreamWriter(new FileOutputStream(file));

        for (int i = 0; i < 10; i++) {
   
            writer.write("abcdefghijklmnopqrstuvwxyz\n");
            writer.write("0123456789011234567890\n");
        }

        writer.close();

        return file;
    }


}


 

Service

package com.example.demo.service;

import com.example.demo.component.OssComponent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.*;

/**
 * @author 
 * @date 2023年01月17日 14:57
 */
@Service
@Slf4j
public class FileService {
   

    @Resource
    private RedisTemplate<String, Long> redisTemplate;

    @Autowired
    private OssComponent ossComponent;

    /**
     * 获取上传进度
     * @param requestId 文件标识id
     * @return
     */
    public String getUploadFileProcess(String requestId){
   
        Long totalSize = redisTemplate.opsForValue().get(requestId + "_total");
        Long uploadedSize = redisTemplate.opsForValue().get(requestId + "_uploaded");

        if (null == totalSize || null == uploadedSize){
   
            return "0%";
        }

        return (int)(uploadedSize * 100.0 / totalSize) + "%";
    }

    /**
     * 模拟文件上传
     * @return
     */
    public String simulateUploadedFile(){
   
        String requestId = "";
        try {
   
            File sampleFile = createSampleFile();
            requestId = ossComponent.upload(sampleFile, sampleFile.getName());
        } catch (Exception e) {
   
            log.error("upload file error!", e);
        }
        return requestId;
    }


    private File createSampleFile() throws IOException {
   
        File file = File.createTempFile("oss-java-sdk-", ".txt");
        file.deleteOnExit();

        Writer writer = new OutputStreamWriter(new FileOutputStream(file));

        for (int i = 0; i < 10; i++) {
   
            writer.write("abcdefghijklmnopqrstuvwxyz\n");
            writer.write("0123456789011234567890\n");
        }
        writer.close();

        return file;
    }

}


 

Controller

package com.example.demo.controller;

import com.example.demo.service.FileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * @author 
 * @date 2023年01月17日 14:34
 */
@RestController()
@RequestMapping("/fileApi")
@Slf4j
@Api(value = "文件接口")
public class FileController {
   

    @Autowired
    private FileService fileService;

    @ApiOperation("获取上传进度")
    @GetMapping("/uploadProgress")
    public String uploadProgress(String requestId) {
   
        return fileService.getUploadFileProcess(requestId);
    }

    @ApiOperation("模拟文件上传")
    @GetMapping("/simulateUploadedFile")
    public String simulateUploadedFile() {
   
        return fileService.simulateUploadedFile();
    }

}


 

Application

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.oas.annotations.EnableOpenApi;

@SpringBootApplication
public class DemoApplication {
   

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

}
package com.example.demo;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {
   

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
   
        return application.sources(DemoApplication.class);
    }

}

Swagger接口操作

启动项目无报错后访问:http://localhost:8080/swagger-ui/index.html#/

可以看到我们的接口在页面上有显示,可以点击对应的接口进行操作

获取上传文件标识号


获取文件上传进度

小结

       文件下载时的进度也可以参考上述代码,进度存储也可以使用其他方式,如ConcurrentHashMap、Mysql等,当然前端也可以实现等。
       Swagger UI页面可以让后端开发更变便捷的操作接口,个人感觉像个快捷版的Postman吧。

Oss官方文档地址: 点我调转
Swagger官方文档地址: 点我调转
Swagger2代3代配置相关疑问可参考文档:点我调转


转载:https://blog.csdn.net/asd1358355022/article/details/128718810
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场