背景:云体检系统需要将不同的体检报告上传到fastdfs服务器,体检报告分为 个人健康体检报告、个人职业体检报告、单位团检报告、个人电测听报告……
原有代码存在的问题:
报告存储路径没有统一,需要多层if else 判断封装,逻辑处理放在Controller(结构混乱),这个方法274行代码,后期再有其他类型报告,难以扩展,命名更糟糕,就不多说了,下面为处理过的伪代码。
public ModelVo uploadReport(@Context HttpServletRequest request) throws Exception {List<FileItem> fileItemList = getfileItemList(request);int type = -1; //默认为-1啥都不操作 ,0 表示 个人报告 1表示单位报告 2 表示纯音报告 3职业病报告for (int i = 0; i < fileItemList.size(); i++) {String filename = readFileItem();if (filename.contains("单位")) {type = 1;doSomething()} else if (filename.contains("纯音")) {type = 2;doSomething()} else if (filename.contains("健康")) {type = 0;doSomething()} else if (filename.contains("职业")) {type = 3;doSomething()} else {type = 0;doSomething()}// read Zip and upload reportString fileId = readZipAndUploadReport(zipFile)if (ftype == 2) {doSomething()} else {generateHtmlReport()if(ftype == 1){buildUrl()}else if(ftype == 3){buildUrl()}else{buildUrl()}logger.info("电子报告—>"+JSON.toJSONString(map));updateReport(map);logger.info("生成电子报告成功");}saveLog();}}
重构的类图:

通过文件路径区分上传报告ReportType (枚举类型)类型,获取对应报告上传ReportUploader,将if else 逻辑剥离到ReportType 获取上:
public enum ReportType {group, audiometry, occupation, physique;public static ReportType getReportType(String itemName) {if (StrUtil.containsAny(itemName, "physique")) {return ReportType.physique;}if (StrUtil.containsAny(itemName, "group")) {return ReportType.group;}if (StrUtil.containsAny(itemName, "audioExam")) {return ReportType.audiometry;}if (StrUtil.containsAny(itemName, "occupation")) {return ReportType.occupation;}return ReportType.physique;}}
ReportUploader 实例获取基于Spring将所有的ReportUploader 实现类存到uploaders对象池中,根据reportType 获取对应的ReportUploader ,代码如下:
@Componentpublic class UploaderFactory implements InitializingBean, ApplicationContextAware {private static final Map<ReportType, ReportUploader> uploaders = new ConcurrentHashMap();private ApplicationContext appContext;public ReportUploader getUploader(ReportType reportType) {return uploaders.get(reportType);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.appContext = applicationContext;}@Overridepublic void afterPropertiesSet() throws Exception {// 将 Spring 容器中所有的 ReportUploader 注册到 uploadersappContext.getBeansOfType(ReportUploader.class).values().forEach(uploader -> uploaders.put(uploader.getType(), uploader));}}
报告上传代码:
@Componentpublic class UploadHandler {public static final Logger logger = LoggerFactory.getLogger(UploadHandler.class);@ResourceUploaderFactory uploaderFactory;/*** @param fileItems* @param type*/@Asyncpublic void uploadReport(List<FileItem> fileItems, String type) {if (CollUtil.isEmpty(fileItems)) {return;}for (FileItem item : fileItems) {String itemName = item.getName();//文件名就是examNumif (item.isFormField() || !StrUtil.equalsIgnoreCase(FileUtil.extName(itemName), "zip")) {continue;}logger.info("itemName:{}", itemName);Long examNum = Long.parseLong(FileUtil.mainName(itemName));ReportType reportType = ReportType.getReportType(itemName);ReportUploader uploader = uploaderFactory.getUploader(reportType);Objects.requireNonNull(item, "文件不存在!");try {boolean isPdf = StrUtil.equalsIgnoreCase(".pdf", type);Map<String, Object> uploadedUrls = uploader.upload(item, examNum, isPdf);if (CollUtil.isEmpty(uploadedUrls)) {continue;}updateReportUrl(uploadedUrls);saveLog();} catch (Exception e) {logger.error("{},{}", e.getMessage(), e);}}}}
Spring @Async 无法复用线程池,每次执行都会新建线程池,如果业务频繁,可以改为自定义线程池异步执行。
题外话:
用到了Spring 的两个接口:
- ApplicationContextAware接口,作用就是将Spring的ApplicationContext 填充到appContext属性。
public interface ApplicationContextAware extends Aware {/*** Set the ApplicationContext that this object runs in.* Normally this call will be used to initialize the object.* <p>Invoked after population of normal bean properties but before an init callback such* as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}* or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},* {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and* {@link MessageSourceAware}, if applicable.* @param applicationContext the ApplicationContext object to be used by this object* @throws ApplicationContextException in case of context initialization errors* @throws BeansException if thrown by application context methods* @see org.springframework.beans.factory.BeanInitializationException*/void setApplicationContext(ApplicationContext applicationContext) throws BeansException;}
- InitializingBean接口,作用就是在实例化UploaderFactory 后填充uploaders属性。
public interface InitializingBean {/*** Invoked by a BeanFactory after it has set all bean properties supplied* (and satisfied BeanFactoryAware and ApplicationContextAware).* <p>This method allows the bean instance to perform initialization only* possible when all bean properties have been set and to throw an* exception in the event of misconfiguration.* @throws Exception in the event of misconfiguration (such* as failure to set an essential property) or if initialization fails.*/void afterPropertiesSet() throws Exception;}
