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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录

诉求

       将文件上传到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代配置相关疑问可参考文档:点我调转

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Springredis