基于ip2region.xdb数据库从IP获取到属地解析全攻略

前言

在日常的Java开发中,IP地址相关的操作是非常常见的需求。无论是用户行为分析、地域化服务,还是安全防护,都需要精准地获取和解析IP信息。本文将详细介绍一个功能完善的Java IP工具类,涵盖IP地址获取、MAC地址获取以及IP属地解析三大核心功能,帮助你在项目中快速落地这些能力。


一、工具类概述

1.1 核心功能

该IP工具类提供了三个核心功能模块:

  • IP地址获取:支持多种代理场景下的客户端IP提取,能够穿透多层代理链获取真实客户端IP
  • MAC地址获取:通过Java原生API获取本地网络接口的MAC地址
  • IP属地解析:基于ip2region.xdb数据库,实现毫秒级的IP地理位置查询

1.2 应用场景

这些功能在实际项目中有着广泛的应用场景:

  • 用户行为分析:通过IP属地信息,统计用户的地域分布,为产品决策提供数据支持
  • 地域化服务:根据用户所在省份/城市,提供个性化的内容推荐或服务
  • 安全防护:识别异常IP访问,进行地域限制或风险预警
  • 日志审计:在关键操作日志中记录IP和属地信息,便于问题追溯

1.3 技术亮点

该工具类的技术亮点主要体现在:

  • 多场景IP获取策略:内置HTTP头参数优先级逻辑,能够处理各种代理和负载均衡场景
  • 高效内存缓存 :ip2region.xdb数据库采用内存缓存模式(Searcher.newWithBuffer),查询性能达到微秒级
  • 完善的异常处理:针对不同场景设计了合理的异常捕获和处理机制

二、核心功能详解

2.1 IP地址获取(getIpAddr

在真实的生产环境中,获取客户端IP并非想象中那么简单。用户请求可能经过多层代理、CDN、负载均衡器,因此需要从多个HTTP头中提取真实IP。

HTTP头参数优先级顺序

java 复制代码
public static String getIpAddr(HttpServletRequest request) {
    if (request == null) {
        return "unknown";
    }
    
    String ip = request.getHeader("x-forwarded-for");
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("X-Real-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("HTTP_CLIENT_IP");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("HTTP_X_FORWARDED_FOR");
    }
    if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }
    
    return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
}

优先级说明

  1. X-Forwarded-For:Squid等代理服务器的标准,最为常用
  2. Proxy-Client-IP:Apache+Mod_Proxy场景
  3. X-Real-IP:Nginx反向代理常用
  4. WL-Proxy-Client-IP:WebLogic代理场景
  5. HTTP_CLIENT_IP:部分代理服务器使用
  6. HTTP_X_FORWARDED_FOR:部分特殊代理场景
  7. getRemoteAddr():直接获取,作为兜底方案

代理场景下的IP提取逻辑

在实际场景中,X-Forwarded-For的值可能是多个IP的列表,格式为:client, proxy1, proxy2。其中第一个IP才是真实客户端IP。

增强版的IP提取方法:

java 复制代码
public static String getRealIp(HttpServletRequest request) {
    String ip = getIpAddr(request);
    
    // 处理多个IP的情况(逗号分隔)
    if (ip != null && ip.contains(",")) {
        String[] ips = ip.split(",");
        ip = ips[0].trim(); // 取第一个IP(真实客户端IP)
    }
    
    // 移除IPv6的端口信息
    if (ip != null && ip.contains(":") && ip.indexOf(":") < ip.lastIndexOf(":")) {
        ip = ip.substring(0, ip.lastIndexOf(":"));
    }
    
    return ip;
}

关键点解析

  • 逗号分隔处理X-Forwarded-For可能包含代理链中的所有IP,必须提取第一个
  • IPv6地址兼容 :将IPv6的本地回环地址0:0:0:0:0:0:0:1转换为127.0.0.1,便于统一处理
  • 端口剥离:某些代理可能返回带端口的IP格式,需要进行清理

2.2 MAC地址获取(getMacAddress

MAC地址是网络接口的唯一标识,在某些场景下(如设备绑定、局域网通信)需要获取。

NetworkInterface和InetAddress的协作原理

java 复制代码
public static String getMacAddress() throws SocketException {
    // 获取本机的所有网络接口
    Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
    
    while (networkInterfaces.hasMoreElements()) {
        NetworkInterface networkInterface = networkInterfaces.nextElement();
        
        // 跳过回环接口和未启用的接口
        if (networkInterface.isLoopback() || !networkInterface.isUp()) {
            continue;
        }
        
        // 获取硬件地址(MAC地址)
        byte[] macBytes = networkInterface.getHardwareAddress();
        if (macBytes != null) {
            return formatMacAddress(macBytes);
        }
    }
    
    return null;
}

/**
 * 将MAC地址字节数组格式化为标准字符串
 */
private static String formatMacAddress(byte[] macBytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < macBytes.length; i++) {
        sb.append(String.format("%02X%s", macBytes[i], (i < macBytes.length - 1) ? "-" : ""));
    }
    return sb.toString();
}

技术原理解析

  • NetworkInterface:Java提供的网络接口抽象,可以枚举本机所有网卡
  • InetAddress :虽然方法名中包含InetAddress,但MAC地址获取主要依赖NetworkInterface的getHardwareAddress()方法
  • 字节数组转字符串 :MAC地址本质上是6字节的十六进制数,需要格式化为XX:XX:XX:XX:XX:XXXX-XX-XX-XX-XX-XX格式

异常处理建议

java 复制代码
public static String getMacAddressSafe() {
    try {
        return getMacAddress();
    } catch (SocketException e) {
        log.error("获取MAC地址失败", e);
        return null;
    }
}

可能的异常场景

  • SecurityException:在受限环境中(如某些云平台、Docker容器),可能没有访问网络接口的权限
  • SocketException:网络接口配置异常或不可用
  • 返回null:没有可用的物理网卡(纯虚拟环境或容器)

2.3 IP属地解析(getCityInfo

ip2region是一个高精度的IP地理位置库,支持多种查询模式。本文介绍基于xdb数据库的内存缓存模式。

ip2region.xdb数据库的加载与使用

java 复制代码
/**
 * ip2region查询器(使用内存缓存模式,性能最优)
 */
private static Searcher searcher = null;

static {
    try {
        // 加载xdb数据库文件
        String dbPath = "classpath:ip2region.xdb";
        InputStream inputStream = new ClassPathResource(dbPath).getInputStream();
        byte[] cBuff = IoUtil.readBytes(inputStream);
        
        // 创建内存缓存模式的查询器
        searcher = Searcher.newWithBuffer(cBuff);
    } catch (Exception e) {
        log.error("ip2region数据库加载失败", e);
    }
}

/**
 * 获取IP属地信息
 */
public static String getCityInfo(String ip) {
    if (searcher == null) {
        log.warn("ip2region查询器未初始化");
        return null;
    }
    
    try {
        // 执行查询
        return searcher.search(ip);
    } catch (Exception e) {
        log.error("IP属地查询失败, ip: {}", ip, e);
        return null;
    }
}

核心流程解析

  1. 数据库加载:从classpath加载ip2region.xdb文件(约11MB)
  2. 内存缓存 :使用Searcher.newWithBuffer()将整个数据库加载到内存
  3. 查询执行 :通过searcher.search(ip)方法执行查询

内存缓存模式的优势

ip2region支持三种查询模式:

模式 内存占用 查询性能 适用场景
内存模式(newWithBuffer) 约11MB 微秒级 高并发、性能敏感
文件模式(newWithFileOnly) 极低 毫秒级 内存受限、低频查询
缓存模式(newWithVectorIndex) 约200KB 微秒级 内存和性能的平衡选择

推荐使用内存模式:在大多数应用场景中,11MB的内存占用是可以接受的,换来的是极致的查询性能。

返回结果格式与处理

getCityInfo返回的原始格式:中国|0|上海|上海市|电信

工具方法封装

java 复制代码
/**
 * 提取IP所属省份
 */
public static String getIpPossession(String ip) {
    String cityInfo = getCityInfo(ip);
    if (cityInfo == null) {
        return null;
    }
    
    String[] parts = cityInfo.split("\\|");
    if (parts.length >= 3) {
        return parts[2]; // 省份信息
    }
    
    return null;
}

/**
 * 提取IP所属城市
 */
public static String getIpCity(String ip) {
    String cityInfo = getCityInfo(ip);
    if (cityInfo == null) {
        return null;
    }
    
    String[] parts = cityInfo.split("\\|");
    if (parts.length >= 4) {
        return parts[3]; // 城市信息
    }
    
    return null;
}

结果字段说明

  • parts[0]:国家(如:中国、美国)
  • parts[1]:区域编码(0表示国内)
  • parts[2]:省份(如:上海、广东)
  • parts[3]:城市(如:上海市、深圳市)
  • parts[4]:运营商(如:电信、联通)

三、实战应用示例

3.1 控制器层集成

在Spring Boot项目中,可以方便地集成IP工具类:

java 复制代码
@RestController
@RequestMapping("/api")
public class UserController {
    
    /**
     * 用户登录接口 - 记录IP和属地信息
     */
    @PostMapping("/login")
    public Result<LoginResponse> login(@RequestBody LoginRequest request, 
                                       HttpServletRequest httpRequest) {
        // 获取客户端IP
        String ip = IpUtil.getRealIp(httpRequest);
        
        // 解析属地信息
        String province = IpUtil.getIpPossession(ip);
        String city = IpUtil.getIpCity(ip);
        
        log.info("用户登录 - IP: {}, 省份: {}, 城市: {}", ip, province, city);
        
        // 业务逻辑处理
        // ...
        
        // 将属地信息返回给前端
        LoginResponse response = new LoginResponse();
        response.setIp(ip);
        response.setProvince(province);
        response.setCity(city);
        
        return Result.success(response);
    }
    
    /**
     * 获取当前用户的属地信息
     */
    @GetMapping("/location")
    public Result<LocationInfo> getLocation(HttpServletRequest httpRequest) {
        String ip = IpUtil.getRealIp(httpRequest);
        
        LocationInfo location = new LocationInfo();
        location.setIp(ip);
        location.setProvince(IpUtil.getIpPossession(ip));
        location.setCity(IpUtil.getIpCity(ip));
        
        return Result.success(location);
    }
}

3.2 结果处理与业务逻辑应用

java 复制代码
@Service
public class UserService {
    
    /**
     * 根据用户属地推荐内容
     */
    public List<Content> recommendContent(String ip) {
        String province = IpUtil.getIpPossession(ip);
        String city = IpUtil.getIpCity(ip);
        
        // 业务逻辑:根据属地推荐不同内容
        if ("上海".equals(province)) {
            return contentRepository.findByRegion("华东");
        } else if ("广东".equals(province)) {
            return contentRepository.findByRegion("华南");
        } else {
            return contentRepository.findDefault();
        }
    }
    
    /**
     * 风险检测:识别异地登录
     */
    public boolean detectAbnormalLogin(Long userId, String currentIp) {
        // 获取用户常用登录属地
        UserLocationHistory history = locationHistoryRepository.findLatestByUserId(userId);
        
        String currentProvince = IpUtil.getIpPossession(currentIp);
        
        // 如果当前省份与常用省份不同,标记为异常
        if (history != null && !history.getProvince().equals(currentProvince)) {
            log.warn("检测到异地登录 - 用户: {}, 常用省份: {}, 当前省份: {}", 
                     userId, history.getProvince(), currentProvince);
            return true;
        }
        
        return false;
    }
}

3.3 完成的IpUtil工具类

java 复制代码
package com.example.util;

import cn.hutool.core.io.IoUtil;
import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.core.io.ClassPathResource;

import java.io.InputStream;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

/**
 * IP工具类
 * 提供IP地址获取、MAC地址获取、IP属地解析等核心功能
 *
 * @author Your Name
 * @since 2026-01-13
 */
@Slf4j
public class IpUtil {

    /**
     * ip2region查询器(使用内存缓存模式,性能最优)
     * 在类加载时初始化,支持高并发查询
     */
    private static Searcher searcher = null;

    /**
     * 静态初始化块:加载ip2region.xdb数据库
     */
    static {
        try {
            // 加载xdb数据库文件
            String dbPath = "classpath:ip2region.xdb";
            InputStream inputStream = new ClassPathResource(dbPath).getInputStream();
            byte[] cBuff = IoUtil.readBytes(inputStream);
            
            // 创建内存缓存模式的查询器
            searcher = Searcher.newWithBuffer(cBuff);
            
            log.info("ip2region数据库加载成功,内存占用: {}MB", cBuff.length / 1024.0 / 1024.0);
        } catch (Exception e) {
            log.error("ip2region数据库加载失败,属地查询功能将不可用", e);
        }
    }

    /**
     * 获取客户端IP地址
     * 支持多层代理场景,按优先级顺序从HTTP头中提取真实IP
     *
     * @param request HttpServletRequest对象
     * @return 客户端IP地址,无法获取时返回"unknown"
     */
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }

        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        // IPv6本地回环地址转换为IPv4
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

    /**
     * 获取真实客户端IP地址(增强版)
     * 处理代理链中的多个IP,提取第一个作为真实客户端IP
     *
     * @param request HttpServletRequest对象
     * @return 真实客户端IP地址
     */
    public static String getRealIp(HttpServletRequest request) {
        String ip = getIpAddr(request);

        if (ip == null || "unknown".equalsIgnoreCase(ip)) {
            return "127.0.0.1";
        }

        // 处理多个IP的情况(逗号分隔),取第一个IP(真实客户端IP)
        if (ip.contains(",")) {
            String[] ips = ip.split(",");
            ip = ips[0].trim();
        }

        // 移除IPv6的端口信息(格式如:2001:db8::1:8080)
        if (ip.contains(":") && ip.indexOf(":") < ip.lastIndexOf(":")) {
            ip = ip.substring(0, ip.lastIndexOf(":"));
        }

        return ip;
    }

    /**
     * 获取本地MAC地址
     * 通过遍历网络接口获取第一个非回环且已启用的网卡的MAC地址
     *
     * @return MAC地址字符串(格式:XX-XX-XX-XX-XX-XX),获取失败返回null
     * @throws SocketException 当访问网络接口失败时抛出
     */
    public static String getMacAddress() throws SocketException {
        // 获取本机的所有网络接口
        Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();

        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface networkInterface = networkInterfaces.nextElement();

            // 跳过回环接口和未启用的接口
            if (networkInterface.isLoopback() || !networkInterface.isUp()) {
                continue;
            }

            // 跳过虚拟接口(常见于Docker、VPN等)
            if (networkInterface.isVirtual()) {
                continue;
            }

            // 获取硬件地址(MAC地址)
            byte[] macBytes = networkInterface.getHardwareAddress();
            if (macBytes != null && macBytes.length == 6) {
                return formatMacAddress(macBytes);
            }
        }

        return null;
    }

    /**
     * 安全获取本地MAC地址(带异常处理)
     *
     * @return MAC地址字符串,获取失败返回null
     */
    public static String getMacAddressSafe() {
        try {
            return getMacAddress();
        } catch (SocketException e) {
            log.error("获取MAC地址失败", e);
            return null;
        }
    }

    /**
     * 将MAC地址字节数组格式化为标准字符串
     * 格式:XX-XX-XX-XX-XX-XX(大写十六进制)
     *
     * @param macBytes MAC地址字节数组(6字节)
     * @return 格式化后的MAC地址字符串
     */
    private static String formatMacAddress(byte[] macBytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < macBytes.length; i++) {
            sb.append(String.format("%02X%s", macBytes[i], (i < macBytes.length - 1) ? "-" : ""));
        }
        return sb.toString();
    }

    /**
     * 获取IP属地信息(原始格式)
     * 返回格式:中国|0|上海|上海市|电信
     *
     * @param ip IP地址
     * @return 属地信息字符串,查询失败返回null
     */
    public static String getCityInfo(String ip) {
        if (searcher == null) {
            log.warn("ip2region查询器未初始化");
            return null;
        }

        if (ip == null || ip.isEmpty()) {
            return null;
        }

        try {
            return searcher.search(ip);
        } catch (Exception e) {
            log.error("IP属地查询失败, ip: {}", ip, e);
            return null;
        }
    }

    /**
     * 安全获取IP属地信息(带异常处理和参数校验)
     *
     * @param ip IP地址
     * @return 属地信息字符串,查询失败返回null
     */
    public static String getCityInfoSafe(String ip) {
        if (ip == null || ip.isEmpty()) {
            return null;
        }
        return getCityInfo(ip);
    }

    /**
     * 提取IP所属省份
     *
     * @param ip IP地址
     * @return 省份名称,查询失败返回null
     */
    public static String getIpPossession(String ip) {
        String cityInfo = getCityInfo(ip);
        if (cityInfo == null) {
            return null;
        }

        String[] parts = cityInfo.split("\\|");
        if (parts.length >= 3) {
            String province = parts[2];
            return "0".equals(province) ? null : province;
        }

        return null;
    }

    /**
     * 安全提取IP所属省份(带异常处理)
     *
     * @param ip IP地址
     * @return 省份名称,查询失败返回null
     */
    public static String getIpPossessionSafe(String ip) {
        try {
            return getIpPossession(ip);
        } catch (Exception e) {
            log.warn("提取省份信息失败, ip: {}", ip, e);
            return null;
        }
    }

    /**
     * 提取IP所属城市
     *
     * @param ip IP地址
     * @return 城市名称,查询失败返回null
     */
    public static String getIpCity(String ip) {
        String cityInfo = getCityInfo(ip);
        if (cityInfo == null) {
            return null;
        }

        String[] parts = cityInfo.split("\\|");
        if (parts.length >= 4) {
            String city = parts[3];
            return "0".equals(city) ? null : city;
        }

        return null;
    }

    /**
     * 安全提取IP所属城市(带异常处理)
     *
     * @param ip IP地址
     * @return 城市名称,查询失败返回null
     */
    public static String getIpCitySafe(String ip) {
        try {
            return getIpCity(ip);
        } catch (Exception e) {
            log.warn("提取城市信息失败, ip: {}", ip, e);
            return null;
        }
    }

    /**
     * 提取IP所属运营商
     *
     * @param ip IP地址
     * @return 运营商名称,查询失败返回null
     */
    public static String getIpIsp(String ip) {
        String cityInfo = getCityInfo(ip);
        if (cityInfo == null) {
            return null;
        }

        String[] parts = cityInfo.split("\\|");
        if (parts.length >= 5) {
            String isp = parts[4];
            return "0".equals(isp) ? null : isp;
        }

        return null;
    }

    /**
     * 提取IP所属国家
     *
     * @param ip IP地址
     * @return 国家名称,查询失败返回null
     */
    public static String getIpCountry(String ip) {
        String cityInfo = getCityInfo(ip);
        if (cityInfo == null) {
            return null;
        }

        String[] parts = cityInfo.split("\\|");
        if (parts.length >= 1) {
            String country = parts[0];
            return "0".equals(country) ? null : country;
        }

        return null;
    }

    /**
     * 判断IP是否为内网IP
     *
     * @param ip IP地址
     * @return true-内网IP,false-外网IP
     */
    public static boolean isInternalIp(String ip) {
        if (ip == null || ip.isEmpty()) {
            return false;
        }

        // 127.0.0.1 本地回环
        if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
            return true;
        }

        // 10.0.0.0 - 10.255.255.255
        if (ip.startsWith("10.")) {
            return true;
        }

        // 172.16.0.0 - 172.31.255.255
        if (ip.startsWith("172.")) {
            String[] parts = ip.split("\\.");
            if (parts.length >= 2) {
                int second = Integer.parseInt(parts[1]);
                if (second >= 16 && second <= 31) {
                    return true;
                }
            }
        }

        // 192.168.0.0 - 192.168.255.255
        if (ip.startsWith("192.168.")) {
            return true;
        }

        return false;
    }

    /**
     * 验证IP地址格式是否正确
     *
     * @param ip IP地址字符串
     * @return true-格式正确,false-格式错误
     */
    public static boolean isValidIp(String ip) {
        if (ip == null || ip.isEmpty()) {
            return false;
        }

        String ipv4Pattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
        return ip.matches(ipv4Pattern);
    }

    /**
     * 获取本地主机名
     *
     * @return 主机名,获取失败返回"unknown"
     */
    public static String getHostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
            log.error("获取主机名失败", e);
            return "unknown";
        }
    }

    /**
     * 获取本地IP地址
     *
     * @return 本地IP地址,获取失败返回null
     */
    public static String getLocalIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("获取本地IP失败", e);
            return null;
        }
    }
}

3.4 注意事项

数据库文件放置位置

ip2region.xdb数据库文件需要正确放置,以便程序加载:

复制代码
src/main/resources/
└── ip2region.xdb

如果使用外部文件路径,修改加载逻辑:

java 复制代码
String dbPath = "/path/to/ip2region.xdb"; // 绝对路径
File file = new File(dbPath);
byte[] cBuff = Files.readAllBytes(file.toPath());
searcher = Searcher.newWithBuffer(cBuff);

异常捕获建议

java 复制代码
// 1. 初始化阶段:数据库加载失败不应影响应用启动
static {
    try {
        initIp2region();
    } catch (Exception e) {
        log.error("ip2region初始化失败,属地查询功能将不可用", e);
    }
}

// 2. 查询阶段:单个查询失败不应影响主流程
public static String getCityInfoSafe(String ip) {
    try {
        return getCityInfo(ip);
    } catch (Exception e) {
        log.warn("IP属地查询异常: {}", ip);
        return null;
    }
}

// 3. 业务层:优雅降级
public void recordUserLogin(Long userId, HttpServletRequest request) {
    String ip = IpUtil.getRealIp(request);
    String province = IpUtil.getIpPossessionSafe(ip); // 使用安全版本
    
    if (province != null) {
        // 正常处理
        loginRecordRepository.save(userId, ip, province);
    } else {
        // 降级处理:仅记录IP
        loginRecordRepository.save(userId, ip, "未知");
    }
}

性能优化点

  1. 静态初始化:查询器在类加载时初始化,避免每次查询都重新加载
  2. 并发安全:ip2region的Searcher是线程安全的,无需额外同步
  3. 缓存热点IP:对于高频查询的IP,可以增加一层本地缓存
  4. 异步日志:属地查询失败时,使用异步日志避免阻塞主流程
java 复制代码
// 热点IP缓存示例
private static final Cache<String, String> ipCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

public static String getCityInfoWithCache(String ip) {
    return ipCache.get(ip, key -> getCityInfo(key));
}

总结

本文详细介绍了一个功能完善的Java IP工具类,涵盖了IP地址获取、MAC地址获取和IP属地解析三大核心功能。通过合理的代码设计和异常处理,能够在各种复杂场景下稳定运行。

关键要点回顾

  • IP获取需要考虑多层代理场景,正确处理HTTP头优先级
  • MAC地址获取受环境影响较大,需要做好异常处理
  • ip2region的内存缓存模式性能优异,推荐在高并发场景使用
  • 业务集成时要注意异常捕获和优雅降级

希望这篇文章能够帮助你在项目中更好地应用IP工具类,为业务提供更精准的地理位置服务。


参考资源

相关推荐
无线图像传输研究探索2 小时前
如何提升机器狗 “超视距” 作战能力?
服务器·网络·5g·机器人·无线图传·机器狗
沛沛老爹2 小时前
Web转AI架构篇:Agent Skills vs MCP-混合架构设计模式实战指南
java·前端·人工智能·架构·llm·rag
独自破碎E2 小时前
Java的CMS垃圾回收流程
java·开发语言
oioihoii2 小时前
C++线程编程模型演进:从Pthread到jthread的技术革命
java·开发语言·c++
成工小白2 小时前
网络复习(1)
服务器·网络·php
数通工程师2 小时前
OSI 七层参考模型
网络
且去填词2 小时前
深入理解 GMP 模型:Go 高并发的基石
开发语言·后端·学习·算法·面试·golang·go
醇氧2 小时前
SqlLogInterceptor mybatis配置打印SQL
java·sql·mybatis
Elcker2 小时前
JAVA-Web 项目研发中如何保持团队研发风格的统一
java·前端·javascript