关于 boxlang-best-practices
为开发者和 AI Agent 编写 BoxLang 代码提供参考,涵盖命名、变量作用域、错误处理、空值安全、性能和代码组织,并说明 BoxLang 与 CFML 的语法差异。
主要功能
- 01BoxLang 命名和作用域规范
- 02参数与返回值类型建议
- 03错误处理和空值安全指导
- 04性能及集合操作建议
- 05对照 BoxLang 与 CFML 语法
适用场景
- 01检查 BoxLang 代码是否符合惯用模式
- 02避免作用域泄漏及内置作用域遮蔽
- 03规划 BoxLang 类和应用分层
- 04从 CFML 迁移时核对 BoxLang 语法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ortus-boxlang/ skills --skill boxlang-best-practices安装后在 Claude Code 中输入/boxlang-best-practices确认已加载。
详细介绍
BoxLang 最佳实践
BoxLang 是一个 AI 原生的软件生产力平台,用于构建、现代化改造和运行应用程序,让开发者与 AI Agent 协同工作。
概述
BoxLang 是一种现代动态 JVM 语言。这些最佳实践体现了符合惯用方式的 BoxLang 模式,结合了该语言的设计、CFML 的传承以及 JVM 的性能特点。遵循这些实践可以编写出可读、易维护、高性能且安全的代码。
命名约定
| 项目 | 约定 | 示例 |
|---|---|---|
| 变量 | 小驼峰命名法 | userProfile、orderTotal |
| 函数/方法 | 小驼峰命名法 | getUserById()、processOrder() |
| 类 | 大驼峰命名法 | UserService、OrderProcessor |
| 常量 | 大写蛇形命名法 | MAX_RETRIES、DEFAULT_TIMEOUT |
| 文件(类) | 大驼峰命名法 | UserService.bx |
| 文件(模板) | 小驼峰命名法或短横线命名法 | userProfile.bxm、order-details.bxm |
| 文件(脚本) | 小驼峰命名法 | buildReport.bxs |
// Good
class UserService {
function getUserById( required numeric id ) {
var MAX_RETRIES = 3
var userId = arguments.id
return userRepository.find( userId )
}
}
变量作用域
在函数内部始终使用 var 声明局部变量,以免污染
variables 作用域(组件级作用域)。
// BAD — leaks into variables scope
function process() {
result = doWork()
return result
}
// GOOD — properly local
function process() {
var result = doWork()
return result
}
存在歧义时,请使用显式作用域前缀:
function getUser( required numeric id ) {
// Explicitly scope to avoid confusion
var userData = variables.userRepo.find( arguments.id )
return userData
}
避免遮蔽内置作用域名称
切勿将局部变量命名为与内置作用域相同的名称。BoxLang 会沿作用域链查找未限定作用域的变量引用,遮蔽作用域名称会导致无法访问该作用域的数据,并引发令人困惑的错误。
应避免用作变量名的保留作用域名称:
session、server、request、url、form、application、cgi、thread
// BAD — shadows the `session` scope, breaking all session access
function login( required string user ) {
var session = { user: arguments.user } // DON'T do this
session.user = arguments.user // writes to local var, not session scope
}
// BAD — shadows the `url` scope
var url = parseUrl( input ) // url.params is now broken
// GOOD — use descriptive names that don't collide
function login( required string user ) {
var sessionData = { user: arguments.user }
session.user = arguments.user // correctly writes to session scope
}
// GOOD — use a different name
var urlParts = parseUrl( input )
var queryParams = url.params // url scope still accessible
需要在局部变量中引用作用域数据时,请使用描述数据的名称, 而不是作用域本身的名称:
| 不要使用 | 请使用 |
|---|---|
var session = ... | var sessionData、var userSession、var sess |
var request = ... | var requestData、var req、var httpRequest |
var url = ... | var urlParts、var currentUrl、var uri |
var form = ... | var formData、var formFields、var payload |
var application = ... | var appConfig、var appData、var settings |
var cgi = ... | var cgiData、var serverInfo |
var thread = ... | var threadInfo、var workerThread |
var server = ... | var serverInfo、var hostConfig |
作用域查找性能
BoxLang 每次访问未限定作用域的变量时,都会遍历作用域链。在热点代码路径中 (紧密循环、高流量请求处理程序),请显式指定变量作用域,以获得可预测的性能:
// GOOD in hot paths — no scope chain walk
var len = arguments.items.len()
for ( var i = 1; i <= len; i++ ) {
// process arguments.items[ i ]
}
函数
始终声明参数类型和必需状态
// BAD — no type information
function processOrder( order, userId ) { ... }
// GOOD — self-documenting, validated at runtime
function processOrder( required struct order, required numeric userId ) {
...
}
使用命名参数以提高清晰度
// Hard to read
createUser( "John", "Doe", "[email protected]", true )
// GOOD — named arguments document intent
createUser(
firstName = "John",
lastName = "Doe",
email = "[email protected]",
active = true
)
返回类型
为公共函数声明返回类型,以说明契约并提供更好的 IDE 支持:
struct function getUser( required numeric id ) {
return userService.find( arguments.id )
}
array function listActiveUsers() {
return userService.findByStatus( "active" )
}
错误处理
捕获特定异常类型
// BAD — catches everything, hides bugs
try {
processOrder( order )
} catch ( any e ) {
logError( e )
}
// GOOD — handle specific cases, re-throw unknown
try {
processOrder( order )
} catch ( "Database" e ) {
handleDatabaseError( e )
} catch ( "Validation" e ) {
return { success: false, message: e.message }
} catch ( any e ) {
// Re-throw unexpected errors
rethrow
}
使用 cffinally 执行清理
transaction {
try {
updateOrder( order )
chargePayment( payment )
transactionCommit()
} catch ( any e ) {
transactionRollback()
rethrow
}
}
Null 安全性
使用安全导航运算符(?.)和 Elvis 运算符(?:),以
避免空指针错误:
// Null-safe chained access
var city = user?.address?.city ?: "Unknown"
// Null-safe method calls
var count = order?.items?.len() ?: 0
优先使用 isNull(),而不是直接与 null 比较:
if ( isNull( result ) ) {
return getDefault()
}
闭包与 Lambda
仅对参数执行纯确定性操作时,使用 Lambda(->)。
访问外部作用域变量或调用外部函数/BIF 时,使用 闭包(=>)。
// Lambda — only uses the item argument (pure transform)
var doubled = numbers.map( ( n ) -> n * 2 )
// Closure — accesses outer variable `threshold`
var filtered = numbers.filter( ( n ) => n > threshold )
// Closure — calls external BIF
var upper = words.map( ( w ) => uCase( w ) )
Struct 和数组字面量
优先使用字面量语法,而不是构造函数:
// GOOD — literal syntax
var user = {
name: "Alice",
email: "[email protected]",
roles: [ "admin", "user" ]
}
// Avoid unless dynamic keys are needed
var user = structNew()
user.name = "Alice"
键顺序很重要时,请使用有序 Struct 字面量语法:
// Ordered struct (insertion order preserved)
var config = [=
host: "localhost",
port: 5432,
database: "myapp"
=]
字符串插值
在字符串和模板中使用 #expression# 进行插值。对于复杂
表达式,为提高可读性,请先将其赋值给变量:
// Simple interpolation
var message = "Hello, #user.name#!"
// Complex — extract first
var formattedDate = dateTimeFormat( now(), "long" )
var header = "Report generated on #formattedDate#"
组件(类)设计
构造函数模式
使用 init() 作为构造函数。返回 this 以支持链式构造:
class UserService {
property name="userRepo" inject="UserRepository"
function init( required UserRepository userRepository ) {
variables.userRepo = arguments.userRepository
return this
}
}
保持类职责聚焦(单一职责)
每个 .bx 类都应有一个主要用途。避免处理无关事务的“上帝对象”。将代码拆分为服务层、仓库层和模型层。
性能提示
- 缓存开销较大的查找 — 将结果存储在
application作用域中,用于共享的只读数据;数据变更时使缓存失效。 - 在生产环境中使用
trustedCache=true— 避免检查类文件时发生磁盘 I/O。 - 在构造函数中预先计算 — 如果某个值不会改变,就在实例化时计算一次。
- 处理集合时优先使用
each()/map()/filter(),而不是手动循环 — 可读性更好,也更适合 JIT。 - I/O 密集型异步工作使用虚拟线程(
runAsync默认使用);CPU 密集型工作使用固定线程池执行器。
代码组织
/app
/models -- Business domain classes (.bx)
/services -- Service layer classes (.bx)
/repositories -- Data access classes (.bx)
/handlers -- Request handlers / controllers (.bx)
/views -- Templates (.bxm)
/includes -- Reusable partial templates (.bxm)
/scripts -- Standalone scripts (.bxs)
Application.bx -- Application lifecycle
应避免的常见反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
| 函数中的变量未指定作用域 | 变量会泄漏到组件作用域 | 始终使用 var |
遮蔽作用域名称(session、url、form 等) | 无法访问内置作用域 | 使用 sessionData、formData 等描述性名称 |
静默捕获所有异常的 catch(any) | 吞掉意料之外的错误 | 重新抛出未知异常 |
| 在模板中编写逻辑 | 难以测试,职责分离不佳 | 移至服务或处理程序中 |
| 在处理程序中直接编写 SQL | 无法复用,存在 SQL 注入风险 | 使用带参数化查询的仓库类 |
| 在代码中存储机密 | 存在安全风险 | 在配置中通过 ${env.VAR_NAME} 使用环境变量 |
过度使用 application 作用域 | 会产生并发问题 | 对写操作使用适当的锁(bx:lock) |
arr[ 0 ](从零开始的索引) | ArrayIndexOutOfBoundsException | BoxLang 数组是从 1 开始索引:使用 arr[ 1 ] 或 arr.first() |
语句末尾的 ; | 风格冗杂且不一致 | 语句中的分号是可选的,可以省略 |
cfheader() / <cfabort> | 这是 CFML 语法,不是 BoxLang 语法 | 使用 bx:header、bx:abort 等 |
| 对 Java 对象使用命名参数 | 不受支持,会引发运行时错误 | 调用 Java 方法时始终使用位置参数 |
每次调用都使用 createObject("java","path") | 冗长且重复 | 使用一次 import java:fully.qualified.Class,然后直接使用类名 |
BoxLang 与 CFML 快速参考
BoxLang 源自 CFML,但许多结构使用了不同的语法。请勿在 BoxLang 源文件中
使用带 cf 前缀的标签或函数。
| CFML | BoxLang 等效写法 |
|---|---|
cfheader(name="X", value="Y") | bx:header name="X" value="Y"; |
cflocation(url="...") | bx:location url="..."; |
cfabort | bx:abort; |
cfparam name="x" default="" | bx:param name="x" default=""; |
cfinclude template="f.cfm" | bx:include template="f.bxm"; |
<cfsilent> | bx:silent { ... } |
createObject("java","path.Class") | import java:path.Class,然后使用 new java:path.Class() |
数组最佳实践
BoxLang 数组是从 1 开始索引的。对于习惯 Java/JavaScript 的开发者来说,这是一个常见的错误来源。
var items = [ "a", "b", "c" ]
// CORRECT
var first = items[ 1 ] // "a"
var last = items[ items.len() ] // "c"
var first = items.first() // preferred — more readable
var last = items.last() // preferred
// WRONG (throws ArrayIndexOutOfBoundsException)
var first = items[ 0 ]
// Looping — i starts at 1
for ( var i = 1; i <= items.len(); i++ ) {
process( items[ i ] )
}
向 Java 可变参数传递单个值
Java 可变参数方法需要 BoxLang 数组,而不是单个标量值:
// CORRECT — wrap in array
storage.query( "SELECT * FROM t WHERE id = ?", [ requestId ] )
// WRONG — bare value is not accepted by Java varargs
storage.query( "SELECT * FROM t WHERE id = ?", requestId ) // throws