前言:
最近在做一块业务,需要开发IP归属地识别相关的相关功能,调查了一下,目前IP归属地是有两种方案,一种是在线接口比如淘宝、百度、ip-api等。这种方案的优点是覆盖面广,查询准确,缺点是有的限制qps,有的收费。另外一种就是离线库查询,具体的开源各个语言不尽相同,这里我使用到JAVA的ip2region开源库,这里记录一下使用方式。
一、maven坐标
ip2region总体分为两个大版本:
1.x、提供三种算法,其中内存查询速度在0.1x毫秒级别,离线库文件ip2region.db
2.x、同样提供三种算法,其中内存查询速度在微秒级别,离线库文件ip2region.xdb
这里我直接使用2.x的最新版本。
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.5</version>
</dependency>
二、离线库下载及加载
离线数据库可以去github项目中下载,地址 ip2region,文件目录地址在/ip2regon/data/ip2region.xdb。
1、放置目录
将文件下载完成后,放到项目的resource目录下新建的ip2region目录中(注意:如果使用maven插件打包的话,需要添加过滤,否则xdb文件会存在混乱的情况。)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>xdb</nonFilteredFileExtension>
<nonFilteredFileExtension>db</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
(这里需要过滤xdb文件)
2、编写文件加载类
这里我为了最小依赖( spring太重了),所以使用了静态加载的模式来加载,初始化当前对象即可使用searcher或者使用提供的封装方法(ip2region searcher类是不提供线程安全的,所以这里直接new Ip2Region对象使用searcher可隔离查询对象)
public class Ip2Region {
protected static Searcher searcher;
static {
InputStream resourceAsStream = Ip2Region.class.getClassLoader().getResourceAsStream("ip2region/ip2region.xdb");
if (resourceAsStream != null) {
byte [] cBuff= null;
try {
cBuff = IOUtils.toByteArray(resourceAsStream);
searcher = Searcher.newWithBuffer(cBuff);
}catch (IOException e) {
e.printStackTrace();
}finally {
try {
resourceAsStream.close();
}catch (Exception e) {}
}
}
}
public static Searcher getSearch(){
return searcher;
}
}
三、封装使用
1、封装操作类
这里封装了一个主要的方法,根据IP获取地区的,返回map。
/**
* ip转long
* @param ip
* @return
*/
public static long ipToLong(String ip){
String[] split = ip.split("\\.");
long i1 = 0L;
i1 += Integer.parseInt(split[0])<<24;
i1 += Integer.parseInt(split[1])<<16;
i1 += Integer.parseInt(split[2])<<8;
return i1 + Integer.parseInt(split[3]);
}
/**
* 获取IP对应地区
* @param ip
* @return Map<String,String>
* country: 中国
* province: 江苏
* city: 苏州
*/
public static Map<String,String> getArea(String ip) {
long ipLong = ipToLong(ip);
Map<String,String> areaMap;
try {
String search = searcher.search(ipLong);
if (search == null || search.isEmpty())
return null;
String[] s1 = search.split("\\|");
if (s1 == null || s1.length == 0) {
return null;
}
areaMap = new HashMap<>(3);
areaMap.put("country",s1[0]);
areaMap.put("province",s1[2]);
areaMap.put("city",s1[3]);
return areaMap;
}catch (IOException ioException) {
ioException.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
2、封装使用类
静态加载 Ip2Regin对象,检查输入参数是否合法。
public class Ip2RegionHandler {
protected static final Ip2Region INSTANCE = new Ip2Region();
public static Map<String,String> getArea(String ip) throws Exception {
checkIP(ip);
return INSTANCE.getArea(ip);
}
public static void checkIP(String ip) throws Exception {
if (ip == null) {
throw new Exception("ip format err");
}
String[] ipSplit = ip.split("\\.");
if (ipSplit.length != 4) {
throw new Exception("ip format err");
}
for (int i = 0, n = ipSplit.length; i < n; i++) {
if (!ipSplit[i].chars().allMatch(Character::isDigit)) {
throw new Exception("ip format err");
}
int i1 = Integer.parseInt(ipSplit[i]);
if ( i1 > 0XFF || i1 < 0X0) {
throw new Exception("ip format err");
}
}
}
}
四、测试使用
这里我写了个单元测试,测试不同的IP归属地识别情况及错误输入参数。
class Ip2RegionHandlerTest {
@ParameterizedTest
@ValueSource(strings = {"12.0.1.123","12.0.1.123","1.125.1.97","5.125.1.97"})
void getArea(String ip) throws Exception {
Map<String, String> area = Ip2RegionHandler.getArea(ip);
System.out.println(area.toString());
}
@ParameterizedTest
@ValueSource(strings = {"12.0.1.123","257.0.0.1","asdasew"})
void checkIP(String ip) {
try {
Ip2RegionHandler.checkIP(ip);
}catch (Exception e) {
System.out.println("errip : " + ip);
}
}
}
1、IP归属地测试
(由于离线库能做到的有限,所以国外IP只能到国家级别,国内能到市级别)
Connected to the target VM, address: '127.0.0.1:54264', transport: 'socket'
{province=0, country=美国, city=0}
{province=0, country=美国, city=0}
{province=南澳大利亚, country=澳大利亚, city=阿德莱德}
{province=0, country=伊朗, city=0}
Disconnected from the target VM, address: '127.0.0.1:54264', transport: 'socket'
Process finished with exit code 0
2、IP参数校验测试
Connected to the target VM, address: '127.0.0.1:54507', transport: 'socket'
errip : 257.0.0.1
errip : asdasew
Disconnected from the target VM, address: '127.0.0.1:54507', transport: 'socket'
Process finished with exit code 0
测试结果:符合预期