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

文件上傳時Web應用最為常見的功能之一颈墅,傳統(tǒng)的文件上傳需要定制一個特殊的form表單來上傳文件蜡镶,以上傳圖片為例,常規(guī)的做法是先上傳圖片恤筛,然后回傳圖片地址官还,最后在使用圖片。這無疑會帶來一個嚴重的問題:如果在接下來使用圖片的過程中web請求中斷了或者其他原因導致請求關閉毒坛,那么在服務器上就會遺留下未被使用的臟數(shù)據(jù)望伦,還需要通過其他的方式進行清理。我將這種設計模式稱之為“粗獷型經(jīng)濟”模式煎殷,不管市場(業(yè)務)是否消費屯伞,先生產(chǎn)(上傳)了再說,最后會導致資源的極度浪費豪直。而本次分享要談的是另外一種設計模式劣摇,我稱之為“節(jié)約型經(jīng)濟”模式,將生產(chǎn)活動(上傳)以“責任承包”制度承包(下方)給具體的業(yè)務弓乙,采用Base64解碼算法的方式末融,通過二進制文本同步傳輸?shù)綐I(yè)務方法,最后將文件解碼存儲暇韧,以達到節(jié)約資源的效果勾习。

基本術語

1. Base64編碼

Base64編碼是從二進制到字符的過程,可用于在HTTP環(huán)境下傳遞較長的標識信息懈玻。例如巧婶,在Java Persistence系統(tǒng)Hibernate中,就采用Base64來將一個較長的唯一標識符(一般為128-bit的UUID)編碼成一個字符串,用作HTTP表單和HTTP GET URL中的參數(shù)艺栈。在其他的應用場景中英岭,也常常需要把二進制數(shù)據(jù)編碼為合適放在URL(包括隱藏表單域)中的形式。此時眼滤,采用Base64編碼具有不可讀性巴席,需要解碼后才能閱讀。

2. 文件上傳

文件上傳就是將信息從個人計算機(本地計算機)傳送到中央服務器(遠程計算機)系統(tǒng)上诅需,讓網(wǎng)絡上的其他用戶可以進行訪問。文件上傳又分為Web上傳和FTP上傳荧库,前者直接通過點擊網(wǎng)頁上的連接即可操作堰塌,后者需要專門的FTP工具進行操作。

案例解析

以添加文章的需求為一個案例分衫,一篇文章需要有ID场刑,標題,封面蚪战,簡介牵现,正文等信息。針對文章封面的設置邀桑,通常的做法是在添加文章的頁面中通過異步的方式先將圖片上傳至服務器瞎疼,然后回傳圖片存儲地址(URL或者URI)綁定到一個隱藏域中和一個用于預覽的IMG節(jié)點上。此時壁畸,文章主體信息是沒有提交到服務器的贼急,但與文章相關的圖片已經(jīng)先于文章到達了服務器,這就好比你想要去洗手間放翔捏萍,結果翔還沒有出來太抓,先從嘴里嘔吐了一些東東。雖然看起來都是一個“異化”過程令杈,但總覺得讓人“惡心”走敌。原本放完翔(提交請求)沖一下馬桶(提交事務)就完事了,你現(xiàn)在還需要額外的擦拭一下地上的嘔吐物(清理垃圾文件)逗噩。

基于上述的一個應用背景掉丽,提出了采用Base64編/解碼的方式同步上傳文件,讓文章的圖片隨文章主體信息一起到達服務端给赞,如果在請求的過程中服務意外終止机打,那么在服務器上也不會產(chǎn)生任何臟數(shù)據(jù)。需求和出發(fā)點就聊這么多片迅,接下來進入本次分享的正題残邀,看看如何實現(xiàn)同步上傳文件的功能。

功能實現(xiàn)

1. 解碼器

我們需要定義一個解碼器對前端傳入的二進制的圖片數(shù)據(jù)進行解碼,對于前端如何將圖片文件采用Base64算法編碼芥挣,在接下來的內(nèi)容當中單獨介紹驱闷。此時解碼器的做用主要是獲取Base64編碼的二進制文本中header信息(編碼方式)和文件類型信息。然后對數(shù)據(jù)域進行解碼空免。完成解碼工作后空另,再講字節(jié)碼轉換成我們熟悉的MultipartFile類型對象。解碼器的實現(xiàn)代碼如下:

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. 轉換器

現(xiàn)在蹋砚,需要定義一個轉換器扼菠,將前端傳入的圖片字符信息轉換成Base64編碼的字節(jié)數(shù)組,然后調用解碼器獲得最終的MultipartFile類型對象坝咐。轉換器的實現(xiàn)比較簡單循榆,器代碼如下:

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算法編碼后的圖片二進制字符的格式:

data:image/png;base64,iVBOR....Px1yGQ9EOFXNAAAAAE1FTkSuQmcc

因此,先通過“墨坚,”分割字符串秧饮,拿到數(shù)據(jù)的頭部信息data:image/png;base64 ,再將數(shù)據(jù)的主體部分通過Base64進行轉碼,獲得一個byte數(shù)組泽篮,最后調用解碼器的解碼方法獲取MultipartFile對象盗尸。

3. 前端的Base64編碼

后端的核心邏輯已經(jīng)完成,接下來將介紹前端如何將一張圖片采用Base64算法進行編碼帽撑。

  • 首先泼各,需要有一個添加文章的form表單,同時將圖片域設置為隱藏狀態(tài)油狂,提供一個圖片預覽的dom節(jié)點和一個瀏覽本地圖片的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");
            }
        };
    

關于這段代碼的核心邏輯专筷,其實與后端的解碼過程剛好相反弱贼,這里不再贅述。

到現(xiàn)在磷蛹,通過Base64編碼方式同步上傳文件的核心功能已經(jīng)完成吮旅,在接下來的內(nèi)容中,使用Spring Boot 2.0快速的演示本次分享的內(nèi)容味咳。

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

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工廠類實現(xiàn)細節(jié)由于篇幅原因不再展開似忧。需要了解更多關于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<>("已經(jīng)成功將文字寫入數(shù)據(jù)庫",HttpStatus.CREATED);
        }catch(Exception e){
            return new ResponseEntity<>(e.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
    
}

結束語

本次分享只給出了核心部位的實現(xiàn)拗盒,其中涉及到的如CDN、HTML锥债、JS等的知識沒有展開陡蝇,如果給你帶來了困惑,可以在評論區(qū)給我留言哮肚,我們再一起討論登夫。再次感謝大家賞光拜讀,謝謝~~~

<center>轉載請保留版權信息允趟,勿做商業(yè)用途</center>

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

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市潮剪,隨后出現(xiàn)的幾起案子涣楷,更是在濱河造成了極大的恐慌,老刑警劉巖抗碰,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狮斗,死亡現(xiàn)場離奇詭異,居然都是意外死亡弧蝇,警方通過查閱死者的電腦和手機碳褒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門折砸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人骤视,你說我怎么就攤上這事鞍爱。” “怎么了专酗?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵睹逃,是天一觀的道長。 經(jīng)常有香客問我祷肯,道長沉填,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任佑笋,我火速辦了婚禮翼闹,結果婚禮上,老公的妹妹穿的比我還像新娘蒋纬。我一直安慰自己猎荠,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布蜀备。 她就那樣靜靜地躺著关摇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碾阁。 梳的紋絲不亂的頭發(fā)上输虱,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音脂凶,去河邊找鬼宪睹。 笑死,一個胖子當著我的面吹牛蚕钦,可吹牛的內(nèi)容都是我干的亭病。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼冠桃,長吁一口氣:“原來是場噩夢啊……” “哼命贴!你這毒婦竟也來了?” 一聲冷哼從身側響起食听,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤胸蛛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后樱报,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葬项,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年迹蛤,在試婚紗的時候發(fā)現(xiàn)自己被綠了民珍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片襟士。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖嚷量,靈堂內(nèi)的尸體忽然破棺而出陋桂,到底是詐尸還是另有隱情,我是刑警寧澤蝶溶,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布嗜历,位于F島的核電站,受9級特大地震影響抖所,放射性物質發(fā)生泄漏梨州。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一田轧、第九天 我趴在偏房一處隱蔽的房頂上張望暴匠。 院中可真熱鬧,春花似錦傻粘、人聲如沸每窖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岛请。三九已至,卻和暖如春警绩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盅称。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工肩祥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缩膝。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓混狠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親疾层。 傳聞我的和親對象是個殘疾皇子将饺,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML標準痛黎。 注意:講述HT...
    kismetajun閱讀 27,422評論 1 45
  • HTML 5 HTML5概述 因特網(wǎng)上的信息是以網(wǎng)頁的形式展示給用戶的予弧,因此網(wǎng)頁是網(wǎng)絡信息傳遞的載體。網(wǎng)頁文件是用...
    阿啊阿吖丁閱讀 3,828評論 0 0
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • 山海苑客棧中自帶酒樓湖饱,十分寬敞掖蛤,此刻正是午間,一樓內(nèi)客人也算不少井厌。周一仙卻看也不看周圍一眼蚓庭,走在眾人前面致讥,對著老板...
    可可豆子閱讀 1,234評論 0 5
  • 211123 李豫 男 遼寧大連 ID:旅一23李豫 擅長唱歌 愛好聽音樂,讀書 希望英語課能一如既往地有趣下去 ...
    旅一23李豫閱讀 386評論 0 1