前言
首先说一下所使用的环境:前端用的是vue.js+vue-cli+typescript,后端用的是spring boot,没有其他多余的依赖。
最后使用效果说明:
上传图片的时候并不是通过提交form表单的形式将文件提交,而是从input节点中获取可以解析为二进制流的字符串,ajax提交这个字符串然后在java后台负责解析这个字符串,生成图片。这种方法现在看起来可能有点多余,但是以下两种情况发生时,这种方法就显得特别好用了,首先是混合式app开发的时候,节点inputtype=file是无效的,无法让用户去选择文件,只能通过调用插件让用户选择文件,调用返回一个字符串,提交这个字符串,然后在后台解析就可以实现混合式app上传文件的功能;还有一个是,当两个系统需要对接的时候,直接把文件提交过去可能有点困难,这时候如果保存了这个字符串,就可以单单把这个字符串提交,让另一个系统单独解析就行了。
先从最接近的地方开始,看vue中的ts代码(不会ts的同学,基础比较好的话,可以把ts代码转换成js,这个不难,只是很简单的逻辑):
<template> <div class="s-dev-img-upload"> <s-card class="card"> <div class="card-head" slot="head"> <Icon type="heart">Icon> <span>测试 图片选择以及预览 组件span> div> <div class="card-body" slot="body"> <span>预览:span> <div class="card-body-content area-center"> <input type="file" ref="fileInput" class="display-none"/> <Button type="primary" @click="pickImg">选择文件Button> div> div> <div class="card-footer" slot="foot"> div> s-card> div> template> <script> import SCard from 'src/base/components/s-card.vue' import {get, post, HttpOption} from 'src/base/js/public/http.js' export default { name: "s-dev-img-upload", components: { SCard }, methods: { pickImg() { this.$fileInput = $(this.$refs.fileInput); this.$fileInput.click(); } }, mounted() { this.$fileInput = $(this.$refs.fileInput); this.$fileInput.change(() => { let files = this.$fileInput.prop('files'); const file = files[0]; if (window.FileReader) { let fr = new FileReader(); fr.onloadend = (e) => { this.$fileInput.val(null); post({ url: 'http://localhost:9321/image/uploadImg', param: { dataUrl: e.target.result, name: file.name }, success: (data) => { console.log(data); fr = null; }, error: (data) => { console.log(data); fr = null; } }); }; fr.readAsDataURL(file); } }); } } script> <style scoped lang="scss"> @import "src/base/css/public/variable.scss"; .s-dev-img-upload { height: 100%; width: 100%; padding: $padding-space; display: flex; justify-content: space-around; flex-wrap: wrap; align-items: center; .card { width: 264px; .card-head { height: 44px; @include vertical-center } .card-body { min-height: 132px; display: flex; flex-direction: column; position: relative; .card-body-content { word-wrap: break-word; word-break: break-all; flex: 1; } } .card-footer { height: 44px; @include vertical-center; } } } style>
script中mounted方法中的代码是重点代码,其他是与本次内容无关的代码,简单解释为,当触发了input的change事件之后,获取所有的files,我这里只是单选,所以只有一个files[0];通过判断FileReader判断浏览器是否支持,然后发送一个ajax,post请求,把字符串以及文件名提交;
java后台代码一览:
controller
package com.martsforever.core.img; import com.martsforever.core.global.RequestManage; import com.sun.imageio.plugins.common.ImageUtil; import org.springframework.web.bind.annotation.*; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; import java.io.*; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("image") public class ImageController { /** * 测试图片文件读写功能读取classes目录下的me.jpg文件,并创建一个新的文件me1.jpg,最后还把图片输出到response供用户下载或者查看 * * @param req * @param resp * @return */ @GetMapping("readWriteImg") public Map<String, Object> getImg(HttpServletRequest req, HttpServletResponse resp) { Map<String, Object> result = new HashMap<>(); try { String resourceName = "me.jpg"; /*获取classes目录绝对路径*/ String path = this.getClass().getClassLoader().getResource(resourceName).getPath(); System.out.println(path); /*读取文件*/ FileInputStream fs = new FileInputStream(path); BufferedImage bi = ImageIO.read(fs); File file = new File("/D:/6_workspace/ideaspace_3/core/target/classes/me1.jpg"); /*写入文件*/ ImageIO.write(bi, "jpg", file); BufferedInputStream bis = new BufferedInputStream(fs); byte[] buffer = new byte[bis.available()]; bis.read(buffer); /*输出文件响应请求*/ resp.getOutputStream().write(buffer); } catch (Exception e) { e.printStackTrace(); } result.put("success", true); return result; } /** * 测试图片上传,跟一般的图片上传不一样的是,这里只接受dataUrl,然后将dataUrl解析为二进制流,最后保存为图片文件 * * @param ii * @return */ @PostMapping("uploadImg") public Map<String, Object> uploadImg(@RequestBody ImageInfo ii) { System.out.println(ii.getDataUrl()); System.out.println(ii.getName()); try { ImgUtils.saveDataUrl(ii); return RequestManage.success(ii); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return RequestManage.error(null, "系统异常!"); } }
图片工具类:
package com.martsforever.core.img; import sun.misc.BASE64Decoder; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; public class ImgUtils { /*用来将二进制字符串解析成byte数组*/ private static BASE64Decoder base64Decoder = new BASE64Decoder(); /*项目根目录*/ private static String rootPath = new File("").getAbsolutePath() + "\\"; private static final int MID_WIDTH = 300; private static final int SMALL_WIDTH = 50; /** * 传入一个ImageInfo对象; * ii.dataUrl:为用来解析为二进制流的字符串,在浏览器端通过FileReader的getAsDataUrl方法获得; * ii.name:最后文件保存的名称 * ii.path:文件保存的路径,咩有则使用项目根目录 * * @param ii * @return ii,最后更新的信息为图片文件的大小以及图片保存的路径 * @throws IOException */ public static ImageInfo saveDataUrl(ImageInfo ii) throws Exception { ii.setDataUrl(ii.getDataUrl().substring(ii.getDataUrl().lastIndexOf(",") + 1)); if (ii.getPath() == null || ii.getPath().equals("")) { ii.setPath(rootPath); } byte[] bytes = base64Decoder.decodeBuffer(ii.getDataUrl()); ByteArrayInputStream bais = new ByteArrayInputStream(bytes); BufferedImage bi = ImageIO.read(bais); ImageInfo sbi = saveBigImage(bi, rootPath, ii.getName()); ImageInfo smi = saveMidImage(bi, rootPath, ii.getName()); ImageInfo ssi = saveSmallImage(bi, rootPath, ii.getName()); ii.setPath(sbi.getPath()); ii.setSize(sbi.getSize()); ii.setMidPath(smi.getPath()); ii.setMidSize(smi.getSize()); ii.setSmallPath(ssi.getPath()); ii.setSmallSize(ssi.getSize()); ii.setType(ii.getName().substring(ii.getName().lastIndexOf(".") + 1)); return ii; } /** * 缩放图片 * * @param oldBi 图片源 * @param fixWidth 固定宽度缩小 * @param fixHeight 固定高度缩小 * @return */ private static BufferedImage scaleImage(BufferedImage oldBi, Integer fixWidth, Integer fixHeight) throws Exception { int width = oldBi.getWidth(); int height = oldBi.getHeight(); /*如果输入的固定宽度为空,则按照固定高度缩小,反之亦然*/ if (fixWidth == null) { fixWidth = (width * fixHeight / height); } else if (fixHeight == null) { fixHeight = (height * fixWidth / width); } else { throw new Exception("固定宽度或者固定宽度不能同时为空!"); } BufferedImage newBi = new BufferedImage(fixWidth, fixHeight, BufferedImage.TYPE_INT_ARGB); Graphics g = newBi.getGraphics(); g.drawImage(oldBi.getScaledInstance(fixWidth, fixHeight, java.awt.Image.SCALE_SMOOTH), 0, 0, null); g.dispose(); return newBi; } /** * 保存图片为文件 * * @param bi * @param fullPath * @return * @throws IOException */ private static ImageInfo saveImage(BufferedImage bi, String fullPath) throws IOException { ImageInfo imageInfo = new ImageInfo(); File newFile = new File(fullPath); if (newFile.exists()) { System.out.println("文件已经存在:" + fullPath); } else { ImageIO.write(bi, "png", newFile); imageInfo.setPath(fullPath); imageInfo.setSize(newFile.length() + ""); } return imageInfo; } /** * 保存大图片,就是原图 * * @param bi * @param path * @param fileName * @return * @throws Exception */ private static ImageInfo saveBigImage(BufferedImage bi, String path, String fileName) throws Exception { return saveImage(bi, path + "big." + fileName); } /** * 保存中图片 * * @param bi * @param path * @param fileName * @return * @throws Exception */ private static ImageInfo saveMidImage(BufferedImage bi, String path, String fileName) throws Exception { bi = scaleImage(bi, null, MID_WIDTH); return saveImage(bi, path + "mid." + fileName); } /** * 保存小图片 * * @param bi * @param path * @param fileName * @return * @throws Exception */ private static ImageInfo saveSmallImage(BufferedImage bi, String path, String fileName) throws Exception { bi = scaleImage(bi, null, SMALL_WIDTH); return saveImage(bi, path + "small." + fileName); } }
图片实体:
package com.martsforever.core.img; public class ImageInfo { private String name; private String size; private String midSize; private String smallSize; private String type; private String dataUrl; private String path; private String midPath; private String smallPath; public String getMidSize() { return midSize; } public void setMidSize(String midSize) { this.midSize = midSize; } public String getSmallSize() { return smallSize; } public void setSmallSize(String smallSize) { this.smallSize = smallSize; } public String getMidPath() { return midPath; } public void setMidPath(String midPath) { this.midPath = midPath; } public String getSmallPath() { return smallPath; } public void setSmallPath(String smallPath) { this.smallPath = smallPath; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSize() { return size; } public void setSize(String size) { this.size = size; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getDataUrl() { return dataUrl; } public void setDataUrl(String dataUrl) { this.dataUrl = dataUrl; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } }
maven中没有其他额外的依赖。在controller中,由于spring boot已经将请求参数体注入到ImageInfo中,所以controller中没有其他逻辑,只有一个数据输入输出;在ImgUtils中,saveDataUrl方法是重点,现在只暴露一个方法,
我们拿到的dataUrl理论上应该是这样的:
很长很长,在解析的时候需要先将开头逗号之前的字符去掉,包括逗号,然后我是顺便将图片保存为大图(原图),中图和小图,如果传入路径参数为空的话,就默认保存在项目根目录下面,最后将大图中图小图的保存路径以及大小放到图片信息实体ImageInfo中,返回给客户端。
在一次文件上传的过程中,java日志只有这么一点:
前端日志:
如果咩有设置特别的保存路径,在项目根目录下会生成三个文件: