巧用 SpringEvent 解决 WebUploader 大文件上传解耦问题

科技   2024-11-02 17:22   江苏  

点击关注公众号,Java干货及时送达

前言

关于Spring的Event机制,相信使用Java开发的朋友们一定非常熟悉。Spring Event是Spring框架内建的一种发布/订阅(Publish-Subscribe)模式的实现,它允许应用内部不同组件之间通过事件进行通信。当某个特定事件发生时,系统中对这类事件感兴趣的监听器可以接收到通知并执行相应操作。是不是看起来跟消息队列差不多,尤其是这种发布/订阅的模式,确实非常符合消息中间件的模式。通常来说,消息队列一般有以下几种作用。异步、解耦和削峰。

不过请大家注意,之所以在这里讲解SpringEvent,在一般的中小型项目中,我们的部署节点是单个,技术的架构选型一般也是单体架构。因此我们可以在不引入复杂架构的前提下来实现一个简单版本的消息队列。

通过发布订阅的模式来进行应用程序解耦,让各个功能组件更加符合实际架构的布置。从而让程序扩展起来更方便。以上是关于SpringEvent这个框架的知识,更多的关于SpringEvent的相关知识,大家感兴趣的可以登录spring的官方网站来进行查询。

除了Spring的Event机制之外,在我们日常的项目开发过程中,肯定会遇到大文件上传的处理场景,而大文件的处理通常是需要蛮长的时间。同时,不同的场景甚至不同的业务,对于上传附件的处理是不尽相同的。比如在用户信息Excel的上传处理中,不仅需要将当前的Excel数据进行关联绑定,同时还需要解析数据后,将Excel提交的数据结果存储到数据库中。

而另外一个类型,比如单据生成的任务表格,就需要根据单据信息,匹配不同的模板来生成不同的任务。由此种种需求,要求我们的应用程序在开发过程中具有较大的扩展性,可以支持将不同的应用程序快速的切入到应用中,同时能针对不同的场景灵活开发。极端的情况下,甚至需要同一种业务,根据不同的状态来定制附件的读取需求。

本文以WebUploader大文件上传组件为例,在大文件处理的场景中使用SpringEvent的事件发布机制,灵活的扩展对文件的处理需求。通过代码实例的讲解,让您快速的了解如何在Spring中快速开发Event应用程序,同时使用枚举来实现动态的注册过程,实现方便灵活的注册机制。最后结合一个具体的场景详细说明在学生信息附件上传中来进行附件处理的过程,方便您掌握上述的知识点。

一、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
@ToString
public 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
@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)上传成功之后以及大文件合并完成之后就可以发布文件上传事件,在下面的代码中,我们通过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
 */

@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("当前处理线程名称:" + 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 fileEntitythrows 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-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] - 执行结束

四、总结

以上就是本文的主要内容,本文以WebUploader大文件上传组件为例,在大文件处理的场景中使用SpringEvent的事件发布机制,灵活的扩展对文件的处理需求。

本文通过代码实例的讲解,让您快速的了解如何在Spring中快速开发Event应用程序,同时使用枚举来实现动态的注册过程,实现方便灵活的注册机制。行文仓促,定有不足之处,真诚期待各位专家朋友在评论区批评指正,不甚感激。

来源:https://blog.csdn.net/yelangkingwuzuhu/

  

1、网红 Arc 浏览器 凉了!

2、6个月收入狂揽700万!17岁高中生开发一个新AI应用爆火,网友:别低估小孩子

3、存QQ号码,究竟该用int类型还是string类型?

4、在一个公司死磕了 5~10 年的人最后都怎么样了?

5、每个程序员都要有 3 份收入!

6、SQLite这么小众的数据库,到底是什么人在用?

点在看

Java技术迷
专注于计算机编程语言知识分享。主要包括Java基础技术、数据结构、相关工具、spring Cloud、intellij idea......,送书、送红包福利等你来!
 最新文章