Ohhnews

分类导航

$ cd ..
foojay原文

BoxLang 1.14.0:集合、范围、内部类与可对话运行时

#boxlang#集合#范围#内部类#ai诊断

[LOADING...]

BoxLang 从未止步,但 1.14.0 版本与众不同。此次发布标志着该语言不再仅仅填补空白,而是开始自主定义现代动态 JVM 语言应有的样貌。共关闭了 65 个问题,引入了四项创新的语言特性,一款成长起来的格式化工具,以及一个 Companion 模块 —— bx-mcp —— 它从根本上改变了你以 AI 方式操作运行中 BoxLang 应用的方式。

这次发布对团队而言,本可以轻松成为一次大版本。这完全是 Ortus 全体成员以及我们的客户在迁移到 BoxLang 过程中所给予的惊人反馈和创意共同努力的结果。我们才刚刚开始!

让我们逐一探索所有新内容。

动态集合(Sets)—— 一等公民集合类型

BoxLang 1.14.0 将 BoxSet 作为真正的第一等类型提供 —— 不是一个轻量包装器,也不是事后追加的库 —— 而是一个完全集成的集合类型,拥有字面量语法、函数式管道、用于集合代数的运算符重载,以及三种后端的变体,以满足不同工作负载的需求。

与数组不同,集合通过设计强制唯一性,并提供高效的查找操作。BoxLang 将集合提升为一等公民,支持字面量语法、函数式集合操作以及丰富的运算符重载,用于集合代数——包括并集、交集、差集和对称差集——使复杂的数据操作既富有表现力又简洁。

无论你是比较数据集、管理唯一标识符、处理大型集合、实现访问控制规则,还是构建推荐系统和分析引擎,BoxSet 都为你提供了一个高效且优雅的基础,用于处理规模化的不同值。

集合提供三种风味:

  • DEFAULT(HashSet)—— 最快,不保证顺序
  • LINKED(LinkedHashSet)—— 保留插入顺序
  • SORTED(TreeSet)—— 通过 Compare.invoke 自然升序
// BIF 构造
s = setNew()
s = setNew( type="linked", values=[ 1, 2, 3 ] )
s = setOf( 1, 2, 2, 3 )   // 自动去重 → {1, 2, 3}

// 字面量语法 —— 简洁且富有表现力
s = set{ 1, 2, 3 }
s = set{}

// 展开支持
arr = [ 3, 4, 5 ]
s   = set{ 1, 2, ...arr }

// 从数组转换
s = [ 1, 2, 2, 3 ].toSet()
s = [ "c", "a", "b", "a" ].toSet( "linked" )

// 从分隔字符串转换
s = "a,b,c,a".listToSet()

运算符重载让一切变得优雅。集合代数成为一等操作:

a = set{ 1, 2, 3 }
b = set{ 3, 4, 5 }

union     = a + b   // {1, 2, 3, 4, 5}
diff      = a - b   // {1, 2}
intersect = a * b   // {3}
symdiff   = a ^ b   // {1, 2, 4, 5}

右侧操作数接受“宽松”类型 —— 你可以传入数组、列表字符串、范围或另一个集合。函数式管道完全符合预期:

result = setOf( 1, 2, 3, 4, 5 )
    .filter( v -> v > 2 )
    .map( v -> v * 10 )
    .toList( ", " )
// → "30, 40, 50"

结构体现在暴露 .keySet().valueSet() 方法,用于将键或值提取为集合。集合序列化为 JSON 数组。任何 java.util.Set 实现都可以透明地包装 —— 修改会传播回底层 Java 对象,与数组包装的契约相同。

集合还完全支持不可变性。调用 .toUnmodifiable() 冻结集合,调用 .toModifiable() 解冻副本以便再次修改。

完整参考: BoxSet 文档

范围(Ranges)—— 惰性、类型化、可扩展的区间

.. 运算符在 BoxLang 1.12 版本中就已存在,但当时它急切地生成数组。对于小序列来说没问题,但对于大型序列却是个问题,并且完全无法表示无限序列、非整数区间或领域特定的递进序列。

BoxLang 1.14.0 从基本原理重新构思了范围。现在范围是 惰性对象,按需生成值。它们不是数组。它们携带类型语义。支持排除边界、自定义步长、Java Stream 集成,以及最强大的 —— 全新的 IRangeable 接口,让你自己的类参与范围操作。

// 包含端点 —— 生成 1, 2, 3, 4, 5
1..5

// 排除边界
1>..5    // 排除起始:2, 3, 4, 5
1..<5    // 排除结束:1, 2, 3, 4
1>..<5   // 排除两端:2, 3, 4

// 半有界与无界
1..      // 从 1 开始无界(无限)
..5      // 起始开放,到 5 为止
..       // 完全无界(包含所有非 null 值)

因为范围是惰性的,即使是庞大无比的范围也非常廉价:

// 这并不会分配 1000 亿个整数
for( i in 1..100_000_000_000 ) {
    result = i
    break   // 即时
}

// 完整的 Java Stream API 集成
( 1.. ).stream().limit( 5 ).toList()   // [1, 2, 3, 4, 5]

除了整数,范围原生支持 小数、字符和 DateTime 值

// 带自定义步长的小数
( 0..1 ).step( 0.25 )   // 0, 0.25, 0.50, 0.75, 1.00

// 字符
for( c in "a".."e" ) { }    // a, b, c, d, e

// 按月递增的 DateTime
start = createDate( 2024, 1, 1 )
end   = createDate( 2024, 6, 1 )
( start..end ).step( 1, "month" )   // 1月, 2月, 3月, 4月, 5月, 6月

带步长的范围在 contains() 方法中执行 步长可达性检查 —— 不仅仅是边界检查。如果一个值在边界内但实际无法通过步长增量到达,contains() 返回 false。这是 Python/Kotlin 的惯例,也是正确的行为:

r = ( 1..10 ).step( 3 )   // 产生:1, 4, 7, 10
r.contains( 4 )            // true  —— 可达
r.contains( 5 )            // false —— 在边界内但不可达

IRangeable 接口是头条功能。任何 BoxLang 或 Java 类都可以通过实现四个方法加入范围系统:rangeAdvance()rangeCompare()rangeCoerce(),以及可选的 rangeStepFromUnit()rangeUnitStepper() 用于非均匀递进。文档提供了三个完整示例 —— 斐波那契数列、罗马数字以及带有完整半音和调式感知步进的音符。这些不是玩具。它们展示了一个真正的可扩展框架。

// 斐波那契:无限非线性范围
( new Fib().. ).stream().limit( 10 ).map( .getCurrent() ).toList()
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

( new Fib().. ).contains( 13 )   // true
( new Fib().. ).contains( 14 )   // false

类型化无界范围让你可以约束 .. 范围认为匹配的内容,使用 BoxLang 的类型系统或严格的 Java 类匹配:

( .. ).type( "number" ).contains( "5" )      // true  —— 可强制转换
( .. ).type( "integer" ).contains( 5.5 )     // false —— 不是整数

import java:java.lang.Number
( .. ).type( Number ).contains( 42 )         // true  —— instanceof 检查
( .. ).type( Number ).contains( "5" )        // false —— 严格,无强制转换

完整参考: BoxLang 范围文档

内部类和模板类

BoxLang 1.14.0 引入了 局部定义类 —— 你可以在 .bxs 脚本内、.bxm 模板的 <bx:script> 块内或嵌套在其他类内部声明类。这是一种结构上的表达能力,有助于保持代码组织,而无需将每个关注点强制放入单独的文件。

脚本中定义的类会被 提升(hoisted),意味着你可以在它们的文本定义出现之前实例化它们:

// 定义之前实例化 —— 提升机制在起作用
result = new Greeter().greet( "World" )

class Greeter {
    function greet( name ) {
        return "Hello, " & name & "!"
    }
}

多个局部类可以自然共存。静态成员、抽象类和继承都能正常工作:

abstract class Shape {
    abstract function area()
}

class Circle extends="Shape" {
    function init( radius ) {
        variables.radius = radius
        return this
    }

    function area() {
        return 3.14159 * variables.radius ^ 2
    }
}

c = new Circle( 5 )
c.area()   // ~78.54

局部类继承其封闭脚本的导入,因此 Java 类型可以直接使用,无需任何额外仪式:

import java.util.Date

class Event {
    function init( name ) {
        variables.name      = name
        variables.timestamp = new Date()
        return this
    }

    function getInfo() {
        return variables.name & " at " & variables.timestamp.toString()
    }
}

内部类 —— 嵌套在其他类内部的类 —— 通过 $ 分隔符语法从外部访问,并完全支持导入别名:

// 完全限定名
result = new src.models.Container$Widget( "my-widget" )

// 带别名的导入
import src.models.Container$Widget as Widget
result = new Widget( "aliased-widget" )

模板类 让你在 .bxm 标记文件的 <bx:script> 代码块内定义类:

<bx:script>
    class Point {
        function init( x, y ) {
            variables.x = x
            variables.y = y
            return this
        }
        function toString() {
            return "(" & variables.x & "," & variables.y & ")"
        }
    }
    result = new Point( 3, 4 ).toString()
</bx:script>

完整参考: 内部类 | 模板类

类引用作为可调用构造函数

这一特性改变了你对对象创建的思考方式。在 BoxLang 1.14.0 中,导入的类引用是 可调用的。将类引用作为函数调用会执行构造函数并返回一个新实例。new 关键字仍然完全支持 —— 这是增强,而非替代。

import java.lang.StringBuilder
import models.User

// 以下三种形式等价
u1 = new User( "Bob", "bob@example.com" )
u2 = User.init( "Bob", "bob@example.com" )
u3 = User( "Bob", "bob@example.com" )      // 类引用作为函数调用

这在函数式编程中变得真正强大。由于类引用现在是可调用对象,你可以直接将它们传递给高阶函数:

import models.User

names = [ "Alice", "Bob", "Charlie" ]

// 以下都等价 —— 选择你喜欢的风格
users = names.map( User )
users = names.map( name -> new User( name ) )
users = names.map( name -> User( name ) )

简写 names.map( User ) 是真正的胜利 —— 将原始值集合转换为领域对象只需一个表达式。在底层,类引用被包装在 ClassInvokerFunction 中,该函数委托给与 new 相同的构造函数管道,因此行为完全相同。Java 类和 BoxLang 类平等参与。

DataNavigator JSONPath 支持

DataNavigator 一直是安全遍历嵌套结构体和数组的有用工具。在 1.14.0 中,它获得了完整的 JSONPath 样式表达式支持 —— 点号表示法、数组索引、切片、通配符、递归下降和过滤表达式 —— 直接在 get()has()from() 以及新的 query() 方法中使用。

nav = dataNavigate( jsonData )

// 点号表示法深层访问
value  = nav.get( "boxlang.settings.hello" )

// 递归下降 —— 在树中任意位置查找 "key1"
found  = nav.has( "..key1" )

// 数组切片 —— 1 起始包含端点
slice  = nav.get( "list[1:3]" )

// 通配符 —— 所有子元素
all    = nav.get( "items[*].name" )

// 过滤表达式
active = nav.query( "items[?(@.active == true && @.priority > 2)]" )
named  = nav.query( "items[?(@.active)].name" )

新的 query() 方法将所有匹配结果作为 BoxLang 数组返回 —— 当路径跨越集合展开时,这正是合适的工具。getOrDefault() 提供保证的非 null 返回值以及显式回退。getByKey() / hasByKey() 处理键名本身包含点号或方括号的精确键查找:

// 多个匹配以数组形式返回
results = nav.query( "store.products[?(@.price > 100)].name" )

// 显式回退 —— 无需空值检查
port = nav.getOrDefault( "server.port", 8080 )

// 字面量键访问 —— 将 "value.sep" 视为一个键名
nav.getByKey( "value.sep" )

所有路径表达式对空格具有容忍性。结果是在消费外部 JSON、API 负载或深层嵌套配置时,大量减少样板代码。

完整参考: DataNavigator 文档## 查询转换器 - 掌控你的结果形状

queryExecute() 一直以来只允许三种返回类型:queryarraystruct。任何其他形状——领域对象、JSON 字符串、带列描述符的表格数组、富元数据结构——都需要单独的后处理步骤。这种摩擦会迅速累积。

新的 查询转换器(Query Transformer)框架干净地解决了这个问题。传入一个 transformer 选项,即可完全控制 queryExecute() 的返回值:

$ java
// 内联闭包 —— 返回一个带元数据的自定义结构体
var result = queryExecute( "SELECT * FROM users", [], {
    datasource: "app",
    transformer: ( query, meta ) => {
        return {
            data:        query.toArrayOfStructs(),
            total:       query.recordCount,
            executedAt:  now(),
            sql:         meta.sql
        }
    }
} )
$ java
// 从查询行生成领域对象
var users = queryExecute( "SELECT * FROM users", [], {
    datasource: "app",
    transformer: ( query, meta ) => query.toArrayOfStructs().map( row -> new User( row ) )
} )

转换器接收原始的 query 对象(可访问 .recordCount.toArrayOfStructs().getData().getColumnNames().getColumnMeta())以及一个 metadata 结构体,其中包含 SQL、参数、执行时间和列元数据。当 transformer 存在时,它优先于 returnType

转换器也可以是类实例或在 Application.bx 中注册的命名对象:

$ java
// Application.bx —— 注册可复用的转换器
this.queryTransformers = {
    "rich":    new RichTransformer(),
    "tabular": ( query, meta ) => {
        return {
            columns: query.getColumnNames(),
            data:    query.getData().map( row -> arrayNew( row ) )
        }
    },
    "json":    ( query, meta ) => serializeJson( query.toArrayOfStructs() )
}

// 在应用的任何地方使用
var tabular = queryExecute( sql, params, { transformer: "tabular" } )
var json    = queryExecute( sql, params, { transformer: "json" } )

bx:query 组件也支持转换器:

$ xml
<bx:query name="result" datasource="app"
    transformer=(( q, m ) => serializeJson( q.toArrayOfStructs() ))>
    SELECT * FROM users
</bx:query>

全局查询默认值

与转换器一同,BL-2477boxlang.json 中引入了 queries 部分,以及在 Application.bx 中引入了 this.queryOptions,用于设置应用级别的查询默认值:

$ cat
"queries": {
    "timeout":       0,
    "returnType":    "query",
    "fetchSize":     0,
    "maxRows":       0,
    "cacheProvider": "default"
}
$ java
// Application.bx
this.queryOptions = {
    "timeout":    30,
    "returnType": "array",
    "maxRows":    1000
}

每个查询的选项始终胜出。this.queryOptions 是应用级别的默认值;boxlang.json 是运行时的后备方案。优先级清晰,不存在意外。

完整参考: 查询转换器 | 全局查询选项

伴生发布:bx-mcp 来了

与 BoxLang 1.14.0 一道,bx-mcp 模块首次公开亮相——它让你的 AI 能够实时观察正在运行的 BoxLang 应用。1.14.0 推进了语言本身,而 bx-mcp 则推进了你如何在生产环境中操作这门语言。

它解决的问题是每个 BoxLang 开发人员都熟悉的:你启动一个应用,流量涌入,调度器执行,缓存预热,线程旋转。当出现问题——或者你只是想了解系统的状态时——你不得不在日志、仪表盘、管理面板和监控工具之间切换以拼凑信息。而你的 AI 助手只理解源代码,对实时系统一无所知。

bx-mcp 改变了这一点。安装它,将任何 MCP 兼容的 AI 客户端指向你正在运行的服务器,你就可以实时对话地访问每个 BoxLang 子系统。

$ bash
box install bx-mcp
$ cat
{
    "mcpServers": {
        "boxlang": {
            "url": "http://localhost:8080/~bxmcp/boxlang.bxm",
            "headers": {
                "Authorization": "Bearer your-auth-token"
            }
        }
    }
}

你获得的内容相当可观:跨 17 个运行时领域的 154 个工具——JVM 诊断、缓存管理、数据源连接池指标、SQL 慢查询捕获、出站 HTTP 诊断、入站请求诊断、逐路由延迟指标、调度器管理、模块重载、拦截器自省、文件监视器控制、日志记录等等。其中五个领域是全新的:SQL 诊断、HTTP/SOAP 诊断、请求诊断、路由指标,以及一个性能快照工具,一次调用即可捕获完整的运行时画面。

除了工具,bx-mcp 还附带 32 个预构建的 AI 诊断提示——预配置的推理工作流,指示你的 AI 调用哪些工具、按什么顺序调用以及如何解释结果。让它诊断内存泄漏、调查缓存退化或排查线程池饱和,它知道如何有条不紊地进行调查。

其结果是一种与运行中的 BoxLang 应用工作的根本不同方式。无需 SSH,无需日志 grep。通过对话式操作获得完整的运行时上下文。

阅读完整公告:

介绍 BoxLang MCP 模块

文档: bx-mcp 文档

其他值得注意的补充

schedulerNew() BIF

无需专门的类文件即可创建和注册轻量级的临时调度器。当你需要一个运行时调度器且无需生命周期回调时,schedulerNew() 是正确的工具;当你需要 onStartuponShutdownonAnyTaskError 时,schedulerStart() 仍然是选择。

$ java
myScheduler = schedulerNew(
    name:     "email-scheduler",
    timezone: "America/Chicago"
)

myScheduler.task( "welcome-email" )
    .call( () => sendWelcomeEmails() )
    .everyHour()
    .startup()

server.webMode

server 作用域上新增了一个布尔值,告诉你运行时是否以 Web 模式运行(servlet 或 MiniServer):

$ java
if ( server.webMode ) {
    // web-specific initialization
}

字符串 BIF:stringStartsWithstringEndsWith

四个新的 BIF,支持完整的成员方法:

$ java
stringStartsWith( "Hello World", "Hello" )    // true
stringEndsWith( "Hello World", "World" )      // true
stringStartsWithNoCase( "HELLO", "hello" )    // true
stringEndsWithNoCase( "WORLD", "world" )      // true

// 成员方法
"Hello World".startsWith( "Hello" )
"Hello World".endsWith( "World" )

Java 互操作:Varargs 改进

传递给 Java varargs 方法的 BoxLang 数组不再需要手动解包为 Object[]。运行时自动处理转换。

extendsimplements 中使用 Java 导入别名

现在导入别名可以在类继承声明中工作:

$ java
import java.util.HashMap as MyMap

class extends="MyMap" {
    // ...
}

格式化器成熟度

格式化器在 1.14.0 中获得了大量投入:

  • 忽略注释 - @formatter:off / @formatter:onbxformat-ignore-start / bxformat-ignore-end(也匹配 cfformat 约定)让你选择性地跳过某些块的格式化
  • 多个源文件 - --source 现在接受逗号分隔的路径
  • 排除标志 - --excludes 跳过文件或目录
  • template.enabled 标志 - 控制实验性 .bxm 格式化,直到它退出预览(默认值为 false
  • class.property_spacing 规则 - 控制属性声明之间的空行数(默认值为 1,与 Ortus 标准一致)

MiniServer 健康指标

/health 端点现在包括 Undertow 工作线程池统计信息、WebSocket 会话计数以及扩展的 JVM 指标——无需额外工具即可获得更丰富的运行状况图景。

应用运行时自省

Application 对象公开了三种新的自省方法:

$ java
application.getWatchers()       // 活动文件监视器
application.getSchedulers()     // 注册的调度器
application.getAppDuration()    // 应用运行时间

ON_DATASOURCE_INITIALIZED 拦截点

一个新的拦截点在数据源配置加载后、HikariCP 连接池建立之前触发——让模块可以完全访问原始连接池配置:

$ java
function onDatasourceInitialized( event, interceptData ) {
    var hikariConfig = interceptData.hikariConfig
    hikariConfig.setMaximumPoolSize( 50 )
    hikariConfig.addDataSourceProperty( "cachePrepStmts", true )
}

Bug 修复亮点

六十五个问题意味着覆盖了大量领域。以下是一些值得特别指出的修复:

  • BL-2425 - 大型 if/else 块不再抛出 LargeMethodErrors。编译器现在会拆分过大的条件块而不是失败。
  • BL-2400 - 1.13 版本中 serializeJSON() 的栈溢出回归问题已解决。
  • BL-2413 - 线程争用下的大型查询中的查询操作现在正确线程安全。
  • BL-2432 - Java 互操作 varargs 现在无需手动解包即可处理 BoxLang 数组。
  • BL-2403 - Decrypt BIF 现在能正确处理复杂/结构化对象——之前会损坏嵌套数据。
  • BL-2479 - BoxCacheStats.hitRate() 由于整数除法不再返回 0
  • BL-2042 - LoggingService 并发修改异常已通过线程安全的日志管理器管理得到修复。
  • BL-2483 - 包含空格的路径上的 URISyntaxException 已解决。
  • BL-1007 - snakeCase()pascalCase()kebabCase() 现在能正确处理 camelCase、PascalCase、snake_case、kebab-case 和混合输入。

获取 1.14.0

通过 CommandBox 更新:

$ bash
box update boxlang

或者从 boxlang.io 获取最新版本。

完整发布说明位于 boxlang.ortusbooks.com/readme/release-history/1.14.0。本文中链接了每个主要功能的新文档。

如果你想要扩展功能——bx-mcpbx-aibx-jwtbx-redis 以及 BoxLang+ 模块生态系统中的其他模块——请访问 boxlang.io/plans 查看适合你团队的内容。

我们快速发布,我们为真实工作负载而发布。BoxLang 1.14.0 是这门语言正在成为什么的最清晰宣言。

问题、反馈和成果分享请前往 Ortus 社区

本文 BoxLang 1.14.0 : 集合、范围、内部类以及会回应的运行时 首发于 foojay