目录结构
ACE Framework 遵循"高内聚、低耦合"的分层原则。每个目录承担单一职责,可独立演进。以下以 task-api 示例项目为蓝本展示完整结构。
完整目录树
task-api/
├── cjpm.toml # 包配置(package、dependencies、sub-packages)
├── src/
│ ├── main.cj # 程序入口,显式 import 触发自注册
│ ├── controller/ # HTTP 路由层(@Controller + @Get/@Post …)
│ │ ├── task_controller.cj
│ │ └── user_controller.cj
│ ├── service/ # 业务逻辑层(@Service / @Prototype)
│ │ ├── task_service.cj
│ │ └── user_service.cj
│ ├── domain/ # 领域模型 / 实体(@Entity、@Id、@Column)
│ │ ├── task.cj
│ │ └── user.cj
│ ├── dto/ # 数据传输对象(请求体 / 响应体结构)
│ │ ├── create_task_dto.cj
│ │ └── task_response.cj
│ ├── middleware/ # 自定义中间件(@Middleware[order])
│ │ ├── auth_middleware.cj
│ │ └── logging_middleware.cj
│ └── exception/ # 全局异常处理(@Catch[ExceptionType])
│ ├── not_found_handler.cj
│ └── validation_handler.cj
└── config/
├── application.toml # 基础配置(所有环境共用)
├── application-local.toml # 本地开发覆盖
├── application-dev.toml # 开发环境覆盖
├── application-test.toml # 测试环境覆盖
└── application-prod.toml # 生产环境覆盖目录职责说明
| 目录 | 职责 | 典型注解 |
|---|---|---|
src/controller/ | 接收 HTTP 请求,参数绑定,调用 Service,返回响应 | @Controller @Get @Post @PathParam @Body |
src/service/ | 核心业务逻辑,事务边界,调用 Repository/外部服务 | @Service @Transactional @Cacheable |
src/domain/ | 实体定义,与数据库表一一映射 | @Entity @Id @Column |
src/dto/ | 纯数据结构,用于请求体反序列化和响应体序列化 | 无注解(普通 struct/class) |
src/middleware/ | 横切关注点:认证、日志、限流等,按 order 排序挂载 | @Middleware[order] |
src/exception/ | 全局异常拦截,将异常映射为标准 HTTP 响应 | @Catch[ExceptionType] |
config/ | 多环境配置文件,TOML 格式,按环境叠加合并 | — |
cjpm.toml 结构说明
toml
[package]
name = "task_api"
version = "1.0.0"
cangjie-version = "1.1.0"
[dependencies]
ace_framework = { path = "../../ace-framework" }
ace_http = { path = "../../ace-http" }
[target.aarch64-apple-darwin.bin-dependencies]
# stdx 预编译路径(本机 macOS 26 绕过,勿删)
path-option = "..."ACE 的各能力模块(ace-web / ace-router / ace-bodyparser / ace-http / ace-framework)均作为独立成员发布在 workspace 下,task-api 只需在 [dependencies] 中声明所需层。cjpm 会自动处理传递依赖,无需手动列出 ace-web。
为什么 main.cj 必须显式 import 子包
ACE 的声明式自注册机制依赖顶层 let 变量的初始化器在程序启动时自动执行。以 @Service 为例,宏展开后会在源文件顶层生成:
cangjie
let __ace_reg_TaskService = Registry.register("TaskService") { TaskService() }仓颉编译器只会编译被直接或间接 import 引用的包。若 main.cj 不导入 controller 和 service 包,这些顶层初始化器将永远不会执行,容器中也就没有任何可注入的 Bean。
cangjie
// src/main.cj
package task_api
// 必须显式 import 各子包,否则 @Service/@Controller 的自注册不会触发
import task_api.controller.*
import task_api.service.*
import task_api.middleware.*
import task_api.exception.*
import ace_framework.*
import ace_http.*
main(): Int64 {
let app = AceApplication.create()
app.listen(8080)
return 0
}与 Spring/MidwayJS 的差异
Spring 通过运行时 classpath 扫描(反射)自动发现 Bean;MidwayJS 通过 TypeScript 装饰器元数据在启动时扫描。ACE 选择编译期宏 + 零反射路线——宏生成等价的显式注册代码,import 语句替代运行时扫描,在保持声明式体验的同时消除了反射开销。
常见错误
忘记 import 某个 controller 包后,路由注册不生效,但框架不会报错——容器中仅仅没有该 Controller 的 Bean。遇到 404 时,优先检查 main.cj 的 import 列表。
与 MidwayJS 目录对比
| MidwayJS | ACE Framework | 说明 |
|---|---|---|
src/controller/ | src/controller/ | 相同职责 |
src/service/ | src/service/ | 相同职责 |
src/entity/ | src/domain/ | ACE 用 domain 体现领域驱动意图 |
src/dto/ | src/dto/ | 相同职责 |
src/middleware/ | src/middleware/ | 相同职责 |
src/filter/ | src/exception/ | ACE 用 @Catch 注解替代 Filter 概念 |
bootstrap.ts 扫描装饰器 | main.cj 显式 import | ACE 无运行时扫描,import 即注册 |
configuration.ts | config/application.toml | ACE 统一用 TOML 配置,无 JS 配置文件 |