一、引言与概述
1.1 ip地址解析的重要性
在当今的互联网应用中,ip地址解析已成为许多系统不可或缺的功能。通过ip地址解析,我们可以:
- 地理位置服务:根据用户ip确定其所在地区,提供本地化内容
- 安全防护:识别异常登录地点,防范账号盗用
- 业务分析:分析用户地域分布,优化市场策略
- 访问控制:限制特定地区的访问权限
- 个性化体验:根据地区提供定制化服务
1.2 springboot集成ip解析的优势
springboot作为java生态中最流行的微服务框架,集成ip地址解析具有以下优势:
- 快速集成:通过starter可以快速引入ip解析功能
- 配置简单:基于约定大于配置的原则
- 生态丰富:可以轻松整合各种ip解析库
- 易于扩展:便于自定义解析逻辑
二、环境准备与基础配置
2.1 创建springboot项目
使用spring initializr创建基础项目:
curl https://start.spring.io/starter.zip \ -d type=maven-project \ -d language=java \ -d bootversion=3.2.0 \ -d basedir=ip-geolocation \ -d groupid=com.example \ -d artifactid=ip-geolocation \ -d name=ip-geolocation \ -d description=ip地址解析服务 \ -d packagename=com.example.ip \ -d packaging=jar \ -d javaversion=17 \ -d dependencies=web,validation,aop \ -o ip-geolocation.zip
2.2 基础依赖配置
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/pom/4.0.0"
xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xsi:schemalocation="http://maven.apache.org/pom/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0</modelversion>
<parent>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-parent</artifactid>
<version>3.2.0</version>
<relativepath/>
</parent>
<groupid>com.example</groupid>
<artifactid>ip-geolocation</artifactid>
<version>1.0.0</version>
<name>ip-geolocation</name>
<properties>
<java.version>17</java.version>
<geoip2.version>4.0.1</geoip2.version>
<ip2region.version>2.7.0</version>
<maxmind.db.version>3.0.0</version>
<caffeine.version>3.1.8</caffeine.version>
</properties>
<dependencies>
<!-- spring boot starters -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-validation</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-aop</artifactid>
</dependency>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-cache</artifactid>
</dependency>
<!-- ip解析库 -->
<dependency>
<groupid>com.maxmind.geoip2</groupid>
<artifactid>geoip2</artifactid>
<version>${geoip2.version}</version>
</dependency>
<!-- 本地ip库 -->
<dependency>
<groupid>org.lionsoul</groupid>
<artifactid>ip2region</artifactid>
<version>${ip2region.version}</version>
</dependency>
<!-- 缓存 -->
<dependency>
<groupid>com.github.ben-manes.caffeine</groupid>
<artifactid>caffeine</artifactid>
<version>${caffeine.version}</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupid>org.apache.commons</groupid>
<artifactid>commons-lang3</artifactid>
</dependency>
<dependency>
<groupid>com.google.guava</groupid>
<artifactid>guava</artifactid>
<version>32.1.3-jre</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-test</artifactid>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-maven-plugin</artifactid>
</plugin>
</plugins>
</build>
</project>2.3 配置文件
# application.yml
spring:
application:
name: ip-geolocation-service
cache:
type: caffeine
caffeine:
spec: maximumsize=10000,expireafterwrite=10m
# ip解析配置
ip:
geolocation:
# 使用哪种解析方式: offline(离线), online(在线), hybrid(混合)
mode: hybrid
# 离线解析配置
offline:
# 离线数据库类型: maxmind, ip2region
database: ip2region
# 数据库文件路径
maxmind-db-path: classpath:geoip/geolite2-city.mmdb
ip2region-db-path: classpath:geoip/ip2region.xdb
# 在线解析配置
online:
# 启用在线解析
enabled: true
# 在线服务提供商: ipapi, ipstack, taobao, baidu
providers:
- name: ipapi
url: http://ip-api.com/json/{ip}?lang=zh-cn
priority: 1
timeout: 3000
- name: taobao
url: http://ip.taobao.com/service/getipinfo.php?ip={ip}
priority: 2
timeout: 5000
# 缓存配置
cache:
enabled: true
# 本地缓存时间(秒)
local-ttl: 3600
# redis缓存时间(秒)
redis-ttl: 86400
# 监控配置
monitor:
enabled: true
# 统计窗口大小
window-size: 100
# 自定义配置
custom:
ip:
# 内网ip范围
internal-ranges:
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "127.0.0.0/8"
- "169.254.0.0/16"
# 敏感操作记录ip
sensitive-operations:
- "/api/admin/**"
- "/api/user/password/**"
- "/api/payment/**"三、ip地址解析基础理论
3.1 ip地址基础知识
ipv4与ipv6
// ip地址工具类
@component
public class ipaddressutils {
/**
* 验证ip地址格式
*/
public static boolean isvalidipaddress(string ip) {
if (ip == null || ip.isempty()) {
return false;
}
// ipv4验证
if (ip.contains(".")) {
return isvalidipv4(ip);
}
// ipv6验证
if (ip.contains(":")) {
return isvalidipv6(ip);
}
return false;
}
/**
* 验证ipv4地址
*/
private static boolean isvalidipv4(string ip) {
try {
string[] parts = ip.split("\\.");
if (parts.length != 4) {
return false;
}
for (string part : parts) {
int num = integer.parseint(part);
if (num < 0 || num > 255) {
return false;
}
}
return !ip.endswith(".");
} catch (numberformatexception e) {
return false;
}
}
/**
* 验证ipv6地址
*/
private static boolean isvalidipv6(string ip) {
try {
// 简化验证,实际项目可使用inet6address
if (ip == null || ip.isempty()) {
return false;
}
// 处理压缩格式
if (ip.contains("::")) {
if (ip.indexof("::") != ip.lastindexof("::")) {
return false; // 只能有一个::
}
}
// 分割各部分
string[] parts = ip.split(":");
if (parts.length > 8 || parts.length < 3) {
return false;
}
return true;
} catch (exception e) {
return false;
}
}
/**
* 将ip地址转换为长整型
*/
public static long iptolong(string ip) {
if (!isvalidipv4(ip)) {
throw new illegalargumentexception("invalid ipv4 address: " + ip);
}
string[] parts = ip.split("\\.");
long result = 0;
for (int i = 0; i < 4; i++) {
result = result << 8;
result += integer.parseint(parts[i]);
}
return result;
}
/**
* 将长整型转换为ip地址
*/
public static string longtoip(long ip) {
return ((ip >> 24) & 0xff) + "." +
((ip >> 16) & 0xff) + "." +
((ip >> 8) & 0xff) + "." +
(ip & 0xff);
}
/**
* 判断是否为内网ip
*/
public static boolean isinternalip(string ip) {
if (!isvalidipv4(ip)) {
return false;
}
long iplong = iptolong(ip);
// 10.0.0.0 - 10.255.255.255
if (iplong >= 0x0a000000l && iplong <= 0x0affffffl) {
return true;
}
// 172.16.0.0 - 172.31.255.255
if (iplong >= 0xac100000l && iplong <= 0xac1fffffl) {
return true;
}
// 192.168.0.0 - 192.168.255.255
if (iplong >= 0xc0a80000l && iplong <= 0xc0a8ffffl) {
return true;
}
// 127.0.0.0 - 127.255.255.255
if (iplong >= 0x7f000000l && iplong <= 0x7fffffffl) {
return true;
}
// 169.254.0.0 - 169.254.255.255
if (iplong >= 0xa9fe0000l && iplong <= 0xa9feffffl) {
return true;
}
return false;
}
/**
* 获取客户端真实ip(处理代理)
*/
public static string getclientip(httpservletrequest request) {
// 常见代理头
string[] headers = {
"x-forwarded-for",
"proxy-client-ip",
"wl-proxy-client-ip",
"http_x_forwarded_for",
"http_x_forwarded",
"http_x_cluster_client_ip",
"http_client_ip",
"http_forwarded_for",
"http_forwarded",
"http_via",
"remote_addr"
};
for (string header : headers) {
string ip = request.getheader(header);
if (ip != null && ip.length() > 0 && !"unknown".equalsignorecase(ip)) {
// 多次代理的情况,取第一个ip
if (ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
if (isvalidipaddress(ip) && !isinternalip(ip)) {
return ip;
}
}
}
// 如果没有获取到,使用远程地址
return request.getremoteaddr();
}
}3.2 cidr表示法与子网划分
@component
public class cidrutils {
/**
* cidr转ip范围
*/
public static long[] cidrtorange(string cidr) {
string[] parts = cidr.split("/");
if (parts.length != 2) {
throw new illegalargumentexception("invalid cidr format: " + cidr);
}
string ip = parts[0];
int prefixlength = integer.parseint(parts[1]);
long iplong = ipaddressutils.iptolong(ip);
long mask = 0xffffffffl << (32 - prefixlength);
long network = iplong & mask;
long broadcast = network | (~mask & 0xffffffffl);
return new long[]{network, broadcast};
}
/**
* 判断ip是否在cidr范围内
*/
public static boolean isipincidr(string ip, string cidr) {
long iplong = ipaddressutils.iptolong(ip);
long[] range = cidrtorange(cidr);
return iplong >= range[0] && iplong <= range[1];
}
/**
* 获取子网掩码
*/
public static string getsubnetmask(int prefixlength) {
long mask = 0xffffffffl << (32 - prefixlength);
return ipaddressutils.longtoip(mask);
}
/**
* 计算可用ip数量
*/
public static long getavailableipcount(string cidr) {
long[] range = cidrtorange(cidr);
return range[1] - range[0] + 1;
}
}四、离线ip地址解析方案
4.1 maxmind geoip2集成
4.1.1 数据库准备
@configuration
@configurationproperties(prefix = "ip.geolocation.offline")
@data
public class geoipconfig {
private string database = "maxmind";
private string maxminddbpath = "classpath:geoip/geolite2-city.mmdb";
private string ip2regiondbpath = "classpath:geoip/ip2region.xdb";
@bean
@conditionalonproperty(name = "ip.geolocation.offline.database",
havingvalue = "maxmind")
public databasereader maxminddatabasereader() throws ioexception {
resource resource = new classpathresource(
maxminddbpath.replace("classpath:", ""));
file database = resource.getfile();
return new databasereader.builder(database).build();
}
}4.1.2 maxmind解析服务实现
@service
@slf4j
@conditionalonproperty(name = "ip.geolocation.offline.database",
havingvalue = "maxmind")
public class maxmindgeoipservice implements geoipservice {
private final databasereader databasereader;
private final cache<string, geolocation> cache;
public maxmindgeoipservice(databasereader databasereader) {
this.databasereader = databasereader;
// 初始化缓存
this.cache = caffeine.newbuilder()
.maximumsize(10000)
.expireafterwrite(10, timeunit.minutes)
.recordstats()
.build();
}
@override
public geolocation query(string ip) {
// 先从缓存获取
return cache.get(ip, this::queryfromdatabase);
}
private geolocation queryfromdatabase(string ip) {
try {
inetaddress ipaddress = inetaddress.getbyname(ip);
cityresponse response = databasereader.city(ipaddress);
geolocation location = new geolocation();
location.setip(ip);
location.setcountry(response.getcountry().getname());
location.setcountrycode(response.getcountry().getisocode());
location.setregion(response.getmostspecificsubdivision().getname());
location.setcity(response.getcity().getname());
location.setpostalcode(response.getpostal().getcode());
if (response.getlocation() != null) {
location.setlatitude(response.getlocation().getlatitude());
location.setlongitude(response.getlocation().getlongitude());
location.settimezone(response.getlocation().gettimezone());
}
location.setsource("maxmind");
location.settimestamp(system.currenttimemillis());
return location;
} catch (addressnotfoundexception e) {
log.warn("ip address not found in database: {}", ip);
return createunknownlocation(ip);
} catch (exception e) {
log.error("error querying maxmind database for ip: {}", ip, e);
throw new geoipexception("failed to query ip location", e);
}
}
private geolocation createunknownlocation(string ip) {
geolocation location = new geolocation();
location.setip(ip);
location.setcountry("unknown");
location.setsource("maxmind");
location.settimestamp(system.currenttimemillis());
return location;
}
@override
public boolean isavailable() {
return databasereader != null;
}
@override
public string getprovidername() {
return "maxmind";
}
@predestroy
public void shutdown() {
try {
if (databasereader != null) {
databasereader.close();
}
} catch (ioexception e) {
log.error("error closing maxmind database reader", e);
}
}
}4.2 ip2region本地库集成
ip2region配置
@slf4j
@service
@conditionalonproperty(name = "ip.geolocation.offline.database",
havingvalue = "ip2region")
public class ip2regionservice implements geoipservice {
private searcher searcher;
private final cache<string, geolocation> cache;
@value("${ip.geolocation.offline.ip2region-db-path}")
private string dbpath;
public ip2regionservice() {
// 初始化缓存
this.cache = caffeine.newbuilder()
.maximumsize(10000)
.expireafterwrite(10, timeunit.minutes)
.recordstats()
.build();
// 延迟初始化数据库
initializedatabase();
}
private void initializedatabase() {
try {
resource resource = new classpathresource(
dbpath.replace("classpath:", ""));
// 加载数据库文件到内存
byte[] dbbinstr = files.readallbytes(resource.getfile().topath());
// 创建完全基于内存的查询对象
this.searcher = searcher.newwithbuffer(dbbinstr);
log.info("ip2region database loaded successfully");
} catch (exception e) {
log.error("failed to initialize ip2region database", e);
throw new geoipexception("failed to initialize ip2region", e);
}
}
@override
public geolocation query(string ip) {
return cache.get(ip, this::queryfromdatabase);
}
private geolocation queryfromdatabase(string ip) {
try {
string region = searcher.search(ip);
// ip2region格式:国家|区域|省份|城市|isp
string[] regions = region.split("\\|");
geolocation location = new geolocation();
location.setip(ip);
if (regions.length >= 5) {
location.setcountry(parsecountry(regions[0]));
location.setregion(regions[2]); // 省份
location.setcity(regions[3]); // 城市
location.setisp(regions[4]); // isp
}
location.setsource("ip2region");
location.settimestamp(system.currenttimemillis());
return location;
} catch (exception e) {
log.error("error querying ip2region for ip: {}", ip, e);
return createunknownlocation(ip);
}
}
private string parsecountry(string countrystr) {
if ("中国".equals(countrystr)) {
return "china";
} else if ("0".equals(countrystr)) {
return "unknown";
}
return countrystr;
}
private geolocation createunknownlocation(string ip) {
geolocation location = new geolocation();
location.setip(ip);
location.setcountry("unknown");
location.setsource("ip2region");
location.settimestamp(system.currenttimemillis());
return location;
}
@override
public boolean isavailable() {
return searcher != null;
}
@override
public string getprovidername() {
return "ip2region";
}
}4.3 实体类定义
@data
@noargsconstructor
@allargsconstructor
@builder
public class geolocation {
/**
* 查询的ip地址
*/
private string ip;
/**
* 国家名称
*/
private string country;
/**
* 国家代码(iso 3166-1 alpha-2)
*/
private string countrycode;
/**
* 区域/省份
*/
private string region;
/**
* 城市
*/
private string city;
/**
* 区县
*/
private string district;
/**
* 邮政编码
*/
private string postalcode;
/**
* 纬度
*/
private double latitude;
/**
* 经度
*/
private double longitude;
/**
* 时区
*/
private string timezone;
/**
* 互联网服务提供商
*/
private string isp;
/**
* 组织
*/
private string organization;
/**
* as号码和名称
*/
private string as;
/**
* as名称
*/
private string asname;
/**
* 是否移动网络
*/
private boolean mobile;
/**
* 是否代理
*/
private boolean proxy;
/**
* 是否托管
*/
private boolean hosting;
/**
* 数据来源
*/
private string source;
/**
* 查询时间戳
*/
private long timestamp;
/**
* 缓存过期时间
*/
private long expiresat;
/**
* 原始响应数据
*/
private string rawdata;
/**
* 是否成功
*/
@builder.default
private boolean success = true;
/**
* 错误信息
*/
private string error;
/**
* 响应时间(毫秒)
*/
private long responsetime;
/**
* 是否内网ip
*/
private boolean internal;
/**
* 可信度评分(0-100)
*/
@builder.default
private integer confidence = 100;
}五、在线ip地址解析方案
5.1 多服务提供商集成
@data
@configurationproperties(prefix = "ip.geolocation.online")
@configuration
public class onlineproviderconfig {
@data
public static class provider {
private string name;
private string url;
private integer priority = 1;
private integer timeout = 3000;
private string apikey;
private boolean enabled = true;
private map<string, string> headers = new hashmap<>();
private map<string, string> params = new hashmap<>();
}
private list<provider> providers = new arraylist<>();
@bean
public list<geoipprovider> geoipproviders(resttemplate resttemplate) {
return providers.stream()
.filter(provider::getenabled)
.sorted(comparator.comparing(provider::getpriority))
.map(config -> createprovider(config, resttemplate))
.collect(collectors.tolist());
}
private geoipprovider createprovider(provider config, resttemplate resttemplate) {
switch (config.getname().tolowercase()) {
case "ipapi":
return new ipapiprovider(config, resttemplate);
case "ipstack":
return new ipstackprovider(config, resttemplate);
case "taobao":
return new taobaoipprovider(config, resttemplate);
case "baidu":
return new baiduipprovider(config, resttemplate);
default:
throw new illegalargumentexception(
"unknown provider: " + config.getname());
}
}
}5.2 服务提供商实现
1ip-api.com实现
@slf4j
@component
public class ipapiprovider implements geoipprovider {
private final resttemplate resttemplate;
private final onlineproviderconfig.provider config;
public ipapiprovider(onlineproviderconfig.provider config,
resttemplate resttemplate) {
this.config = config;
this.resttemplate = resttemplate;
}
@override
public geolocation query(string ip) {
long starttime = system.currenttimemillis();
try {
string url = buildurl(ip);
responseentity<map> response = resttemplate.exchange(
url, httpmethod.get, null, map.class);
map<string, object> data = response.getbody();
if (data == null) {
throw new geoipexception("empty response from ip-api");
}
string status = (string) data.get("status");
if (!"success".equals(status)) {
string message = (string) data.get("message");
throw new geoipexception("ip-api error: " + message);
}
geolocation location = parseresponse(data);
location.setresponsetime(system.currenttimemillis() - starttime);
return location;
} catch (exception e) {
log.warn("failed to query ip-api for ip: {}", ip, e);
throw new geoipexception("ip-api query failed", e);
}
}
private string buildurl(string ip) {
return config.geturl().replace("{ip}", ip);
}
private geolocation parseresponse(map<string, object> data) {
return geolocation.builder()
.country((string) data.get("country"))
.countrycode((string) data.get("countrycode"))
.region((string) data.get("regionname"))
.city((string) data.get("city"))
.postalcode((string) data.get("zip"))
.latitude(double.parsedouble(data.get("lat").tostring()))
.longitude(double.parsedouble(data.get("lon").tostring()))
.timezone((string) data.get("timezone"))
.isp((string) data.get("isp"))
.organization((string) data.get("org"))
.as((string) data.get("as"))
.mobile(boolean.parseboolean(data.get("mobile").tostring()))
.proxy(boolean.parseboolean(data.get("proxy").tostring()))
.hosting(boolean.parseboolean(data.get("hosting").tostring()))
.source("ip-api")
.timestamp(system.currenttimemillis())
.rawdata(jsonutils.tojson(data))
.build();
}
@override
public string getname() {
return "ip-api";
}
@override
public int getpriority() {
return config.getpriority();
}
}淘宝ip库实现
@slf4j
@component
public class taobaoipprovider implements geoipprovider {
private final resttemplate resttemplate;
private final onlineproviderconfig.provider config;
public taobaoipprovider(onlineproviderconfig.provider config,
resttemplate resttemplate) {
this.config = config;
this.resttemplate = resttemplate;
}
@override
public geolocation query(string ip) {
long starttime = system.currenttimemillis();
try {
string url = buildurl(ip);
responseentity<string> response = resttemplate.exchange(
url, httpmethod.get, null, string.class);
string responsebody = response.getbody();
if (responsebody == null) {
throw new geoipexception("empty response from taobao ip");
}
// 解析淘宝返回的json
map<string, object> data = jsonutils.parse(responsebody, map.class);
number code = (number) data.get("code");
if (code == null || code.intvalue() != 0) {
throw new geoipexception("taobao ip api error: " + data.get("msg"));
}
map<string, object> ipdata = (map<string, object>) data.get("data");
geolocation location = parseresponse(ip, ipdata);
location.setresponsetime(system.currenttimemillis() - starttime);
return location;
} catch (exception e) {
log.warn("failed to query taobao ip for ip: {}", ip, e);
throw new geoipexception("taobao ip query failed", e);
}
}
private string buildurl(string ip) {
return config.geturl().replace("{ip}", ip);
}
private geolocation parseresponse(string ip, map<string, object> data) {
return geolocation.builder()
.ip(ip)
.country("中国")
.countrycode("cn")
.region((string) data.get("region"))
.city((string) data.get("city"))
.isp((string) data.get("isp"))
.source("taobao")
.timestamp(system.currenttimemillis())
.rawdata(jsonutils.tojson(data))
.build();
}
@override
public string getname() {
return "taobao ip";
}
@override
public int getpriority() {
return config.getpriority();
}
}5.3 统一调用管理器
@service
@slf4j
public class geoipmanager implements geoipservice {
private final list<geoipprovider> onlineproviders;
private final list<geoipservice> offlineservices;
private final cache<string, geolocation> cache;
private final geoipproperties properties;
@autowired
public geoipmanager(list<geoipprovider> onlineproviders,
list<geoipservice> offlineservices,
geoipproperties properties) {
this.onlineproviders = onlineproviders;
this.offlineservices = offlineservices;
this.properties = properties;
// 初始化缓存
this.cache = caffeine.newbuilder()
.maximumsize(properties.getcache().getmaximumsize())
.expireafterwrite(properties.getcache().getlocalttl(),
timeunit.seconds)
.recordstats()
.build();
}
@override
public geolocation query(string ip) {
// 参数验证
if (!ipaddressutils.isvalidipaddress(ip)) {
throw new illegalargumentexception("invalid ip address: " + ip);
}
// 检查是否为内网ip
if (ipaddressutils.isinternalip(ip)) {
return createinternallocation(ip);
}
// 尝试从缓存获取
geolocation cached = cache.getifpresent(ip);
if (cached != null &&
cached.getexpiresat() > system.currenttimemillis()) {
return cached;
}
// 根据配置模式执行查询
geolocation location;
switch (properties.getmode()) {
case "offline":
location = queryoffline(ip);
break;
case "online":
location = queryonline(ip);
break;
case "hybrid":
default:
location = queryhybrid(ip);
break;
}
// 设置缓存过期时间
if (location != null && location.getsuccess()) {
location.setexpiresat(
system.currenttimemillis() +
properties.getcache().getlocalttl() * 1000);
cache.put(ip, location);
}
return location;
}
private geolocation queryoffline(string ip) {
for (geoipservice service : offlineservices) {
try {
if (service.isavailable()) {
return service.query(ip);
}
} catch (exception e) {
log.warn("offline service {} failed for ip: {}",
service.getprovidername(), ip, e);
}
}
throw new geoipexception("all offline services failed");
}
private geolocation queryonline(string ip) {
for (geoipprovider provider : onlineproviders) {
try {
geolocation location = provider.query(ip);
if (location != null && location.getsuccess()) {
return location;
}
} catch (exception e) {
log.warn("online provider {} failed for ip: {}",
provider.getname(), ip, e);
}
}
throw new geoipexception("all online providers failed");
}
private geolocation queryhybrid(string ip) {
// 首先尝试离线查询
for (geoipservice service : offlineservices) {
try {
if (service.isavailable()) {
return service.query(ip);
}
} catch (exception e) {
log.debug("offline service failed, trying online providers");
}
}
// 离线失败则尝试在线查询
return queryonline(ip);
}
private geolocation createinternallocation(string ip) {
return geolocation.builder()
.ip(ip)
.country("internal")
.city("internal network")
.internal(true)
.success(true)
.source("system")
.timestamp(system.currenttimemillis())
.build();
}
@override
public boolean isavailable() {
return !offlineservices.isempty() || !onlineproviders.isempty();
}
@override
public string getprovidername() {
return "geoipmanager";
}
/**
* 批量查询
*/
public map<string, geolocation> batchquery(list<string> ips) {
map<string, geolocation> results = new concurrenthashmap<>();
executorservice executor = executors.newfixedthreadpool(
math.min(ips.size(), 10));
list<future<?>> futures = new arraylist<>();
for (string ip : ips) {
futures.add(executor.submit(() -> {
try {
geolocation location = query(ip);
results.put(ip, location);
} catch (exception e) {
log.error("failed to query ip: {}", ip, e);
results.put(ip, createerrorlocation(ip, e.getmessage()));
}
}));
}
// 等待所有任务完成
for (future<?> future : futures) {
try {
future.get();
} catch (exception e) {
log.error("error waiting for query task", e);
}
}
executor.shutdown();
return results;
}
private geolocation createerrorlocation(string ip, string error) {
return geolocation.builder()
.ip(ip)
.success(false)
.error(error)
.source("system")
.timestamp(system.currenttimemillis())
.build();
}
/**
* 获取缓存统计信息
*/
public cachestats getcachestats() {
return cache.stats();
}
/**
* 清空缓存
*/
public void clearcache() {
cache.invalidateall();
}
}六、springboot整合与配置
6.1 自动配置类
@configuration
@enableconfigurationproperties(geoipproperties.class)
@conditionalonclass(geoipservice.class)
@autoconfigureafter(webmvcautoconfiguration.class)
public class geoipautoconfiguration {
@bean
@conditionalonmissingbean
public resttemplate geoipresttemplate() {
resttemplate resttemplate = new resttemplate();
// 设置超时时间
simpleclienthttprequestfactory factory =
new simpleclienthttprequestfactory();
factory.setconnecttimeout(5000);
factory.setreadtimeout(10000);
resttemplate.setrequestfactory(factory);
// 添加拦截器
resttemplate.getinterceptors().add(new geoiprequestinterceptor());
return resttemplate;
}
@bean
@conditionalonmissingbean
public objectmapper geoipobjectmapper() {
objectmapper mapper = new objectmapper();
mapper.configure(deserializationfeature.fail_on_unknown_properties, false);
mapper.setpropertynamingstrategy(propertynamingstrategies.snake_case);
mapper.registermodule(new javatimemodule());
mapper.disable(serializationfeature.write_dates_as_timestamps);
return mapper;
}
@bean
@conditionalonmissingbean
public geoipmanager geoipmanager(list<geoipprovider> onlineproviders,
list<geoipservice> offlineservices,
geoipproperties properties) {
return new geoipmanager(onlineproviders, offlineservices, properties);
}
@bean
@conditionalonmissingbean
public geoipaspect geoipaspect(geoipmanager geoipmanager) {
return new geoipaspect(geoipmanager);
}
@bean
@conditionalonmissingbean
public ipaddressutils ipaddressutils() {
return new ipaddressutils();
}
}
/**
* http请求拦截器
*/
class geoiprequestinterceptor implements clienthttprequestinterceptor {
@override
public clienthttpresponse intercept(httprequest request, byte[] body,
clienthttprequestexecution execution)
throws ioexception {
// 添加user-agent
request.getheaders().add("user-agent",
"mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36");
// 记录请求开始时间
long starttime = system.currenttimemillis();
clienthttpresponse response = execution.execute(request, body);
// 记录请求耗时
long duration = system.currenttimemillis() - starttime;
log.debug("http request to {} completed in {}ms",
request.geturi(), duration);
return response;
}
}6.2 属性配置类
@data
@configurationproperties(prefix = "ip.geolocation")
@validated
public class geoipproperties {
@notnull
@pattern(regexp = "offline|online|hybrid")
private string mode = "hybrid";
private offline offline = new offline();
private online online = new online();
private cache cache = new cache();
private monitor monitor = new monitor();
@data
public static class offline {
private string database = "ip2region";
private string maxminddbpath = "classpath:geoip/geolite2-city.mmdb";
private string ip2regiondbpath = "classpath:geoip/ip2region.xdb";
private boolean enabled = true;
}
@data
public static class online {
private boolean enabled = true;
private integer timeout = 5000;
private integer retrycount = 2;
private list<provider> providers = new arraylist<>();
}
@data
public static class provider {
private string name;
private string url;
private integer priority = 1;
private integer timeout = 3000;
private string apikey;
private boolean enabled = true;
}
@data
public static class cache {
private boolean enabled = true;
private long localttl = 3600l;
private long redisttl = 86400l;
private long maximumsize = 10000l;
private boolean recordstats = true;
}
@data
public static class monitor {
private boolean enabled = true;
private integer windowsize = 100;
private long statsinterval = 60000l;
}
}6.3 aop切面处理
@aspect
@component
@slf4j
public class geoipaspect {
private final geoipmanager geoipmanager;
private final threadlocal<geolocation> currentlocation = new threadlocal<>();
public geoipaspect(geoipmanager geoipmanager) {
this.geoipmanager = geoipmanager;
}
/**
* 拦截controller方法,自动注入ip位置信息
*/
@around("@annotation(org.springframework.web.bind.annotation.requestmapping) || " +
"@annotation(org.springframework.web.bind.annotation.getmapping) || " +
"@annotation(org.springframework.web.bind.annotation.postmapping) || " +
"@annotation(org.springframework.web.bind.annotation.putmapping) || " +
"@annotation(org.springframework.web.bind.annotation.deletemapping)")
public object injectgeolocation(proceedingjoinpoint joinpoint) throws throwable {
// 获取httpservletrequest
httpservletrequest request = gethttpservletrequest(joinpoint);
if (request != null) {
// 获取客户端ip
string clientip = ipaddressutils.getclientip(request);
// 查询位置信息
geolocation location = geoipmanager.query(clientip);
// 存储到threadlocal
currentlocation.set(location);
// 设置到请求属性中
request.setattribute("geolocation", location);
request.setattribute("clientip", clientip);
log.debug("injected geo location for ip: {}, country: {}",
clientip, location.getcountry());
}
try {
return joinpoint.proceed();
} finally {
// 清理threadlocal
currentlocation.remove();
}
}
/**
* 获取当前请求的位置信息
*/
public geolocation getcurrentlocation() {
return currentlocation.get();
}
private httpservletrequest gethttpservletrequest(proceedingjoinpoint joinpoint) {
object[] args = joinpoint.getargs();
for (object arg : args) {
if (arg instanceof httpservletrequest) {
return (httpservletrequest) arg;
}
}
// 尝试从requestcontextholder获取
servletrequestattributes attributes =
(servletrequestattributes) requestcontextholder.getrequestattributes();
if (attributes != null) {
return attributes.getrequest();
}
return null;
}
}七、rest api设计
7.1 控制器设计
@restcontroller
@requestmapping("/api/v1/ip")
@validated
@slf4j
@tag(name = "ip地址解析", description = "ip地理位置查询api")
public class ipgeocontroller {
private final geoipmanager geoipmanager;
private final ipaddressutils ipaddressutils;
@autowired
public ipgeocontroller(geoipmanager geoipmanager,
ipaddressutils ipaddressutils) {
this.geoipmanager = geoipmanager;
this.ipaddressutils = ipaddressutils;
}
/**
* 查询单个ip地址信息
*/
@getmapping("/query")
@operation(summary = "查询ip地理位置",
description = "根据ip地址查询地理位置信息")
@apiresponse(responsecode = "200", description = "查询成功")
@apiresponse(responsecode = "400", description = "请求参数错误")
public responseentity<apiresponse<geolocation>> queryip(
@requestparam @pattern(regexp =
"^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$",
message = "ip地址格式不正确") string ip) {
geolocation location = geoipmanager.query(ip);
return responseentity.ok(apiresponse.success(location));
}
/**
* 查询当前请求的ip地址信息
*/
@getmapping("/current")
@operation(summary = "查询当前请求ip",
description = "获取当前请求客户端的地理位置信息")
public responseentity<apiresponse<geolocation>> querycurrentip(
httpservletrequest request) {
string clientip = ipaddressutils.getclientip(request);
geolocation location = geoipmanager.query(clientip);
return responseentity.ok(apiresponse.success(location));
}
/**
* 批量查询ip地址信息
*/
@postmapping("/batch-query")
@operation(summary = "批量查询ip地理位置",
description = "批量查询多个ip地址的地理位置信息")
public responseentity<apiresponse<map<string, geolocation>>> batchqueryip(
@requestbody @valid batchqueryrequest request) {
// 限制批量查询数量
if (request.getips().size() > 100) {
throw new illegalargumentexception("批量查询最多支持100个ip地址");
}
// 验证ip地址格式
for (string ip : request.getips()) {
if (!ipaddressutils.isvalidipaddress(ip)) {
throw new illegalargumentexception("无效的ip地址: " + ip);
}
}
map<string, geolocation> results = geoipmanager.batchquery(request.getips());
return responseentity.ok(apiresponse.success(results));
}
/**
* 验证ip地址
*/
@getmapping("/validate")
@operation(summary = "验证ip地址",
description = "验证ip地址格式和类型")
public responseentity<apiresponse<ipvalidationresult>> validateip(
@requestparam string ip) {
boolean isvalid = ipaddressutils.isvalidipaddress(ip);
boolean isinternal = ipaddressutils.isinternalip(ip);
string iptype = ip.contains(":") ? "ipv6" : "ipv4";
ipvalidationresult result = ipvalidationresult.builder()
.ip(ip)
.valid(isvalid)
.internal(isinternal)
.type(iptype)
.build();
return responseentity.ok(apiresponse.success(result));
}
/**
* 获取服务状态
*/
@getmapping("/status")
@operation(summary = "获取服务状态",
description = "获取ip解析服务的状态信息")
public responseentity<apiresponse<servicestatus>> getservicestatus() {
cachestats stats = geoipmanager.getcachestats();
servicestatus status = servicestatus.builder()
.cachehits(stats.hitcount())
.cachemisses(stats.misscount())
.cachehitrate(stats.hitrate())
.cachesize(stats.evictioncount())
.build();
return responseentity.ok(apiresponse.success(status));
}
/**
* 清空缓存
*/
@postmapping("/cache/clear")
@operation(summary = "清空缓存",
description = "清空ip地理位置查询缓存")
@preauthorize("hasrole('admin')")
public responseentity<apiresponse<void>> clearcache() {
geoipmanager.clearcache();
return responseentity.ok(apiresponse.success());
}
@data
@builder
public static class ipvalidationresult {
private string ip;
private boolean valid;
private boolean internal;
private string type;
private string message;
}
@data
@builder
public static class servicestatus {
private long cachehits;
private long cachemisses;
private double cachehitrate;
private long cachesize;
private date timestamp;
}
@data
public static class batchqueryrequest {
@notnull
@size(min = 1, max = 100, message = "ip数量必须在1-100之间")
private list<string> ips;
}
}7.2 响应封装
@data
@noargsconstructor
@allargsconstructor
@builder
public class apiresponse<t> {
private boolean success;
private string code;
private string message;
private t data;
private long timestamp;
private string requestid;
public static <t> apiresponse<t> success(t data) {
return apiresponse.<t>builder()
.success(true)
.code("200")
.message("success")
.data(data)
.timestamp(system.currenttimemillis())
.build();
}
public static apiresponse<void> success() {
return apiresponse.<void>builder()
.success(true)
.code("200")
.message("success")
.timestamp(system.currenttimemillis())
.build();
}
public static apiresponse<void> error(string code, string message) {
return apiresponse.<void>builder()
.success(false)
.code(code)
.message(message)
.timestamp(system.currenttimemillis())
.build();
}
}7.3 全局异常处理
@restcontrolleradvice
@slf4j
public class globalexceptionhandler {
@exceptionhandler(geoipexception.class)
public responseentity<apiresponse<void>> handlegeoipexception(
geoipexception ex) {
log.error("geoip service error", ex);
return responseentity.status(httpstatus.internal_server_error)
.body(apiresponse.error("geoip_error", ex.getmessage()));
}
@exceptionhandler(illegalargumentexception.class)
public responseentity<apiresponse<void>> handleillegalargumentexception(
illegalargumentexception ex) {
return responseentity.status(httpstatus.bad_request)
.body(apiresponse.error("invalid_param", ex.getmessage()));
}
@exceptionhandler(constraintviolationexception.class)
public responseentity<apiresponse<void>> handleconstraintviolationexception(
constraintviolationexception ex) {
string message = ex.getconstraintviolations().stream()
.map(constraintviolation::getmessage)
.collect(collectors.joining(", "));
return responseentity.status(httpstatus.bad_request)
.body(apiresponse.error("validation_error", message));
}
@exceptionhandler(exception.class)
public responseentity<apiresponse<void>> handlegenericexception(
exception ex) {
log.error("unexpected error", ex);
return responseentity.status(httpstatus.internal_server_error)
.body(apiresponse.error("internal_error", "internal server error"));
}
}
/**
* 自定义异常类
*/
public class geoipexception extends runtimeexception {
public geoipexception(string message) {
super(message);
}
public geoipexception(string message, throwable cause) {
super(message, cause);
}
}八、高级功能实现
8.1 ip地址库自动更新
@component
@slf4j
public class geoipdatabaseupdater {
private final geoipproperties properties;
private final applicationeventpublisher eventpublisher;
@autowired
public geoipdatabaseupdater(geoipproperties properties,
applicationeventpublisher eventpublisher) {
this.properties = properties;
this.eventpublisher = eventpublisher;
}
@scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void scheduledupdate() {
log.info("starting scheduled geoip database update");
try {
if ("maxmind".equals(properties.getoffline().getdatabase())) {
updatemaxminddatabase();
} else if ("ip2region".equals(properties.getoffline().getdatabase())) {
updateip2regiondatabase();
}
log.info("geoip database update completed successfully");
// 发布更新完成事件
eventpublisher.publishevent(new databaseupdateevent(this, true));
} catch (exception e) {
log.error("failed to update geoip database", e);
// 发布更新失败事件
eventpublisher.publishevent(new databaseupdateevent(this, false));
}
}
private void updatemaxminddatabase() throws ioexception {
string downloadurl = "https://download.maxmind.com/app/geoip_download" +
"?edition_id=geolite2-city&license_key=your_license_key&suffix=tar.gz";
// 下载数据库文件
file tempfile = downloadfile(downloadurl);
// 解压文件
file extracteddir = extracttargz(tempfile);
// 查找.mmdb文件
file mmdbfile = findmmdbfile(extracteddir);
if (mmdbfile == null) {
throw new ioexception("could not find .mmdb file in downloaded archive");
}
// 备份原文件
file originalfile = new file(properties.getoffline().getmaxminddbpath()
.replace("classpath:", ""));
file backupfile = new file(originalfile.getparent(),
originalfile.getname() + ".bak");
files.copy(originalfile.topath(), backupfile.topath(),
standardcopyoption.replace_existing);
// 替换数据库文件
files.copy(mmdbfile.topath(), originalfile.topath(),
standardcopyoption.replace_existing);
// 清理临时文件
cleantempfiles(tempfile, extracteddir);
log.info("maxmind database updated successfully");
}
private void updateip2regiondatabase() throws ioexception {
string downloadurl = "https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.xdb";
// 下载数据库文件
file tempfile = downloadfile(downloadurl);
// 备份原文件
file originalfile = new file(properties.getoffline().getip2regiondbpath()
.replace("classpath:", ""));
file backupfile = new file(originalfile.getparent(),
originalfile.getname() + ".bak");
files.copy(originalfile.topath(), backupfile.topath(),
standardcopyoption.replace_existing);
// 替换数据库文件
files.copy(tempfile.topath(), originalfile.topath(),
standardcopyoption.replace_existing);
// 清理临时文件
tempfile.delete();
log.info("ip2region database updated successfully");
}
private file downloadfile(string url) throws ioexception {
resttemplate resttemplate = new resttemplate();
responseentity<byte[]> response = resttemplate.exchange(
url, httpmethod.get, null, byte[].class);
file tempfile = file.createtempfile("geoip", ".tmp");
files.write(tempfile.topath(), response.getbody());
return tempfile;
}
private file extracttargz(file targzfile) throws ioexception {
file outputdir = new file(targzfile.getparent(), "extracted");
try (tararchiveinputstream tarinput = new tararchiveinputstream(
new gzipinputstream(new fileinputstream(targzfile)))) {
tararchiveentry entry;
while ((entry = tarinput.getnextentry()) != null) {
file outputfile = new file(outputdir, entry.getname());
if (entry.isdirectory()) {
outputfile.mkdirs();
} else {
outputfile.getparentfile().mkdirs();
try (fileoutputstream fos = new fileoutputstream(outputfile)) {
ioutils.copy(tarinput, fos);
}
}
}
}
return outputdir;
}
private file findmmdbfile(file directory) {
file[] files = directory.listfiles((dir, name) ->
name.endswith(".mmdb"));
if (files != null && files.length > 0) {
return files[0];
}
// 递归查找子目录
file[] subdirs = directory.listfiles(file::isdirectory);
if (subdirs != null) {
for (file subdir : subdirs) {
file mmdbfile = findmmdbfile(subdir);
if (mmdbfile != null) {
return mmdbfile;
}
}
}
return null;
}
private void cleantempfiles(file tempfile, file extracteddir) {
try {
tempfile.delete();
deletedirectory(extracteddir);
} catch (exception e) {
log.warn("failed to clean temp files", e);
}
}
private void deletedirectory(file directory) throws ioexception {
if (directory.exists()) {
file[] files = directory.listfiles();
if (files != null) {
for (file file : files) {
if (file.isdirectory()) {
deletedirectory(file);
} else {
file.delete();
}
}
}
directory.delete();
}
}
}
/**
* 数据库更新事件
*/
public class databaseupdateevent extends applicationevent {
private final boolean success;
private final date timestamp;
public databaseupdateevent(object source, boolean success) {
super(source);
this.success = success;
this.timestamp = new date();
}
public boolean issuccess() {
return success;
}
public date gettimestamp() {
return timestamp;
}
}8.2 ip访问频率限制
@component
@slf4j
public class ipratelimiter {
private final cache<string, ratelimitinfo> ratelimitcache;
private final list<string> excludedips;
public ipratelimiter() {
this.ratelimitcache = caffeine.newbuilder()
.maximumsize(100000)
.expireafterwrite(1, timeunit.hours)
.build();
// 从配置文件加载排除的ip列表
this.excludedips = loadexcludedips();
}
/**
* 检查ip是否超过频率限制
*/
public boolean isratelimited(string ip, string endpoint) {
// 排除的ip不受限制
if (excludedips.contains(ip)) {
return false;
}
string key = ip + ":" + endpoint;
ratelimitinfo info = ratelimitcache.getifpresent(key);
if (info == null) {
info = new ratelimitinfo();
ratelimitcache.put(key, info);
}
return info.isratelimited();
}
/**
* 记录ip访问
*/
public void recordaccess(string ip, string endpoint) {
string key = ip + ":" + endpoint;
ratelimitinfo info = ratelimitcache.getifpresent(key);
if (info == null) {
info = new ratelimitinfo();
}
info.recordaccess();
ratelimitcache.put(key, info);
}
/**
* 获取ip的访问统计
*/
public ratelimitinfo getratelimitinfo(string ip, string endpoint) {
string key = ip + ":" + endpoint;
return ratelimitcache.getifpresent(key);
}
/**
* 清除ip的限制
*/
public void clearratelimit(string ip, string endpoint) {
string key = ip + ":" + endpoint;
ratelimitcache.invalidate(key);
}
private list<string> loadexcludedips() {
// 从配置文件或数据库加载
return arrays.aslist(
"127.0.0.1",
"192.168.1.1",
"10.0.0.1"
);
}
/**
* 速率限制信息
*/
@data
public static class ratelimitinfo {
private static final int max_requests_per_minute = 100;
private static final int max_requests_per_hour = 1000;
private list<long> accesstimes = new arraylist<>();
public void recordaccess() {
long now = system.currenttimemillis();
accesstimes.add(now);
// 清理过期的记录
long oneminuteago = now - 60000;
long onehourago = now - 3600000;
accesstimes.removeif(time -> time < onehourago);
}
public boolean isratelimited() {
long now = system.currenttimemillis();
long oneminuteago = now - 60000;
long onehourago = now - 3600000;
long minutecount = accesstimes.stream()
.filter(time -> time > oneminuteago)
.count();
long hourcount = accesstimes.stream()
.filter(time -> time > onehourago)
.count();
return minutecount > max_requests_per_minute ||
hourcount > max_requests_per_hour;
}
public map<string, object> getstats() {
long now = system.currenttimemillis();
long oneminuteago = now - 60000;
long onehourago = now - 3600000;
long minutecount = accesstimes.stream()
.filter(time -> time > oneminuteago)
.count();
long hourcount = accesstimes.stream()
.filter(time -> time > onehourago)
.count();
map<string, object> stats = new hashmap<>();
stats.put("minutecount", minutecount);
stats.put("hourcount", hourcount);
stats.put("minutelimit", max_requests_per_minute);
stats.put("hourlimit", max_requests_per_hour);
stats.put("isratelimited", isratelimited());
return stats;
}
}
}8.3 地理位置可视化
@restcontroller
@requestmapping("/api/v1/visualization")
@tag(name = "ip可视化", description = "ip地理位置可视化api")
public class ipvisualizationcontroller {
private final geoipmanager geoipmanager;
private final ipaccessrepository ipaccessrepository;
@autowired
public ipvisualizationcontroller(geoipmanager geoipmanager,
ipaccessrepository ipaccessrepository) {
this.geoipmanager = geoipmanager;
this.ipaccessrepository = ipaccessrepository;
}
/**
* 生成访问热力图数据
*/
@getmapping("/heatmap")
@operation(summary = "访问热力图",
description = "生成ip访问热力图数据")
public responseentity<apiresponse<heatmapdata>> getheatmapdata(
@requestparam(required = false) date starttime,
@requestparam(required = false) date endtime) {
if (starttime == null) {
starttime = date.from(instant.now().minus(7, chronounit.days));
}
if (endtime == null) {
endtime = new date();
}
// 查询访问记录
list<ipaccessrecord> records = ipaccessrepository
.findbyaccesstimebetween(starttime, endtime);
// 按地理位置聚合
map<string, integer> locationcount = new hashmap<>();
for (ipaccessrecord record : records) {
string locationkey = record.getcountry() + "|" + record.getcity();
locationcount.put(locationkey,
locationcount.getordefault(locationkey, 0) + 1);
}
// 生成热力图数据
list<heatmappoint> points = locationcount.entryset().stream()
.map(entry -> {
string[] parts = entry.getkey().split("\\|");
geolocation samplelocation = geoipmanager.query(
records.stream()
.filter(r -> r.getcountry().equals(parts[0]) &&
r.getcity().equals(parts[1]))
.findfirst()
.map(ipaccessrecord::getip)
.orelse("8.8.8.8")
);
return heatmappoint.builder()
.country(parts[0])
.city(parts[1])
.count(entry.getvalue())
.latitude(samplelocation.getlatitude())
.longitude(samplelocation.getlongitude())
.build();
})
.collect(collectors.tolist());
heatmapdata data = heatmapdata.builder()
.starttime(starttime)
.endtime(endtime)
.totalaccesses(records.size())
.uniquelocations(locationcount.size())
.points(points)
.build();
return responseentity.ok(apiresponse.success(data));
}
/**
* 生成访问统计图表数据
*/
@getmapping("/statistics")
@operation(summary = "访问统计",
description = "生成ip访问统计图表数据")
public responseentity<apiresponse<accessstatistics>> getaccessstatistics(
@requestparam(required = false) @pattern(regexp = "day|week|month|year")
string period) {
if (period == null) {
period = "week";
}
date endtime = new date();
date starttime;
switch (period) {
case "day":
starttime = date.from(instant.now().minus(1, chronounit.days));
break;
case "week":
starttime = date.from(instant.now().minus(7, chronounit.days));
break;
case "month":
starttime = date.from(instant.now().minus(30, chronounit.days));
break;
case "year":
starttime = date.from(instant.now().minus(365, chronounit.days));
break;
default:
starttime = date.from(instant.now().minus(7, chronounit.days));
}
list<ipaccessrecord> records = ipaccessrepository
.findbyaccesstimebetween(starttime, endtime);
// 按时间分组统计
map<string, long> timeseries = createtimeseries(records, period);
// 按国家分组统计
map<string, long> countrystats = records.stream()
.collect(collectors.groupingby(
ipaccessrecord::getcountry,
collectors.counting()
));
// 按城市分组统计
map<string, long> citystats = records.stream()
.collect(collectors.groupingby(
record -> record.getcountry() + " - " + record.getcity(),
collectors.counting()
));
accessstatistics statistics = accessstatistics.builder()
.period(period)
.starttime(starttime)
.endtime(endtime)
.totalaccesses(records.size())
.uniqueips(records.stream().map(ipaccessrecord::getip).distinct().count())
.timeseries(timeseries)
.countrystats(countrystats)
.citystats(citystats)
.build();
return responseentity.ok(apiresponse.success(statistics));
}
private map<string, long> createtimeseries(list<ipaccessrecord> records,
string period) {
datetimeformatter formatter;
switch (period) {
case "day":
formatter = datetimeformatter.ofpattern("hh:00");
break;
case "week":
formatter = datetimeformatter.ofpattern("yyyy-mm-dd");
break;
case "month":
formatter = datetimeformatter.ofpattern("yyyy-mm-dd");
break;
case "year":
formatter = datetimeformatter.ofpattern("yyyy-mm");
break;
default:
formatter = datetimeformatter.ofpattern("yyyy-mm-dd");
}
return records.stream()
.collect(collectors.groupingby(
record -> record.getaccesstime().toinstant()
.atzone(zoneid.systemdefault())
.format(formatter),
collectors.counting()
));
}
@data
@builder
public static class heatmapdata {
private date starttime;
private date endtime;
private long totalaccesses;
private int uniquelocations;
private list<heatmappoint> points;
}
@data
@builder
public static class heatmappoint {
private string country;
private string city;
private int count;
private double latitude;
private double longitude;
}
@data
@builder
public static class accessstatistics {
private string period;
private date starttime;
private date endtime;
private long totalaccesses;
private long uniqueips;
private map<string, long> timeseries;
private map<string, long> countrystats;
private map<string, long> citystats;
}
}九、性能优化与缓存策略
9.1 多级缓存实现
@component
@slf4j
public class multilevelcache {
private final cache<string, geolocation> localcache;
private final redistemplate<string, geolocation> redistemplate;
private final boolean useredis;
public multilevelcache(redistemplate<string, geolocation> redistemplate,
geoipproperties properties) {
// 一级缓存:本地缓存
this.localcache = caffeine.newbuilder()
.maximumsize(properties.getcache().getmaximumsize())
.expireafterwrite(properties.getcache().getlocalttl(),
timeunit.seconds)
.recordstats()
.build();
// 二级缓存:redis
this.redistemplate = redistemplate;
this.useredis = redistemplate != null;
}
/**
* 从缓存获取数据
*/
public geolocation get(string ip) {
// 先查本地缓存
geolocation location = localcache.getifpresent(ip);
if (location != null) {
log.debug("cache hit from local cache for ip: {}", ip);
return location;
}
// 本地缓存未命中,查询redis
if (useredis) {
location = redistemplate.opsforvalue().get(buildrediskey(ip));
if (location != null) {
log.debug("cache hit from redis for ip: {}", ip);
// 回填到本地缓存
localcache.put(ip, location);
return location;
}
}
log.debug("cache miss for ip: {}", ip);
return null;
}
/**
* 写入缓存
*/
public void put(string ip, geolocation location) {
if (location == null) {
return;
}
// 写入本地缓存
localcache.put(ip, location);
// 写入redis
if (useredis) {
try {
redistemplate.opsforvalue().set(
buildrediskey(ip),
location,
1, timeunit.hours // redis缓存1小时
);
log.debug("data cached in redis for ip: {}", ip);
} catch (exception e) {
log.warn("failed to cache data in redis for ip: {}", ip, e);
}
}
}
/**
* 批量获取
*/
public map<string, geolocation> batchget(list<string> ips) {
map<string, geolocation> results = new hashmap<>();
list<string> missingkeys = new arraylist<>();
// 先查本地缓存
for (string ip : ips) {
geolocation location = localcache.getifpresent(ip);
if (location != null) {
results.put(ip, location);
} else {
missingkeys.add(ip);
}
}
// 如果还有未命中的,批量查询redis
if (useredis && !missingkeys.isempty()) {
list<string> rediskeys = missingkeys.stream()
.map(this::buildrediskey)
.collect(collectors.tolist());
list<geolocation> redisresults = redistemplate.opsforvalue()
.multiget(rediskeys);
for (int i = 0; i < missingkeys.size(); i++) {
string ip = missingkeys.get(i);
geolocation location = redisresults.get(i);
if (location != null) {
results.put(ip, location);
// 回填到本地缓存
localcache.put(ip, location);
}
}
}
return results;
}
/**
* 批量写入
*/
public void batchput(map<string, geolocation> data) {
if (data == null || data.isempty()) {
return;
}
// 写入本地缓存
data.foreach(localcache::put);
// 批量写入redis
if (useredis) {
try {
map<string, geolocation> redisdata = data.entryset().stream()
.collect(collectors.tomap(
entry -> buildrediskey(entry.getkey()),
map.entry::getvalue
));
redistemplate.opsforvalue().multiset(redisdata);
// 设置过期时间
for (string key : redisdata.keyset()) {
redistemplate.expire(key, 1, timeunit.hours);
}
log.debug("batch cached {} items in redis", data.size());
} catch (exception e) {
log.warn("failed to batch cache data in redis", e);
}
}
}
/**
* 删除缓存
*/
public void evict(string ip) {
localcache.invalidate(ip);
if (useredis) {
redistemplate.delete(buildrediskey(ip));
}
}
/**
* 清空所有缓存
*/
public void clear() {
localcache.invalidateall();
if (useredis) {
// 注意:这会清空所有缓存,生产环境慎用
set<string> keys = redistemplate.keys("geoip:*");
if (keys != null && !keys.isempty()) {
redistemplate.delete(keys);
}
}
}
/**
* 获取缓存统计信息
*/
public cachestats getstats() {
return localcache.stats();
}
private string buildrediskey(string ip) {
return "geoip:" + ip;
}
}9.2 异步处理优化
@component
@slf4j
public class asyncgeoipservice {
private final geoipmanager geoipmanager;
private final executorservice executorservice;
private final completionservice<geolocation> completionservice;
public asyncgeoipservice(geoipmanager geoipmanager) {
this.geoipmanager = geoipmanager;
// 创建线程池
this.executorservice = executors.newfixedthreadpool(
runtime.getruntime().availableprocessors() * 2,
new threadfactorybuilder()
.setnameformat("geoip-async-%d")
.setdaemon(true)
.build()
);
this.completionservice = new executorcompletionservice<>(executorservice);
}
/**
* 异步查询单个ip
*/
public completablefuture<geolocation> queryasync(string ip) {
return completablefuture.supplyasync(() -> geoipmanager.query(ip),
executorservice);
}
/**
* 异步批量查询
*/
public completablefuture<map<string, geolocation>> batchqueryasync(
list<string> ips) {
return completablefuture.supplyasync(() -> {
map<string, geolocation> results = new concurrenthashmap<>();
list<future<geolocation>> futures = new arraylist<>();
// 提交所有查询任务
for (string ip : ips) {
futures.add(completionservice.submit(() -> geoipmanager.query(ip)));
}
// 等待所有任务完成
for (int i = 0; i < futures.size(); i++) {
try {
future<geolocation> future = completionservice.take();
geolocation location = future.get();
// 根据ip地址找到对应的结果
// 这里需要维护ip和任务的映射关系
// 简化处理:在任务提交时记录ip
} catch (interruptedexception e) {
thread.currentthread().interrupt();
log.error("batch query interrupted", e);
} catch (executionexception e) {
log.error("error executing query task", e);
}
}
return results;
}, executorservice);
}
/**
* 带超时的查询
*/
public geolocation querywithtimeout(string ip, long timeout, timeunit unit) {
completablefuture<geolocation> future = queryasync(ip);
try {
return future.get(timeout, unit);
} catch (timeoutexception e) {
future.cancel(true);
throw new geoipexception("query timeout for ip: " + ip);
} catch (interruptedexception e) {
thread.currentthread().interrupt();
throw new geoipexception("query interrupted", e);
} catch (executionexception e) {
throw new geoipexception("query failed", e);
}
}
/**
* 关闭线程池
*/
@predestroy
public void shutdown() {
executorservice.shutdown();
try {
if (!executorservice.awaittermination(60, timeunit.seconds)) {
executorservice.shutdownnow();
}
} catch (interruptedexception e) {
executorservice.shutdownnow();
thread.currentthread().interrupt();
}
}
}十、安全与监控
10.1 ip黑白名单
@component
@slf4j
public class ipfilter {
private final set<string> blacklist = new concurrenthashset<>();
private final set<string> whitelist = new concurrenthashset<>();
private final list<cidr> blacklistcidrs = new arraylist<>();
private final list<cidr> whitelistcidrs = new arraylist<>();
@postconstruct
public void init() {
loadblacklist();
loadwhitelist();
}
/**
* 检查ip是否被禁止
*/
public boolean isblocked(string ip) {
// 检查白名单(白名单优先)
if (iswhitelisted(ip)) {
return false;
}
// 检查黑名单
return isblacklisted(ip);
}
/**
* 检查ip是否在黑名单中
*/
public boolean isblacklisted(string ip) {
// 检查精确ip
if (blacklist.contains(ip)) {
return true;
}
// 检查cidr范围
for (cidr cidr : blacklistcidrs) {
if (cidr.contains(ip)) {
return true;
}
}
return false;
}
/**
* 检查ip是否在白名单中
*/
public boolean iswhitelisted(string ip) {
// 检查精确ip
if (whitelist.contains(ip)) {
return true;
}
// 检查cidr范围
for (cidr cidr : whitelistcidrs) {
if (cidr.contains(ip)) {
return true;
}
}
return false;
}
/**
* 添加ip到黑名单
*/
public void addtoblacklist(string iporcidr) {
if (iporcidr.contains("/")) {
blacklistcidrs.add(new cidr(iporcidr));
} else {
blacklist.add(iporcidr);
}
log.info("added to blacklist: {}", iporcidr);
}
/**
* 添加ip到白名单
*/
public void addtowhitelist(string iporcidr) {
if (iporcidr.contains("/")) {
whitelistcidrs.add(new cidr(iporcidr));
} else {
whitelist.add(iporcidr);
}
log.info("added to whitelist: {}", iporcidr);
}
/**
* 从黑名单移除
*/
public void removefromblacklist(string iporcidr) {
if (iporcidr.contains("/")) {
blacklistcidrs.removeif(cidr -> cidr.tostring().equals(iporcidr));
} else {
blacklist.remove(iporcidr);
}
log.info("removed from blacklist: {}", iporcidr);
}
/**
* 从白名单移除
*/
public void removefromwhitelist(string iporcidr) {
if (iporcidr.contains("/")) {
whitelistcidrs.removeif(cidr -> cidr.tostring().equals(iporcidr));
} else {
whitelist.remove(iporcidr);
}
log.info("removed from whitelist: {}", iporcidr);
}
/**
* 获取黑名单列表
*/
public set<string> getblacklist() {
set<string> all = new hashset<>(blacklist);
blacklistcidrs.foreach(cidr -> all.add(cidr.tostring()));
return all;
}
/**
* 获取白名单列表
*/
public set<string> getwhitelist() {
set<string> all = new hashset<>(whitelist);
whitelistcidrs.foreach(cidr -> all.add(cidr.tostring()));
return all;
}
private void loadblacklist() {
// 从配置文件或数据库加载
// 这里添加示例数据
blacklist.add("192.168.1.100");
blacklist.add("10.0.0.100");
blacklistcidrs.add(new cidr("192.168.2.0/24"));
}
private void loadwhitelist() {
// 从配置文件或数据库加载
// 这里添加示例数据
whitelist.add("127.0.0.1");
whitelist.add("192.168.1.1");
whitelistcidrs.add(new cidr("10.1.0.0/16"));
}
/**
* cidr表示法类
*/
@data
public static class cidr {
private final string cidr;
private final long startip;
private final long endip;
public cidr(string cidr) {
this.cidr = cidr;
long[] range = cidrutils.cidrtorange(cidr);
this.startip = range[0];
this.endip = range[1];
}
public boolean contains(string ip) {
long iplong = ipaddressutils.iptolong(ip);
return iplong >= startip && iplong <= endip;
}
@override
public string tostring() {
return cidr;
}
}
}10.2 监控与告警
@component
@slf4j
public class geoipmonitor {
private final meterregistry meterregistry;
private final list<geoipprovider> providers;
private final map<string, providerstats> providerstats = new concurrenthashmap<>();
private final map<string, slidingwindow> errorrates = new concurrenthashmap<>();
@autowired
public geoipmonitor(meterregistry meterregistry,
list<geoipprovider> providers) {
this.meterregistry = meterregistry;
this.providers = providers;
initmetrics();
startmonitoring();
}
private void initmetrics() {
// 注册micrometer指标
meterregistry.gauge("geoip.cache.size", this,
m -> m.getcachestats().map(cachestats::estimatedsize).orelse(0l));
meterregistry.gauge("geoip.provider.count", providers, list::size);
// 为每个提供商创建指标
providers.foreach(provider -> {
string name = provider.getname();
counter.builder("geoip.query.requests")
.tag("provider", name)
.register(meterregistry);
counter.builder("geoip.query.errors")
.tag("provider", name)
.register(meterregistry);
timer.builder("geoip.query.duration")
.tag("provider", name)
.register(meterregistry);
});
}
private void startmonitoring() {
// 定期收集统计信息
scheduledexecutorservice scheduler = executors.newsinglethreadscheduledexecutor();
scheduler.scheduleatfixedrate(() -> {
try {
collectstats();
checkhealth();
} catch (exception e) {
log.error("error in monitoring task", e);
}
}, 1, 1, timeunit.minutes);
}
private void collectstats() {
providers.foreach(provider -> {
string name = provider.getname();
providerstats stats = providerstats.computeifabsent(name,
k -> new providerstats());
// 这里可以收集实际的使用统计
// 例如:成功次数、失败次数、平均响应时间等
});
}
private void checkhealth() {
providers.foreach(provider -> {
string name = provider.getname();
slidingwindow window = errorrates.computeifabsent(name,
k -> new slidingwindow(100));
// 检查错误率
double errorrate = window.geterrorrate();
if (errorrate > 0.1) { // 错误率超过10%
log.warn("high error rate detected for provider {}: {}%",
name, errorrate * 100);
// 发送告警
sendalert(name, "high error rate: " + (errorrate * 100) + "%");
}
});
}
/**
* 记录查询成功
*/
public void recordsuccess(string provider, long duration) {
meterregistry.counter("geoip.query.requests",
"provider", provider).increment();
meterregistry.timer("geoip.query.duration",
"provider", provider).record(duration, timeunit.milliseconds);
// 更新滑动窗口
slidingwindow window = errorrates.computeifabsent(provider,
k -> new slidingwindow(100));
window.recordsuccess();
}
/**
* 记录查询失败
*/
public void recorderror(string provider) {
meterregistry.counter("geoip.query.errors",
"provider", provider).increment();
// 更新滑动窗口
slidingwindow window = errorrates.computeifabsent(provider,
k -> new slidingwindow(100));
window.recorderror();
}
/**
* 发送告警
*/
private void sendalert(string provider, string message) {
// 实现告警逻辑
// 可以发送邮件、短信、钉钉、企业微信等
log.error("alert: provider {} - {}", provider, message);
}
/**
* 获取缓存统计
*/
public optional<cachestats> getcachestats() {
// 从缓存组件获取统计
return optional.empty();
}
/**
* 获取提供商统计信息
*/
public map<string, providerstats> getproviderstats() {
return new hashmap<>(providerstats);
}
/**
* 提供商统计
*/
@data
public static class providerstats {
private long totalqueries;
private long successfulqueries;
private long failedqueries;
private double averageresponsetime;
private double errorrate;
private date lastquerytime;
private date lasterrortime;
}
/**
* 滑动窗口统计
*/
public static class slidingwindow {
private final int size;
private final deque<boolean> window;
public slidingwindow(int size) {
this.size = size;
this.window = new arraydeque<>(size);
}
public synchronized void recordsuccess() {
window.addlast(true);
if (window.size() > size) {
window.removefirst();
}
}
public synchronized void recorderror() {
window.addlast(false);
if (window.size() > size) {
window.removefirst();
}
}
public synchronized double geterrorrate() {
if (window.isempty()) {
return 0.0;
}
long errors = window.stream().filter(success -> !success).count();
return (double) errors / window.size();
}
public synchronized int getwindowsize() {
return window.size();
}
}
}十一、测试与验证
11.1 单元测试
@springboottest
@extendwith(mockitoextension.class)
class geoipservicetest {
@mock
private databasereader databasereader;
@mock
private resttemplate resttemplate;
@injectmocks
private maxmindgeoipservice geoipservice;
@test
void testvalidipquery() throws exception {
// 准备测试数据
string testip = "8.8.8.8";
inetaddress ipaddress = inetaddress.getbyname(testip);
cityresponse mockresponse = mockito.mock(cityresponse.class);
country country = new country(arrays.aslist("united states"), 6252001, "us", null);
subdivision subdivision = new subdivision(arrays.aslist("california"), 5332921, "ca", null);
city city = new city(arrays.aslist("mountain view"), 5375480, null);
location location = new location(37.386, -122.0838, 0, null, null, "america/los_angeles");
postal postal = new postal("94040", 0);
when(databasereader.city(ipaddress)).thenreturn(mockresponse);
when(mockresponse.getcountry()).thenreturn(country);
when(mockresponse.getmostspecificsubdivision()).thenreturn(subdivision);
when(mockresponse.getcity()).thenreturn(city);
when(mockresponse.getlocation()).thenreturn(location);
when(mockresponse.getpostal()).thenreturn(postal);
// 执行测试
geolocation result = geoipservice.query(testip);
// 验证结果
assertnotnull(result);
assertequals("united states", result.getcountry());
assertequals("us", result.getcountrycode());
assertequals("california", result.getregion());
assertequals("mountain view", result.getcity());
assertequals("94040", result.getpostalcode());
assertequals(37.386, result.getlatitude(), 0.001);
assertequals(-122.0838, result.getlongitude(), 0.001);
assertequals("america/los_angeles", result.gettimezone());
assertequals("maxmind", result.getsource());
}
@test
void testinvalidip() {
string invalidip = "999.999.999.999";
assertthrows(illegalargumentexception.class, () -> {
geolocation result = geoipservice.query(invalidip);
});
}
@test
void testinternalip() {
string internalip = "192.168.1.1";
geolocation result = geoipservice.query(internalip);
assertnotnull(result);
assertequals("internal", result.getcountry());
asserttrue(result.getinternal());
}
}
@webmvctest(ipgeocontroller.class)
class ipgeocontrollertest {
@autowired
private mockmvc mockmvc;
@mockbean
private geoipmanager geoipmanager;
@mockbean
private ipaddressutils ipaddressutils;
@test
void testqueryip() throws exception {
string testip = "8.8.8.8";
geolocation mocklocation = geolocation.builder()
.ip(testip)
.country("united states")
.countrycode("us")
.city("mountain view")
.latitude(37.386)
.longitude(-122.0838)
.source("maxmind")
.timestamp(system.currenttimemillis())
.build();
when(geoipmanager.query(testip)).thenreturn(mocklocation);
mockmvc.perform(get("/api/v1/ip/query")
.param("ip", testip))
.andexpect(status().isok())
.andexpect(jsonpath("$.success").value(true))
.andexpect(jsonpath("$.data.country").value("united states"))
.andexpect(jsonpath("$.data.city").value("mountain view"));
}
@test
void testbatchquery() throws exception {
list<string> ips = arrays.aslist("8.8.8.8", "1.1.1.1");
map<string, geolocation> mockresults = new hashmap<>();
mockresults.put("8.8.8.8", geolocation.builder()
.ip("8.8.8.8")
.country("united states")
.build());
mockresults.put("1.1.1.1", geolocation.builder()
.ip("1.1.1.1")
.country("australia")
.build());
when(geoipmanager.batchquery(anylist())).thenreturn(mockresults);
string requestbody = "{\"ips\": [\"8.8.8.8\", \"1.1.1.1\"]}";
mockmvc.perform(post("/api/v1/ip/batch-query")
.contenttype(mediatype.application_json)
.content(requestbody))
.andexpect(status().isok())
.andexpect(jsonpath("$.success").value(true))
.andexpect(jsonpath("$.data['8.8.8.8'].country").value("united states"))
.andexpect(jsonpath("$.data['1.1.1.1'].country").value("australia"));
}
}11.2 性能测试
@springboottest
@autoconfiguremockmvc
@testpropertysource(properties = {
"ip.geolocation.mode=hybrid",
"ip.geolocation.cache.enabled=true"
})
class geoipperformancetest {
@autowired
private mockmvc mockmvc;
@autowired
private geoipmanager geoipmanager;
@test
void testqueryperformance() {
// 生成测试ip列表
list<string> testips = generatetestips(1000);
long starttime = system.currenttimemillis();
// 执行批量查询
map<string, geolocation> results = geoipmanager.batchquery(testips);
long endtime = system.currenttimemillis();
long duration = endtime - starttime;
system.out.println("batch query of " + testips.size() + " ips took " + duration + "ms");
system.out.println("average time per query: " + (double) duration / testips.size() + "ms");
// 验证性能要求
asserttrue(duration < 5000, "batch query should complete within 5 seconds");
}
@test
void testcacheperformance() {
string testip = "8.8.8.8";
// 第一次查询(缓存未命中)
long starttime1 = system.currenttimemillis();
geolocation result1 = geoipmanager.query(testip);
long duration1 = system.currenttimemillis() - starttime1;
// 第二次查询(缓存命中)
long starttime2 = system.currenttimemillis();
geolocation result2 = geoipmanager.query(testip);
long duration2 = system.currenttimemillis() - starttime2;
system.out.println("first query (cache miss): " + duration1 + "ms");
system.out.println("second query (cache hit): " + duration2 + "ms");
// 验证缓存命中率提升
asserttrue(duration2 < duration1, "cached query should be faster");
asserttrue(duration2 < 10, "cached query should be very fast (<10ms)");
}
@test
void testconcurrentperformance() throws interruptedexception {
int threadcount = 10;
int queriesperthread = 100;
executorservice executor = executors.newfixedthreadpool(threadcount);
list<future<long>> futures = new arraylist<>();
// 提交并发任务
for (int i = 0; i < threadcount; i++) {
futures.add(executor.submit(() -> {
long totaltime = 0;
list<string> ips = generatetestips(queriesperthread);
for (string ip : ips) {
long starttime = system.currenttimemillis();
geoipmanager.query(ip);
totaltime += system.currenttimemillis() - starttime;
}
return totaltime;
}));
}
// 等待所有任务完成
long totalquerytime = 0;
for (future<long> future : futures) {
try {
totalquerytime += future.get();
} catch (executionexception e) {
fail("test execution failed: " + e.getmessage());
}
}
executor.shutdown();
long totalqueries = threadcount * queriesperthread;
double avgtimeperquery = (double) totalquerytime / totalqueries;
system.out.println("concurrent test: " + totalqueries + " queries");
system.out.println("average time per query: " + avgtimeperquery + "ms");
// 验证并发性能
asserttrue(avgtimeperquery < 100, "average query time should be <100ms under concurrent load");
}
private list<string> generatetestips(int count) {
list<string> ips = new arraylist<>();
random random = new random();
for (int i = 0; i < count; i++) {
string ip = random.nextint(256) + "." +
random.nextint(256) + "." +
random.nextint(256) + "." +
random.nextint(256);
ips.add(ip);
}
return ips;
}
}11.3 集成测试
@springboottest(webenvironment = springboottest.webenvironment.random_port)
@testcontainers
class geoipintegrationtest {
@container
static rediscontainer redis = new rediscontainer(dockerimagename.parse("redis:7-alpine"))
.withexposedports(6379);
@autowired
private testresttemplate resttemplate;
@dynamicpropertysource
static void redisproperties(dynamicpropertyregistry registry) {
registry.add("spring.redis.host", redis::gethost);
registry.add("spring.redis.port", redis::getfirstmappedport);
}
@test
void testcompleteworkflow() {
// 测试ip查询
responseentity<apiresponse> response = resttemplate.getforentity(
"/api/v1/ip/query?ip=8.8.8.8",
apiresponse.class
);
assertequals(httpstatus.ok, response.getstatuscode());
assertnotnull(response.getbody());
asserttrue(response.getbody().issuccess());
// 测试批量查询
batchqueryrequest request = new batchqueryrequest();
request.setips(arrays.aslist("8.8.8.8", "1.1.1.1", "114.114.114.114"));
responseentity<apiresponse> batchresponse = resttemplate.postforentity(
"/api/v1/ip/batch-query",
request,
apiresponse.class
);
assertequals(httpstatus.ok, batchresponse.getstatuscode());
// 测试服务状态
responseentity<apiresponse> statusresponse = resttemplate.getforentity(
"/api/v1/ip/status",
apiresponse.class
);
assertequals(httpstatus.ok, statusresponse.getstatuscode());
}
}十二、部署与运维
12.1 docker容器化部署
# dockerfile
from openjdk:17-jdk-slim as builder
workdir /app
# 复制maven包装器
copy mvnw .
copy .mvn .mvn
copy pom.xml .
# 下载依赖
run chmod +x mvnw
run ./mvnw dependency:go-offline -b
# 复制源代码
copy src src
# 构建应用
run ./mvnw clean package -dskiptests
# 运行时镜像
from openjdk:17-jre-slim
# 设置时区
env tz=asia/shanghai
run ln -snf /usr/share/zoneinfo/$tz /etc/localtime && echo $tz > /etc/timezone
workdir /app
# 复制构建产物
copy --from=builder /app/target/*.jar app.jar
# 创建数据目录
run mkdir -p /app/data/geoip
# 复制ip数据库
copy geoip /app/data/geoip
# 创建非root用户
run groupadd -r spring && useradd -r -g spring spring
run chown -r spring:spring /app
user spring
# 暴露端口
expose 8080
# 健康检查
healthcheck --interval=30s --timeout=3s --start-period=60s --retries=3 \
cmd curl -f http://localhost:8080/actuator/health || exit 1
# 启动应用
entrypoint ["java", "-jar", "app.jar"]yaml
# docker-compose.yml
version: '3.8'
services:
ip-geolocation:
build: .
container_name: ip-geolocation
ports:
- "8080:8080"
environment:
- spring_profiles_active=prod
- java_opts=-xmx512m -xms256m
- ip_geolocation_mode=hybrid
- ip_geolocation_cache_enabled=true
volumes:
- geoip-data:/app/data/geoip
- logs:/app/logs
networks:
- geoip-network
restart: unless-stopped
depends_on:
- redis
- mysql
redis:
image: redis:7-alpine
container_name: geoip-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- geoip-network
restart: unless-stopped
mysql:
image: mysql:8.0
container_name: geoip-mysql
ports:
- "3306:3306"
environment:
- mysql_root_password=rootpassword
- mysql_database=geoip
- mysql_user=geoip
- mysql_password=geoip123
volumes:
- mysql-data:/var/lib/mysql
- ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- geoip-network
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
container_name: geoip-prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
networks:
- geoip-network
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: geoip-grafana
ports:
- "3000:3000"
environment:
- gf_security_admin_password=admin123
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
networks:
- geoip-network
restart: unless-stopped
networks:
geoip-network:
driver: bridge
volumes:
geoip-data:
redis-data:
mysql-data:
prometheus-data:
grafana-data:12.2 kubernetes部署
# kubernetes/deployment.yaml
apiversion: apps/v1
kind: deployment
metadata:
name: ip-geolocation
namespace: default
labels:
app: ip-geolocation
spec:
replicas: 3
selector:
matchlabels:
app: ip-geolocation
template:
metadata:
labels:
app: ip-geolocation
spec:
containers:
- name: ip-geolocation
image: your-registry/ip-geolocation:latest
ports:
- containerport: 8080
env:
- name: spring_profiles_active
value: "k8s"
- name: java_opts
value: "-xmx512m -xms256m"
- name: redis_host
value: "geoip-redis"
- name: mysql_host
value: "geoip-mysql"
resources:
requests:
memory: "256mi"
cpu: "250m"
limits:
memory: "512mi"
cpu: "500m"
livenessprobe:
httpget:
path: /actuator/health/liveness
port: 8080
initialdelayseconds: 60
periodseconds: 10
readinessprobe:
httpget:
path: /actuator/health/readiness
port: 8080
initialdelayseconds: 30
periodseconds: 5
volumemounts:
- name: geoip-data
mountpath: /app/data/geoip
- name: logs
mountpath: /app/logs
volumes:
- name: geoip-data
persistentvolumeclaim:
claimname: geoip-data-pvc
- name: logs
emptydir: {}
---
apiversion: v1
kind: service
metadata:
name: ip-geolocation
namespace: default
spec:
selector:
app: ip-geolocation
ports:
- port: 80
targetport: 8080
type: clusterip
---
apiversion: networking.k8s.io/v1
kind: ingress
metadata:
name: ip-geolocation
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: ip-api.example.com
http:
paths:
- path: /
pathtype: prefix
backend:
service:
name: ip-geolocation
port:
number: 8012.3 监控配置
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'ip-geolocation'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['ip-geolocation:8080']
labels:
application: 'ip-geolocation'
environment: 'production'
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+)
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: kubernetes_namespace
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: kubernetes_pod_name十三、总结与最佳实践
13.1 项目总结
通过本文的详细介绍,我们构建了一个完整的springboot ip地址解析系统,实现了:
- 多数据源支持:集成maxmind、ip2region等离线库和多个在线api
- 智能查询策略:支持离线优先、在线优先、混合模式等多种查询策略
- 高性能缓存:实现多级缓存机制,大幅提升查询性能
- 完整api接口:提供restful api,支持单ip查询、批量查询等功能
- 监控告警:集成监控系统,实时监控服务状态
- 安全防护:实现ip黑白名单、访问频率限制等安全机制
- 可视化展示:提供地理位置可视化功能
13.2 最佳实践建议
数据库选择建议
- 生产环境:推荐使用maxmind商业版,数据更准确
- 国内应用:可优先考虑ip2region,对中文支持更好
- 混合模式:结合使用离线库和在线api,平衡成本和准确性
性能优化建议
缓存策略:
- 使用多级缓存(本地+redis)
- 合理设置缓存过期时间
- 对热点数据使用更长的缓存时间
并发控制:
- 使用线程池控制并发查询
- 实现请求队列,避免服务过载
- 设置合理的超时时间
数据库优化:
- 定期更新ip数据库
- 使用内存数据库加载常用数据
- 对查询结果进行压缩存储
安全建议
访问控制:
- 实现api密钥认证
- 限制api调用频率
- 记录所有访问日志
数据安全:
- 对敏感信息进行脱敏
- 定期审计ip访问记录
- 实现数据加密存储
运维建议
监控告警:
- 监控服务健康状态
- 设置性能阈值告警
- 定期分析访问日志
备份恢复:
- 定期备份ip数据库
- 实现服务快速恢复
- 准备应急预案
13.3 扩展方向
功能扩展
- ip威胁情报:集成威胁情报数据,识别恶意ip
- 用户行为分析:分析ip访问模式,识别异常行为
- 个性化推荐:基于地理位置提供个性化内容
技术扩展
- 机器学习:使用机器学习算法优化ip定位精度
- 区块链:使用区块链技术确保数据不可篡改
- 边缘计算:在边缘节点部署ip解析服务,降低延迟
架构扩展
- 微服务化:将ip解析拆分为独立微服务
- serverless:使用云函数实现弹性扩展
- 多区域部署:在全球多个区域部署服务,提供就近访问
13.4 注意事项
- 数据准确性:ip地理位置数据存在一定误差,需告知用户
- 隐私保护:遵循相关法律法规,保护用户隐私
- 服务稳定性:准备备用方案,确保服务高可用
- 成本控制:在线api服务可能产生费用,需合理控制使用量
- 合规要求:确保服务符合地区法律法规要求
十四、附录
性能指标参考
| 指标 | 目标值 | 说明 |
|---|---|---|
| 平均响应时间 | < 50ms | 缓存命中时 |
| 最大响应时间 | < 500ms | 在线查询时 |
| 并发能力 | > 1000 qps | 单节点 |
| 缓存命中率 | > 90% | 正常访问模式 |
| 可用性 | > 99.9% | 生产环境 |
配置文件示例
# application-prod.yml
spring:
datasource:
url: jdbc:mysql://${mysql_host:localhost}:3306/geoip
username: ${mysql_user:geoip}
password: ${mysql_password:geoip123}
hikari:
maximum-pool-size: 20
minimum-idle: 5
redis:
host: ${redis_host:localhost}
port: 6379
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
cache:
type: redis
redis:
time-to-live: 3600s
cache-null-values: false
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
health:
db:
enabled: true
redis:
enabled: true
ip:
geolocation:
mode: hybrid
offline:
database: maxmind
maxmind-db-path: file:/data/geoip/geolite2-city.mmdb
online:
enabled: true
providers:
- name: ipstack
url: http://api.ipstack.com/{ip}?access_key=${ipstack_key}
priority: 1
timeout: 3000
cache:
enabled: true
local-ttl: 300
redis-ttl: 3600
logging:
level:
com.example.ip: info
file:
name: /app/logs/geoip.log
logback:
rollingpolicy:
max-file-size: 10mb
max-history: 30以上就是springboot快速实现ip地址解析的全攻略的详细内容,更多关于springboot解析ip地址的资料请关注代码网其它相关文章!
发表评论