基於Base64編/解碼算法的Spring Boot文件上傳技術解析

文件上傳時Web應用最爲常見的功能之一,傳統的文件上傳須要定製一個特殊的form表單來上傳文件,以上傳圖片爲例,常規的作法是先上傳圖片,而後回傳圖片地址,最後在使用圖片。這無疑會帶來一個嚴重的問題:若是在接下來使用圖片的過程當中web請求中斷了或者其餘緣由致使請求關閉,那麼在服務器上就會遺留下未被使用的髒數據,還須要經過其餘的方式進行清理。我將這種設計模式稱之爲「粗獷型經濟」模式,無論市場(業務)是否消費,先生產(上傳)了再說,最後會致使資源的極度浪費。而本次分享要談的是另一種設計模式,我稱之爲「節約型經濟」模式,將生產活動(上傳)以「責任承包」制度承包(下方)給具體的業務,採用Base64解碼算法的方式,經過二進制文本同步傳輸到業務方法,最後將文件解碼存儲,以達到節約資源的效果。javascript

基本術語

1. Base64編碼

Base64編碼是從二進制到字符的過程,可用於在HTTP環境下傳遞較長的標識信息。例如,在Java Persistence系統Hibernate中,就採用Base64來將一個較長的惟一標識符(通常爲128-bit的UUID)編碼成一個字符串,用做HTTP表單和HTTP GET URL中的參數。在其餘的應用場景中,也經常須要把二進制數據編碼爲合適放在URL(包括隱藏表單域)中的形式。此時,採用Base64編碼具備不可讀性,須要解碼後才能閱讀。html

2. 文件上傳

文件上傳就是將信息從我的計算機(本地計算機)傳送到中央服務器(遠程計算機)系統上,讓網絡上的其餘用戶能夠進行訪問。文件上傳又分爲Web上傳和FTP上傳,前者直接經過點擊網頁上的鏈接便可操做,後者須要專門的FTP工具進行操做。前端

案例解析

以添加文章的需求爲一個案例,一篇文章須要有ID,標題,封面,簡介,正文等信息。針對文章封面的設置,一般的作法是在添加文章的頁面中經過異步的方式先將圖片上傳至服務器,而後回傳圖片存儲地址(URL或者URI)綁定到一個隱藏域中和一個用於預覽的IMG節點上。此時,文章主體信息是沒有提交到服務器的,但與文章相關的圖片已經先於文章到達了服務器,這就比如你想要去洗手間放翔,結果翔尚未出來,先從嘴裏嘔吐了一些東東。雖然看起來都是一個「異化」過程,但總以爲讓人「噁心」。本來放完翔(提交請求)衝一下馬桶(提交事務)就完事了,你如今還須要額外的擦拭一下地上的嘔吐物(清理垃圾文件)。java

基於上述的一個應用背景,提出了採用Base64編/解碼的方式同步上傳文件,讓文章的圖片隨文章主體信息一塊兒到達服務端,若是在請求的過程當中服務意外終止,那麼在服務器上也不會產生任何髒數據。需求和出發點就聊這麼多,接下來進入本次分享的正題,看看如何實現同步上傳文件的功能。web

功能實現

1. 解碼器

咱們須要定義一個解碼器對前端傳入的二進制的圖片數據進行解碼,對於前端如何將圖片文件採用Base64算法編碼,在接下來的內容當中單獨介紹。此時解碼器的作用主要是獲取Base64編碼的二進制文本中header信息(編碼方式)和文件類型信息。而後對數據域進行解碼。完成解碼工做後,再講字節碼轉換成咱們熟悉的MultipartFile類型對象。解碼器的實現代碼以下:算法

package com.ramostear.jfast.common.utils;

import org.springframework.web.multipart.MultipartFile;

import java.io.*;

/** * @author ramostear|譚朝紅 * @create-time 2019/3/19 0019-23:54 * @modify by : * @since: */
public class Base64Decoder implements MultipartFile{

    private final byte[] IMAGE;

    private final String HEADER;

    private Base64Decoder(byte[]image,String header){
        this.IMAGE = image;
        this.HEADER = header;
    }

    public static MultipartFile multipartFile(byte[]image,String header){
        return new Base64Decoder(image,header);
    }

    @Override
    public String getName() {
        return System.currentTimeMillis()+Math.random()+"."+HEADER.split("/")[1];
    }

    @Override
    public String getOriginalFilename() {
        return System.currentTimeMillis()+(int)Math.random()*10000+"."+HEADER.split("/")[1];
    }

    @Override
    public String getContentType() {
        return HEADER.split(":")[1];
    }

    @Override
    public boolean isEmpty() {
        return IMAGE == null || IMAGE.length == 0;
    }

    @Override
    public long getSize() {
        return IMAGE.length;
    }

    @Override
    public byte[] getBytes() throws IOException {
        return IMAGE;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(IMAGE);
    }

    @Override
    public void transferTo(File file) throws IOException, IllegalStateException {
        new FileOutputStream(file).write(IMAGE);
    }
}
複製代碼

2. 轉換器

如今,須要定義一個轉換器,將前端傳入的圖片字符信息轉換成Base64編碼的字節數組,而後調用解碼器得到最終的MultipartFile類型對象。轉換器的實現比較簡單,器代碼以下:spring

package com.ramostear.jfast.common.utils;

import org.springframework.web.multipart.MultipartFile;

import java.util.Base64;

/** * @author ramostear|譚朝紅 * @create-time 2019/3/20 0020-0:00 * @modify by : * @since: */
public class Base64Converter {

    public static MultipartFile converter(String source){
        String [] charArray = source.split(",");
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] bytes = new byte[0];
        bytes = decoder.decode(charArray[1]);
        for (int i=0;i<bytes.length;i++){
            if(bytes[i]<0){
                bytes[i]+=256;
            }
        }
        return Base64Decoder.multipartFile(bytes,charArray[0]);
    }
}
複製代碼

重點介紹一下轉換器的方法:數據庫

首先咱們先看看基於Base64算法編碼後的圖片二進制字符的格式:apache

data:image/png;base64,iVBOR....Px1yGQ9EOFXNAAAAAE1FTkSuQmcc
複製代碼

所以,先經過「,」分割字符串,拿到數據的頭部信息***data:image/png;base64*** ,再將數據的主體部分經過Base64進行轉碼,得到一個byte數組,最後調用解碼器的解碼方法獲取MultipartFile對象。canvas

3. 前端的Base64編碼

後端的核心邏輯已經完成,接下來將介紹前端如何將一張圖片採用Base64算法進行編碼。

  • 首先,須要有一個添加文章的form表單,同時將圖片域設置爲隱藏狀態,提供一個圖片預覽的dom節點和一個瀏覽本地圖片的input輸入框,表單的核心代碼以下:

    ...
     <form action="/articles" method="POST">
         ...
         <div class="file-preview">
              <div class="file-upload-zone">
                    <div class="file-upload-zone-title">Upload & preview img here …</div>
              </div>
         </div>
         <div class="clearfix"></div>
         <input type="hidden" name="cover" id="cover"/>
         <div class="input-group-btn">
             <button class="btn btn-blue" type="button" id="upload-btn">
                 <i class="fa fa-folder-open"></i>
                 <input id="upload-cover" name="upload-cover" multiple="multiple" onchange="fileChange(this)" type="file" accept="image/*"/>
             </button>
         </div>
         ...
    </form>
    ...
    複製代碼
  • 而後是定義一個fileChange方法來處理文件編碼的工做,代碼以下:

    function fileChange(obj){
            try{
                var file = obj.files[0];
                var reader = new FileReader();
                var fileName="";
                if(typeof(fileName) != "undefined"){
                    fileName = $(obj).val().split("\\").pop();
                }
                reader.onload = function(){
                    var img = new Image();
                    img.src = reader.result;
                    img.onload = function(){
                        var w = img.width,h = img.height;
                        var canvas = document.createElement("canvas");
                        var ctx = canvas.getContext("2d");
                        $(canvas).attr({
                            width:w,
                            height:h
                        });
                        ctx.drawImage(img,0,0,w,h);
                        var base64 = canvas.toDataURL("image/png",0.5);
                        var result = {
                            url:window.URL.createObjectURL(file),
                            base64:base64,
                            clearBase64:base64.substr(base64.indexOf(',')+1),
                          suffix:base64.substring(base64.indexOf(',')+1,base64.indexOf(';'))
                        };
                        $(".file-upload-zone-title").hide();
                        $(".file-upload-zone").empty();
                        $("#cover").val(result.base64);
                        $("<img src=\""+result.base64+"\" class=\"img img-responsive center-block\">").appendTo(".file-upload-zone");
                        $(".file-upload-zone").trigger("create");
                        $(".file-name").val(fileName);
                    }
                }
                reader.readAsDataURL(obj.files[0]);
            }catch(e){
                layer.msg("error");
            }
        };
    複製代碼

關於這段代碼的核心邏輯,其實與後端的解碼過程恰好相反,這裏再也不贅述。

到如今,經過Base64編碼方式同步上傳文件的核心功能已經完成,在接下來的內容中,使用Spring Boot 2.0快速的演示本次分享的內容。

添加文章服務組件#文件上傳

1. 添加文章的服務組件

接一開始的需求背景,圖片信息屬於文章對象的一個屬性值,因此處理文件上傳的邏輯後置到service中,在本次測試代碼中,最終的文件存儲採用的是七牛雲的CDN服務,關於CDN部分的代碼不進行展開,能夠上傳到本地,二者操做的對象都是MultipartFile,關於如何存儲不是本次分享的重點。文章服務組件主要代碼以下:

package com.ramostear.jfast.domain.service.impl;

import com.ramostear.jfast.common.ext.Translate;
import com.ramostear.jfast.domain.repo.ArticleRepo;
import com.ramostear.jfast.domain.service.ArticleService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/** * @author ramostear|譚朝紅 * @create-time 2019/3/19 0019-23:37 * @modify by : * @since: */
@Service(value = "articleService")
@Transactional(readOnly = true)
public class ArticleServiceImpl implements ArticleService {
    @Autowired
    private ArticleRepo articleRepo;
    
    @Override
    @Transactional
    public void save(ArticleVo vo) {
        Article article = Translate.toArticle(vo);
        articleRepo.save(article);
    }
    
複製代碼

在ArticleService服務組件中,涉及到一個Translate類,它的做用主要是講前端傳輸過來的ValueObject映射到POJO類中,同時將文件存儲的邏輯也封裝進去了,主要代碼以下:

package com.ramostear.jfast.common.ext;

import com.ramostear.jfast.common.factory.CdnFactory;
import com.ramostear.jfast.common.factory.cdn.CdnRepository;
import com.ramostear.jfast.common.utils.Base64Converter;
import com.ramostear.jfast.domain.model.Article;
import com.ramostear.jfast.domain.vo.ArticleVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.web.multipart.MultipartFile;

import java.util.Date;

/** * @author ramostear|譚朝紅 * @create-time 2019/3/18 0018-3:39 * @modify by : * @since: */
public class Translate {

    private static CdnRepository cdnRepo = CdnFactory.builder(CdnFactory.CdnType.Qiniu);
    
    public static Article toArticle(ArticleVo vo){
        Article article = new Article();
        BeanUtils.copyProperties(vo,article);
        if(StringUtils.isNotBlank(vo.getCover())){
            MultipartFile file = Base64Converter.converter(vo.getCover());
            article.setCover(cdnRepo.save(file));
        }
        return article;
    }
}

複製代碼

此處因爲使用的是七牛雲的CDN服務,因此經過一個CND的工廠類獲取一個CND倉儲實例,用於將文件寫入到倉儲中,並回傳一個文件訪問地址。除了上述的方法,還能夠調用file.transferTo()方法將文件寫入到本地(應用服務器)磁盤中。

這裏的CND工廠類實現細節因爲篇幅緣由再也不展開。須要瞭解更多關於CDN SDK使用方法,能夠在文章末尾給我留言。

2. 文章控制器

最後,定義一個控制器,提供給前端添加文章時進行調用,文章控制器主要工做是得到前端傳入的文章信息,而後調用文章服務組件,完成添加文章工做。核心代碼以下:

package com.ramostear.jfast.domain.controller;

@RestController
public class ArticleController{
    @Autowired
    ArticleService articleService;
    
    @Postmapping(value="/articles")
    public ResponseEntity<Object> createArticle(@RequestBody ArticleVo vo){
        try{
            articleService.save(vo);
            return new ResponseEntity<>("已經成功將文字寫入數據庫",HttpStatus.CREATED);
        }catch(Exception e){
            return new ResponseEntity<>(e.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    
}
複製代碼

結束語

本次分享只給出了核心部位的實現,其中涉及到的如CDN、HTML、JS等的知識沒有展開,若是給你帶來了困惑,能夠在評論區給我留言,咱們再一塊兒討論。再次感謝你們賞光拜讀,謝謝~~~

轉載請保留版權信息,勿作商業用途

做者:譚朝紅,原文標題:基於Base64編/解碼算法的Spring Boot文件上傳技術解析

相關文章
相關標籤/搜索