【开发技巧】-- 什么你还在使用本地作为文件保存服务器?本文将带你了解,如何使用SpringBoot优雅的将文件上传至阿里云OSS、FastDFS(分布式文件系统)

1.1 业务背景

当今互联网项目,需求日渐增多,并且应用服务器的压力也日渐增大,这时就引入了分布式系统的概念,然后又有了动静分离,即动态资源与静态资源分开,使后端的应用服务器专注业务请求的处理,并降低因为请求静态资源而为应用服务器带来的压力。

1.2 文件上传的实现方式有哪些?

  1. 直接上传到应用服务器(缺点:增加应用服务器的压力)。
  2. 通过搭建私有云,比如通过FASTDFS搭建一个分布式文件系统。
  3. 使用第三方云存储(阿里云OSS、七牛云等)。

1.3 文件上传的实现

1.3.1 前置准备

1. 创建一个枚举类FileSourceEnum(用于后期实例化指定文件上传业务实现类)
package com.qingyun.farm.enums;

import lombok.Getter;

@Getter
public enum FileSourceEnum {

    LOCAL(1L,"LOCAL"),
    ALIYUN(2L,"ALIYUN"),
    FAST_DFS(3L,"FAST_DFS");

    private Long code;
    private String desc;

    FileSourceEnum(Long code, String desc) {
        this.code=code;
        this.desc=desc;
    }
}
2. 创建一个文件上传业务实现类创建实例工厂类FileServiceFactory
package com.qingyun.farm.factory;

import com.qingyun.farm.enums.FileSourceEnum;
import com.qingyun.farm.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.HashMap;

/**
 * Created with IntelliJ IDEA.
 * User: 李敷斌.
 * Date: 2020-04-02
 * Time: 11:20
 * Explain: 文件业务工厂类
 */
@Component
public class FileServiceFactory {

    private HashMap<FileSourceEnum, FileService> fileServiceMap=new HashMap<>();

    @Autowired
    private FileService localFileServiceImpl;

    @Autowired
    private FileService aliyunFileServiceImpl;

    @Autowired
    private FileService fastdfsFileServiceImpl;

    @PostConstruct
    private void initFileService(){
        fileServiceMap.put(FileSourceEnum.LOCAL,localFileServiceImpl);
        fileServiceMap.put(FileSourceEnum.ALIYUN,aliyunFileServiceImpl);
        fileServiceMap.put(FileSourceEnum.FAST_DFS,fastdfsFileServiceImpl);
    }

    public FileService getFileService(Long fileSourceCode) {

        if (fileSourceCode.equals(FileSourceEnum.FAST_DFS.getCode())){
            return fileServiceMap.get(FileSourceEnum.FAST_DFS);
        }else if (fileSourceCode.equals(FileSourceEnum.ALIYUN.getCode())){
            return fileServiceMap.get(FileSourceEnum.ALIYUN);
        }

        return fileServiceMap.get(FileSourceEnum.LOCAL);
    }
}
3. 创建一个文件上传业务实现接口类FileService
package com.qingyun.farm.service;

import java.io.IOException;

import com.qingyun.farm.model.FileInfo;
import org.springframework.web.multipart.MultipartFile;

public interface FileService {

	FileInfo upload(MultipartFile file) throws Exception;

	void delete(FileInfo fileInfo);

}
4. 创建一个文件上传业务实现抽象父类AbstractFileService
package com.qingyun.farm.service.impl;

import com.qingyun.farm.dao.FileInfoDao;
import com.qingyun.farm.enums.FileSourceEnum;
import com.qingyun.farm.model.FileInfo;
import com.qingyun.farm.service.FileService;
import com.qingyun.farm.utils.FileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
public abstract class AbstractFileService implements FileService {

	protected abstract FileInfoDao getFileDao();

	@Override
	public FileInfo upload(MultipartFile file) throws Exception {
		FileInfo fileInfo = FileUtil.getFileInfo(file);
		FileInfo oldFileInfo = getFileDao().getById(fileInfo.getId());

		if (oldFileInfo != null) {
			return oldFileInfo;
		}

		if (!fileInfo.getName().contains(".")) {
			throw new IllegalArgumentException("缺少后缀名");
		}

		uploadFile(file, fileInfo);

		fileInfo.setSource(fileSource().name());// 设置文件来源
		getFileDao().save(fileInfo);// 将文件信息保存到数据库

		log.info("上传文件:{}", fileInfo);

		return fileInfo;
	}

	/**
	 * 文件来源
	 * 
	 * @return
	 */
	protected abstract FileSourceEnum fileSource();

	/**
	 * 上传文件
	 * 
	 * @param file
	 * @param fileInfo
	 */
	protected abstract void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception;

	@Override
	public void delete(FileInfo fileInfo) {
		deleteFile(fileInfo);
		getFileDao().delete(fileInfo.getId());
		log.info("删除文件:{}", fileInfo);
	}

	/**
	 * 删除文件资源
	 * 
	 * @param fileInfo
	 * @return
	 */
	protected abstract boolean deleteFile(FileInfo fileInfo);
}
5 .创建一个文件上传请求控制器FileController
package com.qingyun.farm.controller;

import java.io.IOException;
import java.util.List;

import com.qingyun.farm.dao.FileInfoDao;
import com.qingyun.farm.dto.LayuiFile;
import com.qingyun.farm.factory.FileServiceFactory;
import com.qingyun.farm.model.FileInfo;
import com.qingyun.farm.page.table.PageTableHandler;
import com.qingyun.farm.page.table.PageTableRequest;
import com.qingyun.farm.page.table.PageTableResponse;
import com.qingyun.farm.service.FileService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.qingyun.farm.annotation.LogAnnotation;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(tags = "文件")
@RestController
@RequestMapping("/files")
public class FileController {

	@Autowired
	private FileServiceFactory fileServiceFactory;
	@Autowired
	private FileInfoDao fileInfoDao;

	@LogAnnotation
	@PostMapping
	@ApiOperation(value = "文件上传")
	public FileInfo uploadFile(MultipartFile file,@RequestParam(value = "sourceCode",defaultValue = "1") Long sourceCode) throws IOException {
		try {
			return fileServiceFactory.getFileService(sourceCode).upload(file);
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	@LogAnnotation
	@DeleteMapping("/{id}")
	@ApiOperation(value = "文件删除")
	@RequiresPermissions("sys:file:del")
	public void delete(@PathVariable String id,Long sourceCode) {
		FileInfo fileInfo = fileInfoDao.getById(id);

		fileServiceFactory.getFileService(sourceCode).delete(fileInfo);
	}

}
6. 创建一个文件上传工具类FileUtil,用于获取上传文件的信息,以及具体实现本地文件上传。
package com.qingyun.farm.utils;

import com.qingyun.farm.model.FileInfo;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.time.LocalDate;
import java.util.Date;

public class FileUtil {

	public static FileInfo getFileInfo(MultipartFile file) throws Exception {
		String md5 = fileMd5(file.getInputStream());

		FileInfo fileInfo = new FileInfo();
		fileInfo.setId(md5);// 将文件的md5设置为文件表的id
		fileInfo.setName(file.getOriginalFilename());
		fileInfo.setContentType(file.getContentType());
		fileInfo.setIsImg(fileInfo.getContentType().startsWith("image/")?1L:0L);
		fileInfo.setSize(file.getSize());
		fileInfo.setCreateTime(new Date());

		return fileInfo;
	}

	/**
	 * 文件的md5
	 * 
	 * @param inputStream
	 * @return
	 */
	public static String fileMd5(InputStream inputStream) {
		try {
			return DigestUtils.md5Hex(inputStream);
		} catch (IOException e) {
			e.printStackTrace();
		}

		return null;
	}

	public static String saveFile(MultipartFile file, String path) {
		try {
			File targetFile = new File(path);
			if (targetFile.exists()) {
				return path;
			}

			if (!targetFile.getParentFile().exists()) {
				targetFile.getParentFile().mkdirs();
			}
			file.transferTo(targetFile);

			return path;
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	public static boolean deleteFile(String pathname) {
		File file = new File(pathname);
		if (file.exists()) {
			boolean flag = file.delete();

			if (flag) {
				File[] files = file.getParentFile().listFiles();
				if (files == null || files.length == 0) {
					file.getParentFile().delete();
				}
			}

			return flag;
		}

		return false;
	}








    public static String getPath() {
        return "/" + LocalDate.now().toString().replace("-", "/") + "/";
    }

    /**
     * 将文本写入文件
     *
     * @param value
     * @param path
     */
    public static void saveTextFile(String value, String path) {
        FileWriter writer = null;
        try {
            File file = new File(path);
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }

            writer = new FileWriter(file);
            writer.write(value);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static String getText(String path) {
        File file = new File(path);
        if (!file.exists()) {
            return null;
        }

        try {
            return getText(new FileInputStream(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        return null;
    }

    public static String getText(InputStream inputStream) {
        InputStreamReader isr = null;
        BufferedReader bufferedReader = null;
        try {
            isr = new InputStreamReader(inputStream, "utf-8");
            bufferedReader = new BufferedReader(isr);
            StringBuilder builder = new StringBuilder();
            String string;
            while ((string = bufferedReader.readLine()) != null) {
                string = string + "\n";
                builder.append(string);
            }

            return builder.toString();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return null;
    }


}

以上就是文件上传业务实现的准备工作,可以根据具体业务修改相关代码实现。

1.3.2 通过SpringMVC直接将文件上传至应用服务器

1. 创建本地文件上传业务实现类
package com.qingyun.farm.service.impl;

import java.io.IOException;
import java.time.LocalDate;

import com.qingyun.farm.enums.FileSourceEnum;
import com.qingyun.farm.model.FileInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.qingyun.farm.dao.FileInfoDao;
import com.qingyun.farm.service.FileService;
import com.qingyun.farm.utils.FileUtil;

@Service
public class LocalFileServiceImpl extends AbstractFileService {


	@Autowired
	private FileInfoDao fileInfoDao;

	@Override
	protected FileInfoDao getFileDao() {
		return fileInfoDao;
	}

	@Value("${file.local.urlPrefix}")
	private String urlPrefix;
	/**
	 * 上传文件存储在本地的根路径
	 */
	@Value("${file.local.path}")
	private String localFilePath;

	@Override
	protected FileSourceEnum fileSource() {
		return FileSourceEnum.LOCAL;
	}

	@Override
	protected void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception {
		int index = fileInfo.getName().lastIndexOf(".");
		// 文件扩展名
		String fileSuffix = fileInfo.getName().substring(index);

		String suffix = "/" + LocalDate.now().toString().replace("-", "/") + "/" + fileInfo.getId() + fileSuffix;

		String path = localFilePath + suffix;
		String url = urlPrefix + suffix;
		fileInfo.setPath(path);
		fileInfo.setUrl(url);

		FileUtil.saveFile(file, path);
	}

	@Override
	protected boolean deleteFile(FileInfo fileInfo) {
		return FileUtil.deleteFile(fileInfo.getPath());
	}
}
2. 配置上传文件的保存路径
file:
  local:
    path: /Users/qingyun/IdeaProjects/SmartAgriculture/src/main/resources/static/upload
    prefix: /upload
    urlPrefix: http://localhost:8088/${file.local.prefix}

1.3.3 使用FASTDFS搭建私有云实现文件上传。

1. 下载fastdfs-client,并将它安装到maven仓库中【fastdfs-client-java:点击链接即可下载, 密码: ol7r】
2. 引入fastdfs-client所需maven依赖
<dependency>
	<groupId>org.csourcegroupId>
	<artifactId>fastdfs-client-javaartifactId>
	<version>1.29-SNAPSHOTversion>
dependency>
3. 创建一个fastdfs配置文件,tracker.conf

【开发技巧】-- 什么你还在使用本地作为文件保存服务器?本文将带你了解,如何使用SpringBoot优雅的将文件上传至阿里云OSS、FastDFS(分布式文件系统)_第1张图片

tracker_server=192.168.69.139:22122

# 连接超时时间,针对socket套接字函数connect,默认为30秒
connect_timeout=30000

# 网络通讯超时时间,默认是60秒
network_timeout=60000
4. 创建一个Fastdfs文件上传客户端配置类
package com.qingyun.farm.config;

import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.StorageClient;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Created with IntelliJ IDEA.
 * User: 李敷斌.
 * Date: 2020-02-26
 * Time: 17:57
 * Explain: 文件上传配置类
 */
@Configuration
@ConditionalOnProperty(prefix = "file.fastdfs.tracker.config",name = "path",havingValue = "/tracker.conf")
public class FastDFSUploadConfig {

    @Value(value = "${file.fastdfs.tracker.config.path}")
    private String trackerConfigPath;

    @Bean
    public StorageClient storageClient(){

        TrackerClient trackerClient=null;
        TrackerServer trackerServer=null;

        try {
            String tracker = FastDFSUploadConfig.class.getResource(trackerConfigPath).getPath();

            //初始化
            ClientGlobal.init(tracker);

            //创建trackerClient
            trackerClient=new TrackerClient();
            //通过client获取service
            trackerServer=trackerClient.getTrackerServer();
        }catch (Exception e){

        }

        //以trackerservice为参数 构建storageclient
        return new StorageClient(trackerServer,null);
    }
}
5. 创建fastdfs文件上传业务实现类
package com.qingyun.farm.service.impl;

import com.qingyun.farm.dao.FileInfoDao;
import com.qingyun.farm.enums.FileSourceEnum;
import com.qingyun.farm.model.FileInfo;
import lombok.extern.slf4j.Slf4j;
import org.csource.common.MyException;
import org.csource.fastdfs.StorageClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * Created with IntelliJ IDEA.
 * User: 李敷斌.
 * Date: 2020-04-02
 * Time: 11:58
 * Explain: fastdfs文件上传
 */
@Service
@Slf4j
public class FastdfsFileServiceImpl extends AbstractFileService {

    @Autowired
    private StorageClient storageClient;

    @Autowired
    private FileInfoDao fileInfoDao;

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

    @Override
    protected FileInfoDao getFileDao() {
        return fileInfoDao;
    }

    /**
     * 文件来源
     *
     * @return
     */
    @Override
    protected FileSourceEnum fileSource() {
        return FileSourceEnum.FAST_DFS;
    }

    /**
     * 上传文件
     *
     * @param multipartFile
     * @param fileInfo
     */
    @Override
    protected void uploadFile(MultipartFile multipartFile, FileInfo fileInfo) throws Exception {
        //获取文件后缀名
        String sourceFileName = multipartFile.getOriginalFilename();

        //获取后缀名
        String suffixName = sourceFileName.substring(sourceFileName.lastIndexOf(".")+1);

        String[] uploadFileResult=null;

        try {
            //上传文件
            uploadFileResult = storageClient.upload_file(multipartFile.getBytes(), suffixName, null);
        } catch (Exception e){
            log.error("【文件上传】获取二进制数据失败,ex.msg={}",e.getMessage());
        }

        String url="http://"+fastdfs_domain+getUrl(uploadFileResult);
        fileInfo.setUrl(url);

        log.debug("【文件上传】文件访问路径,imageUrl={}",url);
    }

    private String getUrl(String[] uploadFileResult){

        StringBuffer sbf=new StringBuffer();

        for (String item : uploadFileResult) {
            sbf.append("/"+item);
        }

        return sbf.toString();
    }

    /**
     * 删除文件资源
     *
     * @param fileInfo
     * @return
     */
    @Override
    protected boolean deleteFile(FileInfo fileInfo) {
        try {
            return storageClient.delete_file("group1",fileInfo.getName())>0;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (MyException e) {
            e.printStackTrace();
        }

        return false;
    }
}
6. 在配置文件中,配置好fastdfs文件上传业务有关配置信息
file:
  fastdfs:
    tracker:
      config:
        path: /tracker.conf
    domain: 192.168.69.139

fastdfs文件上传至此以及完整地实现了。

1.3.4 使用阿里云OSS实现文件上传

1. 引入阿里云OSS实现文件上传所需maven依赖
<dependency>
	<groupId>com.aliyun.ossgroupId>
	<artifactId>aliyun-sdk-ossartifactId>
	<version>${aliyun-sdk-oss.version}version>
dependency>
<dependency>
	<groupId>com.aliyungroupId>
	<artifactId>aliyun-java-sdk-coreartifactId>
	<version>${aliyun-sdk-core.version}version>
dependency>
<dependency>
	<groupId>com.aliyungroupId>
	<artifactId>aliyun-java-sdk-dysmsapiartifactId>
	<version>${aliyun-sdk-dysmsapi.version}version>
dependency>

对应版本信息:

<aliyun-sdk-oss.version>2.8.2aliyun-sdk-oss.version>
<aliyun-sdk-core.version>3.2.8aliyun-sdk-core.version>
<aliyun-sdk-dysmsapi.version>1.1.0aliyun-sdk-dysmsapi.version>
2. 在配置文件中,配置阿里云OSS文件上传相关配置
file:
  aliyun:
    endpoint: xxx
    accessKeyId: xxx
    accessKeySecret: xxx
    bucketName: xxx
    domain: xxx
3. 写一个阿里云OSS文件上传配置类
package com.qingyun.farm.config;

import com.aliyun.oss.OSSClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;

@Configuration
public class AliyunConfig {

	@Value("${file.aliyun.endpoint}")
	private String endpoint;
	@Value("${file.aliyun.accessKeyId}")
	private String accessKeyId;
	@Value("${file.aliyun.accessKeySecret}")
	private String accessKeySecret;

	/**
	 * 阿里云文件存储client
	 * 
	 */
	@Bean
	public OSSClient ossClient() {
		OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
		return ossClient;
	}


	public static void main(String[] args) throws FileNotFoundException {

		OSSClient ossClient = new OSSClient("oss-cn-beijing.aliyuncs.com", "LTAI3jTQMjLamd0v", "aOR1ZFUoJCKmiSUUQopZcwZDu0uei6");
		InputStream inputStream = new FileInputStream("D://ssfw.sql");

		ossClient.putObject("topwulian", "upload/" + "ss11fw.sql", inputStream);

		Date expiration = new Date(new Date().getTime() + 3600l * 1000 * 24 * 365 * 10);
		// 生成URL
		URL url = ossClient.generatePresignedUrl("topwulian", "upload/" + "ss11fw.sql", expiration);

		System.out.println(url);


	}
}
4. 创建阿里云OSS文件上传业务实现类
package com.qingyun.farm.service.impl;

import com.aliyun.oss.OSSClient;
import com.qingyun.farm.dao.FileInfoDao;
import com.qingyun.farm.enums.FileSourceEnum;
import com.qingyun.farm.model.FileInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * 阿里云存储文件
 * 
 * @author 小威老师
 *
 */
@Service("aliyunFileServiceImpl")
public class AliyunFileServiceImpl extends AbstractFileService {

	@Autowired
	private FileInfoDao fileInfoDao;

	@Override
	protected FileInfoDao getFileDao() {
		return fileInfoDao;
	}

	@Override
	protected FileSourceEnum fileSource() {
		return FileSourceEnum.ALIYUN;
	}

	@Autowired
	private OSSClient ossClient;

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

	@Override
	protected void uploadFile(MultipartFile file, FileInfo fileInfo) throws Exception {
		ossClient.putObject(bucketName, fileInfo.getName(), file.getInputStream());
		fileInfo.setUrl(domain + "/" + fileInfo.getName());
	}

	@Override
	protected boolean deleteFile(FileInfo fileInfo) {
		ossClient.deleteObject(bucketName, fileInfo.getName());
		return true;
	}
}

至此三种文件上传方式整合完毕,如果文章对你有帮助的话请给我点个赞哦,也可以关注博主我将持续更新一些组件使用教程❤️

你可能感兴趣的:(开发技巧,SpringBoot,Spring)