Loading

个人技术总结

个人技术总结

——使用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.经过以上步骤,前端图片就上传到后端并保存到服务器中了。服务器经过进一步设置,前端就可访问图片资源。

base64解码流程

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;
    }
}
posted @ 2021-06-28 15:10  Tarsss  阅读(92)  评论(7编辑  收藏  举报