提问一:SpringBoot为什么要将导出功能从业务服务抽取出来?
-
分离关注点 (Separation of Concerns):导出功能通常涉及文件生成、格式处理、数据提取等与业务逻辑不太相关的操作。通过将导出功能从业务服务中分离出来,可以将关注点分离,使代码更加清晰和可维护。这样,每个部分都可以专注于自己的任务,而不会相互干扰。
-
可重用性 (Reusability):将导出功能抽取成独立的组件或服务可以增加其可重用性。您可以在不同的业务流程或模块中重复使用相同的导出功能,而无需重复编写代码。这样可以提高开发效率并减少代码重复。
-
测试简化 (Simplified Testing):导出功能的独立性使得测试变得更容易。您可以编写针对导出功能的单元测试,而不必考虑整个业务流程的复杂性。这有助于更快地发现和修复问题。
-
团队协作 (Collaboration):在大型项目中,不同的团队或开发者可能负责不同的功能模块。通过将导出功能抽取出来,可以更容易地协作,因为每个团队可以独立开发和维护其自己的导出模块。
-
性能优化 (Performance Optimization):导出功能可能需要针对大量数据进行优化,例如分页、异步处理等。将导出功能与业务逻辑分开使得在性能优化方面更加灵活,可以针对导出功能的需求进行特定的优化。
总之,将导出功能从业务服务中抽取出来有助于提高代码的可维护性、可重用性和可测试性,同时也有助于更好地组织和管理项目代码,使团队协作更加高效。最重要的是在导出大数据量的时候即使导出服务OOM了,也不影响业务服务的正常使用。
提问二:那我们应该从哪几方面思考,如何实现这通用导出服务?
-
输入和配置:
- 定义导出的数据源,例如数据库表、REST API端点、文件等。
- 允许用户或应用程序指定导出的数据范围、筛选条件、排序方式等。
- 支持配置导出的格式,例如CSV、Excel、PDF等。
-
数据提取:
- 根据配置从数据源中提取数据。这可能涉及到数据库查询、HTTP请求、文件读取等操作。
- 考虑性能优化,例如分页查询、缓存、异步数据提取等。
-
数据转换和格式化:
- 将提取的数据转换成指定的导出格式。这包括数据的映射、日期格式化、数值格式化等。
- 对于不同的导出格式,可能需要不同的转换逻辑。
-
文件生成:
- 生成导出文件,例如CSV文件、Excel文档、PDF文档等。
- 考虑文件命名规则和存储位置。
-
错误处理:
- 处理可能的错误情况,例如数据提取失败、格式转换错误、文件生成问题等。
- 提供错误日志和适当的错误信息以供排查问题。
-
安全性:
- 考虑数据的安全性,确保只有授权的用户或应用程序可以执行导出操作。
- 防止潜在的安全漏洞,例如SQL注入、文件包含漏洞等。
-
性能优化:
- 考虑导出操作的性能,特别是当处理大量数据时。可以使用分批处理、缓存等技术来提高性能。
- 考虑导出任务的异步执行,以避免阻塞应用程序的主线程。
-
扩展性:
- 设计通用导出服务以支持不同的数据源和导出格式。
- 考虑插件或扩展机制,使其他开发者可以轻松地添加新的导出格式或数据源。
-
文档和用户界面:
- 提供清晰的文档,解释如何配置和使用导出服务。
- 可以考虑提供一个用户界面,让非技术用户能够方便地配置和执行导出任务。
-
监控和日志:
- 添加监控机制,以便跟踪导出任务的执行情况和性能指标。
- 记录详细的日志,以便在发生问题时进行故障排除。
-
测试:
- 编写单元测试和集成测试,确保导出服务的各个组件正常工作。
- 进行性能测试,以评估导出服务的性能和可伸缩性。
-
部署和维护:
- 部署导出服务到合适的环境,并考虑自动化部署和持续集成/持续交付(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);
}
}