增加APIKey请求头 + 完善全量注释 Groovy脚本(NiFi InvokeScriptedProcessor)
改动说明:
- 请求头新增
apikey,从FlowFile属性api.key获取(方便上游动态传入密钥,不硬编码) - 补充脚本头部说明注释
- 逐段增加清晰中文注释
- 兼容原有逻辑:空Body POST、解决411报错、规范NiFi数据流API、异常处理不变
groovy
/**
* Apache NiFi InvokeScriptedProcessor Groovy脚本
* 功能:发起【无请求体POST请求】调用外部HTTP接口
* 特性:
* 1. POST不带Body,主动设置Content-Length=0,规避服务端411 Length Required错误
* 2. 请求头携带 apikey,API地址、密钥均从FlowFile属性读取,支持动态配置
* 3. 接口返回内容覆盖写入当前FlowFile
* 4. 成功流向REL_SUCCESS,失败流向REL_FAILURE,自动写入各类状态属性便于排查
* 属性依赖(上游需要预先给FlowFile设置属性):
* api.url :目标接口地址
* api.key :接口鉴权apikey
* 输出属性:
* http.response.code HTTP响应码
* http.request.url 请求地址
* request.status success / failure
* request.error 简短错误信息(失败时)
* error.message 全局异常信息
* error.stack 异常堆栈信息
* 超时配置:连接超时60s,读取超时60s
* 编码:UTF-8,交互格式JSON
*/
import org.apache.nifi.processor.io.StreamCallback
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.exception.ExceptionUtils
import java.io.InputStream
import java.io.OutputStream
// 从NiFi会话获取流入的数据流文件FlowFile
def flowFile = session.get()
// ========== 配置区域 ==========
// 目标接口地址,从FlowFile属性api.url获取
def targetUrl = flowFile.getAttribute("api.url")
// 接口鉴权apikey,从FlowFile属性api.key获取
def apiKey = flowFile.getAttribute("api.key")
// HTTP连接超时 60000毫秒=60秒
def connectTimeout = 60000
// HTTP响应读取超时 60000毫秒=60秒
def readTimeout = 60000
// 统一字符编码 UTF-8
def charset = StandardCharsets.UTF_8
// 临时存放待写入FlowFile的属性
// 【重要约束】session.write回调内部禁止直接调用putAttribute,防止线程异常
def attrMap = [:]
// HTTP响应码初始值-1,代表未建立连接
def responseCode = -1
try {
// session.write:标准NiFi API,用于读取原FlowFile、生成新FlowFile内容
flowFile = session.write(flowFile, { InputStream inputStream, OutputStream outputStream ->
HttpURLConnection connection = null
try {
// 构建请求URL对象
URL url = new URL(targetUrl)
connection = (HttpURLConnection) url.openConnection()
// 请求方式POST
connection.setRequestMethod("POST")
// POST请求必须开启doOutput,即使没有请求体
connection.setDoOutput(true)
// 设置超时时间
connection.setConnectTimeout(connectTimeout)
connection.setReadTimeout(readTimeout)
// ========== HTTP 请求头配置 ==========
connection.setRequestProperty("Content-Type", "application/json;charset=" + charset.name())
connection.setRequestProperty("Accept", "application/json")
// 空POST关键配置:设置内容长度0,解决411报错
connection.setRequestProperty("Content-Length", "0")
// 新增鉴权头 apikey
connection.setRequestProperty("apikey", apiKey)
log.info("发起无请求体POST调用,URL:${targetUrl},已携带apikey请求头")
// 发起请求,获取响应状态码
responseCode = connection.getResponseCode()
String responseBody = ""
if (responseCode >= 200 && responseCode < 300) {
// 2xx 成功状态码,读取正常响应流
responseBody = IOUtils.toString(connection.getInputStream(), charset)
// 将成功相关属性存入临时map
attrMap["http.response.code"] = String.valueOf(responseCode)
attrMap["http.request.url"] = targetUrl
attrMap["request.status"] = "success"
} else {
// 4xx/5xx 错误状态码,读取错误流
responseBody = IOUtils.toString(connection.getErrorStream(), charset)
throw new RuntimeException("接口返回非2xx状态码:${responseCode},响应内容:${responseBody}")
}
// 将接口返回结果写入FlowFile,覆盖原有内容
outputStream.write(responseBody.getBytes(charset))
} catch (Exception e) {
log.error("HTTP接口调用异常:${e.getMessage()}", e)
attrMap["request.error"] = e.getMessage()
attrMap["request.status"] = "failure"
// 抛出异常,向上传递进入外层异常分支
throw e
} finally {
// 强制关闭连接,避免HTTP连接长期占用导致资源泄漏
if (connection != null) {
connection.disconnect()
}
}
} as StreamCallback)
// 回调执行完毕,统一批量写入FlowFile属性
attrMap.each { key, value ->
flowFile = session.putAttribute(flowFile, key, value)
}
// 正常流程:路由至成功端口,提交会话
session.transfer(flowFile, REL_SUCCESS)
session.commit()
log.info("POST接口调用成功,响应码:${responseCode},请求地址:${targetUrl}")
} catch (Exception e) {
// 全局捕获所有异常(网络错误、参数缺失、接口报错等)
log.error("FlowFile整体处理失败:${e.getMessage()}", e)
// 写入失败相关属性
flowFile = session.putAttribute(flowFile, "error.message", e.getMessage())
flowFile = session.putAttribute(flowFile, "error.stack", ExceptionUtils.getStackTrace(e))
flowFile = session.putAttribute(flowFile, "request.status", "failure")
flowFile = session.putAttribute(flowFile, "http.response.code", String.valueOf(responseCode))
// 路由至失败端口,会话回滚
session.transfer(flowFile, REL_FAILURE)
session.rollback()
}
配套使用说明
- 上游处理器必须设置两个属性
api.url:完整接口地址,例如https://xxx.xxx.com/api/testapi.key:接口所需的密钥字符串
- 如果你的 apikey header名称大小写不一样(例如
ApiKey、APIKEY),直接修改这一行:
groovy
connection.setRequestProperty("apikey", apiKey)
- 风险提示
- 如果
api.url/api.key属性为空,执行new URL(targetUrl)或header赋值时会直接抛出异常,进入失败端口; - 如需增加空值校验,我可以再帮你补充前置判断。
可选增强(如果你需要)
增加前置校验,防止属性缺失直接报错,在def apiKey = ...下方添加:
groovy
// 参数合法性校验
if (!targetUrl) {
throw new RuntimeException("FlowFile属性【api.url】不能为空,请检查上游流程")
}
if (!apiKey) {
throw new RuntimeException("FlowFile属性【api.key】apikey不能为空,请检查上游流程")
}