架构设计:文件服务的设计与实现( 三 )

  • 结果信息是一次读取内容,且频率不高
  • 本身就是文件服务,使用文件存储也合理
  • 不需要部署数据库,不需要设计表结构,节省部署与设计时间
  • 实现
    架构设计:文件服务的设计与实现

    文章插图
    结构与架构图一致
    事件实现事件串联了整个上传流程:
    • 文件上传,触发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=GROUP2fileupload.config.videoConfigList[1].type=webm# 转换为webmfileupload.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; }}


    推荐阅读