使用EasyExcel 導(dǎo)入數(shù)據(jù),失敗原因數(shù)據(jù)導(dǎo)出

引言

在日常開(kāi)發(fā)過(guò)程中呻拌,Excel 導(dǎo)入是非常常見(jiàn)的場(chǎng)景葱轩,而且也有很多開(kāi)源的項(xiàng)目是針對(duì)Excel的讀寫的,如Apache 的poi 藐握,最近用的比較好的還是阿里的EasyExcel 開(kāi)源工具靴拱。平時(shí)我們只是簡(jiǎn)單的讀取文件并寫入數(shù)據(jù)庫(kù)持久化即可,但是前段時(shí)間猾普,產(chǎn)品搞了個(gè)需求袜炕,需要將導(dǎo)入失敗的數(shù)據(jù)及原因?qū)懭隕xcel并下載,那這就有得玩了初家,廢話不多說(shuō)偎窘,上才藝。

產(chǎn)品需求

  • 導(dǎo)入Excel數(shù)據(jù)
  • 數(shù)據(jù)格式校驗(yàn)
  • 數(shù)據(jù)合法性校驗(yàn)(校驗(yàn)數(shù)據(jù)庫(kù))
  • 失敗數(shù)據(jù)提供用戶下載溜在,并支持再次導(dǎo)入

技術(shù)選型

  • 陌知,Excel 讀取/寫入
  • ,做異步處理

需求實(shí)現(xiàn)

項(xiàng)目依賴(maven)

<!-- easyexcle -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>easyexcle</artifactId>
  <version>2.2.6</version>
</dependency>
<!-- xxl job -->
<dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>${xxl-job.version}</version>
        </dependency>

文件解析

解析導(dǎo)入文件掖肋,獲取文件數(shù)據(jù)量仆葡,用于判定導(dǎo)入是否走異步導(dǎo)入。

public class EasyExcelUtils {
    
    /**
     *
     * 解析文件培遵,獲取最后一行
     * @param inputStream 文件流
     * @param sheetNum 讀取excel表格的sheetNum 索引
     * @return 總行數(shù)
     */
    public static Integer lastNum(InputStream inputStream,Integer sheetNum){
    
        Workbook wb = null;
        sheetNum = sheetNum == null ? 0 : sheetNum;
        try {
            wb = WorkbookFactory.create(inputStream);
            Sheet sheet = wb.getSheetAt(sheetNum);
            CellReference cellReference = new CellReference("A4");
            // 處理空行
            for (int i = cellReference.getRow();i <= sheet.getLastRowNum();){
                // 省略部分代碼
            }
            return sheet.getLastRowNum();
        } catch (Exception e){
            
        }
        return 0;
    }
}

判定導(dǎo)入數(shù)據(jù)文件是否為空浙芙,如果為空登刺,將返回錯(cuò)誤信息

@RestController
// 省略其他注解
public class ProjectInfoController {

    /**
     * 項(xiàng)目信息導(dǎo)入
     */
    @PostMapping("/import")
    public R projectInfoImport(MultipartFile file,HttpServletResponse response){
        InputStream inputStream = null;
        int lastNum = 0;
        try {
            lastNum = EasyExcelUtils.lastNum(file.getInputStream());
        }catch(IOException e){
            // 省略部分代碼
        }
        if (lastNum <= 0 ){
            throw CustomExcetpoin(500,"導(dǎo)入文件數(shù)據(jù)為空,請(qǐng)重新上傳");
        }
        
    }
}

文件解析拿到導(dǎo)入數(shù)據(jù)的數(shù)據(jù)量嗡呼,與系統(tǒng)配置的文件導(dǎo)入上限值進(jìn)行判定纸俭,如果大于上限值將走異步處理(異步導(dǎo)入,請(qǐng)查看異步“異步導(dǎo)入”導(dǎo)入內(nèi)容)南窗。

@RestController
// 省略其他注解
public class ProjectInfoController {
    
    @Resource
    private AsyncExcelService asyncExcelService;

    /**
     * 項(xiàng)目信息導(dǎo)入
     */
    @PostMapping("/import")
    public R projectInfoImport(MultipartFile file,HttpServletResponse response){
        InputStream inputStream = null;
        int lastNum = 0;
        try {
            lastNum = EasyExcelUtils.lastNum(file.getInputStream());
        }catch(IOException e){
            // 省略部分代碼
        }
        if (lastNum <= 0 ){
            throw CustomExcetpoin(500,"導(dǎo)入文件數(shù)據(jù)為空揍很,請(qǐng)重新上傳");
        }
        // 獲取系統(tǒng)配置的導(dǎo)入上限值
        Integer importMax = asyncExcelService.asyncProjectImportMax();
        if (lastNum > importMax ){
            // 達(dá)到上限,走異步
            asyncExcelService.asyncProjectImport(file,response);
            return R.success("數(shù)據(jù)導(dǎo)入成功万伤,因數(shù)據(jù)量比較大窒悔,已轉(zhuǎn)為異步導(dǎo)入");
        }
        // 省略其他代碼
    }
}

AsyncExcelService 接口實(shí)現(xiàn)

/**
 * 異步導(dǎo)出/導(dǎo)入 service 
 */
public interface AsyncExcelService {
    
    /** 默認(rèn)導(dǎo)入數(shù)據(jù)上限 **/
    Integer DEFAULT_IMPORT_DATA_MAX = 500;
    
    /**
     * 獲取最大導(dǎo)入上限值,超過(guò)則走異步
     */
    Integer getImportMax();
    
    /**
     * 異步導(dǎo)入數(shù)據(jù)
     */
    void asyncProjectImport(MultipartFile file,HttpServletResponse response);
}

@Service
// 省略其他注解
public class AsyncExcelServiceImpl implements AsyncExcelService {

    @Resource
    private IParamtersClient paramtersClient;
    
    @Override
    public Integer getImportMax(){
        Integer value = getParamVaule("paramName",Integer.class);
        return value == null ? DEFAULT_IMPORT_DATA_MAX : value;
    }
    
    /**
     * 調(diào)用框架接口獲取系統(tǒng)參數(shù)
     *
     */
    private <T> T getParamVaule(String name,Class<T> clazz){
        CCBHousingUser user = SecureUtil.getUser();
        // 省略部分代碼
        
        // 獲取系統(tǒng)配置參數(shù)
        Parameters parameters = paramtersClient.getParamterByCodeAndOrg(name,user.getOrganizationId());
        
        // 省略部分代碼
    }
}

其中敌买,IParamtersClient 屬于框架提供的feign 接口简珠,也可以根據(jù)自己的實(shí)際場(chǎng)景實(shí)現(xiàn)相關(guān)邏輯。

數(shù)據(jù)合法校驗(yàn)

導(dǎo)入數(shù)據(jù)文件解析使用的是alibaba 提供的 EasyExcel 開(kāi)源工具虹钮,我們需要在 EasyExcel 工具的基礎(chǔ)上做一些增強(qiáng)處理聋庵,如:導(dǎo)入格式校驗(yàn)、導(dǎo)入表頭校驗(yàn)芙粱、導(dǎo)入數(shù)據(jù)格式校驗(yàn)等祭玉,如果發(fā)生校驗(yàn)失敗,將錯(cuò)誤信息寫入錯(cuò)誤報(bào)告(excel)輸出到客戶端春畔。

定義easyexcel 導(dǎo)入文件到列與實(shí)體映射關(guān)系脱货,將使用到 easyexcel 到@ExcleProperty 注解進(jìn)行關(guān)系綁定

@Data
// 省略其他注解
public class ProjectInfoExcelDTO {

    @ExcelProperty(index=0,value="序列號(hào)")
    private String number;
    
    @ExcelProperty(index=1,value="項(xiàng)目名稱")
    private String name;
    
    // 省略其他字段屬性

}

注解 @ExcleProperty 常用屬性

  • index,與excel文件中律姨,表頭列的索引位置對(duì)應(yīng)(從0開(kāi)始)
  • value振峻,與excel文件中,表頭列的名稱相對(duì)應(yīng)
  • converter,指定解析數(shù)據(jù)時(shí)线召,該列需要使用的數(shù)據(jù)轉(zhuǎn)換器铺韧,轉(zhuǎn)換器實(shí)現(xiàn)Converter接口

定義校驗(yàn)錯(cuò)誤的數(shù)據(jù)結(jié)構(gòu)類型

@Data
// 省略其他注解
public class ExcelChcekErrDTO<T> {

    private T t;
    
    private String errMsg;
}

備注:@Data 屬于 lombok 工具,簡(jiǎn)化Bean的封裝缓淹,感興趣的同學(xué),可以自行查閱資料塔逃。

定義Excel導(dǎo)入校驗(yàn)返回的數(shù)據(jù)VO

@Data
// 省略其他注解
public class ExcelCheckResultVO<T> {
    
    /** 校驗(yàn)成功的數(shù)據(jù) **/
    private List<T> successDatas;
    
    /** 校驗(yàn)失敗的數(shù)據(jù) **/
    private List<ExcelChcekErrDTO> errData;
}

定義數(shù)據(jù)解析監(jiān)聽(tīng)器EasyExcelListener

@Data
// 省略部分注解
public class EasyExcelListener<T> extends AnalysisEventListener<T> {

    // 省略部分代碼
}

定義excel 業(yè)務(wù)校驗(yàn)管理器 ExcelCheckManager,需要做業(yè)務(wù)校驗(yàn)的(與數(shù)據(jù)庫(kù)匹配等)需要實(shí)現(xiàn)該接口

public interface ExcelCheckManager<T> {
    
    ExcelCheckResultVO checkImportExcle(List<T> datas);
}

表頭校驗(yàn)
使用EasyExcelListener 用來(lái)監(jiān)聽(tīng)數(shù)據(jù)解析過(guò)程讯壶,其中,invokHeadMap 方法將在解析完成excel表頭時(shí)將被執(zhí)行

@Data
// 省略部分注解
public class EasyExcelListener<T> extends AnalysisEventListener<T> {

    /** excel 對(duì)象的反射類 **/
    private Class<T> clazz;
    
    private ExcelCheckManager<T> excelCheckManager;
    
    public EasyExcelListener(ExcelCheckManager<T> excelCheckManager,Class<T> clazz){
        this.clazz = clazz;
        this.excelCheckManager = excelCheckManager;
    }
    
    @Override
    public void invokHeadMap(Map<Integer,String> headMap,AnalysisContext context){
    
        super.invokHeadMap(headMap,context);
        // 反射獲取實(shí)體到屬性值
        Map<Integer,String> indexNameMap = getIndexNameMap(clazz);
        // 將 headMap 與 indexNameMap 進(jìn)行對(duì)比湾盗,是否完全匹配
        Set<Integer> keySet = indexNameMap.keySet();
        for (Integer key : keySet ){
            if (StringUtils.isEmpty(headMap.get(key)){
                throw ExcelAnalysisExcetpion("數(shù)據(jù)解析錯(cuò)誤伏蚊,請(qǐng)傳入正確的excel格式");
            }
            if (!headMap.get(key).equals(indexNameMap.get(key)){
                throw ExcelAnalysisExcetpion("數(shù)據(jù)解析錯(cuò)誤,請(qǐng)傳入正確的excel格式");
            }
        }
        
    }
    
    /**
     * 反射獲取解析數(shù)據(jù)實(shí)體的@ExcleProperty 的value
     */
    public Map<Integer,String> getIndexNameMap(Class clazz){
    
        Map<Integer,String> result = new HashMap<>();
        Field field;
        Field[] fields = clazz.getDeclaredFields();
        
        for (int i = 0; i < fields.length; i++){
            field = clazz.getDeclaredField(fields[i].getName());
            field.setAccessible(true);
            ExcelProperty excleProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null){
                int index = excleProperty.index();
                String[] values = excleProperty.value();
                StringBuilder value = new StringBuilder();
                for (String v : values ){
                    value.append(v);
                }
                result.put(index,value.toString());
            }
        }
        
        return result;
    }
    
}

數(shù)據(jù)非空格粪、格式校驗(yàn)
數(shù)據(jù)非空校驗(yàn)躏吊、格式校驗(yàn)氛改,我們將使用hibernate-validator 校驗(yàn)器進(jìn)行校驗(yàn)格式。
定義validator 工具類

@component
public class EasyExcelValidatorHelper {

    private static Validtor validtor;
    
    @Autowired
    public EasyExcelValidatorHelper(Validtor validtor){
        this.EasyExcelValidatroHelper.validtor = validtor;
    }
    public static <T> String validateEntity(T obj) throws NoSuchFieldException{
        StringBuilder result = new StringBuilder();
        // 執(zhí)行校驗(yàn)
        Set<ConstraionViolation<T>> set = validtor.validate(obj,Default.class);
        // 組裝結(jié)果
        if(set != null && !set.isEmpty()){
            for (ConstraionViolation<T> cv : set ){
                Field declaredField = obj.getClass.getDeclaredField(cv.getPropertiyPath().toString());
                ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class);
                result.append(annotation.value[0]+":"+cv.getMessage()).append(";");
            }
        }
        return result;
    }
}

數(shù)據(jù)格式校驗(yàn)比伏,使用EasyExcelListener 用來(lái)監(jiān)聽(tīng)數(shù)據(jù)解析過(guò)程胜卤,其中,invok 方法將逐行解析excel數(shù)據(jù)的時(shí)候?qū)⒈徽{(diào)用

@Data
// 省略部分注解
public class EasyExcelListener<T> extends AnalysisEventListener<T> {
    
    /** 標(biāo)記是否執(zhí)行數(shù)據(jù)解析 **/
    private boolean baseMatching = false;
    
    /** 解析成功的數(shù)據(jù) **/
    private List<T> successList = new ArrayList<>();
    
    /** 解析失敗的數(shù)據(jù) **/
    private List<ExcelCheckErrDTO<T>> errList = new ArrayList<>();

    /** excel 對(duì)象的反射類 **/
    private Class<T> clazz;
    
    private List<T> list;
    
    private ExcelCheckManager<T> excelCheckManager;
    
    public EasyExcelListener(ExcelCheckManager<T> excelCheckManager,Class<T> clazz){
        this.clazz = clazz;
        this.excelCheckManager = excelCheckManager;
    }
    
    @Override
    public void invok(T t,AnalysisContext context){
        // 數(shù)據(jù)解析/轉(zhuǎn)換完成赁项,標(biāo)記進(jìn)入到解析起
        baseMatching = true;
        String errMsg;
        try {
            // 調(diào)用驗(yàn)證器驗(yàn)證數(shù)據(jù)格式
            errMsg = EasyExcelValidatorHelper.validateEntity(t);
        }catch(Exception e){
            errMsg = "解析數(shù)據(jù)出錯(cuò)";
            // 省略部分代碼
        }
        // 校驗(yàn)不通過(guò)
        if (!StringUtils.isEmpty(errMsg){
            // 將錯(cuò)誤數(shù)據(jù)放入錯(cuò)誤列表中
            ExcelChcekErrDTO errDTO = new ExcelChcekErrDTO(t,errMsg);
            errList.add(errDTO);
        } else{
            // 校驗(yàn)成功
            list.add(t);
        }
        if (list.size() > 1000){
            // 業(yè)務(wù)校驗(yàn)
            ExcelCheckResultVO excelCheckResultVO = excelCheckManager.checkImportExcel(list);
            successList.addAll(excelCheckResultVO.getSuccessDatas());
            errList.addAll(excelCheckResultVO.getErrDatas());
            list.clear();
        }
    }
    
    /**
     * 所有數(shù)據(jù)解析完成后調(diào)用此方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context){
        ExcelCheckResultVO excelCheckResultVO = excelCheckManager.checkImportExcel(list);
        successList.addAll(excelCheckResultVO.getSuccessDatas());
        errList.addAll(excelCheckResultVO.getErrDatas());
        list.clear();
    }
    
    @Override
    public void invokHeadMap(Map<Integer,String> headMap,AnalysisContext context){
    
        super.invokHeadMap(headMap,context);
        // 反射獲取實(shí)體到屬性值
        Map<Integer,String> indexNameMap = getIndexNameMap(clazz);
        // 將 headMap 與 indexNameMap 進(jìn)行對(duì)比葛躏,是否完全匹配
        Set<Integer> keySet = indexNameMap.keySet();
        for (Integer key : keySet ){
            if (StringUtils.isEmpty(headMap.get(key)){
                throw ExcelAnalysisExcetpion("數(shù)據(jù)解析錯(cuò)誤,請(qǐng)傳入正確的excel格式");
            }
            if(!headMap.get(key).equals(indexNameMap.get(key)){
                throw ExcelAnalysisExcetpion("數(shù)據(jù)解析錯(cuò)誤悠菜,請(qǐng)傳入正確的excel格式");
            }
        }
        
    }
    
    /**
     * 反射獲取解析數(shù)據(jù)實(shí)體的@ExcleProperty 的value
     */
    public Map<Integer,String> getIndexNameMap(Class clazz){
    
        Map<Integer,String> result = new HashMap<>();
        Field field;
        Field[] fields = clazz.getDeclaredFields();
        
        for (int i = 0; i < fields.length; i++){
            field = clazz.getDeclaredField(fields[i].getName());
            field.setAccessible(true);
            ExcelProperty excleProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null){
                int index = excleProperty.index();
                String[] values = excleProperty.value();
                StringBuilder value = new StringBuilder();
                for (String v : values ){
                    value.append(v);
                }
                result.put(index,value.toString());
            }
        }
        
        return result;
    }
    
    
    
}

對(duì)需要進(jìn)行校驗(yàn)對(duì)字段添加注解

@Data
// 省略其他注解
public class ProjectInfoExcelDTO {

    @ExcelProperty(index=0,value="序列號(hào)")
    private String number;
    
    @ExcelProperty(index=1,value="項(xiàng)目名稱")
    @NotBlank(message = "請(qǐng)?zhí)顚戫?xiàng)目名稱")
    private String name;
    
    // 省略其他字段屬性

}

validator 常用注解傳送門(validator 常用注解)舰攒。

EasyExcel 讀取數(shù)據(jù),并調(diào)用格式校驗(yàn)

@RestController
// 省略其他注解
public class ProjectInfoController {
    
    @Resource
    private AsyncExcelService asyncExcelService;
    
    @Resource
    private ProjectInfoService projectInfoService;

    /**
     * 項(xiàng)目信息導(dǎo)入
     */
    @PostMapping("/import")
    public R projectInfoImport(MultipartFile file,HttpServletResponse response){
        InputStream inputStream = null;
        int lastNum = 0;
        try {
            lastNum = EasyExcelUtils.lastNum(file.getInputStream());
        }catch(IOException e){
            // 省略部分代碼
        }
        if (lastNum <= 0 ){
            throw CustomExcetpoin(500,"導(dǎo)入文件數(shù)據(jù)為空悔醋,請(qǐng)重新上傳");
        }
        // 獲取系統(tǒng)配置的導(dǎo)入上限值
        Integer importMax = asyncExcelService.asyncProjectImportMax();
        if (lastNum > importMax ){
            // 達(dá)到上限摩窃,走異步
            asyncExcelService.asyncProjectImport(file,response);
            return R.success("數(shù)據(jù)導(dǎo)入成功,因數(shù)據(jù)量比較大芬骄,已轉(zhuǎn)為異步導(dǎo)入");
        }
        // 省略部分代碼
        
        // 實(shí)例數(shù)據(jù)解析監(jiān)聽(tīng)器
        EasyExcelListener<ProjectInfoDTO> easyExcleListener = new EasyExcelListener(projectInfoService,ProjectInfoDTO.class);
        // 文件讀取/解析偶芍,并注冊(cè)監(jiān)聽(tīng)器
        EasyExcle.read(file.getInputStream(),ProjectInfoDTO.class,easyExcleListener).sheet(1).doRead();
        // 獲取錯(cuò)誤數(shù)據(jù)
        List<ExcelCheckErrDTO<ProjectInfoExcelDTO>> errList = easyExcleListener.getErrList();
        // 獲取解析成功到數(shù)據(jù)
        List<ProjectinfoExcelDTO> successList = easyExcleListener.getSuccessList();
        // 如果錯(cuò)誤數(shù)據(jù)不為空,將錯(cuò)誤數(shù)據(jù)寫入到excel文件德玫,并輸出到瀏覽器
        // 省略代碼
        
        // 將成功到數(shù)據(jù)匪蟀,批量寫入到數(shù)據(jù)庫(kù)中
        // 省略代碼
        
        
        // 省略其他代碼
    }
}

ProjectInfoService 聲明與實(shí)現(xiàn),因?yàn)樾枰鰳I(yè)務(wù)數(shù)據(jù)到校驗(yàn)宰僧,因此ProjectInfoService 需要繼承 ExcelCheckManager 驗(yàn)證管理器

public interface ProjectInfoService extends ExcelCheckManager{
    
}

@Service
// 省略其他注解
public class ProjectInfoServiceImpl implements ProjectInfoService {

    // 省略部分代碼
    
    @Override
    public ExcelCheckResultVO checkImportExcel(List<ProjectInfoExcelDTO> datas){
        // 省略代碼
    }
}

輸出錯(cuò)誤報(bào)告
文件校驗(yàn)完成之后材彪,如果沒(méi)有完全通過(guò),需要將錯(cuò)誤對(duì)數(shù)據(jù)以及錯(cuò)誤信息通過(guò)easyExcel 輸出到客戶端琴儿。

@RestController
// 省略其他注解
public class ProjectInfoController {
    
    @Resource
    private AsyncExcelService asyncExcelService;
    
    @Resource
    private ProjectInfoService projectInfoService;

    /**
     * 項(xiàng)目信息導(dǎo)入
     */
    @PostMapping("/import")
    public R projectInfoImport(MultipartFile file,HttpServletResponse response){
        InputStream inputStream = null;
        int lastNum = 0;
        try {
            lastNum = EasyExcelUtils.lastNum(file.getInputStream());
        }catch(IOException e){
            // 省略部分代碼
        }
        if (lastNum <= 0 ){
            throw CustomExcetpoin(500,"導(dǎo)入文件數(shù)據(jù)為空段化,請(qǐng)重新上傳");
        }
        // 獲取系統(tǒng)配置的導(dǎo)入上限值
        Integer importMax = asyncExcelService.asyncProjectImportMax();
        if (lastNum > importMax ){
            // 達(dá)到上限,走異步
            asyncExcelService.asyncProjectImport(file,response);
            return R.success("數(shù)據(jù)導(dǎo)入成功造成,因數(shù)據(jù)量比較大显熏,已轉(zhuǎn)為異步導(dǎo)入");
        }
        // 省略部分代碼
        
        // 實(shí)例數(shù)據(jù)解析監(jiān)聽(tīng)器
        EasyExcelListener<ProjectInfoDTO> easyExcleListener = new EasyExcelListener(projectInfoService,ProjectInfoDTO.class);
        // 文件讀取/解析,并注冊(cè)監(jiān)聽(tīng)器
        EasyExcle.read(file.getInputStream(),ProjectInfoDTO.class,easyExcleListener).sheet(1).doRead();
        // 獲取錯(cuò)誤數(shù)據(jù)
        List<ExcelCheckErrDTO<ProjectInfoExcelDTO>> errList = easyExcleListener.getErrList();
        // 獲取解析成功到數(shù)據(jù)
        List<ProjectinfoExcelDTO> successList = easyExcleListener.getSuccessList();
        // 如果錯(cuò)誤數(shù)據(jù)不為空晒屎,將錯(cuò)誤數(shù)據(jù)寫入到excel文件喘蟆,并輸出到瀏覽器
        if (errList.size() > 0 ){
            // 省略部分代碼
        }        
        // 將成功到數(shù)據(jù),批量寫入到數(shù)據(jù)庫(kù)中
        // 省略代碼
        
        
        // 省略其他代碼
    }
}

異步導(dǎo)入

異步導(dǎo)入操作鼓鲁,將思考幾個(gè)問(wèn)題:

  • 導(dǎo)入文件存到什么地方蕴轨?當(dāng)一個(gè)同步請(qǐng)求結(jié)束之后,后續(xù)我們想再次拿到該請(qǐng)求到數(shù)據(jù)骇吭,我們應(yīng)該考慮將文件放到某一個(gè)單獨(dú)到地方橙弱,提供我們二次使用,比如:自己到文件服務(wù)器、oss 存儲(chǔ)等棘脐,這里我們使用自己的文件服務(wù)器斜筐。
  • 怎么異步執(zhí)行?我們可以使用新啟用一個(gè)本地線程去執(zhí)行我們的操作蛀缝,不影響當(dāng)前請(qǐng)求主線程的操作顷链,也是可以的,但是考慮到執(zhí)行重試問(wèn)題内斯,我們將使用(#xxl-job)分布式調(diào)度系統(tǒng)蕴潦,進(jìn)行調(diào)度執(zhí)行任務(wù)。
  • 客戶如何查看任務(wù)執(zhí)行狀態(tài)俘闯?我們需要提供一個(gè)任務(wù)執(zhí)行日志列表潭苞,讓用戶可以清晰的看到本次導(dǎo)出的任務(wù)是否執(zhí)行完成/是否存在導(dǎo)入錯(cuò)誤。
  • 怎么將錯(cuò)誤報(bào)告輸出給到客戶真朗?我們需要將導(dǎo)入到錯(cuò)誤報(bào)告文件(excel)上傳至文件服務(wù)器此疹,提供用戶二次或多次下載使用;同時(shí)遮婶,需要將文件信息保存至任務(wù)執(zhí)行日志信息中蝗碎,為用戶提供下載入口。

定義通用的job handler 父類 AsyncTaskHandler 旗扑,所有需要使用xxl-job 發(fā)起異步任務(wù)和給xxl-job 發(fā)起回調(diào)蹦骑,都需要繼承AsyncTaskHandler ,并實(shí)現(xiàn)execute 抽象方法臀防。

public abstract class AsyncTaskHandler <T extends AsyncTaskPramsDTO> {
    
    /** xxl-job server 端提供的創(chuàng)建任務(wù)接口 uri **/
    private final static String JOB_ADMIN_URI = "/outapi/asyn/";
    
    /** 與xxl-job server 通訊的加密密鑰對(duì) **/
    @Setter
    protected String publicKey;
    
    /**
     * xxl-job server 回調(diào)對(duì)方法 
     */
    public abstract ReturnT<String> execute(String params);
    
    /**
     * 向xxl-job 發(fā)起調(diào)度任務(wù) 
     */
    public JobResponseDTO sendTask(T prams){
        prams.setUser(null);
        
        // 省略部分代碼眠菇,相關(guān)內(nèi)容,請(qǐng)查詢xxl-job server 端所提供的接口文檔
        
        // 將 params 中的 user 對(duì)象保存至redis 中袱衷,xxl-job 接口有長(zhǎng)度限制
    }
    
    public abstract RedisUtil getRedisUtil();
    
    public abstract JobProperties getJobProperties();
    
    /** 回調(diào)方法名稱 **/
    public abstract String getHandlerName();
}

定義 AsyncTaskPramsDTO 異步參數(shù)實(shí)體

@Data
// 省略其他注解
public class AsyncTaskPramsDTO {
    
    private String requestId;
    
}

數(shù)據(jù)導(dǎo)出

數(shù)據(jù)導(dǎo)出功能常指捎废,客戶想將系統(tǒng)中的相關(guān)(按照查詢條件篩選)數(shù)據(jù)通過(guò)excel形式保存到自己本地。在數(shù)據(jù)導(dǎo)出過(guò)程中致燥,需要通過(guò)數(shù)據(jù)篩選條件將數(shù)據(jù)從系統(tǒng)數(shù)據(jù)庫(kù)中篩選出來(lái)登疗,然后通過(guò)一定格式(excel導(dǎo)出模版格式)寫入到excel中,最后輸出到客戶端(瀏覽器)提供客戶下載保存到本地嫌蚤。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載辐益,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末搬葬,一起剝皮案震驚了整個(gè)濱河市荷腊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌急凰,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抡锈,居然都是意外死亡疾忍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門床三,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)一罩,“玉大人,你說(shuō)我怎么就攤上這事撇簿∧粼ǎ” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵四瘫,是天一觀的道長(zhǎng)汉嗽。 經(jīng)常有香客問(wèn)我,道長(zhǎng)找蜜,這世上最難降的妖魔是什么饼暑? 我笑而不...
    開(kāi)封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮洗做,結(jié)果婚禮上弓叛,老公的妹妹穿的比我還像新娘。我一直安慰自己诚纸,他們只是感情好撰筷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著畦徘,像睡著了一般毕籽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旧烧,一...
    開(kāi)封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天影钉,我揣著相機(jī)與錄音,去河邊找鬼掘剪。 笑死平委,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的夺谁。 我是一名探鬼主播廉赔,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼匾鸥!你這毒婦竟也來(lái)了蜡塌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤勿负,失蹤者是張志新(化名)和其女友劉穎馏艾,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琅摩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年铁孵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片房资。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜕劝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轰异,到底是詐尸還是另有隱情岖沛,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布搭独,位于F島的核電站婴削,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏戳稽。R本人自食惡果不足惜馆蠕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望惊奇。 院中可真熱鬧互躬,春花似錦、人聲如沸颂郎。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乓序。三九已至寺酪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間替劈,已是汗流浹背寄雀。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留陨献,地道東北人盒犹。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像眨业,于是被迫代替她去往敵國(guó)和親急膀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容