# 请求处理

# 介绍

Handler用于根据客户端请求路径进行针对性处理。

客户端请求进入ServiceCore后,将先后经过全局拦截器全局中间件管道。当全局中间件管道中的最后一个中间件执行完成后,ServiceCore将自动创建与请求路径匹配的Handler实例并引导请求进入其中进行后续处理。

提示

Handler拥有独立于ServiceCore中间件系统。因此,我们在Handler维度指定的中间件只作用于符合请求路径规则的客户端请求。

另外,Handler的中间件系统兼容Express生态且支持动态中间件,我们可以根据客户端请求的实际上下文(比如:请求参数):

  • 动态指定中间件列表
  • 控制中间件执行规则(比如:跳过执行)

Web服务-设置请求路径一章中我们已经了解到,自定义Handler需要实现一个继承自Core.Handler的类。本质上Core.Handler是一个包含了核心处理流程的抽象类,我们通过实现抽象类内的HOOK方法以定制请求处理流程的各个环节。

下面,我们将分类介绍Core.Handler中推荐被重写的方法:

# 请求路径

  • static getRoutePath()

    • 使用场景:设置Handler的请求路径规则。ServiceCore处理客户端请求时,将根据请求路径指定完成实际处理动作的Handler实例

    • 调用时机ServiceCore执行实例方法bind()时调用此方法获取Handler的请求路径规则。

    • 注意事项:重写时无需执行super操作,使用return返回请求路径规则。

      注意

      ServiceCore将自动校正Handler的请求路径规则:当请求路径规则不以'/'开头时,附加'/'作为前缀

      因此,我们配置请求路径规则时应尽量以'/'开头。

    • 默认行为:设置请求路径规则为'/'

      static getRoutePath() {
        return '/';
      }
      
      1
      2
      3

# 生命周期

  • initHandler(req, res, next)

    • 使用场景:指定Handler初始化逻辑。

      提示

      此方法中通常执行的是非业务相关的通用逻辑(比如:创建日志输出器),对于业务相关的初始化逻辑推荐放入请求预处理请求后处理阶段。

    • 调用时机ServiceCore处理每个客户端请求时,都将创建与请求路径匹配的Handler实例并调用此方法触发Handler初始化

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      我们在实现Handler初始化逻辑时,通常不直接操作客户端返回实例res,而是通过流程控制函数控制请求处理链路。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:直接调用流程控制函数进入下一处理阶段。

      initHandler(req, res, next) {
        next();
      }
      
      1
      2
      3
  • destroyHandler(req, res)

    • 使用场景:指定Handler析构逻辑。

    • 调用时机:进入Handler处理的客户端请求返回时将调用此方法触发Handler析构

      提示

      如果客户端请求在ServiceCore的全局拦截器全局中间件管道阶段返回则不会触发Handler析构

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      进入Handler析构阶段时,客户端请求已返回处理结果。因此,我们应仅对客户端请求实例req客户端返回实例res发起读取动作。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

# 中间件系统

  • getMiddlewares(req, res)

    • 使用场景:根据客户端请求的实际上下文(比如:请求参数)动态指定Handler中间件列表。

    • 调用时机:在Handler初始化完成后将进入中间件阶段,在开始分发Handler中间件前将调用此方法获取中间件列表

    • 注意事项:重写时无需执行super操作,使用return返回中间件列表即可;当执行过程中产生异常时将触发统一错误处理

      提示

      我们在指定Handler中间件列表时,通常不直接操作客户端返回实例res

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:返回[]

      getMiddlewares(req, res) {
        return [];
      }
      
      1
      2
      3
  • onInterceptMiddleware(middleware, req, res, next)

    • 使用场景动态中间件的核心HOOK方法,提供了在分发中间件时控制其执行行为的能力。

    • 调用时机中间件阶段分发每个中间件时,都将调用此方法完成中间件的实际执行行为。

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      我们在实现动态中间件时,通常不直接操作客户端返回实例res,而是通过流程控制函数控制中间件的执行链路。

      中间件拦截阶段,我们可以根据当前分发中间件middleware.type的类型,决定是否调用中间件的执行函数middleware.exec以实现对中间件执行行为的动态控制。

    • 默认行为:执行中间件并将其结果作为执行流程控制函数的参数。

      onInterceptMiddleware(middleware, req, res, next) {
        const { exec } = middleware;
        exec((result) => next(result));
      }
      
      1
      2
      3
      4

# 请求处理

  • preHandler(req, res, next)

    • 使用场景:根据中间件执行结果初始化处理客户端请求所需的基础环境,比如:参数聚合、创建基础资源等。

    • 调用时机:在中间件阶段完成后(即:最后一个中间件的拦截阶段结束时),将调用此方法触发请求预处理

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      我们在指定请求预处理逻辑时,通常不直接操作客户端返回实例res,而是通过流程控制函数控制请求处理链路。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:直接调用流程控制函数进入下一处理阶段。

      preHandler(req, res, next) {
        next();
      }
      
      1
      2
      3
  • [METHOD]Handler(req, res, next)

    • 使用场景:根据客户端请求方式触发对应的业务处理逻辑。

    • 调用时机:当请求预处理阶段结束时,将调用与客户端请求方式匹配的实例方法以进入请求后处理

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      [METHOD]Handler不是在指定请求后处理逻辑时实际重写的方法名,只是Handler Method的代称,比如:处理客户端POST请求,我们应该重写Handler中的实例方法postHandler()

      我们在指定请求后处理逻辑时,通常不直接操作客户端返回实例res,而是通过流程控制函数控制请求处理链路。

      如果在Handler中没有实现与客户端请求方式匹配的实例方法,此时将调用defaultHandler()执行默认后处理逻辑。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

  • defaultHandler(req, res, next)

    • 使用场景:对请求方式不符合预期的客户端请求进行统一处理。

    • 调用时机:当请求预处理结束时,如果Handler中没有实现与客户端请求方式匹配的实例方法时,将调用此方法触发默认的请求后处理

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      我们在指定默认的请求后处理逻辑时,通常不直接操作客户端返回实例res,而是通过流程控制函数控制请求处理链路。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:直接调用流程控制函数触发统一完成处理向客户端返回404状态码。

      defaultHandler(req, res, next) {
        next(404);
      }
      
      1
      2
      3

# 统一处理

  • onFinish(data, req, res)

    • 使用场景:对客户端请求处理完成后的逻辑进行统一处理。

    • 调用时机:在任意请求处理阶段使用流程控制函数执行next(data)时将调用此方法触发统一完成处理

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发统一错误处理

      提示

      我们在指定统一完成处理逻辑时,通常应直接操作客户端返回实例res,向客户端返回处理结果。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:操作客户端返回实例res,向客户端返回处理结果。

      onFinish(data, req, res) {
        // 当请求已返回时不再执行实际逻辑
        if (this.isEnded) {
          return;
        }
        // 当data为null或undefined时 - 返回204
        else if (isNullOrUndefined(data)) {
          res.status(204).end();
        }
        // 当data为Number类型时 - data作为状态码
        else if (getType(data) === VALUE_TYPE_NUMBER) {
          res.status(data).end();
        }
        // 其他情况 - data作为应答内容
        else {
          res.status(200).send(data);
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
  • onError(error, req, res)

    • 使用场景:对客户端请求处理过程中产生的异常进行统一处理。

    • 调用时机:在任意请求处理阶段的跟函数中产生了未被捕获的异常,或使用流程控制函数执行next(error)时,将调用此方法触发统一错误处理

    • 注意事项:重写时无需执行super操作,当执行过程中产生异常时将触发ServiceCore中的错误拦截器

      提示

      我们在指定统一错误处理逻辑时,通常应直接操作客户端返回实例res,向客户端返回处理结果。

      另外,为了更精确的捕获执行过程中异常,我们应根据阶段内实际逻辑的同步或异步指定此方法为FunctionAsyncFunction

    • 默认行为:操作客户端返回实例res,向客户端返回500状态码。

      onError(error, req, res) {
        !this.isEnded && res.status(500).end();
      }
      
      1
      2
      3

# 处理流程

Handler处理流程

# 流程控制函数

ServiceCore自动为每个客户端请求创建与请求路径对应的Handler实例,并调用其私有实例方法_onStart()以启动处理流程。

注意

Handler的实例方法_onStart()中实现了处理流程控制逻辑。因此,我们在自定义Handler时,一定不要使用_onStart作为实例方法名。

通常,我们仅在指定统一完成处理统一错误处理阶段直接操作客户端返回实例res,其余阶段中使用next进行流程控制:

  • 执行next(data):触发统一完成处理data将作为实例方法onFinish()参数列表的第一个入参。

    提示

    默认的统一完成处理逻辑使流程控制函数支持多种调用方式向客户端返回请求处理结果:

    • next(message)

      流程控制函数指定的data不为Number类型、nullundefined,认为期望向客户端返回处理结果报文。

      此时,将向客户端返回200状态码,并将data作为返回报文。

    • next(httpCode)

      流程控制函数指定的dataNumber类型时,认为期望向客户端返回状态码。

      此时,将向客户端返回data中指定的状态码和空报文。

    • next()next(null)next(undefined)

      仅在请求后处理阶段执行next()时命中此逻辑,其余处理阶段中执行next()将分发至下一处理阶段。

      流程控制函数指定的datanullundefined,认为期望向客户端返回空报文。

      此时,将向客户端返回204状态码和空报文。

    我们可以通过指定统一完成处理阶段的逻辑以定制流程控制函数向客户端返回请求处理结果的方式。

  • 执行next(error):触发统一错误处理error将作为实例方法onError()参数列表的第一个入参。

  • 执行next()next(null)next(undefined):进入下一处理阶段。

提示

Handler的流程控制函数与Express的中间件分发函数拥有一致的使用体验。因此,我们可以在Handler的中间件系统直接使用Express的中间件生态。

不得不提的是,在自定义Handler时,我们应按照Handler处理阶段对业务逻辑进行拆分以实现各个HOOK方法,并在期望阶段处理结束时使用next()分发处理流程。

虽然在实现上,Handler使用洋葱圈模型串联了每个请求处理阶段,理论上可以通过await next()进行二次穿透。

# 设置请求路径

Web服务-设置请求路径一章中,我们已经了解了如何通过重写Handler的静态方法getRoutePath()以指定Handler的请求路径规则。

接下来,我们将重点讨论指定请求路径规则时的注意事项:

  • 请求路径规则必须为非空字符串。

    ServiceCore在执行bind()时,将对Handler中指定的请求路径规则进行校验,如果为非字符串型值或空字符串将跳过对此Handler的挂载。

  • 请求路径规则应尽量使用'/'开头。

    ServiceCore在执行bind()时,还将对Handler中指定的请求路径规则进行校正,如果不以'/'开头时,将自动附加'/'作为前缀。

  • 使用前缀型请求路径规则场景下,在业务层执行bind()时,应将指定了更长请求路径规则的Handler放置于绑定列表中较前的位置。

    ServiceCore匹配与客户端请求对应的Handler时,将按照执行bind()时的指定的Handler顺序依次进行。

    注意

    比如,业务层在执行bind()时依次绑定了两个Handler,其请求路径规则分别为'/api''/api/Test.do'

    此时当客户端请求路径为/api/Test.do时,ServiceCore将优先匹配到请求路径规则/apiHandler用于此次请求处理。

    因此,我们在使用前缀型请求路径规则场景下,应在执行bind()时关注绑定列表中的Handler顺序。

# 初始化和析构

Handler的初始化和析构标志着Handler生命周期的开始和结束:

  • ServiceCore分发客户端请求进入与请求路径匹配的Handler时,首先将触发Handler初始化
  • Handler初始化触发后(非初始化执行完成后),客户端请求返回时将触发Handler析构

提示

通常,我们仅在统一完成处理统一错误处理中操作客户端返回实例res,向客户端返回请求处理结果以触发Handler析构

当然也有例外,比如在Handler中使用静态资源中间件express.static,当客户端请求命中静态资源时将直接返回该资源,此时也将触发Handler析构

我们应在Handler初始化Handler析构阶段执行相反的资源操作,以使内存得到有效释放。

# Handler初始化

我们通过重写Handler的实例方法initHandler()以指定Handler初始化阶段执行的逻辑。

出于复用性考虑,推荐在Handler初始化阶段执行一些与业务无关的通用初始化逻辑,比如:创建日志输出器读取全局配置等;与业务相关的初始化逻辑推荐放入请求预处理阶段中。

提示

通常,我们将Handler初始化阶段中创建的资源提升为实例属性,以在各个请求处理阶段中共享:

initHandler(req, res, next) {
  // 创建基础输出器并提升为实例属性
  this.logger = new Core.BaseLogger();
  // 分发至下一处理阶段
  next();
}
1
2
3
4
5
6

Handler内置的异常捕获套件将自动作用在实例方法initHandler维度。即:当initHandler执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据Handler初始化阶段中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的initHandler

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当Handler初始化逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

当然,不要忘记在Handler初始化阶段结束时使用流程控制函数分发处理流程至下一阶段。


接下来,我们将实现一个在处理请求时打印请求处理开始时间的Handler。

首先,创建一个用于模拟耗时同步和异步任务BaseHandler作为基类:

class BaseHandler extends Core.Handler {
  // 异步耗时任务
  asyncTask(duration = 0) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), duration);
    });
  }

  // 同步耗时任务
  syncTask(duration = 0) {
    const startDate = new Date();
    while (true) {
      if (((new Date()) - startDate) > duration) {
        break;
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

接下来,基于BaseHandler实现在请求处理时打印开始时间的自定义Handler

对于只包含同步行为的Handler初始化逻辑,我们使用Function类型的initHandler

class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  initHandler(req, res, next) {
    // 记录请求时间
    this.startDate = new Date();
    // 创建日志输出器并打印日志
    this.logger = new Core.BaseLogger();
    this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
    // 执行1000ms的同步任务
    this.syncTask(1000);
    next();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

同样,当Handler初始化逻辑中包含异步行为时,我们使用AsyncFunction类型的initHandler,并通过await关键字执行异步任务






 





 
 




class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  async initHandler(req, res, next) {
    // 记录请求时间
    this.startDate = new Date();
    // 创建日志输出器并打印日志
    this.logger = new Core.BaseLogger();
    this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    next();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

最后,我们将自定义Handler挂载至ServiceCore并启动服务:

const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
1
2
3

最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do -w 'res -> %{http_code}\n'检查实现效果:

  • 服务端控制台将于收到请求时打印请求开始处理时间的相关日志。

  • 客户端控制台将于请求发起后约1000ms收到ServiceCore返回的404状态码。

# Handler析构

我们通过重写Handler的实例方法destroyHandler()以指定Handler析构阶段执行的逻辑。

注意

进入Handler析构阶段时,客户端请求已返回处理结果。

因此,我们在Handler析构阶段仅允许对客户端请求实例req客户端返回实例res发起读取动作。

通常,我们在Handler析构阶段释放对Handler初始化阶段创建资源的引用。

当然,不要忘记在Handler初始化阶段结束时使用流程控制函数分发处理流程至下一阶段。

同样,Handler内置的异常捕获套件也将自动作用在实例方法destroyHandler维度。即:当destroyHandler执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据Handler析构阶段中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的destroyHandler

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当Handler析构逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

Handler析构阶段标志着Handler生命周期的终结,其中无法使用流程控制函数


接下来,我们将完善Handler初始化阶段中的🌰,使其在请求处理结束时执行任务并打印处理耗时。

对于只包含同步行为的Handler析构逻辑,我们使用Function类型的destroyHandler

















 
 
 
 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  initHandler(req, res, next) {
    // 记录请求时间
    this.startDate = new Date();
    // 创建日志输出器并打印日志
    this.logger = new Core.BaseLogger();
    this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
    // 执行1000ms的同步任务
    this.syncTask(1000);
    next();
  }

  destroyHandler(req, res) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 计算并打印日志
    const duration = (new Date()) - this.startDate;
    this.logger.log(`请求处理耗时[${duration}]ms`);
    // 关闭日志输出器
    this.logger.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

同样,当Handler析构逻辑中包含异步行为时,我们使用AsyncFunction类型的destroyHandler,并通过await关键字执行异步任务

















 
 
 
 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  async initHandler(req, res, next) {
    // 记录请求时间
    this.startDate = new Date();
    // 创建日志输出器并打印日志
    this.logger = new Core.BaseLogger();
    this.logger.log(`请求处理开始时间:[${this.startDate.toLocaleString()}]`);
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    next();
  }

  async destroyHandler(req, res) {
    // 执行1000ms的同步任务
    await this.asyncTask(1000);
    // 计算并打印日志
    const duration = (new Date()) - this.startDate;
    this.logger.log(`请求处理耗时[${duration}]ms`);
    // 关闭日志输出器
    this.logger.close();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

最后,我们打开控制台,使用curl http://localhost:3000/Test.do -w 'res -> %{http_code}\n'检查实现效果:

  • 服务端控制台将于收到请求时打印请求开始处理时间的相关日志。

  • 服务端控制台将于收到请求后约2000ms打印请求处理时长的相关日志。

  • 客户端控制台将于请求发起后约1000ms收到ServiceCore返回的404状态码。

# 中间件系统

Handler使用的中间件系统兼容Express生态,且支持动态中间件

我们通过重写Handler的实例方法getMiddlewares以指定其应用于客户端请求的中间件列表。

提示

Handler初始化阶段完成后,Handler将构建并执行中间件列表

需要注意的是:Handler维度指定的中间件将仅作用于匹配Handler请求路径规则的客户端请求。

同样,Handler内置的异常捕获套件也将自动作用在实例方法getMiddlewares维度。即:当getMiddlewares执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据构建中间件列表时实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的getMiddlewares

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当构建中间件列表逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

构建中间件列表时无需使用流程控制函数控制处理流程,使用return关键字返回应用于客户端请求处理的中间件列表即可。


接下来,我们将实现一个在请求处理过程中动态指定中间件列表的Handler。

首先,创建一个用于模拟耗时同步、异步任务和创建中间件BaseHandler作为基类:

class BaseHandler extends Core.Handler {
  // 异步任务
  asyncTask(duration = 0) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), duration);
    });
  }

  // 同步任务
  syncTask(duration = 0) {
    const startDate = new Date();
    while (true) {
      if (((new Date()) - startDate) > duration) {
        break;
      }
    }
  }

  // 创建中间件
  createMiddleware(value) {
    // 将在res.header中自动记录应用于请求处理的中间件
    return (req, res, next) => {
      const middlewareFieldKey = 'x-middlewares';
      const middlewareFieldValue = res.get(middlewareFieldKey);
      const middlewareList = middlewareFieldValue ? middlewareFieldValue.split(',') : [];
      middlewareList.push(`middleware_${value}`);
      res.set(middlewareFieldKey, middlewareList.join(','));
      next();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

接下来,基于BaseHandler实现根据客户端请求入参动态指定中间件列表的自定义Handler

对于只包含同步行为的构建中间件列表逻辑,我们使用Function类型的getMiddlewares

class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }
  
  getMiddlewares(req, res) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 构建中间件列表
    const middlewares = [];
    const { count = 0 } = req.query;
    for (let i = 1; i < parseInt(count) + 1; i++) {
      middlewares.push(this.createMiddleware(i));
    }
    return middlewares;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

同样,当构建中间件列表逻辑中包含异步行为时,我们使用AsyncFunction类型的getMiddlewares,并通过await关键字执行异步任务






 
 
 










class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }
  
  async getMiddlewares(req, res) {
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    // 构建中间件列表
    const middlewares = [];
    const { count = 0 } = req.query;
    for (let i = 1; i < parseInt(count) + 1; i++) {
      middlewares.push(this.createMiddleware(i));
    }
    return middlewares;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

最后,我们将自定义Handler挂载至ServiceCore并启动服务:

const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
1
2
3

最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?count\=5 -D -检查实现效果:

  • 客户端控制台将于请求发起后约1000ms打印请求处理结果。

  • 客户端收到的请求处理结果中res.header['x-middlewares']记录了5个中间件的信息

# 中间件拦截

Handler构建中间件列表完成后,将逐个分发其中的中间件。

我们通过重写实例方法onInterceptMiddleware以拦截每个中间件的分发以动态控制其执行行为。

同样,Handler内置的异常捕获套件也将自动作用在实例方法onInterceptMiddleware维度。即:当onInterceptMiddleware执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据中间件拦截时实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的onInterceptMiddleware

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当中间件拦截逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

Handler将自动包装当前分发的中间件中间件执行函数作为onInterceptMiddleware参数列表的第一个参数middleware

  • middleware.type:中间件本体函数,用于进行中间件标识校验。

  • middleware.exec:中间件执行函数,用于实际执行中间件,其参数列表为(callBack)

    提示

    middleware.exec(callBack)实际上是middleware.type(req, res, callBack)的语法糖,中间件执行过程中产生的异常将通过callBack回抛。

    我们可以通过require('util').promisify包装middleware.exec,并使用await关键字调用,以保证在AsyncFuntion类型的onInterceptMiddleware中的逻辑一致性。

当然,在对中间件进行拦截处理后,不要忘记使用流程控制函数分发处理流程至下一阶段。


接下来,我们将完善构建中间件列表时的🌰,以50%的概率随机执行中间件列表中的中间件。

对于只包含同步行为的中间件拦截逻辑,我们使用Function类型的onInterceptMiddleware


















 
 
 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }
  
  getMiddlewares(req, res) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 构建中间件列表
    const middlewares = [];
    const { count = 0 } = req.query;
    for (let i = 1; i < parseInt(count) + 1; i++) {
      middlewares.push(this.createMiddleware(i));
    }
    return middlewares;
  }

  onInterceptMiddleware(middleware, req, res, next) {
    // 每个中间件都执行500ms的同步任务
    this.syncTask(500);
    // 计算概率并应用至执行链路
    const { exec } = middleware;
    const canExec = Math.random() >= 0.5;
    canExec ? exec((result) => next(result)) : next();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

同样,当构中间件拦截逻辑中包含异步行为时,我们使用AsyncFunction类型的onInterceptMiddleware,并通过await关键字执行异步任务




















 
 
 
 
 
 
 
 
 
 


const { promisify } = require('util');

class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  async getMiddlewares(req, res) {
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    // 构建中间件列表
    const middlewares = [];
    const { count = 0 } = req.query;
    for (let i = 1; i < parseInt(count) + 1; i++) {
      middlewares.push(this.createMiddleware(i));
    }
    return middlewares;
  }

  async onInterceptMiddleware(middleware, req, res, next) {
    // 每个中间件都执行500ms的异步任务
    await this.asyncTask(500);
    // 计算概率并应用至执行链路
    const { exec } = middleware;
    const canExec = Math.random() >= 0.5;
    canExec
      ? next(await promisify(exec)())
      : next();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?count\=5 -D -检查实现效果:

  • 客户端控制台将于请求发起后约3500ms打印请求处理结果。

  • 客户端收到的请求处理结果中res.header['x-middlewares']随机记录了0-5个中间件的信息

# 请求预处理

Handler维度的中间件列表内最后一个中间件的拦截逻辑执行完成后,将进入请求预处理阶段。

我们通过重写Handler的实例方法preHandler()以指定请求预处理阶段执行的逻辑。

提示

通常,我们在请求预处理阶段执行实际业务处理前的准备动作。

比如,在实现支持客户端使用多种请求方式的自定义Handler时,可以在请求预处理阶段对不同请求方式带入的参数进行归并统一。

或是提前创建请求处理所需的基础资源

我们通常在请求后处理阶段对请求参数进行有效性校验并驳回参数无效的客户端请求。因此,出于性能考虑,我们应在请求预处理阶段创建与客户端请求类型无关的基础资源。

同样,Handler内置的异常捕获套件也将自动作用在实例方法preHandler维度。即:当preHandler执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的preHandler

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当请求预处理逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

当然,不要忘记在请求预处理阶段结束时使用流程控制函数分发处理流程至下一阶段。


接下来,我们将实现一个在预处理阶段归并请求参数并向客户端返回的Handler。

首先,创建一个用于模拟耗时同步和异步任务BaseHandler作为基类:

class BaseHandler extends Core.Handler {
  // 异步耗时任务
  asyncTask(duration = 0) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), duration);
    });
  }

  // 同步耗时任务
  syncTask(duration = 0) {
    const startDate = new Date();
    while (true) {
      if (((new Date()) - startDate) > duration) {
        break;
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

接下来,基于BaseHandler实现自动归并GET方式POST方式中客户端附加至querybody中参数的自定义Handler

对于只包含同步行为的请求预处理逻辑,我们使用Function类型的preHandler

const bodyParser = require('body-parser');
const jsonParserMiddleware = bodyParser.json({ limit: 2 * 1024 * 1024 });
const qsParserMiddleware = bodyParser.urlencoded({ limit: 2 * 1024 * 1024, extended: true });

class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  getMiddlewares() {
    // 挂载解析HTTP BODY的中间件
    return [jsonParserMiddleware, qsParserMiddleware];
  }

  preHandler(req, res, next) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 合并query和body
    const body = req.body;
    const query = req.query;
    req.requestParams = Object.assign({}, body, query);
    // 向客户端返回
    next(req.requestParams);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

同样,当请求预处理逻辑中包含异步行为时,我们使用AsyncFunction类型的preHandler,并通过await关键字执行异步任务















 
 
 









const bodyParser = require('body-parser');
const jsonParserMiddleware = bodyParser.json({ limit: 2 * 1024 * 1024 });
const qsParserMiddleware = bodyParser.urlencoded({ limit: 2 * 1024 * 1024, extended: true });

class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  getMiddlewares() {
    // 挂载解析HTTP BODY的中间件
    return [jsonParserMiddleware, qsParserMiddleware];
  }

  async preHandler(req, res, next) {
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    // 合并query和body
    const body = req.body;
    const query = req.query;
    req.requestParams = Object.assign({}, body, query);
    // 向客户端返回
    next(req.requestParams);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

最后,我们将自定义Handler挂载至ServiceCore并启动服务:

const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
1
2
3

最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?queryKey1\=queryValue1\&queryKey2\=queryValue2 -d 'bodyKey1=bodyValue1&bodyKey2=bodyValue2'检查实现效果:

  • 客户端控制台将于请求发起后约1000ms打印请求处理结果。

  • 客户端收到的请求处理结果中将包含querybody中附带的参数

# 请求后处理

请求预处理阶段使用流程控制函数分发至下一处理阶段时,Handler将尝试调用与客户端请求方式匹配的实例方法进入请求后处理阶段。

我们通过重写Handler中与请求方式对应的实例方法以指定其请求后处理逻辑。比如,实现实例方法postHandler将指定客户端POST请求对应的后处理逻辑。

提示

在进入请求后处理阶段时,如果Handler中没有实现与请求方式对应的实例方法,默认使用defaultHandler作为后处理逻辑。

默认的defaultHandler逻辑中,将直接使用流程控制函数执行next(404)向客户端返回404状态码。

同样,Handler内置的异常捕获套件也将自动作用在[METHOD]Handler维度。即:全部请求方式对应的实例方法defaultHandler执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据请求后处理中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的[METHOD]Handler

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当请求后处理逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。

通常,在请求后处理阶段首先应对客户端附带的参数进行有效性判断,驳回参数无效的客户端请求;在请求参数有效时,触发实际的业务处理。

需要注意的是,请求后处理阶段为请求处理链路的末端节点,通常应使用流程控制函数向客户端发起应答动作。另外,在请求后处理阶段执行next()时不会继续分发处理流程至下一阶段,而是进入统一完成处理


接下来,我们将实现一个支持客户端通过POST请求读取本地指定目录的Handler。

对于只包含同步行为的请求后处理逻辑,我们使用Function类型的postHandler

const fs = require('fs');

class Handler extends Core.Handler {
  static getRoutePath() {
    return '/Test.do';
  }

  postHandler(req, res, next) {
    const { path } = req.query;
    try {
      // 同步读取目录内容并向客户端返回结果
      const result = fs.readdirSync(path);
      next(result);
    } catch (error) {
      // 执行过程中产生异常时向客户端返回异常信息
      next(error.message);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction类型的postHandler,并通过await关键字执行异步任务









 
 
 
 
 
 
 
 
 


const fs = require('fs');
const { promisify } = require('util');

class Handler extends Core.Handler {
  static getRoutePath() {
    return '/Test.do';
  }

  async postHandler(req, res, next) {
    const { path } = req.query;
    // 异步读取目录内容并向客户端返回结果
    const result = await promisify(fs.readdir)(path).catch((error) => {
      // 执行过程中产生异常时向客户端返回异常信息
      next(error.message);
    });
    result && next(result);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最后,我们将自定义Handler挂载至ServiceCore并启动服务:

const serviceCore = new Core.ServiceCore();
serviceCore.bind([Handler]);
serviceCore.start();
1
2
3

最后的最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[本地目录绝对路径]检查实现效果:

  • 当指定的本地目录绝对路径存在时,客户端收到的处理结果为本地目录中的内容

  • 当未指定本地目录绝对路径本地目录绝对路径不存在时,客户端收到的处理结果为异常原因

注意

样例代码中使用的异常处理方式不推荐在实际业务代码中使用。对于如何优雅的处理异常,我们将在统一错误处理中进行详细讨论。

# 统一完成处理

任意请求处理阶段中使用流程控制函数执行next(data)时将触发统一完成处理

我们通过重写Handler的实例方法onFinish()以指定统一完成处理阶段执行的逻辑。

说明

我们通常在统一完成处理阶段完成对客户端返回内容的统一结构组装,并操作客户端返回实例res向客户端返回处理结果。

需要注意的是,直接操作客户端返回实例res发起返回动作将跳过统一完成处理阶段直接触发Handler析构

因此,我们在请求处理过程中期望向客户端返回处理结果时应使用流程控制函数,而不是直接操作客户端返回实例res

同样,Handler内置的异常捕获套件也将自动作用在实例方法onFinish维度。即:当onFinish执行过程中产生了未被捕获的异常时将自动进入统一错误处理

因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的onFinish

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当统一完成处理逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。


接下来,我们将完善请求后处理中的🌰,为其向客户端的返回内容添加统一结构。

首先,创建一个用于模拟耗时同步和异步任务BaseHandler作为基类:

class BaseHandler extends Core.Handler {
  // 异步耗时任务
  asyncTask(duration = 0) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), duration);
    });
  }

  // 同步耗时任务
  syncTask(duration = 0) {
    const startDate = new Date();
    while (true) {
      if (((new Date()) - startDate) > duration) {
        break;
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

提示

通常,我们不需要完全重写onFinish中包含的逻辑,而是使用super.onFinish()复用默认的统一完成处理逻辑。

对于统一完成处理默认支持的行为,我们可以参考流程控制函数

接下来,我们将自定义Handler继承的基类变更为BaseHandler,并在统一完成处理中完成报文结构包装:

对于只包含同步行为的请求预处理逻辑,我们使用Function类型的onFinish

 
















 
 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  postHandler(req, res, next) {
    const { path } = req.query;
    try {
      // 同步读取目录内容并向客户端返回结果
      const result = fs.readdirSync(path);
      next(result);
    } catch (error) {
      // 执行过程中产生异常时向客户端返回异常信息
      next(error.message);
    }
  }

  onFinish(data, req, res) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 构造报文结构并执行原始的onFinish()方法向客户端返回
    const backMessage = { code: 0, data };
    super.onFinish(backMessage, req, res);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction类型的onFinish,并通过await关键字执行异步任务














 
 
 






class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  async postHandler(req, res, next) {
    const { path } = req.query;
    const result = await promisify(fs.readdir)(path).catch((error) => {
      next(error.message);
    });
    result && next(result);
  }

  async onFinish(data, req, res) {
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    // 构造报文结构并执行原始的onFinish()方法向客户端返回
    const backMessage = { code: 0, data };
    super.onFinish(backMessage, req, res);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[本地目录绝对路径]检查实现效果:

  • 客户端控制台将于请求发起后约1000ms打印请求处理结果,且处理结果拥有统一的结构{ code: 0, data: [处理结果] }

  • 当指定的本地目录绝对路径存在时,客户端收到的处理结果为本地目录中的内容

  • 当未指定本地目录绝对路径本地目录绝对路径不存在时,客户端收到的处理结果为异常原因

注意

样例代码中使用的异常处理方式不推荐在实际业务代码中使用。对于如何优雅的处理异常,我们将在统一错误处理中进行详细讨论。

# 统一错误处理

任意请求处理阶段的根函数中产生未被捕获的异常时,Handler将自动引导处理流程进入统一完成处理阶段。

另外,我们可以在请求处理任意阶段中使用流程控制函数执行next(error)手动触发统一完成处理

我们通过重写Handler的实例方法onError以指定统一完成处理阶段执行的逻辑。

说明

我们通常在统一错误处理阶段处理异常后,直接操作客户端返回实例res向客户端返回处理结果。

同样,Handler内置的异常捕获套件也将自动作用在实例方法onError维度。与其他处理阶段不同的是,onError执行过程中产生了未被捕获的异常时将进入ServiceCore的错误拦截器

因此,我们应根据请求预处理中实际逻辑的同步或异步,选择使用FunctionAsyncFunction类型的onFinish

注意

Handler内置的异常捕获机制仅在请求处理节点的根函数中产生未被捕获的异常时才产生效力。

因此,当统一错误处理逻辑中包含异步任务时,我们可以使用async/awaitPromise处理异步任务,以保证异常可以正常抛出。


接下来,我们将使用更优雅的异常处理方案来完善请求后处理中的🌰。

首先,创建一个用于模拟耗时同步和异步任务BaseHandler作为基类:

class BaseHandler extends Core.Handler {
  // 异步耗时任务
  asyncTask(duration = 0) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), duration);
    });
  }

  // 同步耗时任务
  syncTask(duration = 0) {
    const startDate = new Date();
    while (true) {
      if (((new Date()) - startDate) > duration) {
        break;
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

接下来,我们将自定义Handler继承的基类变更为BaseHandler,删除Handler后处理中异常处理相关的代码,并在统一错误处理中收口异常处理:

对于只包含同步行为的请求预处理逻辑,我们使用Function类型的onError

 




 
 
 
 
 
 

 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  postHandler(req, res, next) {
    const { path } = req.query;
    // 同步读取目录内容并向客户端返回结果
    const result = fs.readdirSync(path);
    next(result);
  }

  onError(error, req, res) {
    // 执行1000ms的同步任务
    this.syncTask(1000);
    // 向客户端返回异常信息
    res.status(500).send(error.message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

同样,当请求后处理逻辑中包含异步行为时,我们使用AsyncFunction类型的onError,并通过await关键字执行异步任务

 




 
 
 
 
 

 
 
 
 
 
 


class Handler extends BaseHandler {
  static getRoutePath() {
    return '/Test.do';
  }

  async postHandler(req, res, next) {
    const { path } = req.query;
    const result = await promisify(fs.readdir)(path);
    next(result);
  }

  async onError(error, req, res) {
    // 执行1000ms的异步任务
    await this.asyncTask(1000);
    // 向客户端返回异常信息
    res.status(500).send(error.message);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最后,我们打开控制台,使用curl http://localhost:3000/Test.do\?path\=[不存在的本地目录绝对路径]检查实现效果:

  • 客户端控制台将于请求发起后约1000ms打印请求处理结果,处理结果为异常原因