文章目录
- 诉求
- 技术选型
- pom配置
- 项目结构
- 文件树
- 图示结构
- 代码实现
- 配置相关
- 配置文件yaml
- Swagger3配置
- 跨域问题配置
- oss相关
- Service
- Controller
- Application
- Swagger接口操作
- 获取上传文件标识号
- 获取文件上传进度
- 小结
诉求
将文件上传到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代配置相关疑问可参考文档:点我调转