一、SpringEvent涉及的相关组件
为了让不熟悉SpringEvent的朋友对Event也有一个大致的印象。这里还是对SpringEvent对象包含的方法和相关组件的应用进行简单的介绍。
1、 事件(Event)
事件是应用程序中发生的某种事情,可以是用户行为、系统状态改变等。在Spring中,事件通常表示为一个Java类,它包含了与事件相关的信息。如果大家做过GUI界面的实际与实现,或者进行过Web界面的开发,相信对事件机制一定非常熟悉。比如鼠标点击事件、鼠标双击事件、鼠标拖拽事件、鼠标悬浮事件等等。事件一定是经过触发的,由某一种设备或者事务来进行触发,从而形成某种事件。在本文的场景中,文件上传后在服务器端进行合成是一种事件。
2、事件监听器
事件监听器是一段代码,它等待并响应事件的发生。在Spring中,事件监听器通常实现了ApplicationListener
接口,该接口定义了监听事件的方法。如果对监听器模式有所了解朋友一定了解,监听器类的设计非常友好,会根据设计进行监听,而当有相应的变化进行发生时,监听器则会根据发生的情况同时相应的类或者接口,从而实现消息的动态传递。
3、事件发布器
事件发布器负责发布事件,通知所有监听该事件的监听器。在Spring中,ApplicationEventPublisher
接口表示事件发布器,可以通过Spring容器自动注入或手动获取。通常在Spring工作环境中,我们会使用applicationContext
来进行事件的发布。
上面三者就是SpringEvent的核心组件。事件发布器(publisher)会在事件(event)发生时进行事件的发布,事件发布后,有监听者进行事件监听,当监听到自己感兴趣的主体事件,则进行相应的事件处理。由此形成时间的发布、监听和处理的闭环操作。
在介绍上述的重要组件之后,我们通过大文件处理的实例来具体介绍SpringEvent的详细应用。
二、WebUploader大文件处理的相关事件分析
本节重点介绍WebUploader大文件处理组件中的后台相关事件处理。通过本节将了解何时进行相应事件的注册,具体的事件发布方法是什么?
1、事件发布的时机
事件的发布时机是非常重要的,关于Webuploader则不再进行具体介绍。但是需要注意的是,如果在应用程序中采用了WebUploader这种后台处理机制,我们需要在后台实现数据的分片上传处理、分片的合并的操作。同时为了能兼容大文件和小文件的处理。
以Webuploader为例,针对大文件,我们以5MB作为一个分片的切分逻辑,这种情况下可能有两种情况需要处理。
第一种是单个文件的大小小于5MB,根据分片的策略,小于5MB的文件将不会进行分片而直接上传到后台。这时候也同样不会触发分片的合并逻辑。
第二种情况是文件的大小超过5MB,比如有一个256MB的文件,就会进行分片上传。在服务端我们实现自定义的分片上传之后,还需要进行文件的合并。
因此,我们在选择事件的发布时机时,就有两个点需要考虑的。需要分片的和不需要分片的文件处理时机。这两种都需要考虑,才能不漏掉相应的文件处理。
2、事件发布的代码
在掌握了事件的发布时机后,我们就知道了在处理文件上传时的程序中如何切入事件的发布。事件的发布入口有两个地方,第一个无需分片的事件处理入口。第二个是在分片合并完成的事件入口。
在进行事件发布前,我们需要在程序中创建一个Event的实例对象,用来进行事件信息的绑定和设置。这里我们取名位文件上传事件,关键代码如下所示:
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@ToStringpublic class FileUploadEvent extends ApplicationEvent { private static final long serialVersionUID = 7396389156436678379L; private FileEntity fileEntity;//上传文件对象 /** * 重写构造函数 * @param source 事件源对象 * @param fileEntity 已上传的文件对象 */ public FileUploadEvent(Object source,FileEntity fileEntity) { super(source); this.fileEntity = fileEntity; }}
为了方便大家可以获取上传的文件信息实体,我们将文件实体类在事件发布时一同绑定到事件上下文中。fileEntity其实就是一个文件上传的接收实体,关键代码如下:
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@ToStringpublic 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)上传成功之后以及大文件合并完成之后就可以发布文件上传事件,在下面的代码中,我们通过applicationContext
上下文对象发布了一个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(), "找不到数据"); } if (db_file.getState() == 1) { //未分片文件上传成功合并成功后发布相应事件,各监听器自由监听并执行 applicationContext.publishEvent(new FileUploadEvent(this, db_file)); return AjaxResult.success(); } if(db_file.getSize() > block_size){ //xxx 其它业务逻辑 db_file.setState(1); fileService.updateById(db_file); File tempFile = new File(chunk_dir); if (tempFile.isDirectory() && tempFile.exists()) { tempFile.delete(); } //分片文件上传成功合并成功后发布相应事件,各监听器自由监听并执行 applicationContext.publishEvent(new FileUploadEvent(this, db_file)); } return AjaxResult.success();}
三、事件监听器及实际的业务处理
在上面小节中,我们介绍如何发布Spring的Event,同时以一个大文件的上传为例,具体的介绍了如何进行文件上传事件的发布。本节接着在上面的例子中,重点讲解在事件发布后,如何进行事件的监听以及具体的业务回调处理机制。通过本节可以掌握在实际业务中进行灵活的业务扩展和定制。
1、文件上传处理枚举
在讲解事件监听器之前,首先我们对监听器中的具体回调业务类进行注册。在实际业务中,我们可以选择将具体回调业务类进行持久化处理,比如使用关系型数据库 进行处理,将具体的业务类、物理表、业务属性、回调业务实现类统一保存的数据库中。这样在执行的时候统一通过数据去获取即可。这种模式也是可以的,实现起来也比较简单。
如何在不引入数据库的前提下实现呢?其实我们可以利用枚举类来轻松实现这类需求。下面分享一下这种设计,文件上传处理枚举类的业务逻辑如下所示:
package com.yelang.framework.aspectj.lang.enums;/** * 文件上传监听服务注册枚举类 * @author 夜郎king */public enum FileUploadServiceRegisterEnum { UNKOWN(-1,"UNKOWN","","","未知"), PROJZSPRCSINFSERVIMPL(0,"biz_student","studentUploadCallbackServiceImpl","123a","项目程序管理文件上传回调处理枚举"); private int index;//下标,编号作用 private String tableName;//业务表名称,根据表名检索具体执行的servcie private String execService;//业务实际执行service private String bizType;//业务类型 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; }}
在进行业务注册时,我们会定义具体的枚举实例,如下:PROJZSPRCSINFSERVIMPL(0,"biz_student","studentUploadCallbackServiceImpl","123a","项目程序管理文件上传回调处理枚举");
0
是下标索引号,biz_student
是业务表,studentUploadCallbackServiceImpl
是回调的具体业务实现类,123a
是业务类型描述,根据需要可以用来区分同一个表的不同业务实现。最后一个是业务的描述。
2、文件上传监听器的实现
在定义上述的枚举类之后,我们来进行文件上传监听器的实现,核心代码如下:
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;/** * 公共事件监听器组件,具体实现使用策略模式实现,统一由本类处理后进行相应转发, * 多种event监听均在本类中实现注册监听,使用event模式便于程序解耦,程序处理逻辑更加清晰 * @author 夜郎king */@Componentpublic class YelangSpringListener { private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user"); @EventListener public void fileUploadEventRegister(FileUploadEvent event){ try { sys_user_logger.info("当前处理线程名称:" + Thread.currentThread().getName()); FileEntity fileEntity = event.getFileEntity(); if(StringUtils.isNotEmpty(fileEntity.getTablename())){ FileUploadServiceRegisterEnum rigisterEnum = null; if(StringUtils.isNotBlank(fileEntity.getBizType())) {//业务类型不为空,则根据表名和业务名称来查找执行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("未注册文件上传监听回调处理器."); } } } catch (Exception e) { sys_user_logger.error("文件上传事件监听发生错误.",e); } }}
上面的逻辑中,重点就是找到回调的具体枚举实例,然后使用Spring的IOC机制,找到注册到Spring上下文中的IFileUploadCallbackService
类
if(null != rigisterEnum && StringUtils.isNotEmpty(rigisterEnum.getExecService())){ String execService = rigisterEnum.getExecService(); IFileUploadCallbackService service = SpringUtils.getBean(execService); service.process(fileEntity);}
然后调用process方法开始进行文件的处理。
3、文件具体处理逻辑
为了让不同的业务实现不同的业务处理需要,我们将文件处理方法封装成统一的一个接口,然后通过不同的实例类来进行实现。接口的定义如下:
package com.yelang.project.common.service;import com.yelang.project.webupload.domain.FileEntity;public interface IFileUploadCallbackService { /** * 文件上传事件监听器回调服务接口,封装公共服务,可以读取相关表格或修改业务表,具体实现由各实现类来完成 * @param fileEntity 文件实体 * @throws Exception */ void process(FileEntity fileEntity) throws Exception;}
然后定义统一的文件处理实现类,实现上述的接口,并实现具体的文件处理方法。
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("执行结束"); } }}
上面的程序逻辑比较简单,我们仅演示了如何从事件发布器中获取FileEntity实体的信息,同时打印相应的信息。在实际业务中,可以实现更复杂的业务。
4、实际处理实例
下面我们结合实际场景来看一下具体的实现及调用过程。
我们来看一下后台的处理信息的输出,
可以很明显的看到,在后台的控制台已经成功的输出相应的内容,表明事件的发布、监听、处理按照预定的设计运行。
23:14:53.169 [http-nio-8080-exec-37] INFO sys-user - [fileUploadEventRegister,32] - 当前处理线程名称:http-nio-8080-exec-3723:14:53.198 [http-nio-8080-exec-37] DEBUG c.y.p.e.s.m.S.selectById - [debug,137] - <== Total: 123: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] - 执行结束
四、总结
以上就是本文的主要内容,本文以WebUploader大文件上传组件为例,在大文件处理的场景中使用SpringEvent的事件发布机制,灵活的扩展对文件的处理需求。
本文通过代码实例的讲解,让您快速的了解如何在Spring中快速开发Event应用程序,同时使用枚举来实现动态的注册过程,实现方便灵活的注册机制。行文仓促,定有不足之处,真诚期待各位专家朋友在评论区批评指正,不甚感激。