文件上傳時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文件上傳技術解析