上一篇算过账:升级成本几乎正比于你手改过的生成文件数。这篇讲怎么把业务代码写进 JHipster 工程而不弄脏生成代码——不是教条式的”一行不许动”,而是一套可执行的分层策略:能不改就不改,必须改就标记,改得多了就固化进 blueprint。
为什么”不改生成代码”是核心纪律
生成代码的价值在于可再生:.jhipster/Customer.json 在,jhipster entity Customer --single-entity 随时能重演这份代码。一旦你往 CustomerResource.java 里塞了二十行业务分支,这个文件就”变质”了——重新生成会覆盖你的逻辑,不重新生成它就从此脱轨于版本演进。两难一旦出现,升级(第 22 篇)就从 diff 管理退化为考古。
所以纪律的目标很单纯:让”删掉所有生成文件再重新生成”永远是一个可恢复操作,业务代码零损失。
业务代码的三种组织形态
按业务体量递进:
形态一:同工程新包(小项目默认)。 在生成的包结构旁边开平行包:
com/mycompany/myapp/
├── domain/ repository/ service/ web/rest/ # 生成区(可再生)
└── biz/ # 手写区
├── order/
│ ├── OrderFacade.java # 业务入口,组合生成的 Service
│ ├── OrderPricingEngine.java
│ └── dto/
├── inventory/
└── shared/ # 跨域工具
规则只有一条:依赖方向必须是 biz → 生成区,反向禁止。生成代码不知道 biz 的存在,重新生成才无感。
形态二:独立 Maven module(中大型项目)。 业务成规模后拆模块,生成器只管”应用壳”:
myapp/
├── pom.xml # 聚合根
├── myapp-generated/ # JHipster 生成的应用(少动)
├── myapp-order/ # 订单域:domain/service/api
├── myapp-inventory/
└── myapp-common/
<!-- myapp-generated/pom.xml -->
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>myapp-order</artifactId>
<version>${project.version}</version>
</dependency>
代价是 JHipster 默认单工程结构要手工调整(.yo-rc.json 仍描述生成壳),收益是业务模块可以独立测试、独立演进,甚至复用到别的工程。我们在订单域超过 3 万行时做了这次拆分。
形态三:前端 feature 目录。 Angular 侧同理,生成的 main/ 结构旁边开 feature 区:
src/main/webapp/app/
├── entities/ home/ admin/ # 生成区
└── features/
├── order-center/
│ ├── order-center.component.ts
│ ├── order-center.routes.ts # lazy route 独立加载
│ └── order-center.service.ts
└── inventory-board/
路由挂载到生成的 layout 下,生成的实体页面与手写 feature 页面并存。
组合优先于修改
业务逻辑需要基于生成的 CustomerService 扩展时,第一反应该是包一层而不是进去改:
@Service
public class OrderService {
private final CustomerService customerService; // 生成的
private final OrderRepository orderRepository; // 生成的(若是生成实体)
private final OrderPricingEngine pricingEngine; // 手写的
public OrderService(CustomerService customerService,
OrderRepository orderRepository,
OrderPricingEngine pricingEngine) {
this.customerService = customerService;
this.orderRepository = orderRepository;
this.pricingEngine = pricingEngine;
}
public OrderDTO placeOrder(PlaceOrderCmd cmd) {
var customer = customerService.findOne(cmd.customerId())
.orElseThrow(() -> new CustomerNotFoundException(cmd.customerId()));
var order = pricingEngine.quote(customer, cmd.items());
// ... 领域逻辑全在自己文件里
return new OrderDTO(orderRepository.save(order));
}
}
生成代码提供的是能力(CRUD、查询、权限),业务模块负责编排。升级时生成 Service 内部怎么变(比如 9.x 的重构)只要方法签名稳定,你的编排层纹丝不动。
需要替换行为时:覆盖 Spring Bean
当组合不够用(必须改变生成 Bean 本身的行为)时,用 Spring 的装配机制覆盖而不是直接编辑生成类:
// 方式一:@Primary 让手写 Bean 在注入点胜出
@Service
@Primary
public class AuditingUserService extends tech.mycompany.myapp.service.UserService {
private final AuditClient auditClient;
public AuditingUserService(UserRepository userRepository, AuditClient auditClient) {
super(userRepository);
this.auditClient = auditClient;
}
@Override
public User save(User user) {
auditClient.recordUserChange(user);
return super.save(user);
}
}
// 方式二:条件装配,配置开关切换实现
@Service
@ConditionalOnProperty(name = "myapp.notification.provider", havingValue = "sms")
@Primary
public class SmsNotificationService implements NotificationService { ... }
两条底线:覆盖类放在自己的包里,生成的 UserService 保持出厂状态;覆盖点登记到团队文档(见文末 review 清单),否则半年后没人知道行为被谁改过。
接口扩展点:让生成代码调用你
反向场景:想在生成的流程里挂业务钩子(用户注册后发券、实体保存后同步审计)。JHipster/Spring 提供的正规扩展点优先用:
- Spring 事件:生成的
UserService.save会发布AuditEvent等;用户注册后发券监听 Spring Security 的认证成功事件或自定义事件,零侵入; - Bean 生命周期钩子:
@Async的监听器把旁路逻辑挪出主事务; - MapStruct/Lifecycle 注解:
@PrePersist放在自己的Listener类,通过 entity listener 挂上,不改生成实体。
@Component
public class WelcomeCouponListener {
@Async
@TransactionalEventListener
public void onUserRegistered(UserRegisteredEvent event) {
couponService.grantWelcomeCoupon(event.userId());
}
}
必须改生成文件时:标记纪律
总有一小撮文件绕不开(SecurityConfiguration 加路由规则、application.yml 加配置)。约定:
- 改动前先确认”真的不能在 biz 层做吗”;
- 改动处用统一注释包裹,便于升级时 grep:
// >>> CUSTOM-BEGIN: 订单回调白名单(升级时保留)
.requestMatchers("/api/callback/order/**").permitAll()
// <<< CUSTOM-END
- 维护一份
GENERATED-OVERRIDES.md登记所有手改点:文件、原因、升级时的处置方式。我们 8→9 升级时这份清单 23 行,逐行搬运一小时收工——没有它就是考古。
目录纪律与 review 清单
每次 PR 自检,五分钟能做完:
| # | 检查项 | 通过标准 |
|---|---|---|
| 1 | diff 是否触碰生成文件 | 未触碰,或带 CUSTOM 标记且已登记 |
| 2 | 依赖方向 | biz → 生成区,无反向 import |
| 3 | 实体结构变更是否同步事实源 | .jhipster/*.json / JDL 已更新并重新生成 |
| 4 | 覆盖 Bean 是否必要且唯一 | 组合方案被否决的理由成立,@Primary 只有一处 |
| 5 | 新前端代码位置 | features/ 目录,lazy route |
CI 上可以加一个守卫脚本:对生成文件路径的 diff 若不含 CUSTOM-BEGIN 标记则fail。规则很粗糙,但足够拦住手滑。
小结
- “不改生成代码”的本质是保住可再生性,让升级永远便宜。
- 三级组织形态:新包 → Maven module → 前端 feature 目录,按业务体量递进。
- 组合优先于修改;必须覆盖时用 @Primary/条件装配且落在自己包里;钩子用 Spring 事件。
- 绕不开的手改必须 CUSTOM 标记 + 登记,review 清单五分钟守住纪律。
系列导航
- 上一篇:升级策略——JHipster 版本怎么跟
- 下一篇:子生成器二次开发与蓝图机制
- 相关阅读:本文是第 22 篇升级策略的前置投资;标记改多了就该升级到第 24 篇 blueprint;实体事实源维护见JDL 从入门到驯服