赞
踩
在绝大多数的项目中都会涉及到文件上传等,下面我们来说一下技术派中是如何实现原生图片上传的,这个功能说起来简单,但其实对于技术还是有考验的。图片的上传涉及到IO读写,一个文件上传的功能,就可以把IO流涉及到的知识点全覆盖,比如字节流ByteArrayInputStream、缓存流BufferedOutputStream、文件File的读写权限、文件魔数等等。
如果你想实现一个自己的文件读写Util类,需要考虑的细节还是很多的,比如静态资源的配置、图片大小限制、前端图片上传组件,后端图片接收参数MutiparHttpServletRequest等等。
技术派中关于图片上传的入口有三处:
看发表文章时,涉及到四种方式:
这四种方式都会出发图片上传功能(严格一点,后面两个还涉及到图片转链)后台的接口都是一样的,都调用的是ImageRestController,上传图片调用的是upload方法,请求参数为HttpServletRequest;转存图片链接调用的是save方法,参数为图片的外部链接。响应的结果为ResVo<ImageVo> ,其中包含最关键的信息------图片路径。
- image:
- abs-tmp-path: /tmp/storage/
- web-img-path: /forum/image/
- tmp-upload-path: /tmp/forum/
- cdn-host:
- oss:
- type: local
- prefix: paicoding/
- endpoint:
- ak:
- sk:
- bucket:
- host: https://cdn.tobebetterjavaer.com
-
- spring:
- web:
- resources:
- # 支持本地图片上传之后的链接,其中 file:///d的用于win系统,后面的file: 适用于mac/linux系统
- static-locations:
- - classpath:/static/
- - file:///d:${image.abs-tmp-path}
- - file:${image.abs-tmp-path}
来解释一下参数的含义:
静态资源通常包括CSS、JavaScript、图片等文件,通过设置 spring: web: resources: static-locations,我们可以告诉 Spring Boot 在哪些位置查找静态资源。 Spring Boot 的默认静态资源位置包括;
当我们为spring.web. resources. static-locations 提供自定义的值时,Spring Boot会覆盖这些默认值。在技术派的项目结构中,我们将CSS和javaScript,以及一些图片资源放在了,paicoding-ui模块static目录下。
也就意味着,在我们的前端页面中,如果遇到类似这样的<link href="/css/views/home.css" rel="stylesheet" />请求时,Spring Boot将会从 classpath:/static/目录下去找。
注意,我们还指定了另外两个静态资源位置: file:///d:${image.abs-tmp-path} 和 file:${image.abs-tmp-path} ,前者用于Windows系统 ,后者用于macOS和Linux系统。用macOs举例,我们会把图片保存在 /tmp/storage/forum/image目录下。
也就是说,我们可以通过 http://127.0.0.1:8080/forum/image/20230423060009676_69.jpg这种形式访问图片。
file:/是一个URI(统一资源标识符)的方案,表示在本地文件上的系统资源。例如,如果你想要引用本地文件系统上的一个文件,可以使用file:/。以下是一些实例:
使用 @ConfigurationProperties 注解使其和配置文件中的图片配置关联起来。
- @Setter
- @Getter
- @Component
- @ConfigurationProperties(prefix = "image")
- public class ImageProperties {
-
- /**
- * 存储绝对路径
- */
- private String absTmpPath;
-
- /**
- * 存储相对路径
- */
- private String webImgPath;
-
- /**
- * 上传文件的临时存储目录
- */
- private String tmpUploadPath;
-
- /**
- * 访问图片的host
- */
- private String cdnHost;
-
- private OssProperties oss;
-
- public String buildImgUrl(String url) {
- if (!url.startsWith(cdnHost)) {
- return cdnHost + url;
- }
- return url;
- }
- }
我们先来看比较简单的一种,上传文章封面,在发表文章的页面,点击保存按钮,会弹出文章封面的上传模态框。
代码非常简单,用了一个input组件,type为file,接受的文件类型为image。
- <input type = "file"
- accept = "image/*"
- id = "upload"
- class = "click-input"
- />
当选择图片后,会触发change事件。
- upload.on("change", function (e) {
- let objUrl = getObjectURL(this.files[0]) //获取图片的路径,该路径不是图片在本地的路径
-
- if (objUrl) {
- console.log("uploadImg", this.value)
- uploadImg(() => (this.value = null), objUrl)
- }
- })
在事件回调函数中,代码会执行以下操作。
来看一下getObjectURL函数(创建一个临时URL,用于访问本地文件):
- //建立一?可存取到?file的url
- const getObjectURL = function (file) {
- let url = null
-
- if (window.createObjectURL != undefined) {
- // basic
- url = window.createObjectURL(file)
- } else if (window.URL != undefined) {
- // mozilla(firefox)
- url = window.URL.createObjectURL(file)
- } else if (window.webkitURL != undefined) {
- // webkit or chrome
- url = window.webkitURL.createObjectURL(file)
- }
- return url
- }
这段代码使用三种不同的方式来创建临时URL,以确保兼容性:
window.createObjectURL(file): 这是一个比较旧的方法,用于创建临时URL。在现代的浏览器中,这个方法可能被废弃。
window.URL.createObjectURL(file): 这是一个比较新的方法,用于创建按临时URL。在许多现代浏览器中(如FIrefox、Chrome、Edge等),这个方法已经取代了window.createObjectURL。
window.webkitURL.createObjectURL(file): 这是一个WenKit特定的方法,用于创建临时的URL。在基于WebKit的浏览器中(如旧版本的Chrome和Safari)这个方法可能是唯一可用的方法。
再来看uploadImge方法(实用jQuery的Ajax实现图片上传):
- // 上传头图到服务器
- function uploadImg(callback, objUrl) {
- let uploadPic = upload[0].files[0]
- console.log("准备上传", uploadPic)
-
- if (!checkFileSize(uploadPic)) {
- return;
- }
-
- let file = new FormData()
- file.append("image", uploadPic)
- $.ajax({
- url: "/image/upload",
- type: "post",
- data: file,
- cache: false,
- contentType: false,
- processData: false,
- success: function (data) {
- console.log("response data", data);
- if (data.status.code > 0) {
- // 图片上传失败
- toastr.error(data.status.msg, "图片上传失败!");
- return;
- }
-
- const {result: { imagePath },} = data || {}
- defaults['cover'] = imagePath;
-
- //将图片路径存入src中,显示出图片
- pic.attr("src", objUrl).css('visibility', 'visible') // 展示图片
- $('.upload-icon-up').css('visibility', 'hidden') // 隐藏上传
-
- callback();
- toastr.info("图片上传成功!");
- },
- error : function(jqXHR, textStatus, errorThrown) {
- toastr.error(jqXHR.responseText, "图片上传失败!");
- },
- })
- }
解释一下代码:
这段代码是一个用于上传图片到服务器的函数。下面是对代码的解释:
```javascript
// 上传头图到服务器
function uploadImg(callback, objUrl) {
let uploadPic = upload[0].files[0] // 获取上传的图片文件
console.log("准备上传", uploadPic) // 打印准备上传的图片信息
if (!checkFileSize(uploadPic)) { // 检查文件大小是否符合要求
return; // 如果不符合要求,直接返回
}
let file = new FormData() // 创建一个新的FormData对象
file.append("image", uploadPic) // 将上传的图片文件添加到FormData对象中
$.ajax({ // 使用jQuery的ajax方法发送POST请求
url: "/image/upload", // 请求的URL地址
type: "post", // 请求类型为POST
data: file, // 请求的数据为FormData对象
cache: false, // 禁用缓存
contentType: false, // 不设置Content-Type请求头
processData: false, // 不处理数据
success: function (data) { // 请求成功时的回调函数
console.log("response data", data); // 打印响应数据
if (data.status.code > 0) { // 判断图片上传是否失败
// 图片上传失败
toastr.error(data.status.msg, "图片上传失败!"); // 显示错误提示信息
return; // 结束函数执行
}
const {result: { imagePath },} = data || {} // 从响应数据中提取图片路径
defaults['cover'] = imagePath; // 将图片路径存入defaults对象中的cover属性
// 将图片路径存入src中,显示出图片
pic.attr("src", objUrl).css('visibility', 'visible') // 展示图片
$('.upload-icon-up').css('visibility', 'hidden') // 隐藏上传按钮
callback(); // 调用回调函数
toastr.info("图片上传成功!"); // 显示成功提示信息
},
error : function(jqXHR, textStatus, errorThrown) { // 请求失败时的回调函数
toastr.error(jqXHR.responseText, "图片上传失败!"); // 显示错误提示信息
},
})
}
```
这段代码定义了一个名为`uploadImg`的函数,该函数接受两个参数:`callback`和`objUrl`。`callback`是一个回调函数,在图片上传成功后会被调用;`objUrl`是图片的URL地址。
函数内部首先获取上传的图片文件,并打印出准备上传的图片信息。然后通过调用`checkFileSize`函数来检查文件大小是否符合要求,如果不符合要求则直接返回。
接下来,创建一个新的`FormData`对象,并将上传的图片文件添加到其中。然后使用jQuery的`ajax`方法发送POST请求,将`FormData`对象作为请求的数据发送给服务器的`/image/upload`接口。
在请求成功时,会打印响应数据,并根据响应结果判断图片上传是否失败。如果上传失败,会显示错误提示信息并结束函数执行。如果上传成功,会从响应数据中提取图片路径,并将其存入`defaults`对象的`cover`属性中。然后通过修改DOM元素的样式,将图片路径存入`src`属性中,并显示图片。同时,隐藏上传按钮,并调用传入的回调函数。最后,显示成功提示信息。
在请求失败时,会显示错误提示信息。
解释:
- @Permission(role = UserRole.LOGIN)
- @RequestMapping(path = {"image/", "admin/image/", "api/admin/image/",})
- @RestController
- @Slf4j
- public class ImageRestController {
-
- @Autowired
- private ImageService imageService;
-
- /**
- * 图片上传
- *
- * @return
- */
-
- @RequestMapping(path = "upload")
- public ResVo<ImageVo> upload(HttpServletRequest request) {
- ImageVo imageVo = new ImageVo();
- try {
- String imagePath = imageService.saveImg(request);
- imageVo.setImagePath(imagePath);
- } catch (Exception e) {
- log.error("save upload file error!", e);
- return ResVo.fail(StatusEnum.UPLOAD_PIC_FAILED);
- }
- return ResVo.ok(imageVo);
- }
来详细解释一下。
@RequestMapping(path = "image/")注解用于指定控制器处理的请求路径为"image"。
@RestController 注解表示这是一个用于处理RESTful风格请求的控制器。
@Slf4j 注解用于自动注入一个SLF4J日志对象
很简单,不在解释。
- /**
- * 保存图片
- *
- * @param request
- * @return
- */
- String saveImg(HttpServletRequest request);
- }
- @Override
- public String saveImg(HttpServletRequest request) {
- MultipartFile file = null;
- if (request instanceof MultipartHttpServletRequest) {
- file = ((MultipartHttpServletRequest) request).getFile("image");
- }
- if (file == null) {
- throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "缺少需要上传的图片");
- }
-
- // 目前只支持 jpg, png, webp 等静态图片格式
- String fileType = validateStaticImg(file.getContentType());
- if (fileType == null) {
- throw ExceptionUtil.of(StatusEnum.ILLEGAL_ARGUMENTS_MIXED, "图片只支持png,jpg,gif");
- }
-
- try {
- return imageUploader.upload(file.getInputStream(), fileType);
- } catch (IOException e) {
- log.error("Parse img from httpRequest to BufferedImage error! e:", e);
- throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
- }
- }
这个方法的主要功能是从HTTP请求中提取图片并保存,描述一下该方法的逻辑:
validateStaticImg方法检查传入的内容类型是否属于这些支持的类型,并返回相应的文件类型,如果文件不支持,则抛出一个异常。
- public interface ImageUploader {
- String DEFAULT_FILE_TYPE = "txt";
- Set<MediaType> STATIC_IMG_TYPE = new HashSet<>(Arrays.asList(MediaType.ImagePng, MediaType.ImageJpg, MediaType.ImageWebp, MediaType.ImageGif));
-
- /**
- * 文件上传
- *
- * @param input
- * @param fileType
- * @return
- */
- String upload(InputStream input, String fileType);
-
-
- /**
- * 获取文件类型
- *
- * @param input
- * @param fileType
- * @return
- */
- default String getFileType(ByteArrayInputStream input, String fileType) {
- if (StringUtils.isNotBlank(fileType)) {
- return fileType;
- }
-
- MediaType type = MediaType.typeOfMagicNum(FileReadUtil.getMagicNum(input));
- if (STATIC_IMG_TYPE.contains(type)) {
- return type.getExt();
- }
- return DEFAULT_FILE_TYPE;
- }
- }
来解释一下这段代码。
来看一下获取文件魔数的静态方法 getMagicNum:
- public static String getMagicNum(ByteArrayInputStream inputStream) {
- byte[] bytes = new byte[28];
- inputStream.read(bytes, 0, 28);
- inputStream.reset();
- return bytesToHex(bytes);
- }
假如是一张jpg的文件,我们来看一下魔术是多少?
- @Test
- public void testMagic() throws FileNotFoundException {
- FileInputStream fileInputStream = new FileInputStream("docs/imgs/init_00.jpg");
- ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
- byte[] buffer = new byte[4039];
- int bytesRead;
- try {
- while ((bytesRead = fileInputStream.read(buffer)) != -1) {
-
- byteArrayOutputStream.write(buffer, 0, bytesRead);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
- //算魔数
- String magicNum = FileReadUtil.getMagicNum(byteArrayInputStream);
-
- System.out.println(magicNum);
-
- //根据魔数判断文件类型
-
- MediaType mediaType = MediaType.typeOfMagicNum(magicNum);
-
- System.out.println("文件类型" + mediaType);
- }
解释:
这段代码是一个Java方法,名为`testMagic`,它没有返回值(void)并且声明了可能抛出`FileNotFoundException`异常。下面是对代码的详细解释:
1. 首先,创建一个`FileInputStream`对象`fileInputStream`,用于读取指定路径下的文件"docs/imgs/init_00.jpg"。
2. 创建一个`ByteArrayOutputStream`对象`byteArrayOutputStream`,用于存储从文件中读取的数据。
3. 定义一个长度为4039的字节数组`buffer`,用于临时存储每次从文件中读取的数据。
4. 使用循环结构,通过调用`fileInputStream.read(buffer)`方法从文件中读取数据,并将读取到的字节数赋值给变量`bytesRead`。
5. 如果`bytesRead`不等于-1,表示还有数据可以读取,将`buffer`中的数据写入`byteArrayOutputStream`中,从索引0开始,写入`bytesRead`个字节。
6. 如果在读取文件过程中发生`IOException`异常,捕获该异常并打印堆栈跟踪信息。
7. 创建一个`ByteArrayInputStream`对象`byteArrayInputStream`,使用`byteArrayOutputStream.toByteArray()`方法将`byteArrayOutputStream`中的数据转换为字节数组作为参数传入。
8. 调用`FileReadUtil.getMagicNum(byteArrayInputStream)`方法获取魔数(magic number),并将结果赋值给字符串变量`magicNum`。
9. 输出魔数`magicNum`。
10. 调用`MediaType.typeOfMagicNum(magicNum)`方法根据魔数判断文件类型,并将结果赋值给`MediaType`类型的变量`mediaType`。
11. 输出文件类型`mediaType`。
总结:这段代码主要用于读取指定路径下的文件,并将其内容转换为字节数组,然后根据魔数判断文件类型,并输出魔数和文件类型。
来看输出结果:
Java字节码文件(.class)的魔数是一个4字节的十六进制: 0xCAFEBABE。魔数是文件格式的标识符,用于表示文件类型。
- @Slf4j
- @ConditionalOnExpression(value = "#{'local'.equals(environment.getProperty('image.oss.type'))}")
- @Component
- public class LocalStorageWrapper implements ImageUploader {
- @Autowired
- private ImageProperties imageProperties;
- private Random random;
-
- public LocalStorageWrapper() {
- random = new Random();
- }
-
- @Override
- public String upload(InputStream input, String fileType) {
- // 记录耗时分布
- StopWatchUtil stopWatchUtil = StopWatchUtil.init("图片上传");
- try {
- if (fileType == null) {
- // 根据魔数判断文件类型
- InputStream finalInput = input;
- byte[] bytes = stopWatchUtil.record("流转字节", () -> StreamUtils.copyToByteArray(finalInput));
- input = new ByteArrayInputStream(bytes);
- fileType = getFileType((ByteArrayInputStream) input, fileType);
- }
-
- String path = imageProperties.getAbsTmpPath() + imageProperties.getWebImgPath();
- String fileName = genTmpFileName();
-
- InputStream finalInput = input;
- String finalFileType = fileType;
- FileWriteUtil.FileInfo file = stopWatchUtil.record("存储", () -> FileWriteUtil.saveFileByStream(finalInput, path, fileName, finalFileType));
- return imageProperties.buildImgUrl(imageProperties.getWebImgPath() + file.getFilename() + "." + file.getFileType());
- } catch (Exception e) {
- log.error("Parse img from httpRequest to BufferedImage error! e:", e);
- throw ExceptionUtil.of(StatusEnum.UPLOAD_PIC_FAILED);
- } finally {
- log.info("图片上传耗时: {}", stopWatchUtil.prettyPrint());
- }
- }
-
- /**
- * 获取文件临时名称
- *
- * @return
- */
- private String genTmpFileName() {
- return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddhhmmssSSS")) + "_" + random.nextInt(100);
- }
imageProperties: 一个ImageProperties 类型的对象,用于获取图片相关的配置信息。
upload(InputStream input, String fileType): 实现 ImageUploader接口中的upload方法,用于将给定的输入流保存到本地的文件系统。首先检查fileType是否为null,如果是,则根据输入流中的字节数据的魔术确定文件的类型,然后,根据配置文件中的路径设置和文件类型,将文件保存到本地文件,并返回文件的URL。
genTmpFileName(): 一个辅助方法,用于生成临时文件名,他使用当前日期和一个随机数生成文件名。
- public static FileInfo saveFileByStream(InputStream stream, FileInfo fileInfo) throws FileNotFoundException {
- if (!StringUtils.isBlank(fileInfo.getPath())) {
- mkDir(new File(fileInfo.getPath()));
- }
-
- String tempAbsFile = fileInfo.getPath() + "/" + fileInfo.getFilename() + "." + fileInfo.getFileType();
- BufferedOutputStream outputStream = null;
- InputStream inputStream = null;
-
- FileInfo var6;
- try {
- inputStream = new BufferedInputStream(stream);
- outputStream = new BufferedOutputStream(new FileOutputStream(tempAbsFile));
- int len = inputStream.available();
- //判断长度是否大于4K
- if (len <= 4096) {
- byte[] bytes = new byte[len];
- inputStream.read(bytes);
- outputStream.write(bytes);
- } else {
- int byteCount = false;
- byte[] bytes = new byte[4096];
- //1M逐个读取
- int byteCount;
- while((byteCount = inputStream.read(bytes)) != -1) {
- outputStream.write(bytes, 0, byteCount);
- }
- }
-
- var6 = fileInfo;
- return var6;
- } catch (Exception var16) {
- log.error("save stream into file error! filename: {} e: {}", tempAbsFile, var16);
- var6 = null;
- } finally {
- try {
- if (outputStream != null) {
- outputStream.flush();
- outputStream.close();
- }
-
- if (inputStream != null) {
- inputStream.close();
- }
- } catch (IOException var15) {
- log.error("close stream error!", var15);
- }
-
- }
-
- return var6;
- }
该方法的参数包括输入流和一个FileInfo对象,其中FileInfo对象包含了文件路径、文件名和文件类型。
代码实现了一下功能:
小结:
简单总结一下,本地图片上传和保存的逻辑可以分为前端和后端两个部分:
前端(使用Ajax上传):
a.用户选择一张土图片并上传。
b.使用FormData对象封装图片数据。FormData对象能够让你通过XMLHttpRequest发送表单数据。
c. 利用jQuery的$.ajax方法发送一个POST请求,将FormData 对象传递给后端的服务器。
后端(Java代码处理上传和保存):
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。