一个方法两个触发口:反编译 XXL-Job 后,我看清了 inner/job 子包的裸奔现状

老炮踩坑录 · 💎 老炮视野系列

基于真实企业项目实读:19 个 inner 类、3 个 job 触发口(@XxlJob 方法),逐类核对包名、URL 前缀、触发方式

关键词:inner 接口 · XXL-Job · URL 前缀 · 暴露面 · 租户上下文 · @GetMapping

🏠个人主页: 知守观

📘我的专栏: 老炮踩坑录

💻当前内容:内部接口与定时任务触发口的暴露面复盘

引言

上一篇写了这个供应链中台的分层,application 和 domain 怎么切。这篇往回收一点,只看 controller 目录里的两个子包:inner/ 和 job/。

项目开发文档里写了这两个包是干什么的,两条约定,两行字。我把三个业务模块的 78 个 controller 翻了一遍,逐类对了包名、URL 前缀和触发方式。对完发现,目录约定和实际暴露面是两套东西,而且对不上。

统计口径是主代码,src/test 不算。

如果只记三件事:

① inner/ 和 job/ 子包管的是开发者找代码,管不了暴露面。项目约定用 unit-deploy.prefix-inner 这个 URL 前缀区分内外------但网关是否真按它隔离,仓库里看不到,文末「三点没确认」有交代;

② 采购模块根目录下的 Inner 类,前缀倒是用对了;主数据 job/ 包里的类,反而挂在没有任何内部前缀的路径下,还留着两个必抛异常的测试接口;

③ 仓配 4 个任务方法同时标了 @XxlJob 和 @GetMapping,调度中心能调,HTTP GET 也能调,全租户的事件重试和历史数据清理都在里面。

文档写了两条约定,三个模块执行出三种样子

项目开发文档里的原文,两条:

web/controller/inner/:内部服务间 API 端点,不对外暴露 web/controller/job/:XXL-Job 触发端点

实扫出来的目录是这样的。

inner/ 子包一共 19 个类:主数据 12 个,仓配 5 个,采购 2 个。采购另外还有 1 个 PurchaseInnerController,直接放在 controller 根目录,没进 inner/。

job/ 子包只有主数据在用,1 个类。仓配的 JobController 放在 inner/ 里,采购的 PurchaseJobController 放在根目录。三个模块,三个位置。

inner/、job/ 加上两个放错在根目录的类,一共 22 个,占 78 个 controller 的近三成。文档写的是 A,代码里跑的是 B。看目录找接口,基本会找错。

决定暴露面的是 URL 前缀,包名对不上

先说这个项目的实际机制。接口内外不由包名决定,由 URL 前缀配置项决定:unit-deploy.prefix-inner 是内部前缀,部署和网关按约定用它做隔离;unit-deploy.prefix-main 是业务前缀。包名 inner/ 只是给开发者看的提示。

提示和配置对齐也就罢了。实际五种写法:

java 复制代码
// 主数据 inner/ 下的 SupplierInnerController:内部前缀,路径里再带一层 inner
@RequestMapping("${unit-deploy.prefix-inner:}/masterdata/inner/supplier")

// 采购根目录下的 PurchaseInnerController:不在 inner/ 包,前缀倒是用对了
@RequestMapping("${unit-deploy.prefix-inner:}/purchase/api")

// 仓配 inner/ 下的 JobController:main 前缀后面拼一段 "-inner"
@RequestMapping("${unit-deploy.prefix-main:}-inner/job/api")

// 采购根目录下的 PurchaseJobController:main 前缀,路径里带 /job/
@RequestMapping("${unit-deploy.prefix-main:}/job/purchaseJob")

// 主数据 job/ 下的 MasterDataJobController:什么前缀都没有
@RequestMapping("/masterDataJob")

同一个"内部接口"语义,五种路径表达。

采购那个类,位置不守约定,前缀守了。主数据那个 job 类,位置守了约定,前缀没了。按目录判断内外,这两个判断都会错:前者你会当成对外接口,后者你会当成内部接口。

仓配那种 prefix-main 拼 -inner 的写法,还多一个问题:隔离依赖的是字符串拼接的结果,配置项本身还是 main。网关如果按配置项做路由规则,这条路径归在哪一类,得看网关怎么配的。配置不在仓库里,我说不准,放在后面「三点没确认」。

对照档案服务那套做法。它的 controller 按访问来源分包:external、externalno(外部免鉴权)、internal、self、store,谁要鉴权、谁免鉴权、谁只给内网,目录结构直接对应策略。中台这套,目录只是提示,策略在配置里,配置本身还没写统一。两种管理思路,落地差别就在这。

一个方法两个触发口,job 类里还留着测试接口

仓配的 JobController,301 行。里面 4 个任务方法,全是双注解:

java 复制代码
@XxlJob("retryTask")
@GetMapping("/retryTask")
@Operation(summary = "单据事件重试", description = "单据事件重试任务")
public Boolean retryTask() {
    SpringUtil.getAllTenantIdFromConfig().forEach(tenantId -> {
        TenantContext.clear();
        TenantContext.set(tenantId);
        // 查最近一天、重试次数少于 3 次的异常事件,逐个重新执行
        ...
    });
    return true;
}

@XxlJob 注册到调度中心,@GetMapping 注册到 HTTP。同一个入口,两个触发方。调度中心能调;只要这个路径对外可达,发一个 GET 也能触发。

清理历史数据那个方法,同样是 GET:

java 复制代码
@XxlJob("delHistoryLog")
@GetMapping("/delHistoryLog")
public Boolean delHistoryLog(@RequestParam("beforeDate") String beforeDate) {

注解上 beforeDate 是必填,不传直接 400;要落到「默认清三个月」这个行为,签名得写成 required = false 并带默认值。方法体是全租户循环,挨个 TenantContext.set,然后 lambdaUpdate().remove() 删除------实体上有没有挂 @TableLogic 我没核对,物理删还是逻辑删以实体为准。往这个 URL 发一个 GET,这批清理就跑一遍。类上没有类级或方法级鉴权注解;Filter、网关都不在类上留痕,拦不拦我没有证据。

主数据那边更随意。job/ 包里那个 201 行的类,末尾挂着两个测试方法:

java 复制代码
@Operation(summary = "测试方法")
@GetMapping("/testDetail")
public Result<SupplierDTO> testDetail() {
    BizExceptions.throwWithErrorCode(ErrorCodeEnum.SUPPLIER_ERROR, new String[]{"0001"});
    return Results.ofSuccess();
}

方法体第一行就抛异常,return 永远走不到。另一个 testPurchase 一模一样,换了个错误码。两个方法挂在 /masterDataJob 下面,这个路径前面说过,没有任何内部前缀。

这种测试代码留在生产环境,本身就是问题;网关到底拦没拦,我没有证据。

同一个类里,风格也不统一。前三个任务方法只有 @XxlJob,没有 HTTP 映射,但方法签名还带着 @RequestParam:

java 复制代码
@XxlJob("execContractJob")
public void execContractJob(@RequestParam("tenantIds") String tenantIds,
                            @RequestParam("contractNo") String contractNo) {

没有 HTTP 映射的方法上写 @RequestParam,这个注解是失效的:Spring 不会处理它,XXL-Job 也不按名字注入。反编译 2.3.1 的 MethodJobHandler 可以确认,它只给带参方法传一个等长的 null 数组,参数值仍由方法体里的 XxlJobHelper.getJobParam() 兜底。

所以这类注解不影响调度执行,但它会让人误以为这里有 HTTP 入口------采购那个 job 类是真正的双入口(@XxlJob + @PostMapping),两种写法混在一起,光看签名分不出来。

job 类里的手工租户上下文

定时任务没有登录用户,租户上下文得自己塞。仓配的做法,是每个方法开头手写四行:

java 复制代码
TenantContext.clear();
TenantContext.set(tenantId);
TraceIds.clear();
TraceIds.setOrRandom(null);

301 行的类里,这段重复 4 次。全租户循环也手写:SpringUtil.getAllTenantIdFromConfig().forEach(...)。

主数据没用这个工具方法,自己从框架的分片配置里解析:

java 复制代码
private List<String> getAllTenantIdFromConfig() {
    Collection<List<String>> tenantList = Optional.ofNullable(context.getBean(ShardingProperties.class))
            .map(ShardingProperties::getDataSourceRouter)
            .map(ShardingProperties.DataSourceRouter::getConfig)
            .map(Map::values)
            .orElseThrow(() -> new RuntimeException("未查询到多租户配置信息"));
    return tenantList.stream().flatMap(Collection::stream).collect(Collectors.toList());
}

采购的 job 类自己写了同样一段解析,方法体我核过,和主数据一字不差。仓配走工具方法,主数据自己写解析,同一个项目里两种获取方式并存。工具方法就在公共包里,主数据这段属于是没看见,或者看见了没改。

set 漏一次,任务跑在错误租户上;clear 漏一次,带上一个租户的残留。四行代码,全靠人记。一个基类或者一个切面就能收走的事,没人做。

还有分层。仓配这个 JobController 直接注入了 repository 和 domain service,门店过滤、单据分组、Redis 缓存,全写在 controller 方法里。上一篇说应用层偶尔越层,inner 和 job 这层是直接把 application 整个跳过去。分层约定管不到这两个子包。

接口不止一套。主数据的内部接口分了新老两代:SupplierInnerController 旁边放着 SupplierInnerOldController,DTO 也分成 SupplierDTO 和 SupplierOldDTO;仓配根目录有 BillEventController,inner/ 下有 BillEventInnerController,采购 inner/ 下也有一个 BillEventInnerController。同一类接口或事件有两个入口,想拼出完整的路由表,得把根目录和 inner/ 翻两遍。

对照表

维度 主数据模块 采购模块 仓配模块
inner 类数量与位置 inner/ 12 个 inner/ 2 个 + 根目录 1 个 inner/ 5 个
job 触发口位置 job/ 1 个 根目录 1 个 inner/ 里 1 个
inner 接口路径特征 prefix-inner + 路径再带 /inner/ prefix-inner,路径无 inner 事件口在 inner/,导出类不带 Inner 命名
job 接口 URL 前缀 无前缀,/masterDataJob prefix-main + /job/ prefix-main 拼 -inner
任务触发方式 3 个 @XxlJob + 2 个仅 @GetMapping的测试方法 4 个 @XxlJob,全部另带 @PostMapping 4 个方法 @XxlJob + @GetMapping 双注解
租户列表获取 自己解析分片配置 自己解析同一个分片配置 公共工具方法
额外发现 2 个必抛异常的测试接口、Old 后缀接口 Inner 类位置不守约定但前缀用对 301 行,直注 repository/domain

三点没确认:

一是 prefix-inner 的网关隔离配置不在仓库里,内部前缀实际怎么生效、仓配那种 main 拼 -inner 的路径归在哪类路由,我没有证据;

二是 inner 接口是否完全无鉴权,我只看到类上没有类级或方法级鉴权注解,Filter 和网关侧有没有拦截没抓到;

三是 SupplierInnerOldController 在整个仓库里没有调用方,仓外是否还在调,现状未知。

自查打卡

inner/ 目录类数:

bash 复制代码
find . -path '*/web/controller/inner/*' -name '*.java' | wc -l

job 触发口的实际位置,不限于 job/ 子包:

bash 复制代码
grep -rl '@XxlJob' --include='*Controller.java' .

URL 前缀写法分布:

bash 复制代码
grep -rh '@RequestMapping' --include='*Controller.java' . | sort | uniq -c | sort -rn

双注解方法,@XxlJob 下一行就是 @GetMapping:

bash 复制代码
grep -rn -A1 '@XxlJob' --include='*.java' . | grep '@GetMapping'

老炮点评

三件事。

第一,暴露面清单从 URL 前缀和网关配置生成,别从目录读。grep @RequestMapping 全量导一遍,按前缀分组,对外、对内、没标记三类直接分出来。目录约定可以作为辅助,不能作为依据。

第二,定时任务的租户上下文和 TraceId,收进基类或者切面。现在这种每个方法手写四行的做法,漏一处就是一次串租户。

第三,测试接口别留在生产代码里。要留就加 @Profile 限定环境,要么删。挂在无内部前缀路径下的测试方法,等于给暴露面清单添了两条没人认领的记录。

问你一句

你们项目的内部接口清单,是从目录里数出来的,还是从网关配置里拉出来的?两个来源对过没有?

评论区聊聊。


如果这篇对你有帮助,欢迎: 👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言 我是老炮,18 年 Java 老兵,仍在一线。🔥关注「Java老炮踩坑录」,真实企业项目复盘,不错过每一篇真实案例,少踩坑。

下期预告

《controller 五分包的暴露面管理:external/externalno/internal/self/store》

中台这边,目录只是提示,策略在配置里。档案服务那边反过来,谁要鉴权、谁免鉴权、谁只给内网,目录结构直接对应安全策略。同一套暴露面问题,另一种解法,下期对照着看。

相关推荐
用户31268748772012 分钟前
CompletableFuture 链式编排:异步编程的优雅解法
java
瑞雪兆丰年兮28 分钟前
JavaWeb-day14-SpringBootWeb原理篇
java·开发语言·spring boot·spring·bean·mybatis·javaweb
神仙别闹41 分钟前
基于 Java GUI 贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏
java·毕业设计·gui·贪心策略·极大极小博弈树
Flynn Liang2 小时前
RAG 评估体系:黄金集、检索指标与 LLM-as-Judge 落地
java·langchain·rag·评估·langchain4j·检索指标
SimonKing3 小时前
Workbuddy太占C盘空间了,写个迁移工具迁走10G的数据
java·后端·程序员
Hvitur3 小时前
Java - Lambda
java·开发语言·python
杨凯凡5 小时前
Spring Boot 大文件下载如何实现断点续传?HTTP Range、206 与 416 实战
spring boot·后端·http
apcipot_rain8 小时前
C++转Java日记(06)
java·开发语言