需求
之前寫過一個圖片上傳實作方法:https://www.cnblogs.com/phdeblog/p/13236363.html
不過這種方法局限性很大:
- 圖片存盤的位置寫死,不可以靈活配置,
- 沒有專門實作“下載”,雖然可以直接預覽例如瀏覽器輸入圖片地址,http://localhost:8080/image/1.jpg,可以直接預覽圖片,但是如果想下載,必須右擊選擇下載到本地,
- 直接把檔案放在專案工程里面,專案臃腫,服務器壓力很大,
- 檔案名寫死,無法保留原檔案的檔案名,
現在新的需求是:
- 檔案保存的路徑可以配置,
- 可以通過檔案名等識別符號,下載指定檔案,
- 保留檔案原有的名稱,在下載的時候可以指定新的檔案名,也可以用原先的檔案名,
- 可以指定只能上傳特定格式的檔案,例如word檔案、壓縮包、excel表格等,
思路
注意:
資料庫只存放檔案的描述資訊(例如檔案名、所在路徑),不存檔案本身,
上傳流程:
(1)用戶點擊上傳檔案 ——> (2)傳到后臺服務器——>(3)初步校驗,上傳的檔案不能為空——>(4)唯一性校驗,如果你的專案只能存在一個檔案,必須把已有的檔案刪去(可選)——> (5) 檢查是否有同名檔案,同名檔案是否覆寫(可選)
——> (6) 開始上傳檔案 ——> (7) 檢查檔案型別是否滿足需求——> (8) 用一個變數保留原有的名字,將檔案寫入服務器本地 ——> (9) 如果寫入成功,將路徑、新的檔案名、舊的檔案名、檔案的功能 等等寫入資料庫,
下載流程:
從資料庫取出指定檔案的描述資訊,描述資訊里面有檔案所在目錄,用java的api獲取檔案物件,轉化成位元組寫入response,回傳給前端,
完整實作
依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
目錄結構

檔案上傳工具類
檔案上傳工具類有三個,功能不一致,
FileUploadUtils
******可以在這里修改檔案默認存放位置
上傳檔案,支持默認路徑存盤、也支持指定目錄存盤,
在SpringBoot還需要在組態檔中配置上傳檔案的大小上限,默認是2MB,
public class FileUploadUtils { /** * 默認大小 50M */ public static final long DEFAULT_MAX_SIZE = 50 * 1024 * 1024; /** * 默認的檔案名最大長度 100 */ public static final int FILE_NAME_MAX = 100; /** * 默認上傳的地址 */ private static String DEFAULT_BASE_FILE = "D:\\personalCode\\activemq-learn\\file-upload-learn\\src\\main\\resources\\upload"; /** * 按照默認的配置上床檔案 * * @param file 檔案 * @return 檔案名 * @throws IOException */ public static final String upload(MultipartFile file) throws IOException { try { return upload(FileUploadUtils.DEFAULT_BASE_FILE, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 根據檔案路徑上傳 * * @param baseDir 相對應用的基目錄 * @param file 上傳的檔案 * @return 檔案名稱 * @throws IOException */ public static final String upload(String baseDir, MultipartFile file) throws IOException { try { return upload(baseDir, file, MimeTypeUtils.DEFAULT_ALLOWED_EXTENSION); } catch (Exception e) { throw new IOException(e.getMessage(), e); } } /** * 檔案上傳 * @param baseDir 相對應用的基目錄 * @param file 上傳的檔案 * @param allowedExtension 上傳檔案型別 * @return 回傳上傳成功的檔案名 * @throws FileSizeLimitExceededException 如果超出最大大小 * @throws IOException 比如讀寫檔案出錯時 */ public static final String upload(String baseDir, MultipartFile file, String[] allowedExtension) throws Exception { //合法性校驗 assertAllowed(file, allowedExtension); String fileName = encodingFileName(file); File desc = getAbsoluteFile(baseDir, fileName); file.transferTo(desc); return desc.getAbsolutePath(); } private static final File getAbsoluteFile(String uploadDir, String fileName) throws IOException { File desc = new File(uploadDir + File.separator + fileName); if (!desc.getParentFile().exists()) { desc.getParentFile().mkdirs(); } if (!desc.exists()) { desc.createNewFile(); } return desc; } /** * 對檔案名特殊處理一下 * * @param file 檔案 * @return */ private static String encodingFileName(MultipartFile file) { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); String datePath = simpleDateFormat.format(new Date()); return datePath + "-" + UUID.randomUUID().toString() + "." + getExtension(file); } /** * 檔案合法性校驗 * * @param file 上傳的檔案 * @return */ public static final void assertAllowed(MultipartFile file, String[] allowedExtension) throws Exception { if (file.getOriginalFilename() != null) { int fileNamelength = file.getOriginalFilename().length(); if (fileNamelength > FILE_NAME_MAX) { throw new Exception("檔案名過長"); } } long size = file.getSize(); if (size > DEFAULT_MAX_SIZE) { throw new Exception("檔案過大"); } String extension = getExtension(file); if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) { throw new Exception("請上傳指定型別的檔案!"); } } /** * 判斷MIME型別是否是允許的MIME型別 * * @param extension * @param allowedExtension * @return */ public static final boolean isAllowedExtension(String extension, String[] allowedExtension) { for (String str : allowedExtension) { if (str.equalsIgnoreCase(extension)) { return true; } } return false; } /** * 獲取檔案名的后綴 * * @param file 表單檔案 * @return 后綴名 */ public static final String getExtension(MultipartFile file) { String fileName = file.getOriginalFilename(); String extension = null; if (fileName == null) { return null; } else { int index = indexOfExtension(fileName); extension = index == -1 ? "" : fileName.substring(index + 1); } if (StringUtils.isEmpty(extension)) { extension = MimeTypeUtils.getExtension(file.getContentType()); } return extension; } public static int indexOfLastSeparator(String filename) { if (filename == null) { return -1; } else { int lastUnixPos = filename.lastIndexOf(47); int lastWindowsPos = filename.lastIndexOf(92); return Math.max(lastUnixPos, lastWindowsPos); } } public static int indexOfExtension(String filename) { if (filename == null) { return -1; } else { int extensionPos = filename.lastIndexOf(46); int lastSeparator = indexOfLastSeparator(filename); return lastSeparator > extensionPos ? -1 : extensionPos; } } public void setDEFAULT_BASE_FILE(String DEFAULT_BASE_FILE) { FileUploadUtils.DEFAULT_BASE_FILE = DEFAULT_BASE_FILE; } public String getDEFAULT_BASE_FILE() { return DEFAULT_BASE_FILE; } }
FileUtils
******檔案下載需要用到這邊的writeByte
主要功能:洗掉檔案、檔案名校驗、檔案下載時進行位元組流寫入
public class FileUtils { //檔案名正則校驗 public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; public static void writeBytes(String filePath, OutputStream os) { FileInputStream fi = null; try { File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException(filePath); } fi = new FileInputStream(file); byte[] b = new byte[1024]; int length; while ((length = fi.read(b)) > 0) { os.write(b, 0, length); } } catch (Exception e) { e.printStackTrace(); } finally { if(os != null) { try { os.close(); }catch (IOException e) { e.printStackTrace(); } } if(fi != null) { try { fi.close(); }catch (IOException e) { e.printStackTrace(); } } } } /** * 洗掉檔案 * @param filePath 檔案路徑 * @return 是否成功 */ public static boolean deleteFile(String filePath) { boolean flag = false; File file = new File(filePath); if (file.isFile() && file.exists()) { file.delete(); flag = true; } return flag; } /** * 檔案名校驗 * @param fileName 檔案名 * @return true 正常, false 非法 */ public static boolean isValidName(String fileName) { return fileName.matches(FILENAME_PATTERN); } /** * 下載檔案名重新編碼 * * @param request 請求物件 * @param fileName 檔案名 * @return 編碼后的檔案名 */ public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException { final String agent = request.getHeader("USER-AGENT"); String filename = fileName; if (agent.contains("MSIE")) { // IE瀏覽器 filename = URLEncoder.encode(filename, "utf-8"); filename = filename.replace("+", " "); } else if (agent.contains("Firefox")) { // 火狐瀏覽器 filename = new String(fileName.getBytes(), "ISO8859-1"); } else if (agent.contains("Chrome")) { // google瀏覽器 filename = URLEncoder.encode(filename, "utf-8"); } else { // 其它瀏覽器 filename = URLEncoder.encode(filename, "utf-8"); } return filename; } }
MimeTypeUtils
******DEFAULT_ALLOWED_EXTENSION 可以指定允許檔案上傳型別
媒體工具類,支持指定上傳檔案格式,
public class MimeTypeUtils { public static final String IMAGE_PNG = "image/png"; public static final String IMAGE_JPG = "image/jpg"; public static final String IMAGE_JPEG = "image/jpeg"; public static final String IMAGE_BMP = "image/bmp"; public static final String IMAGE_GIF = "image/gif"; public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"}; public static final String[] FLASH_EXTENSION = {"swf", "flv"}; public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg", "asf", "rm", "rmvb"}; public static final String[] DEFAULT_ALLOWED_EXTENSION = { // 圖片 "bmp", "gif", "jpg", "jpeg", "png", // word excel powerpoint "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // 壓縮檔案 "rar", "zip", "gz", "bz2", // pdf "pdf"}; public static String getExtension(String prefix) { switch (prefix) { case IMAGE_PNG: return "png"; case IMAGE_JPG: return "jpg"; case IMAGE_JPEG: return "jpeg"; case IMAGE_BMP: return "bmp"; case IMAGE_GIF: return "gif"; default: return ""; } } }
controller層
因為是測驗demo,比較簡陋,一般專案里會在controller層這邊做例外捕捉,和統一回傳格式,我這邊就偷個懶,省了哈,
@RestController public class FileUploadController { @Autowired FileUploadService fileUploadService; //使用默認路徑 @RequestMapping("/upload") public String upload(MultipartFile file) throws Exception { fileUploadService.upload(file, null); return null; } //自定義路徑 @RequestMapping("/upload/template") public String uploadPlace(MultipartFile file) throws Exception { fileUploadService.upload(file, "H:\\upload"); return null; } //下載 @GetMapping("/download/file") public String downloadFile(HttpServletResponse response) throws IOException { fileUploadService.download(response, "word檔案"); return null; } }
entity物體類
@TableName("db_upload")
@Data
public class UploadEntity {
@TableId(type = IdType.AUTO)
private Long id;
//存在本地的地址
private String location;
//名稱,業務中用到的名稱,比如 ”檔案模板“、”用戶資訊“、”登錄記錄“等等
private String name;
//保留檔案原來的名字
private String oldName;
//描述(可以為空)
private String description;
private Date createTime;
private Date updateTime;
}
mapper
public interface UploadMapper extends BaseMapper<UploadEntity> { }
service層
public interface FileUploadService { void upload(MultipartFile file, String baseDir) throws Exception; void download(HttpServletResponse response , String newName) throws IOException; }
service實作層
@Service public class FileUploadServiceImpl implements FileUploadService { @Autowired UploadMapper uploadMapper; @Override public void upload(MultipartFile file, String baseDir) throws Exception { //就算什么也不傳,controller層的file也不為空,但是originalFilename會為空(親測) String originalFilename = file.getOriginalFilename(); if(originalFilename == null || "".equals(originalFilename)) { throw new Exception( "上傳檔案不能為空"); } //檢測是否上傳過同樣的檔案,如果有的話就洗掉,(這邊可根據個人的情況修改邏輯) QueryWrapper<UploadEntity> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("old_name", originalFilename); UploadEntity oldEntity = uploadMapper.selectOne(queryWrapper); //新的檔案 UploadEntity uploadEntity = new UploadEntity(); uploadEntity.setCreateTime(new Date()); uploadEntity.setUpdateTime(new Date()); uploadEntity.setOldName(file.getOriginalFilename());
//這邊可以根據業務修改,專案中不要寫死 uploadEntity.setName("上傳模板"); String fileLocation = null ; if(baseDir != null) { fileLocation = FileUploadUtils.upload(baseDir, file); }else { fileLocation = FileUploadUtils.upload(file); } uploadEntity.setLocation(fileLocation); uploadMapper.insert(uploadEntity); if(oldEntity != null) { //確保新的檔案保存成功后,洗掉原有的同名檔案(物體檔案 and 資料庫檔案) FileUtils.deleteFile(oldEntity.getLocation()); uploadMapper.deleteById(oldEntity.getId()); } } @Override public void download(HttpServletResponse response, String newName) throws IOException { QueryWrapper<UploadEntity> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", newName); UploadEntity uploadEntity = uploadMapper.selectOne(queryWrapper); response.setHeader("content-type", "application/octet-stream"); response.setContentType("application/octet-stream");
//這邊可以設定檔案下載時的名字,我這邊用的是檔案原本的名字,可以根據實際場景設定 response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(uploadEntity.getOldName(), "UTF-8")); FileUtils.writeBytes(uploadEntity.getLocation(), response.getOutputStream()); } }
啟動類
@SpringBootApplication @MapperScan("com.dayrain.mapper") public class FileUploadLearnApplication { public static void main(String[] args) { SpringApplication.run(FileUploadLearnApplication.class, args); } }
組態檔
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://ip:3306/upload?useUnicode=true&characterEncoding=UTF-8 username: root password: root servlet: multipart: max-file-size: 10MB #單次上傳檔案最大不超過10MB max-request-size: 100MB #檔案總上傳大小不超過100MB
SQL檔案
DROP TABLE IF EXISTS `db_upload`;
CREATE TABLE `db_upload` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`old_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
總結
上述代碼以經過簡單測驗,無中文亂碼現象,邏輯基本滿足目前專案使用,
因為專案用到檔案的地方不是很多,所以就把檔案和專案放在一個服務器里面,不涉及遠程呼叫,
如果檔案上傳下載使用頻繁,例如電子檔案系統,電子書,網盤等等,需要考慮使用專門的檔案服務器,拆分業務,緩解服務端壓力,
如果對您有幫助,歡迎給在下點個推薦,
如有錯誤,懇請批評指正!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/204733.html
標籤:其他
