文章目录
- 前言
- ArkTS 里的 AOP:装饰器就是切面
- 第一个切面:@LogExecution
- @TrackTime:性能耗时追踪
- @CatchError:统一异常捕获
- 全链路追踪:组合装饰器
- PerfMonitor 数据收集
- 踩过的坑
- 小结
前言
你有没有过这种体验:业务代码写得好好的,PM 说"加个性能监控",于是你在每个方法前后都塞上了Date.now()和console.info。代码瞬间变得又长又丑,业务逻辑和监控代码搅在一起,改起来特别痛苦。
这就是典型的横切关注点问题——日志、性能监控、错误捕获这些逻辑,跟业务本身没关系,但又散落在到处都是。AOP(面向切面编程)就是专门搞定这类事的。
ArkTS 里的 AOP:装饰器就是切面
在 Java 世界,AOP 靠的是 Spring AOP 或者 AspectJ,底层用动态代理或字节码织入。ArkTS 没这么复杂——装饰器天然就是 AOP 的实现方式。
方法装饰器可以在方法执行前后插入逻辑,完全不需要修改原始代码。这比 Java 那套优雅多了。
第一个切面:@LogExecution
先来个最简单的,自动记录方法调用和参数:
// decorators/LogExecution.etsfunctionLogExecution(target:Object,propertyKey:string,descriptor:PropertyDescriptor):PropertyDescriptor{constoriginalMethod=descriptor.value descriptor.value=function(...args:any[]){conststart=Date.now()console.info(`[Log] 调用${propertyKey},参数:${JSON.stringify(args)}`)try{constresult=originalMethod.apply(this,args)constelapsed=Date.now()-startconsole.info(`[Log]${propertyKey}完成,耗时${elapsed}ms`)returnresult}catch(e){console.error(`[Log]${propertyKey}异常:${e}`)throwe}}returndescriptor}用法特别简单,往方法上一贴就行:
classUserService{@LogExecutionasyncfetchUser(userId:string):Promise<User>{constresponse=awaithttp.request(`/api/users/${userId}`)returnJSON.parse(response.resultasstring)}}业务代码干干净净,日志逻辑全在装饰器里。想加就加,想摘就摘,零侵入。
@TrackTime:性能耗时追踪
做性能优化的时候,需要知道每个关键方法的实际耗时。手动埋点太累,用装饰器批量搞定:
// decorators/TrackTime.etsimport{PerfMonitor}from'../monitor/PerfMonitor'functionTrackTime(tag?:string){returnfunction(target:Object,propertyKey:string,descriptor:PropertyDescriptor):PropertyDescriptor{constoriginalMethod=descriptor.valueconstlabel=tag??propertyKey descriptor.value=function(...args:any[]){conststart=Date.now()PerfMonitor.getInstance().markStart(label)constfinalize=()=>{constelapsed=Date.now()-start PerfMonitor.getInstance().record(label,elapsed)if(elapsed>100){console.warn(`[Perf] 慢方法告警:${label}耗时${elapsed}ms`)}}try{constresult=originalMethod.apply(this,args)// 处理异步方法if(resultinstanceofPromise){returnresult.then((val)=>{finalize();returnval},(err)=>{finalize();throwerr})}finalize()returnresult}catch(e){finalize()throwe}}returndescriptor}}这里有个细节——异步方法需要特殊处理。如果直接finalize(),拿到的是同步耗时,没有意义。判断返回值是不是 Promise,是的话就在.then()里再记录。
@CatchError:统一异常捕获
每个方法都写 try-catch 太烦了。装饰器统一管理:
// decorators/CatchError.etsfunctionCatchError(fallbackValue?:any){returnfunction(target:Object,propertyKey:string,descriptor:PropertyDescriptor):PropertyDescriptor{constoriginalMethod=descriptor.value descriptor.value=asyncfunction(...args:any[]){try{returnawaitoriginalMethod.apply(this,args)}catch(e){// 上报到错误监控平台ErrorReporter.getInstance().report({method:propertyKey,error:e,args,timestamp:Date.now()})// 返回降级值,避免页面崩溃if(fallbackValue!==undefined){console.warn(`[CatchError]${propertyKey}异常,返回降级值`)returnfallbackValue}throwe}}returndescriptor}}有了这个装饰器,网络请求失败、数据解析出错这些场景都能优雅降级,不会直接白屏:
classProductRepository{@CatchError([])asyncgetProductList():Promise<Product[]>{constdata=awaithttp.request('/api/products')returnJSON.parse(data.resultasstring)}@CatchError(null)asyncgetProductDetail(id:string):Promise<Product|null>{constdata=awaithttp.request(`/api/products/${id}`)returnJSON.parse(data.resultasstring)}}全链路追踪:组合装饰器
单个装饰器好用,但实际项目中经常需要多个切面叠加。来个全链路追踪的完整实现:
// monitor/TraceContext.etsexportclassTraceContext{privatestaticinstance:TraceContextprivatetraces:Map<string,TraceSpan[]>=newMap()privatecurrentTraceId:string=''staticgetInstance():TraceContext{if(!TraceContext.instance){TraceContext.instance=newTraceContext()}returnTraceContext.instance}beginTrace(traceId:string):void{this.currentTraceId=traceIdthis.traces.set(traceId,[])}addSpan(span:TraceSpan):void{constspans=this.traces.get(this.currentTraceId)if(spans){spans.push(span)}}endTrace():TraceReport{constspans=this.traces.get(this.currentTraceId)??[]consttotal=spans.reduce((sum,s)=>sum+s.duration,0)return{traceId:this.currentTraceId,spans,totalDuration:total,spanCount:spans.length}}}exportinterfaceTraceSpan{name:stringstartTime:numberduration:numberstatus:'success'|'error'}exportinterfaceTraceReport{traceId:stringspans:TraceSpan[]totalDuration:numberspanCount:number}然后是串联多个装饰器的组合装饰器:
// decorators/Traced.etsfunctionTraced(spanName?:string){returnfunction(target:Object,propertyKey:string,descriptor:PropertyDescriptor):PropertyDescriptor{constoriginalMethod=descriptor.valueconstname=spanName??propertyKey descriptor.value=asyncfunction(...args:any[]){conststart=Date.now()letstatus:'success'|'error'='success'try{constresult=awaitoriginalMethod.apply(this,args)returnresult}catch(e){status='error'ErrorReporter.getInstance().report({method:name,error:e,args})throwe}finally{TraceContext.getInstance().addSpan({name,startTime:start,duration:Date.now()-start,status})}}returndescriptor}}现在给一个完整的用户下单流程打上追踪:
classOrderService{@Traced('创建订单')asynccreateOrder(items:CartItem[]):Promise<Order>{constorder=awaitthis.buildOrder(items)awaitthis.validateStock(order)awaitthis.submitOrder(order)returnorder}@Traced('构建订单')privateasyncbuildOrder(items:CartItem[]):Promise<Order>{// ...}@Traced('校验库存')@CatchError(null)privateasyncvalidateStock(order:Order):Promise<boolean>{// ...}@Traced('提交订单')privateasyncsubmitOrder(order:Order):Promise<void>{// ...}}调用createOrder后,就能拿到整条链路上每个步骤的耗时和状态。哪个环节慢了、哪个环节报错了,一目了然。
PerfMonitor 数据收集
前面用到的PerfMonitor简单实现一下:
// monitor/PerfMonitor.etsexportclassPerfMonitor{privatestaticinstance:PerfMonitorprivaterecords:Map<string,number[]>=newMap()staticgetInstance():PerfMonitor{if(!PerfMonitor.instance){PerfMonitor.instance=newPerfMonitor()}returnPerfMonitor.instance}markStart(label:string):void{// 可扩展:记录开始时间戳}record(label:string,duration:number):void{if(!this.records.has(label)){this.records.set(label,[])}this.records.get(label)!.push(duration)}getStats(label:string):{avg:number;max:number;count:number}{constdurations=this.records.get(label)??[]if(durations.length===0)return{avg:0,max:0,count:0}return{avg:durations.reduce((a,b)=>a+b,0)/durations.length,max:Math.max(...durations),count:durations.length}}dumpReport():string{constlines:string[]=['=== 性能报告 ===']this.records.forEach((durations,label)=>{constavg=(durations.reduce((a,b)=>a+b,0)/durations.length).toFixed(1)constmax=Math.max(...durations)lines.push(`${label}: 平均${avg}ms, 最大${max}ms, 调用${durations.length}次`)})returnlines.join('\n')}}踩过的坑
装饰器顺序很重要。多个装饰器叠加时,执行顺序是从下往上(靠近方法的先执行)。@Traced要在@CatchError上面,这样错误先被捕获,追踪记录的 status 才准确。
异步方法的耗时统计。很多人忘了async方法的返回值是 Promise,直接同步记录拿到的不是真实耗时。一定要判断返回值类型,异步的在.then()/.finally()里记录。
生产环境记得关掉详细日志。开发阶段全量记录没问题,上线后只保留慢方法告警和错误上报,不然日志量会很大。
小结
AOP 在 ArkTS 里用装饰器实现,比传统 Java 方案轻量得多。核心思路就是:把横切逻辑(日志、监控、错误处理)从业务代码里抽出来,通过装饰器织入。
我自己在项目里的做法是,给所有网络请求方法和关键业务方法统一加上@Traced+@CatchError,一行代码搞定监控和容错。再也不用在业务代码里到处写 try-catch 和Date.now()了。代码干净了,监控也没落下。