📚专栏
「Java数据集成」专栏
- 《Java发起HTTP请求并解析JSON返回数据》:下图简称为《请求和解析》
- 《基于MyBatis实现依次、批量、分页增删改查操作》:下图简称为《依批分增删改查》
- 《用Python根据JSON生成Java类代码和数据库建表SQL语句》:下图简称为《生成代码脚本》
- 《基于SpringBoot+MyBatis的数据增删改查模板》:下图简称为《增删改查模板》
- 《Java发起同异步HTTP请求和处理数据》:下图简称为《同异步请求和处理》
- 《基于SpringBoot+MyBatis的数据集成模板》:下图简称为《数据集成模板》
- 《JavaHTTP请求工具类HTTPUtils》:下图简称为《HTTP请求工具类》
- 《JavaJSON处理工具类JSONUtils》:下图简称为《JSON处理工具类》
- 《JavaXML处理工具类XMLUtils》:下图简称为《XML处理工具类》
- 《用Python生成随机JSON数据》:下图简称为《生成随机数据脚本》
💬相关
本文涉及的模板代码已放在 Git 仓库,供学习交流(下面二选一,都一样)
https://gitee.com/dreature1328/springboot-mybatis-integrate-template
https://github.com/dreature1328/springboot-mybatis-integrate-template
由于作者最近频繁在集成数据,因而基于 Spring Boot + MyBatis 写了两套模板:数据增删改查模板和数据集成模板,辅之以两篇博客文章作为姊妹篇进行说明,前者可以说是后者的基础。
💬相关
博客文章《基于Spring Boot + MyBatis的数据增删改查模板》
https://blog.csdn.net/weixin_42077074/article/details/128868655
博客文章《基于Spring Boot + MyBatis的数据集成模板》
https://blog.csdn.net/weixin_42077074/article/details/129802650
数据集成指的是将不同数据源的数据进行整合、转换和加载到目标库的过程
本文涉及数据集成过程中的常规方法
数据集成场景
对于 Java 中这样一个数据集成过程
- 接入数据
- 查询数据表获取数据:基于 MyBatis 映射 Java 到 SQL 实现
- 发起 HTTP 请求并获取接口数据:借助
HttpURLConnection
发送请求
- 加工数据:按自己的需求自己实现,如用阿里巴巴的 Fastjson 包解析响应内容中的 JSON 形式的字符串
- 写入数据:基于 MyBatis 映射 Java 到 SQL 实现,如插入或更新进数据库
这些过程的实现都在往期文章中有详细介绍
后文将以发起 HTTP 请求的方式接入数据为例,而查询数据表的方式不再赘述,查看往期文章即可
💬相关
请求并获取接口数据和加工数据过程请查看
博客文章《Java发起HTTP请求并解析JSON返回数据》,下文的请求函数
requestHTTPContent()
出自于此https://blog.csdn.net/weixin_42077074/article/details/128672130
查询数据库,插入或更新数据进数据库过程请查看
博客文章《基于Spring Boot + MyBatis的数据增删改查模板》,下文的
dataMapper
的 Mapper 层函数均出自于此https://blog.csdn.net/weixin_42077074/article/details/128868655
假如我们是以发起 HTTP 请求的方式接入数据,对于请求繁多,数据量巨大的情况,我们可以怎么优化来提高效率?
- 依次同步请求 → 批量异步请求 → 分页异步请求
- 传统加工数据 → 流水线加工数据
- 依次插入或更新数据 → 批量插入或更新数据 → 分页插入或更新数据
优化的方法也在往期文章中有详细介绍
💬相关
同步和异步请求过程请查看
博客文章《Java发起同异步HTTP请求和处理数据和处理数据》,下文的异步请求函数
asyncHTTPRequest()
出自于此https://blog.csdn.net/weixin_42077074/article/details/129601132
依次、批量、分页处理数据过程请查看
博客文章《基于Spring Boot + MyBatis的数据增删改查模板》
https://blog.csdn.net/weixin_42077074/article/details/128868655
流水线处理数据过程则查看下文
那么本文就对其中两种组合进行介绍
- 集成数据:依次同步请求 + 传统加工数据 + 依次插入或更新数据
- 优化集成数据:分页异步请求 + 流水线加工数据 + 分页插入或更新数据
如果数据量大到无法想象,我们还可以在最外层再进行一次分页,可以说是二重分页:第一重是分页集成数据,第二重是分页异步请求/分页插入或更新数据
- 优化集成数据 → 分页优化集成数据
以下给出一个将接口数据定期写入进数据库的示例场景
- 需向接口发起 HTTP 请求,接口返回的 JSON 数据含有键
id
、key1
、key2
,如
{
"code":"200",
"msg":"success",
"data":[
{"id":"000001","key1":"5WoFrZxFR5ZXi6tA","key2":"0afba4s6HATkE9N4"},
{"id":"000002","key1":"aKeHAyL10oGXYcB1","key2":"cG5SlzRavO2zMLkW"},
{"id":"000003","key1":"O7zdMpEilsatFHRo","key2":"rKsqN0nOfU06vQ8E"},
{"id":"000004","key1":"xD6s7KlaUQ9zY5pR","key2":"8oe1RTbDu8gH30Fn"},
{"id":"000005","key1":"lkpnmv47rybG3hw2","key2":"rht3MhVvDOuaB9cQ"}
]
}
- 在数据库
data_base
建立数据表data_table
,含有字段id
、field1
、field2
- 基于 Spring Boot + MyBatis 实现,采用多层架构(Controller 层、Service 层、Mapper层)
- 建立 Java 类
Data
,含有属性id
、attr1
、attr2
- 将接口数据插入或更新进数据库,对应 SQL 的
INSERT INTO ... ON DUPLICATE KEY UPDATE ...
语句 - 定期或定时执行任务
建立数据表
DROP TABLE IF EXISTS `data_table`;
CREATE TABLE `data_table` (
`id` VARCHAR(255) NOT NULL PRIMARY KEY,
`field1` VARCHAR(255) DEFAULT NULL,
`field2` VARCHAR(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
数据库配置
配置文件 application.properties
常见配置
serverTimezone
:时区,如亚洲/上海时区Asia/Shanghai
useUnicode
和characterEncoding
:编码方式,如true
和utf8
allowMultiQueries
:是否支持”;"号分隔的多条 SQL 语句的执行,如true
autoReconnect
:是否超时重连(当一个连接的空闲时间超过 8 小时后,MySQL就会断开该连接),如true
useSSL
:是否使用 SSL,如true
spring.datasource.url=jdbc:mysql://<域名或IP地址>:<端口号>/<数据库名>?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=Asia/Shanghai&useSSL=false&allowMultiQueries=true
建立 Java 类
public class Data {
// 类的属性
private String id;
private String attr1;
private String attr2;
// 类的成员列表构造函数
public Data(
String id,
String attr1,
String attr2
){
this.id = id;
this.attr1 = attr1;
this.attr2 = attr2;
}
// 类的复制构造函数
public Data(Data data) {
this.id = data.getId();
this.attr1 = data.getAttr1();
this.attr2 = data.getAttr2();
}
// 类的 Getter 方法
public String getId() {
return id;
}
public String getAttr1() {
return attr1;
}
public String getAttr2() {
return attr2;
}
// 类的 Setter 方法
public void setId(String id) {
this.id = id;
}
public void setAttr1(String attr1) {
this.attr1 = attr1;
}
public void setAttr2(String attr2) {
this.attr2 = attr2;
}
// 重写类的 toString 方法
@Override
public String toString() {
return
"Data["
+ "id=" + id + ", "
+ "attr1=" + attr1 + ", "
+ "attr2=" + attr2
+ "]";
}
}
若根据个人偏好引入了 Lombok 依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.10</version>
</dependency>
此处还可以用注解进一步简化成
@lombok.Data
@lombok.AllArgsConstructor
@lombok.NoArgsConstructor
public class Data {
// 类的属性
private String id;
private String attr1;
private String attr2;
}
不过注意,使用 Lombok 的弊端也很明显,慎用,如代码可读性低,影响升级,需要别人也安装否则报错(不过现在 IDEA 已经内置支持了 Lombok),其使用非标准注释处理方法易有遗患等
搭建多层架构
本模板采用分层架构,其中包括服务层(Service)、数据访问层(Mapper)、控制层(Controller),这种 SMC 架构是基于传统 MVC 架构的一种衍生架构,一般在前后端分离的应用开发中比较常见
- Service 层具体实现了业务逻辑
- Mapper 层中的 Java 接口在 MyBatis 框架下与 XML 映射文件相结合,映射 Java 方法至 SQL 语句,从而实现了对数据库的操作
本文仅涉及纯 Java 部分,而对应的 MyBatis 实现则在另一篇博客文章中有详细介绍
💬相关
博客文章《基于MyBatis实现依次、批量、分页增删改查操作》
https://blog.csdn.net/weixin_42077074/article/details/129405833
- Controller 层负责接收和处理 URL 请求并调用相应服务层函数
后文的 Service 层函数都有在 Controller 层对应的测试函数,通过发起一个请求快速调用并进行测试,函数返回类型均为 HTTP 响应结果类 HTTPResult
,并使用 @ResponseBody
注解将其序列化为 JSON 字符串
更简便的方法是,把 Controller 层中类最前面的 @Controller
换成 @RestController
(相当于 @Controller
注解和 @ResponseBody
注解的结合体),这样类中的所有方法都默认使用 @ResponseBody
注解,不需要再在方法上额外添加 @ResponseBody
注解
当然,你也可以用 Fastjson 包中 JSON.toJSONString()
达到同样的效果,不过前者更为简洁
💬相关
博客文章《JavaHTTP响应结果类HTTPResult》
https://blog.csdn.net/weixin_42077074/article/details/130054057
考虑参数列表
这里有个小细节要说明一下
当你把类型为 Map<String, String>
的实参传入形参为 Map<String, ?>
的函数,编译是通过的
然而当你把类型为 List<Map<String, String>>
的实参传入形参类型为 List<Map<String, ?>>
的函数时,就会报错,如果想达到同样的效果可以把形参类型写成 List<? extends Map<String,?>>
请求数据
先设置请求头,Service 层
// 请求头示例
public Map<String, String> headers = new HashMap<String, String>() {{
// 设置接收内容类型
put("Accept", "application/json");
// 设置发送内容类型
put("Content-Type", "application/json;charset=UTF-8");
// 设置字符集
put("charset", "UTF-8");
// 设置访问者系统引擎版本、浏览器信息的字段信息,此处伪装成用户通过浏览器访问
put("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
}};
再生成请求参数,Service 层
public List<Map<String, String>> generateDataParams() {
// 总请求数
int totalRequests = 1000;
// 自己按需求生成自定义参数列表
List<Map<String, String>> paramsList = new ArrayList<>();
for(int i = 0; i < totalRequests; i++) {
// key 和 value 是你需要添加的参数名与参数值
String key = "";
String value = "";
paramsList.add(new HashMap<String, String>(){{
put(key, value);
}});
}
return paramsList;
}
再实现请求,Service 层
// 单次请求
public String requestData(Map<String, ?> params) throws Exception {
String strURL = "http://www.example.com";
String method = "GET";
String response = requestHTTPContent(strURL, method, headers, params);
return response;
}
// 依次同步请求
public List<String> requestData(List<? extends Map<String,?>> paramList) throws Exception {
List<String> responses = new ArrayList<>();
for(Map<String, ?> params : paramList) {
responses.add(requestData(params));
}
return responses;
}
// 批量异步请求
public List<String> batchRequestData(List<? extends Map<String,?>> paramList) {
String strURL = "http://www.example.com";
String method = "GET";
List<CompletableFuture<String>> futures = new ArrayList<>();
// 添加异步请求任务
for (Map<String, ?> params : paramList) {
CompletableFuture<String> future = asyncHTTPRequest(strURL, method, headers, params);
futures.add(future);
}
// 等待异步任务完成,超时时间为 30 分钟
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(1800, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("请求超时");
e.printStackTrace();
return Collections.emptyList();
}
// 将所有异步请求的结果获取为 List<String>
List<String> responses = new ArrayList<>();
for (CompletableFuture<String> future : futures) {
try {
responses.add(future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
return responses;
}
// 分页异步请求
public List<String> pageRequestData(List<? extends Map<String,?>> paramList) {
int pageSize = 300;
return pageHandle(paramList, pageSize, this::batchRequestData);
}
Controller 层
// 获取参数
@RequestMapping("/data/params")
public HTTPResult generateDataParams() {
List<Map<String, String>> paramsList = dataService.generateDataParams();
return HTTPResult.success(paramsList);
}
// 依次同步请求
@RequestMapping("/data/request")
public HTTPResult requestData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
List<String> responses = dataService.requestData(paramsList);
return HTTPResult.success(responses);
}
// 批量异步请求
@RequestMapping("/data/brequest")
public HTTPResult batchRequestData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
List<String> responses = dataService.batchRequestData(paramsList);
return HTTPResult.success(responses);
}
// 分页异步请求
@RequestMapping("/data/prequest")
public HTTPResult pageRequestData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
List<String> responses = dataService.pageRequestData(paramsList);
return HTTPResult.success(responses);
}
加工数据
流水线加工比起传统加工在更多场景下速度更快,且代码更简洁易读,其特点在于
- 使用流水线操作
- 使用
stream()
将 List 转换成 Stream 对象 - 使用
map()
将 List 中的每个元素解析成特定的对象 - 使用
filter()
过滤掉解析失败的对象 - 使用
flatMap()
展开数组,每个元素单独作为一个流元素。 - 使用
collect(Collectors.toList())
将流元素转换成 List
- 使用
- 使用 Optional 的
ofNullable()
判断对象是否为 null
Service 层
// 单项加工
public List<Data> processData(String response) {
List<Data> dataList = new ArrayList<>();
JSONObject responseObj = JSON.parseObject(response);
if (responseObj != null) {
JSONArray dataArray = responseObj.getJSONArray("data");
if (dataArray != null) {
for (int i = 0; i < dataArray.size(); i++) {
JSONObject dataObj = dataArray.getJSONObject(i);
dataList.add(new Data(
dataObj.getString("id"),
dataObj.getString("key1"),
dataObj.getString("key2")
));
}
}
}
return dataList;
}
// 依次加工
public List<Data> processData(List<String> responses) {
List<Data> dataList = new ArrayList<>();
for (String response : responses) {
dataList.addAll(processData(response));
}
return dataList;
}
// 流水线加工
public List<Data> pielineProcessData(List<String> responses) {
return responses.stream()
.map(JSON::parseObject)
.filter(Objects::nonNull)
.flatMap(responseObj -> Optional.ofNullable(responseObj.getJSONArray("data")).orElse(new JSONArray()).stream())
.map(dataObj -> {
JSONObject dataJsonObj = (JSONObject) dataObj;
return Optional.ofNullable(dataJsonObj)
.map(obj -> new Data(
obj.getString("id"),
obj.getString("key1"),
obj.getString("key2")
)).orElse(null);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
Controller 层
// 传统加工
@RequestMapping("/data/process")
public HTTPResult processData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
List<String> responses = dataService.requestData(paramsList);
List<Data> dataList = dataService.processData(responses);
return HTTPResult.success(dataList);
}
// 流水线加工
@RequestMapping("/data/pprocess")
public HTTPResult pipelineProcessData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
List<String> responses = dataService.pageRequestData(paramsList);
List<Data> dataList = dataService.pielineProcessData(responses);
return HTTPResult.success(dataList);
}
插入或更新数据
Service 层
// 依次插入或更新
public void insertOrUpdateData(List<Data> dataList) {
for(Data data : dataList) {
dataMapper.insertOrUpdateData(data);
}
return ;
}
// 分页插入或更新
public void pageInsertOrUpdateData(List<Data> dataList) throws Exception {
int pageDataSize = 12000; // 页面数据量大小,即每页记录数 × 字段数,可自行设置
int totalFields = Data.class.getDeclaredFields().length; // 总字段数,即数据表中的列数
int pageSize = pageDataSize / totalFields; // 页面大小,即每页记录数
pageHandle(dataList, pageSize, dataMapper::batchInsertOrUpdateData);
return ;
}
Mapper 层
// 依次插入或更新
public void insertOrUpdateData(Data data);
// 批量插入或更新
public void batchInsertOrUpdateData(List<Data> dataList);
此处没有给出插入或更新数据对应的 Controller 层函数,是因为直接用下文集成数据的 Controller 层函数测试就可以了
集成数据
Service 层
// 集成数据
public void integrateData(List<? extends Map<String,?>) {
// 同步请求并获取响应内容
List<String> responses = requestData(paramList);
// 依次加工数据,将响应内容加工成对象列表
List<Data> dataList = processData(responses);
// 将对象依次插入或更新进数据库
insertOrUpdateData(dataList);
return ;
}
// 优化集成数据
public void integrateDataOptimized(List<? extends Map<String,?>){
// 分页异步请求并获取响应内容
List<String> responses = pageRequestData(paramList);
// 流水线加工数据,将响应内容加工成对象列表
List<Data> DataList = processData(responses);
// 将对象分页插入或更新进数据库
pageInsertOrUpdateData(DataList);
return ;
}
// 分页集成数据
public void pageIntegrateDataOptimized(List<? extends Map<String,?>){
int pageSize = 300;
pageHandle(paramList, pageSize, this::integrateDataOptimized);
}
Controller 层
// 集成
@RequestMapping("/data/integrate")
public HTTPResult integrateData() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
dataService.integrateData(paramsList);
return HTTPResult.success(null);
}
// 优化集成
@RequestMapping("/data/integratex")
public HTTPResult integrateDataOptimized() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
dataService.integrateDataOptimized(paramsList);
return HTTPResult.success(null);
}
// 分页优化集成
@RequestMapping("/data/pintegratex")
public HTTPResult pageIntegrateDataOptimized() throws Exception {
List<Map<String, String>> paramsList = dataService.generateDataParams();
dataService.pageIntegrateDataOptimized(paramsList);
return HTTPResult.success(null);
}
定期执行任务
Service 层
// 定期执行
public void performTaskOnSchedule(long secondPeriod) throws InterruptedException {
Timer timer = new Timer();
// 任务执行频率
long period = secondPeriod * 1000;
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
// 将要定时执行的任务(函数调用)写在此处
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
// 第一次任务延迟时间
// long delay = 2000;
// 延迟时间后立即开始运行
// timer.schedule(timerTask, delay, period);
// 设置首次执行时间
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
// 当天的 23:00:00 首次执行,
calendar.set(year, month, day, 23, 00, 00);
Date date = calendar.getTime();
// 指定首次运行时间,并每隔 period 再次执行(默认每隔一天)
timer.schedule(timerTask, DateUtils.addSeconds(date, 5), period);
// 在某段时间内让任务按频率执行,此后结束进程,Long.MAX_VALUE 可以近似理解为只要不受外力结束进程,就永久按频率执行
Thread.sleep(Long.MAX_VALUE);
// 终止并移除任务
timer.cancel();
timer.purge();
}
前面是纯 Java 实现定期执行任务,若想借助 Spring Boot 框架实现可以学习这篇文章
💬相关
博客文章《Spring boot开启定时任务的三种方式》
https://blog.csdn.net/qianlixiaomage/article/details/106599951
在 Spring Boot 启动类里写成这样
@SpringBootApplication
@MapperScan("com.springboot.data.mapper")
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class StarterDataCenter {
public static void main(String[] args) throws Exception {
SpringApplication.run(StarterDataCenter.class, args);
}
@Autowired
private DataService dataService;
// 此处的 cron 表达式意为每 30 分钟执行一次任务
@Scheduled(cron ="0 */30 * * * ?")
public void performTaskOnSchedule() throws Exception {
dataService.pageIntegrateDataOptimized(dataService.generateDataParams());
}
}