导入百万数据(*.txt)到oracle数据库,同时需要请求第三方API(发送Http请求)
SpringBoot 使用 RestTemplate 查询地理编码API
步骤分析
- 获取
.txt
文件中的地址; - 调用 高德地理编码API,得到经纬度;
- 将相应的信息入库。
实现思路
- 第一种
1. Java 读取文件内容,返回文件对应实体类InfoDto;
1. 地址为空 >> 返回地址为空的List >> 转为实体类映射entity的List<InfoDto>;
2. 地址不为空
1. 拆分为十个一组的二维数组,不足10的再新建数组加入末尾;
2. 拆分两个不同的List<InfoDto>为size的长度数组,余数不为空则数组长度加1,
1. 地址为空List >> 实体类映射List
2. 不为空List
1. 多线程遍历二维数组
1. 查询API(十个一组的地址List),返回经纬度entityList
2. 添加进List<List<InfoDo>> List,返回
2. 查询API失败,将失败的地址等信息存入文件
3. 获取不同的数据库映射entity的List,多线程循环二维数组 >> 执行入库
- 第二种
1. Java 读取文件内容,返回转换后的经纬度信息List;
1. 地址为空,文件读完后返回设置成入库entity的List;
2. 地址不为空
1. 十个一组,查询高德地图API;
2. 查询异常,将地址、唯一编号等信息写入文件;
3. 将查询解析查询结果,设置为与数据库映射一致List
2. 将两个List拆分为等同大小的List,执行入库操作;
3. 读取查询API错误地址文件,执行入库
1. 查询API,返回结果异常或查询失败时返回经纬度为0的entity;
2. 将查询结果存入List;
3. 按指定size拆分List,多线程执行入库
配置 pom 文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
SpringBoot 配置 RestTemplate 访问 API
使用
RestTemplate
调用RESTful
配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
return new RestTemplate(factory);
}
@Bean
public ClientHttpRequestFactory simplClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
// factory.setConnectTimeout(2000);
// factory.setReadTimeout(2000);
return factory;
}
}
SpringBoot 配置访问 API 地址等
创建连接高德API
.properties
# 高德——地理编码
geo.url= http://restapi.amap.com/v3/geocode/geo
geo.key= xxx #自己申请的key
- 自定义的配置文件最好还是使用
*.properties
格式的文件,注解的方式还不支持手动加载*.yml
格式文件的功能
创建实体类,获取API配置地址
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
@Primary
@Configuration
@PropertySource("classpath:geocode.properties")
@ConfigurationProperties(prefix = "geo")
@Data
@ToString
public class GeocodingProperties {
private String url;
private String key;
}
- 注:
@Primary
如果只有一个自定义配置文件,则可以省略;如果有两个或两个以上则必须在某个@Configuration
实体类上增加该注解
编写访问高德地理编码API工具类
import com.sanss.config.GeocodingProperties;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.annotation.PostConstruct;
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* @ClassName: GeoDto
* @Description: 调用高德地理编码API传递的参数实体类
* @date 2019/03/27
*/
class GeoDto {
private Boolean batch;
private String city;
private String address;
private String url;
private String key;
public GeoDto() {
}
public GeoDto(List<String> address, String city, Boolean batch, String url, String key) {
StringBuilder addressSb = new StringBuilder();
for (int i = 0, length = address.size(); i < length; i++) {
addressSb.append(address.get(i));
addressSb.append("|");
}
this.address = addressSb.substring(0, addressSb.lastIndexOf("|")).toString();
this.city = city;
this.batch = batch;
this.url = url;
this.key = key;
}
@Override
public String toString() {
return url + "?batch=" + batch + "&key=" + key + "&city=" + city + "&address=" + address;
}
}
/**
* @ClassName: Geocodes
* @Description: 返回结果实体类, 只获取了location:经纬度 API 地址:https://lbs.amap.com/api/webservice/guide/api/georegeo
* 返回结果示例: {"status":"1","info":"OK","infocode":"10000","count":"1","geocodes":[{"formatted_address":"上海市浦东新区东方明珠","country":"中国","province":"上海市","citycode":"021","city":"上海市","district":"浦东新区","township":[],"neighborhood":{"name":[],"type":[]},"building":{"name":[],"type":[]},"adcode":"310115","street":[],"number":[],"location":"121.499740,31.239853","level":"兴趣点"}]}
* @date 2019/03/27
*/
@Data
@ToString
class Geocodes {
private String location;
}
/**
* @Title
* @Description 高德地理编码API返回JSON实体类, 部分需要的属性, 完整的返回值在上方
* @return
**/
@Data
@ToString
class Geocode {
private int status;
private int count;
private List<Geocodes> geocodes;
}
/**
* @ClassName: GeoCodingUtil
* @Description: 高德地理编码/逆编码(将地址转换为经度,纬度) API: https://lbs.amap.com/api/webservice/guide/api/georegeo/#scene
* @date 2019/03/14
*/
@Component
public class GeoCodingUtil {
@Autowired
private RestTemplate restTemplate;
@Autowired
private GeocodingProperties properties;
private static GeoCodingUtil geoCodingUtil;
@PostConstruct
private void init() {
geoCodingUtil = this;
geoCodingUtil.properties = this.properties;
geoCodingUtil.restTemplate = this.restTemplate;
}
/**
* 获取批量地址地理编码
**/
public List<String> getGeocoding(List<String> address, String city) throws Exception {
return getGeocoding(address, city, true);
}
/**
* 获取上海地区批量地址的地理编码
**/
public List<String> getShanghaiGeocoding(List<String> address)
throws Exception {
return getGeocoding(address, "上海", true);
}
/**
* 获取上海地区单个地址的地理编码
**/
public String getShanghaiGeocoding(String address) throws Exception {
List<String> list = new ArrayList();
list.add(address);
list = getGeocoding(list, "上海", false);
if ("[]".equals(list.toString())) {
return list.toString();
}
return list.get(0);
}
/**
* 上海地区的高德地理编码获取
**/
private List<String> getGeocoding(List<String> address, String city, boolean batch)
throws Exception {
List<String> point = new ArrayList<>();
GeoDto geoDto = new GeoDto(address, city, batch, geoCodingUtil.properties.getUrl(),
geoCodingUtil.properties.getKey());
String realUrl = geoDto.toString();
Geocode geocode = geoCodingUtil.restTemplate.getForObject(realUrl, Geocode.class);
int count = geocode.getCount();
if (count > 0) {
List<Geocodes> geocodes = geocode.getGeocodes();
Iterator<Geocodes> iterator = geocodes.iterator();
String location;
while (iterator.hasNext()) {
location = iterator.next().getLocation();
if (location == null) {
point.add("");
continue;
}
point.add(location);
}
}
return point;
}
}
- 注: 在
非 controller
中读取配置文件时获取不到配置类的属性值,欲了解详情可以看下我的另一篇博客SpringBoot配置文件详解
现在已经可以访问API了,接下来是service
调用,返回经纬度信息。以上为通用的代码块,自此只写实现拆分List、多线程实现等主要代码块。
拆分List,多线程执行入库
- 拆分List工具类
package com.sanss.util;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName ArrayUtil
* @Description <br/> TODO
* @Author Dew
* @Date 2019/4/1 13:53
* @Version 1.0
**/
public class ArrayUtil<T> {
/**
* @Author Dew
* @Description
* @Param [values 待分组Array 分组, groupSize 分组大小]
* @Date 13:54 2019/4/1
* @Return java.util.List<T>
**/
public List<List<T>> spilitGroup(List<T> values, int groupSize) {
List<List<T>> listGroup = new ArrayList<>();
// List 长度
int listSize = values.size();
int runSize = (listSize / groupSize) + 1;
List<T> value = null;
for (int i = 0; i < runSize; i++) {
int start = i * groupSize;
if (i + 1 == runSize) {
int end = listSize;
value = values.subList(start, end);
} else {
int end = (i + 1) * groupSize;
value = values.subList(start, end);
}
listGroup.add(value);
}
return listGroup;
}
}
- 多线程执行入库(使用线程池的方式创建线程)
/**
* @ClassName: BatchSaveThread
* @Description: 多线程执行入库操作
* @date 2019/03/14
*/
class BatchSaveThread implements Runnable {
private List<AdUserGridDo> list;
public BatchSaveThread(List<AdUserGridDo> list) {
this.list = list;
}
@Override
public void run() {
if (list.size() > 0) {
adUserRepository.batchSaveAdvertising(list);
}
}
}
public void batchSave(List<AdUserGridDo> models, int groupSize) {
List<List<AdUserGridDo>> groupList = new ArrayUtil<AdUserGridDo>().spilitGroup(models, groupSize);
// 创建线程数 = 数据总数 / groupList
int threadPoolSize = groupList.size();
ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
try {
// 拆分网格数据
for (int i = 0, length = groupList.size(); i < length; i++) {
logger.error("线程:" + i + "save API");
List<AdUserGridDo> list = groupList.get(i);
BatchSaveThread saveThread = new BatchSaveThread(list);
executor.execute(saveThread);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
executor.shutdown();
}
}
使用 Connection 批量入库,降低入库时间
- 注: 如果使用Hibernate 执行入库则可能出现数据库连接超时的现象,可以使用JDBC批量入库进行批量操作. 百万数据秒级入库
常见问题
RestTemplate 调用API超时
解决方案
- 配置类
RestTemplateConfig
中不设置connectTimeout、readTimeout
- 将超时时间变长,
timeout
时间单位为 毫秒
实现多线程有返回值的执行
/**
* @ClassName: BatchRestAPIThread
* @Description: 多线程查询API,地理编码操作
* @date 2019/03/18
*/
class BatchRestAPIThread implements Callable<List<InfoDo>> {
private List<List<InfoDto>> list;
public BatchRestAPIThread(List<List<InfoDto>> list) {
this.list = list;
}
@Override
public List<InfoDo> call() throws Exception {
List<InfoDo> result = new ArrayList<>();
// 执行代码块
return result;
}
}