# baidu-ip-server **Repository Path**: ppnt/baidu-ip-server ## Basic Information - **Project Name**: baidu-ip-server - **Description**: 百度IP查询服务 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2024-11-02 - **Last Updated**: 2024-12-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 封装 IP 查询服务 在互联网应用中,获取用户的地理位置信息是一项常见且重要的功能。本文将详细介绍如何封装一个基于百度 IP 查询服务的 IP 服务,包括从申请百度 API 密钥到实现完整的业务逻辑。通过本文,您将学会如何搭建一个高效、可缓存的 IP 查询服务,减少对百度 API 的调用次数,从而降低成本。 ## 文章目的 本文旨在为开发者提供一个完整的、可参考的案例,展示如何利用百度的 IP 查询 API,结合数据库缓存机制,构建一个高效的 IP 服务。通过本文,您将了解如何配置项目依赖、设计数据库、编写业务逻辑以及实现接口层,从而实现一个功能完善的 IP 查询服务。 ## 前置条件 在开始之前,确保您具备以下条件: - 熟悉 Java 编程语言 - 了解 Maven 项目管理工具 - 具备基本的数据库操作知识(本文以 PostgreSQL 为例) - 拥有百度 API 的访问权限(需要申请 API 密钥) ## 步骤一:申请百度 API 密钥 首先,您需要在百度开放平台([百度开放平台](https://developer.baidu.com/))注册并申请 IP 查询服务的 API 密钥(AK)。申请过程通常包括: 1. 注册百度开放平台账号。 2. 创建新应用,并启用 IP 定位服务。 3. 获取分配的 API 密钥(AK)。 申请完成后,将获得一个唯一的 AK,用于后续 API 调用。 ## 步骤二:数据库设置 由于调用百度的 IP 查询服务是需要付费的,为了降低成本,我们将采用缓存机制,将查询结果存储在数据库中。以下是创建缓存表的 SQL 语句: ```sql DROP TABLE IF EXISTS sys_baidu_ip; CREATE TABLE sys_baidu_ip ( ip VARCHAR PRIMARY KEY, address VARCHAR, adcode VARCHAR, city VARCHAR, city_code INT, district VARCHAR, province VARCHAR, street VARCHAR, street_number VARCHAR, x VARCHAR, y VARCHAR, creator VARCHAR(64) DEFAULT '', create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updater VARCHAR(64) DEFAULT '', update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT DEFAULT 0, tenant_id BIGINT NOT NULL DEFAULT 0 ); ``` ### 表结构说明 - **ip**:IP 地址,作为主键。 - **address**:地理位置描述。 - **adcode**:行政区划代码。 - **city**、**province**、**district**等字段:详细的地理信息。 - **x**、**y**:坐标信息。 - 其他字段用于记录创建和更新时间,以及租户信息。 ## 步骤三:Maven 项目配置(pom.xml) 在项目的`pom.xml`文件中,配置必要的依赖项和项目属性。以下是一个示例配置: ```xml UTF-8 1.8 ${java.version} ${java.version} 23.1.1 2.0.52 1.18.30 1.1.4 3.7.3.v20241014-RELEASE 1.8.5 1.2.6 1.3.3 1.4.3 web-hello com.litongjava.ip.baidu.BaiduAppServer ch.qos.logback logback-classic 1.2.3 org.lionsoul ip2region 2.7.0 ``` ### 关键依赖说明 - **logback-classic**:日志框架,用于记录日志信息。 - **ip2region**:本地 IP 查询库,提高查询效率,减少对外部 API 的依赖。 - **fastjson2**:用于 JSON 解析。 - **OkHttp**:HTTP 客户端,用于发送 API 请求。 - **Lombok**:简化 Java 代码,减少样板代码。 - **JUnit**:用于编写和运行测试用例。 ## 步骤四:配置文件 配置项目所需的属性文件,包括应用环境、数据库连接信息和 API 密钥。 ### app.properties ```properties app.env=dev server.port=8011 ``` ### app-dev.properties ```properties DATABASE_DSN=postgresql://postgres:123456@192.168.3.9/little_red_book_data jdbc.MaximumPoolSize=2 ``` ### .env ```properties baidu.ak=YOUR_BAIDU_AK_HERE ``` **说明**: - `app.env`:指定应用运行环境(开发环境为`dev`)。 - `server.port`:应用服务器监听的端口号。 - `DATABASE_DSN`:数据库连接字符串,包含数据库类型、用户名、密码和地址。 - `baidu.ak`:百度 API 的访问密钥,需要替换为实际申请到的 AK。 ## 步骤五:客户端实现(BaiduIpClient) 实现与百度 IP 查询 API 的交互,负责发送 HTTP 请求并处理响应。 ```java package com.litongjava.ip.baidu.client; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.LinkedHashMap; import java.util.Map; import com.litongjava.model.http.response.ResponseVo; import com.litongjava.tio.utils.environment.EnvUtils; import com.litongjava.tio.utils.http.Http; public class BaiduIpClient { public static String URL = "https://api.map.baidu.com/location/ip?"; public static String AK = EnvUtils.get("baidu.ak"); /** * 查询IP信息。 * * @param ip 要查询的IP地址 * @return 响应结果 */ public static ResponseVo index(String ip) { if (AK == null) { throw new RuntimeException("AK为空,请检查配置文件中的baidu.ak"); } Map params = new LinkedHashMap<>(); params.put("ip", ip); params.put("coor", "bd09ll"); params.put("ak", AK); return requestGetAK(URL, params); } /** * 发送GET请求到指定URL,并附带参数。 * * @param strUrl 请求URL * @param param 请求参数 * @return 响应结果 */ public static ResponseVo requestGetAK(String strUrl, Map param) { if (strUrl == null || strUrl.isEmpty() || param == null || param.isEmpty()) { return null; } StringBuilder targetUrl = new StringBuilder(strUrl); for (Map.Entry pair : param.entrySet()) { targetUrl.append(pair.getKey()).append("="); try { String encodedValue = URLEncoder.encode(pair.getValue(), "UTF-8").replace("+", "%20") + "&"; targetUrl.append(encodedValue); } catch (UnsupportedEncodingException e) { throw new RuntimeException("URL编码失败", e); } } // 移除最后一个'&'字符 if (targetUrl.length() > 0) { targetUrl.deleteCharAt(targetUrl.length() - 1); } return Http.get(targetUrl.toString()); } } ``` ### 关键功能说明 - **index 方法**:接收 IP 地址,构建请求参数,并调用`requestGetAK`方法发送请求。 - **requestGetAK 方法**:构建完整的请求 URL,进行 URL 编码,并发送 GET 请求。 ## 步骤六:常量定义 定义数据库表名等常量,便于维护和修改。 ```java package com.litongjava.ip.baidu.consts; public interface TableNames { String SYS_BAIDU_IP = "sys_baidu_ip"; } ``` **说明**:将表名定义为常量,避免在代码中硬编码,增加可读性和可维护性。 ## 步骤七:数据层实现(BaiduIpDao) 实现数据访问对象(DAO),负责与数据库进行交互,包括查询和保存 IP 信息。 ```java package com.litongjava.ip.baidu.dao; import com.litongjava.db.activerecord.Db; import com.litongjava.db.activerecord.Record; import com.litongjava.ip.baidu.consts.TableNames; public class BaiduIpDao { /** * 根据IP查询地址信息。 * * @param ip IP地址 * @return 地址信息 */ public String getAddressById(String ip) { String sql = "SELECT address FROM %s WHERE ip=?"; return Db.queryStr(String.format(sql, TableNames.SYS_BAIDU_IP), ip); } /** * 保存IP信息到数据库。 * * @param ip IP地址 * @param address 地址信息 * @param adcode 行政区划代码 * @param province 省份 * @param city 城市 * @param city_code 城市代码 * @param district 区县 * @param street 街道 * @param street_number 街道编号 * @param x 经度 * @param y 纬度 * @return 保存是否成功 */ public boolean save(String ip, String address, String adcode, String province, String city, Integer city_code, String district, String street, String street_number, String x, String y) { Record record = Record.by("ip", ip); record.set("address", address) .set("adcode", adcode) .set("province", province) .set("city", city) .set("city_code", city_code) .set("district", district) .set("street", street) .set("street_number", street_number) .set("x", x) .set("y", y); return Db.save(TableNames.SYS_BAIDU_IP, record); } } ``` ### 关键功能说明 - **getAddressById 方法**:通过 IP 地址查询缓存表中的地址信息。 - **save 方法**:将从百度 API 获取的 IP 信息保存到数据库中,便于下次查询时直接使用缓存。 ## 步骤八:业务层实现(BaiduIpService 和 IpService) 业务层负责处理具体的业务逻辑,包括查询、缓存和判断 IP 归属地。 ### BaiduIpService ```java package com.litongjava.ip.baidu.service; import com.alibaba.fastjson2.JSONObject; import com.litongjava.ip.baidu.client.BaiduIpClient; import com.litongjava.ip.baidu.dao.BaiduIpDao; import com.litongjava.jfinal.aop.Aop; import com.litongjava.model.http.response.ResponseVo; import com.litongjava.tio.utils.json.FastJson2Utils; public class BaiduIpService { /** * 根据IP查询地址信息,如果缓存中有则直接返回,否则调用百度API查询并缓存结果。 * * @param ip IP地址 * @return 地址信息或null */ public String search(String ip) { // 从缓存中查询 String address = Aop.get(BaiduIpDao.class).getAddressById(ip); if (address != null) { return address; } ResponseVo responseVo; try { // 调用百度API查询 responseVo = BaiduIpClient.index(ip); } catch (Exception e) { throw new RuntimeException("调用百度API失败", e); } if (responseVo.isOk()) { JSONObject jsonObject = FastJson2Utils.parseObject(responseVo.getBodyString()); Integer status = jsonObject.getInteger("status"); if (status == 0) { // 解析响应结果 address = jsonObject.getString("address"); JSONObject content = jsonObject.getJSONObject("content"); JSONObject address_detail = content.getJSONObject("address_detail"); JSONObject point = content.getJSONObject("point"); String adcode = address_detail.getString("adcode"); String city = address_detail.getString("city"); Integer city_code = address_detail.getInteger("city_code"); String district = address_detail.getString("district"); String province = address_detail.getString("province"); String street = address_detail.getString("street"); String street_number = address_detail.getString("street_number"); String x = point.getString("x"); String y = point.getString("y"); // 将结果保存到缓存 Aop.get(BaiduIpDao.class).save(ip, address, adcode, province, city, city_code, district, street, street_number, x, y); return address; } else { return null; } } else { return null; } } } ``` ### IpService ```java package com.litongjava.ip.baidu.service; import com.litongjava.ip.baidu.utils.Ip2RegionUtils; import com.litongjava.ip.baidu.utils.IpRegionUtils; import com.litongjava.jfinal.aop.Aop; public class IpService { /** * 根据IP查询地理位置信息。 * * @param ip IP地址 * @return 地理位置信息 */ public String search(String ip) { // 校验IP格式 boolean checkIp = Ip2RegionUtils.checkIp(ip); if (!checkIp) { return "不是有效的IP地址"; } // 使用ip2region进行本地查询 String searchIp = Ip2RegionUtils.searchIp(ip); String[] split = searchIp.split("\\|"); String area = split[2]; // 判断是否为中国大陆IP if (isChinaMainland(searchIp, area)) { String result = Aop.get(BaiduIpService.class).search(ip); if (result != null) { return result; } } return searchIp; } /** * 根据IP查询所属区域代码。 * * @param ip IP地址 * @return 区域代码 */ public String area(String ip) { // 校验IP格式 boolean checkIp = Ip2RegionUtils.checkIp(ip); if (!checkIp) { return "不是有效的IP地址"; } // 使用ip2region进行本地查询 String searchIp = Ip2RegionUtils.searchIp(ip); String[] split = searchIp.split("\\|"); String area = split[2]; // 判断是否为中国大陆IP if (isChinaMainland(searchIp, area)) { String result = Aop.get(BaiduIpService.class).search(ip); if (result != null) { return "86"; // 中国大陆区号 } } return String.valueOf(IpRegionUtils.type(area)); } /** * 判断IP是否属于中国大陆。 * * @param searchIp 查询结果 * @param area 区域名称 * @return 是否为中国大陆IP */ private boolean isChinaMainland(String searchIp, String area) { if (searchIp.startsWith("中国")) { if ("香港".equals(area) || "澳门".equals(area) || "台湾".equals(area)) { return false; } else { return true; } } else if (searchIp.startsWith("0")) { return true; } return false; } } ``` ### 关键功能说明 - **search 方法**:首先校验 IP 格式,然后使用本地的 ip2region 库查询 IP 信息。如果 IP 属于中国大陆,则调用百度 API 查询详细信息并缓存结果;否则,直接返回本地查询结果。 - **area 方法**:返回 IP 所属区域的代码,例如中国大陆为`86`,香港为`852`等。 - **isChinaMainland 方法**:辅助判断 IP 是否属于中国大陆,排除香港、澳门和台湾。 ## 步骤九:工具类实现(Ip2RegionUtils 和 IpRegionUtils) 工具类用于 IP 格式校验和 IP 归属地查询。 ### Ip2RegionUtils ```java package com.litongjava.ip.baidu.utils; import java.io.IOException; import java.net.URL; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.lionsoul.ip2region.xdb.Searcher; import com.litongjava.tio.utils.hutool.FileUtil; import com.litongjava.tio.utils.hutool.ResourceUtil; public enum Ip2RegionUtils { INSTANCE; // IP地址正则表达式 private static final String IP_REGEX = "([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}"; private static final Pattern pattern = Pattern.compile(IP_REGEX); private static Searcher searcher; static { // 加载ip2region数据库 URL resource = ResourceUtil.getResource("ipdb/ip2region.xdb"); if (resource != null) { byte[] bytes = FileUtil.readUrlAsBytes(resource); try { searcher = Searcher.newWithBuffer(bytes); } catch (IOException e) { e.printStackTrace(); } } } /** * 校验IP格式是否有效。 * * @param ipAddress IP地址 * @return 是否有效 */ public static boolean checkIp(String ipAddress) { Matcher matcher = pattern.matcher(ipAddress); return matcher.matches(); } /** * 根据IP地址查询归属地。 * * @param ip IP地址 * @return 归属地信息 */ public static String searchIp(String ip) { if (searcher != null) { try { return searcher.search(ip); } catch (Exception e) { throw new RuntimeException("IP查询失败", e); } } return null; } } ``` ### IpRegionUtils ```java package com.litongjava.ip.baidu.utils; public class IpRegionUtils { /** * 根据区域名称返回对应的区域代码。 * * @param area 区域名称 * @return 区域代码 */ public static int type(String area) { switch (area) { case "香港": return 852; case "澳门": return 853; case "台湾": return 886; default: return 0; // 全球 } } } ``` ### 关键功能说明 - **Ip2RegionUtils**: - **checkIp 方法**:使用正则表达式校验 IP 地址格式。 - **searchIp 方法**:通过 ip2region 库查询 IP 归属地信息,避免频繁调用外部 API。 - **IpRegionUtils**: - **type 方法**:将区域名称转换为对应的区号,例如香港为`852`。 ## 步骤十:接口层实现(IpHandler) 实现 HTTP 接口,接收客户端请求并返回查询结果。 ```java package com.litongjava.ip.baidu.handler; import com.litongjava.ip.baidu.service.IpService; import com.litongjava.jfinal.aop.Aop; import com.litongjava.tio.boot.http.TioRequestContext; import com.litongjava.tio.http.common.HttpRequest; import com.litongjava.tio.http.common.HttpResponse; import com.litongjava.tio.http.server.util.CORSUtils; import com.litongjava.tio.http.server.util.Resps; public class IpHandler { /** * 处理IP查询请求。 * * @param request HTTP请求 * @return HTTP响应 */ public HttpResponse search(HttpRequest request) { HttpResponse response = TioRequestContext.getResponse(); // 启用CORS支持 CORSUtils.enableCORS(response); // 获取请求参数中的IP地址 String ip = request.getParam("ip"); // 调用业务层查询IP信息 String result = Aop.get(IpService.class).search(ip); // 返回文本响应 Resps.txt(response, result); return response; } /** * 处理IP区域代码查询请求。 * * @param request HTTP请求 * @return HTTP响应 */ public HttpResponse area(HttpRequest request) { HttpResponse response = TioRequestContext.getResponse(); // 启用CORS支持 CORSUtils.enableCORS(response); // 获取请求参数中的IP地址 String ip = request.getParam("ip"); // 调用业务层查询IP所属区域代码 String result = Aop.get(IpService.class).area(ip); // 返回文本响应 Resps.txt(response, result); return response; } } ``` ### 关键功能说明 - **search 方法**:接收 IP 查询请求,调用业务层获取地址信息,并返回给客户端。 - **area 方法**:接收 IP 区域代码查询请求,调用业务层获取区域代码,并返回给客户端。 - **CORS 支持**:允许跨域请求,确保 API 可以被不同域名的前端应用访问。 ## 步骤十一:配置层实现(DbConfig 和 WebConfig) 配置数据库连接和 HTTP 路由,确保各个组件能够正确协同工作。 ### DbConfig ```java package com.litongjava.ip.baidu.config; import com.jfinal.template.Engine; import com.jfinal.template.source.ClassPathSourceFactory; import com.litongjava.annotation.AConfiguration; import com.litongjava.annotation.Initialization; import com.litongjava.db.activerecord.ActiveRecordPlugin; import com.litongjava.db.activerecord.OrderedFieldContainerFactory; import com.litongjava.db.activerecord.dialect.PostgreSqlDialect; import com.litongjava.db.hikaricp.HikariCpPlugin; import com.litongjava.model.dsn.JdbcInfo; import com.litongjava.tio.boot.server.TioBootServer; import com.litongjava.tio.utils.dsn.DbDSNParser; import com.litongjava.tio.utils.environment.EnvUtils; import lombok.extern.slf4j.Slf4j; @Slf4j @AConfiguration public class DbConfig { @Initialization public void config() { // 获取数据库连接信息 String dsn = EnvUtils.getStr("DATABASE_DSN"); log.info("数据库DSN: {}", dsn); if (dsn == null) { return; } JdbcInfo jdbc = new DbDSNParser().parse(dsn); // 初始化 HikariCP 数据库连接池 final HikariCpPlugin hikariCpPlugin = new HikariCpPlugin(jdbc.getUrl(), jdbc.getUser(), jdbc.getPswd()); int maximumPoolSize = EnvUtils.getInt("jdbc.MaximumPoolSize", 10); hikariCpPlugin.setMaximumPoolSize(maximumPoolSize); hikariCpPlugin.start(); // 初始化 ActiveRecordPlugin final ActiveRecordPlugin arp = new ActiveRecordPlugin(hikariCpPlugin); // 开发环境下启用开发模式 if (EnvUtils.isDev()) { arp.setDevMode(true); } // 是否展示 SQL boolean showSql = EnvUtils.getBoolean("jdbc.showSql", false); log.info("是否展示SQL: {}", showSql); arp.setShowSql(showSql); arp.setDialect(new PostgreSqlDialect()); arp.setContainerFactory(new OrderedFieldContainerFactory()); // 配置模板引擎 Engine engine = arp.getEngine(); engine.setSourceFactory(new ClassPathSourceFactory()); engine.setCompressorOn(); // 启用压缩功能 // 启动 ActiveRecordPlugin arp.start(); // 添加销毁方法,确保应用关闭时释放资源 TioBootServer.me().addDestroyMethod(() -> { hikariCpPlugin.stop(); arp.stop(); }); } } ``` ### WebConfig ```java package com.litongjava.ip.baidu.config; import com.litongjava.annotation.AConfiguration; import com.litongjava.annotation.Initialization; import com.litongjava.ip.baidu.handler.IpHandler; import com.litongjava.tio.boot.server.TioBootServer; import com.litongjava.tio.http.server.router.HttpRequestRouter; @AConfiguration public class WebConfig { @Initialization public void config() { // 获取服务器实例和路由器 TioBootServer server = TioBootServer.me(); HttpRequestRouter requestRouter = server.getRequestRouter(); // 创建IP处理器实例 IpHandler ipHandler = new IpHandler(); // 添加路由映射 requestRouter.add("/ip", ipHandler::search); requestRouter.add("/area", ipHandler::area); } } ``` ### 关键功能说明 - **DbConfig**: - 配置数据库连接池(HikariCP)和 ActiveRecord 插件,确保应用能够与数据库正常通信。 - 根据环境变量设置开发模式和 SQL 日志输出。 - **WebConfig**: - 配置 HTTP 路由,将特定 URL 路径映射到对应的处理器方法。 - 例如,`/ip`路径映射到`IpHandler`的`search`方法,`/area`路径映射到`area`方法。 ## 步骤十二:测试接口 配置完成后,可以通过以下 URL 进行接口测试: 1. **查询 IP 地址信息** ```http GET http://127.0.0.1:8011/ip?ip=111.55.13.145 ``` **预期响应**: ``` 中国|0|香港|0|0 ``` 2. **查询 IP 地址区域代码** ```http GET http://127.0.0.1:8011/area?ip=154.212.150.171 ``` **预期响应**: ``` 852 ``` ### 测试说明 - **/ip 接口**:返回指定 IP 的地理位置信息。如果 IP 属于中国大陆,会优先从缓存中查询,否则返回本地查询结果。 - **/area 接口**:返回指定 IP 所属区域的代码,例如中国大陆为`86`,香港为`852`等。 ## 结论 通过本文的详细步骤,您已经学会了如何封装一个基于百度 IP 查询服务的 IP 服务。该服务结合了本地缓存和外部 API 查询,既提高了查询效率,又有效控制了成本。您可以根据实际需求,进一步扩展和优化该服务,例如增加更多的缓存策略、支持更多的查询参数或集成到更复杂的系统中。 希望本文对您在实际项目中实现 IP 查询功能有所帮助!