基于POI创建通用导出服务(SpringBoot+MyBatisPlus版)

提问一:SpringBoot为什么要将导出功能从业务服务抽取出来?

  1. 分离关注点 (Separation of Concerns):导出功能通常涉及文件生成、格式处理、数据提取等与业务逻辑不太相关的操作。通过将导出功能从业务服务中分离出来,可以将关注点分离,使代码更加清晰和可维护。这样,每个部分都可以专注于自己的任务,而不会相互干扰。

  2. 可重用性 (Reusability):将导出功能抽取成独立的组件或服务可以增加其可重用性。您可以在不同的业务流程或模块中重复使用相同的导出功能,而无需重复编写代码。这样可以提高开发效率并减少代码重复。

  3. 测试简化 (Simplified Testing):导出功能的独立性使得测试变得更容易。您可以编写针对导出功能的单元测试,而不必考虑整个业务流程的复杂性。这有助于更快地发现和修复问题。

  4. 团队协作 (Collaboration):在大型项目中,不同的团队或开发者可能负责不同的功能模块。通过将导出功能抽取出来,可以更容易地协作,因为每个团队可以独立开发和维护其自己的导出模块。

  5. 性能优化 (Performance Optimization):导出功能可能需要针对大量数据进行优化,例如分页、异步处理等。将导出功能与业务逻辑分开使得在性能优化方面更加灵活,可以针对导出功能的需求进行特定的优化。

总之,将导出功能从业务服务中抽取出来有助于提高代码的可维护性、可重用性和可测试性,同时也有助于更好地组织和管理项目代码,使团队协作更加高效。最重要的是在导出大数据量的时候即使导出服务OOM了,也不影响业务服务的正常使用

提问二:那我们应该从哪几方面思考,如何实现这通用导出服务?

  1. 输入和配置

    • 定义导出的数据源,例如数据库表、REST API端点、文件等。
    • 允许用户或应用程序指定导出的数据范围、筛选条件、排序方式等。
    • 支持配置导出的格式,例如CSV、Excel、PDF等。
  2. 数据提取

    • 根据配置从数据源中提取数据。这可能涉及到数据库查询、HTTP请求、文件读取等操作。
    • 考虑性能优化,例如分页查询、缓存、异步数据提取等。
  3. 数据转换和格式化

    • 将提取的数据转换成指定的导出格式。这包括数据的映射、日期格式化、数值格式化等。
    • 对于不同的导出格式,可能需要不同的转换逻辑。
  4. 文件生成

    • 生成导出文件,例如CSV文件、Excel文档、PDF文档等。
    • 考虑文件命名规则和存储位置。
  5. 错误处理

    • 处理可能的错误情况,例如数据提取失败、格式转换错误、文件生成问题等。
    • 提供错误日志和适当的错误信息以供排查问题。
  6. 安全性

    • 考虑数据的安全性,确保只有授权的用户或应用程序可以执行导出操作。
    • 防止潜在的安全漏洞,例如SQL注入、文件包含漏洞等。
  7. 性能优化

    • 考虑导出操作的性能,特别是当处理大量数据时。可以使用分批处理、缓存等技术来提高性能。
    • 考虑导出任务的异步执行,以避免阻塞应用程序的主线程。
  8. 扩展性

    • 设计通用导出服务以支持不同的数据源和导出格式。
    • 考虑插件或扩展机制,使其他开发者可以轻松地添加新的导出格式或数据源。
  9. 文档和用户界面

    • 提供清晰的文档,解释如何配置和使用导出服务。
    • 可以考虑提供一个用户界面,让非技术用户能够方便地配置和执行导出任务。
  10. 监控和日志

    • 添加监控机制,以便跟踪导出任务的执行情况和性能指标。
    • 记录详细的日志,以便在发生问题时进行故障排除。
  11. 测试

    • 编写单元测试和集成测试,确保导出服务的各个组件正常工作。
    • 进行性能测试,以评估导出服务的性能和可伸缩性。
  12. 部署和维护

    • 部署导出服务到合适的环境,并考虑自动化部署和持续集成/持续交付(CI/CD)。
    • 定期更新和维护导出服务,以适应新的需求和技术变化。

最后,通用导出服务的设计应该考虑您的具体需求和应用场景。它应该能够适应不同的导出需求,并提供足够的灵活性和可扩展性,以便随着项目的发展进行调整和扩展。同时,确保服务的可靠性和安全性是非常重要的。

结合以上问题的思考,直接上开发整理好的Demo  

1、介绍项目目录(随便创建一个:SpringBoot+MyBatisPlus的项目)

 2、application.yml文件

server:
  port: 9800

spring: #springboot的配置
  datasource: #定义数据源
    #127.0.0.1为本机测试的ip,3306是mysql的端口号。serverTimezone是定义时区,照抄就好,mysql高版本需要定义这些东西
    #useSSL也是某些高版本mysql需要问有没有用SSL连接
    url: jdbc:mysql://127.0.0.1:3306/rbgt?serverTimezone=GMT%2B8&useSSL=FALSE
    username: root  #数据库用户名,root为管理员
    password: 123456 #该数据库用户的密码

# mybatis-plus相关配置
mybatis-plus:
  # 搜索指定包别名
  typeAliasesPackage: com.ycw.excel
  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
  mapper-locations: classpath:mapper/excel/*.xml
  # 以下配置均有默认值,可以不设置
  global-config:
    db-config:
      #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: auto
      #字段策略 IGNORED:"忽略判断"  NOT_NULL:"非 NULL 判断")  NOT_EMPTY:"非空判断"
      field-strategy: NOT_EMPTY
      #数据库类型
      db-type: MYSQL
    enable-sql-runner: true
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 如果查询结果中包含空值的列,则 MyBatis 在映射的时候,不会映射这个字段
    call-setters-on-nulls: true
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3、pom文件信息

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- XLS(03) -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.9</version>
        </dependency>

        <!-- XLSX(07) -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.9</version>
        </dependency>

        <!-- time -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
            <version>2.10.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--mybatisplus的包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--generator需要的包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
    </dependencies>

4、OrderEntity实体及DDL脚本

package com.ycw.excel.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

/**
 * @ClassName OrderEntity
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 下午 02:46:19
 * @Version 1.0
 */
@Data
@TableName("rb_order")
public class OrderEntity {

    private String id;

    private String orderName;

    private String orderPrice;

    private String trIntroduce;

    private String customerName;

    private String deliveryAddress;

    private Integer numberXh;
}
-- rbgt.rb_order definition

CREATE TABLE `rb_order` (
  `id` varchar(100) NOT NULL COMMENT '主键',
  `order_name` varchar(100) DEFAULT NULL COMMENT '订单名称',
  `order_price` varchar(100) DEFAULT NULL COMMENT '订单价格',
  `tr_introduce` varchar(100) DEFAULT NULL COMMENT '事物描述',
  `customer_name` varchar(100) DEFAULT NULL COMMENT '客户名称',
  `delivery_address` varchar(100) DEFAULT NULL COMMENT '派送地址',
  `number_xh` int(11) DEFAULT NULL COMMENT '订单序号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5、OrderEntityMapper接口类

package com.ycw.excel.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ycw.excel.entity.OrderEntity;
import org.apache.ibatis.annotations.Mapper;

/**
 * @ClassName OrderEntityMapper
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 下午 02:48:37
 * @Version 1.0
 */
@Mapper
public interface OrderEntityMapper extends BaseMapper<OrderEntity> {
}

6、ExportTitleParam、ExportParam类

package com.ycw.excel.model;

import lombok.Data;

import java.util.List;

/**
 * @ClassName ExportParam
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 下午 02:14:58
 * @Version 1.0
 */
@Data
public class ExportParam {

    /**
     * 文件名称:订单名称.xlsx
     **/
    private String exlFileName;

    /**
     * 文件Sheet名称:20230816
     **/
    private String exlSheetName;

    /**
     * 对应映射的内容
     **/
    private List<ExportTitleParam> exportTitleParams;

    /**
     * 导出执行的SQL
     **/
    private String exportDateSql;

    /**
     * 查询条件的SQL
     **/
    private String exportDateCountSql;
}

package com.ycw.excel.model;

import lombok.Data;

/**
 * @ClassName ExportParam
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 下午 02:14:58
 * @Version 1.0
 */
@Data
public class ExportTitleParam {

    /**
     * 表头描述:订单名称
     **/
    private String exlTitleValue;

    /**
     * 映射数据表字段:order_name
     **/
    private String exlTitleKey;
}

7、关键基础数据处理类:ExportDataBizService
 

package com.ycw.excel.data;

import com.baomidou.mybatisplus.extension.toolkit.SqlRunner;
import com.ycw.excel.entity.OrderEntity;
import com.ycw.excel.mapper.OrderEntityMapper;
import com.ycw.excel.model.ExportParam;
import com.ycw.excel.model.ExportTitleParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * @ClassName ExportDataBizService
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 上午 11:28:14
 * @Version 1.0
 */
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ExportDataBizService {

    private final static String url = "E:\\RBGT\\spring-boot\\SpringBoot-Poi-EasyExcel\\";
    private final static Integer offset = 1000;
    private final OrderEntityMapper orderEntityMapper;

    /**
     * 查询 - 对应数据列表
     *
     * @param
     * @return java.util.List<java.util.Map < java.lang.String, java.lang.Object>>
     * @Author 俞春旺
     * @Date 上午 11:37:21 2023年8月19日 0019
     **/
    public void executeCustomSql(ExportParam exportParam) throws IOException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 获取 - 分页数据信息
        long count = SqlRunner.db().selectCount(exportParam.getExportDateCountSql());
        log.info("数据总数_count : {}", count);
        Integer page = this.getPage(Integer.parseInt(count + ""));
        log.info("分页大小_page : {}", page);

        // 创建一个新的工作簿
        Workbook workbook = new XSSFWorkbook();
        // 创建一个工作表
        Sheet sheet = workbook.createSheet(exportParam.getExlSheetName());

        // 创建标题行
        Row headerRow = sheet.createRow(0);
        for (int i = 0; i < exportParam.getExportTitleParams().size(); i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(exportParam.getExportTitleParams().get(i).getExlTitleValue());
        }

        if (page == 0) {
            List<Map<String, Object>> result = SqlRunner.db().selectList(exportParam.getExportDateSql() + " limit 0,1000");
            // 填充数据行
            // 填充数据行
            assemblyData(0, offset, sheet, result, exportParam.getExportTitleParams());
        } else {
            for (Integer i = 0; i < page; i++) {
                String sql = exportParam.getExportDateSql() + " limit " + i * 1000 + ",1000";
                log.info("执行的sql : {}", sql);
                List<Map<String, Object>> result = SqlRunner.db().selectList(sql);
                // 填充数据行
                assemblyData(i, offset, sheet, result, exportParam.getExportTitleParams());
                // 清空 - 操作里面的数据
                result.clear();
            }
        }

        // 保存工作簿到文件
        FileOutputStream fileOut = new FileOutputStream(url + exportParam.getExlFileName());
        workbook.write(fileOut);
        fileOut.close();
        //执行业务等
        stopWatch.stop();
        log.info("导出花费的时间 -> time : {} 毫秒", stopWatch.getTotalTimeMillis());
    }

    /**
     * 组装数据
     *
     * @param i
     * @param offset
     * @param sheet
     * @param result
     * @param exportTitleParams
     * @return void
     * @Author 俞春旺
     * @Date 下午 03:34:13 2023年8月19日 0019
     **/
    private void assemblyData(Integer i, Integer offset, Sheet sheet, List<Map<String, Object>> result, List<ExportTitleParam> exportTitleParams) {
        // 填充数据行 1001
        for (int rowNum = 1 + (i * offset); rowNum <= result.size() + (i * offset); rowNum++) {
            // 创建 - 每一行数据
            Row row = sheet.createRow(rowNum);
            // 获取 - 每一行数据
            Map<String, Object> stringObjectMap = result.get((rowNum - (i * offset)) - 1);
            for (int iv = 0; iv < exportTitleParams.size(); iv++) {
                row.createCell(iv).setCellValue(stringObjectMap.get(exportTitleParams.get(iv).getExlTitleKey()) + "");
            }
        }
    }

    /**
     * 获取 - 分页数据
     *
     * @param dataCount
     * @return java.lang.Integer
     * @Author 俞春旺
     * @Date 下午 03:22:43 2023年8月19日 0019
     **/
    private Integer getPage(Integer dataCount) {
        if (dataCount > offset) {
            if (dataCount % offset > 0) {
                return dataCount / offset + 1;
            } else {
                return dataCount / offset;
            }
        } else {
            return 0;
        }
    }

    /**
     * 造数平台
     *
     * @param
     * @return void
     * @Author 俞春旺
     * @Date 下午 02:55:21 2023年8月19日 0019
     **/
    public void manufactureDate(Integer lx) {
        for (int i = 0; i < lx; i++) {
            OrderEntity orderEntity = new OrderEntity();
            orderEntity.setId(UUID.randomUUID().toString().replace("-", ""));
            orderEntity.setOrderName("酸菜鱼小鱼号");
            orderEntity.setOrderPrice("0.38");
            orderEntity.setTrIntroduce("这是一个多吗令人振奋的事情");
            orderEntity.setCustomerName("张惠妹");
            orderEntity.setDeliveryAddress("福建省厦门市集美区");
            orderEntity.setNumberXh(i + 1);
            int insert = orderEntityMapper.insert(orderEntity);
            if (insert > 0) {
                System.out.println("插入数据成功 -> i :" + i);
            }
        }
    }
}

8、ExportDataController控制类

package com.ycw.excel.controller;

import com.ycw.excel.data.ExportDataBizService;
import com.ycw.excel.model.ExportParam;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

/**
 * @ClassName ExportDataController
 * @Description TODO
 * @Author 俞春旺
 * @Company RBGT
 * @Date 2023年8月19日 0019 上午 11:38:01
 * @Version 1.0
 */
@RestController
@RequestMapping("/export")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ExportDataController {

    private final ExportDataBizService exportDataBizService;

    @PostMapping("/send")
    public void exportData(@RequestBody ExportParam exportParam) throws IOException {
        // 创建 - 生成文件
        exportDataBizService.executeCustomSql(exportParam);
    }

    @PutMapping("/manufactureDate")
    public void manufactureDate(@RequestParam("lx") Integer lx) {
        // 创建 - 生成文件
        exportDataBizService.manufactureDate(lx);
    }
}

9、效果演示截图

项目启动成功截图
 项目正常访问通用导出接口
 项目导出过程日志信息
文件导出内容信息 

 总结:该通用导出服务,可以结合任务的创建及对导出策略做一系列设计(比如:分页导出、一次性导出、时间段导出等),让导出服务更贴合自己的业务走向。

PS:后续会结合该Demo继续深化,将通用导出服务已任务的形式呈现及使用,继续加油。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值