后端思想-如何抽取业务组件

前言

继前文组件开发指北之后,今天想聊聊如何抽取业务组件。我相信工作时间长了之后,大家都会有自己独到的偷懒小工具,身边也有同事分享过自己的工具类啥的,但是少有抽成组件的。无他,大部分情况下组件的限制过大了,适用范围狭窄,反而不如工具类型。或者换一种说法,当你的工作环境中没有统一的开发规范,抽取业务组件就近乎登天之路。因此要抽取业务组件,第一件事就是统一代码规范,让大家尽量风格一致,这样才能在基础之上去抽取组件,提升总体的开发效率。

我现在提供了四种组件,分别是tool-box(详情见组件开发指北),business-common(业务组件),sso-zero(单点登录组件),log-transfer(日志收集组件),工具类和日志的前两篇文章已经讲解过了,单点登录的话后面有机会再说。

业务场景

统一controller层风格+全局异常处理

美化Controller层

美化后效果,需要统一返回值并且配置全局异常监听

统一返回值

有一个类似于RemoteResult的类,包含状态码,消息,返回值,如果你有更多的内容需要输出那就扩展这个类

全局异常监听

简易版的会用到三个类

异常类ToolException,标准异常,错误码,错误信息。

抽象异常类AbstractException,这个类的主要作用是提供一个异常的架子,方便扩展,如果没啥需求,可以不用这个,只提供普通异常类就行

全局异常监听类ToolExceptionHandler,在这个类里面去监听不同的错误,根据不同的错误来进行对应的处理

放置公用类或者常量等

比如我之前做了一个门户网站,门户网站里面需要统一展示所有子系统的待办,待办的信息是子系统通过接口来进行增删改查操作。为了避免各个地方的入参和返回值的类对不上,就需要单独抽出来放在组件里面,随着组件的更新而更新,当然这种情况下为了兼容最好都加上类似@JsonIgnoreProperties(ignoreUnknown = true)的注解。

导入导出

导入导出这块我是集成的阿里的EasyExcel,将读写和性能上的问题交给成熟的中间件去处理,在此基础上对自己的业务进行封装。封装的话,主要是围绕与门户网站以及文件服务器的交互,除此之外,还针对具体的场景进行了优化,提供更便捷以及人性化的操作。

通用监听器

通用的监听器分成两种,一种是固定头的,一种是动态头读取,动态头的采用EasyExcel官方提供的监听类,暂时没有做什么修改。固定头的我是取巧做了一个通用的,官方原版是需要写泛型的,相当于一个导入类一个监听器。我的思路是抹掉泛型,数据用Object接收,最后读取后再强转为固定的类。

通用监听器除了提供读取的功能还针对以下场景做了优化

  • 将读取数据时的类型转换异常封装到字符串里,方便做异常信息提醒

  • 读取头信息并封装

PS:这里其实读取超大容量的excel可能会报OOM之类的问题,为了避免出现问题,可以在监听类中分批处理消息

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.exception.ExcelDataConvertException;
import com.alibaba.fastjson.JSON;
import com.ruijie.common.pojo.excel.ExcelAnalyzeResDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/***
 * @Author WangZY
 * @Date 2020/3/27 15:56
 * @Description 通用读取excel数据
 */
@Slf4j
public class ExcelListener extends AnalysisEventListener {
    private List<Object> dataList = new ArrayList<>();
    private List<String> headList = new ArrayList<>();
    private StringBuilder dateError = new StringBuilder();

    @Override
    public void invoke(Object data, AnalysisContext context) {
        Integer curRowNum = context.readRowHolder().getRowIndex();
        String json = JSON.toJSONString(data);
        if (!StringUtils.isEmpty(json) && !"{}".equals(json)) {
            log.info("解析第{}行数据:{}", curRowNum + 1, json);
            dataList.add(data);
        }
    }

    @Override
    public void onException(Exception exception, AnalysisContext context) {
        log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
        // 如果是某一个单元格的转换异常 能获取到具体行号
        // 如果要获取头的信息 配合invokeHeadMap使用
        Integer curRowNum = context.readRowHolder().getRowIndex();
        if (curRowNum > 2) {
            if (exception instanceof ExcelDataConvertException) {
                ExcelDataConvertException convertEx = (ExcelDataConvertException) exception;
                log.error("第{}行,第{}列解析异常,数据为:{}", convertEx.getRowIndex(),
                        convertEx.getColumnIndex(), convertEx.getCellData());
                if (convertEx.getMessage().contains("Integer") || convertEx.getMessage().contains("BigDecimal")) {
                    String errStr = subErrStr(convertEx);
                    dateError.append("[第").append(convertEx.getRowIndex()).append("行,第")
                            .append(convertEx.getColumnIndex()).append("列,数据")
                            .append(StringUtils.isEmpty(errStr) ? "" : errStr)
                            .append("不能解析为数字]");
                } else if (convertEx.getMessage().contains("Date")) {
                    String errStr = subErrStr(convertEx);
                    dateError.append("[第").append(convertEx.getRowIndex()).append("行,第")
                            .append(convertEx.getColumnIndex()).append("列,数据").append(errStr)
                            .append("不能解析为日期]");
                } else {
                    dateError.append("[第").append(convertEx.getRowIndex()).append("行,第")
                            .append(convertEx.getColumnIndex()).append("列,数据").append("不能解析]");
                }
            }
        }
    }

    private String subErrStr(ExcelDataConvertException convertEx) {
        String errStr = "";
        String message = convertEx.getCause().getMessage();
        int i = message.indexOf(":");
        int j = message.indexOf(":");
        if (i > 0) {
            errStr = message.substring(i);
        }
        if (j > 0) {
            errStr = message.substring(j);
        }
        return errStr;
    }

    @Override
    public void invokeHeadMap(Map headMap, AnalysisContext context) {
        Collection values = headMap.values();
        headList.addAll(values);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("所有数据解析完成!");
    }

    public ExcelAnalyzeResDTO getExcelData() {
        return new ExcelAnalyzeResDTO(dataList, dateError.toString(), headList);
    }
}

通用读取方法

读取的话,一般分为MultipartFile和普通文件,用以下两种即可。然后我们的业务场景呢,会对表头有校验,我这里思路就是去找easy excel的字段注解,去拿对应的实体类上的表头信息,用这个与读取的表头信息做对比。除此之外我还封装了类型转换异常,针对常见的日期和数字做了更友好的提示。

读取MultipartFile需转换成InputStream
public List<?> readMultipartFile(MultipartFile mFile, Class<?> clazz, boolean headCheck) {
    ExcelListener excelListener = new ExcelListener();
    InputStream inputStream = null;
    try {
        inputStream = mFile.getInputStream();
    } catch (IOException e) {
        log.error("获取文件流失败", e);
        throw new PtmException("读取文件失败");
    }
    EasyExcel.read(inputStream, clazz, excelListener).sheet().doRead();
    .....
}
public List<?> readFile(String fileName, Class<?> clazz, boolean headCheck) {
    ExcelListener excelListener = new ExcelListener();
    EasyExcel.read(fileName, clazz, excelListener).sheet().doRead();
    ExcelAnalyzeResDTO excelData = excelListener.getExcelData();
    //校验表头是否正确
    if (headCheck) {
        checkHeadRight(clazz, excelData);
    }
    //判断是否有类型转换异常
    String dateError = excelData.getDateError();
    if (!StringUtils.isEmpty(dateError)) {
        throw new PtmException(dateError);
    } else {
        return excelData.getExcelDataList();
    }
}
private void checkHeadRight(Class<?> clazz, ExcelAnalyzeResDTO excelData) {
    Field[] fields = clazz.getDeclaredFields();
    List<String> realHeadList = new ArrayList<>();
    for (Field field : fields) {
        field.setAccessible(true);
        ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
        if (excelProperty != null) {
            String[] value = excelProperty.value();
            realHeadList.addAll(Arrays.asList(value));
        }
    }
    List<String> headList = excelData.getHeadList();
    String collect = headList.stream().filter(e -> !realHeadList.contains(e))
            .collect(Collectors.joining(","));
    if (!StringUtils.isEmpty(collect)) {
        throw new PtmException("模板异常,请确认以下表头是否填写错误=" + collect);
    }
}

根据文件名创建文件

内置一个文件创建类,主要方便导出时候创建文件用

public File assemblyFile(String filePath, String fileName) {
    if (StringUtils.isEmpty(filePath)) {
        filePath = commonProperties.getFileDir();
    }
    // 先判断文件夹是否存在,避免不存在时报错
    File filePathExist = new File(filePath);
    if (!filePathExist.exists()) {
        boolean mkdirs = filePathExist.mkdirs();
        if (!mkdirs) {
            return null;
        }
    }
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日HHmmss");
    String format = LocalDateTime.now().format(dtf);
    if (fileName.contains(".")) {
        String[] split = fileName.split("\\.");
        if (split.length > 2) {
            throw new PtmException("请注意文件格式,不允许文件名中存在多个小数点");
        }
        return new File(filePath + split[0] + format + "." + split[1]);
    } else {
        // 默认导出xlsx格式
        return new File(filePath + fileName + ".xlsx");
    }
}

分页导出文件

解决xls,也就是老版excel单sheet不能超过65536行的问题,这里做了分页输出

private void creatExportFile(File file, Class<?> clazz, List<?> data) {
    int sheetSize = 60000;
    if (file.getName().endsWith("xls") && data.size() > sheetSize) {
        ExcelWriter excelWriter = null;
        try {
            excelWriter = EasyExcel.write(file, clazz).build();
            for (int i = 0; i < (data.size() / sheetSize) + 1; i++) {
                int start = i * sheetSize;
                int end = (i + 1) * sheetSize;
                List<?> subList;
                if (data.size() < end) {
                    subList = data.subList(start, data.size());
                } else {
                    subList = data.subList(start, end);
                }
                WriteSheet writeSheet = EasyExcel.writerSheet(i, "数据" + i).build();
                excelWriter.write(subList, writeSheet);

            }
        } finally {
            // 千万别忘记finish 会帮忙关闭流
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    } else {
        EasyExcel.write(file, clazz).sheet(file.getName()).doWrite(data);
    }
}

代码实现

导入案例

导入步骤

1.调用commonImportExcel方法读取并传递文件到PTM2.0(这一步必须放在外面,是对excel的基本校验,有错误及时推送前端,不能异步)

2.开启异步,使用readFile或者readMultipartFile解析文件,并进行业务处理

3.handle处理部分,调用finishFileStatus方法回传PTM2.0状态

可选操作

1.identifier标识可使用LuaTool生成,例如String generateOrder = luaTool.generateOrder("SMB-PRODUCT-");

导出案例

导出步骤

1.自定义文件名称,并调用createPtmFile(此刻fileId和fileName可以乱写,后续被覆盖)

2.开启异步,进行业务处理,调用asyncExportExcel方法导出,注意return返回的fileid

3.handle处理部分,调用finishFileStatus方法回传PTM2.0状态

为啥用completablefutrue不用async?--如有性能问题,请自行创建线程池配置,默认的线程池采用无限队列

推荐completablefutrue的原因是使用比async方便,async在spring中通过切面实现,使用时需要注意不能内部调用,方法pubic修饰,类被spring管理等使切面失效的问题。

写在最后

业务组件这一篇,篇幅比较短。原因呢,自然是我接触的场景还是比较少,因为就如我前面所说,业务组件都是基于通用业务场景的代码封装,需要较高的代码规范。如何在工作中发现并抽取业务组件,这才是困难的第一步,我的工作场景能抽出组件来的基本是excel的导入导出,这一块封装了门户,文件服务器以及easy excel的操作,将原本繁杂的代码缩短到了几十行,极端情况下甚至可以十几行解决一个导入或者导出,这都是对工作效率的提升。当然推广起来也是比较麻烦,为了应对业务的需求和变动,组件需要不断的更新,同事用起来有问题就得找你,其实也侧面反映了一旦依赖上组件,就逃不开了,哈哈。

上一篇关于日志收集的反响不错,下一篇的思路我也想好了,既然这一篇是关于业务组件,那么下一篇就是关于具体开发场景的套路介绍。具体是并行批量插入数据库,并行处理数据,缓存应用等等实际场景的套路代码。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值