个人技术总结
个人技术总结
——使用JSON传输图片
这个作业属于哪个课程 | 2021春软件工程实践|S班 |
---|---|
这个作业要求在哪里 | 软件工程实践总结&个人技术博客 |
这个作业的目标 | 分析描述并总结擅长的相关技术 |
其他参考文献 | 见文末 |
1技术概述
一种后端使用JSON接收前端上传图片的技术,适用于前端使用JSON提交文件的情况,且该方法相较于其他传输方法有适用平台广、更符合RESTful思想的特点,难点在于对图片信息进行Base64格式的编解码。
2技术详述
2.1主要思想
本文介绍的图片上传方式,与Base64编解码密接相关,主要思路如下:
1.前端将图片转换为Base64格式字符串(因为在团队开发中前端使用微信小程序技术,直接将文件转换Base64的图片上传组件能很容易找到),并提取必要的图片信息;
2.将该字符串和图片信息根据接口文档填充到JSON中,并发送给后端;
3.后端接收前端JSON,解析JSON内容,将图片Base64字符串转换为字节数组;
4.根据前端提供的图片信息将字节数组保存为相应格式文件。
2.2后端实现过程
接下来主要讲解后端对图片的处理过程
1.在Controller层中,从JSON数据中获取图片的Base64字符串
@PostMapping("/imgupload")
public Result saveImage(@RequestBody ImageRequest imageRequest) {
/*可在此鉴权,并进行其他初始化工作*/
String base64Source = imageRequest.getBase64Str();
Pair<ExceptionInfo,String> info = imageService.saveImage(base64Source);//交由Service层处理
/*......*/
}
2.在Service层中,为图片生成文件名
一个前端传来的Base64字符串的例子:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABTMAAAOtCAYAAABdcvHGAAASFnRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMnd3dy5mcmVlZG
首先判断文件后缀名。根据字符串前端包含的Mime类型信息,判断文件格式,对应相应的扩展名(如上例中的image/png对应的就是.png扩展名)。
/*为方便从字符串中提取信息,这里的prefix包含第一个,前的所有内容。
Map<String,String> mimeTypeMap中包含类似("data:image/png;base64",".png"),("data:image/jpeg;base64",".jpeg"),("data:image/gif;base64",".gif")这样的(key,value)键值对
/**
* 根据Base64字符串前缀生成文件后缀名
*
* @param prefix Base64字符串前缀,类似data:image/png;base64
* @return 文件后缀名
*/
public static String generateFileSuffixByBase64Prefix(String prefix) {
String suffix = "";
if (prefix != null) {
if(mimeTypeMap.containsKey(prefix)){
suffix = mimeTypeMap.get(prefix);
}
}
return suffix;
}
之后,为图片生成文件名前缀,这里使用UUID减少名字重复的可能。
String uuid = UUID.randomUUID().toString().replace("-","");//去除原本UUID中的横杠“-”
最后,将前缀和后缀拼接形成文件名。
String backendFileName = uuid + suffix;
3.解码Base64并保存。使用Java8及以上的Base64标准类库java.util.Base64中的解码器将Base64字符串转换为字节数组。再保存到文件中。
Decoder decoder = Base64.getDecoder();//获取解码器
try (OutputStream outputStream = new FileOutputStream(Paths.get(folderName,backendFileName).toString())) {
byte[] bytes = decoder.decode(source);//使用解码器解码Base64字符串为字节数组
for (int i = 0;i < bytes.length;i++) {//矫正偏移
if (bytes[i] < 0) {
bytes[i] += 256;
}
}
outputStream.write(bytes);//写入文件
}catch (FileNotFoundException e) {
/*处理异常*/
e.printStackTrace();
}catch (IOException e) {
/*处理异常*/
e.printStackTrace();
}
4.经过以上步骤,前端图片就上传到后端并保存到服务器中了。服务器经过进一步设置,前端就可访问图片资源。
3技术使用中遇到的问题和解决过程
问题:先前使用的sun.misc.BASE64Decoder提供的解码器会在Eclipse或者VSCode中报告错误
Access restriction: The type 'BASE64Decoder' is not API
并且经过进一步了解,使用这种解码器的效率不高
解决过程:通过网上搜索资料,一开始查找的是如何解决报错问题,但是找到解决方法后,考虑到其他队友的配置问题遂放弃这条思路,转变为查找更多Java中用于Base64解码的API,在网上的一篇文章中找到了Java8及以上版本可以使用java.util.Base64包对Base64进行编解码,并且效率是sun.misc的11倍以上,并且无需多余的配置,直接引包即可使用,不会报错,问题圆满解决。
import java.util.Base64;
import java.util.Base64.Decoder;//解码器
import java.util.Base64.Encoder;//编码器
4总结
使用JSON传输图片有好处也有坏处。
好处是相对于其他文件传输方式更易于理解,和处理其它接口一样,都是使用JSON与后端交互,后端可以有一个统一的处理方法,处理方法也更加灵活,可以加入鉴权等功能,而且可以隐藏用户文件的隐私细节,只需要图片的格式信息即可;此外,JSON格式的请求处理并没有限制后端使用的技术,后端使用其他框架,其他语言,前端无需作出修改也能够比较好的实现后端文件的接收,更加符合REST风格,并且也降低了对前端的要求,前端一般都可以都可以实现图片到Base64的转换。
坏处是增加了编码和解码的两个环节,增加了处理时间,降低了效率;更糟糕的是使用Base64表示文件数据会使数据量增大,增加了33.3%的字节数,我想这可能就是这种方法没有其他文件传输方法常用的原因。
5参考文献与参考博客
1.base64 - 廖雪峰的官方网站 (liaoxuefeng.com)
2.Base64编码为什么会使数据量变大? - 尘恍若梦 - 博客园 (cnblogs.com)
3.eclipse报Access restriction: The type 'BASE64Decoder' is not API处理方法_蝈蝈的博客-CSDN博客
4.Java实现Base64加解密的方式_小菜鸟入门-CSDN博客
6主题提炼 ——一个简单的例子
前端发送json的一个例子
{
"base64Str":"data:image/jpg;base64,/9j/4RXrRXhpZgAATU0AKgAAAAgADQEAAAMAAAABAuoAAAEBAAMAAAABAuoAAAECAAMAAAADAAAAqgEGAAMAAAABAAIAAAESAAMAAAABAAEAAAEVAAMAAAABAA"/*省略了之后的部分*/
}
1.后端Controller处理代码
@PostMapping("/imgupload")
public Result savePostImage(@RequestBody Map<String,Object> requestMap
, HttpServletRequest request) {
String base64Source = (String)requestMap.get("base64Str");//分离Base64字符串
Result result;
if (base64Source != null && fileName != null) {
Pair<ExceptionInfo,String> info = imageService.saveImage(base64Source);//交由Service层处理
if (info.getKey().equals(ExceptionInfo.OK)) {
result = Result.success(info.getValue());
} else {
result = Result.error(info.getKey().getCode(), info.getKey().getMessage());
}
}else {
result = Result.error(ExceptionInfo.POST_IMAGE_CONTENT_EMPTY.getCode()
,ExceptionInfo.POST_IMAGE_CONTENT_EMPTY.getMessage());
}
return result;
}
2.Service层处理方法saveImage
public Pair<ExceptionInfo,String> saveImage(String source, String fileName) {
Pair<ExceptionInfo,String> info = new Pair<>(ExceptionInfo.POST_IMAGE_STORE_FAIL,"");
//分离字符串中前部的信息串data:image/jpg;base64和以/9j/4RXrRXhp开始的可以解码的Base64字符串
String[] sourceOrigin = source.split(",");
if (sourceOrigin.length == 2) {
//使用工具类Base64Util根据信息头生成文件名后缀
String suffix = Base64Util.generateFileSuffixByBase64Prefix(sourceOrigin[0]);
if (!StringUtils.isBlank(suffix)) {
String uuid = UUID.randomUUID().toString().replace("-","");//使用UUID生成文件名
String backendFileName = uuid + suffix;
//使用工具类Base64Util解码并保存图片文件
int result = Base64Util.decryptByBase64AndSave(sourceOrigin[1],backendFileName);
if (result == 0) {
info = new Pair<>(ExceptionInfo.OK,backendFileName);
}else if (result == 1 ) {
info = new Pair<>(ExceptionInfo.POST_IMAGE_FOLDER_NOT_CREATED,"");
}else if (result == 2) {
info = new Pair<>(ExceptionInfo.POST_IMAGE_CONTENT_EMPTY,"");
}else if (result == 3) {
info = new Pair<>(ExceptionInfo.POST_IMAGE_STORE_PATH_NOT_FOUND,"");
}
}
}
return info;
}
3.工具类Base64Util的实现
/**
* Base64图片解码工具类
*
* <p>
* 用于解析前端发送的Base64图片数据,并保存到程序根目录Base64Decoded/文件夹下
* </p>
*
* @author Tars
* @since 2021-5-2
*/
public class Base64Util {
private static boolean isFolderExist;
private static String folderName="static";//存放在根目录该文件夹下
private static Map<String,String> mimeTypeMap;
//初始化信息头与文件名后缀的映射
static {
Map<String,String> typeMap = new HashMap<>();
typeMap.put("data:image/png;base64",".png");//png格式图片
typeMap.put("data:image/jpeg;base64",".jpeg");//jpeg格式图片
typeMap.put("data:image/gif;base64",".gif");//gif格式图片
typeMap.put("data:image/bmp;base64",".bmp");//bmp格式图片
typeMap.put("data:image/x-icon;base64",".ico");//ico格式图片
setMimeTypeMap(typeMap);
folderName = Paths.get(System.getProperty("user.dir"),folderName).toString();
File file = new File(folderName);
//存放创建图片的文件夹,存放在根目录static文件夹下
if (!file.exists()) {
isFolderExist = file.mkdir();
if (isFolderExist) {
System.out.println("文件夹创建成功");
}else {
System.out.println("文件夹创建失败");
}
}else {
isFolderExist = true;
}
}
public static String getFolderName(){
return Base64Util.folderName;
}
/**
* 解码Base64字符串,并以指定文件名存储到Base64图片存储位置
*
* @param source 要解码的Base64字符串
* @param saveName 存储文件名
* @return the int 结果代码:0:成功,1:文件夹未创建,:2:Base64字符串或存储文件名为空
* ,3:未找到存储位置,4:文件写入失败
*/
public static int decryptByBase64AndSave(String source,String saveName) {
int result = 0;
if (!isFolderExist) {
result = 1;
}else if (StringUtils.isBlank(source) || StringUtils.isBlank(saveName)) {
result = 2;
}else {
Decoder decoder = Base64.getDecoder();//获取解码器
try (OutputStream outputStream = new FileOutputStream(Paths.get(folderName,saveName).toString())) {
byte[] bytes = decoder.decode(source);//使用解码器解码Base64字符串为字节数组
for (int i = 0;i < bytes.length;i++) {//矫正偏移
if (bytes[i] < 0) {
bytes[i] += 256;
}
}
outputStream.write(bytes);//写入文件
}catch (FileNotFoundException e) {
result = 3;
e.printStackTrace();
}catch (IOException e) {
result = 4;
e.printStackTrace();
}
}
return result;
}
/**
* 根据Base64字符串前缀生成文件后缀名
*
* @param prefix Base64字符串前缀,类似data:image/png;base64
* @return 文件后缀名
*/
public static String generateFileSuffixByBase64Prefix(String prefix) {
String suffix = "";
if (prefix != null) {
if(mimeTypeMap.containsKey(prefix)){
suffix = mimeTypeMap.get(prefix);
}
for (Map.Entry<String,String> mimePair : mimeTypeMap.entrySet()) {
if (prefix.equals(mimePair.getKey())) {
suffix = mimePair.getValue();
break;
}
}
}
return suffix;
}
public static void setMimeTypeMap(Map<String, String> mimeTypeMap) {
Base64Util.mimeTypeMap = mimeTypeMap;
}
}