一、核心需求与背景
当多台服务器(如两台应用服务器)运行相同代码时,日志文件 / 日志平台中无法直接区分日志来自哪台机器,排查问题时效率极低。解决思路是:在日志中固定输出当前服务器的 IPv4 地址,通过 IP 字段快速定位日志归属。
二、前置知识:Java 获取服务器有效 IPv4
要打印 IP 首先要能正确获取服务器的真实业务 IPv4(排除回环地址、虚拟网卡),以下是封装好的通用工具类:
1. IPv4 获取工具类(ServerIpUtils)
|---|---------------------------------------------------------------------------------------|
| | import java.net.*; |
| | import java.util.Enumeration; |
| | |
| | /** |
| | * 服务器IP工具类:获取真实业务IPv4,缓存IP提升性能 |
| | */ |
| | public class ServerIpUtils { |
| | // 静态缓存本机IP,仅应用启动时获取一次 |
| | private static final String LOCAL_IP; |
| | |
| | // 静态代码块初始化IP |
| | static { |
| | LOCAL_IP = getMainLocalIpv4(); |
| | } |
| | |
| | /** |
| | * 核心方法:获取服务器对外通信的主IPv4 |
| | * 过滤回环地址、禁用网卡、虚拟网卡(Docker/VPN等) |
| | */ |
| | private static String getMainLocalIpv4() { |
| | try { |
| | Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); |
| | while (interfaces.hasMoreElements()) { |
| | NetworkInterface ni = interfaces.nextElement(); |
| | // 过滤规则:跳过回环/禁用/虚拟网卡 |
| | if (ni.isLoopback() || !ni.isUp() |
| | || ni.getName().startsWith("docker") |
| | || ni.getName().startsWith("veth")) { |
| | continue; |
| | } |
| | // 遍历网卡下的IP地址 |
| | Enumeration<InetAddress> addresses = ni.getInetAddresses(); |
| | while (addresses.hasMoreElements()) { |
| | InetAddress addr = addresses.nextElement(); |
| | // 仅保留IPv4地址 |
| | if (addr instanceof Inet4Address) { |
| | return addr.getHostAddress(); |
| | } |
| | } |
| | } |
| | } catch (SocketException e) { |
| | e.printStackTrace(); |
| | } |
| | // 兜底返回回环地址 |
| | return "127.0.0.1"; |
| | } |
| | |
| | /** |
| | * 对外提供获取本机IP的方法 |
| | */ |
| | public static String getLocalIp() { |
| | return LOCAL_IP; |
| | } |
| | } |
工具类关键说明
- 缓存优化:通过静态代码块初始化 IP,仅在应用启动时获取一次,避免每次打印日志遍历网卡,提升性能;
- 精准过滤:排除回环地址(127.0.0.1)、禁用网卡、Docker/VPN 虚拟网卡,确保获取服务器真实业务 IP;
- 跨平台兼容:适配 Linux/Windows 服务器,无需修改即可使用。
三、两种日志打印 IP 的实现方案
方案 1:快速调试 - 手动拼接 IP(适合临时排查)
直接在日志语句中拼接 IP 字段,快速生效,适合临时调试场景。
代码示例
|---|----------------------------------------------------------------------------------------|
| | import org.slf4j.Logger; |
| | import org.slf4j.LoggerFactory; |
| | |
| | public class BusinessService { |
| | // 初始化日志对象 |
| | private static final Logger logger = LoggerFactory.getLogger(BusinessService.class); |
| | // 获取服务器IP(启动时加载,全局复用) |
| | private static final String SERVER_IP = ServerIpUtils.getLocalIp(); |
| | |
| | public void doBusiness() { |
| | // 日志中拼接IP前缀,清晰标识服务器 |
| | logger.info("[SERVER_IP:{}] 执行业务逻辑,参数:{}", SERVER_IP, "test123"); |
| | logger.error("[SERVER_IP:{}] 业务执行失败,异常信息:{}", SERVER_IP, "空指针异常"); |
| | } |
| | |
| | public static void main(String[] args) { |
| | new BusinessService().doBusiness(); |
| | } |
| | } |
输出效果
|---|------------------------------------------------------------------------------------------------------------------|
| | 2026-02-25 10:00:00.123 INFO [main] com.example.BusinessService - [SERVER_IP:192.168.10.20] 执行业务逻辑,参数:test123 |
| | 2026-02-25 10:00:01.456 ERROR [main] com.example.BusinessService - [SERVER_IP:192.168.10.21] 业务执行失败,异常信息:空指针异常 |
方案 2:生产环境 - MDC + 日志框架配置(推荐)
通过日志框架(Logback/Log4j2)的 MDC(映射诊断上下文)实现 IP 自动附加,业务代码无侵入,符合生产环境最佳实践。
步骤 1:SpringBoot 项目初始化 MDC
在应用启动时将 IP 放入 MDC,全局生效:
|---|------------------------------------------------------------------------|
| | import org.slf4j.MDC; |
| | import org.springframework.boot.SpringApplication; |
| | import org.springframework.boot.autoconfigure.SpringBootApplication; |
| | import javax.annotation.PostConstruct; |
| | |
| | @SpringBootApplication |
| | public class ServerApplication { |
| | |
| | public static void main(String[] args) { |
| | SpringApplication.run(ServerApplication.class, args); |
| | } |
| | |
| | /** |
| | * 项目启动后初始化MDC,添加服务器IP |
| | * @PostConstruct:Bean初始化完成后执行 |
| | */ |
| | @PostConstruct |
| | public void initMdc() { |
| | MDC.put("SERVER_IP", ServerIpUtils.getLocalIp()); |
| | } |
| | } |
步骤 2:配置 Logback 日志格式(logback-spring.xml)
修改日志输出模板,自动包含 MDC 中的SERVER_IP字段:
|---|--------------------------------------------------------------------------------------------------------------------|
| | <?xml version="1.0" encoding="UTF-8"?> |
| | <configuration> |
| | <!-- 控制台输出 --> |
| | <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> |
| | <encoder> |
| | <!-- 日志格式:时间 [线程] [服务器IP] 级别 类名 - 内容 --> |
| | <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [SERVER_IP:%X{SERVER_IP}] %-5level %logger{50} - %msg%n</pattern> |
| | <charset>UTF-8</charset> |
| | </encoder> |
| | </appender> |
| | |
| | <!-- 文件输出 --> |
| | <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> |
| | <file>/logs/app.log</file> |
| | <!-- 按天分割日志 --> |
| | <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> |
| | <fileNamePattern>/logs/app.%d{yyyy-MM-dd}.log</fileNamePattern> |
| | </rollingPolicy> |
| | <encoder> |
| | <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [SERVER_IP:%X{SERVER_IP}] %-5level %logger{50} - %msg%n</pattern> |
| | <charset>UTF-8</charset> |
| | </encoder> |
| | </appender> |
| | |
| | <!-- 根日志级别 --> |
| | <root level="INFO"> |
| | <appender-ref ref="CONSOLE"/> |
| | <appender-ref ref="FILE"/> |
| | </root> |
| | </configuration> |
步骤 3:业务代码正常打印日志
无需手动拼接 IP,日志框架自动附加:
|---|-------------------------------------------------|
| | logger.info("执行业务逻辑,参数:{}", "test123"); |
| | logger.error("业务执行失败,异常:{}", e.getMessage()); |
最终输出效果
|---|-----------------------------------------------------------------------------------------------------------------|
| | 2026-02-25 10:05:00.123 [main] [SERVER_IP:192.168.10.20] INFO com.example.BusinessService - 执行业务逻辑,参数:test123 |
| | 2026-02-25 10:05:01.456 [main] [SERVER_IP:192.168.10.21] ERROR com.example.BusinessService - 业务执行失败,异常:空指针异常 |
四、关键注意事项
- IP 准确性:工具类过滤了 Docker、VPN 等虚拟网卡,确保获取的是服务器真实业务 IP;
- 性能优化:IP 仅在应用启动时获取一次并缓存,避免高频日志场景下重复遍历网卡;
- 兼容性:工具类兼容 JDK8+,支持 Linux/Windows 服务器,无需额外依赖;
- 日志框架适配 :Log4j2 配置逻辑与 Logback 一致,仅需修改日志格式中的
%X{SERVER_IP}(Log4j2 中同样生效)。
五、总结
| 方案类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 手动拼接 IP | 临时调试 | 实现简单、快速生效 | 业务代码侵入、不优雅 |
| MDC + 日志配置 | 生产环境 | 无代码侵入、全局生效 | 需配置日志框架 |