基于SpringBoot+MyBatis的数据集成模板

📚专栏

「Java数据集成」专栏

基础支撑
辅助(可选)
进阶
基础支撑
基础支撑
辅助(可选)
辅助(可选)
辅助(可选)
测试(可选)
《请求和解析》
《依批分增删改查》
《生成代码脚本》
《增删改查模板》
《同异步请求和处理》
《集成模板》
《HTTP请求工具类》
《JSON处理工具类》
《XML处理工具类》
《生成随机数据脚本》

💬相关

本文涉及的模板代码已放在 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 中这样一个数据集成过程

  1. 接入数据
    • 查询数据表获取数据:基于 MyBatis 映射 Java 到 SQL 实现
    • 发起 HTTP 请求并获取接口数据:借助 HttpURLConnection 发送请求
  2. 加工数据:按自己的需求自己实现,如用阿里巴巴的 Fastjson 包解析响应内容中的 JSON 形式的字符串
  3. 写入数据:基于 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 数据含有键 idkey1key2,如
{
    "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,含有字段 idfield1field2
  • 基于 Spring Boot + MyBatis 实现,采用多层架构(Controller 层、Service 层、Mapper层)
  • 建立 Java 类 Data,含有属性 idattr1attr2
  • 将接口数据插入或更新进数据库,对应 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
  • useUnicodecharacterEncoding :编码方式,如 trueutf8
  • 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());
	}
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值