AOP 架构

AOP的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑就可以复用,还可以动态增删

Posted by chanweiyan on April 26, 2026

MVC 架构

在 MVC 架构下,请求会先发送给 controller,由 controller 调度 model 层的 service 来完成 业务逻辑,然后,返回对应的 view。

mvc

在这个流程中,nest 还提供了 AOP(Aspect Oriented Programming)的能力,也就是面向切面编程的能力。

后端框架基本都是 AOP 架构

QA: ruby on rails 也是 AOP 架构吗?

💬点击展开/收起

严格来说,Ruby on Rails 并不标榜自己是一个“AOP(面向切面编程)框架”。它的核心架构身份是经典的 MVC 框架

但是,Rails 在设计和日常开发中,非常深度且广泛地贯彻了 AOP 的核心思想。它并没有像 Java 的 Spring 或 Node.js 的 NestJS 那样显式地使用 Pointcut(切入点)、Advice(通知)这样的传统 AOP 术语,而是通过 Ruby 强大的动态语言特性(元编程)来实现同样的目标。

在 Rails 的语境中,AOP 的思想主要化身为了 Callbacks (回调)Filters (过滤器)Concerns (关注点)

既然你在写关于 NestJS AOP 的文章 (2026-04-26-nest-aop.md),其实可以将 Rails 和 NestJS 的 AOP 概念做一个很好的映射:

1. 控制器层的“切面”(相当于 NestJS 的 Guards / Interceptors)

在 Rails 的 Controller 中,最经典的 AOP 体现就是 before_actionafter_actionaround_action。我们将横切关注点(如:身份验证、权限校验、日志记录)抽离出来,在不污染核心业务逻辑的情况下注入:

1
2
3
4
5
6
7
class ApplicationController < ActionController::Base
# 相当于 AOP 的 Before Advice / NestJS 的 Guard
before_action :require_login

# 相当于 AOP 的 Around Advice / NestJS 的 Interceptor
around_action :log_time_spent
end

2. 模型层的生命周期“切面”

Rails 的 ORM (ActiveRecord) 在数据的整个生命周期中预留了大量的切入点: 比如 before_validation, before_save, after_commit, before_destroy 等。这些钩子可以让你无侵入地在保存数据前后插入公共行为(例如密码哈希、自动生成编号、发送异步通知),这也是 AOP 典型的应用场景。

3. 横切关注点的组织:ActiveSupport::Concern

Rails 特意提供了一个名为 ActiveSupport::Concern 的特性。它的名字直译就是“关注点”,专门用来处理跨面关注点(Cross-cutting Concerns)。 你可以把在多个不同类中重复出现的逻辑(如:点赞功能、软删除逻辑)抽离成一个 Concern 模块,然后通过 include 直接混入到对应的 Model 或 Controller 中。这和 AOP 追求解藕和复用的理念完全一致。

4. 语言级支持:Module#prepend

不同于 Java/Spring 需要通过动态代理(Proxy)或字节码提升(CGLib)来实现 AOP,Ruby 本身是一门极其灵活的语言。通过原生的 Module#prepend(早期被称为 alias_method_chain),Rails 开发者或 Gem 包的作者可以轻松地包装一个正在运行的方法,在其周围包裹新的逻辑(拦截调用),从底层语言级别原生支持类似“环绕通知”的特性。

总结 Rails 并不是一个标准学术意义上的 AOP 架构,但它满身都是 AOP 模式的应用。

与 NestJS 相比,NestJS (受 Angular 和 Spring 启发)将 AOP 概念非常显性化、规范化,并拆分出了清晰的固定层级(Middleware、Guard、Interceptor、Pipe、Exception Filter)。而 Rails 则倾向于将这些思想“隐藏”在“Ruby 魔法”中,让你以为在用普通的方法回调(Callback),不知不觉间就吃到了 AOP 解藕的红利。

AOP是什么意思呢?什么是面向切面编程呢?

一个请求过来,可能会经过 Controller(控制器),Service(服务),Repository(数据库访问)的逻辑

一个请求过来,可能会经过 Controller(控制器),Service(服务),Repository(数据库访问)的逻辑

如果想在这个调用链路里加入一些通用逻辑要怎么加呢?比如日志记录,权限控制,异常处理等。

容易想到的是直接改造 controller 层代码,加入这段逻辑。

这样可以,但是不优雅,应为这些通用的逻辑侵入到了业务逻辑里面。能不能透明的给这些业务逻辑加上日志,权限等处理呢?

那是不是可以在调用 controller 之前和之后加入一个执行通用逻辑的阶段呢?

比如这样

那是不是可以在调用 controller 之前和之后加入一个执行通用逻辑的阶段呢?

是不是就和切了一刀一样?

这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做AOP(面向切面编程)

其实 Express 的中间件的洋葱模型也是一种 AOP 的实现,应为你可以透明的在外面包一层,加入一些逻辑,内层感受不到。

QA: React 前端编程中有没有用到 AOP

💬点击展开/收起

在 React 前端编程中,虽然极少直接使用“AOP(面向切面编程)”这个名词,但 AOP 的核心思想(抽离横切关注点、无侵入式增强)在 React 中无处不在

在后端(如 NestJS)中,AOP 常被用于鉴权、日志、异常截获等。在 React 中,处理这类非 UI 核心逻辑(横切关注点)时,演进出了几种非常经典的纯前端“AOP”模式:

1. 高阶组件 (HOC - Higher-Order Components)

这是 React 中最经典、最接近传统 AOP “代理/环绕通知 (Around Advice)” 概念的模式。 HOC 是一个函数,接收一个组件作为参数,返回一个包装后的新组件。你可以在包装过程中注入权限校验、埋点、主题等逻辑,而不修改原组件的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 典型的 AOP 思想:抽离鉴权逻辑
function withAuth(WrappedComponent) {
  return function(props) {
    const isAuthenticated = checkAuth();
    if (!isAuthenticated) {
      return <Redirect to="/login" />;
    }
    // 权限校验通过,渲染原组件,像极了 NestJS 的 Guard
    return <WrappedComponent {...props} />;
  };
}

// 使用切面增强
const ProtectedDashboard = withAuth(Dashboard);

2. Custom Hooks (自定义 Hook)

自从 React 16.8 引入 Hooks 后,Hooks 成为了目前分离横切关注点最主流的方式。 虽然 Hook 是“侵入式”地写在组件内部调用的,但它把复杂的副作用(数据获取、事件监听、埋点日志等)剥离到了独立的函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 剥离页面停留时间的埋点逻辑 (Cross-cutting concern)
function usePageTracking(pageName) {
  useEffect(() => {
    trackEvent('page_view', { page: pageName });
    const timer = setInterval(() => trackTime(), 1000);
    return () => clearInterval(timer);
  }, [pageName]);
}

function HomePage() {
  // 一行代码注入切面逻辑,不污染 UI 渲染
  usePageTracking('Home');
  return <div>Welcome Home</div>;
}

3. 状态级中间件 (例如 Redux Middleware)

如果你用过 Redux,它的 Middleware 是前端最标准的 AOP 实现。 在派发(Dispatch)一个动作(Action)到达状态(Reducer)之间,Redux 允许你插入一层或多层中间件。

这和 NestJS 的 Interceptor/Middleware 几乎是一模一样的设计。常用于: *打印所有的状态变更日志(redux-logger) *处理异步请求(redux-thunk, redux-saga) *统一上报错误

4. 装饰器 (Decorators) 在 React 还在流行 Class Component(类组件)的时代,配合 Babel 的装饰器语法,前端的 AOP 写法和类似 Spring/NestJS 的写法可以说一模一样。比如 mobx 的 @observer,或者自定义的埋点装饰器:

1
2
3
4
5
@withAuth
@logRenderTime
class UserProfile extends React.Component {
  render() { return <div>Profile</div>; }
}

5. 错误边界 (Error Boundaries) React 的 Error Boundary 专门用来捕获整个子组件树中发生的 JavaScript 错误,记录这些错误,并展示备用 UI。这实际上就是 AOP 中的 异常通知 (After Throwing Advice) 层面的应用(相当于 NestJS 里的 Exception Filters)。


总结

前端同学不爱把这些叫 AOP,主要是因为前端更推崇 FP(函数式编程)Composition(组合) 的理念。

但在架构本质上: HOC** = 环绕代理 (Proxy / Around Advice) **Redux Middleware = 拦截器层 (Interceptor) Error Boundary** = 全局异常过滤器 (Exception Filter) **Custom Hook = 逻辑维度的切面注入

所以,React 早就把 AOP 思想融进了它的骨血里,只是换了一套前端更加习惯用语(Pattern)而已。

nest 有5种实现 AOP 的方式

nest 有5种实现 AOP 的方式

1.Middleware

中间件是 express 里的概念,nest 的底层是 express,自然也可以使用中间件,但是做了进一步的细分, 分为全局中间件和路由中间件。

1.全局中间件

在 main.ts 通过 app.use 使用

1
2
3
4
5
6
7
app.use(function(req: Request, res: Response, next: NextFunction) {
  console.log('before', req.url);  // middleware
  next();                          // handler
  console.log('after');            // middleware
})

可以在多个路由件复用中间件

可以在多个路由件复用中间件

在 handler 前后动态增加一些可复用的逻辑,就是 AOP 的切面编程的思想。

2.路由中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';

@Injectable()
export class LogMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => void) {
    console.log('before2', req.url);

    next();

    console.log('after2');
  }
}

在 AppModule 中消费中间件

在 AppModule 中启用

在 configure 方法里配置 LogMiddleware 在哪些路由生效

2.Guard

guard 是路由守卫的意思,可以用于在调用某个 controller 之前判断权限,返回 true 或者 false 来 决定是否放行

可以用于在调用某个 controller 之前判断权限,返回 true 或者 false 来决定是否放行

1
2
3
4
5
6
7
8
9
10
11
12
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class LoginGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    console.log('login check')
    return false;
  }
}

guard 要实现 CanActivate 接口,实现 canActivate 方法,可以冲 context 拿到请求的信息, 然后,做一些权限校验校验处理后返回 true 或 false。

1.在 AppController 中启用

  • @UseGuards()

在 AppController 中启用

controller 本身不需要做什么修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。

2.app.useGlobalGuards 全局启用guard

手动 new的 Guard 实例,不在 IoC 容器中

在全局启用

3.在 AppModule 中全局启用 guard

用 provider 的方式声明的 guard 在 IoC 容器里,可以注入别的 provider

  • APP_GUARD

.在 AppModule 中全局启用 guard

注入 AppService 到 guard 中

注入 AppService 到 guard 中

3.Interceptor

拦截器,可以在目标 controller 方法前后加入一些逻辑

拦截器,可以在目标 controller 方法前后加入一些逻辑

Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会 调用目标 controller,可以在之前和之后加入一些逻辑。处理逻辑可能是异步的,nest 里通过 rxjs 来 组织它们,所以可以使用 rxjs 的各种 operator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class TimeInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {

    const startTime = Date.now();

    return next.handle().pipe(
      tap(() => {
        console.log('time: ', Date.now() - startTime)
      })
    );
  }
}

tap() 操作符,它会在可观察流正常或异常终止时调用我们的匿名日志记录函数,但不会干扰响应周期。

@UseInterceptor 启用 interceptor

启用 interceptor

interceptor 和 middleware 之间的区别,在于参数不同,interceptor 可以拿到调用的 controller 和 handler

interceptor 可以拿到调用的 controller 和 handler

在 controller 和 handler 上加一些 metadata,这种只有 interceptor 和 guard 里可以取出来,middleware 不行

interceptor 支持每个路由单独启用,只作用于莫个 handler

@UseInterceptors

可以在 controller 级别启用,作用于下面所有 handler

可以在 controller 级别启用,作用于下面所有 handler

也可以全局启用,作用于所有 controller

app.useGlobalInterceptors

APP_INTERCEPTOR

4.Pipe

对参数的处理逻辑也是一个通用的逻辑,nest 抽出了对应的切面 - Pipe

Pipe 管道,用来对参数做一些校验和转换

Pipe 管道,用来对参数做一些校验和转换

Pipe 要实现 PipeTransform 接口,实现 transform 方法,可以对传入的参数值 value 做参数校验, 比如格式,类型是否正确,不正确抛出异常。也可以做转换,返回转换后的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class ValidatePipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {

    if(Number.isNaN(parseInt(value))) {
      throw new BadRequestException(`参数${metadata.data}错误`)
    }

    return typeof value === 'number' ? value * 10 : parseInt(value) * 10;
  }
}

  • @UsePipe
  • app.useGlobalPipes
  • APP_PIPE

5.Exception Filter

对抛出的异常进行处理,返回对应的响应

对抛出的异常进行处理,返回对应的响应

实现 ExceptionFilter 接口,catch 方法,就可以拦截异常了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';

@Catch(BadRequestException)
export class TestFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {

    const response: Response = host.switchToHttp().getResponse();

    response.status(400).json({
      statusCode: 400,
      message: 'test: ' + exception.message
    })
  }
}

  • @UseFilters
  • app.useGlobalFilters
  • APP_FILTER