How Douyin Implements IP Geolocation: A Java Backend Tutorial
This article explains how to retrieve a client’s real IP behind proxies, choose the appropriate forwarding header, and use the open‑source ip2region library to map the IP to a geographic location, with complete Java and Spring Boot code examples.
1. Background
Douyin added an IP‑location feature that displays a user's region when posting, commenting or chatting. To support similar functionality, a Java backend utility is built for obtaining the client IP, performing rate‑limiting and handling whitelist/blacklist.
2. Getting the client IP
In a web request the HttpServletRequest object holds the client address. When a reverse proxy (e.g., Nginx) is in front, request.getRemoteAddr() returns the proxy IP, so the real client IP must be read from forwarding headers. Common headers are:
X-Forwarded-For : added by Squid or other proxies; the first IP in the comma‑separated list is the client IP.
Proxy-Client-IP / WL-Proxy-Client-IP : set by Apache HTTP Server and WebLogic.
HTTP_CLIENT_IP
X-Real-IP : used by Nginx.
Because there is no standard, the code checks each header in order until a non‑empty, non‑"unknown" value is found.
@Slf4j
public class IpUtils {
private static final String UNKNOWN_VALUE = "unknown";
private static final String LOCALHOST_V4 = "127.0.0.1";
private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";
private static final String X_FORWARDED_FOR = "X-Forwarded-For";
private static final String X_REAL_IP = "X-Real-IP";
private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
private static byte[] contentBuff;
/**
* 获取客户端ip地址
* @param request
* @return
*/
public static String getRemoteHost(HttpServletRequest request) {
String ip = request.getHeader(X_FORWARDED_FOR);
if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
} else {
return ip;
}
}
ip = request.getHeader(X_REAL_IP);
if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
return ip;
}
if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
ip = request.getHeader(PROXY_CLIENT_IP);
}
if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
ip = request.getHeader(WL_PROXY_CLIENT_IP);
}
if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
ip = request.getHeader(HTTP_CLIENT_IP);
}
if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip;
}
}3. Mapping IP to location
Public IP‑to‑region services from Taobao, GeoIP or CZ88 are either unmaintained or paid. The open‑source ip2region library provides a 99.9 % accurate offline database ( .xdb) with microsecond‑level query latency.
3.1 ip2region features
Standardized data format : Country|Region|Province|City|ISP. Chinese entries are city‑level; other countries may only have country data.
Deduplication and compression : the generated ip2region.xdb file is about 11 MiB; size grows with data detail.
Fast query response : pure file queries take tens of microseconds. Two acceleration options are available: VectorIndex cache (~512 KiB) avoids one disk I/O per query, keeping average latency 10‑20 µs.
Loading the entire .xdb file into memory eliminates disk I/O, achieving sub‑microsecond latency.
v2.0 format supports billions of IP segments and allows custom region fields (e.g., GPS, postal code).
Accuracy : data aggregated from Taobao (≈80 %), GeoIP (≈10 %) and CZ88 (≈2 %) yields an overall 99.9 % accuracy.
3.2 Using ip2region in Java
Add the Maven dependency:
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.6.5</version>
</dependency>Three query modes are provided. The following examples demonstrate each mode.
import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
// 1. Create searcher (file‑only mode)
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s
", dbPath, e);
return;
}
// 2. Query
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}
", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s
", ip, e);
}
// 3. Close resource
searcher.close();
}
} import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";
// 1. Load VectorIndex cache
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s
", dbPath, e);
return;
}
// 2. Create searcher with VectorIndex
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s
", dbPath, e);
return;
}
// 3. Query
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}
", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s
", ip, e);
}
// 4. Close resource
searcher.close();
}
} import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;
public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";
// 1. Load entire xdb into memory
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s
", dbPath, e);
return;
}
// 2. Create memory‑cached searcher
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s
", e);
return;
}
// 3. Query
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}
", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s
", ip, e);
}
// 4. Close (optional for long‑running services)
// searcher.close();
}
}3.3 Spring Boot integration
In a Spring Boot project the lookup is wrapped in a utility class IpUtils. The class loads the ip2region.xdb file into a static byte array, creates a Searcher with newWithBuffer, parses the returned region string and populates an IpRegion object.
@Slf4j
public class IpUtils {
private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
private static byte[] contentBuff;
static {
try {
contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信
*/
public static IpRegion getIpRegion(String ip) {
Searcher searcher = null;
IpRegion ipRegion = new IpRegion();
try {
searcher = Searcher.newWithBuffer(contentBuff);
String region = searcher.search(ip);
String[] info = StringUtils.split(region, "|");
ipRegion.setCountry(info[0]);
ipRegion.setArea(info[1]);
ipRegion.setProvince(info[2]);
ipRegion.setCity(info[3]);
ipRegion.setIsp(info[4]);
} catch (Exception e) {
log.error("get ip region error: ", e);
} finally {
if (searcher != null) {
try {
searcher.close();
} catch (IOException e) {
log.error("close searcher error:", e);
}
}
}
return ipRegion;
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
