日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

在面向架構編程一文中,我闡述了自己對架構和代碼之間的關系的看法:「代碼需要反映出架構」!

本文通過對文件服務核心功能的設計與實現,來驗證這一觀點。設計過程融合了「用例驅動設計」和「領域驅動設計」!

本文及后續幾篇文章會設計并開發幾個實際的系統,同時嘗試總結一套適用的架構設計與開發流程。歡迎探討!

功能

文件服務器的核心功能就兩個:「文件上傳」和「文件下載」!其中上傳可能需要支持斷點續傳、分片上傳。而下載可能需要進行下載保護,例如非指定客戶端無法下載。

除了這兩個核心功能,一般都會有一個額外功能,就是「轉換」!轉換包括:

  • 圖片規格轉換:一張圖片需要切分多個不同的尺寸
  • 添加水印:圖片或視頻需要添加水印
  • 格式轉換
  • 文件格式轉換:office轉pdf,pdf轉word,pdf轉圖片,office轉圖片等
  • 視頻格式轉換:mp4轉m3u8,碼率轉換等

除了上面的業務功能外,還包括如下非功能性約束:

  • 安全性:是否需要認證后才能上傳或下載
  • 伸縮性:是否支持擴容,提高訪問量
  • 可用性:作為基礎服務,可用性不低于4個9
  • 可配置性:對于轉換方式、上傳下載方式等內容需要提供可配置能力
  • 擴展性:能方便的進行功能擴展,例如對轉換方式的擴展

初步流程

  • 上傳流程
架構設計:文件服務的設計與實現

 

  • 下載流程
架構設計:文件服務的設計與實現

 

初步模塊劃分

根據功能,可劃分如下功能模塊:

  • 上傳模塊(核心模塊):處理文件上傳
  • 下載模塊(核心模塊):處理文件下載
  • 轉換模塊:處理文件類型轉換
  • 配置模塊:對文件服務進行配置
  • 安全模塊:對文件服務進行安全保護

架構設計

首先通過分層架構對模塊進行一個大致的劃分,按照領域設計的分層方式:

  • 應用層:配置模塊,安全模塊
  • 領域層:上傳模塊,下載模塊,轉換模塊
架構設計:文件服務的設計與實現

 

從上面的流程可以看到「上傳模塊」對「轉換模塊」有一定的依賴,像下面這樣:

架構設計:文件服務的設計與實現

 

但是,「上傳模塊」是核心模塊,而「轉換模塊」是非核心模塊。核心模塊的功能相對穩定,非核心模塊的功能相對不穩定。讓穩定的模塊去依賴不穩定的模塊,會導致穩定的模塊也不穩定,所以需要對依賴進行「倒置」。

架構設計:文件服務的設計與實現

 

「依賴倒置」解決了模塊依賴問題。但是轉換是個很耗時的過程,例如用戶上傳視頻,在不轉換的情況下,只要上傳完成就可以得到響應,但是如果轉換的話,可能就需要雙倍甚至三四倍的時間才能得到反饋,體驗非常的不好。且一般上傳和觀看的時效性并不需要即時性,所以轉換應該是個異步的過程。

異步執行的方式很多,比如基于事件,自定義線程等。這里通過事件的方式來進行處理。(領域事件可參考領域設計:領域事件)

架構設計:文件服務的設計與實現

 

文件上傳會創建UploadEvent,UploadListener監聽UploadEvent事件,當監聽到了UploadEvent,則執行轉換。

轉換流程異步化后,如何告知客戶端轉換結果呢?有幾種方案:

  • 上傳完成后,文件服務返回一個token,后續業務系統通過token來獲取轉換后的URL。此方案需要業務系統請求兩次。
  • 文件服務轉換完成后入庫,業務系統從數據庫獲取。此方案也需要業務系統請求兩次,且對不同的業務需要有不同的實現。
  • 文件服務轉換完成后回調業務系統。此方案可能需要實現不同的業務回調接口。
  • 文件服務器返回一個事先生成的URL,在文件轉換完成時返回特定狀態碼,在轉換完成后,返回文件。對于某些場景無法事先生成URL,例如office轉圖片,一個文檔會轉成多張圖片,轉換前無法得知圖片URL

目前主流做法是第一種,不過為保證文件服務器的適用性,需要能支持多種方案。故對轉換后的通知也基于事件進行處理,轉換后創建對應事件,關注該事件的對象來做出對應的處理。一個可能處理流程如下:

  • 上傳完成后,文件服務器返回原始文件地址以及token。業務系統在redis針對此token創建監聽
  • 文件服務器在轉換完成后創建轉換事件,轉換事件監聽對象監聽到此事件后,向redis發送通知
  • 業務系統接收到通知,更新URL

另外對于下載來說,實際直接通過Nginx這樣的web服務器就可以了,所以下載模塊可以直接獨立。

對于配置模塊來說,配置可以分為兩種:

  • 文件服務自身需要的配置信息。例如:上傳文件目錄。這屬于「靜態配置」
  • 各個調用系統需要的各自的配置。例如:某些系統需要切100*100的圖,而有些系統需要切200*200的圖。這屬于「動態配置」

「靜態配置」可以使用屬性文件進行配置即可。「動態配置」需要根據不同的系統進行相應的配置,故針對圖片和視頻等資源配置,創建對應的配置類,根據參數通過Respository動態構建。

整體結構如下:

架構設計:文件服務的設計與實現

 

流程調整

基于上面的設計,流程需要進行相應的調整。

  • 上傳流程
架構設計:文件服務的設計與實現

 

下載流程不變,多了一個獲取轉換后文件鏈接的流程:

架構設計:文件服務的設計與實現

 

模塊調整

相應的模塊也有調整,新增了一個消息模塊,用于處理消息的發送與監聽。這個消息屬于領域事件,所以也放在領域層。

架構設計:文件服務的設計與實現

 

架構驗證

業務流程驗證

上傳流程

  • 客戶端上傳文件
  • 通過「安全模塊」驗證。如果驗證失敗,返回驗證失敗信息
  • 如果驗證成功,通過「上傳模塊」上傳文件
  • 「上傳模塊」構建「上傳事件」,添加到消息總線中
  • 上傳完成,返回用戶消息。消息包含原始文件URL,如果需要轉換的話,則包含轉換對應的token
  • 「轉換模塊」監聽到「上傳事件」,根據「配置模塊」的配置,進行轉換
  • 「轉換模塊」構建轉換消息,添加到消息總線中
  • 對應「監聽模塊」監聽到轉換消息,進行后續處理。例如信息入庫或通知業務系統

下載流程

  • 客戶端下載文件
  • 通過「安全模塊」驗證。如果驗證失敗,返回驗證失敗信息
  • 如果驗證成功,通過「下載模塊」下載文件

獲取真實鏈接流程

  • 客戶端攜帶token獲取真實鏈接
  • 「下載模塊」根據token查詢文件是否轉換成功
  • 如果轉換成功,則返回轉換后的URL
  • 否則返回未轉換成功狀態碼

非功能性約束驗證

  • 安全性:由「安全模塊」保障
  • 伸縮性:對于下載來說,可通過CDN處理。對于上傳來說,文件服務本身沒有狀態,可方便擴容
  • 可用性:支持多點部署,常用故障轉移手段都可使用
  • 可配置性:由「配置模塊」保障
  • 擴展性:基于事件的處理方式,通過添加事件響應對象來進行功能擴展

例如,現在要新增一個「秒傳功能」,即對于服務器已經存在的文件,不再進行上傳操作,直接返回文件URL!那么需要做如下擴展:

  • 新增存儲邏輯,用于保存文件地址與文件hash的關系
  • 新增一個檢查文件hash的接口,如果hash已存在,返回文件URL,否則返回false
  • 添加一個UploadEvent同步監聽事件,當文件上傳成功后,對文件取hash,將數據保存到上面創建的表中

上面的修改不需要對現有流程做任何改動。

技術選型

  • 公司核心技術語言為JAVA,故優先選擇使用Java語言開發
  • 框架基于SpringBoot,基于如下考慮:
  • SpringBoot是目前JavaEE開發事實上的標準框架
  • 可獨立部署,亦可以升級到基于SpringCloud的微服務,方便向微服務架構遷移
  • 配置信息決定不使用數據庫,而使用屬性文件配置,基于如下考量:
  • 靜態配置配置后基本不需要修改
  • 動態配置修改幾率也不大,如果需要調整,SpringBoot本身支持實時刷新配置
  • 微服務部署,可結合分布式配置服務器實現動態配置
  • 不需要部署數據庫,不需要設計表結構,節省部署與設計時間。但是考慮到擴展性,配置邏輯需要抽象,以支持其他持久化方式
  • 轉換結果信息使用文件形式存儲,基于如下考量:
  • 結果信息是一次讀取內容,且頻率不高
  • 本身就是文件服務,使用文件存儲也合理
  • 不需要部署數據庫,不需要設計表結構,節省部署與設計時間

實現

架構設計:文件服務的設計與實現

結構與架構圖一致

事件實現

事件串聯了整個上傳流程:

  • 文件上傳,觸發UploadEvent
  • UploadListener監聽到UploadEvent,委托各個Converter進行文件處理
  • 轉換完成后觸發ConvertEvent
  • ConvertListener監聽到ConvertEvent后,進行轉換后的信息處理

由于目前大部分是內部事件,故使用Spring事件來處理,代碼邏輯如下:

// 配置線程池,Spring默認線程池沒有設置大小,如果出現阻塞,可能會出現OOM@Bean("eventThread")
 public TaskExecutor taskExecutor() {
 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
 // 設置核心線程數,轉換是個很耗時的過程,所以直接排隊執行
 executor.setCorePoolSize(1);
 // 設置最大線程數
 executor.setMaxPoolSize(1);
 // 設置隊列容量
 executor.setQueueCapacity(100);
 // 設置線程活躍時間(秒)
 executor.setKeepAliveSeconds(60);
 // 設置默認線程名稱
 executor.setThreadNamePrefix("eventThread-");
 // 設置拒絕策略
 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
 // 等待所有任務結束后再關閉線程池
 executor.setWaitForTasksToCompleteOnShutdown(true);
 return executor;
 }
/**
 * 內部消息總線
 */@Service@EnableAsyncpublic class EventBus implements ApplicationEventPublisherAware {
 private ApplicationEventPublisher publisher;
 @Override
 public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
 this.publisher = applicationEventPublisher;
 }
 public void add(ApplicationEvent event) {
 publisher.publishEvent(event);
 }
}
// 事件類public class UploadEvent extends ApplicationEvent {
 public UploadEvent(Object source) {
 super(source);
 }
}
public class ConvertEvent extends ApplicationEvent {
 public ConvertEvent(Object source) {
 super(source);
 }
}
// 監聽類@Componentpublic class UploadListener {
 @EventListener
 @Async("eventThread") // 使用自定義的線程池
 public void process(UploadEvent event) {
 }
}
@Componentpublic class ConvertListener {
 @EventListener
 @Async("eventThread")
 public void process(ConvertEvent event) {
 }
}

配置管理實現

為了提高文件服務器的靈活性,對于轉換邏輯可進行配置。如果沒有進行相應的配置,則不會進行對應的處理。

下面的四個類是對各個文件類型的配置:

  • ImageConfig:切圖大小
  • OfficeConfig:轉換類型,是否獲取頁碼
  • PdfConfig:轉換類型,是否獲取頁碼
  • VideoConfig:轉換類型,是否獲取長度,是否取幀

對應的Respository是對其保存與恢復的倉儲類:

  • ImageConfigRespository
  • OfficeConfigRespository
  • PdfConfigRespository
  • VideoConfigRespository

此處基于屬性配置來實現(原因請見「技術選型」)!以VideoConfigRespository為例:

@Configuration@ConfigurationProperties(prefix = "fileupload.config")
public class VideoConfigRespository {
 private List<VideoConfig> videoConfigList;
 /**
 * 根據分組(系統)找到對應的視頻配置
 *
 * @param group
 * @return
 */
 public List<VideoConfig> find(String group) {
 if (videoConfigList == null) {
 return new ArrayList<>();
 } else {
 return videoConfigList.stream().filter(it -> it.getGroup().equals(group)).collect(Collectors.toList());
 }
 }
 public List<VideoConfig> getVideoConfigList() {
 return videoConfigList;
 }
 public void setVideoConfigList(List<VideoConfig> videoConfigList) {
 this.videoConfigList = videoConfigList;
 }
}

通過Spring的ConfigurationProperties注解,將屬性文件中的屬性配置到videoConfigList中。

# 視頻配置
fileupload.config.videoConfigList[0].group=GROUP1 
# 默認配置
fileupload.config.videoConfigList[1].group=GROUP2
fileupload.config.videoConfigList[1].type=webm 
 # 轉換為webm
fileupload.config.videoConfigList[1].frameSecondList[0]=3 # 取第3秒的圖片

轉換結果實現

轉換結果通過ConvertResult和ConvertFileInfo表示:

  • ConvertResult中包含了源文件信息,以及多個轉換結果。ConvertFileInfo表示一個轉換結果
  • ConvertResult是Entity而ConvertFileInfo是VO
  • ConvertResult與ConvertFileInfo是一對多的關系
  • 兩者構成聚合,其中ConvertResult是聚合根(關于聚合與聚合根請參考領域設計:聚合與聚合根)

ConvertResultRespository是這個聚合的倉儲,用于保存與恢復此聚合。此處沒有使用數據庫,而是直接使用的文本形式保存(原因見「技術選型」)。

@Componentpublic class ConvertResultRespository {
 ......
 /**
 * 保存轉換結果
 *
 * @param result
 * @return
 */
 public void save(ConvertResult result) {
 Path savePath = Paths.get(tokenPath, result.getToken());
 try {
 if(!Files.exists(savePath.getParent())) {
 Files.createDirectories(savePath.getParent());
 }
 Files.write(savePath, gson.toJson(result).getBytes(UTF8_CHARSET));
 } catch (IOException e) {
 logger.error("save ConvertResult[{}} error!", result, e);
 }
 }
 /**
 * 查找轉換結果
 *
 * @param token
 * @return
 */
 public ConvertResult find(String token) {
 Path findPath = Paths.get(tokenPath, token);
 try {
 if (Files.exists(findPath)) {
 String result = new String(Files.readAllBytes(findPath), UTF8_CHARSET);
 return gson.fromJson(result, ConvertResult.class);
 }
 } catch (IOException e) {
 logger.error("find ConvertResult by token[{}} error!", token, e);
 }
 return null;
 }
}

轉換服務實現

轉換服務根據配置委托對應的工具類來進行相應的操作(代碼略):

  • 使用ffmpeg轉換視頻
  • 使用pdfbox轉換pdf
  • 使用libreoffice轉換office

安全實現

  • 安全通過Spring攔截器實現
  • 按需求增加對應攔截即可

使用

提供兩個接口:

/**
* 獲取轉換后的信息
*/@ResponseBody@GetMapping(value = "/realUrl/{token}")
public ResponseEntity realUrl(@PathVariable String token) {
 .....
}
/**
* 上傳文件
*/@ResponseBody@PostMapping(value = {"/partupload/{group}"})
public ResponseEntity upload(HttpServletRequest request, @PathVariable String group) {
 .....
}
  • 通過upload接口上傳文件,支持分片上傳
  • 上傳完成后,會返回上傳結果,結構如下:
{
 "code": 1,
 "message": "maps.mp4",
 "token": "key_286400710002612",
 "group": "GROUP1",
 "fileType": "VIDEO",
 "filePath": "http://www.abc.com/1556172522968_maps.mp4"
}
  • 其中的filePath是原始文件路徑
  • 通過token,使用realUrl接口可以獲取轉換后的文件信息,結構如下:
{
 "token": "key_282816586380196",
 "group": "SHILU",
 "fileType": "IMAGE",
 "filePath": "http://www.abc.com/SHILU/1/1556164891252_0.jpeg",
 "convertFileInfoList": [
 {
 "fileLength": 0,
 "fileType": "IMAGE",
 "filePath": null,
 "imgPaths": [
 "http://www.abc.com/SHILU/1/1556164891252_0_100_200.jpeg"
 ]
 }
 ]
}

配置

## 對外提供服務的域名
fileupload.server.name=http://www.abc.com## libreoffice home路徑
office.home=/snap/libreoffice/115/lib/libreoffice
# 文件上傳保存路徑
fileupload.upload.root=/home/files
# 文件服務器動態配置# 圖片配置,切100*200的圖fileupload.config.imageConfigList[0].group=group1
fileupload.config.imageConfigList[0].width=100
fileupload.config.imageConfigList[0].height=200
# 視頻配置
# 默認配置,轉換m3u8
fileupload.config.videoConfigList[0].group=group1
# 轉換webm,切第3秒的圖
fileupload.config.videoConfigList[1].group=group2
fileupload.config.videoConfigList[1].type=webm
fileupload.config.videoConfigList[1].frameSecondList[0]=3
# office配置,默認轉png
fileupload.config.officeConfigList[0].group=group1
# 轉PDF
fileupload.config.officeConfigList[0].type=PDF
# pdf配置,轉png
fileupload.config.pdfConfigList[0].group=group1
# 上傳文件大小,當前端不支持分片上傳時設置
spring.servlet.multipart.max-file-size=1024MB
spring.servlet.multipart.max-request-size=1024MB

總結

本文給出了一個文件服務相對完整的架構設計與實現過程。整個架構設計流程如下:

  • 梳理業務功能
  • 梳理用例流程
  • 基于業務功能,進行初步的模塊劃分
  • 結合用例流程進行架構設計,期間可能反過來對模塊及流程進行調整
  • 對架構進行驗證
  • 業務流程驗證:將用例套用到架構中進行驗證
  • 非功能性約束驗證:模擬非功能性約束場景進行驗證
  • 技術選型(架構設計是與技術無關的)
  • 遵循架構設計實現代碼,測試(可能調整架構)
  • 完整流程驗證,使用說明

整個過程對各個約束做出了對應的決策,并進行了驗證。代碼結構與架構設計完全匹配。從架構設計圖依圖索驥即可理解代碼邏輯。

如有不妥或紕漏之處,歡迎大家探討指教!

分享到:
標簽:架構
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定