├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── coderman │ │ └── uploader │ │ ├── UploaderApplication.java │ │ ├── config │ │ ├── MvcConfig.java │ │ ├── RedisConfig.java │ │ └── UploadConfig.java │ │ ├── controller │ │ ├── TestController.java │ │ └── UploadController.java │ │ ├── dto │ │ ├── FileChunkDTO.java │ │ └── FileChunkResultDTO.java │ │ ├── model │ │ └── FileResource.java │ │ ├── response │ │ ├── RestApiResponse.java │ │ └── error │ │ │ ├── BaseErrorCode.java │ │ │ ├── BusinessErrorCode.java │ │ │ └── BusinessException.java │ │ └── service │ │ ├── IUploadService.java │ │ └── impl │ │ └── UploadServiceImpl.java └── resources │ └── application.yml └── test └── java └── com └── coderman └── uploader ├── RedisTest.java └── TestFile.java /README.md: -------------------------------------------------------------------------------- 1 | # 大文件分片上传 2 | 3 | SpringBoot+Redis 大文件实现文件分片上传,断点续传,急速秒传。前端使用vue-simpler-uploader实现文件并发上传。 4 | 5 | 6 | ![](https://img2018.cnblogs.com/blog/1294929/201812/1294929-20181205144202486-1422913409.gif) 7 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.2.4.RELEASE 11 | 12 | 13 | com.coderman 14 | springboot_uploader 15 | 1.0 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-thymeleaf 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-test 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-data-redis 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/UploaderApplication.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @Author zhangyukang 8 | * @Date 2021/1/16 12:58 9 | * @Version 1.0 10 | **/ 11 | @SpringBootApplication 12 | public class UploaderApplication { 13 | public static void main(String[] args) { 14 | SpringApplication.run(UploaderApplication.class,args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/config/MvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.config; 2 | 3 | import com.coderman.uploader.response.RestApiResponse; 4 | import com.coderman.uploader.response.error.BusinessException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.cors.CorsConfiguration; 12 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 13 | import org.springframework.web.filter.CorsFilter; 14 | import org.springframework.web.servlet.HandlerExceptionResolver; 15 | import org.springframework.web.servlet.ModelAndView; 16 | import org.springframework.web.servlet.NoHandlerFoundException; 17 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 18 | 19 | import javax.servlet.http.HttpServletResponse; 20 | import java.io.IOException; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | 24 | /** 25 | * @Author zhangyukang 26 | * @Date 2021/1/16 14:05 27 | * @Version 1.0 28 | **/ 29 | @Configuration 30 | public class MvcConfig implements WebMvcConfigurer { 31 | 32 | private static Logger logger = LoggerFactory.getLogger(MvcConfig.class); 33 | 34 | @Override 35 | public void configureHandlerExceptionResolvers(List resolvers) { 36 | resolvers.add((httpServletRequest, httpServletResponse, o, e) -> { 37 | String requestUrl = httpServletRequest.getRequestURL().toString(); 38 | final String defaultErrorMsg = "服务器冒烟了,请联系管理员"; 39 | HashMap errorMap = new HashMap<>(); 40 | logger.info("请求地址:{}", requestUrl); 41 | if (e instanceof BusinessException) { 42 | BusinessException businessException = (BusinessException) e; 43 | logger.info("业务异常,code:{},errorMsg:{}", businessException.getErrorCode().getErrorCode() 44 | , businessException.getErrorCode().getErrorMsg()); 45 | errorMap.put("errorCode", businessException.getErrorCode().getErrorCode()); 46 | errorMap.put("errorMsg", businessException.getErrorCode().getErrorMsg()); 47 | }else if(e instanceof NoHandlerFoundException){ 48 | logger.info("接口不存在,code:{},errorMsg:{}",HttpStatus.NOT_FOUND.value(),e.getMessage()); 49 | errorMap.put("errorCode", HttpStatus.NOT_FOUND.value()); 50 | errorMap.put("errorMsg", "接口: ["+httpServletRequest.getServletPath()+"] 不存在"); 51 | } else { 52 | logger.info("系统异常,code:{},errorMsg:{}", HttpStatus.INTERNAL_SERVER_ERROR.value() 53 | , e.getMessage(),e); 54 | errorMap.put("errorCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); 55 | errorMap.put("errorMsg", defaultErrorMsg); 56 | } 57 | responseError(httpServletResponse,RestApiResponse.error(errorMap)); 58 | return new ModelAndView(); 59 | }); 60 | } 61 | 62 | private void responseError(HttpServletResponse httpServletResponse, RestApiResponse> error) { 63 | ObjectMapper objectMapper = new ObjectMapper(); 64 | try { 65 | httpServletResponse.setContentType("application/json;charset=utf-8"); 66 | String json = objectMapper.writeValueAsString(error); 67 | httpServletResponse.getWriter().println(json); 68 | } catch (IOException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | 73 | /** 74 | * 配置跨域 75 | * @return 76 | */ 77 | private CorsConfiguration corsConfig() { 78 | CorsConfiguration corsConfiguration = new CorsConfiguration(); 79 | corsConfiguration.addAllowedOrigin("*"); 80 | corsConfiguration.addAllowedHeader("*"); 81 | corsConfiguration.addAllowedMethod("*"); 82 | corsConfiguration.setAllowCredentials(true); 83 | corsConfiguration.setMaxAge(3600L); 84 | return corsConfiguration; 85 | } 86 | @Bean 87 | public CorsFilter corsFilter() { 88 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 89 | source.registerCorsConfiguration("/**", corsConfig()); 90 | return new CorsFilter(source); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.PropertyAccessor; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.data.redis.connection.RedisConnectionFactory; 9 | import org.springframework.data.redis.core.RedisTemplate; 10 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 11 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 12 | import org.springframework.data.redis.serializer.StringRedisSerializer; 13 | 14 | /** 15 | * @Author zhangyukang 16 | * @Date 2020/12/2 11:26 17 | * @Version 1.0 18 | **/ 19 | @Configuration 20 | public class RedisConfig { 21 | 22 | @Bean 23 | @SuppressWarnings("all") 24 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) { 25 | RedisTemplate template = new RedisTemplate(); 26 | template.setConnectionFactory(factory); 27 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); 28 | ObjectMapper om = new ObjectMapper(); 29 | om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 30 | om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); 31 | jackson2JsonRedisSerializer.setObjectMapper(om); 32 | StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); 33 | // key采用String的序列化方式 34 | template.setKeySerializer(stringRedisSerializer); 35 | // hash的key也采用String的序列化方式 36 | template.setHashKeySerializer(stringRedisSerializer); 37 | // value序列化方式采用jackson 38 | template.setValueSerializer(jackson2JsonRedisSerializer); 39 | template.setHashValueSerializer(jackson2JsonRedisSerializer); 40 | template.afterPropertiesSet(); 41 | return template; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/config/UploadConfig.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | /** 6 | * @Author zhangyukang 7 | * @Date 2021/1/17 17:48 8 | * @Version 1.0 9 | **/ 10 | @Configuration 11 | public class UploadConfig { 12 | private String fileStoragePath; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/controller/TestController.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.controller; 2 | 3 | import com.coderman.uploader.response.RestApiResponse; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | /** 9 | * @Author zhangyukang 10 | * @Date 2021/1/16 15:14 11 | * @Version 1.0 12 | **/ 13 | @RestController 14 | @RequestMapping(value = "/test") 15 | public class TestController { 16 | 17 | @GetMapping(value = "/error") 18 | public RestApiResponse error(){ 19 | return RestApiResponse.success(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/controller/UploadController.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.controller; 2 | 3 | import com.coderman.uploader.dto.FileChunkDTO; 4 | import com.coderman.uploader.dto.FileChunkResultDTO; 5 | import com.coderman.uploader.response.RestApiResponse; 6 | import com.coderman.uploader.service.IUploadService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.RestController; 14 | 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | /** 20 | * @Author zhangyukang 21 | * @Date 2021/1/16 19:22 22 | * @Version 1.0 23 | **/ 24 | @RestController 25 | public class UploadController { 26 | 27 | @Autowired 28 | private IUploadService uploadService; 29 | 30 | private Logger logger= LoggerFactory.getLogger(UploadController.class); 31 | 32 | /** 33 | * 检查分片是否存在 34 | * @return 35 | */ 36 | @GetMapping(value = "/upload/chunk") 37 | public RestApiResponse checkChunkExist(FileChunkDTO chunkDTO, HttpServletResponse response) { 38 | FileChunkResultDTO fileChunkCheckDTO; 39 | try { 40 | fileChunkCheckDTO = uploadService.checkChunkExist(chunkDTO); 41 | return RestApiResponse.success( fileChunkCheckDTO); 42 | } catch (Exception e) { 43 | Map error = getErrorMap(response, e); 44 | logger.error("check chunk exist error :{}",e.getMessage()); 45 | return RestApiResponse.error(error); 46 | } 47 | } 48 | 49 | 50 | /** 51 | * 上传文件分片 52 | * @param chunkDTO 53 | * @return 54 | */ 55 | @PostMapping(value = "/upload/chunk") 56 | public RestApiResponse uploadChunk(FileChunkDTO chunkDTO,HttpServletResponse response) { 57 | try { 58 | uploadService.uploadChunk(chunkDTO); 59 | return RestApiResponse.success(chunkDTO.getIdentifier()); 60 | } catch (Exception e) { 61 | Map error = getErrorMap(response, e); 62 | logger.error("upload chunk error :{}",e.getMessage()); 63 | return RestApiResponse.error(error); 64 | } 65 | } 66 | 67 | /** 68 | * 设置上传错误响应 69 | * @param response 70 | * @param e 71 | * @return 72 | */ 73 | private Map getErrorMap(HttpServletResponse response, Exception e) { 74 | response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); 75 | Map error = new HashMap<>(); 76 | error.put("errorMsg", e.getMessage()); 77 | error.put("errorCode", HttpStatus.INTERNAL_SERVER_ERROR.value()); 78 | return error; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/dto/FileChunkDTO.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.dto; 2 | 3 | import org.springframework.web.multipart.MultipartFile; 4 | 5 | /** 6 | * 文件分片 7 | * 8 | * @Author zhangyukang 9 | * @Date 2021/1/16 19:35 10 | * @Version 1.0 11 | **/ 12 | public class FileChunkDTO { 13 | /** 14 | * 文件md5 15 | */ 16 | private String identifier; 17 | /** 18 | * 分块文件 19 | */ 20 | MultipartFile file; 21 | /** 22 | * 当前分块序号 23 | */ 24 | private Integer chunkNumber; 25 | /** 26 | * 分块大小 27 | */ 28 | private Integer chunkSize; 29 | /** 30 | * 当前分块大小 31 | */ 32 | private Integer currentChunkSize; 33 | /** 34 | * 文件总大小 35 | */ 36 | private Integer totalSize; 37 | /** 38 | * 分块总数 39 | */ 40 | private Integer totalChunks; 41 | /** 42 | * 文件名 43 | */ 44 | private String filename; 45 | 46 | public String getIdentifier() { 47 | return identifier; 48 | } 49 | 50 | public void setIdentifier(String identifier) { 51 | this.identifier = identifier; 52 | } 53 | 54 | public MultipartFile getFile() { 55 | return file; 56 | } 57 | 58 | public void setFile(MultipartFile file) { 59 | this.file = file; 60 | } 61 | 62 | 63 | public Integer getChunkNumber() { 64 | return chunkNumber; 65 | } 66 | 67 | public void setChunkNumber(Integer chunkNumber) { 68 | this.chunkNumber = chunkNumber; 69 | } 70 | 71 | public Integer getChunkSize() { 72 | return chunkSize; 73 | } 74 | 75 | public void setChunkSize(Integer chunkSize) { 76 | this.chunkSize = chunkSize; 77 | } 78 | 79 | public Integer getCurrentChunkSize() { 80 | return currentChunkSize; 81 | } 82 | 83 | public void setCurrentChunkSize(Integer currentChunkSize) { 84 | this.currentChunkSize = currentChunkSize; 85 | } 86 | 87 | public Integer getTotalSize() { 88 | return totalSize; 89 | } 90 | 91 | public void setTotalSize(Integer totalSize) { 92 | this.totalSize = totalSize; 93 | } 94 | 95 | public Integer getTotalChunks() { 96 | return totalChunks; 97 | } 98 | 99 | public void setTotalChunks(Integer totalChunks) { 100 | this.totalChunks = totalChunks; 101 | } 102 | 103 | public String getFilename() { 104 | return filename; 105 | } 106 | 107 | public void setFilename(String filename) { 108 | this.filename = filename; 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return "FileChunkDTO{" + 114 | "identifier='" + identifier + '\'' + 115 | ", file=" + file + 116 | ", chunkNumber=" + chunkNumber + 117 | ", chunkSize=" + chunkSize + 118 | ", currentChunkSize=" + currentChunkSize + 119 | ", totalSize=" + totalSize + 120 | ", totalChunks=" + totalChunks + 121 | ", filename='" + filename + '\'' + 122 | '}'; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/dto/FileChunkResultDTO.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.dto; 2 | 3 | import java.util.Set; 4 | 5 | /** 6 | * @Author zhangyukang 7 | * @Date 2021/1/17 10:14 8 | * @Version 1.0 9 | **/ 10 | public class FileChunkResultDTO { 11 | /** 12 | * 是否跳过上传 13 | */ 14 | private Boolean skipUpload; 15 | 16 | /** 17 | * 已上传分片的集合 18 | */ 19 | private Set uploaded; 20 | 21 | public Boolean getSkipUpload() { 22 | return skipUpload; 23 | } 24 | 25 | public void setSkipUpload(Boolean skipUpload) { 26 | this.skipUpload = skipUpload; 27 | } 28 | 29 | public Set getUploaded() { 30 | return uploaded; 31 | } 32 | 33 | public void setUploaded(Set uploaded) { 34 | this.uploaded = uploaded; 35 | } 36 | 37 | 38 | public FileChunkResultDTO(Boolean skipUpload, Set uploaded) { 39 | this.skipUpload = skipUpload; 40 | this.uploaded = uploaded; 41 | } 42 | 43 | public FileChunkResultDTO(Boolean skipUpload) { 44 | this.skipUpload = skipUpload; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/model/FileResource.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.model; 2 | 3 | /** 4 | * @Author zhangyukang 5 | * @Date 2021/1/16 19:48 6 | * @Version 1.0 7 | **/ 8 | public class FileResource { 9 | /** 文件名 */ 10 | private String fileName; 11 | /** 文件后缀 */ 12 | private String suffixName; 13 | /** 文件大小 */ 14 | private Long size; 15 | 16 | public String getFileName() { 17 | return fileName; 18 | } 19 | 20 | public void setFileName(String fileName) { 21 | this.fileName = fileName; 22 | } 23 | 24 | public String getSuffixName() { 25 | return suffixName; 26 | } 27 | 28 | public void setSuffixName(String suffixName) { 29 | this.suffixName = suffixName; 30 | } 31 | 32 | public Long getSize() { 33 | return size; 34 | } 35 | 36 | public void setSize(Long size) { 37 | this.size = size; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/response/RestApiResponse.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.response; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @Author zhangyukang 7 | * @Date 2021/1/16 14:37 8 | * @Version 1.0 9 | **/ 10 | public class RestApiResponse { 11 | 12 | /**是否成功 */ 13 | private boolean success; 14 | 15 | /** 响应数据 */ 16 | private T data; 17 | 18 | public boolean isSuccess() { 19 | return success; 20 | } 21 | 22 | public void setSuccess(boolean success) { 23 | this.success = success; 24 | } 25 | 26 | public T getData() { 27 | return data; 28 | } 29 | 30 | public void setData(T data) { 31 | this.data = data; 32 | } 33 | 34 | public static RestApiResponse success(T data){ 35 | RestApiResponse result = new RestApiResponse<>(); 36 | result.success=true; 37 | result.data=data; 38 | return result; 39 | } 40 | 41 | public static RestApiResponse success(){ 42 | RestApiResponse result = new RestApiResponse<>(); 43 | result.success=true; 44 | return result; 45 | } 46 | 47 | public static RestApiResponse error(T data){ 48 | RestApiResponse result = new RestApiResponse<>(); 49 | result.success=false; 50 | result.data=data; 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/response/error/BaseErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.response.error; 2 | 3 | /** 4 | * @Author zhangyukang 5 | * @Date 2021/1/16 14:40 6 | * @Version 1.0 7 | **/ 8 | public interface BaseErrorCode { 9 | /** 10 | * 获取错误码 11 | * @return 12 | */ 13 | int getErrorCode(); 14 | 15 | /** 16 | * 获取错误信息 17 | * @return 18 | */ 19 | String getErrorMsg(); 20 | 21 | /** 22 | * 设置错误信息 23 | * @param errorMsg 24 | */ 25 | void setErrorMsg(String errorMsg); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/response/error/BusinessErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.response.error; 2 | 3 | /** 4 | * @Author zhangyukang 5 | * @Date 2021/1/16 14:49 6 | * @Version 1.0 7 | **/ 8 | public enum BusinessErrorCode implements BaseErrorCode{ 9 | 10 | USER_NOT_LOGIN(10001, "用户未登入"), 11 | INVALID_PARAMETER(10002,"参数错误"); 12 | 13 | /** 错误消息 */ 14 | private String errorMsg; 15 | /** 错误码 */ 16 | private Integer errorCode; 17 | 18 | BusinessErrorCode(Integer errorCode, String errorMsg) { 19 | this.errorMsg = errorMsg; 20 | this.errorCode = errorCode; 21 | } 22 | 23 | @Override 24 | public int getErrorCode() { 25 | return this.errorCode; 26 | } 27 | 28 | @Override 29 | public String getErrorMsg() { 30 | return this.errorMsg; 31 | } 32 | 33 | @Override 34 | public void setErrorMsg(String errorMsg) { 35 | this.errorMsg=errorMsg; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/response/error/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.response.error; 2 | 3 | /** 4 | * @Author zhangyukang 5 | * @Date 2021/1/16 14:45 6 | * @Version 1.0 7 | **/ 8 | public class BusinessException extends Exception { 9 | 10 | private BaseErrorCode errorCode; 11 | 12 | public BusinessException(BaseErrorCode errorCode){ 13 | super(errorCode.getErrorMsg()); 14 | this.errorCode=errorCode; 15 | } 16 | 17 | public BusinessException(BaseErrorCode errorCode,String customizedErrorMsg){ 18 | super(customizedErrorMsg); 19 | this.errorCode=errorCode; 20 | errorCode.setErrorMsg(customizedErrorMsg); 21 | } 22 | 23 | public static void main(String[] args) throws BusinessException { 24 | throw new BusinessException(BusinessErrorCode.USER_NOT_LOGIN); 25 | } 26 | 27 | public BaseErrorCode getErrorCode() { 28 | return errorCode; 29 | } 30 | 31 | public void setErrorCode(BaseErrorCode errorCode) { 32 | this.errorCode = errorCode; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/service/IUploadService.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.service; 2 | 3 | import com.coderman.uploader.dto.FileChunkDTO; 4 | import com.coderman.uploader.dto.FileChunkResultDTO; 5 | import com.coderman.uploader.response.error.BusinessException; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * @Author zhangyukang 11 | * @Date 2021/1/17 10:21 12 | * @Version 1.0 13 | **/ 14 | public interface IUploadService { 15 | 16 | /** 17 | * 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合 18 | * @param chunkDTO 19 | * @return 20 | */ 21 | FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) throws BusinessException; 22 | 23 | 24 | /** 25 | * 上传文件分片 26 | * @param chunkDTO 27 | */ 28 | void uploadChunk(FileChunkDTO chunkDTO) throws BusinessException, IOException; 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/coderman/uploader/service/impl/UploadServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader.service.impl; 2 | 3 | import com.coderman.uploader.dto.FileChunkDTO; 4 | import com.coderman.uploader.dto.FileChunkResultDTO; 5 | import com.coderman.uploader.response.error.BusinessErrorCode; 6 | import com.coderman.uploader.response.error.BusinessException; 7 | import com.coderman.uploader.service.IUploadService; 8 | import org.apache.tomcat.util.http.fileupload.IOUtils; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.io.InputStream; 18 | import java.io.RandomAccessFile; 19 | import java.util.*; 20 | 21 | 22 | /** 23 | * @Author zhangyukang 24 | * @Date 2021/1/17 10:24 25 | * @Version 1.0 26 | **/ 27 | @Service 28 | @SuppressWarnings("all") 29 | public class UploadServiceImpl implements IUploadService { 30 | 31 | private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class); 32 | 33 | @Autowired 34 | private RedisTemplate redisTemplate; 35 | 36 | private final static String uploadFolder = "F:\\upload\\"; 37 | 38 | /** 39 | * 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合 40 | * 41 | * @param chunkDTO 42 | * @return 43 | */ 44 | @Override 45 | @SuppressWarnings("all") 46 | public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) throws BusinessException { 47 | //1.检查文件是否已上传过 48 | //1.1)检查在磁盘中是否存在 49 | String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier()); 50 | String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename()); 51 | File file = new File(filePath); 52 | boolean exists = file.exists(); 53 | //1.2)检查Redis中是否存在,并且所有分片已经上传完成。 54 | Set uploaded = (Set) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded"); 55 | if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) { 56 | return new FileChunkResultDTO(true); 57 | } 58 | File fileFolder = new File(fileFolderPath); 59 | if (!fileFolder.exists()) { 60 | boolean mkdirs = fileFolder.mkdirs(); 61 | logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs); 62 | } 63 | return new FileChunkResultDTO(false, uploaded); 64 | } 65 | 66 | 67 | /** 68 | * 上传分片 69 | * 70 | * @param chunkDTO 71 | */ 72 | @Override 73 | public void uploadChunk(FileChunkDTO chunkDTO) throws BusinessException { 74 | //分块的目录 75 | String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier()); 76 | File chunkFileFolder = new File(chunkFileFolderPath); 77 | if (!chunkFileFolder.exists()) { 78 | boolean mkdirs = chunkFileFolder.mkdirs(); 79 | logger.info("创建分片文件夹:{}", mkdirs); 80 | } 81 | //写入分片 82 | try ( 83 | InputStream inputStream = chunkDTO.getFile().getInputStream(); 84 | FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber())) 85 | ) { 86 | IOUtils.copy(inputStream, outputStream); 87 | logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber()); 88 | //将该分片写入redis 89 | long size = saveToRedis(chunkDTO); 90 | //合并分片 91 | if (size == chunkDTO.getTotalChunks()) { 92 | File mergeFile = mergeChunks(chunkDTO.getIdentifier(), chunkDTO.getFilename()); 93 | if (mergeFile == null) { 94 | throw new BusinessException(BusinessErrorCode.INVALID_PARAMETER, "合并文件失败"); 95 | } 96 | } 97 | } catch (Exception e) { 98 | throw new BusinessException(BusinessErrorCode.INVALID_PARAMETER, e.getMessage()); 99 | } 100 | } 101 | 102 | /** 103 | * 合并分片 104 | * 105 | * @param identifier 106 | * @param filename 107 | */ 108 | private File mergeChunks(String identifier, String filename) throws BusinessException { 109 | String chunkFileFolderPath = getChunkFileFolderPath(identifier); 110 | String filePath = getFilePath(identifier, filename); 111 | File chunkFileFolder = new File(chunkFileFolderPath); 112 | File mergeFile = new File(filePath); 113 | File[] chunks = chunkFileFolder.listFiles(); 114 | //排序 115 | Arrays.stream(chunks).sorted(Comparator.comparing(o -> Integer.valueOf(o.getName()))); 116 | try { 117 | RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw"); 118 | byte[] bytes = new byte[1024]; 119 | for (File chunk : chunks) { 120 | RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r"); 121 | int len; 122 | while ((len = randomAccessFileReader.read(bytes)) != -1) { 123 | randomAccessFileWriter.write(bytes, 0, len); 124 | } 125 | randomAccessFileReader.close(); 126 | } 127 | randomAccessFileWriter.close(); 128 | } catch (Exception e) { 129 | throw new BusinessException(BusinessErrorCode.INVALID_PARAMETER); 130 | } 131 | return mergeFile; 132 | } 133 | 134 | /** 135 | * 分片写入Redis 136 | * 137 | * @param chunkDTO 138 | */ 139 | @SuppressWarnings("all") 140 | private synchronized long saveToRedis(FileChunkDTO chunkDTO) { 141 | Set uploaded = (Set) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded"); 142 | if (uploaded == null) { 143 | uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber())); 144 | HashMap objectObjectHashMap = new HashMap<>(); 145 | objectObjectHashMap.put("uploaded", uploaded); 146 | objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks()); 147 | objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize()); 148 | objectObjectHashMap.put("path", getFileRelativelyPath(chunkDTO.getIdentifier(), chunkDTO.getFilename())); 149 | redisTemplate.opsForHash().putAll(chunkDTO.getIdentifier(), objectObjectHashMap); 150 | } else { 151 | uploaded.add(chunkDTO.getChunkNumber()); 152 | redisTemplate.opsForHash().put(chunkDTO.getIdentifier(), "uploaded", uploaded); 153 | } 154 | return uploaded.size(); 155 | } 156 | 157 | /** 158 | * 得到文件的绝对路径 159 | * 160 | * @param identifier 161 | * @param filename 162 | * @return 163 | */ 164 | private String getFilePath(String identifier, String filename) { 165 | String ext = filename.substring(filename.lastIndexOf(".")); 166 | return getFileFolderPath(identifier) + identifier + ext; 167 | } 168 | 169 | /** 170 | * 得到文件的相对路径 171 | * 172 | * @param identifier 173 | * @param filename 174 | * @return 175 | */ 176 | private String getFileRelativelyPath(String identifier, String filename) { 177 | String ext = filename.substring(filename.lastIndexOf(".")); 178 | return "/" + identifier.substring(0, 1) + "/" + 179 | identifier.substring(1, 2) + "/" + 180 | identifier + "/" + identifier 181 | + ext; 182 | } 183 | 184 | 185 | /** 186 | * 得到分块文件所属的目录 187 | * 188 | * @param identifier 189 | * @return 190 | */ 191 | private String getChunkFileFolderPath(String identifier) { 192 | return getFileFolderPath(identifier) + "chunks" + File.separator; 193 | } 194 | 195 | /** 196 | * 得到文件所属的目录 197 | * 198 | * @param identifier 199 | * @return 200 | */ 201 | private String getFileFolderPath(String identifier) { 202 | return uploadFolder + identifier.substring(0, 1) + File.separator + 203 | identifier.substring(1, 2) + File.separator + 204 | identifier + File.separator; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8989 3 | spring: 4 | servlet: 5 | multipart: 6 | enabled: true 7 | max-request-size: 100MB #最大请求文件的大小 8 | max-file-size: 100MB 9 | file-size-threshold: 20MB 10 | thymeleaf: 11 | cache: false 12 | mvc: 13 | throw-exception-if-no-handler-found: true 14 | resources: 15 | add-mappings: false 16 | redis: 17 | host: 127.0.0.1 18 | port: 6379 19 | 20 | -------------------------------------------------------------------------------- /src/test/java/com/coderman/uploader/RedisTest.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.data.redis.core.RedisTemplate; 8 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 9 | 10 | import java.util.Arrays; 11 | import java.util.HashSet; 12 | 13 | /** 14 | * @Author zhangyukang 15 | * @Date 2021/1/17 12:40 16 | * @Version 1.0 17 | **/ 18 | @SpringBootTest(classes = {UploaderApplication.class}) 19 | @RunWith(SpringJUnit4ClassRunner.class) 20 | public class RedisTest { 21 | 22 | @Autowired 23 | private RedisTemplate redisTemplate; 24 | 25 | @Test 26 | public void test() { 27 | redisTemplate.opsForHash().put("1ae8da00bc9cd674619ffd41a382a742", 28 | "uploaded",new HashSet<>(Arrays.asList(1L, 2L, 3L, 4L, 5L))); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/coderman/uploader/TestFile.java: -------------------------------------------------------------------------------- 1 | package com.coderman.uploader; 2 | import org.slf4j.Logger; 3 | import org.slf4j.LoggerFactory; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.RandomAccessFile; 7 | import java.util.Arrays; 8 | import java.util.Comparator; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * 使用Java进行文件分块 14 | * 15 | * @Author zhangyukang 16 | * @Date 2021/1/16 12:59 17 | * @Version 1.0 18 | **/ 19 | public class TestFile { 20 | 21 | private static Logger logger = LoggerFactory.getLogger(TestFile.class); 22 | 23 | public static void main(String[] args) throws IOException { 24 | //源文件 25 | File sourceFile = new File("C:\\Users\\Administrator\\Desktop\\java.mp4"); 26 | //保存块文件的目录 27 | String chunkFolder = "E:\\IDM下载\\视频\\chunks\\"; 28 | //分片的大小 29 | long shareSize = 1024 * 1024; 30 | logger.info("分片大小:{}", shareSize); 31 | //总块数 32 | long total = (long) Math.ceil(sourceFile.length() * 1.0 / shareSize); 33 | logger.info("分片总数:{}", total); 34 | //创建读文件的对象 35 | RandomAccessFile randomAccessFileReader = new RandomAccessFile(sourceFile, "r"); 36 | byte[] bytes = new byte[1024]; 37 | for (int i = 0; i < total; i++) { 38 | File chunkFile = new File(chunkFolder + i); 39 | logger.info("分片:{},chunkFile:{}", i + 1, chunkFile.getName()); 40 | //创建一个写对象 41 | RandomAccessFile randomAccessFileWriter = new RandomAccessFile(chunkFile, "rw"); 42 | int len; 43 | while ((len = randomAccessFileReader.read(bytes)) != -1) { 44 | randomAccessFileWriter.write(bytes, 0, len); 45 | //如果分片的大小>=分片的大小,读下一块 46 | if (chunkFile.length() >= shareSize) { 47 | break; 48 | } 49 | } 50 | randomAccessFileWriter.close(); 51 | } 52 | randomAccessFileReader.close(); 53 | 54 | //合并文件 55 | mergeFile(new File(chunkFolder)); 56 | } 57 | 58 | /** 59 | * 合并块文件 60 | * @param chunkFolder 61 | */ 62 | public static void mergeFile(File chunkFolder) throws IOException { 63 | //块文件夹下文件列表 64 | File[] files = chunkFolder.listFiles(); 65 | assert files != null; 66 | List fileList = Arrays.stream(files).sorted( 67 | Comparator.comparing(o -> Long.valueOf(o.getName()))) 68 | .collect(Collectors.toList()); 69 | //合并的文件 70 | File mergeFile = new File("E:\\IDM下载\\视频\\java_merge.mp4"); 71 | //创建新文件 72 | boolean success = mergeFile.createNewFile(); 73 | //创建写对象 74 | RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw"); 75 | byte[] bytes = new byte[1024]; 76 | for (int i = 0; i < fileList.size(); i++) { 77 | File chunkFile = fileList.get(i); 78 | logger.info("合并------>分片:{},chunkFile:{}", i + 1, chunkFile.getName()); 79 | RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunkFile, "r"); 80 | int len; 81 | while ((len = randomAccessFileReader.read(bytes)) != -1) { 82 | randomAccessFileWriter.write(bytes, 0, len); 83 | } 84 | randomAccessFileReader.close(); 85 | } 86 | randomAccessFileWriter.close(); 87 | } 88 | } 89 | --------------------------------------------------------------------------------