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