Ohhnews

分类导航

$ cd ..
foojay原文

BoxLang 1.14.0发布:JSONPath集成到DataNavigator,实现全方位数据导航

#boxlang#datanavigator#jsonpath#数据处理#版本更新

[LOADING...]

每个应用程序最终都要处理深层嵌套的数据。六层深的 JSON API 响应载荷;你需要的键埋在对象数组里的配置文件,其中一个对象的字段你认为必填却为 null;没有人编写过 schema 的模块元数据结构;以及长得像没有规划过的树一样的运行时自省数据。

BoxLang 在 1.0 版本就引入了 DataNavigator,让你可以使用 dataNavigate(),调用 .from() 来限定作用域,链式调用 .get(),并为每个路径提供默认值。简洁且安全。但即使是流式 API,从复杂载荷中提取特定切片仍然需要多次导航跳转、循环,或者三层深的防御性键存在检查。

BoxLang 1.14.0 改变了这一点。DataNavigator 现在原生支持 JSONPath 风格的路径表达式,并且新增的 query() 方法让你可以在结构上展开并一次性收集所有匹配项。更少的仪式感,更多的信息。

快速回顾:什么是 DataNavigator?

DataNavigator 是 BoxLang 的流式助手,用于安全地遍历嵌套数据结构:结构体、数组、解析后的 JSON、配置文档、运行时元数据。其关键特性是当路径不存在时不会抛出异常——而是根据调用方式返回 null、默认值或空导航器。

// 旧方式——三层 structKeyExists
if ( structKeyExists( config, "database" ) ) {
    if ( structKeyExists( config.database, "connection" ) ) {
        maxSize = config.database.connection.pool?.maxSize ?: 10
    }
}

// 使用 DataNavigator
maxSize = dataNavigate( config ).get( ["database", "connection", "pool", "maxSize"], 10 )

通过 dataNavigate() BIF 创建导航器,该 BIF 接受结构体、JSON 字符串、JSON 文件路径或 Java Map。然后使用 .from() 限定作用域,.has() 检查存在性,.get() / .getOrThrow() 提取值。

该 API 仍然像以前一样正常工作。1.14.0 新增的是可以直接放入这些方法中的紧凑表达式语言。

1.14.0 的新增内容

DataNavigator 新增了四个重点功能:

DataNavigator 1.14.0 新增类型
get( "dot.path[0]" )JSONPath
has( "..recursive" )JSONPath
from( "dot.path" )JSONPath
query( "path[*].key" )新增:多结果
getOrDefault( key, v )新增:安全
getByKey( "x.y" )新增:精确
hasByKey( "x.y" )新增:精确
  • get()has()from() 中使用 JSONPath 表达式——当传入包含 .[ 的单个字符串时,会被视为路径表达式。多参数和普通键调用则完全保持原有行为。
  • query(path) —— 返回所有匹配项,以 BoxLang 数组形式。通配符、切片、过滤器和递归下降可能产生多个匹配项;query() 会全部收集。
  • getOrDefault(key, value) —— get() 的显式回退变体。保证非 null。比 null 检查模式更简洁。
  • getByKey(key) / hasByKey(key) —— 精确键查找,其中点和括号是字面字符,不是分隔符。用于处理如 "db.host" 这类实际键名中包含点或括号的载荷。

路径表达式语法

BoxLang 的 JSONPath 方言涵盖了日常工作中最重要的操作。以下是完整参考:

路径表达式语法

路径表达式语法

路径表达式含义
boxlang.settings.hello点号表示法:导航嵌套结构体键
keywords[1]数组索引:基于 1,返回该元素
..hello递归下降:树中任意位置的第一个匹配
items[*].name通配符:所有元素,然后对每个取 .name
keywords[1:3]切片:基于 1 的包含范围
keywords[3:]开放式切片:索引 3 到末尾
items[?(@.active == true)]过滤器:匹配条件的元素
items[?(@.priority > 2)]过滤器:数值比较
items[?(@.active && @.n > 2)]过滤器:组合 AND 条件
items[?(@.email)]过滤器:存在性检查,真值字段
items[?(@.active)].name过滤器 + 键:从匹配项中提取字段

空格在任何位置都是允许的:

带空格的表达式等价的表达式
"boxlang . settings . hello""boxlang.settings.hello"
"keywords [ * ]""keywords[*]"

触发规则:

  • 含有 .[ 的单个字符串被视为路径表达式。
  • 多个参数或没有分隔符的字符串保持原有的可变键行为。

所有这些语法都可以在 get()has()from()query() 内部使用。

真实场景

场景 1:处理 API 响应

你正在使用一个第三方 REST API。载荷如下所示:

{
  "status": "ok",
  "data": {
    "store": {
      "products": [
        { "id": 1, "name": "Keyboard", "price": 149.99, "inStock": true,  "category": "hardware" },
        { "id": 2, "name": "Mouse",    "price": 49.99,  "inStock": false, "category": "hardware" },
        { "id": 3, "name": "License",  "price": 299.00, "inStock": true,  "category": "software" },
        { "id": 4, "name": "Cable",    "price": 9.99,   "inStock": true,  "category": "hardware" }
      ]
    }
  }
}

在 1.14.0 之前,提取有库存的产品名称需要先导航到数组,然后循环:

// 1.14.0 之前
products = dataNavigate( apiResponse )
    .from( ["data", "store", "products"] )
    .get( [] )

inStockNames = products
    .filter( p -> p.inStock )
    .map( p -> p.name )

在 1.14.0 中使用 JSONPath 表达式,导航器在一次调用中处理遍历和过滤:

nav = dataNavigate( apiResponse )

// 一个表达式获取所有有库存的产品名称
names = nav.query( "data.store.products[?(@.inStock == true)].name" )
// => [ "Keyboard", "License", "Cable" ]

// 仅价格超过 $50 的硬件项目
expensive = nav.query( "data.store.products[?(@.category == 'hardware' && @.price > 50)]" )
// => [ { id:1, name:"Keyboard", price:149.99, ... } ]

// 第一个有库存的产品(单个结果)
first = nav.get( "data.store.products[?(@.inStock == true)]" )
// => { id:1, name:"Keyboard", ... }

// 总产品数量(安全,带默认值)
count = nav.getOrDefault( "data.store.meta.count", 0 )
// => 0(字段不存在,干净地返回默认值)

注意区别:get() 返回第一个匹配项query() 返回所有匹配项(数组形式)。

场景 2:配置自省

你正在编写一个需要检查运行时 boxlang.json 并跨嵌套路径提取设置的模块。某些键可能因部署环境而不存在。

config = dataNavigate( server.system.config )

// 点号路径读取——无需链式 .from() 调用
logLevel   = config.getOrDefault( "logging.level", "WARN" )
datasource = config.getOrDefault( "defaultDatasource", "main" )
cacheMax   = config.getOrDefault( "caches.default.maxObjects", 1000 )

// 递归下降——在配置树中查找任何位置的 "timeout" 键
// 当你不知道确切嵌套层级时很有用
timeout = config.get( "..timeout", 30 )

// 在使用可选部分之前进行存在性检查
if ( config.has( "modules.bx-ai.providers[*]" ) ) {
    providers = config.query( "modules.bx-ai.providers[*].name" )
    // => [ "openai", "anthropic", "bedrock" ]
}

// 字面包含点的键(常见于 Java 风格的属性文件)
// 使用 getByKey() 跳过路径解析
jdbcUrl = config.getByKey( "datasources.main.db.url" )
//                          ^ 被视为一个字面键,而非路径

getByKey() / hasByKey() 的区别在以下情况尤为重要:当你的数据由 Java 属性系统、点键配置库或任何键名中包含 .[ 作为真实字符的载荷所构建时。

场景 3:通配符和切片提取

你有一个模块的元数据结构体,并希望为仪表板或诊断工具提取特定切片。

moduleData = {
    "name": "bx-ai",
    "version": "3.2.0",
    "providers": [
        { "name": "openai",    "models": [ "gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo" ] },
        { "name": "anthropic", "models": [ "claude-opus-4-5", "claude-sonnet-4-5" ] },
        { "name": "bedrock",   "models": [ "titan", "llama3" ] }
    ],
    "settings": {
        "timeout": 30,
        "retries": 3,
        "debug": false
    }
}

nav = dataNavigate( moduleData )

// 所有提供商名称
nav.query( "providers[*].name" )
// => [ "openai", "anthropic", "bedrock" ]

// 仅前两个提供商(切片)
nav.query( "providers[1:2]" )
// => [ { name:"openai", ... }, { name:"anthropic", ... } ]

// 所有提供商的所有模型名称——通配符 + 通配符
nav.query( "providers[*].models[*]" )
// => [ "gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo", "claude-opus-4-5", ... ]

// 所有设置值(结构体上的通配符)
nav.query( "settings.*" )
// => [ 30, 3, false ]

// 拥有超过 2 个模型的提供商
nav.query( "providers[?(@.models)]" )
// => 所有具有 models 字段的提供商(存在性检查)

选择正确的方法

四种检索方法服务于不同目的。使用以下指南:

方法使用时机
get(path, dflt)你期望一个值;路径可能不存在;返回 null 或默认值。
getOrDefault(path, value)get() 相同,但希望保证非 null 返回并提供显式回退。
getOrThrow(path)该值是必需的。缺失键是致命配置错误;快速失败。
query(path)你期望多个匹配:通配符、切片或过滤器导致展开。始终返回数组。
getByKey(key)键名字面包含 .[;希望精确查找,不进行路径解析。

当值可能存在也可能不存在时使用 get()getOrDefault();当缺失数据应立即失败时使用 getOrThrow();当路径可能返回多个结果时使用 query();当需要字面键查找而非路径解析时使用 getByKey()

一条经验法则:如果你的路径表达式包含 [*][n:m].. 或过滤器 [?(...)],请选择 query()。这些运算符可能匹配零个、一个或多个节点。get() 只返回第一个命中并静默丢弃其余部分。

汇总示例

以下是一个完整的、真实的示例:加载和验证多环境应用程序配置,然后从每个部分提取所需内容。

class AppConfigLoader {

    function load( required string environment ) {
        var nav = dataNavigate( expandPath( "/config/app.json" ) )

        // 必需字段——如果缺失则快速失败
        var appName = nav.getOrThrow( "app.name" )
        var appVersion = nav.getOrThrow( "app.version" )

        // 限定到请求的环境
        var envNav = nav.from( "environments.#environment#" )
        if ( envNav.isEmpty() ) {
            throw( type="ConfigError", message="未知环境: #environment#" )
        }

        // 收集数据库设置,带默认值
        var db = {
            host:     envNav.getOrDefault( "database.host", "localhost" ),
            port:     envNav.getOrDefault( "database.port", 5432 ),
            ssl:      envNav.getOrDefault( "database.ssl", true ),
            poolMax:  envNav.getOrDefault( "database.pool.maxConnections", 10 )
        }

        // 收集所有已启用的功能标志
        var enabledFeatures = envNav.query( "features[?(@.enabled == true)].name" )

        // 查找第一个配置的缓存提供者(递归下降)
        var cacheProvider = envNav.get( "..cacheProvider", "default" )

        // 任何模块级覆盖——跨所有模块的通配符
        var moduleOverrides = envNav.query( "modules[*].overrides" )

        return {
            name:             appName,
            version:          appVersion,
            environment:      environment,
            database:         db,
            enabledFeatures:  enabledFeatures,
            cacheProvider:    cacheProvider,
            moduleOverrides:  moduleOverrides
        }
    }

}

没有循环。没有空值守卫塔。没有嵌套的 structKeyExists() 链。路径表达式描述了你想要的数据形状,而导航器处理遍历。

升级与探索

DataNavigator 中的 JSONPath 支持已包含在 BoxLang 1.14.0 中,今天即可使用。无需添加依赖,无需更改配置。如果你已经在使用 dataNavigate(),新表达式是即插即用的增强。现有的可变键调用保持不变。

完整文档位于 boxlang.ortusbooks.com/boxlang-language/syntax/data-navigators

完整的 1.14.0 发布说明,包括动态集合、区间、内部类、查询转换器以及所有 65 个已关闭的问题,位于 boxlang.ortusbooks.com/readme/release-history/1.14.0

资源

BoxLang 是由 Ortus Solutions 开发的现代、多运行时 JVM 语言。专为 Web、云端和命令行而生。开源,专业支持。

原文:BoxLang 1.14.0 : Navigate Anything: JSONPath Comes to BoxLang's DataNavigator 首发于 foojay