<- Back to Software Development

AOP and Middleware: Injecting Logging Without Touching Business Logic

June 5, 202610 min read
Share

Performance logging should not be scattered across every service method. If logging logic is mixed directly into business logic, the code becomes noisy, inconsistent, and hard to maintain. Java AOP and Node.js middleware solve the same architectural problem from two different angles: they let us inject cross-cutting behavior around the core execution flow without rewriting the business service itself.

Short Answer

Java AOP and Node.js middleware both help us add logging, tracing, timing, authentication checks, and error handling outside the core business logic.

The difference is where they attach.

TechniqueCommon RuntimeWhere It AttachesBest For
AOPJava / SpringAround method executionService-layer logging, transaction-like behavior, method timing
MiddlewareNode.js / Express / Koa / FastifyAround HTTP request lifecycleRequest logging, route timing, request ID, response status tracking

AOP is usually method-centric.

Middleware is usually request-centric.

For example, if we want to log how long OrderService.createOrder() takes, Java AOP is a clean fit. If we want to log how long POST /api/orders takes from HTTP entry to response finish, Node.js middleware is a clean fit.

Both approaches follow the same principle:

Do not pollute business logic with repeated infrastructure code.
Wrap the execution boundary and observe it from the outside.

The Problem: Logging Mixed Into Business Logic

A common beginner implementation puts logging directly inside the service method.

async function createOrder(orderInput) {
  const startedAt = Date.now();
  console.log("createOrder started");

  const user = await userRepository.findById(orderInput.userId);
  const order = await orderRepository.insert(orderInput);
  await paymentService.charge(order);

  const durationMs = Date.now() - startedAt;
  console.log("createOrder completed", { durationMs });

  return order;
}

This works, but it creates several problems.

The service method now has two responsibilities:

  • Create an order.
  • Handle observability logic.

That looks harmless at first, but it becomes messy when every service method repeats the same pattern.

The same problem appears in Java:

public Order createOrder(CreateOrderRequest request) {
    long startedAt = System.currentTimeMillis();
    log.info("createOrder started");

    User user = userRepository.findById(request.userId());
    Order order = orderRepository.save(request);
    paymentService.charge(order);

    long durationMs = System.currentTimeMillis() - startedAt;
    log.info("createOrder completed in {} ms", durationMs);

    return order;
}

The business logic is now surrounded by logging code. If the logging format changes, many methods must be changed. If a developer forgets to add timing logic, observability becomes inconsistent.

This is exactly the type of problem AOP and middleware are designed to solve.

What Cross-Cutting Concern Means

A cross-cutting concern is behavior needed by many parts of the system but not owned by the core business domain.

Logging is a cross-cutting concern because almost every route and service may need it. But OrderService should not need to know how logs are formatted for ELK, Datadog, Loki, or CloudWatch.

Common cross-cutting concerns include:

ConcernWhy It Should Be Separated
Request loggingNeeded everywhere, not specific to one feature
Performance timingSame measurement pattern repeated across methods
Error handlingNeeds consistent structure across the system
AuthenticationProtects access before business logic runs
AuthorizationChecks permissions without polluting domain flow
Rate limitingControls traffic before expensive work
Transaction handlingWraps database work around service methods
Metrics collectionReports system signals, not domain behavior

The business service should answer domain questions:

Can this order be created?
Is the user allowed to buy this product?
How much should the user pay?
What state should the order become?

Infrastructure layers should answer operational questions:

How long did the request take?
Which endpoint failed?
Which method threw an exception?
Which user or request ID triggered this flow?

AOP and middleware exist to keep these two categories separate.

How Java AOP Injects Logging

AOP means Aspect-Oriented Programming.

In Java, especially in Spring, AOP allows us to define logic that runs before, after, or around selected method executions. The business method does not call the logging code directly. Instead, the framework wraps the method call.

The flow looks like this:

Controller
  -> AOP Proxy
  -> Before Logic
  -> Business Service Method
  -> After Logic
  -> Controller

The service method stays focused:

@Service
public class OrderService {
    public Order createOrder(CreateOrderRequest request) {
        User user = userRepository.findById(request.userId());
        Order order = orderRepository.save(request);
        paymentService.charge(order);
        return order;
    }
}

The logging concern moves into an aspect:

@Aspect
@Component
public class PerformanceLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceLoggingAspect.class);

    @Around("execution(* com.example.order..*(..))")
    public Object logDuration(ProceedingJoinPoint joinPoint) throws Throwable {
        long startedAt = System.nanoTime();

        try {
            return joinPoint.proceed();
        } finally {
            long durationMs = (System.nanoTime() - startedAt) / 1_000_000;

            log.info("method={} durationMs={}",
                joinPoint.getSignature().toShortString(),
                durationMs
            );
        }
    }
}

Now every matched method can be timed without adding logging code into every method.

The important part is joinPoint.proceed(). It represents the original method execution.

Before proceed -> code runs before the business method
proceed        -> original business method runs
After proceed  -> code runs after the business method

This is why AOP is useful for service-layer observability. It gives us a controlled wrapper around method execution.

How Node.js Middleware Injects Logging

Node.js middleware usually wraps the HTTP request lifecycle.

In Express, the middleware receives req, res, and next.

Client
  -> Middleware
  -> Route Handler
  -> Response Finish
  -> Middleware Finish Listener

The route handler stays clean:

app.post("/api/orders", async (req, res) => {
  const order = await orderService.createOrder(req.body);
  res.status(201).json(order);
});

The logging concern is injected at the HTTP layer:

app.use((req, res, next) => {
  const startedAt = process.hrtime.bigint();

  res.on("finish", () => {
    const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;

    console.log(JSON.stringify({
      event: "http_request_completed",
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      durationMs: Math.round(durationMs),
    }));
  });

  next();
});

The business route does not need to know how request logging works. It only handles the domain operation.

Middleware is especially good at request-level observability because it can see:

  • HTTP method.
  • Path.
  • Query string.
  • Request headers.
  • Response status code.
  • Response finish event.
  • Request ID.
  • Client IP.
  • User identity after authentication middleware.

AOP observes method execution. Middleware observes request execution.

Same Goal, Different Boundary

AOP and middleware are not enemies. They are different tools for different execution boundaries.

QuestionBetter ToolReason
How long did POST /api/orders take?MiddlewareIt observes the full HTTP lifecycle
How long did OrderService.createOrder() take?AOPIt observes service method execution
Which response status was returned?MiddlewareIt has access to res.statusCode
Which internal method is slow?AOPIt can wrap selected service methods
Did the request reach the app?MiddlewareIt runs near the HTTP entry point
Did a specific domain operation become slow?AOPIt targets domain service methods
Should logging avoid controller/service pollution?BothBoth separate cross-cutting concerns

A practical backend can use both layers.

Middleware log:
POST /api/orders -> 920ms -> 201

AOP method log:
OrderService.createOrder() -> 730ms
PaymentService.charge() -> 510ms
InventoryService.reserve() -> 120ms

The middleware log tells us the request was slow. The AOP logs help identify which internal method consumed the time.

This creates a layered debugging path:

Request is slow
  -> Which route?
  -> Which service method?
  -> Which database query or downstream call?

Why This Keeps Business Logic Clean

The main benefit is separation of concerns.

Business logic should not care about how logging infrastructure works. It should care about the rules of the domain.

A clean service method should look like this:

public Order createOrder(CreateOrderRequest request) {
    validateOrder(request);
    reserveInventory(request);
    Order order = orderRepository.save(request);
    paymentService.charge(order);
    return order;
}

This is readable because every line belongs to the order creation process.

A polluted service method looks like this:

public Order createOrder(CreateOrderRequest request) {
    long startedAt = System.nanoTime();
    log.info("createOrder started requestId={}", RequestContext.getRequestId());

    try {
        validateOrder(request);
        reserveInventory(request);
        Order order = orderRepository.save(request);
        paymentService.charge(order);
        return order;
    } catch (Exception error) {
        log.error("createOrder failed", error);
        throw error;
    } finally {
        long durationMs = (System.nanoTime() - startedAt) / 1_000_000;
        log.info("createOrder completed durationMs={}", durationMs);
    }
}

This second version is harder to scan. The domain flow is hidden inside infrastructure code.

AOP and middleware solve this by moving repeated operational logic into a wrapper layer.

Business Logic Stays Focused

Service methods describe domain behavior instead of logging mechanics.

Logging Becomes Consistent

Every matched route or method follows the same event structure and timing rule.

Changes Become Safer

Updating log format, fields, or destination can happen in one layer instead of many services.

Debugging Becomes Searchable

Structured events make it easier to filter by route, method, status code, request ID, or duration.

Clean business logic is not only aesthetic. It reduces maintenance cost.

What Each Layer Should Log

Do not make middleware and AOP log the exact same thing. That creates noise.

Each layer should own a different level of detail.

LayerShould LogShould Avoid
HTTP MiddlewareRequest path, method, status, total duration, request IDDeep domain details
AOP Service AspectMethod name, class name, method duration, exception typeRaw HTTP headers
Error MiddlewareNormalized error response, request ID, error categorySensitive payload
Database LoggerQuery duration, query type, lock/wait signalFull private data
External Call WrapperTarget service, duration, status, timeoutAccess tokens

A good rule:

Middleware tells us what happened at the request boundary.
AOP tells us what happened inside the application boundary.

For example, middleware may produce:

{
  "event": "http_request_completed",
  "requestId": "req_123",
  "method": "POST",
  "path": "/api/orders",
  "statusCode": 201,
  "durationMs": 920
}

AOP may produce:

{
  "event": "method_completed",
  "requestId": "req_123",
  "className": "OrderService",
  "methodName": "createOrder",
  "durationMs": 730
}

Together, they tell a stronger story than either one alone.

Request ID Is the Bridge

AOP and middleware become much more useful when they share the same request ID.

The middleware should create or read a request ID when the HTTP request enters the application.

Incoming HTTP request
  -> create requestId
  -> attach requestId to request context
  -> route handler runs
  -> service method runs
  -> AOP logs method with same requestId
  -> response returns requestId

In Node.js, the request ID can be attached to req or stored using AsyncLocalStorage.

import { AsyncLocalStorage } from "node:async_hooks";
import crypto from "node:crypto";

const requestContext = new AsyncLocalStorage();

app.use((req, res, next) => {
  const requestId = req.headers["x-request-id"] || crypto.randomUUID();

  requestContext.run({ requestId }, () => {
    res.setHeader("x-request-id", requestId);
    next();
  });
});

function getRequestId() {
  return requestContext.getStore()?.requestId;
}

In Java, request context is often stored using a logging MDC.

@Component
public class RequestIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        String requestId = Optional
            .ofNullable(request.getHeader("x-request-id"))
            .orElse(UUID.randomUUID().toString());

        MDC.put("requestId", requestId);
        response.setHeader("x-request-id", requestId);

        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

Then the AOP aspect can log normally, and the logging framework can include requestId from MDC.

This is the key design: middleware or filter creates the request context, and deeper layers reuse it.

Common Mistakes

AOP and middleware are powerful, but they can be misused.

Using AOP for Everything

AOP is useful for method-level cross-cutting behavior, but too much hidden behavior makes code hard to reason about.

Logging Full Payloads

Request bodies and method arguments may contain passwords, tokens, or personal data. Log identifiers and safe metadata by default.

No Shared Request ID

Middleware logs and AOP logs are much weaker when they cannot be connected to the same request.

Duplicate Logs at Every Layer

If every layer logs the same event, the signal becomes noisy. Each layer should own a different level of detail.

Blocking Remote Logging

Sending logs synchronously to a remote system inside the request path can add latency or create failure coupling.

Forgetting Error Paths

Logging only successful completion hides the most important cases. Failure and timeout paths need structured events too.

The goal is not to add more logs. The goal is to add better boundaries.

A Practical Implementation Order

A good implementation does not start with perfect observability. It starts with a small number of consistent signals.

Step one: add HTTP middleware or filter logging.

requestId, method, path, statusCode, durationMs

Step two: add centralized error logging.

requestId, path, errorName, errorMessage, statusCode

Step three: add AOP only to important service packages.

OrderService, PaymentService, InventoryService

Step four: connect all logs through the same request ID.

HTTP request log -> service method log -> database or downstream log

Step five: create dashboards around questions, not around random fields.

Useful first dashboards:

  • Slowest routes by p95 latency.
  • Error count by route.
  • Slowest service methods.
  • Error count by exception type.
  • Timeout count by downstream service.
  • Latency before and after deployment.

The best first version is boring but consistent.

The Main Principle

AOP and middleware are both wrapper mechanisms.

Middleware wraps the HTTP lifecycle.

AOP wraps method execution.

They help us inject logging, timing, tracing, and error observation without touching the core business service logic. This keeps business code readable and makes operational behavior consistent.

The practical rule is:

Use middleware to observe the request.
Use AOP to observe the service method.
Use request ID to connect both.
Keep business logic focused on business rules.

That is how logging becomes an architecture feature instead of scattered console output.

Performance logging 不应该散落在每一个 service method 里面。如果 logging logic 直接混进 business logic,代码会变得吵、不一致、难维护。Java AOP 和 Node.js middleware 其实是在解决同一个架构问题:把 logging、timing、tracing 这些 cross-cutting behavior 注入到执行流程外层,而不是改动核心业务 service。

简短答案

Java AOP 和 Node.js middleware 都可以帮助我们在不污染 core business logic 的情况下,加入 logging、tracing、timing、authentication check 和 error handling。

差别在于它们 attach 的位置不同。

Technique常见 RuntimeAttach 位置最适合做什么
AOPJava / SpringMethod execution 外层Service-layer logging、method timing、类似 transaction 的包装逻辑
MiddlewareNode.js / Express / Koa / FastifyHTTP request lifecycle 外层Request logging、route timing、request ID、response status tracking

AOP 通常是 method-centric。

Middleware 通常是 request-centric。

例如,如果我们要记录 OrderService.createOrder() 花了多久,Java AOP 很适合。如果我们要记录 POST /api/orders 从 HTTP 入口到 response 结束总共花了多久,Node.js middleware 很适合。

两者背后的原则一样:

不要把重复的 infrastructure code 塞进 business logic。
把 execution boundary 包起来,然后从外部观察它。

问题:Logging 混进 Business Logic

很常见的初级写法,是直接在 service method 里面加 logging。

async function createOrder(orderInput) {
  const startedAt = Date.now();
  console.log("createOrder started");

  const user = await userRepository.findById(orderInput.userId);
  const order = await orderRepository.insert(orderInput);
  await paymentService.charge(order);

  const durationMs = Date.now() - startedAt;
  console.log("createOrder completed", { durationMs });

  return order;
}

这可以工作,但会制造几个问题。

这个 service method 现在有两个责任:

  • 创建订单。
  • 处理 observability logic。

一开始看起来没什么,但当每个 service method 都重复同样模式时,代码就会变得很乱。

Java 里面也会出现同样问题:

public Order createOrder(CreateOrderRequest request) {
    long startedAt = System.currentTimeMillis();
    log.info("createOrder started");

    User user = userRepository.findById(request.userId());
    Order order = orderRepository.save(request);
    paymentService.charge(order);

    long durationMs = System.currentTimeMillis() - startedAt;
    log.info("createOrder completed in {} ms", durationMs);

    return order;
}

Business logic 现在被 logging code 包起来了。如果 log format 要改,就要改很多 method。如果某个 developer 忘记加 timing logic,observability 就不一致。

这正是 AOP 和 middleware 要解决的问题。

Cross-Cutting Concern 是什么

Cross-cutting concern 指的是很多地方都需要,但它不属于核心业务领域的行为。

Logging 是 cross-cutting concern,因为几乎每个 route 和 service 都可能需要它。但是 OrderService 不应该关心 logs 最后是送去 ELK、Datadog、Loki 还是 CloudWatch。

常见 cross-cutting concerns 包括:

Concern为什么应该分离
Request logging到处都需要,不属于某一个 feature
Performance timing同样的 measurement pattern 会重复出现在很多 method
Error handling系统需要一致的 error 结构
Authentication应该在 business logic 之前保护入口
Authorization检查权限,但不应该盖住 domain flow
Rate limiting在昂贵业务逻辑前控制流量
Transaction handling包住 database work
Metrics collection报告系统信号,不是业务行为本身

Business service 应该回答 domain questions:

这个 order 能不能被创建?
这个 user 能不能买这个 product?
用户应该付多少钱?
Order 应该进入什么状态?

Infrastructure layer 应该回答 operational questions:

Request 花了多久?
哪个 endpoint 失败?
哪个 method 抛 exception?
哪个 user 或 request ID 触发了这个 flow?

AOP 和 middleware 的价值,就是把这两类问题分开。

Java AOP 如何注入 Logging

AOP 是 Aspect-Oriented Programming。

在 Java,尤其是 Spring 里面,AOP 允许我们定义一段逻辑,在某些 method execution 之前、之后,或者前后包起来执行。Business method 不需要主动调用 logging code,而是由 framework 在外面包一层。

Flow 大概是这样:

Controller
  -> AOP Proxy
  -> Before Logic
  -> Business Service Method
  -> After Logic
  -> Controller

Service method 保持干净:

@Service
public class OrderService {
    public Order createOrder(CreateOrderRequest request) {
        User user = userRepository.findById(request.userId());
        Order order = orderRepository.save(request);
        paymentService.charge(order);
        return order;
    }
}

Logging concern 移到 aspect 里面:

@Aspect
@Component
public class PerformanceLoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceLoggingAspect.class);

    @Around("execution(* com.example.order..*(..))")
    public Object logDuration(ProceedingJoinPoint joinPoint) throws Throwable {
        long startedAt = System.nanoTime();

        try {
            return joinPoint.proceed();
        } finally {
            long durationMs = (System.nanoTime() - startedAt) / 1_000_000;

            log.info("method={} durationMs={}",
                joinPoint.getSignature().toShortString(),
                durationMs
            );
        }
    }
}

现在,所有被匹配到的 method 都可以被 timing,而不需要把 logging code 写进每个 method。

关键是 joinPoint.proceed()。它代表原本的 method execution。

Before proceed -> business method 之前执行
proceed        -> 原本的 business method 执行
After proceed  -> business method 之后执行

所以 AOP 很适合 service-layer observability。它让我们可以在 method execution 外面加一层可控的 wrapper。

Node.js Middleware 如何注入 Logging

Node.js middleware 通常是包住 HTTP request lifecycle。

在 Express 里面,middleware 会拿到 reqresnext

Client
  -> Middleware
  -> Route Handler
  -> Response Finish
  -> Middleware Finish Listener

Route handler 可以保持干净:

app.post("/api/orders", async (req, res) => {
  const order = await orderService.createOrder(req.body);
  res.status(201).json(order);
});

Logging concern 被注入在 HTTP layer:

app.use((req, res, next) => {
  const startedAt = process.hrtime.bigint();

  res.on("finish", () => {
    const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;

    console.log(JSON.stringify({
      event: "http_request_completed",
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      durationMs: Math.round(durationMs),
    }));
  });

  next();
});

Business route 不需要知道 request logging 是怎么实现的。它只处理 domain operation。

Middleware 特别适合 request-level observability,因为它可以看到:

  • HTTP method。
  • Path。
  • Query string。
  • Request headers。
  • Response status code。
  • Response finish event。
  • Request ID。
  • Client IP。
  • Authentication middleware 之后得到的 user identity。

AOP 观察 method execution。Middleware 观察 request execution。

同一个目标,不同的 Boundary

AOP 和 middleware 不是互相替代的关系。它们只是观察不同 execution boundary 的工具。

问题更适合的工具原因
POST /api/orders 总共花了多久?Middleware它观察完整 HTTP lifecycle
OrderService.createOrder() 花了多久?AOP它观察 service method execution
最后返回了什么 response status?Middleware它可以拿到 res.statusCode
内部哪个 method 慢?AOP它可以包住指定 service methods
Request 有没有到达 app?Middleware它靠近 HTTP 入口
某个 domain operation 是不是变慢?AOP它针对 domain service method
想避免 controller/service 被 logging 污染?两者都可以两者都能分离 cross-cutting concerns

一个实际 backend 可以同时使用两层。

Middleware log:
POST /api/orders -> 920ms -> 201

AOP method log:
OrderService.createOrder() -> 730ms
PaymentService.charge() -> 510ms
InventoryService.reserve() -> 120ms

Middleware log 告诉我们 request 慢了。AOP log 帮我们定位内部哪个 method 花时间。

这样 debugging path 会分层:

Request is slow
  -> 哪个 route?
  -> 哪个 service method?
  -> 哪条 database query 或 downstream call?

为什么这能保持 Business Logic 干净

核心价值是 separation of concerns。

Business logic 不应该关心 logging infrastructure 怎么实现。它应该关心 domain rules。

干净的 service method 应该像这样:

public Order createOrder(CreateOrderRequest request) {
    validateOrder(request);
    reserveInventory(request);
    Order order = orderRepository.save(request);
    paymentService.charge(order);
    return order;
}

这段代码容易读,因为每一行都属于 order creation process。

被污染的 service method 会像这样:

public Order createOrder(CreateOrderRequest request) {
    long startedAt = System.nanoTime();
    log.info("createOrder started requestId={}", RequestContext.getRequestId());

    try {
        validateOrder(request);
        reserveInventory(request);
        Order order = orderRepository.save(request);
        paymentService.charge(order);
        return order;
    } catch (Exception error) {
        log.error("createOrder failed", error);
        throw error;
    } finally {
        long durationMs = (System.nanoTime() - startedAt) / 1_000_000;
        log.info("createOrder completed durationMs={}", durationMs);
    }
}

第二种版本比较难读。Domain flow 被 infrastructure code 挤在中间。

AOP 和 middleware 通过 wrapper layer,把重复的 operational logic 移出去。

Business Logic 保持专注

Service methods 只描述 domain behavior,而不是 logging mechanics。

Logging 更一致

每个被匹配的 route 或 method 都使用同样的 event structure 和 timing rule。

修改更安全

Log format、fields、destination 要调整时,可以集中在一个 layer 改,不需要改很多 services。

Debugging 更容易搜索

Structured events 可以按 route、method、status code、request ID、duration 过滤。

Business logic 干净不是为了好看。它会直接降低维护成本。

每一层应该记录什么

不要让 middleware 和 AOP 记录完全一样的东西。那会制造 noise。

每一层应该负责不同粒度的细节。

Layer应该记录应该避免
HTTP MiddlewareRequest path、method、status、total duration、request ID太深的 domain details
AOP Service AspectMethod name、class name、method duration、exception typeRaw HTTP headers
Error MiddlewareNormalized error response、request ID、error categorySensitive payload
Database LoggerQuery duration、query type、lock/wait signal完整 private data
External Call WrapperTarget service、duration、status、timeoutAccess tokens

一个好规则是:

Middleware 告诉我们 request boundary 发生了什么。
AOP 告诉我们 application 内部 boundary 发生了什么。

例如,middleware 可以产出:

{
  "event": "http_request_completed",
  "requestId": "req_123",
  "method": "POST",
  "path": "/api/orders",
  "statusCode": 201,
  "durationMs": 920
}

AOP 可以产出:

{
  "event": "method_completed",
  "requestId": "req_123",
  "className": "OrderService",
  "methodName": "createOrder",
  "durationMs": 730
}

两个合起来,比单独任何一个都更有解释力。

Request ID 是桥梁

AOP 和 middleware 要变得真正有用,就必须共享同一个 request ID。

Middleware 应该在 HTTP request 进入 application 时,创建或读取 request ID。

Incoming HTTP request
  -> create requestId
  -> attach requestId to request context
  -> route handler runs
  -> service method runs
  -> AOP logs method with same requestId
  -> response returns requestId

在 Node.js 里,request ID 可以挂在 req 上,也可以用 AsyncLocalStorage 保存。

import { AsyncLocalStorage } from "node:async_hooks";
import crypto from "node:crypto";

const requestContext = new AsyncLocalStorage();

app.use((req, res, next) => {
  const requestId = req.headers["x-request-id"] || crypto.randomUUID();

  requestContext.run({ requestId }, () => {
    res.setHeader("x-request-id", requestId);
    next();
  });
});

function getRequestId() {
  return requestContext.getStore()?.requestId;
}

在 Java 里,request context 通常可以用 logging MDC 保存。

@Component
public class RequestIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain filterChain
    ) throws ServletException, IOException {
        String requestId = Optional
            .ofNullable(request.getHeader("x-request-id"))
            .orElse(UUID.randomUUID().toString());

        MDC.put("requestId", requestId);
        response.setHeader("x-request-id", requestId);

        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove("requestId");
        }
    }
}

之后 AOP aspect 正常写 log,logging framework 就可以从 MDC 里面带上 requestId

这个设计的重点是:middleware 或 filter 负责建立 request context,深层 service 只复用它。

常见错误

AOP 和 middleware 很好用,但也很容易被滥用。

Using AOP for Everything

AOP 适合 method-level cross-cutting behavior,但如果太多隐藏逻辑都靠 AOP,代码会变得难推理。

Logging Full Payloads

Request body 和 method arguments 可能包含 password、token 或 personal data。默认只记录 identifiers 和 safe metadata。

No Shared Request ID

Middleware logs 和 AOP logs 如果不能连接到同一个 request,价值会弱很多。

Duplicate Logs at Every Layer

如果每一层都记录同样的 event,signal 会变吵。每一层应该负责不同 detail level。

Blocking Remote Logging

在 request path 里面同步发送 logs 到远端系统,可能增加 latency,也可能造成 failure coupling。

Forgetting Error Paths

只记录 successful completion 会漏掉最重要的情况。Failure 和 timeout path 也需要 structured events。

目标不是增加更多 logs。目标是增加更好的 boundaries。

一个实用的实施顺序

好的 implementation 不需要一开始就追求完美 observability。先建立少量稳定 signals 更重要。

第一步:加入 HTTP middleware 或 filter logging。

requestId, method, path, statusCode, durationMs

第二步:加入 centralized error logging。

requestId, path, errorName, errorMessage, statusCode

第三步:只对重要 service packages 加 AOP。

OrderService, PaymentService, InventoryService

第四步:用同一个 request ID 把所有 logs 串起来。

HTTP request log -> service method log -> database or downstream log

第五步:根据问题建立 dashboard,不要根据随机字段建立 dashboard。

第一批有用 dashboard:

  • 按 p95 latency 排列的 slowest routes。
  • 按 route 分组的 error count。
  • 最慢的 service methods。
  • 按 exception type 分组的 error count。
  • 按 downstream service 分组的 timeout count。
  • Deployment 前后的 latency 对比。

最好的第一版通常很无聊,但必须一致。

核心原则

AOP 和 middleware 本质上都是 wrapper mechanism。

Middleware 包住 HTTP lifecycle。

AOP 包住 method execution。

它们让我们在不触碰 core business service logic 的情况下,注入 logging、timing、tracing 和 error observation。这样 business code 保持可读,operational behavior 也保持一致。

实用规则是:

用 middleware 观察 request。
用 AOP 观察 service method。
用 request ID 把两者接起来。
让 business logic 专心处理 business rules。

这样 logging 才会从散落的 console output,变成系统架构的一部分。

In this series

Art of Middleware

View series ->

Part 2 of 2. Move between logs in the same learning sequence.