如何在 Spring Boot 中集成 IP2Region 实现高效 IP 地址地理位置查询

文章目录

IP2Region 是一个高效的 IP 地址地理位置查询库,能够快速根据 IP 地址获取其地理位置信息。它支持 IPv4 和 IPv6 地址的查询,并提供多种缓存方式来优化查询性能。在这篇博客中,我们将介绍如何将 IP2Region 集成到 Spring Boot 项目中,实现 IP 地址的地理位置查询服务。

一、添加 Maven 依赖

首先,在 Spring Boot 项目中使用 IP2Region,你需要在 pom.xml 文件中添加 IP2Region 的 Maven 依赖。

xml 复制代码
<dependency>
    <groupId>org.lionsoul</groupId>
    <artifactId>ip2region</artifactId>
    <version>3.1.1</version>
</dependency>

该依赖包含了 IP2Region 查询库,使得你能够在 Spring Boot 项目中使用 Searcher 类进行高效的 IP 地址查询。

二、配置数据库文件路径与版本

在 Spring Boot 的 application.propertiesapplication.yml 中配置 IP2Region 数据库文件的路径和 IP 版本(IPv4 或 IPv6)。

application.properties

properties 复制代码
# IP2Region 数据库文件路径
ip2region.db-path=ip2region_v4.xdb  # 可根据需要替换为 IPv6 文件路径
# 版本选择 IPv4 或 IPv6
ip2region.version=IPv4

application.yml

yaml 复制代码
ip2region:
  db-path: ip2region_v4.xdb  # 可根据需要替换为 IPv6 文件路径
  version: IPv4

ip2region_v4.xdb 是 IP2Region 的数据库文件,你需要下载并将其放置在 src/main/resources 目录下,确保 Spring Boot 能够通过 classpath 读取。

三、创建 IP 查询服务

接下来,我们将 IP2Region 封装为一个 Spring Bean,通过 @Service 注解让 Spring 管理这个服务类。在 IpService 类中,我们会初始化 Searcher 实例,并提供查询 IP 地址的功能。

IpService.java

java 复制代码
package com.donglin.service;

import jakarta.annotation.PostConstruct;
import org.lionsoul.ip2region.xdb.Searcher;
import org.lionsoul.ip2region.xdb.Version;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

@Service
public class IpService {

    private Searcher searcher;

    // 从配置文件中加载数据库路径和版本
    @Value("${ip2region.db-path}")
    private String dbPath;

    @Value("${ip2region.version}")
    private String version;

    // 服务启动时初始化 Searcher
    @PostConstruct
    public void init() throws IOException {
        // 根据配置选择 IP 版本
        Version ipVersion = version.equalsIgnoreCase("IPv4") ? Version.IPv4 : Version.IPv6;

        try {
            // 使用 ClassPathResource 加载类路径中的数据库文件
            Resource resource = new ClassPathResource(dbPath);
            if (!resource.exists()) {
                throw new IOException("Unable to find " + dbPath + " in classpath");
            }

            // 获取文件的输入流
            InputStream inputStream = resource.getInputStream();

            // 将 InputStream 保存到临时文件
            Path tempFile = Files.createTempFile("ip2region", ".xdb");
            try (OutputStream out = new FileOutputStream(tempFile.toFile())) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
            }

            // 使用临时文件创建 Searcher 对象
            searcher = Searcher.newWithFileOnly(ipVersion, tempFile.toString());

        } catch (IOException e) {
            System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
            throw new IOException("Failed to initialize IP2Region searcher", e);
        }
    }

    // 查询 IP 地理位置
    public String getCityInfo(String ip) {
        try {
            long sTime = System.nanoTime();
            String region = searcher.search(ip);  // 查询 IP 地址
            long cost = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - sTime);
            System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
            return region;
        } catch (Exception e) {
            System.out.printf("failed to search(%s): %s\n", ip, e);
            return null;
        }
    }

    // 在 Spring Boot 中使用 @PreDestroy 优雅地关闭 searcher
    public void close() throws IOException {
        if (searcher != null) {
            searcher.close();
        }
    }
}

解析:

  • @PostConstruct :这是 Spring 提供的生命周期注解,表示方法将在 Bean 初始化完成后执行。我们在这里初始化 Searcher 实例,基于配置的数据库文件路径和 IP 版本。
  • getCityInfo:此方法用于查询 IP 地址的地理位置信息。它会返回一个字符串,包含 IP 地址的地理位置(如国家、省份、城市等)。

四、创建控制器提供查询 API

为了让外部可以查询 IP 地址,我们创建一个 REST 控制器类 IpController,通过 HTTP GET 请求提供查询接口。

IpController.java

java 复制代码
package com.example.ip2region.controller;

import com.example.ip2region.service.IpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class IpController {

    private final IpService ipService;

    @Autowired
    public IpController(IpService ipService) {
        this.ipService = ipService;
    }

    // 查询 IP 地址的地理信息
    @GetMapping("/get-ip-location")
    public String getIpLocation(@RequestParam String ip) {
        return ipService.getCityInfo(ip);
    }
}

解析:

  • @GetMapping("/get-ip-location") :此注解将 HTTP GET 请求映射到 getIpLocation 方法。通过 @RequestParam 获取 IP 地址参数,并查询该 IP 的地理位置。
  • 该控制器提供了一个查询接口,可以通过 http://localhost:8080/get-ip-location?ip=1.2.3.4 查询 IP 地址 1.2.3.4 的地理位置。

五、测试

启动 Spring Boot 应用后,可以通过浏览器或 Postman 发起查询请求:

bash 复制代码
GET http://localhost:8080/get-ip-location?ip=1.2.3.4

查询结果将是 IP 地址 1.2.3.4 的地理位置信息,例如:

json 复制代码
{
  "region": "美国|华盛顿|0|谷歌"
}

六、优化:使用缓存来加速查询

IP2Region 支持使用缓存来优化查询性能。你可以通过预加载 VectorIndex 或整个 XDB 文件的内容,将其存储在内存中,从而减少文件 IO 操作,提升查询速度。

使用 VectorIndex 缓存

java 复制代码
package com.donglin.service;

import jakarta.annotation.PostConstruct;
import org.lionsoul.ip2region.xdb.Searcher;
import org.lionsoul.ip2region.xdb.Version;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;

@Service
public class IpService {

    private Searcher searcher;

    @Value("${ip2region.db-path}")
    private String dbPath;

    @Value("${ip2region.version}")
    private String version;

    @PostConstruct
    public void init() throws IOException {
        Version ipVersion = version.equalsIgnoreCase("IPv4") ? Version.IPv4 : Version.IPv6;

        try {
            // ✅ 1. 从 classpath 读取 XDB 文件(无论打包与否都兼容)
            Resource resource = new ClassPathResource(dbPath);
            if (!resource.exists()) {
                throw new IOException("无法在 classpath 中找到文件:" + dbPath);
            }

            // ✅ 2. 先把 XDB 文件写入临时文件(VectorIndex 只能基于文件加载)
            Path tempFile = Files.createTempFile("ip2region", ".xdb");
            try (InputStream is = resource.getInputStream()) {
                Files.copy(is, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
            }

            // ✅ 3. 预加载 VectorIndex(大幅减少 I/O)
            byte[] vIndex;
            try {
                vIndex = Searcher.loadVectorIndexFromFile(tempFile.toString());
                System.out.println("VectorIndex loaded, length = " + vIndex.length);
            } catch (Exception e) {
                throw new IOException("加载 VectorIndex 失败: " + e.getMessage(), e);
            }

            // ✅ 4. 创建使用 VectorIndex 缓存的 Searcher
            searcher = Searcher.newWithVectorIndex(ipVersion, tempFile.toString(), vIndex);
            System.out.println("IP2Region searcher initialized with vector index cache.");

        } catch (IOException e) {
            System.err.printf("failed to create searcher with `%s`: %s%n", dbPath, e);
            throw new IOException("Failed to initialize IP2Region searcher", e);
        }
    }

    // 查询 IP 地理位置
    public String getCityInfo(String ip) {
        try {
            long start = System.nanoTime();
            String region = searcher.search(ip);
            long cost = TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - start);
            System.out.printf("{region: %s, ioCount: %d, took: %d μs}%n",
                    region, searcher.getIOCount(), cost);
            return region;
        } catch (Exception e) {
            System.err.printf("failed to search(%s): %s%n", ip, e);
            return null;
        }
    }

    public void close() throws IOException {
        if (searcher != null) {
            searcher.close();
        }
    }
}

七、总结

通过将 ip2region 集成到 Spring Boot 中,我们可以非常高效地查询 IP 地址的地理位置信息。通过预加载缓存(如 VectorIndex 或整个 XDB 文件),我们可以进一步提高查询性能,减少 IO 压力,尤其是在高并发场景下。

相关推荐
山枕檀痕1 小时前
Spring Boot中LocalDateTime接收“yyyy-MM-dd HH:mm:ss“格式参数的最佳实践
java·spring boot·后端
乔伊酱2 小时前
Bean Searcher 遇“鬼”记:为何我的查询条件偷偷跑进了 HAVING?
java·前端·orm
invicinble2 小时前
idea提供maven处理机制
java·maven·intellij-idea
fantasy5_52 小时前
C++11 核心特性实战博客
java·开发语言·c++
喜欢流萤吖~2 小时前
Java函数式接口详解
java
夏乌_Wx2 小时前
练题100天——DAY22:数字拼接+只出现一次的数字
java·数据结构·算法
二川bro2 小时前
类型错误详解:Python TypeError排查手册
android·java·python
青云交2 小时前
Java 大视界 -- Java 大数据在智能医疗电子病历数据分析与临床决策支持中的应用
java·flink·数据清洗·电子病历·智能医疗·医疗信息化·临床决策
万物皆字节2 小时前
spring-boot之@KafkaListener注解自动装配流程
spring boot