技术栈认识完了,进入日常开发。JHipster 的开发工作流是”双进程”模式:后端 Spring Boot 跑在 8080,前端 Vite dev server 跑在 9000,两者各自热重载、通过代理打通。这个模式我们用了五年,把它吃透后日常迭代速度非常快。这篇把机制和坑一次讲完。
双进程模型
先建立全局认知:
flowchart LR
B["浏览器<br/>http://localhost:9000"] -->|"页面资源"| V["Vite dev server<br/>:9000"]
B -->|"/api/** 请求"| V
V -->|"代理转发"| S["Spring Boot<br/>:8080"]
S --> DB[("H2 / 数据库")]
subgraph 热重载
V -.->|"改 ts/html → 秒级刷新"| B
S -.->|"改 java → 自动重启"| S
end
关键在于:浏览器永远只访问 9000,/api 请求由 Vite 代理转发给 8080。因此开发期不存在跨域问题——请求与页面同源,这正是代理存在的意义。直接访问 8080 反而不行,那上面没有前端页面。
日常两个终端:
# 终端 1:后端
./mvnw
# 终端 2:前端
npm start
后端热重载:Spring Boot DevTools
JHipster dev profile 默认集成 Spring Boot DevTools,监听 classpath 变化自动重启:
- IDE 里改 Java 文件,保存触发增量编译(IDEA 的”自动构建”、Eclipse 的自动编译),DevTools 检测到 class 变化即重启;
- 重启是半热的:容器快速重建,比冷启动快得多,但会丢掉应用内状态(比如你手填的内存数据);
- 改
application-dev.yml等资源文件同样触发。
两个实用细节。第一,DevTools 默认排除了哪些目录、要不要触发重启,可以在 application.yml 的 spring.devtools.restart.* 下微调。第二,IDEA 用户需要开启 Build project automatically 并勾选允许运行中编译(Registry 的 idea.auto.make 一族设置),否则保存不会触发编译,DevTools 自然无感。
我们的习惯:改业务代码靠 DevTools 足够;涉及依赖变更(pom.xml)或 JDL 重新生成,还是老老实实 Ctrl+C 重启。
前端热重载:Vite
npm start 起的是 Vite dev server(9000 端口)。Angular 21 的组件改模板、改类,保存后毫秒到秒级完成热更新(HMR),页面状态尽量保留。代理配置在 vite.config.mts(生成时已写好):
export default defineConfig({
server: {
port: 9000,
proxy: {
'/api': { target: 'http://127.0.0.1:8080', changeOrigin: true },
// OIDC/Keycloak 等回调路径同理代理
},
},
});
如果后端端口改了,记得同步改这里,否则表现就是”页面能开,一登录就 502”。
浏览器无痕窗口
一个不起眼但极省时间的小技巧:调试 JHipster 前端时开无痕窗口。原因有两个:
- 前端资源带强缓存策略,改了页面却看到旧版本,八成是缓存作祟;无痕窗口每次干净的缓存,结果是确定性的。
- 认证状态存在 localStorage(JWT)里,admin/user 多账号切换测试权限时,无痕窗口天然隔离,不用手动清存储。
我们团队的规矩:复现”我这里不对”的前端问题,先在无痕窗口验一遍再说。
日志排障
双进程意味着日志也在两处:
- 后端日志直接打在
./mvnw的终端里;SQL 日志在 dev profile 默认可见(logging.level.org.hibernate.SQL=debug可在application-dev.yml调),排查”为什么这条 SQL 不对”很方便; - 如果配置了日志文件输出,
tail -f跟踪:
tail -f ./target/logs/*.log # 后端
# 前端编译报错看 npm start 终端,构建失败会直接贴出错误位置
Liquibase 在启动时的执行日志也值得扫一眼——每个 changeset 的执行结果都会列出来,数据库没升上去第一现场就在这里(第 7 篇)。
IDE 与双进程调试
两个进程意味着两套调试入口,配置一次受益长期:
- 后端:IDEA 里直接跑应用主类(
Application.java)替代./mvnw,断点、条件断点、表达式求值全部可用;DevTools 与 IDE 运行兼容,重启照旧自动; - 前端:Vite 默认产出 source map,浏览器 DevTools 的 Sources 面板里直接对 TS 源码下断点,配合无痕窗口排障效率很高;
- 网络层:DevTools 的 Network 面板按
Fetch/XHR过滤,重点盯三样——请求是否带了Authorization: Bearer头(认证问题第 8 篇)、响应是 4xx 还是 5xx(前端问题还是后端问题)、payload 分页参数是否正确。
H2 Console:直接看数据
dev 环境的 H2 带网页控制台,查数据比写测试快得多。application-dev.yml 里默认开启 spring.h2.console.enabled=true,通过 /h2-console(走 8080)登录,JDBC URL 在启动日志里打印。排查”数据到底存成什么样了""Liquibase 建的表长什么样”时,这是第一现场。想玩真 SQL 方言,等 CI 里 Testcontainers 的 PostgreSQL(第 17 篇)。
常见联调问题速查表
| 症状 | 原因 | 解法 |
|---|---|---|
| 页面能开,接口全 401/404 或 502 | Vite 代理目标与后端实际端口不一致 | 对齐 vite.config.mts 与后端端口 |
| CORS 报错(开发期) | 绕过了代理,前端直接打了 8080 或其他域 | 检查代码里写死的 URL,一律走相对路径 /api |
| 8080 被占用 | 上一个后端没退干净 | lsof -i :8080 找到进程杀掉,或改端口 |
| 9000 被占用 | 多个前端实例 | 同上;JHipster 会提示端口占用 |
| 改了 Java 没生效 | IDE 没开自动编译,DevTools 无触发 | 开自动构建,或手动 Build |
| 改了前端”没变” | 浏览器缓存 | 无痕窗口 / 硬刷新 |
| 登录后立即 401 | JWT 过期时间与系统时钟不一致(WSL2 时钟漂移常见) | 同步系统时间 |
| 改了实体但接口返回旧结构 | 后端没重启(依赖变更级别的修改 DevTools 不触发) | 手动重启后端进程 |
特别展开一下 CORS:开发期不应看到 CORS。同源走代理是设计内状态;如果控制台出现 CORS 报错,说明有人写了绝对地址(http://localhost:8080/api/...),把它改回相对路径即可。生产期前后端同 jar 部署也不存在跨域;只有前后端分开部署时才需要在 application-cors.yml(Spring Boot 4 的 CORS configuration properties)里显式放行域名。
一个完整的迭代循环
以”给 Customer 加一个字段并出现在页面”为例,我们的日常节奏:
- JDL 加字段,
jhipster jdl app.jdl(或jhipster entity Customer --single-entity)——后端、DTO、Liquibase changeset、前端模型、测试全部更新(第 6 篇); - DevTools 自动重启,Liquibase 自动执行新 changeset;
- 前端页面细节手工微调,Vite 秒级热更;
- 无痕窗口验证 admin/user 两种视角;
./mvnw verify跑测试后提交。
整个循环通常几分钟,这正是 JHipster 双进程工作流的甜点。
联调前自检清单
遇到联调问题时按这个顺序过一遍,比随机尝试快得多:
- 两个终端都活着:8080 后端日志正常滚动、9000 前端无编译报错;
- 浏览器地址是
localhost:9000而不是8080; - 请求路径以
/api开头(走代理)而非绝对地址; - Network 面板确认请求到达的是 Vite(端口 9000)而不是直连后端;
- 后端终端里能看到对应请求的日志(到了后端就是后端问题,没到就是代理问题);
- 认证问题先看
Authorization头是否存在与 token 是否过期; - 仍无解:无痕窗口 + 重启两个进程,排除缓存与僵尸状态。
小结
- 双进程:
./mvnw(8080)+npm start(9000),浏览器只访问 9000,/api由 Vite 代理转发。 - 后端靠 Spring Boot DevTools 热重启(IDEA 记得开自动编译),前端靠 Vite HMR 秒级生效。
- 无痕窗口规避缓存与账号状态污染,是前端排障第一动作。
- 开发期出现 CORS 即代码写了绝对地址的信号;端口冲突查
lsof;日志分看两个终端。
系列导航
- 上一篇:技术栈全景图
- 下一篇:微服务 vs 单体——怎么选
- 延伸阅读:JDL 从入门到驯服 · 环境搭建与第一个应用