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