开发自定义插件

您正在查看 Apigee Edge 文档。
前往 Apigee X 文档
信息

Edge Microgateway v. 3.0.x

受众群体

本主题面向希望通过编写自定义插件来扩展 Edge Microgateway 功能的开发者。如果您想编写新插件,则需要具备 JavaScript 和 Node.js 方面的经验。

什么是自定义 Edge Microgateway 插件?

插件是一种 Node.js 模块,可为 Edge Microgateway 添加功能。插件模块遵循一致的模式,并存储在 Edge Microgateway 已知的位置,因此可以自动发现和运行。安装 Edge Microgateway 时,系统会提供多个预定义插件。这些插件包括用于身份验证、峰值抑制、配额和分析的插件。使用插件中介绍了这些现有插件。

您可以编写自定义插件,为微网关添加新功能。默认情况下,Edge Microgateway 本质上是一个安全的直通代理,可将请求和响应原封不动地传递给目标服务,并从目标服务传递回来。借助自定义插件,您可以以编程方式与流经微网关的请求和响应进行交互。

自定义插件代码应放在何处

自定义插件的文件夹包含在 Edge Microgateway 安装中,如下所示:

[prefix]/lib/node_modules/edgemicro/node_modules/microgateway-plugins

其中,[prefix]安装 Edge Microgateway 中“Edge Microgateway 安装在何处”部分所述的 npm 前缀目录。

您可以更改此默认插件目录。请参阅在哪里查找插件

查看预定义的插件

在尝试开发自己的插件之前,最好先检查一下预定义的插件是否满足您的要求。这些插件位于以下位置:

[prefix]/lib/node_modules/edgemicro/node_modules/microgateway-plugins

其中,[prefix]npm 前缀目录。另请参阅安装 Edge Microgateway 中的“Edge Microgateway 安装在何处”。

如需了解详情,另请参阅 Edge Microgateway 随附的预定义插件

编写一个简单的插件

在本部分中,我们将逐步介绍创建简单插件所需的步骤。此插件会使用字符串“Hello, World!”替换响应数据(无论是什么),并将其输出到终端。

  1. 如果 Edge Microgateway 正在运行,请立即停止:
    edgemicro stop
  2. cd 到自定义插件目录:

    cd [prefix]/lib/node_modules/edgemicro/plugins

    其中,[prefix]安装 Edge Microgateway 中“Edge Microgateway 安装在何处”一节中所述的 npm 前缀目录。

  3. 创建一个名为 response-override 的新插件项目,并使用 cd 命令进入该项目:
    mkdir response-override && cd response-override
  4. 创建新的 Node.js 项目:
    npm init
    多次按 Return 键以接受默认值。
  5. 使用文本编辑器创建名为 index.js 的新文件。
  6. 将以下代码复制到 index.js 中,然后保存该文件。
    'use strict';
    var debug = require('debug')
    
    module.exports.init = function(config, logger, stats) {
    
      return {
       
        ondata_response: function(req, res, data, next) {
          debug('***** plugin ondata_response');
          next(null, null);
        },
        
        onend_response: function(req, res, data, next) {
          debug('***** plugin onend_response');
          next(null, "Hello, World!\n\n");
        }
      };
    }
  7. 现在,您已创建插件,接下来需要将其添加到 Edge Microgateway 配置中。 打开文件 $HOME/.edgemicro/[org]-[env]-config.yaml,其中 orgenv 是您的 Edge 组织和环境名称。
  8. response-override 插件添加到 plugins:sequence 元素,如下所示。
          ...
          
          plugins:
            dir: ../plugins
            sequence:
              - oauth
              - response-override
              
          ...
  9. 重启 Edge Microgateway。
  10. 通过 Edge Microgateway 调用 API。(此 API 调用假设您已设置与本教程相同的配置,并采用 API 密钥安全性,如设置和配置 Edge Microgateway 中所述:
    curl -H 'x-api-key: uAM4gBSb6YoMvTHfx5lXJizYIpr5Jd' http://localhost:8000/hello/echo
    Hello, World!

插件结构

以下 Edge Microgateway 插件示例展示了开发自有插件时应遵循的模式。本部分讨论的示例插件的源代码位于 plugins/header-uppercase/index.js.

  • 插件是标准 NPM 模块,根文件夹中包含 package.jsonindex.js
  • 插件必须导出 init() 函数。
  • init() 函数接受三个实参:configloggerstats。这些实参在插件 init() 函数实参中进行了说明。
  • init() 会返回一个包含命名函数处理程序的对象,当请求生命周期内发生特定事件时,系统会调用这些处理程序。

事件处理脚本

插件必须实现这些事件处理脚本中的部分或全部。这些函数的实现由您自行决定。任何给定函数都是可选的,而典型的插件至少会实现这些函数的一部分。

请求流程事件处理程序

这些函数在 Edge Microgateway 中的请求事件上调用。

  • onrequest
  • ondata_request
  • onend_request
  • onclose_request
  • onerror_request

onrequest 函数

在客户端请求开始时调用。当 Edge Microgateway 收到请求的第一个字节时,此函数会触发。此函数可让您访问请求标头、网址、查询参数和 HTTP 方法。如果您使用真值作为第一个实参(例如 Error 的实例)调用 next,则请求处理会停止,并且不会启动目标请求。

示例:

onrequest: function(req, res, next) {
      debug('plugin onrequest');
      req.headers['x-foo-request-start'] = Date.now();
      next();
    }

ondata_request 函数

在从客户端收到数据块时调用。将请求数据传递给插件序列中的下一个插件。序列中最后一个插件的返回值会发送到目标。一个典型的使用场景(如下所示)是在将请求数据发送到目标之前对其进行转换。

示例:

ondata_request: function(req, res, data, next) {
      debug('plugin ondata_request ' + data.length);
      var transformed = data.toString().toUpperCase();
      next(null, transformed);
    }

onend_request 函数

当已从客户端收到所有请求数据时调用。

示例:

onend_request: function(req, res, data, next) {
      debug('plugin onend_request');
      next(null, data);
    }

onclose_request 函数

表示客户端连接已关闭。如果客户端连接不可靠,您可以使用此函数。当与客户端的套接字连接关闭时,系统会调用此方法。

示例:

onclose_request: function(req, res, next) {
      debug('plugin onclose_request');
      next();
    }

onerror_request 函数

如果在接收客户端请求时发生错误,则调用此方法。

示例:

onerror_request: function(req, res, err, next) {
      debug('plugin onerror_request ' + err);
      next();
    }

响应流事件处理程序

这些函数会在 Edge Microgateway 中的响应事件上调用。

  • onresponse
  • ondata_response
  • onend_response
  • onclose_response
  • onerror_response

onresponse 函数

在目标响应开始时调用。当 Edge Microgateway 收到响应的第一个字节时,此函数会触发。此函数可让您访问响应标头和状态代码。

示例:

onresponse: function(req, res, next) {      
    debug('plugin onresponse');     
    res.setHeader('x-foo-response-time', Date.now() - req.headers['x-foo-request-start'])    
    next();    
}


ondata_response 函数

在从目标接收到数据块时调用。

示例:

ondata_response: function(req, res, data, next) {
      debug('plugin ondata_response ' + data.length);
      var transformed = data.toString().toUpperCase();
      next(null, transformed);
    }


onend_response 函数

当从目标接收到所有响应数据时调用。

示例:

onend_response: function(req, res, data, next) {
      debug('plugin onend_response');
      next(null, data);
    }

onclose_response 函数

表示目标连接已关闭。如果目标连接不可靠,您可以使用此函数。当与目标的套接字连接关闭时调用。

示例:

onclose_response: function(req, res, next) {
      debug('plugin onclose_response');
      next();
    }


onerror_response 函数

如果在接收目标响应时出错,则调用此方法。

示例:

onerror_response: function(req, res, err, next) {
      debug('plugin onerror_response ' + err);
      next();
    }

插件事件处理函数须知事项

插件事件处理脚本会在 Edge Microgateway 处理给定的 API 请求时,响应发生的特定事件而被调用。

  • 每个 init() 函数处理程序(ondata_requestondata_response 等)在完成处理后都必须调用 next() 回调。如果您不调用 next(),处理将会停止,并且请求会挂起。
  • next() 的第一个实参可能是错误,这会导致请求处理终止。
  • ondata_onend_ 处理程序必须使用包含要传递给目标或客户端的数据的第二个实参来调用 next()。如果插件正在缓冲,但目前没有足够的数据进行转换,则此实参可以为 null。
  • 请注意,插件的单个实例用于处理所有请求和响应。 如果插件希望在调用之间保留每个请求的状态,它可以将该状态保存在添加到所提供的 request 对象 (req) 的属性中,该对象的生命周期为 API 调用的持续时间。
  • 请务必捕获所有错误,并使用该错误调用 next()。如果未能调用 next(),会导致 API 调用挂起。
  • 请注意不要引入内存泄漏,因为这会影响 Edge Microgateway 的整体性能,并在内存不足时导致其崩溃。
  • 请务必遵循 Node.js 模型,不要在主线程中执行计算密集型任务,因为这可能会对 Edge Microgateway 的性能产生不利影响。

关于插件 init() 函数

本部分介绍传递给 init() 函数的实参:configloggerstats

config

在将 Edge Microgateway 配置文件与从 Apigee Edge 下载的信息(例如产品和配额)合并后获得的配置对象。您可以在以下对象中找到特定于插件的配置:config.<plugin-name>

如需向名为 response-override 的插件添加值为 foo 的配置参数 param,请将以下内容放入 default.yaml 文件中:

response-override:
    param: foo

然后,您可以在插件代码中访问该参数,如下所示:

// Called when response data is received
    ondata_response: function(req, res, data, next) {
      debug('***** plugin ondata_response');
      debug('***** plugin ondata_response: config.param: ' + config.param);
      next(null, data);
    },

在这种情况下,您将在插件调试输出中看到“foo”:

Sun, 13 Dec 2015 21:25:08 GMT plugin:response-override ***** plugin ondata_response: config.param: foo

logger

系统记录器。当前使用的记录器会导出这些函数,其中 object 可以是字符串、HTTP 请求、HTTP 响应或 Error 实例。

  • info(object, message)
  • warn(object, message)
  • error(object, message)

stats

一个对象,用于保存与流经微网关实例的请求和响应相关的请求数、响应数、错误数和其他汇总统计信息。

  • treqErrors - 出现错误的目标请求数。
  • treqErrors - 出现错误的目标响应数。
  • statusCodes - 一个包含响应代码计数的对象:
{
  1: number of target responses with 1xx response codes
  2: number of target responses with 2xx response codes
  3: number of target responses with 3xx response codes
  4: number of target responses with 4xx response codes
  5: number of target responses with 5xx response codes
  }
  
  • 请求数 - 请求总数。
  • 回答数 - 回答总数。
  • 连接数 - 活跃目标连接数。

next() 函数简介

所有插件方法都必须调用 next() 才能继续处理序列中的下一个方法(否则插件进程会挂起)。在请求生命周期中,第一个被调用的方法是 onrequest()。接下来要调用的方法是 ondata_request() 方法;不过,只有当请求包含数据时(例如在 POST 请求的情况下),才会调用 ondata_request接下来调用的方法是 onend_request(),该方法会在请求处理完成后调用。onerror_* 函数仅在发生错误时调用,并且允许您使用自定义代码处理错误(如果需要)。

假设在请求中发送了数据,并调用了 ondata_request()。请注意,该函数使用两个参数调用了 next()

next(null, data);

按照惯例,第一个参数用于传递错误信息,然后您可以在链中的后续函数中处理该信息。通过将其设置为 null(一个虚值实参),我们表示没有错误,请求处理应正常进行。如果此实参为真值(例如 Error 对象),则请求处理会停止,并且请求会发送到目标。

第二个参数将请求数据传递给链中的下一个函数。如果您不进行任何其他处理,请求数据将原封不动地传递给 API 的目标。不过,您可以在此方法中修改请求数据,并将修改后的请求传递给目标。例如,如果请求数据是 XML,而目标需要 JSON,那么您可以向 ondata_request() 方法添加代码,以执行以下操作:(a) 将请求标头的 Content-Type 更改为 application/json,并使用您希望的任何方式(例如,您可以使用从 NPM 获取的 Node.js xml2json 转换器)将请求数据转换为 JSON。

我们来看看效果如何:

ondata_request: function(req, res, data, next) {
  debug('****** plugin ondata_request');
  var translated_data = parser.toJson(data);
  next(null, translated_data);
},

在这种情况下,请求数据(假设为 XML)会转换为 JSON,转换后的数据会通过 next() 传递给请求链中的下一个函数,然后再传递给后端目标。

请注意,您可以添加另一个调试语句来打印转换后的数据,以用于调试。例如:

ondata_request: function(req, res, data, next) {
  debug('****** plugin ondata_request');
  var translated_data = parser.toJson(data);
  debug('****** plugin ondata_response: translated_json: ' + translated_json);
  next(null, translated_data);
},

关于插件处理程序的执行顺序

如果您为 Edge Microgateway 编写插件,则需要了解插件事件处理脚本的执行顺序。

请务必谨记,在 Edge Microgateway 配置文件中指定插件序列时,请求处理程序会按升序执行,而响应处理程序会按降序执行。

以下示例旨在帮助您了解此执行顺序。

1. 创建三个简单的插件

请考虑以下插件。它只会在其事件处理程序被调用时打印控制台输出:

plugins/plugin-1/index.js

module.exports.init = function(config, logger, stats) {

  return {

    onrequest: function(req, res, next) {
      console.log('plugin-1: onrequest');
      next();
    },

    onend_request: function(req, res, data, next) {
      console.log('plugin-1: onend_request');
      next(null, data);
    },

    ondata_response: function(req, res, data, next) {
      console.log('plugin-1: ondata_response ' + data.length);
      next(null, data);
    },

    onend_response: function(req, res, data, next) {
      console.log('plugin-1: onend_response');
      next(null, data);
    }
  };
}

现在,考虑再创建两个插件 plugin-2plugin-3,这两个插件的代码与 相同(只是将 console.log() 语句分别更改为 plugin-2plugin-3)。

2. 查看插件代码

<microgateway-root-dir>/plugins/plugin-1/index.js 中导出的插件函数是事件处理程序,在请求和响应处理期间的特定时间执行。例如,onrequest 在收到请求标头的第一个字节时执行。而 onend_response 在收到响应数据的最后一个字节后执行。

请查看处理程序 ondata_response,它会在每次收到一块响应数据时被调用。需要了解的重要一点是,响应数据不一定是一次性接收的。相反,数据可能会以任意长度的块接收。

3. 将插件添加到插件序列

继续沿用此示例,我们将插件添加到 Edge Microgateway 配置文件 (~./edgemicro/config.yaml) 中的插件序列,如下所示。顺序很重要。它定义了插件处理程序的执行顺序。

  plugins:
    dir: ../plugins
    sequence:
      - plugin-1
      - plugin-2
      - plugin-3
  

4. 检查调试输出

现在,我们来看看调用这些插件时会生成哪些输出。请务必注意以下几个要点:

  • Edge Microgateway 配置文件 (~./edgemicro/config.yaml) 指定的插件序列决定了事件处理程序的调用顺序。
  • 系统会按升序调用请求处理程序(即它们在插件序列中的显示顺序 - 1、2、3)。
  • 系统会按降序(3、2、1)调用响应处理程序。
  • 对于到达的每个数据块,系统会调用一次 ondata_response 处理程序。在此示例(输出如下所示)中,收到了两个块。

以下是使用这三个插件并通过 Edge Microgateway 发送请求时生成的调试输出示例。请注意处理程序的调用顺序:

  plugin-1: onrequest
  plugin-2: onrequest
  plugin-3: onrequest

  plugin-1: onend_request
  plugin-2: onend_request
  plugin-3: onend_request

  plugin-3: ondata_response 931
  plugin-2: ondata_response 931
  plugin-1: ondata_response 931

  plugin-3: ondata_response 1808
  plugin-3: onend_response

  plugin-2: ondata_response 1808
  plugin-2: onend_response

  plugin-1: ondata_response 1808
  plugin-1: onend_response

摘要

在尝试实现自定义插件功能(例如累积和转换请求或响应数据)时,了解插件处理程序的调用顺序非常重要。

请注意,请求处理程序的执行顺序与插件在 Edge Microgateway 配置文件中的指定顺序相同,而响应处理程序的执行顺序则相反。

关于在插件中使用全局变量

发送到 Edge Microgateway 的每个请求都会发送到同一插件实例;因此,来自另一个客户端的第二个请求的状态会覆盖第一个请求的状态。保存插件状态的唯一安全方式是将状态存储在请求或响应对象(其生命周期仅限于请求的生命周期)的属性中。

重写插件中的目标网址

添加于:v2.3.3

您可以在插件中通过修改插件代码中的以下变量来动态替换默认目标网址:req.targetHostnamereq.targetPath

添加于:v2.4.x

您还可以替换目标端点端口,并在 HTTP 和 HTTPS 之间进行选择。在插件代码中修改以下变量:req.targetPortreq.targetSecure。如需选择 HTTPS,请将 req.targetSecure 设置为 true;如需选择 HTTP,请将其设置为 false。如果您将 req.targetSecure 设置为 true,请参阅此讨论串了解详情。

我们已向 Edge Microgateway 添加了一个名为 eurekaclient 的示例插件。此插件演示了如何使用 req.targetPort 和 req.targetSecure 变量,并说明了 Edge Microgateway 如何使用 Eureka 作为服务端点目录来执行动态端点查找。


插件示例

这些插件随 Edge Microgateway 安装一起提供。您可以在 Edge Microgateway 安装中找到它们,具体位置如下:

[prefix]/lib/node_modules/edgemicro/plugins

其中,[prefix]安装 Edge Microgateway 中“Edge Microgateway 安装在何处”部分所述的 npm 前缀目录。

accumulate-request

此插件将来自客户端的数据块累积到附加到请求对象的数组属性中。当收到所有请求数据后,该数组会串联成一个缓冲区,然后传递给序列中的下一个插件。此插件应是序列中的第一个插件,以便后续插件接收累积的请求数据。

module.exports.init = function(config, logger, stats) {

  function accumulate(req, data) {

    if (!req._chunks) req._chunks = [];
    req._chunks.push(data);

  }

  return {

    ondata_request: function(req, res, data, next) {

      if (data && data.length > 0) accumulate(req, data);

      next(null, null);

    },


    onend_request: function(req, res, data, next) {

      if (data && data.length > 0) accumulate(req, data);

      var content = null;

      if (req._chunks && req._chunks.length) {

        content = Buffer.concat(req._chunks);

      }

      delete req._chunks;

      next(null, content);

    }

  };

}

accumulate-response

此插件会将目标中的数据块累积到附加到响应对象的数组属性中。当收到所有响应数据后,该数组会连接成一个缓冲区,然后传递给序列中的下一个插件。由于此插件对按相反顺序处理的响应进行操作,因此您应将其放置在序列中的最后一个位置。

module.exports.init = function(config, logger, stats) {

  function accumulate(res, data) {
    if (!res._chunks) res._chunks = [];
    res._chunks.push(data);
  }

  return {

    ondata_response: function(req, res, data, next) {
      if (data && data.length > 0) accumulate(res, data);
      next(null, null);
    },

    onend_response: function(req, res, data, next) {
      if (data && data.length > 0) accumulate(res, data);
      var content = Buffer.concat(res._chunks);
      delete res._chunks;
      next(null, content);
    }

  };

}

header-uppercase 插件

Edge Microgateway 分发版包含一个名为 <microgateway-root-dir>/plugins/header-uppercase 的示例插件。该示例包含描述每个函数处理程序的注释。此示例对目标响应进行了一些简单的数据转换,并向客户端请求和目标响应添加了自定义标头。

以下是 <microgateway-root-dir>/plugins/header-uppercase/index.js 的源代码:

'use strict';

var debug = require('debug')('plugin:header-uppercase');

// required
module.exports.init = function(config, logger, stats) {

  var counter = 0;

  return {

    // indicates start of client request
    // request headers, url, query params, method should be available at this time
    // request processing stops (and a target request is not initiated) if
    // next is called with a truthy first argument (an instance of Error, for example)
    onrequest: function(req, res, next) {
      debug('plugin onrequest');
      req.headers['x-foo-request-id'] = counter++;
      req.headers['x-foo-request-start'] = Date.now();
      next();
    },

    // indicates start of target response
    // response headers and status code should be available at this time
    onresponse: function(req, res, next) {
      debug('plugin onresponse');
      res.setHeader('x-foo-response-id', req.headers['x-foo-request-id']);
      res.setHeader('x-foo-response-time', Date.now() - req.headers['x-foo-request-start']);
      next();
    },

    // chunk of request body data received from client
    // should return (potentially) transformed data for next plugin in chain
    // the returned value from the last plugin in the chain is written to the target
    ondata_request: function(req, res, data, next) {
      debug('plugin ondata_request ' + data.length);
      var transformed = data.toString().toUpperCase();
      next(null, transformed);
    },

    // chunk of response body data received from target
    // should return (potentially) transformed data for next plugin in chain
    // the returned value from the last plugin in the chain is written to the client
    ondata_response: function(req, res, data, next) {
      debug('plugin ondata_response ' + data.length);
      var transformed = data.toString().toUpperCase();
      next(null, transformed);
    },

    // indicates end of client request
    onend_request: function(req, res, data, next) {
      debug('plugin onend_request');
      next(null, data);
    },

    // indicates end of target response
    onend_response: function(req, res, data, next) {
      debug('plugin onend_response');
      next(null, data);
    },

    // error receiving client request
    onerror_request: function(req, res, err, next) {
      debug('plugin onerror_request ' + err);
      next();
    },

    // error receiving target response
    onerror_response: function(req, res, err, next) {
      debug('plugin onerror_response ' + err);
      next();
    },

    // indicates client connection closed
    onclose_request: function(req, res, next) {
      debug('plugin onclose_request');
      next();
    },

    // indicates target connection closed
    onclose_response: function(req, res, next) {
      debug('plugin onclose_response');
      next();
    }

  };

}

transform-uppercase

这是一个常规转换插件,您可以对其进行修改,以实现所需的任何类型的转换。此示例只是将响应和请求数据转换为大写。

 */
module.exports.init = function(config, logger, stats) {

  // perform content transformation here
  // the result of the transformation must be another Buffer
  function transform(data) {
    return new Buffer(data.toString().toUpperCase());
  }

  return {

    ondata_response: function(req, res, data, next) {
      // transform each chunk as it is received
      next(null, data ? transform(data) : null);
    },

    onend_response: function(req, res, data, next) {
      // transform accumulated data, if any
      next(null, data ? transform(data) : null);
    },

    ondata_request: function(req, res, data, next) {
      // transform each chunk as it is received
      next(null, data ? transform(data) : null);
    },

    onend_request: function(req, res, data, next) {
      // transform accumulated data, if any
      next(null, data ? transform(data) : null);
    }

  };

}