在面向架構編程一文中,我闡述了本身對架構和代碼之間的關係的見解:「代碼須要反映出架構」!前端
本文經過對文件服務核心功能的設計與實現,來驗證這一觀點。設計過程融合了「用例驅動設計」和「領域驅動設計」!web
本文及後續幾篇文章會設計並開發幾個實際的系統,同時嘗試總結一套適用的架構設計與開發流程。歡迎探討!redis
文件服務器的核心功能就兩個:「文件上傳」和「文件下載」!其中上傳可能須要支持斷點續傳、分片上傳。而下載可能須要進行下載保護,例如非指定客戶端沒法下載。spring
除了這兩個核心功能,通常都會有一個額外功能,就是「轉換」!轉換包括:數據庫
除了上面的業務功能外,還包括以下非功能性約束:編程
根據功能,可劃分以下功能模塊:安全
首先經過分層架構對模塊進行一個大體的劃分,按照領域設計的分層方式:服務器
從上面的流程能夠看到「上傳模塊」對「轉換模塊」有必定的依賴,像下面這樣:markdown
可是,「上傳模塊」是核心模塊,而「轉換模塊」是非核心模塊。核心模塊的功能相對穩定,非核心模塊的功能相對不穩定。讓穩定的模塊去依賴不穩定的模塊,會致使穩定的模塊也不穩定,因此須要對依賴進行「倒置」。架構
「依賴倒置」解決了模塊依賴問題。可是轉換是個很耗時的過程,例如用戶上傳視頻,在不轉換的狀況下,只要上傳完成就能夠獲得響應,可是若是轉換的話,可能就須要雙倍甚至三四倍的時間才能獲得反饋,體驗很是的很差。且通常上傳和觀看的時效性並不須要即時性,因此轉換應該是個異步的過程。
異步執行的方式不少,好比基於事件,自定義線程等。這裏經過事件的方式來進行處理。(領域事件可參考領域設計:領域事件)
文件上傳會建立UploadEvent,UploadListener監聽UploadEvent事件,當監聽到了UploadEvent,則執行轉換。
轉換流程異步化後,如何告知客戶端轉換結果呢?有幾種方案:
目前主流作法是第一種,不過爲保證文件服務器的適用性,須要能支持多種方案。故對轉換後的通知也基於事件進行處理,轉換後建立對應事件,關注該事件的對象來作出對應的處理。一個可能處理流程以下:
另外對於下載來講,實際直接經過Nginx這樣的web服務器就能夠了,因此下載模塊能夠直接獨立。
對於配置模塊來講,配置能夠分爲兩種:
「靜態配置」可使用屬性文件進行配置便可。「動態配置」須要根據不一樣的系統進行相應的配置,故針對圖片和視頻等資源配置,建立對應的配置類,根據參數經過Respository動態構建。
總體結構以下:
基於上面的設計,流程須要進行相應的調整。
下載流程不變,多了一個獲取轉換後文件連接的流程:
相應的模塊也有調整,新增了一個消息模塊,用於處理消息的發送與監聽。這個消息屬於領域事件,因此也放在領域層。
上傳流程:
下載流程:
獲取真實連接流程:
例如,如今要新增一個「秒傳功能」,即對於服務器已經存在的文件,再也不進行上傳操做,直接返回文件URL!那麼須要作以下擴展:
上面的修改不須要對現有流程作任何改動。
結構與架構圖一致
事件串聯了整個上傳流程:
因爲目前大部分是內部事件,故使用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) {
}
}
複製代碼
爲了提升文件服務器的靈活性,對於轉換邏輯可進行配置。若是沒有進行相應的配置,則不會進行對應的處理。
下面的四個類是對各個文件類型的配置:
對應的Respository是對其保存與恢復的倉儲類:
此處基於屬性配置來實現(緣由請見「技術選型」)!以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表示:
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;
}
}
複製代碼
轉換服務根據配置委託對應的工具類來進行相應的操做(代碼略):
提供兩個接口:
/**
* 獲取轉換後的信息
*/@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": "www.abc.com/15561725229…" }
其中的filePath是原始文件路徑
經過token,使用realUrl接口能夠獲取轉換後的文件信息,結構以下:
{ "token": "key_282816586380196", "group": "SHILU", "fileType": "IMAGE", "filePath": "www.abc.com/SHILU/1/155…", "convertFileInfoList": [ { "fileLength": 0, "fileType": "IMAGE", "filePath": null, "imgPaths": [ "www.abc.com/SHILU/1/155…" ] } ] }
## 對外提供服務的域名
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
複製代碼
本文給出了一個文件服務相對完整的架構設計與實現過程。整個架構設計流程以下:
整個過程對各個約束作出了對應的決策,並進行了驗證。代碼結構與架構設計徹底匹配。從架構設計圖依圖索驥便可理解代碼邏輯。
若有不妥或紕漏之處,歡迎你們探討指教!