使用SpringEvent解決WebUploader大文件上傳解耦問題
前言
關(guān)于Spring的Event機制,相信使用Java開發(fā)的朋友們一定非常熟悉。Spring Event是Spring框架內(nèi)建的一種發(fā)布/訂閱(Publish-Subscribe)模式的實現(xiàn),它允許應(yīng)用內(nèi)部不同組件之間通過事件進行通信。當(dāng)某個特定事件發(fā)生時,系統(tǒng)中對這類事件感興趣的監(jiān)聽器可以接收到通知并執(zhí)行相應(yīng)操作。是不是看起來跟消息隊列差不多,尤其是這種發(fā)布/訂閱的模式,確實非常符合消息中間件的模式。通常來說,消息隊列一般有以下幾種作用。異步、解耦和削峰。不過請大家注意,之所以在這里講解SpringEvent,在一般的中小型項目中,我們的部署節(jié)點是單個,技術(shù)的架構(gòu)選型一般也是單體架構(gòu)。因此我們可以在不引入復(fù)雜架構(gòu)的前提下來實現(xiàn)一個簡單版本的消息隊列。通過發(fā)布訂閱的模式來進行應(yīng)用程序解耦,讓各個功能組件更加符合實際架構(gòu)的布置。從而讓程序擴展起來更方便。以上是關(guān)于SpringEvent這個框架的知識,更多的關(guān)于SpringEvent的相關(guān)知識,大家感興趣的可以登錄spring的官方網(wǎng)站來進行查詢。
除了Spring的Event機制之外,在我們?nèi)粘5捻椖块_發(fā)過程中,肯定會遇到大文件上傳的處理場景,而大文件的處理通常是需要蠻長的時間。同時,不同的場景甚至不同的業(yè)務(wù),對于上傳附件的處理是不盡相同的。比如在用戶信息Excel的上傳處理中,不僅需要將當(dāng)前的Excel數(shù)據(jù)進行關(guān)聯(lián)綁定,同時還需要解析數(shù)據(jù)后,將Excel提交的數(shù)據(jù)結(jié)果存儲到數(shù)據(jù)庫中。而另外一個類型,比如單據(jù)生成的任務(wù)表格,就需要根據(jù)單據(jù)信息,匹配不同的模板來生成不同的任務(wù)。由此種種需求,要求我們的應(yīng)用程序在開發(fā)過程中具有較大的擴展性,可以支持將不同的應(yīng)用程序快速的切入到應(yīng)用中,同時能針對不同的場景靈活開發(fā)。極端的情況下,甚至需要同一種業(yè)務(wù),根據(jù)不同的狀態(tài)來定制附件的讀取需求。
本文以WebUploader大文件上傳組件為例,在大文件處理的場景中使用SpringEvent的事件發(fā)布機制,靈活的擴展對文件的處理需求。本文通過代碼實例的講解,讓您快速的了解如何在Spring中快速開發(fā)Event應(yīng)用程序,同時使用枚舉來實現(xiàn)動態(tài)的注冊過程,實現(xiàn)方便靈活的注冊機制。最后結(jié)合一個具體的場景詳細(xì)說明在學(xué)生信息附件上傳中來進行附件處理的過程,方便您掌握上述的知識點。
一、SpringEvent涉及的相關(guān)組件
為了讓不熟悉SpringEvent的朋友對Event也有一個大致的印象。這里還是對SpringEvent對象包含的方法和相關(guān)組件的應(yīng)用進行簡單的介紹。
1、 事件(Event)
事件(Event): 事件是應(yīng)用程序中發(fā)生的某種事情,可以是用戶行為、系統(tǒng)狀態(tài)改變等。在Spring中,事件通常表示為一個Java類,它包含了與事件相關(guān)的信息。如果大家做過GUI界面的實際與實現(xiàn),或者進行過Web界面的開發(fā),相信對事件機制一定非常熟悉。比如鼠標(biāo)點擊事件、鼠標(biāo)雙擊事件、鼠標(biāo)拖拽事件、鼠標(biāo)懸浮事件等等。事件一定是經(jīng)過觸發(fā)的,由某一種設(shè)備或者事務(wù)來進行觸發(fā),從而形成某種事件。在本文的場景中,文件上傳后在服務(wù)器端進行合成是一種事件。
2、事件監(jiān)聽器
事件監(jiān)聽器(Event Listener): 事件監(jiān)聽器是一段代碼,它等待并響應(yīng)事件的發(fā)生。在Spring中,事件監(jiān)聽器通常實現(xiàn)了ApplicationListener接口,該接口定義了監(jiān)聽事件的方法。如果對監(jiān)聽器模式有所了解朋友一定了解,監(jiān)聽器類的設(shè)計非常友好,會根據(jù)設(shè)計進行監(jiān)聽,而當(dāng)有相應(yīng)的變化進行發(fā)生時,監(jiān)聽器則會根據(jù)發(fā)生的情況同時相應(yīng)的類或者接口,從而實現(xiàn)消息的動態(tài)傳遞。
3、事件發(fā)布器
事件發(fā)布器(Event Publisher): 事件發(fā)布器負(fù)責(zé)發(fā)布事件,通知所有監(jiān)聽該事件的監(jiān)聽器。在Spring中,ApplicationEventPublisher接口表示事件發(fā)布器,可以通過Spring容器自動注入或手動獲取。通常在Spring工作環(huán)境中,我們會使用applicationContext來進行事件的發(fā)布。上面三者就是SpringEvent的核心組件。事件發(fā)布器(publisher)會在事件(event)發(fā)生時進行事件的發(fā)布,事件發(fā)布后,有監(jiān)聽者進行事件監(jiān)聽,當(dāng)監(jiān)聽到自己感興趣的主體事件,則進行相應(yīng)的事件處理。由此形成時間的發(fā)布、監(jiān)聽和處理的閉環(huán)操作。
在介紹上述的重要組件之后,我們通過大文件處理的實例來具體介紹SpringEvent的詳細(xì)應(yīng)用。
二、WebUploader大文件處理的相關(guān)事件分析
本節(jié)重點介紹WebUploader大文件處理組件中的后臺相關(guān)事件處理。通過本節(jié)將了解何時進行相應(yīng)事件的注冊,具體的事件發(fā)布方法是什么?
1、事件發(fā)布的時機
事件的發(fā)布時機是非常重要的,關(guān)于Webuploader則不再進行具體介紹。但是需要注意的是,如果在應(yīng)用程序中采用了WebUploader這種后臺處理機制,我們需要在后臺實現(xiàn)數(shù)據(jù)的分片上傳處理、分片的合并的操作。同時為了能兼容大文件和小文件的處理。以Webuploader為例,針對大文件,我們以5MB作為一個分片的切分邏輯,這種情況下可能有兩種情況需要處理。第一種是單個文件的大小小于5MB,根據(jù)分片的策略,小于5MB的文件將不會進行分片而直接上傳到后臺。這時候也同樣不會觸發(fā)分片的合并邏輯。第二種情況是文件的大小超過5MB,比如有一個256MB的文件,就會進行分片上傳。在服務(wù)端我們實現(xiàn)自定義的分片上傳之后,還需要進行文件的合并。因此,我們在選擇事件的發(fā)布時機時,就有兩個點需要考慮的。需要分片的和不需要分片的文件處理時機。這兩種都需要考慮,才能不漏掉相應(yīng)的文件處理。
2、事件發(fā)布的代碼
在掌握了事件的發(fā)布時機后,我們就知道了在處理文件上傳時的程序中如何切入事件的發(fā)布。事件的發(fā)布入口有兩個地方,第一個無需分片的事件處理入口。第二個是在分片合并完成的事件入口。
在進行事件發(fā)布前,我們需要在程序中創(chuàng)建一個Event的實例對象,用來進行事件信息的綁定和設(shè)置。這里我們?nèi)∶晃募蟼魇录?,關(guān)鍵代碼如下所示:
package com.yelang.framework.event; import org.springframework.context.ApplicationEvent; import com.yelang.project.webupload.domain.FileEntity; import lombok.Getter; import lombok.Setter; import lombok.ToString; @Setter @Getter @ToString public class FileUploadEvent extends ApplicationEvent { private static final long serialVersionUID = 7396389156436678379L; private FileEntity fileEntity;//上傳文件對象 /** * 重寫構(gòu)造函數(shù) * @param source 事件源對象 * @param fileEntity 已上傳的文件對象 */ public FileUploadEvent(Object source,FileEntity fileEntity) { super(source); this.fileEntity = fileEntity; } }
為了方便大家可以獲取上傳的文件信息實體,我們將文件實體類在事件發(fā)布時一同綁定到事件上下文中。fileEntity其實就是一個文件上傳的接收實體,關(guān)鍵代碼如下:
package com.yelang.project.webupload.domain; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.yelang.framework.web.domain.BaseEntity; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @TableName("biz_file") @NoArgsConstructor @AllArgsConstructor @Setter @Getter @ToString public class FileEntity extends BaseEntity { private static final long serialVersionUID = 1L; private Long id; @TableField(value = "f_id") private String fid; @TableField(value = "b_id") private String bid; @TableField(value = "f_type") private String type; @TableField(value = "f_name") private String name; @TableField(value = "f_desc") private String desc; @TableField(value = "f_state") private Integer state; @TableField(value = "f_size") private Long size; @TableField(value = "f_path") private String path; @TableField(value = "table_name") private String tablename = "temp_table"; private String md5code; private String directory; @TableField(value = "biz_type") private String bizType; @TableField(exist = false) private boolean previewSign; @TableField(exist = false) private String previewType; }
我們在小文件(小于5MB)上傳成功之后以及大文件合并完成之后就可以發(fā)布文件上傳事件,在下面的代碼中,我們通過applicationContext上下文對象發(fā)布了一個FilaUploadEvent的事件。大致的代碼如下所示:
@SuppressWarnings("resource") private AjaxResult mergeChunks(FileEntity db_file,String chunk_dir,String chunks,String f_path) throws IOException { if (db_file == null) { return AjaxResult.error(AjaxResult.Type.WEBUPLOADERROR.value(), "找不到數(shù)據(jù)"); } if (db_file.getState() == 1) { //未分片文件上傳成功合并成功后發(fā)布相應(yīng)事件,各監(jiān)聽器自由監(jiān)聽并執(zhí)行 applicationContext.publishEvent(new FileUploadEvent(this, db_file)); return AjaxResult.success(); } if(db_file.getSize() > block_size){ //xxx 其它業(yè)務(wù)邏輯 db_file.setState(1); fileService.updateById(db_file); File tempFile = new File(chunk_dir); if (tempFile.isDirectory() && tempFile.exists()) { tempFile.delete(); } //分片文件上傳成功合并成功后發(fā)布相應(yīng)事件,各監(jiān)聽器自由監(jiān)聽并執(zhí)行 applicationContext.publishEvent(new FileUploadEvent(this, db_file)); } return AjaxResult.success(); }
三、事件監(jiān)聽器及實際的業(yè)務(wù)處理
在上面小節(jié)中,我們介紹如何發(fā)布Spring的Event,同時以一個大文件的上傳為例,具體的介紹了如何進行文件上傳事件的發(fā)布。本節(jié)接著在上面的例子中,重點講解在事件發(fā)布后,如何進行事件的監(jiān)聽以及具體的業(yè)務(wù)回調(diào)處理機制。通過本節(jié)可以掌握在實際業(yè)務(wù)中進行靈活的業(yè)務(wù)擴展和定制。
1、文件上傳處理枚舉
在講解事件監(jiān)聽器之前,首先我們對監(jiān)聽器中的具體回調(diào)業(yè)務(wù)類進行注冊。在實際業(yè)務(wù)中,我們可以選擇將具體回調(diào)業(yè)務(wù)類進行持久化處理,比如使用關(guān)系型數(shù)據(jù)庫 進行處理,將具體的業(yè)務(wù)類、物理表、業(yè)務(wù)屬性、回調(diào)業(yè)務(wù)實現(xiàn)類統(tǒng)一保存的數(shù)據(jù)庫中。這樣在執(zhí)行的時候統(tǒng)一通過數(shù)據(jù)去獲取即可。這種模式也是可以的,實現(xiàn)起來也比較簡單。如何在不引入數(shù)據(jù)庫的前提下實現(xiàn)呢?其實我們可以利用枚舉類來輕松實現(xiàn)這類需求。下面分享一下這種設(shè)計,文件上傳處理枚舉類的業(yè)務(wù)邏輯如下所示:
package com.yelang.framework.aspectj.lang.enums; /** * 文件上傳監(jiān)聽服務(wù)注冊枚舉類 * @author 夜郎king */ public enum FileUploadServiceRegisterEnum { UNKOWN(-1,"UNKOWN","","","未知"), PROJZSPRCSINFSERVIMPL(0,"biz_student","studentUploadCallbackServiceImpl","123a","項目程序管理文件上傳回調(diào)處理枚舉"); private int index;//下標(biāo),編號作用 private String tableName;//業(yè)務(wù)表名稱,根據(jù)表名檢索具體執(zhí)行的servcie private String execService;//業(yè)務(wù)實際執(zhí)行service private String bizType;//業(yè)務(wù)類型 private String desc;//描述說明 public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public String getTableName() { return tableName; } public void setTableName(String tableName) { this.tableName = tableName; } public String getExecService() { return execService; } public void setExecService(String execService) { this.execService = execService; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } public String getBizType() { return bizType; } public void setBizType(String bizType) { this.bizType = bizType; } private FileUploadServiceRegisterEnum(int index, String tableName, String execService, String bizType, String desc) { this.index = index; this.tableName = tableName; this.execService = execService; this.bizType = bizType; this.desc = desc; } public static FileUploadServiceRegisterEnum getEnumByTableName(String tableName){ FileUploadServiceRegisterEnum result = null; for (FileUploadServiceRegisterEnum enumObj : FileUploadServiceRegisterEnum.values()) { if(enumObj.getTableName().equals(tableName)){ result = enumObj; break; } } return result; } public static FileUploadServiceRegisterEnum getEnumByTableNameAndBizType(String tableName,String bizType){ FileUploadServiceRegisterEnum result = null; for (FileUploadServiceRegisterEnum enumObj : FileUploadServiceRegisterEnum.values()) { if(enumObj.getTableName().equals(tableName) && enumObj.getBizType().equals(bizType)){ result = enumObj; break; } } return result; } }
在進行業(yè)務(wù)注冊時,我們會定義具體的枚舉實例,如下:PROJZSPRCSINFSERVIMPL(0,"biz_student","studentUploadCallbackServiceImpl","123a","項目程序管理文件上傳回調(diào)處理枚舉");0是下標(biāo)索引號,biz_student是業(yè)務(wù)表,studentUploadCallbackServiceImpl是回調(diào)的具體業(yè)務(wù)實現(xiàn)類,123a是業(yè)務(wù)類型描述,根據(jù)需要可以用來區(qū)分同一個表的不同業(yè)務(wù)實現(xiàn)。最后一個是業(yè)務(wù)的描述。
2、文件上傳監(jiān)聽器的實現(xiàn)
在定義上述的枚舉類之后,我們來進行文件上傳監(jiān)聽器的實現(xiàn),核心代碼如下:
package com.yelang.framework.event.listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import com.yelang.common.utils.StringUtils; import com.yelang.common.utils.spring.SpringUtils; import com.yelang.framework.aspectj.lang.enums.FileUploadServiceRegisterEnum; import com.yelang.framework.event.FileUploadEvent; import com.yelang.project.common.service.IFileUploadCallbackService; import com.yelang.project.webupload.domain.FileEntity; /** * 公共事件監(jiān)聽器組件,具體實現(xiàn)使用策略模式實現(xiàn),統(tǒng)一由本類處理后進行相應(yīng)轉(zhuǎn)發(fā), * 多種event監(jiān)聽均在本類中實現(xiàn)注冊監(jiān)聽,使用event模式便于程序解耦,程序處理邏輯更加清晰 * @author 夜郎king */ @Component public class YelangSpringListener { private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); @EventListener public void fileUploadEventRegister(FileUploadEvent event){ try { sys_user_logger.info("當(dāng)前處理線程名稱:" + Thread.currentThread().getName()); FileEntity fileEntity = event.getFileEntity(); if(StringUtils.isNotEmpty(fileEntity.getTablename())){ FileUploadServiceRegisterEnum rigisterEnum = null; if(StringUtils.isNotBlank(fileEntity.getBizType())) {//業(yè)務(wù)類型不為空,則根據(jù)表名和業(yè)務(wù)名稱來查找執(zhí)行service rigisterEnum = FileUploadServiceRegisterEnum.getEnumByTableNameAndBizType(fileEntity.getTablename(), fileEntity.getBizType()); }else { rigisterEnum = FileUploadServiceRegisterEnum.getEnumByTableName(fileEntity.getTablename()); } if(null != rigisterEnum && StringUtils.isNotEmpty(rigisterEnum.getExecService())){ String execService = rigisterEnum.getExecService(); IFileUploadCallbackService service = SpringUtils.getBean(execService); service.process(fileEntity); }else{ sys_user_logger.info("未注冊文件上傳監(jiān)聽回調(diào)處理器."); } } } catch (Exception e) { sys_user_logger.error("文件上傳事件監(jiān)聽發(fā)生錯誤.",e); } } }
上面的邏輯中,重點就是找到回調(diào)的具體枚舉實例,然后使用Spring的IOC機制,找到注冊到Spring上下文中的IFileUploadCallbackService類,
if(null != rigisterEnum && StringUtils.isNotEmpty(rigisterEnum.getExecService())){ String execService = rigisterEnum.getExecService(); IFileUploadCallbackService service = SpringUtils.getBean(execService); service.process(fileEntity); }
然后調(diào)用process方法開始進行文件的處理。
3、文件具體處理邏輯
為了讓不同的業(yè)務(wù)實現(xiàn)不同的業(yè)務(wù)處理需要,我們將文件處理方法封裝成統(tǒng)一的一個接口,然后通過不同的實例類來進行實現(xiàn)。接口的定義如下:
package com.yelang.project.common.service; import com.yelang.project.webupload.domain.FileEntity; public interface IFileUploadCallbackService { /** * 文件上傳事件監(jiān)聽器回調(diào)服務(wù)接口,封裝公共服務(wù),可以讀取相關(guān)表格或修改業(yè)務(wù)表,具體實現(xiàn)由各實現(xiàn)類來完成 * @param fileEntity 文件實體 * @throws Exception */ void process(FileEntity fileEntity) throws Exception; }
然后定義統(tǒng)一的文件處理實現(xiàn)類,實現(xiàn)上述的接口,并實現(xiàn)具體的文件處理方法。
package com.yelang.project.common.service.impl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.yelang.common.utils.StringUtils; import com.yelang.project.common.service.IFileUploadCallbackService; import com.yelang.project.extend.student.domain.Student; import com.yelang.project.extend.student.service.IStudentService; import com.yelang.project.webupload.domain.FileEntity; @Service("studentUploadCallbackServiceImpl") public class StudentUploadCallbackServiceImpl implements IFileUploadCallbackService{ private static final Logger logger = LoggerFactory.getLogger("sys-user"); @Autowired private IStudentService studentService; @Override @Transactional(propagation=Propagation.REQUIRED,rollbackFor=Exception.class) public void process(FileEntity fileEntity) throws Exception { if(null != fileEntity && StringUtils.isNotEmpty(fileEntity.getBid())){ String pkId = fileEntity.getBid(); Student stu = studentService.selectStudentById(Long.valueOf(pkId)); //System.out.println(fileEntity.getPath()); //System.out.println(stu.getName() + "\t" + stu.getAddress()); logger.info("開始處理........"); Thread.sleep(35 * 1000);//休眠35秒測試 logger.info("執(zhí)行結(jié)束"); } } }
上面的程序邏輯比較簡單,我們僅演示了如何從事件發(fā)布器中獲取FileEntity實體的信息,同時打印相應(yīng)的信息。在實際業(yè)務(wù)中,可以實現(xiàn)更復(fù)雜的業(yè)務(wù)。
4、實際處理實例
下面我們結(jié)合實際場景來看一下具體的實現(xiàn)及調(diào)用過程。
我們來看一下后臺的處理信息的輸出,
可以很明顯的看到,在后臺的控制臺已經(jīng)成功的輸出相應(yīng)的內(nèi)容,表明事件的發(fā)布、監(jiān)聽、處理按照預(yù)定的設(shè)計運行。
23:14:53.169 [http-nio-8080-exec-37] INFO sys-user - [fileUploadEventRegister,32] - 當(dāng)前處理線程名稱:http-nio-8080-exec-37 23:14:53.198 [http-nio-8080-exec-37] DEBUG c.y.p.e.s.m.S.selectById - [debug,137] - <== Total: 1 23:14:53.199 [http-nio-8080-exec-37] INFO sys-user - [process,32] - 開始處理........ 23:15:08.200 [http-nio-8080-exec-37] INFO sys-user - [process,34] - 執(zhí)行結(jié)束
四、總結(jié)
以上就是本文的主要內(nèi)容,本文以WebUploader大文件上傳組件為例,在大文件處理的場景中使用SpringEvent的事件發(fā)布機制,靈活的擴展對文件的處理需求。本文通過代碼實例的講解,讓您快速的了解如何在Spring中快速開發(fā)Event應(yīng)用程序,同時使用枚舉來實現(xiàn)動態(tài)的注冊過程,實現(xiàn)方便靈活的注冊機制。行文倉促,定有不足之處,真誠期待各位專家朋友在評論區(qū)批評指正,不甚感激。
以上就是使用SpringEvent解決WebUploader大文件上傳解耦問題的詳細(xì)內(nèi)容,更多關(guān)于SpringEvent解決WebUploader解耦的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot如何動態(tài)改變?nèi)罩炯墑e
這篇文章主要介紹了SpringBoot如何動態(tài)改變?nèi)罩炯墑e,幫助大家更好的理解和使用springboot框架,感興趣的朋友可以了解下2020-12-12spring boot 加載web容器tomcat流程源碼分析
本文章主要描述spring boot加載web容器 tomcat的部分,為了避免文章知識點過于分散,其他相關(guān)的如bean的加載,tomcat內(nèi)部流程等不做深入討論,具體內(nèi)容詳情跟隨小編一起看看吧2021-06-06使用RestTemplate調(diào)用https接口跳過證書驗證
這篇文章主要介紹了使用RestTemplate調(diào)用https接口跳過證書驗證,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10springmvc實現(xiàn)導(dǎo)出數(shù)據(jù)信息為excle表格示例代碼
本篇文章主要介紹了springmvc實現(xiàn)導(dǎo)出數(shù)據(jù)信息為excle表格,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧。2017-01-01Spring File Storage文件的對象存儲框架基本使用小結(jié)
在開發(fā)過程當(dāng)中,會使用到存文檔、圖片、視頻、音頻等等,這些都會涉及存儲的問題,文件可以直接存服務(wù)器,但需要考慮帶寬和存儲空間,另外一種方式就是使用云存儲,這篇文章主要介紹了Spring File Storage文件的對象存儲框架基本使用小結(jié),需要的朋友可以參考下2024-08-08