开发自定义插件

您正在查看 Apigee Edge 文档。
转到 Apigee X 文档
信息

Edge Microgateway v. 3.3.x

观众群

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

什么是自定义 Edge Microgateway 插件?

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

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

自定义插件代码的放置位置

Edge Microgateway 安装过程的一部分包含自定义插件的文件夹,如下所示:

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

其中,[prefix]npm 前缀目录(如安装 Edge Microgateway 中的“Edge Microgateway 安装在哪里”中所述)。

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

查看预定义插件

在尝试开发自己的插件之前,最好检查一下是否没有任何预定义插件符合您的要求。这些插件位于:

[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]npm 前缀目录(如安装 Edge Microgateway 中的“Edge Microgateway 安装在哪里”中所述)。

  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 调用假定您已按照设置和配置 Edge Microgateway 中所述设置了与 API 密钥安全性相同的配置:
    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 的对象中。

要将名为 param 且值为 foo 的配置参数添加到名为 response-override 的插件中,请将以下内容放入 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

您可以在子对象 config.emgConfigs 中访问合并的微网关配置和下载的 Apigee Edge 数据。例如,您可以在 init 函数中访问此配置数据,如下所示:

module.exports.init = function(config, logger, stats) {
   let emgconfigs = config.emgConfigs;

以下是 emgConfigs 包含的数据的示例:

{
    edgemicro:
    {
        port: 8000,
        max_connections: 1000,
        config_change_poll_interval: 600,
        logging:
        {
            level: 'error',
            dir: '/var/tmp',
            stats_log_interval: 60,
            rotate_interval: 24,
            stack_trace: false
        },
        plugins: { sequence: [Array] },
        global: { org: 'Your Org', env: 'test' }
    },
    headers:
    {
        'x-forwarded-for': true,
        'x-forwarded-host': true,
        'x-request-id': true,
        'x-response-time': true,
        via: true
    },
    proxies:
    [    {
                max_connections: 1000,
                name: 'edgemicro_delayed',
                revision: '1',
                proxy_name: 'default',
                base_path: '/edgemicro_delayed',
                target_name: 'default',
                url: 'https://httpbin.org/delay/10',
                timeout: 0
            }
    ],
    product_to_proxy: { EdgeMicroTestProduct: [ 'edgemicro-auth','edgemicro_delayed',] },
    product_to_scopes: {prod4: [ 'Admin', 'Guest', 'Student' ] },
    product_to_api_resource: { EdgeMicroTestProduct: [ '/*' ] },
    _hash: 0,
    keys: { key: 'Your key', secret: 'Your key ' },
    uid: 'Internally generated uuid',
    targets: []
  }

logger

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

  • info(object, message)
  • warn(object, message)
  • error(object, message)
  • trace(object, message)
  • debug(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
  }
  
  • requests - 请求总数。
  • responses - 响应总数。
  • connections - 活跃目标连接的数量。

关于 next() 函数

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

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

next(null, data);

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

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

我们来看一下它是什么样的:

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,请参阅此讨论会话了解详情。

已从 v3.3.3 中移除

名为 eurekaclient 的示例插件已从 v.3.3.3 的 Edge Microgateway 中移除。请参阅版本说明

移除此功能不会影响 Edge 微网关的核心功能或重写目标网址。您可以在插件级别配置动态端点查找和替换 req.targetHostnamereq.targetPathreq.targetPortreq.targetSecure 等目标变量。请参阅在插件中重写目标网址


示例插件

这些插件随 Edge Microgateway 安装提供。您可以在 Edge Microgateway 安装中找到它们(如下所示):

[prefix]/lib/node_modules/edgemicro/plugins

其中,[prefix]npm 前缀目录(如安装 Edge Microgateway 中的“Edge Microgateway 安装在哪里”中所述)。

累积请求

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

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);

    }

  };

}

累积响应

此插件会将来自目标的数据区块累积到附加到响应对象的数组属性中。收到所有响应数据后,该数组会串联到一个 Buffer,然后传递给序列中的下一个插件。由于此插件对按倒序处理的响应运行,因此应将其放在序列中的最后一个插件的位置。

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();
    }

  };

}


转换为大写

这是一个常规转换插件,您可以修改该插件,执行任何所需的转换。本示例只是将响应和请求数据转换为大写。

 */
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);
    }

  };

}