非受检异常适合表达程序逻辑不该到达的风控失败场景;结合自定义注解与AOP,可实现声明式、轻量、透明、可复用的动态风控,支持多维判断(如单日调用次数≤100、近5分钟失败率)。
非受检异常(runtimeexception 及其子类)本身不强制要求捕获或声明,适合表达“程序逻辑不该走到此处”的业务风控失败场景;配合自定义注解,可将风控规则声明式地嵌入方法边界,由 aop 统一拦截、执行校验、并在不满足条件时抛出非受检异常——从而实现轻量、透明、可复用的动态风控指标落地。
注解设计需支持风控维度与动态开关
风控不是“有无权限”,而是多维判断:如「单日调用次数 ≤ 100」「近5分钟失败率
- 用数组声明多个风控指标键,例如
@RiskControl({"api.daily.count", "user.level", "call.fail.rate"}) - 支持配置是否启用(避免测试/灰度环境误触发),如
enabled = "${risk.control.enabled:true}"——注意:该值不能直接写进注解属性,需在切面中通过@Value或Environment动态读取后参与判断 - 建议保留
level = RiskLevel.WARN枚举,区分是记录告警(不中断)、降级执行,还是直接抛异常阻断
切面中统一执行风控校验并抛非受检异常
必须使用 @Around,在校验失败时主动抛出继承自 RuntimeException 的自定义风控异常(如 RiskBlockedException),确保业务流程被明确中断:
- 先检查
enabled配置,为false则直接proceed()放行 - 从统一上下文(如
SecurityContext.getCurrentUserId()或MDC.get("traceId"))提取当前请求标识 - 调用风控服务(如
riskService.evaluate(method, userId, riskKeys)),返回Boolean或更丰富的RiskResult - 若不通过且
level == BLOCK,立即throw new RiskBlockedException("触发风控规则: " + key)——该异常会被全局异常处理器捕获,转为 403 或定制错误码
非受检异常要能被上层正确识别和响应
抛出的异常不是为了 crash,而是为了精准传递风控语义:
- 定义
RiskBlockedException extends RuntimeException,并添加字段如ruleKey、triggerValue、threshold,便于日志追踪和监控埋点 - 配合
@ControllerAdvice编写专用异常处理器,对RiskBlockedException返回结构化 JSON(含code=40301、message="访问受限:单日调用超限"、detail对象) - 在 Prometheus + Grafana 中,可基于该异常的全限定类名或 error_code 做指标聚合,形成「每分钟风控拦截数」「各规则触发占比」等动态看板
规避常见陷阱
实际落地中容易忽略的关键细节:
- 异步方法(
@Async)中注解失效:因RequestContextHolder和线程上下文丢失,需用TransactionSynchronizationManager或TaskDecorator显式传递风控上下文 - 注解未加
@Retention(RetentionPolicy.RUNTIME)→ 切面匹配不到,风控静默失效 - 在切面里吞掉异常(如
try-catch后只打日志)→ 业务方法照常执行,风控形同虚设 - 把复杂计算逻辑(如滑动窗口计数)写死在切面里 → 难以单元测试、无法热更新规则。应抽离为独立
RiskEvaluator组件,支持 SPI 扩展

















