Skip to content

服务

服务层承载核心业务逻辑,与请求/响应解析无关。ACE 通过编译期宏 @Service 在构建阶段自动生成 Bean 注册代码,运行时直接查表——无反射、无字符串扫描。

基本用法

cangjie
package task_api.service

import ace_framework.*
import task_api.repository.TaskRepository
import task_api.dto.*

@Service
public class TaskService {
    @Log var log: Logger
    @Inject var repo: TaskRepository

    public func listAll(): Array<TaskView> {
        repo.findAll().map { t => TaskView.from(t) }.toArray()
    }

    public func get(id: Int64): ?TaskView {
        repo.findById(id).map { t => TaskView.from(t) }
    }

    @Timed
    public func create(title: String, priority: Int64, owner: String): TaskView {
        log.info("creating task: ${title}")
        let task = repo.save(Task(title: title, priority: priority, owner: owner))
        TaskView.from(task)
    }

    @Transactional
    public func transferOwner(fromId: Int64, toId: Int64, taskId: Int64): Unit {
        repo.updateOwner(taskId, toId)
        repo.logTransfer(fromId, toId, taskId)
    }
}

@Service 无额外参数时,注册为 Singleton(默认作用域)。

Bean 作用域

装饰器作用域说明
@ServiceSingleton整个应用共享一个实例(默认)
@PrototypePrototype每次 resolve<T>() 创建新实例
@Service["request"]Request同一 HTTP 请求内共享;请求结束自动销毁
cangjie
// 每次 resolve 新实例
@Prototype
public class ReportBuilder {
    var rows: ArrayList<Row> = ArrayList<Row>()
    // ...
}

// 请求内共享(可安全存储当前用户等请求级数据)
@Service["request"]
public class CurrentUser {
    var id: Int64 = 0
    var roles: Array<String> = []
}

依赖注入 @Inject

字段注入

cangjie
@Service
public class OrderService {
    @Inject var userSvc: UserService
    @Inject var inventory: InventoryService
    @Inject var repo: OrderRepository
}

接口注入与多实现区分

当同一接口有多个实现时,用 @Primary 标记默认实现,用 @Qualifier 按名称区分:

cangjie
// 声明接口
public interface NotificationSender {
    func send(to: String, msg: String): Unit
}

@Service
@Primary
public class EmailSender <: NotificationSender { ... }

@Service
@Qualifier["sms"]
public class SmsSender <: NotificationSender { ... }

// 注入时
@Service
public class AlertService {
    @Inject var sender: NotificationSender            // 注入 @Primary -> EmailSender
    @Inject @Qualifier["sms"] var smsSender: NotificationSender  // 注入 SmsSender
}

生命周期钩子

@PostConstruct — 初始化钩子

在 Bean 完成依赖注入后、第一次被使用前执行。适合预热缓存、建立连接池等操作:

cangjie
@Service
public class CacheWarmupService {
    @Inject var repo: ProductRepository
    var hotProducts: Array<Product> = []

    @PostConstruct
    public func warmup(): Unit {
        hotProducts = repo.findTopN(100).toArray()
    }
}

@PreDestroy — 停机钩子

应用正常关闭时调用,适合释放资源:

cangjie
@Service
public class ConnectionPoolService {
    var pool: ConnectionPool = ConnectionPool()

    @PreDestroy
    public func shutdown(): Unit {
        pool.close()
    }
}

AOP 宏组合使用

@Service 类的方法可叠加多种 AOP 宏,编译期生成增强代理,运行时无额外开销:

cangjie
@Service
public class ProductService {
    @Log var log: Logger
    @Inject var repo: ProductRepository

    @Cacheable[ttl: 300_000]   // 缓存 5 分钟
    @Timed                      // 记录执行耗时
    public func getById(id: Int64): ?ProductView {
        repo.findById(id).map { p => ProductView.from(p) }
    }

    @Retry[3]                   // 失败最多重试 3 次
    @CircuitBreaker[5]          // 5 次失败后熔断
    public func fetchExternal(sku: String): ExternalInfo { ... }

    @Async                      // 异步执行,返回 Future<T>
    public func sendReport(email: String): Unit {
        log.info("sending report to ${email}")
        // 耗时操作...
    }

    @Scheduled[ms: 60_000]     // 每分钟执行
    public func cleanup(): Unit {
        repo.deleteExpired()
    }

    @Transactional
    @Timed
    public func batchCreate(items: Array<CreateReq>): Array<ProductView> {
        items.map { req => repo.save(Product.from(req)) }
             .map { p => ProductView.from(p) }
             .toArray()
    }
}

常用 AOP 宏一览:

说明
@Log注入结构化 Logger,自动绑定类名
@Timed记录方法执行耗时到 metrics
@Cacheable[ttl: ms]方法结果缓存,ttl 单位毫秒
@Async方法异步执行,调用方立即返回
@Scheduled[ms: n]定时任务,每 n 毫秒执行一次
@Scheduled["cron"]按 cron 表达式调度
@Retry[n]失败自动重试,最多 n 次
@CircuitBreaker[n]n 次失败后触发熔断保护
@EventListener监听应用事件总线消息
@Transactional开启数据库事务,异常自动回滚
@RequiresNew挂起当前事务,开启独立新事务
@Around["name"]挂载自定义拦截器

完整示例

cangjie
package task_api.service

import ace_framework.*
import task_api.repository.TaskRepository
import task_api.dto.*

@Service
public class TaskService {
    @Log var log: Logger
    @Inject var repo: TaskRepository

    @Timed
    public func create(title: String, priority: Int64, owner: String): TaskView {
        log.info("create task", "title" => title, "owner" => owner)
        let task = repo.save(Task(
            title: title,
            priority: priority,
            owner: owner,
            status: "pending"
        ))
        TaskView.from(task)
    }

    @Cacheable[ttl: 10_000]
    public func get(id: Int64): ?TaskView {
        repo.findById(id).map { t => TaskView.from(t) }
    }

    public func page(page: Int64, size: Int64): PageResult<TaskView> {
        let offset = (page - 1) * size
        let items = repo.findPage(offset, size).map { t => TaskView.from(t) }.toArray()
        let total = repo.count()
        PageResult(items: items, total: total, page: page, size: size)
    }

    @Transactional
    public func updateStatus(id: Int64, status: String): TaskView {
        let task = repo.findById(id) ?? throw NotFoundException("task not found")
        repo.save(task.withStatus(status))
        |> TaskView.from
    }

    @Transactional
    public func delete(id: Int64): Unit {
        repo.deleteById(id)
        log.info("task deleted", "id" => id)
    }

    @PostConstruct
    public func init(): Unit {
        log.info("TaskService initialized")
    }

    @PreDestroy
    public func destroy(): Unit {
        log.info("TaskService shutting down")
    }
}

Singleton 字段安全

@Service(Singleton)的实例在整个应用中只有一份,不要在字段中存储请求级状态(如当前用户、请求 ID)。如需请求级共享状态,请使用 @Service["request"] 作用域,或通过 ctx.state 传递。

接口优先

推荐为 Service 提取接口,便于测试时 Mock 替换:

cangjie
public interface TaskService {
    func create(title: String, priority: Int64, owner: String): TaskView
    func get(id: Int64): ?TaskView
}

@Service
public class TaskServiceImpl <: TaskService { ... }

测试时注入 MockTaskService 即可,无需启动完整容器。

@Scheduled 与 Singleton

@Scheduled 方法挂在 Singleton Bean 上,整个应用只会注册一个定时任务实例。 避免在 @Scheduled 方法内访问 request scope Bean,请求上下文在定时任务中不存在,访问会抛出 NoActiveRequestException

基于 Apache-2.0 许可证发布