diff --git a/binding/java/ReadMe.md b/binding/java/ReadMe.md
index 3b7b8ef..77d5ce2 100644
--- a/binding/java/ReadMe.md
+++ b/binding/java/ReadMe.md
@@ -7,10 +7,48 @@
org.lionsoul
ip2region
- 3.1.1
+ 3.2.0
```
+### 关于查询服务
+从 `3.2.0` 版本开始提供了一个双协议兼容且并发安全的 `Ip2Region` 查询服务,**建议优先使用该方式来进行查询调用**,具体使用方式如下:
+```java
+// 1, 创建 v4 的配置:指定缓存策略和 v4 的 xdb 文件路径
+final Config v4Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache) // 指定缓存策略: NoCache / VIndexCache / BufferCache
+ .setSeachers(15) // 设置初始化的查询器数量
+ .setXdbPath("ip2region v4 xdb path") // 设置 v4 xdb 文件的路径
+ .asV4(); // 指定为 v4 配置
+
+// 2, 创建 v6 的配置:指定缓存策略和 v6 的 xdb 文件路径
+final Config v6Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache) // 指定缓存策略: NoCache / VIndexCache / BufferCache
+ .setSeachers(15) // 设置初始化的查询器数量
+ .setXdbPath("ip2region v6 xdb path") // 设置 v6 xdb 文件的路径
+ .asV6(); // 指定为 v6 配置
+
+// 3,通过上述配置创建 Ip2Region 查询服务
+final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);
+
+// 4,导出 ip2region 服务进行双版本的IP地址的并发查询,例如:
+final String v4Region = ip2Region.search("113.92.157.29"); // 进行 IPv4 查询
+final String v6Region = ip2Region.search("240e:3b7:3272:d8d0:db09:c067:8d59:539e"); // 进行 IPv6 查询
+
+// 5,在服务需要关闭的时候,同时关闭 ip2region 查询服务
+ip2Region.close();
+```
+关于 `Ip2Region`查询服务的信息:
+1. 该查询服务的 API 并发安全且同时支持 `IPv4` 和 `Ipv6` 的地址,内部实现会自动判断。
+2. v4 和 v6 的配置需要单独创建,可以给 v4 和 v6 设置使用不同的缓存策略,也可以指定其中一个为 `null` 则该版本的 IP 地址查询都会返回 `null`。
+3. 请结合您项目的并发数给 `setSearchers` 一个合适的查询器数量,默认为 20 个,这个值在运行过程中是固定的,每次查询会从池子里租借一个查询器来完成查询操作,查询完成后再归还回去,如果租借的时候池子已经空了则等待直到有可用的查询器来完成查询服务,租借的锁是使用的 `ReentrantLock` 来管理,也可以通过如下方式来设置 `Ip2Region` 查询服务使用公平锁:
+```java
+final Ip2Region ip2region = Ip2Region.create(v4Config, v6Config, true);
+```
+4. 如果配置设置的缓存策略为 `Config.BufferCache` 即 `全内存缓存` 则默认会使用单实例的内存查询器,该实现天生并发安全,此时通过 `setSearchers` 指定的查询器数量无效。
+5. 如果 `ip2region` 查询器在提供服务期间,调用 close 默认会最大等待 10 秒钟来等待尽量多的查询器归还。
+
+
### 关于查询 API
定位信息查询 API 的原型为:
```java
diff --git a/binding/java/pom.xml b/binding/java/pom.xml
index 295628b..b7ea414 100644
--- a/binding/java/pom.xml
+++ b/binding/java/pom.xml
@@ -4,7 +4,7 @@
org.lionsoul
ip2region
- 3.1.1
+ 3.2.0
jar
ip2region
@@ -116,7 +116,7 @@
- org.lionsoul.ip2region.SearchApp
+ org.lionsoul.ip2region.SearcherTest
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/Config.java b/binding/java/src/main/java/org/lionsoul/ip2region/Config.java
new file mode 100644
index 0000000..3012ef8
--- /dev/null
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/Config.java
@@ -0,0 +1,83 @@
+// Copyright 2022 The Ip2Region Authors. All rights reserved.
+// Use of this source code is governed by a Apache2.0-style
+// license that can be found in the LICENSE file.
+package org.lionsoul.ip2region;
+
+import java.io.IOException;
+
+import org.lionsoul.ip2region.xdb.Header;
+import org.lionsoul.ip2region.xdb.LongByteArray;
+import org.lionsoul.ip2region.xdb.Version;
+import org.lionsoul.ip2region.xdb.XdbException;
+
+/**
+ * ip2region config class
+ * @Author Lion
+ * @Date 2025/11/20
+*/
+public class Config {
+ // cache policy consts
+ public static final int NoCache = 0;
+ public static final int VIndexCache = 1;
+ public static final int BufferCache = 2;
+
+ // search cache policy
+ public final int cachePolicy;
+ public final Version ipVersion;
+
+ // xdb file path
+ public final String xdbPath;
+ public final Header header;
+
+ public final byte[] vIndex;
+ public final LongByteArray cBuffer;
+
+ public final int searchers;
+
+ // config builder
+ public static ConfigBuilder custom() {
+ return new ConfigBuilder();
+ }
+
+ protected Config(int cachePolicy, Version ipVersion, String xdbPath,
+ Header header, byte[] vIndex, LongByteArray cBuffer, int searchers) throws IOException, XdbException {
+ this.cachePolicy = cachePolicy;
+ this.ipVersion = ipVersion;
+
+ this.xdbPath = xdbPath;
+ this.header = header;
+ this.vIndex = vIndex;
+ this.cBuffer = cBuffer;
+
+ final Version xVersion = Version.fromHeader(header);
+ // verify the ip version (ipVersion and the version of the xdb file should be the same)
+ if (header.ipVersion != ipVersion.id) {
+ throw new XdbException("ip verison not match: xdb file "
+ + xdbPath + " (" + xVersion.name + "), as " + ipVersion.name + " expected");
+ }
+
+ this.searchers = searchers;
+ }
+
+ @Override public String toString() {
+ final StringBuffer sb = new StringBuffer();
+ sb.append('{');
+ sb.append("cache_policy:").append(cachePolicy).append(',');
+ sb.append("version:").append(ipVersion.toString()).append(',');
+ sb.append("xdb_path:").append(xdbPath).append(',');
+ sb.append("header:").append(header.toString()).append(',');
+ if (vIndex == null) {
+ sb.append("v_index: null, ");
+ } else {
+ sb.append("v_index: {bytes: ").append(vIndex.length).append("},");
+ }
+ if (cBuffer == null) {
+ sb.append("c_buffer: null, ");
+ } else {
+ sb.append("c_buffer: {bytes: ").append(cBuffer.length()).append("},");
+ }
+ sb.append("searchers:").append(searchers);
+ sb.append('}');
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/ConfigBuilder.java b/binding/java/src/main/java/org/lionsoul/ip2region/ConfigBuilder.java
new file mode 100644
index 0000000..4bdb77d
--- /dev/null
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/ConfigBuilder.java
@@ -0,0 +1,83 @@
+// Copyright 2022 The Ip2Region Authors. All rights reserved.
+// Use of this source code is governed by a Apache2.0-style
+// license that can be found in the LICENSE file.
+
+package org.lionsoul.ip2region;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+
+import org.lionsoul.ip2region.xdb.Header;
+import org.lionsoul.ip2region.xdb.LongByteArray;
+import org.lionsoul.ip2region.xdb.Searcher;
+import org.lionsoul.ip2region.xdb.Version;
+import org.lionsoul.ip2region.xdb.XdbException;
+
+/**
+ * ip2region config builder
+ * @Author Lion
+ * @Date 2025/11/20
+*/
+public class ConfigBuilder {
+
+ // cache policy
+ private int cachePolicy = Config.VIndexCache;
+
+ // xdb file path
+ private String xdbPath = null;
+
+ // searchers
+ private int searchers = 20;
+
+ public ConfigBuilder() {}
+
+ public ConfigBuilder(String xdbPath) {
+ this.xdbPath = xdbPath;
+ }
+
+ public ConfigBuilder setCachePolicy(int cachePolicy) {
+ this.cachePolicy = cachePolicy;
+ return this;
+ }
+
+ public ConfigBuilder setXdbPath(String xdbPath) {
+ this.xdbPath = xdbPath;
+ return this;
+ }
+
+ public ConfigBuilder setSeachers(int searchers) {
+ this.searchers = searchers;
+ return this;
+ }
+
+ private Config build(Version ipVersion) throws IOException, XdbException {
+ // load the header and the cache buffer
+ final RandomAccessFile raf = new RandomAccessFile(xdbPath, "r");
+
+ // 1, verify the xdb
+ Searcher.verify(raf);
+
+ // 2, load the header
+ final Header header = Searcher.loadHeader(raf);
+
+ // 3, check and load the vector index buffer
+ final byte[] vIndex = cachePolicy == Config.VIndexCache ? Searcher.loadVectorIndex(raf) : null;
+
+ // 4, check and load the content buffer
+ final LongByteArray cBuffer = cachePolicy == Config.BufferCache ? Searcher.loadContent(raf) : null;
+
+ raf.close();
+ return new Config(cachePolicy, ipVersion, xdbPath, header, vIndex, cBuffer, searchers);
+ }
+
+ // build the final #Config instance for IPv4
+ public Config asV4() throws IOException, XdbException {
+ return build(Version.IPv4);
+ }
+
+ // build the final #Config instance for IPv6
+ public Config asV6() throws IOException, XdbException {
+ return build(Version.IPv6);
+ }
+
+}
\ No newline at end of file
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/Ip2Region.java b/binding/java/src/main/java/org/lionsoul/ip2region/Ip2Region.java
new file mode 100644
index 0000000..03f5de1
--- /dev/null
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/Ip2Region.java
@@ -0,0 +1,171 @@
+// Copyright 2022 The Ip2Region Authors. All rights reserved.
+// Use of this source code is governed by a Apache2.0-style
+// license that can be found in the LICENSE file.
+
+package org.lionsoul.ip2region;
+
+import java.io.IOException;
+
+import org.lionsoul.ip2region.xdb.InetAddressException;
+import org.lionsoul.ip2region.xdb.Searcher;
+import org.lionsoul.ip2region.xdb.Util;
+import org.lionsoul.ip2region.xdb.XdbException;
+
+/**
+ * ip2region searcher manager service to provider:
+ * 1. Unified query interface for IPv4 and IPv6 address.
+ * 2. Concurrency search support.
+ *
+ * @Author Lion
+ * Date 2025/11/21
+*/
+public class Ip2Region {
+
+ /* v4 pool for cache policy vIndex or NoCache */
+ private final SearcherPool v4Pool;
+
+ /* v4 xdb searcher for cache policy cBuffer */
+ private final Searcher v4InMemSearcher;
+
+ /* v6 pool for cache policy vIndex or NoCache*/
+ private final SearcherPool v6Pool;
+
+ /* v6 xdb searcher for cache policy cBuffer */
+ private final Searcher v6InMemSearcher;
+
+ public static final Ip2Region create(final Config v4Config, final Config v6Config) throws IOException {
+ return new Ip2Region(v4Config, v6Config).init();
+ }
+
+ public static final Ip2Region create(final String v4XdbPath, final String v6XdbPath) throws IOException, XdbException {
+ return new Ip2Region(v4XdbPath, v6XdbPath).init();
+ }
+
+ /**
+ * init the ip2reigon with two xdb file path and default cachePolicy vIndex.
+ * set it to null to disabled the search for specified version
+ *
+ * @param v4XdbPath
+ * @param v6XdbPath
+ * @throws XdbException
+ * @throws IOException
+ */
+ protected Ip2Region(String v4XdbPath, String v6XdbPath) throws IOException, XdbException {
+ this(
+ v4XdbPath == null ? null : Config.custom().setXdbPath(v4XdbPath).asV4(),
+ v6XdbPath == null ? null : Config.custom().setXdbPath(v6XdbPath).asV6()
+ );
+ }
+
+ /**
+ * init the ip2region with specified config.
+ * set it to null for disabled the search for specified version
+ *
+ * @param v4Config
+ * @param v6Config
+ * @throws IOException
+ */
+ protected Ip2Region(Config v4Config, Config v6Config) throws IOException {
+ if (v4Config == null) {
+ // @Note: with IPv4 disabled ?
+ this.v4InMemSearcher = null;
+ this.v4Pool = null;
+ } else if (v4Config.cachePolicy == Config.BufferCache) {
+ this.v4InMemSearcher = Searcher.newWithBuffer(v4Config.ipVersion, v4Config.cBuffer);
+ this.v4Pool = null;
+ } else {
+ this.v4InMemSearcher = null;
+ this.v4Pool = new SearcherPool(v4Config);
+ }
+
+ if (v6Config == null) {
+ // @Note: with IPv6 disabled ?
+ this.v6InMemSearcher = null;
+ this.v6Pool = null;
+ } else if (v6Config.cachePolicy == Config.BufferCache) {
+ this.v6InMemSearcher = Searcher.newWithBuffer(v6Config.ipVersion, v6Config.cBuffer);
+ this.v6Pool = null;
+ } else {
+ this.v6InMemSearcher = null;
+ this.v6Pool = new SearcherPool(v6Config);
+ }
+ }
+
+ // init the current ip2region service
+ protected Ip2Region init() throws IOException {
+ if (v4Pool != null) {
+ v4Pool.init();
+ }
+
+ if (v6Pool != null) {
+ v6Pool.init();
+ }
+
+ return this;
+ }
+
+ public String search(String ipString) throws InetAddressException, IOException, InterruptedException {
+ return search(Util.parseIP(ipString));
+ }
+
+ public String search(byte[] ipBytes) throws InetAddressException, IOException, InterruptedException {
+ if (ipBytes.length == 4) {
+ return v4Search(ipBytes);
+ } else if (ipBytes.length == 16) {
+ return v6Search(ipBytes);
+ } else {
+ throw new InetAddressException("invalid byte ip address with length=" + ipBytes.length);
+ }
+ }
+
+ protected String v4Search(final byte[] ipBytes) throws IOException, InetAddressException, InterruptedException {
+ if (v4InMemSearcher != null) {
+ return v4InMemSearcher.search(ipBytes);
+ }
+
+ // IPv4 search is disabled
+ if (v4Pool == null) {
+ return null;
+ }
+
+ final Searcher searcher = v4Pool.borrowSearcher();
+ try {
+ return searcher.search(ipBytes);
+ } finally {
+ v4Pool.returnSearcher(searcher);
+ }
+ }
+
+ protected String v6Search(final byte[] ipBytes) throws IOException, InetAddressException, InterruptedException {
+ if (v6InMemSearcher != null) {
+ return v6InMemSearcher.search(ipBytes);
+ }
+
+ // IPv6 search is disabled
+ if (v6Pool == null) {
+ return null;
+ }
+
+ final Searcher searcher = v6Pool.borrowSearcher();
+ try {
+ return searcher.search(ipBytes);
+ } finally {
+ v6Pool.returnSearcher(searcher);
+ }
+ }
+
+ public void close() throws InterruptedException {
+ close(10000);
+ }
+
+ public void close(long timeoutMillis) throws InterruptedException {
+ if (v4Pool != null) {
+ v4Pool.close(timeoutMillis);
+ }
+
+ if (v6Pool != null) {
+ v6Pool.close(timeoutMillis);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/SearcherPool.java b/binding/java/src/main/java/org/lionsoul/ip2region/SearcherPool.java
new file mode 100644
index 0000000..46f07da
--- /dev/null
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/SearcherPool.java
@@ -0,0 +1,123 @@
+// Copyright 2022 The Ip2Region Authors. All rights reserved.
+// Use of this source code is governed by a Apache2.0-style
+// license that can be found in the LICENSE file.
+
+package org.lionsoul.ip2region;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.lionsoul.ip2region.xdb.Searcher;
+
+/**
+ * ip2region searcher pool manager to provider Concurrency search support.
+ *
+ * @author Leon
+ * Date 2025/11/21
+*/
+public class SearcherPool {
+ // config instance
+ public final Config config;
+
+ // searcher pool
+ private final Queue pool;
+
+ // lock & conditions
+ private final ReentrantLock lock;
+ private final Condition emptyCondition;
+ private final Condition fullCondition;
+
+ // searcher numbers that was loaned out
+ private int loanCount;
+
+ // static method to create and init the searcher pool
+ public static final SearcherPool create(final Config config) throws IOException {
+ return new SearcherPool(config).init();
+ }
+
+ public static final SearcherPool create(final Config config, boolean fair) throws IOException {
+ return new SearcherPool(config, fair).init();
+ }
+
+ protected SearcherPool(Config config) throws IOException {
+ this(config, false);
+ }
+
+ protected SearcherPool(Config config, boolean fair) {
+ assert config.searchers > 0;
+ this.config = config;
+ this.pool = new LinkedList<>();
+ this.lock = new ReentrantLock(fair);
+ this.emptyCondition = this.lock.newCondition();
+ this.fullCondition = this.lock.newCondition();
+ this.loanCount = 0;
+ }
+
+ protected SearcherPool init() throws IOException {
+ // create the searchers
+ for (int i = pool.size(); i < config.searchers; i++) {
+ final Searcher searcher = new Searcher(config.ipVersion, config.xdbPath, config.vIndex, config.cBuffer);
+ pool.add(searcher);
+ }
+
+ return this;
+ }
+
+ public Searcher borrowSearcher() throws InterruptedException {
+ lock.lock();
+ try {
+ while (pool.isEmpty()) {
+ emptyCondition.await();
+ }
+
+ loanCount++;
+ return pool.poll();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public void returnSearcher(final Searcher searcher) {
+ lock.lock();
+ try {
+ pool.add(searcher);
+ loanCount--;
+ emptyCondition.signal();
+
+ // check and signal the full condition.
+ // pool close
+ if (loanCount == 0) {
+ fullCondition.signal();
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ // close the searcher pool
+ public void close() throws InterruptedException {
+ close(10000);
+ }
+
+ public void close(long timeoutMillis) throws InterruptedException {
+ lock.lock();
+ try {
+ while (loanCount > 0) {
+ fullCondition.wait(timeoutMillis);
+ }
+
+ final Iterator it = pool.iterator();
+ while (it.hasNext()) {
+ final Searcher searcher = it.next();
+ try {searcher.close();} catch (IOException e) {}
+ it.remove();
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+}
\ No newline at end of file
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java b/binding/java/src/main/java/org/lionsoul/ip2region/SearcherTest.java
similarity index 99%
rename from binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java
rename to binding/java/src/main/java/org/lionsoul/ip2region/SearcherTest.java
index 64fa6da..448f3b5 100644
--- a/binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/SearcherTest.java
@@ -17,7 +17,7 @@ import java.io.*;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
-public class SearchApp {
+public class SearcherTest {
public static void printHelp(String[] args) {
System.out.print("ip2region xdb searcher\n");
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java
index dbae4de..5dd22c4 100644
--- a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java
@@ -29,6 +29,7 @@ public class Searcher {
private final Version version;
// random access file handle for file-based search
+ private final String xdbPath;
private final RandomAccessFile handle;
private int ioCount = 0;
@@ -61,6 +62,7 @@ public class Searcher {
public Searcher(Version version, String dbFile, byte[] vectorIndex, LongByteArray cBuff) throws IOException {
this.version = version;
+ this.xdbPath = dbFile;
if (cBuff != null) {
this.handle = null;
this.vectorIndex = null;
@@ -173,6 +175,16 @@ public class Searcher {
}
}
+ @Override public String toString() {
+ return String.format(
+ "%s->{version:%s, xdb:%s, vIndex:%s, cBuffer:%s}",
+ super.toString(),
+ version.name, xdbPath,
+ vectorIndex == null ? "null" : String.valueOf(vectorIndex.length),
+ contentBuff == null ? "null" : String.valueOf(contentBuff.length())
+ );
+ }
+
// --- static util function
public static Header loadHeader(RandomAccessFile handle) throws IOException {
diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java
index 461d24c..496c4cc 100644
--- a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java
+++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java
@@ -79,4 +79,36 @@ public class Util
return 0;
}
+ public static byte[] ipAddOne(byte[] ip) {
+ final byte[] r = new byte[ip.length];
+ System.arraycopy(ip, 0, r, 0, ip.length);
+ for (int i = ip.length - 1; i >= 0; i--) {
+ final int v = (int)(r[i] & 0xFF);
+ if (v < 255) { // No overflow
+ r[i]++;
+ break;
+ }
+
+ r[i] = 0;
+ }
+
+ return r;
+ }
+
+ public static byte[] ipSubOne(byte[] ip) {
+ final byte[] r = new byte[ip.length];
+ System.arraycopy(ip, 0, r, 0, ip.length);
+ for (int i = ip.length - 1; i >= 0; i--) {
+ final int v = (int)(r[i] & 0xFF);
+ if (v > 0) { // No borrow needed
+ r[i]--;
+ break;
+ }
+
+ r[i] = (byte) 0xFF; // borrow from the next byte
+ }
+
+ return r;
+ }
+
}
diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/ConfigTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/ConfigTest.java
new file mode 100644
index 0000000..178e070
--- /dev/null
+++ b/binding/java/src/test/java/org/lionsoul/ip2region/ConfigTest.java
@@ -0,0 +1,42 @@
+package org.lionsoul.ip2region;
+import java.io.IOException;
+import java.security.CodeSource;
+
+import org.junit.Test;
+import org.lionsoul.ip2region.xdb.Log;
+import org.lionsoul.ip2region.xdb.XdbException;
+
+public class ConfigTest {
+
+ private static final Log log = Log.getLogger(ConfigTest.class).setLevel(Log.DEBUG);
+
+ public static final String getDataPath(String xdbFile) {
+ final CodeSource cs = ConfigTest.class.getProtectionDomain().getCodeSource();
+ if (cs != null) {
+ // log.debugf("code path: %s", cs.getLocation().getPath().concat("../../../../data/"));
+ return cs.getLocation().getPath().concat("../../../../data/").concat(xdbFile);
+ } else {
+ return "../../../../data/".concat(xdbFile);
+ }
+ }
+
+ @Test
+ public void testBuildV4Config() throws IOException, XdbException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.BufferCache)
+ .setXdbPath(getDataPath("ip2region_v4.xdb"))
+ .setSeachers(20)
+ .asV4();
+ log.debugf("builded config: %s", v4Config);
+ }
+
+ @Test
+ public void testBuildV6Config() throws IOException, XdbException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setXdbPath(getDataPath("ip2region_v6.xdb"))
+ .setSeachers(20)
+ .asV6();
+ log.debugf("builded config: %s", v4Config);
+ }
+}
\ No newline at end of file
diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/Ip2RegionTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/Ip2RegionTest.java
new file mode 100644
index 0000000..f5b562b
--- /dev/null
+++ b/binding/java/src/test/java/org/lionsoul/ip2region/Ip2RegionTest.java
@@ -0,0 +1,156 @@
+package org.lionsoul.ip2region;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.Test;
+import org.lionsoul.ip2region.xdb.InetAddressException;
+import org.lionsoul.ip2region.xdb.Log;
+import org.lionsoul.ip2region.xdb.Util;
+import org.lionsoul.ip2region.xdb.XdbException;
+
+public class Ip2RegionTest {
+
+ private static final Log log = Log.getLogger(Ip2RegionTest.class).setLevel(Log.DEBUG);
+
+ @Test
+ public void TestConfigCreate() throws IOException, XdbException, InetAddressException, InterruptedException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.NoCache)
+ .setSeachers(10)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v4.xdb"))
+ .asV4();
+
+ final Config v6Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setSeachers(10)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v6.xdb"))
+ .asV6();
+
+ byte[] v4Bytes = Util.parseIP("113.92.157.29");
+ byte[] v6Bytes = Util.parseIP("240e:3b7:3272:d8d0:db09:c067:8d59:539e");
+ final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);
+ for (int i = 0; i < 50; i++) {
+ v4Bytes = Util.ipAddOne(v4Bytes);
+ v6Bytes = Util.ipAddOne(v6Bytes);
+ final String v4Region = ip2Region.search(v4Bytes);
+ final String v6Region = ip2Region.search(v6Bytes);
+ log.debugf("search(%s)=%s, search(%s)=%s", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);
+ }
+
+ ip2Region.close();
+ log.debugf("ip2region closed gracefully");
+ }
+
+ @Test
+ public void TestPathCreate() throws InetAddressException, IOException, XdbException, InterruptedException {
+ byte[] v4Bytes = Util.parseIP("113.92.157.29");
+ byte[] v6Bytes = Util.parseIP("240e:3b7:3272:d8d0:db09:c067:8d59:539e");
+ final Ip2Region ip2Region = Ip2Region.create(ConfigTest.getDataPath("ip2region_v4.xdb"), ConfigTest.getDataPath("ip2region_v6.xdb"));
+ for (int i = 0; i < 50; i++) {
+ v4Bytes = Util.ipAddOne(v4Bytes);
+ v6Bytes = Util.ipAddOne(v6Bytes);
+ final String v4Region = ip2Region.search(v4Bytes);
+ final String v6Region = ip2Region.search(v6Bytes);
+ log.debugf("search(%s)=%s, search(%s)=%s", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);
+ }
+
+ ip2Region.close();
+ log.debugf("ip2region closed gracefully");
+ }
+
+ @Test
+ public void TestInMemSearch() throws IOException, XdbException, InetAddressException, InterruptedException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.BufferCache)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v4.xdb"))
+ .asV4();
+
+ final Config v6Config = Config.custom()
+ .setCachePolicy(Config.BufferCache)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v6.xdb"))
+ .asV6();
+
+ byte[] v4Bytes = Util.parseIP("113.92.157.29");
+ byte[] v6Bytes = Util.parseIP("240e:3b7:3272:d8d0:db09:c067:8d59:539e");
+ final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);
+ for (int i = 0; i < 50; i++) {
+ v4Bytes = Util.ipAddOne(v4Bytes);
+ v6Bytes = Util.ipAddOne(v6Bytes);
+ final String v4Region = ip2Region.search(v4Bytes);
+ final String v6Region = ip2Region.search(v6Bytes);
+ log.debugf("search(%s)=%s, search(%s)=%s", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);
+ }
+
+ ip2Region.close();
+ log.debugf("ip2region closed gracefully");
+ }
+
+ @Test
+ public void TestConcurrentCall() throws IOException, XdbException, InetAddressException, InterruptedException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setSeachers(15)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v4.xdb"))
+ .asV4();
+
+ final Config v6Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setSeachers(15)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v6.xdb"))
+ .asV6();
+
+ byte[] v4Bytes = Util.parseIP("113.92.157.29");
+ byte[] v6Bytes = Util.parseIP("240e:3b7:3272:d8d0:db09:c067:8d59:539e");
+ final int threads = 50;
+ final Ip2Region ip2Region = Ip2Region.create(v4Config, v6Config);
+ final CountDownLatch latch = new CountDownLatch(threads);
+ final long startTime = System.currentTimeMillis();
+ for (int i = 0; i < threads; i++) {
+ final Runnable t = new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < 1000; i++) {
+ final byte[] ipBytes = i % 2 == 0 ? v4Bytes : v6Bytes;
+ try {
+ ip2Region.search(ipBytes);
+ } catch (InetAddressException | IOException | InterruptedException e) {
+ log.errorf("failed to search(%s): %s", Util.ipToString(ipBytes), e.getMessage());
+ }
+ }
+
+ latch.countDown();
+ }
+ };
+ t.run();
+ }
+
+ latch.await();
+ final long costs = System.currentTimeMillis() - startTime;
+ log.debugf("all search finished in %dms", costs);
+ ip2Region.close();
+ log.debugf("ip2region closed gracefully");
+ }
+
+ @Test
+ public void TestV4Only() throws IOException, XdbException, InetAddressException, InterruptedException {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.NoCache)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v4.xdb"))
+ .asV4();
+
+ byte[] v4Bytes = Util.parseIP("113.92.157.29");
+ byte[] v6Bytes = Util.parseIP("240e:3b7:3272:d8d0:db09:c067:8d59:539e");
+ final Ip2Region ip2Region = Ip2Region.create(v4Config, null);
+ for (int i = 0; i < 10; i++) {
+ v4Bytes = Util.ipAddOne(v4Bytes);
+ final String v4Region = ip2Region.search(v4Bytes);
+ final String v6Region = ip2Region.search(v6Bytes);
+ log.debugf("search(%s)=%s, search(%s)=%s", Util.ipToString(v4Bytes), v4Region, Util.ipToString(v6Bytes), v6Region);
+ }
+
+ ip2Region.close();
+ log.debugf("ip2region closed gracefully");
+ }
+
+}
diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/SearcherPoolTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/SearcherPoolTest.java
new file mode 100644
index 0000000..e6a5157
--- /dev/null
+++ b/binding/java/src/test/java/org/lionsoul/ip2region/SearcherPoolTest.java
@@ -0,0 +1,58 @@
+package org.lionsoul.ip2region;
+
+import org.junit.Test;
+import org.lionsoul.ip2region.xdb.Log;
+import org.lionsoul.ip2region.xdb.Searcher;
+
+public class SearcherPoolTest {
+
+ private static final Log log = Log.getLogger(SearcherPoolTest.class).setLevel(Log.DEBUG);
+
+ @Test
+ public void testV4SeacherPool() throws Exception {
+ final Config v4Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setSeachers(5)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v4.xdb"))
+ .asV4();
+
+
+ final String ipStr = "58.250.36.41";
+ final SearcherPool v4Pool = SearcherPool.create(v4Config);
+ for (int i = 0; i < 20; i++) {
+ final Searcher searcher = v4Pool.borrowSearcher();
+ log.debugf("borrowed searcher %d: %s", i, searcher.toString());
+ final String region = searcher.search(ipStr);
+ log.debugf("search(%s)=%s", ipStr, region);
+ v4Pool.returnSearcher(searcher);
+ log.debugf("return searcher %d", i);
+ }
+
+ v4Pool.close();
+ log.debugf("v4 searcher pool closed gracefully");
+ }
+
+ @Test
+ public void testV6SeacherPool() throws Exception {
+ final Config v6Config = Config.custom()
+ .setCachePolicy(Config.VIndexCache)
+ .setSeachers(5)
+ .setXdbPath(ConfigTest.getDataPath("ip2region_v6.xdb"))
+ .asV6();
+
+
+ final String ipStr = "240e:3b7:3272:d8d0:db09:c067:8d59:539e";
+ final SearcherPool v4Pool = SearcherPool.create(v6Config);
+ for (int i = 0; i < 20; i++) {
+ final Searcher searcher = v4Pool.borrowSearcher();
+ log.debugf("borrowed searcher %d: %s", i, searcher.toString());
+ final String region = searcher.search(ipStr);
+ log.debugf("search(%s)=%s", ipStr, region);
+ v4Pool.returnSearcher(searcher);
+ log.debugf("return searcher %d", i);
+ }
+
+ v4Pool.close();
+ log.debugf("v6 searcher pool closed gracefully");
+ }
+}