尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

如何安全清理项目中无人使用的死代码与无用接口?

如何安全清理项目中无人使用的死代码与无用接口? 在开发团队里待久了你会发现一个很有意思的现象很多系统里都存在一些“看起来根本没人用”的功能。它们可能是接入文档都找不到的老接口可能是配置中心里躺了好几年的开关项也可能是一段连作者都说不清用途的代码逻辑。但当你跟同事说“这功能是不是没用删了吧”的时候大概率会收到一句“先留着吧万一以后要用呢”。于是这些功能继续躺着占用代码仓库的空间占用服务器的内存占用每次排障时的人力。这篇文章想换个角度聊这件事。我们不讨论“产品经理提了个没用的需求”这种段子而是从技术视角出发讲清楚一个项目里为什么会出现“没用的功能”如何科学地判断一个功能是否真的没用以及如何安全、可回滚地把这些功能清理掉。无论你是后端开发、项目负责人还是正在做系统重构的技术人员这套思路和实操步骤都可以直接参考。1. 背景与核心概念1.1 什么是“没用的功能”先给“没用的功能”下一个可操作的定义一个功能如果不再被任何真实用户、任何上游系统、任何内部任务调用也没有参与核心业务流程同时未来也没有明确的可预期使用计划那么它在当前阶段就可以被认为是“没用的功能”。这里要强调一下“当前阶段”。技术上的无用和产品上的无用不是一回事。一个功能可能产品侧已经停止推广但技术上仍然有外部合作方在调用一个接口可能页面入口已经下线了但定时任务还在执行。所以我们不能凭感觉判断而是要通过代码、日志、配置、流量四个维度去验证。1.2 项目里为什么会积累“没用的功能”原因通常集中在以下几类业务快速迭代时旧功能被新功能替代但旧代码没有同步下线。大版本升级时为了兼容老客户端保留了一批老接口后来客户端升级完了接口忘了删。配置中心里的开关项开启了之后再也没人调整过默认值就被一直沿用。开发阶段临时加的调试接口、Mock 逻辑、测试数据生成脚本顺手留在了主干代码里。团队交接不到位模块归属不明确后来者不敢动怕出问题。这些原因背后其实都是同一个问题缺少“功能生命周期管理”的意识。我们平时很关注功能的开发上线却很少关注功能的退役下线。1.3 清理无用功能能带来什么收益清理无用功能不是“强迫症”行为而是实打实的技术收益减少代码维护量新同学接手项目时不会被大量无效代码干扰。降低安全风险不再维护的接口可能缺少安全补丁容易成为攻击面。缩小部署产物体积减少不必要的依赖和类加载。降低排查问题的噪音日志和监控指标里不会混入无效调用。让架构更清晰后续重构时有更准确的边界。不过清理无用功能也是有风险的尤其在生产环境中删错了代码导致线上故障的概率并不低。所以这篇文章会把“如何安全识别”和“如何安全删除”作为重点。2. 无用功能的常见形态与识别思路2.1 代码层面的无用功能代码层面的无用功能最常见的就是死代码dead code和无效依赖。死代码包括没有被任何地方引用的类、方法、字段被注释掉的大段逻辑catch 住异常却什么都不做的空处理以及通过反射、SPI 等方式加载但实际没有注册实现的类。无效依赖包括pom.xml 或 build.gradle 中声明了但从未 import 的第三方包同一个功能有多个版本重复引入测试代码引用了生产代码里的临时工具类等。识别这类问题可以借助 IDE 的静态检查。IDEA 里对长时间没被调用的方法会显示灰色高亮提示但需要注意这只适用于单模块工程。对于多模块工程可以借助mvn dependency:analyze分析依赖引用情况不过它也只是一个辅助参考不能完全替代人工确认。2.2 接口层面的无用功能接口层面的无用功能指的是已经部署到生产环境但已经不再被真实客户端调用的 HTTP 接口、RPC 接口或 MQ 消费者。这类功能最典型的表现是监控大盘上长期调用量为 0或者只有测试环境偶尔有人 ping 一下。但这里要特别小心调用量为 0 不代表真的没人用可能只是日志没有采集到也可能是调用方走了 Nginx 缓存、本地缓存或者通过内网域名绕过了网关。所以接口层面的判断一定要结合调用链追踪Trace和网关访问日志。如果没有接入调用链系统可以先通过网关层按接口路径维度统计调用量再结合下游服务日志确认。2.3 配置层面的无用功能配置层面的无用功能在 Sprin g Boot Nacos/Apollo 这类项目里很常见。比如apollo.bootstrap.enabledtrue这种基础配置当然不能乱删但有些业务配置项比如“XX 活动开关”、“XX 功能灰度比例”活动结束后就再也没变过对应的代码逻辑也已经被移除了这些配置项就成了无用配置。配置项的问题在于它不像代码那样容易检索。很多时候配置项分散在多个命名空间、多个环境里而且没有注释说明根本不知道是谁在用。识别配置层面无用功能最可靠的方法是做全链路检索先找到所有读取该配置项的代码位置再确认这些代码是否仍然存活如果代码已经删除配置项就可以进入清理流程。2.4 数据层面的无用功能数据层面的无用功能包括历史遗留的旧表、被废弃但还在同步的中间表、冗余的索引、不落库但还在定时写入的临时表。清理数据表是比较敏感的操作一般不建议直接删除而是采用“先停写、观察、再迁移、最后归档/删除”的策略。尤其是已有线上服务依赖的表直接删除等于自杀。后面会专门讲这部分操作。3. 实战第一步用工具建立“无用功能清单”在动手之前先要把项目里可能存在无用功能的候选名单列出来。推荐用“从入口到出口”的梳理方式逐层排查最后形成一份表格。3.1 从入口层筛选零调用接口以 Spring Boot 项目为例先梳理所有 Controller 里的接口路径然后在网关或 Nginx 日志中统计这些路径的访问次数。下面是一个简单的 Shell 脚本示例用于从 Nginx access log 中按 URL 路径统计近 30 天的调用次数#!/bin/bash # 文件路径scripts/check_api_hits.sh LOG_FILE/var/log/nginx/access.log # 只统计 30 天内日志实际上需要按日期切割后的日志文件列表 zgrep -h GET /api/ /var/log/nginx/access.log*.gz | \ awk {print $7} | \ sort | uniq -c | sort -rn | \ awk $1 10 {print $2 调用次数:$1} /tmp/low_hit_api.txt echo 访问量低于10次的接口路径 cat /tmp/low_hit_api.txt这段脚本的逻辑是从 Nginx 日志含历史 gz 压缩日志中提取 URL 路径统计每个路径的出现次数筛选出出现次数低于 10 的接口。结果写入临时文件作为人工核查的候选清单。需要注意几点这里的$7对应 Nginx 默认 access log 格式中的 request 字段如果你们的日志格式有调整需要同步修改字段序号另外脚本只演示了思路生产环境中一般会使用日志系统或者专门的日志分析平台来处理直接跑 grep 在日志量很大的时候效率很低。3.2 用 Maven 分析无效依赖在项目根目录执行mvn dependency:analyze运行后Maven 会打印出两部分信息Used undeclared dependencies和Unused declared dependencies。Used undeclared dependencies代码里用了但 pom.xml 里没有直接声明说明依赖传递依赖了某个包。这类建议在 pom.xml 中显式声明方便版本管理。Unused declared dependenciespom.xml 里声明了但代码里没有直接引用。这类才进入“无用依赖”候选清单。不过这里要特别说明dependency:analyze是基于字节码扫描的对通过反射加载的类、SPI 方式加载的类、仅用于运行时动态代理的类可能会误报为 unused。所以分析结果只能作为参考删除依赖前一定要做全量编译 冒烟测试。3.3 用 IDEA 静态检查定位死代码在 IDEA 中可以选中模块根目录右键选择Analyze-Inspect Code...检查项选择Java-Declaration redundancy下的Unused declaration。IDEA 会给出未使用类/方法/字段的列表。建议按包名分组查看结果优先关注controller、service、util包下的类。但同样需要注意IDEA 静态检查判断的是“当前工程内未引用”如果某个类被 Spring 通过配置文件扫描、被 MyBatis 通过 Mapper 接口动态代理、被 Dubbo 通过 XML 引用IDEA 可能识别不出来。3.4 建立功能台账将所有候选功能记录到一张表中建议使用以下字段功能名称类型代码位置最近调用时间调用方负责人状态旧版订单详情接口HTTP 接口OrderDetailController.java2024-01-15无张三待确认老积分同步定时任务定时任务PointsSyncJob.java2024-02-01无李四待确认活动开关-双11配置项activity.double11.enabled2023-11-12无王五待确认台账的作用有两个一是防止重复排查二是作为后续清理操作的执行清单。没有台账就动手很容易漏掉某些功能或者删完才发现还有一个隐藏调用方。4. 实战第二步验证功能是否真的“没用”清单只是候选名单不能直接删。这一步的核心是把“看起来没用”升级成“确认没用”。我一般会采取四步验证法日志验证、调用链验证、开关验证、数据验证。4.1 日志验证确认调用量为 0前面的 Shell 脚本已经统计过接口调用量这里还要补两步第一步确认网关层覆盖了所有外部流量入口。很多系统除了 Nginx还有内部服务通过 Feign/HTTP 直接调用甚至 MQ 消息也会触发接口逻辑。所以还需要在业务日志中打印更细粒度的调用记录按方法维度统计。第二步确认是否存在“有调用但客户端错误地拿到缓存”的情况。比如某个查询接口被 Nginx proxy_cache 缓存了即使后端代码已经下线客户端也不会报错。要排除缓存干扰可以观察业务日志中是否有真实的方法进入记录而不只是看网关访问日志。下面是一段简单的方法调用统计代码借助 Spring Boot 的HandlerInterceptor实现接口级调用记录// 文件路径src/main/java/com/example/demo/interceptor/ApiUsageInterceptor.java Component public class ApiUsageInterceptor implements HandlerInterceptor { private static final Logger log LoggerFactory.getLogger(ApiUsageInterceptor.class); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri request.getRequestURI(); log.info([API_USAGE] uri{}, method{}, query{}, uri, request.getMethod(), request.getQueryString()); return true; } }然后在 WebMvcConfigurer 中注册拦截器只作用于需要统计的路径// 文件路径src/main/java/com/example/demo/config/WebMvcConfig.java Configuration public class WebMvcConfig implements WebMvcConfigurer { Resource private ApiUsageInterceptor apiUsageInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiUsageInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/monitor/**); } }把日志接入 Elasticsearch 或 Loki 之后按接口路径聚合查询就能得到精确的调用次数。注意这种统计需要观察一个完整周期建议至少观察 7 到 30 天避免出现“每周只跑一次的报表任务”被漏掉的情况。4.2 调用链验证找到真正的调用方如果系统已经接入 SkyWalking、Pinpoint 或 Zipkin 等调用链工具可以直接在调用链查询界面搜索该接口全链路信息查看 Trace 里是否存在上游服务节点。如果整个调用链中只有客户端 - 网关 - 本项目服务没有第二级调用那大概率可以认为只剩一个入口。如果还没有接入调用链可以通过并行日志的 traceId 串联。例如在 A 服务调用 B 服务时A 会在日志里打印完整 URL那么 grep B 服务的接口路径就能看到 A 服务的 IP再根据 IP 反查服务名。这样就能找到所有调用方。4.3 开关验证用“熔断式开关”灰度验证即使前面两步验证都通过也不能保证万无一失。最稳妥的方式是在要清理的功能入口加一个“手动开关”先把流量切到“拒绝”状态观察一段时间如果没有用户或调用方反馈异常再删除代码。下面是一个简单的开关实现示例。利用配置中心下发一个开关当开关开启时接口直接返回 503终止业务逻辑执行// 文件路径src/main/java/com/example/demo/controller/LegacyOrderController.java RestController RequestMapping(/api/legacy/order) public class LegacyOrderController { Value(${legacy.order.enabled:false}) private boolean legacyOrderEnabled; GetMapping(/detail) public ResponseEntityObject detail(RequestParam(orderId) String orderId) { if (!legacyOrderEnabled) { // 开关关闭说明该接口处于“禁用观察期” return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(Collections.singletonMap(message, 接口已下线请使用新接口 /api/v2/order/detail)); } // 原业务逻辑... return ResponseEntity.ok(legacy order detail); } }这里我用了Value从 Spring 配置文件中读取开关值配置如下# 文件路径src/main/resources/application.properties legacy.order.enabledfalse如果你们使用 Nacos 或 Apollo可以把该配置项放进配置中心动态修改。当legacy.order.enabledfalse时所有访问该接口的请求都会返回 503。这样一旦有漏掉的调用方会立刻在调用方侧暴露问题而不会静默出错。观察期建议持续 1 到 2 个完整业务周期确认没有异常反馈后再进入下一步删除操作。4.4 数据验证确认相关表不再写入如果该功能涉及数据库读写还要确认相关表在观察期内没有新的插入和更新操作。可以通过查询数据库的更新时间字段或者用 Binlog 工具进行监控。-- 以订单扩展表为例查询最近是否有数据变更 SELECT MAX(update_time) AS last_update_time, COUNT(*) AS total_count FROM legacy_order_ext WHERE update_time DATE_SUB(NOW(), INTERVAL 30 DAY);如果结果中last_update_time为空说明 30 天内没有任何数据更新表处于“静止状态”。但这里同样要排除定时任务每天 update 但结果不变的情况最好结合 Binlog 监控确认。5. 实战第三步安全删除无用功能确认一个功能真的没用之后删除也不是直接rm -rf而是要按照“分支备份 - 打标记 - 灰度发布 - 观察 - 清理记录”的流程执行。5.1 分支与备份策略在删除代码前先基于当前主干创建一条备份分支git checkout -b feature/remove-legacy-order -t origin/master然后把要删除的代码全部提交到这条分支上。推送远端git push origin feature/remove-legacy-order这样做的意义是即使删除后出了问题也能通过这条分支快速恢复。同时在项目文档或 MR 描述中把删除的功能清单、删除原因、开关验证结论都写清楚方便后续排查。对于数据库表不建议直接 DROP TABLE而是先重命名保留一段时间RENAME TABLE legacy_order_ext TO _del_legacy_order_ext_20250101;通过重命名可以让业务代码在未修改前直接报错从而快速暴露是否还有程序在访问。确认 1 个月后无异常再执行归档或删除。5.2 删除 Controller、Service 与相关代码如果确认过程中使用了开关禁用删除版代码就非常简单直接把 Controller、Service、Mapper 等相关类整体删除。以一个订单模块为例删除LegacyOrderController、LegacyOrderService、LegacyOrderServiceImpl、LegacyOrderMapper等类同时删除配置文件中的相关配置项以及 pom.xml 中不再使用的依赖。删除完代码后需要编译验证mvn clean compile如果编译通过再执行单元测试mvn test如果有集成测试也一并跑一遍。注意即使测试通过也不能保证静态引用的类没有通过反射访问删除的类所以发布前的 Code Review 和测试需要双管齐下。5.3 清理配置项配置项的删除有两种做法做法一对于已经确认没有代码引用的配置项可以先将配置项注释掉观察一段时间。# 已废弃等待观察期结束后删除 # legacy.order.enabledfalse做法二如果配置中心支持“配置项灰度发布 回滚”可以直接删除配置项并保留操作记录。无论哪种做法一个核心原则是配置项的删除必须和代码发布解耦。建议先发布代码代码中不再读取该配置项再删除配置项。如果顺序反了先删配置项但代码还在读取启动时可能因为缺少配置而报错。5.4 发布与验证删除代码完成后建议走灰度发布流程而不是全量发布。发布后重点观察两个指标服务启动是否成功日志中是否出现NoSuchBeanDefinitionException、ClassNotFoundException。错误率是否上升特别是删除的功能相关的错误日志是否出现。监控观察期建议至少 24 小时。如果发现问题立即回滚到上一个稳定版本然后在备份分支上排查是哪个调用方漏掉了。这里还要强调一点发布回滚并不可怕可怕的是没有回滚方案。所以删除前一定要记录上一个稳定版本的镜像 tag 或制品 ID。5.5 清理技术债记录确认删除后的功能稳定运行一段时间后把之前功能台账中对应的状态改为“已清理”并补上删除日期、发布版本号、回滚方案。这一步看似简单但非常重要。否则过几个月又会出现“这个功能之前好像删过”的疑问。6. 一个完整的清理案例旧版订单详情接口为了让你更直观地理解上面整套流程这里用一个简洁案例串起来。6.1 场景描述某电商系统存在一个/api/legacy/order/detail接口是早期版本提供的订单查询入口。新版本上线后前端已全部切换到/api/v2/order/detail但旧接口一直没有下线。现在需要确认旧接口能否清理。6.2 步骤一统计调用量通过 Nginx 日志脚本统计了最近 30 天访问量发现/api/legacy/order/detail访问次数为 0。为了排除缓存干扰在应用日志中搜索该接口路径对应的日志也没有发现任何业务日志。6.3 步骤二配置禁用开关在配置中心添加配置项legacy.order.enabledfalse代码中通过Value读取该开关。开关关闭后旧接口返回 503并附带提示信息。观察 7 天后无任何调用方反馈异常。6.4 步骤三删除代码从主干创建分支feature/remove-legacy-order删除以下文件LegacyOrderController.javaLegacyOrderService.javaLegacyOrderServiceImpl.javaLegacyOrderMapper.java相关 XML 文件import-legacy-order.sql历史测试脚本同时在配置中心删除legacy.order.enabled配置项。6.5 步骤四发布与回滚预案构建镜像 tag 为release_20250101_v2.10.3作为回滚基线。灰度发布 20% 流量观察 24 小时无异常后全量发布。发布后持续观察 1 周确认错误率无波动。6.6 步骤五更新台账将功能台账中旧版订单详情接口的状态改为“已清理”记录清理日期2025-01-02回滚基线release_20250101_v2.10.3。这个案例比较理想化实际清理时可能需要反复确认多次但整体思路是一样的。7. 常见问题与排查思路7.1 配置项删了服务启动失败问题现象常见原因解决思路启动报错Could not resolve placeholder legacy.order.enabled配置项被删除但代码中仍通过Value读取先恢复配置项检查代码中是否仍然存在引用接口返回 404前端或上游仍访问旧接口通过网关日志确认调用方联系对方切换新接口调用量为 0但服务内存报警死代码不是内存高的主因使用jstack排查线程使用MAT分析堆 dump删除依赖后运行时报ClassNotFoundException依赖被反射或 SPI 代码引用mvn analyze 未识别出回滚依赖添加显式依赖声明或添加持久化测试数据库重命名表后定时任务报错定时任务代码未同步修改回滚表名修改任务代码确认无引用再重试7.2 如何避免“删了又冒出来”这个问题很常见。你以为删干净了结果三个月后又有新需求基于旧逻辑重新实现了一遍甚至出现了新旧两套代码共存的局面。避免方法是做“代码归档索引”。删除代码后在文档中进行记录这个功能为什么删除实现思路在哪里可以找到对应 MR 链接是什么。后续新开发时可以通过搜索文档快速找到历史方案而不是重新造轮子。7.3 调用量为 0 但不敢删怎么办如果调用量为 0 但心里没底可以把步骤拆得更细先只删一半。例如先保留服务实现只删掉 Controller 层入口也就是让外部调用直接 404观察一段周期后再删除 Service 和 Mapper。如果外部确实没有调用方404 阶段就能暴露问题。7.4 如何应对“领导让保留”的情况有时候功能确实没用但业务方或领导出于各种原因要求保留。这时不要硬刚可以建议将功能从核心链路中摘除放入独立模块或独立服务并做好隔离。如果未来真的需要启用也不会影响主应用稳定性。这比直接删除更容易获得认可。8. 最佳实践与工程建议8.1 建立功能生命周期管理机制建议在项目立项或迭代时就为每个功能明确“负责人”和“退役条件”。功能上线时在技术文档中记录接入方功能下线时由负责人发起下线申请经过技术评审后执行。这套机制不一定需要专门工具一个共享文档、一张表格就可以启动。8.2 善用 Deprecated 与废弃标记如果暂时不能删除某些接口或方法至少要在代码中添加清晰的废弃标记并注明替代方案。/** * 该接口已废弃请使用 {link OrderDetailV2Controller#detail(String)}替代。 * 计划于 2025-06-01 删除如有疑问请联系订单组。 */ Deprecated GetMapping(/legacy/order/detail) public OrderDetail legacyDetail(RequestParam(orderId) String orderId) { return orderService.queryLegacyDetail(orderId); }有了废弃标记IDE 会在调用处给出提示后续删除时也更容易定位所有引用位置。8.3 配置开关不能只做“临时措施”很多团队在使用开关禁用无用功能后就把开关留在代码里结果变成了新的“无用配置”。建议给开关设置一个明确的过期时间并在开关代码中打印告警日志。# 示例带过期时间的配置描述 legacy: order: enabled: false deprecated: true delete-after: 2025-06-01同时在 logback 或日志框架中对deprecatedtrue的配置定期输出提醒日志直到删除为止。8.4 代码评审时增加“删除视角”Code Review 时除了关注新增代码也要关注是否有机会删除旧代码。例如当新接口完全替代旧接口时评审人应该主动追问“旧接口是否可以下线是否有调用方在继续使用”这个简单的提问可以避免大量无用功能的堆积。8.5 删除功能不等于删除数据删除代码功能时一定要区分代码和数据。代码可以立刻删除并回滚但数据需要保留更长时间。建议的数据保留策略是业务表重命名后保留至少一个完整年度再归档。日志表按日志保留策略自动过期一般 30 到 90 天。临时表确认无写入后立即清理。8.6 性能优化不是删除功能的唯一理由有些开发清理无用功能是为了性能。但实际上一个没人调用的接口并不会占用太多 CPU真正的性能瓶颈往往在数据库慢查询、锁竞争、循环调用等位置。所以不要用“性能优化”作为清理无用功能的唯一理由更合适的理由是“安全、可维护性、成本控制”。9. 总结与后续学习路线这篇内容从“没用的功能”这个概念出发梳理了无用功能产生的背景给出了从日志验证、调用链验证、开关验证到安全删除的完整实操路径。你可以从两个角度使用这套思路如果你是刚接手老项目可以用它快速梳理系统中可能存在风险的遗留功能如果你正在做系统重构可以用它作为删除代码阶段的标准化清单。接下来的学习方向我建议关注三个点一是深入学习调用链追踪工具的使用比如 SkyWalking 或 Zipkin 的接口依赖分析能力二是掌握配置中心动态配置与灰度发布机制这是安全删除功能的重要前提三是培养“代码是可替换的、数据是可恢复的”工程意识学会怎么走一条留好退路的变更路径。如果这篇文章对你有帮助可以收藏备用。下次再遇到“没用的功能”先别急着删先按这套流程走一遍你会发现很多坑都可以提前绕开。
返回列表