50. 建立靜态資源子子產品項目
建立新的straw-resource子子產品項目,用于管理使用者上傳的檔案等靜态資源。
建立出來後,在straw-resource的pom.xml中,自行将父級項目由SpringBoot改為straw項目,删除<dependencies>和<build>節點(因為沒有存在的必要,在父項目中已經配置好了)。
在straw項目中的<mudules>中添加子子產品項目。
在straw-resource的application.properties中顯式的配置端口号,必須與straw-portal的不同:
server.port=8081
全部完成後,更新Maven,straw-portal和straw-resource這2個項目是可以同時啟動的!
51. 設定straw-resource子子產品項目的靜态目錄
在straw-resource項目的application.properties中添加配置:
spring.resources.static-locations=file:D:/IdeaProjects/straw-static-resource
1
則straw-resource項目的靜态目錄就是以上指定的位置,後續straw-portal項目中涉及上傳操作時,上傳的檔案也應該存放到以上位置。
52.設定straw-resource子子產品項目的靜态目錄
在straw-portal項目的application.properties中添加配置:
# 釋出問題時,将圖檔上傳到哪裡,需要與straw-resource項目的靜态資源目錄保持一緻
project.question.image-upload-path=D:/IdeaProjects/straw-static-resource
# 釋出問題時,上傳的圖檔通過哪個伺服器提供通路,配置的端口号需要與straw-resource項目保持一緻
project.question.image-host=http://localhost:8081/
# 釋出問題時,允許上傳的檔案的最大大小
project.question.image-max-size=307200
# 釋出問題時,允許上傳的圖檔檔案的類型
project.question.image-content-types=image/jpeg, image/png, image/bmp
并且,在straw-portal中調整預設限制的檔案大小:
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(500));
factory.setMaxRequestSize(DataSize.ofMegabytes(500));
return factory.createMultipartConfig();
}
53. 開發簡易上傳功能
說明:由于上傳功能不可以通過在URL上填寫參數直接進行測試,為了更快的進行測試并體驗上傳的效果,暫且忽略不必要的代碼,例如上傳檔案的相關檢查等細節問題,當然,測試時也應該使用正确的檔案和資料進行測試。當簡單的上傳已經完成後,再補全細節部分。
在QuestionController中開發伺服器端的簡易上傳處理:
@Value("${project.question.image-upload-path}")
private String imageUploadPath;
@Value(("${project.question.image-host}"))
private String imageHost;
@PostMapping("/upload-image")
public R<String> uploadImage(MultipartFile imageFile) {
File dest = new File(imageUploadPath, "1.jpg");
try {
imageFile.transferTo(dest);
} catch (IOException e) {
e.printStackTrace();
}
String imageUrl = imageHost + "1.jpg"; // http://localhost:8081/1.jpg
log.debug("image url >>> {}", imageUrl);
return R.ok(imageUrl);
}
本次需要處理的頁面是“發表問題”的question/create.html,在發表問題時,使用的富文本編輯Summernote提供了名為callbacks的回調機制,其中,存在名為onImageUpload的回調屬性,該屬性值是函數,是以,可以自定義函數配置到這個回調屬性中,則後續上傳圖檔時,就會自動觸發自定義的函數,通過自定義函數實作圖檔的上傳,并傳回上傳圖檔的URL,生成圖檔插入到Summernote富文本編輯器中即可。
在question/create.html中,先将底部關于Summernote的JavaScript代碼移到新建立的commons/init_summernote.js中,并調整這段代碼:
$(document).ready(function () {
$('#summernote').summernote({
height: 300,
tabsize: 2,
lang: 'zh-CN',
placeholder: '請輸入問題的較長的描述...',
callbacks: {
onImageUpload: function () {
alert("準備上傳圖檔!");
}
}
});
});
完成後,重新開機項目,打開“釋出問題”頁面,插入圖檔,選擇圖檔檔案就會彈出對話框!
然後,在以上回調中,使用$.ajax()送出異步請求,在處理結果時,建立Image對象,将結果中的圖檔URL作為Image對象的src屬性值,并将整個Image對象(就是一個<src>标簽)插入到富文本編輯器中:
$(document).ready(function () {
$('#summernote').summernote({
height: 300,
tabsize: 2,
lang: 'zh-CN',
placeholder: '請輸入問題的較長的描述...',
callbacks: {
onImageUpload: function (files) {
// ---------------------------------------
// 目前函數的參數名稱是自定義,它表示使用者選擇的若幹個檔案
// Summernote在調用該函數時,會把使用者選擇的檔案作為函數的參數
// ---------------------------------------
if (!files || files.length < 1) {
alert("請選擇您要上傳的檔案!");
return;
}
if (files.length > 1) {
alert("一次隻允許上傳1個檔案!");
return;
}
let formData = new FormData();
let file = files[0];
formData.append("imageFile", file);
console.log("form data >>> " + formData);
$.ajax({
url: '/api/v1/questions/upload-image',
type: 'post',
data: formData,
contentType: false,
processData: false,
success: function(json) {
if (json.state == 2000) {
// alert(json.data);
let img = new Image(); // <img>
img.src = json.data; // <img src="xxx">
$('#summernote').summernote('insertNode', img);
} else {
alert(json.message);
}
}
});
}
}
});
});
54. 完善伺服器端的上傳功能
先建立關于檔案上傳的異常類型:
public class FileUploadException extends RuntimeException {
}
public class FileEmptyException extends FileUploadException {
}
public class FileSizeException extends FileUploadException {
}
public class FileTypeException extends FileUploadException {
}
public class FileIOException extends FileUploadException {
}
在GlobalExceptionHandler中處理以上異常,完整代碼如下(需在R.State中添加常量):
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler
public R handleException(Throwable e) {
if (e instanceof ParameterValidationException) {
return R.failure(R.State.ERR_PARAMETER_INVALIDATION, e);
} else if (e instanceof InviteCodeException){
return R.failure(R.State.ERR_INVITE_CODE, e);
} else if (e instanceof ClassDisabledException) {
return R.failure(R.State.ERR_CLASS_DISABLED, e);
} else if (e instanceof PhoneDuplicateException) {
return R.failure(R.State.ERR_PHONE_DUPLICATE, e);
} else if (e instanceof InsertException) {
return R.failure(R.State.ERR_INSERT, e);
} else if (e instanceof FileEmptyException) {
return R.failure(R.State.ERR_UPLOAD_EMPTY, e);
} else if (e instanceof FileSizeException) {
return R.failure(R.State.ERR_UPLOAD_FILE_SIZE, e);
} else if (e instanceof FileTypeException) {
return R.failure(R.State.ERR_UPLOAD_FILE_TYPE, e);
} else if (e instanceof FileIOException) {
return R.failure(R.State.ERR_UPLOAD_FILE_IO, e);
} else if (e instanceof AccessDeniedException) {
return R.failure(R.State.ERR_ACCESS_DENIED, e);
} else {
log.debug("Unknown Exception", e);
return R.failure(R.State.ERR_UNKNOWN, e);
}
}
}
在處理上傳請求之前,先聲明2個全局屬性,用于讀取配置中的“檔案最大大小”和“檔案類型”:
@Value("${project.question.image-max-size}")
private long imageMaxSize;
@Value(("${project.question.image-content-types}"))
private List<String> imageContentTypes;
在處理上傳請求的過程中:
應該建立子級檔案夾,避免所有的檔案都傳到指定的同一個檔案夾中,推薦使用“年”和“月”分别建立2級子檔案夾,上傳的圖檔應該放在“月”的檔案夾中;
可以使用UUID作為檔案名;
不需要判斷原始擴充名,而是直接從原始檔案全名中截取即可;
及時打樁,輸出關鍵資訊,例如儲存檔案的檔案夾路徑、檔案名、完整路徑等,便于出錯時排查問題。
具體代碼:
@Value("${project.question.image-upload-path}")
private String imageUploadPath;
@Value(("${project.question.image-host}"))
private String imageHost;
@Value("${project.question.image-max-size}")
private long imageMaxSize;
@Value(("${project.question.image-content-types}"))
private List<String> imageContentTypes;
@PostMapping("/upload-image")
public R<String> uploadImage(MultipartFile imageFile) {
// 判斷上傳的檔案是否為空
if (imageFile.isEmpty()) {
throw new FileEmptyException("上傳圖檔失敗!請選擇有效的圖檔檔案!");
}
// 判斷上傳的檔案大小是否超标
if (imageFile.getSize() > imageMaxSize) {
throw new FileSizeException("上傳圖檔失敗!不允許使用超過" + (imageMaxSize / 1024) + "KB的圖檔檔案!");
}
// 判斷上傳的檔案類型是否超标
if (!imageContentTypes.contains(imageFile.getContentType())) {
throw new FileTypeException("上傳圖檔失敗!圖檔類型錯誤!允許上傳的圖檔類型有:" + imageContentTypes);
}
// 确定本次上傳時使用的檔案夾
String dir = DateTimeFormatter.ofPattern("yyyy/MM").format(LocalDateTime.now());
File parent = new File(imageUploadPath, dir);
if (!parent.exists()) {
parent.mkdirs();
}
log.debug("dir >>> {}", parent);
// 确定本次上傳時使用的檔案名
String filename = UUID.randomUUID().toString();
String originalFilename = imageFile.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String child = filename + suffix;
// 建立最終儲存時的檔案對象
File dest = new File(parent, child);
// 執行儲存
try {
imageFile.transferTo(dest);
} catch (IOException e) {
throw new FileIOException("上傳圖檔失敗!目前伺服器忙,請稍後再次嘗試!");
}
// 确定網絡通路路徑
String imageUrl = imageHost + dir + "/" + child; // http://localhost:8081/1.jpg
log.debug("image url >>> {}", imageUrl);
// 傳回
return R.ok(imageUrl);
}