需求
之前寫過一個圖片上傳實現方法: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, "上傳模板"); 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;
測試
如何用postman測試文件上傳呢?
1、設置請求頭
2、設置請求體
選擇File
3、上傳文件
前端上傳代碼
有朋友問前端代碼,我就寫了幾個demo。因為不是專業的前端人員,如果有問題,歡迎指出。
表單
原生的html就可以實現文件的上傳,只是不能對數據進行二次處理,且不是異步的,如果文件大,會比較耗時。
<html> <head></head> <body> <form id="upload" enctype="multipart/form-data" action="http://localhost:8080/upload" method="post"> <input type="file" name="file" /> <input type="submit" value="提交" /> </form> </body> </html>
ajax
如果是異步的話,並且前后端分離,那么后端要解決一下跨域問題。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(30*1000);
}
}
前端代碼
<html> <head> </head> <body> <form id="upload" enctype="multipart/form-data" method="post"> <input type="file" name="file" id="pic" /> <!-- 多文件上傳 --> <!-- <input type="file" name="file" id="pic" multiple="multipart"/> --> <input type="button" value="提交" onclick="uploadFile()" /> </form> </body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"> </script> <script> function uploadFile() { //第一種 // formData = new FormData($('#upload')[0]); //第二種 formData = new FormData(); formData.append('file', $('#pic')[0].files[0]) $.ajax({ url: "http://localhost:8080/upload", type: "post", data: formData, processData: false, contentType: false, success: function (res) { alert('success') }, error: function (err) { alert('fail') } }) } </script> </html>
axios
axios是ajax的封裝,因為用的人比較多,我也貼一下
<html> <head> </head> <body> <form id="upload" enctype="multipart/form-data" method="post"> <input type="file" name="file" id="pic" /> <!-- 多文件上傳 --> <!-- <input type="file" name="file" id="pic" multiple="multipart"/> --> <input type="button" value="提交" onclick="uploadFile()" /> </form> </body> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"> </script> <script> function uploadFile() { //第一種 // formData = new FormData($('#upload')[0]); //第二種 formData = new FormData(); formData.append('file', $('#pic')[0].files[0]) $.ajax({ url: "http://localhost:8080/upload", type: "post", data: formData, processData: false, contentType: false, success: function (res) { alert('success') }, error: function (err) { alert('fail') } }) } </script> </html>
前端下載代碼
項目中實現下載功能通常有兩種方法。
方法一:
前端不做任何處理,直接訪問后台的地址,比如本文中的 http://localhost:8080/download/file,后台返回的是文件的輸出流,瀏覽器會自動轉化成文件,開始下載。
(本文就是按照這種方式實現的,可以看示例中的 “controller層” 第三個接口)
方法二:
后端不做處理,只提供數據接口,前端接收到數據后,通過js將數據整理並轉成對應格式的文件,比如doc、pdf之類的。
推薦:
推薦使用第一種方法,因為數據量比較大時,通過前端導出的話,后台需要向前台傳大量的數據,壓力比較大。不如后台處理,直接轉化成文件流交給瀏覽器處理,還省了rpc的開銷。
總結
上述代碼以經過簡單測試,無中文亂碼現象,邏輯基本滿足目前項目使用。
因為項目用到文件的地方不是很多,所以就把文件和項目放在一個服務器里面,不涉及遠程調用。
如果文件上傳下載使用頻繁,例如電子檔案系統,電子書,網盤等等,需要考慮使用專門的文件服務器,拆分業務,緩解服務端壓力。
如果對您有幫助,歡迎給在下點個推薦。
如有錯誤,懇請批評指正!