ParkingServerApplication.java
16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package com.zteits.parking.server;
import com.clouds.common.mybatis.LegacyMapperBeanNameGenerator;
import com.zteits.charge.config.ChargeConfig;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 项目的启动类
*
* ⚠️ 注意:不要扫描 {@code com.zteits.parking.framework} 包!
* 框架层(parking-framework)的组件全部通过 Spring Boot 的
* {@code META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports} 注册为
* {@code @AutoConfiguration} 类,如果这里再直接扫描该包,会导致它们被重复注册。
* 同理也不要图省事扫 {@code com.zteits}:那会把 {@code com.zteits.parking.framework} 一起扫进来。
*
* <h2>scanBasePackages 为什么是这一长串</h2>
*
* 它是**老工程 9 个独立 JVM 各自 {@code @ComponentScan} 的并集**(前 7 个逐个抄自
* {@code ParkApplication} / {@code OrderApplication} / {@code PayApplication} /
* {@code BillApplication} / {@code ChargeApplication} / {@code BusinessApplication} /
* {@code com.zteits.sys.Application},第 8 个是随迁的企业微信服务子系统、
* 第 9 个是只有空实现的设备采集,均见下)。
* 合并单 Jar 后这些包必须一起扫,否则任何一个
* {@code @Component}/{@code @Service} 都不会注册,运行起来是「启动成功、调用报无此 Bean」。
*
* 逐项来源:
* <ul>
* <li>{@code com.zteits.{park,order,pay,bill,charge,business,sys}} —— 6 个业务模块 + 系统域,
* 7 个启动类的共同项</li>
* <li>{@code com.zteits.wxcp} —— 老 {@code zteits-wxcp-portal}(独立 JVM,端口 18031)
* 的「企业微信服务子系统」按文件随迁进 {@code parking-module-system} 后留下的包,
* 清单见 {@code tools/p2_import_legacy.py} 的 {@code EXPLICIT_SOURCES}</li>
* <li>{@code com.zteits.glockcollect} —— 老 {@code zteits-glock-collection}(独立 JVM,
* 德飞地锁 TCP 网关)的包。该模块本轮**未随迁**(netty 5.0.0.Alpha2 与 Redisson 的
* netty 4.1.x 冲突),{@code parking-module-datacollect} 里只放了一个空实现,
* 用来满足 order 两处裸 {@code @Autowired DfGroundLockMsgPushService};
* 包名沿用老包名,P5 搬入真实实现时无需再动扫描配置</li>
* <li>{@code com.zteits.ipay}、{@code com.zteits.configuration} —— 只有
* {@code OrderApplication} 扫(前者的支付宝/微信/建行支付实现,后者的通用配置)</li>
* <li>{@code com.clouds.common.redis} —— 7 个启动类都扫({@code RedisCacheUtil} 是
* {@code AppTokenUtil.getBean("redisCacheUtil")} 按名取的,必须注册)</li>
* <li>{@code com.clouds.common.threadpool} —— 只有 park / order 扫</li>
* <li>{@code com.clouds.common.web} —— 只有 order / pay 扫({@code SpringContextUtil}
* 要做 Bean 才能被 {@code AppTokenUtil} 取到 ApplicationContext)</li>
* <li>{@code com.clouds.common.oss} —— 只有 pay 扫({@code OssClientService} 三个包装类)</li>
* </ul>
*
* <h2>三个「故意没扫」的包</h2>
*
* <ul>
* <li>{@code com.clouds.common.cache} / {@code aop} / {@code log} / {@code config}
* —— 老工程 7 个启动类本来就没扫。已逐个核对:{@code cache} 下全是静态工具类、
* {@code log.LogSelf} 是纯工具,都不是 Bean({@code ICache} 的实现散在
* {@code com.zteits.{park,sys,pay}.cache} 里,那些包**在**扫描列表内,所以
* {@code RefreshCache} 注入的 {@code Map<String,ICache>} 照样能装配上)。
* 唯一带 {@code @Component} 的是 {@code aop.DaoLogAspect},老工程从未扫它,
* 这里保持一致 —— 多扫会凭空多一层 DAO 日志 AOP。</li>
* <li>{@code com.zteits.clouds.api} —— 纯契约(接口 + DTO),无实现类,没有 Bean。</li>
* </ul>
*
* <h2>扫描顺序:{@code com.clouds.common.*} 为什么必须写在最前面</h2>
*
* Spring 按 {@code basePackages} 的书写顺序扫描,扫描顺序 = BeanDefinition 注册顺序,
* 也就是(无依赖关系时的)Bean 创建顺序。老框架里有一批 CacheUtil 把
* {@code RedisCacheUtil} **缓存进了静态字段**:
*
* <pre>
* private static RedisCacheUtil redisCacheUtil = RedisCacheFactory.getRedisCacheUtil(); // 41 个类这么写
* </pre>
*
* 而 {@code RedisCacheFactory} 的静态字段是在它自己的
* {@code setApplicationContext} 回调里才赋值的。静态字段只在**类加载时**求值一次,
* 所以只要有一个 {@code com.zteits.*} 的 Bean 在它之前被创建、且创建期间就类加载了这些
* CacheUtil 类,那个静态字段就被**永久钉死为 null**,之后每次调用都 NPE。
* 老代码里确实有这种写法:{@code TdCAcctPersonServiceImpl} 的 {@code @PostConstruct}
* 直接起后台线程,线程第一件事就是查字典(走 CacheUtil)。
*
* <p>把 {@code com.clouds.common.redis} 排到最前,{@code RedisCacheFactory} 就会在任何
* {@code com.zteits.*} Bean 之前建好,上面那条竞态窗口随之关闭。**这是启动能否成功的必要条件,
* 不要因为「看起来只是顺序」而把它调回去**。老工程 7 个 JVM 各自的扫描列表里
* {@code com.clouds.common.redis} 也都排在业务包之前,合并后必须保持。</p>
*
* <p>⚠️⚠️ <b>2026-10-15 实测补正</b>:上句的「任何 {@code com.zteits.*} Bean」**包括新工程自己的
* {@code com.zteits.parking.server} / {@code com.zteits.parking.module}**。这两个包原先写在
* 4 个 {@code com.clouds.common.*} **之前**,恰好与本节结论相抵——而且**靠服务层依赖就足以踩中**:
* 控制器是用 {@code @Resource} 把服务层「拽」进创建流程的(服务层 BeanDefinition 注册在再后面也没用),
* 所以只要新栈有一个控制器的传递性依赖能触达带 {@code @PostConstruct} 起线程的
* {@code TdCAcctPersonServiceImpl},整条链就会先于 {@code RedisCacheFactory} 建好。
* 之前 46 个已迁控制器之所以没炸,只是**没有一个能触达它**;批 6 第十五切片引进的 4 个统计控制器
* ({@code EmployeeStatisticController} → {@code TpEEmployeeChargeService}、
* {@code UrbanIncomeStatisticController} → {@code PayOrderQueryService},
* 两者都再注入 {@code TdCAcctPersonService})是**第一批触达它的**,于是启动**确定性失败**
* (两次复现,故障链与上文一字不差:{@code Thread-2} 在 {@code TdCAcctPersonServiceImpl.java:771}
* NPE → {@code aliOssClientConfig} NPE → 启动中止)。
* 修法就是把 4 个 {@code com.clouds.common.*} 挪到**最前**,连这两个新工程包一起让位
* ——这本来就是本节结论的字面要求。</p>
*
* <h2>@MapperScan 为什么是两条</h2>
*
* 两条各自管一段,互不重叠(靠 {@code annotationClass} 区分):
*
* <ol>
* <li>新代码:{@code com.zteits.parking} 下**带 {@code @Mapper} 注解**的 Mapper
* —— 其实这条已经由 {@code ParkingMybatisAutoConfiguration} 的
* {@code @AutoConfiguration + @MapperScan} 提供,这里不重复声明。</li>
* <li>存量:{@code com.zteits.{bill,business,charge,order,park,pay,sys}.mapper}
* —— 老 Mapper 接口**没有 {@code @Mapper} 注解**,所以必须靠第 2 条
* 「按包扫、不限注解」收进来。这 7 个包名来自 `tools/p2_audit_mapper_ns.py`
* 对全部 651 个存量 mapper XML 的 namespace 审计:**每一个**都落在这 7 个包下。</li>
* </ol>
*
* ⚠️ 两处红线:
* <ul>
* <li>**绝不能**扫 {@code com.zteits.*.util}:里面有 tk.mybatis 的 {@code MyMapper<T>}
* 标记接口,老代码自己的注释就写着「该接口不能被扫描到,否则会出错」。</li>
* <li>{@code nameGenerator = LegacyMapperBeanNameGenerator.class} **不能省**:
* 存量有 9 对不同包同名的 Mapper(order↔pay 7 对、pay↔business 2 对),
* 默认的简单类名命名会让 Spring 抛 {@code ConflictingBeanDefinitionException}。
* 详见该类的注释。</li>
* </ul>
*
* <h2>为什么要排除 {@code com.zteits.charge.config.ChargeConfig}</h2>
*
* 它是**本次搬迁里唯一一个来自封闭 Jar 的冲突**:依附在 {@code parking-legacy-common}
* 上的 {@code zteits-parking-charge-spring-boot-starter}(源码不可改)自己把自己撞了:
*
* <pre>
* @Configuration
* @ComponentScan("com.zteits.charge") // ← 与我们的扫描重复
* public class ChargeConfig {
* @Bean(name = "chargeService") // ← 与下面那个 @Component 同 bean 名
* public ChargeService chargeService() { return new ChargeService(); }
* }
* @Component public class ChargeService {} // 同一个 jar 里
* </pre>
*
* 老工程能跑,只因 Spring Boot 1.5 的 {@code spring.main.allow-bean-definition-overriding}
* 默认是 {@code true},后注册的 {@code @Bean} 把先扫到的 {@code @Component} 悄悄顶掉了;
* Boot 2.1 起默认 {@code false},合并单 Jar 后启动即抛
* {@code BeanDefinitionOverrideException}(报错原文见 AGENTS.md 坑 13)。
*
* ⚠️ **只关一扇门没用,实测过**:只配 {@code spring.autoconfigure.exclude} 时,
* {@code ChargeConfig} 仍会被 {@code @ComponentScan} 当成 {@code @Configuration}
* 扫进来(它在条件报告的 Exclusions 段里出现了两次,错误照旧)——因为
* {@code @Configuration} 本身就是 {@code @Component}。两扇门都要关:
* <ol>
* <li>{@code @EnableAutoConfiguration(exclude = ...)} —— 关掉 spring.factories 那扇门
* (也可以用配置 {@code spring.autoconfigure.exclude},但那样就成了「同一件事写在两个
* 文件里」,且丢掉编译期校验,故不采用)</li>
* <li>{@code @ComponentScan(excludeFilters = ...)} —— 关掉包扫描那扇门</li>
* </ol>
*
* 排除它**不损失任何能力**:{@code @Bean chargeService()} 的实现就是
* {@code new ChargeService()},而 {@code ChargeService} 本身带 {@code @Component},
* 本来就会被 {@code com.zteits.charge} 这条扫描注册成**同名**的 {@code chargeService}
* (那 5 个 {@code @Resource} 字段由 {@code AutowiredAnnotationBeanPostProcessor}
* 注入,两条路得到的实例完全等价);{@code ChargeConfig} 自己的
* {@code @ComponentScan("com.zteits.charge")} 又与我们那条扫描重合。
* 新老两个仓库的 .java/.xml 全 grep 过 {@code ChargeConfig}:**零引用**。
*
* 为什么不用 {@code spring.main.allow-bean-definition-overriding: true} 一了百了:
* 那个开关连「两个**不同类**撞名」也会一起放行,等于把本次重构最该发现的错误静默掉
* (源码侧那 9 组真冲突正是靠启动报错逐一揪出来、再补上显式 bean 名修掉的)。
* 这里是封闭 jar 内部的自我重复,**精确排除 > 全局放水**。
*/
@EnableAsync
@EnableScheduling
// 这三条就是 @SpringBootApplication 的全部内容,展开只是为了能挂 excludeFilters
// (@SpringBootApplication 自带的扫描注释不接受过滤器)。语义完全等价:
// @SpringBootConfiguration == @Configuration + 一个供测试识别的标记
// @EnableAutoConfiguration == 开启自动配置
// @ComponentScan == 包扫描(原来的 scanBasePackages 挪到 basePackages)
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = ChargeConfig.class)
@ComponentScan(
basePackages = {
// ⚠️⚠️ 这 4 个 com.clouds.common.* 必须**排在最前**,即排在**所有** com.zteits.* 之前,
// 这里面**包括紧随其后的新工程两个包** `${parking.info.base-package}.server` /
// `.module`(它们也都是 com.zteits.*,2026-10-15 批 6 第十五切片实测踩过,见类注释
// 「扫描顺序」一节的实测补正)。这不是风格问题,是**启动能不能过**的问题:
// Spring 按 basePackages 的书写顺序扫描 → 该顺序即 BeanDefinition 注册顺序
// → 也就是无依赖关系时的 Bean 创建顺序。而老框架里有 41 个 CacheUtil 类把
// RedisCacheUtil **存进了静态字段**(`private static RedisCacheUtil x =
// RedisCacheFactory.getRedisCacheUtil();`),RedisCacheFactory 那个静态字段
// 又是在它自己的 setApplicationContext 里才赋值的。只要有任何一个
// com.zteits.* 的 Bean 在创建期间就类加载了这些 CacheUtil(老代码里有
// @PostConstruct 直接起后台线程、线程里立刻查字典的写法),静态字段就被永久
// 钉死成 null,之后每次调用都 NPE。
// ⚠️ 实测故障链:TdCAcctPersonServiceImpl(@PostConstruct 起线程) 先 NPE,
// 紧接着 AliOssClientConfig.afterPropertiesSet(建 Bean 时读 OSS 字典)再 NPE,
// 启动直接失败。两类都只靠「RedisCacheFactory 更早建好」才能避免。
"com.clouds.common.redis",
"com.clouds.common.threadpool",
"com.clouds.common.web",
"com.clouds.common.oss",
// ↓↓↓ 新工程自身的两个包:**必须让位于上面的 com.clouds.common.***(理由同上)↓↓↓
"${parking.info.base-package}.server",
"${parking.info.base-package}.module",
// ↓↓↓ 存量 7 个 JVM 的 @ComponentScan 并集(详见类注释)↓↓↓
"com.zteits.park",
"com.zteits.order",
"com.zteits.pay",
"com.zteits.bill",
"com.zteits.charge",
"com.zteits.business",
"com.zteits.sys",
"com.zteits.ipay",
"com.zteits.configuration",
// 第 8 个 JVM:企业微信(老 zteits-wxcp-portal,端口 18031)。
// 它整体不搬(门户 Web 层留在老工程),只有「企业微信服务子系统」
// 按文件随迁进了 parking-module-system,包名沿用 com.zteits.wxcp,
// 清单见 tools/p2_import_legacy.py 的 EXPLICIT_SOURCES。
// 必须在这里扫,否则 SendMessageServiceImpl(@Service)不注册,
// 4 处裸 @Autowired SendMessageService 的调用方启动即失败。
"com.zteits.wxcp",
// 第 9 个 JVM:设备采集(老 zteits-glock-collection,德飞地锁 TCP 网关)。
// 真实实现(netty 5.0.0.Alpha2)本轮不随迁,parking-module-datacollect 里
// 只有一个空实现 NoopDfGroundLockMsgPushService,用来满足 order 的两处
// 裸 @Autowired DfGroundLockMsgPushService。包名沿用 com.zteits.glockcollect,
// 这样 P5 搬入真实实现时无需再动这里。详见该类注释。
"com.zteits.glockcollect"
},
// 封闭 jar 里那个自我冲突的 @Configuration,理由见类注释。
// 用 ASSIGNABLE_TYPE 而非 REGEX:编译期就能校验这个类还在,jar 换版本时会直接编译失败
// (老工程的 portal 启动类排除 WebMvcConfigurer 用的也是 ASSIGNABLE_TYPE,此处沿用同一写法)。
excludeFilters = @ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = ChargeConfig.class))
@MapperScan(basePackages = {
"com.zteits.park.mapper",
"com.zteits.order.mapper",
"com.zteits.pay.mapper",
"com.zteits.bill.mapper",
"com.zteits.charge.mapper",
"com.zteits.business.mapper",
"com.zteits.sys.mapper"
}, nameGenerator = LegacyMapperBeanNameGenerator.class)
public class ParkingServerApplication {
public static void main(String[] args) {
SpringApplication.run(ParkingServerApplication.class, args);
}
}