SpringBoot - 搭建静态资源存储服务器

前言

记录下SpringBoot下静态资源存储服务器的搭建。


环境

win10 + SpringBoot2.5.3


实现效果

  • 文件上传:

SpringBoot - 搭建静态资源存储服务器

  • 文件存储位置:

SpringBoot - 搭建静态资源存储服务器

  • 文件访问:

SpringBoot - 搭建静态资源存储服务器


具体实现

文件上传

配置类

  • 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;
}

上传测试

  • 上传

SpringBoot - 搭建静态资源存储服务器

  • 文件存储位置为当前项目/assets目录

SpringBoot - 搭建静态资源存储服务器


文件访问

配置类

  • 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();
    }
}
  • 访问结果

SpringBoot - 搭建静态资源存储服务器


项目源码

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


- End -
梦想是咸鱼
关注一下吧
SpringBoot - 搭建静态资源存储服务器

SpringBoot - 搭建静态资源存储服务器

上一篇:PULPino datasheet中文翻译并结合部分论文给出注释(前四章:Overview、Memory Map、CPU Core、Advanced Debug Unit)


下一篇:Interview----求 1+2+...+n, 不能用乘除法、for、while if、else、switch、case 等关键字以及条件判断语句 (A?B:C)