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} 一起扫进来。
*
*
scanBasePackages 为什么是这一长串
*
* 它是**老工程 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」。
*
* 逐项来源:
*
* - {@code com.zteits.{park,order,pay,bill,charge,business,sys}} —— 6 个业务模块 + 系统域,
* 7 个启动类的共同项
* - {@code com.zteits.wxcp} —— 老 {@code zteits-wxcp-portal}(独立 JVM,端口 18031)
* 的「企业微信服务子系统」按文件随迁进 {@code parking-module-system} 后留下的包,
* 清单见 {@code tools/p2_import_legacy.py} 的 {@code EXPLICIT_SOURCES}
* - {@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 搬入真实实现时无需再动扫描配置
* - {@code com.zteits.ipay}、{@code com.zteits.configuration} —— 只有
* {@code OrderApplication} 扫(前者的支付宝/微信/建行支付实现,后者的通用配置)
* - {@code com.clouds.common.redis} —— 7 个启动类都扫({@code RedisCacheUtil} 是
* {@code AppTokenUtil.getBean("redisCacheUtil")} 按名取的,必须注册)
* - {@code com.clouds.common.threadpool} —— 只有 park / order 扫
* - {@code com.clouds.common.web} —— 只有 order / pay 扫({@code SpringContextUtil}
* 要做 Bean 才能被 {@code AppTokenUtil} 取到 ApplicationContext)
* - {@code com.clouds.common.oss} —— 只有 pay 扫({@code OssClientService} 三个包装类)
*
*
* 三个「故意没扫」的包
*
*
* - {@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} 照样能装配上)。
* 唯一带 {@code @Component} 的是 {@code aop.DaoLogAspect},老工程从未扫它,
* 这里保持一致 —— 多扫会凭空多一层 DAO 日志 AOP。
* - {@code com.zteits.clouds.api} —— 纯契约(接口 + DTO),无实现类,没有 Bean。
*
*
* 扫描顺序:{@code com.clouds.common.*} 为什么必须写在最前面
*
* Spring 按 {@code basePackages} 的书写顺序扫描,扫描顺序 = BeanDefinition 注册顺序,
* 也就是(无依赖关系时的)Bean 创建顺序。老框架里有一批 CacheUtil 把
* {@code RedisCacheUtil} **缓存进了静态字段**:
*
*
* private static RedisCacheUtil redisCacheUtil = RedisCacheFactory.getRedisCacheUtil(); // 41 个类这么写
*
*
* 而 {@code RedisCacheFactory} 的静态字段是在它自己的
* {@code setApplicationContext} 回调里才赋值的。静态字段只在**类加载时**求值一次,
* 所以只要有一个 {@code com.zteits.*} 的 Bean 在它之前被创建、且创建期间就类加载了这些
* CacheUtil 类,那个静态字段就被**永久钉死为 null**,之后每次调用都 NPE。
* 老代码里确实有这种写法:{@code TdCAcctPersonServiceImpl} 的 {@code @PostConstruct}
* 直接起后台线程,线程第一件事就是查字典(走 CacheUtil)。
*
* 把 {@code com.clouds.common.redis} 排到最前,{@code RedisCacheFactory} 就会在任何
* {@code com.zteits.*} Bean 之前建好,上面那条竞态窗口随之关闭。**这是启动能否成功的必要条件,
* 不要因为「看起来只是顺序」而把它调回去**。老工程 7 个 JVM 各自的扫描列表里
* {@code com.clouds.common.redis} 也都排在业务包之前,合并后必须保持。
*
* ⚠️⚠️ 2026-10-15 实测补正:上句的「任何 {@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.*} 挪到**最前**,连这两个新工程包一起让位
* ——这本来就是本节结论的字面要求。
*
* @MapperScan 为什么是两条
*
* 两条各自管一段,互不重叠(靠 {@code annotationClass} 区分):
*
*
* - 新代码:{@code com.zteits.parking} 下**带 {@code @Mapper} 注解**的 Mapper
* —— 其实这条已经由 {@code ParkingMybatisAutoConfiguration} 的
* {@code @AutoConfiguration + @MapperScan} 提供,这里不重复声明。
* - 存量:{@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 个包下。
*
*
* ⚠️ 两处红线:
*
* - **绝不能**扫 {@code com.zteits.*.util}:里面有 tk.mybatis 的 {@code MyMapper}
* 标记接口,老代码自己的注释就写着「该接口不能被扫描到,否则会出错」。
* - {@code nameGenerator = LegacyMapperBeanNameGenerator.class} **不能省**:
* 存量有 9 对不同包同名的 Mapper(order↔pay 7 对、pay↔business 2 对),
* 默认的简单类名命名会让 Spring 抛 {@code ConflictingBeanDefinitionException}。
* 详见该类的注释。
*
*
* 为什么要排除 {@code com.zteits.charge.config.ChargeConfig}
*
* 它是**本次搬迁里唯一一个来自封闭 Jar 的冲突**:依附在 {@code parking-legacy-common}
* 上的 {@code zteits-parking-charge-spring-boot-starter}(源码不可改)自己把自己撞了:
*
*
* @Configuration
* @ComponentScan("com.zteits.charge") // ← 与我们的扫描重复
* public class ChargeConfig {
* @Bean(name = "chargeService") // ← 与下面那个 @Component 同 bean 名
* public ChargeService chargeService() { return new ChargeService(); }
* }
* @Component public class ChargeService {} // 同一个 jar 里
*
*
* 老工程能跑,只因 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}。两扇门都要关:
*
* - {@code @EnableAutoConfiguration(exclude = ...)} —— 关掉 spring.factories 那扇门
* (也可以用配置 {@code spring.autoconfigure.exclude},但那样就成了「同一件事写在两个
* 文件里」,且丢掉编译期校验,故不采用)
* - {@code @ComponentScan(excludeFilters = ...)} —— 关掉包扫描那扇门
*
*
* 排除它**不损失任何能力**:{@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);
}
}