背景:云体检系统需要将不同的体检报告上传到fastdfs服务器,体检报告分为 个人健康体检报告、个人职业体检报告、单位团检报告、个人电测听报告……
    原有代码存在的问题:
    报告存储路径没有统一,需要多层if else 判断封装,逻辑处理放在Controller(结构混乱),这个方法274行代码,后期再有其他类型报告,难以扩展,命名更糟糕,就不多说了,下面为处理过的伪代码。

    1. public ModelVo uploadReport(@Context HttpServletRequest request) throws Exception {
    2. List<FileItem> fileItemList = getfileItemList(request);
    3. int type = -1; //默认为-1啥都不操作 ,0 表示 个人报告 1表示单位报告 2 表示纯音报告 3职业病报告
    4. for (int i = 0; i < fileItemList.size(); i++) {
    5. String filename = readFileItem();
    6. if (filename.contains("单位")) {
    7. type = 1;
    8. doSomething()
    9. } else if (filename.contains("纯音")) {
    10. type = 2;
    11. doSomething()
    12. } else if (filename.contains("健康")) {
    13. type = 0;
    14. doSomething()
    15. } else if (filename.contains("职业")) {
    16. type = 3;
    17. doSomething()
    18. } else {
    19. type = 0;
    20. doSomething()
    21. }
    22. // read Zip and upload report
    23. String fileId = readZipAndUploadReport(zipFile)
    24. if (ftype == 2) {
    25. doSomething()
    26. } else {
    27. generateHtmlReport()
    28. if(ftype == 1){
    29. buildUrl()
    30. }else if(ftype == 3){
    31. buildUrl()
    32. }else{
    33. buildUrl()
    34. }
    35. logger.info("电子报告—>"+JSON.toJSONString(map));
    36. updateReport(map);
    37. logger.info("生成电子报告成功");
    38. }
    39. saveLog();
    40. }
    41. }

    重构的类图:

    ReportUploader.png
    通过文件路径区分上传报告ReportType (枚举类型)类型,获取对应报告上传ReportUploader,将if else 逻辑剥离到ReportType 获取上:

    1. public enum ReportType {
    2. group, audiometry, occupation, physique;
    3. public static ReportType getReportType(String itemName) {
    4. if (StrUtil.containsAny(itemName, "physique")) {
    5. return ReportType.physique;
    6. }
    7. if (StrUtil.containsAny(itemName, "group")) {
    8. return ReportType.group;
    9. }
    10. if (StrUtil.containsAny(itemName, "audioExam")) {
    11. return ReportType.audiometry;
    12. }
    13. if (StrUtil.containsAny(itemName, "occupation")) {
    14. return ReportType.occupation;
    15. }
    16. return ReportType.physique;
    17. }
    18. }

    ReportUploader 实例获取基于Spring将所有的ReportUploader 实现类存到uploaders对象池中,根据reportType 获取对应的ReportUploader ,代码如下:

    1. @Component
    2. public class UploaderFactory implements InitializingBean, ApplicationContextAware {
    3. private static final Map<ReportType, ReportUploader> uploaders = new ConcurrentHashMap();
    4. private ApplicationContext appContext;
    5. public ReportUploader getUploader(ReportType reportType) {
    6. return uploaders.get(reportType);
    7. }
    8. @Override
    9. public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    10. this.appContext = applicationContext;
    11. }
    12. @Override
    13. public void afterPropertiesSet() throws Exception {
    14. // 将 Spring 容器中所有的 ReportUploader 注册到 uploaders
    15. appContext.getBeansOfType(ReportUploader.class)
    16. .values()
    17. .forEach(uploader -> uploaders.put(uploader.getType(), uploader));
    18. }
    19. }

    报告上传代码:

    1. @Component
    2. public class UploadHandler {
    3. public static final Logger logger = LoggerFactory.getLogger(UploadHandler.class);
    4. @Resource
    5. UploaderFactory uploaderFactory;
    6. /**
    7. * @param fileItems
    8. * @param type
    9. */
    10. @Async
    11. public void uploadReport(List<FileItem> fileItems, String type) {
    12. if (CollUtil.isEmpty(fileItems)) {
    13. return;
    14. }
    15. for (FileItem item : fileItems) {
    16. String itemName = item.getName();
    17. //文件名就是examNum
    18. if (item.isFormField() || !StrUtil.equalsIgnoreCase(FileUtil.extName(itemName), "zip")) {
    19. continue;
    20. }
    21. logger.info("itemName:{}", itemName);
    22. Long examNum = Long.parseLong(FileUtil.mainName(itemName));
    23. ReportType reportType = ReportType.getReportType(itemName);
    24. ReportUploader uploader = uploaderFactory.getUploader(reportType);
    25. Objects.requireNonNull(item, "文件不存在!");
    26. try {
    27. boolean isPdf = StrUtil.equalsIgnoreCase(".pdf", type);
    28. Map<String, Object> uploadedUrls = uploader.upload(item, examNum, isPdf);
    29. if (CollUtil.isEmpty(uploadedUrls)) {
    30. continue;
    31. }
    32. updateReportUrl(uploadedUrls);
    33. saveLog();
    34. } catch (Exception e) {
    35. logger.error("{},{}", e.getMessage(), e);
    36. }
    37. }
    38. }
    39. }

    Spring @Async 无法复用线程池,每次执行都会新建线程池,如果业务频繁,可以改为自定义线程池异步执行。

    题外话:

    用到了Spring 的两个接口:

    1. ApplicationContextAware接口,作用就是将Spring的ApplicationContext 填充到appContext属性。
    1. public interface ApplicationContextAware extends Aware {
    2. /**
    3. * Set the ApplicationContext that this object runs in.
    4. * Normally this call will be used to initialize the object.
    5. * <p>Invoked after population of normal bean properties but before an init callback such
    6. * as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
    7. * or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},
    8. * {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and
    9. * {@link MessageSourceAware}, if applicable.
    10. * @param applicationContext the ApplicationContext object to be used by this object
    11. * @throws ApplicationContextException in case of context initialization errors
    12. * @throws BeansException if thrown by application context methods
    13. * @see org.springframework.beans.factory.BeanInitializationException
    14. */
    15. void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
    16. }
    1. InitializingBean接口,作用就是在实例化UploaderFactory 后填充uploaders属性。
    1. public interface InitializingBean {
    2. /**
    3. * Invoked by a BeanFactory after it has set all bean properties supplied
    4. * (and satisfied BeanFactoryAware and ApplicationContextAware).
    5. * <p>This method allows the bean instance to perform initialization only
    6. * possible when all bean properties have been set and to throw an
    7. * exception in the event of misconfiguration.
    8. * @throws Exception in the event of misconfiguration (such
    9. * as failure to set an essential property) or if initialization fails.
    10. */
    11. void afterPropertiesSet() throws Exception;
    12. }