Java注解实现异步导入与导出(一)

1.背景:

Java批量导入百万级数据到mysql
之前写过批量导入百万级数据到mysql,但是这个局限性比较大,遇到需要复杂校验(重复性校验,有效性校验)的场景下,这种很容易就超时,同时一个系统内,肯定会有多个地方需要用到导入导出,每个地方都写一堆类似的代码,同时还得不断优化性能(数据越来越多,需要越来越复杂),这时候一个管理系统所有导入/导出记录的页面就很实用了,可以让各个模块业务专注在业务上,不需要关心上传和下载。

2.设计:

2.1 常规的导入同步流程如下, 这就引发了一个问题:如果 Excel 的行非常多,或者解析非常复杂,那么解析+校验的过程就非常耗时。如果接口是一个同步的接口,则非常容易出现接口超时,进而返回的校验错误信息也无法展示给前端,这就需要从功能上解决这个问题。
image.png
2.2 把同步改为异步,同时引入NFS用来临时存储文件
image.png
2.3 导入导出日志记录表设计如下:
CREATE TABLE `t_import_export_records` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `module_name` varchar(32) NOT NULL COMMENT '模块名称',
  `file_name` varchar(32) NOT NULL COMMENT '文件名称',
  `type` tinyint(2) NOT NULL DEFAULT '1' COMMENT '导入:1;导出:2;',
  `state` tinyint(2) NOT NULL DEFAULT '0' COMMENT '进行中:0;成功:1;失败:2;已过期:3',
  `nfs_path` varchar(200) DEFAULT NULL COMMENT '文件路径',
 `error_reason` text COMMENT '异常日志文件路径',
  `expire_time` varchar(32) NOT NULL COMMENT '过期时间,小时',
  `start_time` datetime DEFAULT NULL COMMENT '开始时间',
  `end_time` datetime DEFAULT NULL COMMENT '开始时间',
  `creator` varchar(32) DEFAULT NULL COMMENT '创建人员',
  `create_tm` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `modifier` varchar(32) DEFAULT NULL COMMENT '修改人员',
  `modify_tm` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `creator` (`creator`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导入导出日志记录表';
2.4 代码实现

从上面的流程中可以看出,每个业务自己只需要实现自己的 数据校验,持久化到db 这两个个步骤,别的都是通用的,简单点的就是我们写几个工具类,然后让大家去调用,但是这样还是麻烦,不够简便和优雅,我想的是可以把这些完全抽象出来,让大家不用关心这些重复的步骤。

使用注解来完成

2.4.1 定义一个注解,用于标识切点:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Upload {

    //业务模块名称
    String moduleName() default "";
    //文件过期时间
    int expireTime() default 24;
    //文件名称
    String fileName() default "";
      //excel解析类
    Class     clazz();
    //excel 监听器
    Class listener();
}
2.4.2 编写切面:
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.demo.framework.domain.Result;
import com.demo.management.util.DateUtil;
import com.demo.management.util.FileUploadUtil;
import com.demo.management.util.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.*;


@Component
@Aspect
@Slf4j
public class UploadAspect {
    public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("upload-pool-%d")
            .setPriority(Thread.NORM_PRIORITY).build();
    public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(1, 20, 300L,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());


    @Pointcut("@annotation(com.demo.management.mallgoods.controller.Upload)")
    public void uploadPoint() {}

    @Around(value = "uploadPoint()")
    public Object uploadControl(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法上的注解,进而获取uploadType
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Upload annotation = signature.getMethod().getAnnotation(Upload.class);

        //导入文件
        MultipartFile file = (MultipartFile)joinPoint.getArgs()[0];
        //1、文件上传
        String filePath =  uploadFile(file);


        // 线程池启动异步线程,开始执行上传的逻辑,joinPoint.proceed()就是你实现的业务功能
        uploadExecuteService.submit(() -> {
            ImportExportRecords records = new ImportExportRecords();
            try {
                // 2、初始化导入日志,记录开始时间
                records.setModuleName(annotation.moduleName());
                records.setExpireTime(DateUtil.format(DateUtils.addHours(new Date(),annotation.expireTime()),DateUtil.DATE_TIME_PATTERN));
                records.setType(1);
                records.setStartTime(new Date());
                //  records= writeRecordsToDB(records);
                System.out.println("writeInitToDB");

                //2.1 子线程读取文件,并解析excel文件
                List list= EasyExcel.read(new FileInputStream(new File(filePath)),annotation.clazz(),(AnalysisEventListener)annotation.listener().newInstance()).sheet().doReadSync();

                //2.1 下面是通过反序列化构建的excel列表,过于复杂了,上面的是优化版本
//                List list= EasyExcel.read(new FileInputStream(new File(filePath))).sheet().doReadSync();
//                //反序列化 构建读取excel列表
//                Class clazz =  annotation.clazz();
//                Field[] fields = clazz.getDeclaredFields();
//                List<Map<String,Object>> resultList = new ArrayList();
//                for(int i=0;i<list.size()-1;i++){
//                    LinkedHashMap<Integer,Object> map = (LinkedHashMap)list.get(i);
//                    Map<String,Object> paramMap = new HashMap<>();
//                    for(Map.Entry<Integer,Object> entry: map.entrySet()){
//                        Field field = Arrays.stream(fields).filter(item -> item.getAnnotation(ExcelProperty.class).index()==entry.getKey()).findFirst().get();
//                        paramMap.put(field.getName(),entry.getValue());
//                    }
//                    resultList.add(paramMap);
//                }
//                list = JSON.parseArray(JSON.toJSONString(resultList), annotation.clazz());

                //2.2 执行业务方法  数据校验,持久化DB
                //传参
                Object result = joinPoint.proceed(new Object[]{file,list});
                Result  errorResult = JSON.parseObject(JSON.toJSONString(result),Result.class);

                //2.3 更新导入日志结果
                if (errorResult!=null && errorResult.isSuccess()) {
                    // 成功,
                    records.setState(1);
                } else {
                    // 失败,
                    records.setState(2);
                    records.setErrorReason(errorResult.getErrorMessage());
                }
            } catch (Throwable e) {
                // 异常,需要记录
                log.error("error",e);
                records.setState(2);
                records.setErrorReason(e.getMessage());
            }

            //2.3 更新导入日志结果
//            updateByRecordsId(records);
            System.out.println("updateByRecordsId");
        });
        return ResultUtils.success();
    }

文件上传方法

    public String uploadFile(MultipartFile file) {
        String path = FileUploadUtil.getDefaultSavePath("businessCoupon", file.getOriginalFilename());
        String fileSavePath = null;
        try {
            boolean upload = FileUploadUtil.saveFileUpload(path, FileUploadUtil.multipartFileToFile(file));
            if (upload) {
                fileSavePath = path;
            }
        } catch (Exception e) {
            log.error("文件上传失败,", e);
        }
        return fileSavePath;
    }

    public static boolean saveFileUpload(String savePath, File file) {
        try {
            if (StringUtils.isEmpty(savePath)) {
                log.info("savePath is null");
                return false;
            }
            log.info("save file path : " + savePath);
            java.nio.file.Files.copy(file.toPath(), new File(savePath).toPath());
            return true;
        } catch (IOException e) {
            log.error("saveFileUpload error", e);
        }
        return false;
    }


    /**
     * MultipartFile 转 File
     */
    public File multipartFileToFile(MultipartFile file) {

        File toFile = null;
        try{
            if (file == null || StringUtils.isBlank(file.getOriginalFilename()) || file.getSize() <= 0) {
                return null;
            } else {
                InputStream ins;
                ins = file.getInputStream();
                toFile = new File(file.getOriginalFilename());
                inputStreamToFile(ins, toFile);
                ins.close();
            }
        }catch (Exception e){
            log.error("multipartFileToFile err", e);
//            throw new ManagementRuntimeException(e.getMessage());
        }
        return toFile;
    }

    /**
     *  获取文件流
     */
    private static void inputStreamToFile(InputStream ins, File file) {
        try {
            OutputStream os = new FileOutputStream(file);
            int bytesRead = 0;
            byte[] buffer = new byte[8192];
            while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
            os.close();
            ins.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

2.4.3 注解使用:

用户只需要定义好execl解析类,和解析监听器就可以

@Upload(moduleName="aaa",expireTime=3,clazz = MallOrderExcelVO.class,listener = MallOrderExcelListener.class)
    public Result<Integer> uploadWaybillNo(MultipartFile file, ArrayList<MallOrderExcelVO> list) {
        if (null == file) {
            return ResultUtils.error(ResponseResultStatusEnum.ERR_UPLOAD_FILE);
        }
        try {
              //list 数据校验
              if(CollectionUtils.isEmpty(list)){
                 return ResultUtils.error("数据为空");
              }
              //list 数据持久到db
               this.uploadWaybillNo(list);
         
            return ResultUtils.success();
        } catch (Exception e) {
            return ResultUtils.error(ResponseResultStatusEnum.ERR_UNKNOWN);
        }
    }

异步导出见下一篇Java注解实现异步导入与导出(二)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容