SpringBoot - 搭建靜態資源儲存伺服器

Maggieq8324 發表於 2021-09-08
Spring

前言

記錄下SpringBoot下靜態資源儲存伺服器的搭建。


環境

win10 + SpringBoot2.5.3


實現效果

  • 檔案上傳:

在這裡插入圖片描述

  • 檔案儲存位置:

在這裡插入圖片描述

  • 檔案訪問:

在這裡插入圖片描述


具體實現

檔案上傳

配置類

  • pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>
  • application.yml
spring:
  # 檔案編碼 UTF8
  mandatory-file-encoding: UTF-8

server:
  # 服務埠
  port: 8000

#檔案上傳配置
file:
  # 檔案服務域名
  domain: http://localhost:8000/
  # 排除檔案型別
  exclude:
  # 包括檔案型別
  include:
    - .jpg
    - .png
    - .jpeg
  # 檔案最大數量
  nums: 10
  # 伺服器檔案路徑
  serve-path: assets/**
  # 單個檔案最大體積
  single-limit: 2MB
  # 本地檔案儲存位置
  store-dir: assets/
  • yml讀取工廠類
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.List;

/**
 * @Description yml讀取工廠類
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
public class YmlPropertySourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
        return sources.get(0);
    }
}
  • 檔案上傳屬性配置類
import com.coisini.file.factory.YmlPropertySourceFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * @Description 檔案上傳屬性配置類
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Component
@ConfigurationProperties(prefix = "file")
@PropertySource(value = "classpath:application.yml",
        encoding = "UTF-8",factory = YmlPropertySourceFactory.class)
public class FilePropertiesConfiguration {

    private static final String[] DEFAULT_EMPTY_ARRAY = new String[0];

    private String storeDir = "/assets";

    private String singleLimit = "2MB";

    private Integer nums = 10;

    private String domain;

    private String[] exclude = DEFAULT_EMPTY_ARRAY;

    private String[] include = DEFAULT_EMPTY_ARRAY;

    public String getStoreDir() {
        return storeDir;
    }

    public void setStoreDir(String storeDir) {
        this.storeDir = storeDir;
    }

    public String getSingleLimit() {
        return singleLimit;
    }

    public void setSingleLimit(String singleLimit) {
        this.singleLimit = singleLimit;
    }

    public Integer getNums() {
        return nums;
    }

    public void setNums(Integer nums) {
        this.nums = nums;
    }

    public String[] getExclude() {
        return exclude;
    }

    public void setExclude(String[] exclude) {
        this.exclude = exclude;
    }

    public String[] getInclude() {
        return include;
    }

    public void setInclude(String[] include) {
        this.include = include;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }
}

上傳介面

  • 檔案上傳控制器
import com.coisini.file.vo.FileVo;
import com.coisini.file.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * @Description 檔案上傳控制器
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@RestController
@RequestMapping("/file")
public class FileController {

    @Autowired
    private FileService fileService;

    /**
     * 檔案上傳
     * @param request
     * @return
     */
    @PostMapping("/upload")
    public List<FileVo> upload(HttpServletRequest request) {
        MultipartHttpServletRequest multipartHttpServletRequest = ((MultipartHttpServletRequest) request);
        MultiValueMap<String, MultipartFile> fileMap = multipartHttpServletRequest.getMultiFileMap();
        List<FileVo> files = fileService.upload(fileMap);
        return files;
    }

}
  • 檔案上傳介面
import com.coisini.file.vo.FileVo;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;

/**
 * @Description 檔案上傳介面
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
public interface FileService {

    /**
     * 上傳檔案
     * @param fileMap 檔案map
     * @return 檔案資料
     */
    List<FileVo> upload(MultiValueMap<String, MultipartFile> fileMap);

}
  • 檔案上傳實現類
import com.coisini.file.model.FileModel;
import com.coisini.file.core.Uploader;
import com.coisini.file.vo.FileVo;
import com.coisini.file.service.FileService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Description 檔案上傳實現類
 * @author coisini
 * @date
 * @Version 1.0
 */
@Service
public class FileServiceImpl implements FileService {

    @Autowired
    private Uploader uploader;

    @Value("${file.domain}")
    private String domain;

    @Value("${file.serve-path:assets/**}")
    private String servePath;

    @Override
    public List<FileVo> upload(MultiValueMap<String, MultipartFile> fileMap) {
        return uploader.upload(fileMap).stream().map(item ->{
            /**
             * 這裡可以拿到檔案具體資訊
             * 在此做資料庫儲存記錄操作等業務處理
             */
            return transform(item);
        }).collect(Collectors.toList());
    }

    /**
     * 出參序列化
     * @param fileModel
     * @return
     */
    private FileVo transform(FileModel fileModel) {
        FileVo model = new FileVo();
        BeanUtils.copyProperties(fileModel, model);
        String s = servePath.split("/")[0];
        model.setUrl(domain + s + "/" + fileModel.getPath());
        return model;
    }
}

上傳實現

  • 檔案上傳配置類
import com.coisini.file.core.LocalUploader;
import com.coisini.file.core.Uploader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

/**
 * @Description 檔案上傳配置類
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Configuration
public class UploaderConfiguration {
    /**
     * @return 本地檔案上傳實現類
     */
    @Bean
    @Order
    @ConditionalOnMissingBean
    public Uploader uploader(){
        return new LocalUploader();
    }
}
  • 檔案上傳服務介面
import com.coisini.file.model.FileModel;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;

/**
 * @Description 檔案上傳服務介面
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
public interface Uploader {

    /**
     * 上傳檔案
     * @param fileMap 檔案map
     * @return 檔案資料
     */
    List<FileModel> upload(MultiValueMap<String, MultipartFile> fileMap);

}
  • 本地上傳實現類
import com.coisini.file.config.FilePropertiesConfiguration;
import com.coisini.file.model.FileModel;
import com.coisini.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * @Description 本地上傳
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Slf4j
public class LocalUploader implements Uploader {

    @Autowired
    private FilePropertiesConfiguration filePropertiesConfiguration;

    /**
     * 初始化本地儲存
     * 依賴注入完成後初始化
     */
    @PostConstruct
    public void initStoreDir() {
        System.out.println("initStoreDir start:" + this.filePropertiesConfiguration.getStoreDir());
        FileUtil.initStoreDir(this.filePropertiesConfiguration.getStoreDir());
        System.out.println("initStoreDir end");
    }

    /**
     * 檔案上傳
     * @param fileMap 檔案map
     * @return
     */
    @Override
    public List<FileModel> upload(MultiValueMap<String, MultipartFile> fileMap) {
        // 檢查檔案
        checkFileMap(fileMap);
        return handleMultipartFiles(fileMap);
    }

    /**
     * 檔案配置
     * @return
     */
    protected FilePropertiesConfiguration getFilePropertiesConfiguration() {
        return filePropertiesConfiguration;
    }

    /**
     * 單個檔案體積限制
     * @return
     */
    private long getSingleFileLimit() {
        String singleLimit = getFilePropertiesConfiguration().getSingleLimit();
        return FileUtil.parseSize(singleLimit);
    }

    /**
     * 檢查檔案
     * @param fileMap
     */
    protected void checkFileMap(MultiValueMap<String, MultipartFile> fileMap){
        if (fileMap.isEmpty()) {
            throw new RuntimeException("file not found");
        }

        // 上傳檔案數量限制
        int nums = getFilePropertiesConfiguration().getNums();
        if (fileMap.size() > nums) {
            throw new RuntimeException("too many files, amount of files must less than" + nums);
        }
    }

    /**
     * 檔案處理
     * @param fileMap
     * @return
     */
    protected List<FileModel> handleMultipartFiles(MultiValueMap<String, MultipartFile> fileMap) {
        long singleFileLimit = getSingleFileLimit();
        List<FileModel> res = new ArrayList<>();
        fileMap.keySet().forEach(key -> fileMap.get(key).forEach(file -> {
            if (!file.isEmpty()) {
                handleOneFile(res, singleFileLimit, file);
            }
        }));
        return res;
    }

    /**
     * 單檔案處理
     * @param res
     * @param singleFileLimit
     * @param file
     */
    private void handleOneFile(List<FileModel> res, long singleFileLimit, MultipartFile file) {
        byte[] bytes = FileUtil.getFileBytes(file);
        String[] include = getFilePropertiesConfiguration().getInclude();
        String[] exclude = getFilePropertiesConfiguration().getExclude();
        String ext = UploadHelper.checkOneFile(include, exclude, singleFileLimit, file.getOriginalFilename(), bytes.length);
        String newFilename = UploadHelper.getNewFilename(ext);
        String storePath = getStorePath(newFilename);
        // 生成檔案的md5值
        String md5 = FileUtil.getFileMD5(bytes);
        FileModel fileModelData = FileModel.builder().
                name(newFilename).
                md5(md5).
                key(file.getName()).
                path(storePath).
                size(bytes.length).
                extension(ext).
                build();

        boolean ok = writeFile(bytes, newFilename);
        if (ok) {
            res.add(fileModelData);
        }
    }

    /**
     * 寫入儲存
     * @param bytes
     * @param newFilename
     * @return
     */
    protected boolean writeFile(byte[] bytes, String newFilename) {
        // 獲取絕對路徑
        String absolutePath =
                FileUtil.getFileAbsolutePath(filePropertiesConfiguration.getStoreDir(), getStorePath(newFilename));
        System.out.println("absolutePath:" + absolutePath);
        try {
            BufferedOutputStream stream =
                    new BufferedOutputStream(new FileOutputStream(new File(absolutePath)));
            stream.write(bytes);
            stream.close();
        } catch (Exception e) {
            System.out.println("write file error:" + e);
            return false;
        }
        return true;
    }

    /**
     * 獲取快取地址
     * @param newFilename
     * @return
     */
    @SuppressWarnings("ResultOfMethodCallIgnored")
    protected String getStorePath(String newFilename) {
        Date now = new Date();
        String format = new SimpleDateFormat("yyyy/MM/dd").format(now);
        Path path = Paths.get(filePropertiesConfiguration.getStoreDir(), format).toAbsolutePath();
        File file = new File(path.toString());
        if (!file.exists()) {
            file.mkdirs();
        }

        return Paths.get(format, newFilename).toString();
    }
}

輔助類

  • 檔案上傳Helper
import com.coisini.file.util.FileUtil;
import java.util.UUID;

/**
 * @Description 檔案上傳Helper
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
public class UploadHelper {

    /**
     * 單個檔案檢查
     * @param singleFileLimit 單個檔案大小限制
     * @param originName      檔案原始名稱
     * @param length          檔案大小
     * @return 檔案的副檔名,例如: .jpg
     */
    public static String checkOneFile(String[] include, String[] exclude, long singleFileLimit, String originName, int length) {
        // 寫到了本地
        String ext = FileUtil.getFileExt(originName);
        // 檢測擴充套件
        if (!UploadHelper.checkExt(include, exclude, ext)) {
            throw new RuntimeException(ext + "檔案型別不支援");
        }
        // 檢測單個大小
        if (length > singleFileLimit) {
            throw new RuntimeException(originName + "檔案不能超過" + singleFileLimit);
        }
        return ext;
    }

    /**
     * 檢查檔案字尾
     * @param ext 字尾名
     * @return 是否通過
     */
    public static boolean checkExt(String[] include, String[] exclude, String ext) {
        int inLen = include == null ? 0 : include.length;
        int exLen = exclude == null ? 0 : exclude.length;
        // 如果兩者都有取 include,有一者則用一者
        if (inLen > 0 && exLen > 0) {
            return UploadHelper.findInInclude(include, ext);
        } else if (inLen > 0) {
            // 有include,無exclude
            return UploadHelper.findInInclude(include, ext);
        } else if (exLen > 0) {
            // 有exclude,無include
            return UploadHelper.findInExclude(exclude, ext);
        } else {
            // 二者都沒有
            return true;
        }
    }

    /**
     * 檢查允許的檔案型別
     * @param include
     * @param ext
     * @return
     */
    public static boolean findInInclude(String[] include, String ext) {
        for (String s : include) {
            if (s.equals(ext)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 檢查不允許的檔案型別
     * @param exclude
     * @param ext
     * @return
     */
    public static boolean findInExclude(String[] exclude, String ext) {
        for (String s : exclude) {
            if (s.equals(ext)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 獲得新檔案的名稱
     * @param ext 檔案字尾
     * @return 新名稱
     */
    public static String getNewFilename(String ext) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        return uuid + ext;
    }
}
  • 檔案工具類
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Path;

/**
 * @Description 檔案工具類
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
public class FileUtil {

    /**
     * 獲取當前檔案系統
     * @return
     */
    public static FileSystem getDefaultFileSystem() {
        return FileSystems.getDefault();
    }

    /**
     * 是否絕對路徑
     * @param str
     * @return
     */
    public static boolean isAbsolute(String str) {
        Path path = getDefaultFileSystem().getPath(str);
        return path.isAbsolute();
    }

    /**
     * 初始化儲存資料夾
     * @param dir
     */
    @SuppressWarnings("ResultOfMethodCallIgnored")
    public static void initStoreDir(String dir) {
        String absDir;
        if (isAbsolute(dir)) {
            absDir = dir;
        } else {
            String cmd = getCmd();
            Path path = getDefaultFileSystem().getPath(cmd, dir);
            absDir = path.toAbsolutePath().toString();
        }
        File file = new File(absDir);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    /**
     * 獲取程式當前路徑
     * @return
     */
    public static String getCmd() {
        return System.getProperty("user.dir");
    }

    /**
     * 獲取檔案絕對路徑
     * @param dir
     * @param filename
     * @return
     */
    public static String getFileAbsolutePath(String dir, String filename) {
        if (isAbsolute(dir)) {
            return getDefaultFileSystem()
                    .getPath(dir, filename)
                    .toAbsolutePath().toString();
        } else {
            return getDefaultFileSystem()
                    .getPath(getCmd(), dir, filename)
                    .toAbsolutePath().toString();
        }
    }

    /**
     * 獲取副檔名
     * @param filename
     * @return
     */
    public static String getFileExt(String filename) {
        int index = filename.lastIndexOf('.');
        return filename.substring(index);
    }

    /**
     * 獲取檔案MD5值
     * @param bytes
     * @return
     */
    public static String getFileMD5(byte[] bytes) {
        return DigestUtils.md5DigestAsHex(bytes);
    }

    /**
     * 檔案體積
     * @param size
     * @return
     */
    public static Long parseSize(String size) {
        DataSize singleLimitData = DataSize.parse(size);
        return singleLimitData.toBytes();
    }

    /**
     * 是否是絕對路徑
     * @param path
     * @return
     */
    public static boolean isAbsolutePath(String path) {
        if (StringUtils.isEmpty(path)) {
            return false;
        } else {
            return '/' == path.charAt(0) || path.matches("^[a-zA-Z]:[/\\\\].*");
        }
    }

    /**
     * 檔案位元組
     * @param file 檔案
     * @return 位元組
     */
    public static byte[] getFileBytes(MultipartFile file) {
        byte[] bytes;
        try {
            bytes = file.getBytes();
        } catch (Exception e) {
            throw new RuntimeException("read file date failed");
        }
        return bytes;
    }
}

實體

  • 檔案具體資訊
import lombok.*;

/**
 * @Description 檔案具體資訊,可儲存資料庫
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FileModel {

    /**
     * url
     */
    private String url;

    /**
     * key
     */
    private String key;

    /**
     * 檔案路徑
     */
    private String path;

    /**
     * 檔名稱
     */
    private String name;

    /**
     * 副檔名,例:.jpg
     */
    private String extension;

    /**
     * 檔案大小
     */
    private Integer size;

    /**
     * md5值,防止上傳重複檔案
     */
    private String md5;
}
  • 檔案出參
import lombok.Data;

/**
 * @Description 檔案出參
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Data
public class FileVo {

    /**
     * 檔案 key
     */
    private String key;

    /**
     * 檔案路徑
     */
    private String path;

    /**
     * 檔案 URL
     */
    private String url;
}

上傳測試

  • 上傳

在這裡插入圖片描述

  • 檔案儲存位置為當前專案/assets目錄

在這裡插入圖片描述


檔案訪問

配置類

  • SpringBoot訪問靜態資源有兩種方式:模板引擎和改變資源對映,這裡採用改變資源對映來實現
  • Spring MVC配置類
import com.coisini.file.util.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.FileSystems;
import java.nio.file.Path;

/**
 * @Description Spring MVC 配置
 * @author coisini
 * @date Sep 7, 2021
 * @Version 1.0
 */
@Configuration(proxyBeanMethods = false)
@Slf4j
public class WebConfiguration implements WebMvcConfigurer {

    @Value("${file.store-dir:assets/}")
    private String dir;

    @Value("${file.serve-path:assets/**}")
    private String servePath;

    /**
     * 跨域設定
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }

    /**
     * 攔截處理請求資訊
     * 新增檔案真實地址
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler(getDirServePath())
                .addResourceLocations("file:" + getAbsDir() + "/");
    }

    /**
     * 獲取伺服器url
     * @return
     */
    private String getDirServePath() {
        return servePath;
    }

    /**
     * 獲得資料夾的絕對路徑
     */
    private String getAbsDir() {
        if (FileUtil.isAbsolutePath(dir)) {
            return dir;
        }
        String cmd = System.getProperty("user.dir");
        Path path = FileSystems.getDefault().getPath(cmd, dir);
        return path.toAbsolutePath().toString();
    }
}
  • 訪問結果

在這裡插入圖片描述


專案原始碼

Gitee: https://gitee.com/maggieq8324/java-learn-demo/tree/master/springboot-file-simple


- End -
夢想是鹹魚
關注一下吧
SpringBoot - 搭建靜態資源儲存伺服器