当前位置: 代码网 > it编程>编程语言>Java > SpringBoot快速实现IP地址解析的全攻略

SpringBoot快速实现IP地址解析的全攻略

2026年02月05日 Java 我要评论
一、引言与概述1.1 ip地址解析的重要性在当今的互联网应用中,ip地址解析已成为许多系统不可或缺的功能。通过ip地址解析,我们可以:地理位置服务:根据用户ip确定其所在地区,提供本地化内容安全防护:

一、引言与概述

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: 80

12.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地址的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com