12、Java 22 新特性:字符串模板与作用域值组合实战

兄弟们,鹏磊今天来聊聊字符串模板和作用域值这两个特性的组合使用,这玩意儿组合起来用,效果贼好。字符串模板让字符串拼接更简单,作用域值让上下文传递更安全,把它们组合起来,可以解决很多实际开发中的问题,比如日志记录、错误消息生成、用户上下文相关的字符串生成啥的。

字符串模板和作用域值都是 Java 22 的预览特性,它们可以很好地配合使用。字符串模板可以在字符串中嵌入表达式,作用域值可以在作用域内共享不可变数据,组合起来就是:在字符串模板中直接使用作用域值,生成包含上下文信息的字符串,不用到处传参数,代码更简洁,逻辑更清晰。这玩意儿特别适合需要根据上下文生成字符串的场景,比如日志记录、错误消息、用户信息展示啥的。

为什么需要组合使用

先说说单独使用的问题。虽然字符串模板和作用域值单独用都挺好,但组合起来用效果更好:

// 单独使用字符串模板:需要手动传参数
public void logMessage(String username, String action) {
    // 用字符串模板生成日志消息
    String message = STR."用户 \{username} 执行了操作 \{action}";  // 需要手动传参数
    System.out.println(message);
}

// 问题:每次调用都要传参数,如果参数很多,调用起来很麻烦
logMessage("张三", "登录");  // 需要传两个参数
logMessage("李四", "下单");  // 每次都要传

// 单独使用作用域值:需要手动拼接字符串
private static final ScopedValue<String> username = ScopedValue.newInstance();  // 定义作用域值

public void logMessage(String action) {
    String user = username.get();  // 获取用户名
    // 需要手动拼接字符串
    String message = "用户 " + user + " 执行了操作 " + action;  // 手动拼接,容易出错
    System.out.println(message);
}

// 问题:字符串拼接容易出错,代码可读性差

组合使用的好处是,既可以利用作用域值自动传递上下文,又可以利用字符串模板简化字符串生成,代码更简洁,逻辑更清晰。

基本组合用法

在字符串模板中使用作用域值

最基础的用法就是在字符串模板中直接使用作用域值:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义作用域值,用于存储用户名
private static final ScopedValue<String> currentUser = ScopedValue.newInstance();  // 创建作用域值

// 在作用域内使用字符串模板和作用域值
ScopedValue.where(currentUser, "张三").run(() -> {  // 绑定用户名"张三"
    // 在字符串模板中直接使用作用域值
    String message = STR."当前用户: \{currentUser.get()}";  // 在模板中使用作用域值
    System.out.println(message);  // 输出:当前用户: 张三
    
    // 可以组合多个作用域值
    processRequest();  // 处理请求,内部也会用到作用域值
});

// 处理请求的方法
void processRequest() {
    // 在字符串模板中使用作用域值,不需要传参数
    String log = STR."用户 \{currentUser.get()} 开始处理请求";  // 直接使用作用域值
    System.out.println(log);  // 输出:用户 张三 开始处理请求
}

这玩意儿的好处是,不用到处传参数,作用域值自动传递,字符串模板自动生成,代码更简洁。

多值组合使用

可以定义多个作用域值,在字符串模板中组合使用:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义多个作用域值
private static final ScopedValue<String> username = ScopedValue.newInstance();  // 用户名
private static final ScopedValue<String> traceId = ScopedValue.newInstance();  // 追踪ID
private static final ScopedValue<String> requestId = ScopedValue.newInstance();  // 请求ID

// 在作用域内绑定多个值
ScopedValue.where(username, "李四")  // 绑定用户名
    .where(traceId, "trace-12345")  // 绑定追踪ID
    .where(requestId, "req-67890")  // 绑定请求ID
    .run(() -> {  // 执行代码块
        // 在字符串模板中组合使用多个作用域值
        String log = STR."""
            用户: \{username.get()}
            追踪ID: \{traceId.get()}
            请求ID: \{requestId.get()}
            开始处理请求
            """;  // 多行模板,组合多个作用域值
        System.out.println(log);
        
        // 调用其他方法,所有作用域值都会自动传递
        processOrder();  // 处理订单
    });

// 处理订单的方法
void processOrder() {
    // 在字符串模板中使用多个作用域值
    String log = STR."用户 \{username.get()} (追踪: \{traceId.get()}) 处理订单";  // 组合使用
    System.out.println(log);  // 输出:用户 李四 (追踪: trace-12345) 处理订单
}

多值组合的好处是,可以同时传递多个上下文信息,在字符串模板中灵活组合,生成各种格式的字符串。

实际应用场景

场景1:日志记录

日志记录是最典型的应用场景,可以自动包含上下文信息:

import java.util.concurrent.ScopedValue;  // 导入作用域值类
import java.time.LocalDateTime;  // 导入时间类

// 定义日志相关的作用域值
private static final ScopedValue<String> username = ScopedValue.newInstance();  // 用户名
private static final ScopedValue<String> traceId = ScopedValue.newInstance();  // 追踪ID
private static final ScopedValue<String> requestId = ScopedValue.newInstance();  // 请求ID

// 日志记录方法
void logInfo(String message) {
    // 在字符串模板中使用作用域值,自动包含上下文信息
    String log = STR."""
        [\{LocalDateTime.now()}] INFO
        用户: \{username.get()}
        追踪ID: \{traceId.get()}
        请求ID: \{requestId.get()}
        消息: \{message}
        """;  // 多行日志,自动包含所有上下文信息
    System.out.println(log);
}

// 处理请求的方法
void handleRequest(String user, String reqId) {
    // 生成追踪ID
    String trace = "trace-" + System.currentTimeMillis();  // 生成追踪ID
    
    // 绑定作用域值
    ScopedValue.where(username, user)  // 绑定用户名
        .where(traceId, trace)  // 绑定追踪ID
        .where(requestId, reqId)  // 绑定请求ID
        .run(() -> {  // 执行代码块
            logInfo("开始处理请求");  // 记录日志,自动包含上下文
            processData();  // 处理数据
            logInfo("请求处理完成");  // 记录日志
        });
}

// 处理数据的方法
void processData() {
    logInfo("正在处理数据");  // 记录日志,自动包含上下文
    // 处理逻辑...
    logInfo("数据处理完成");  // 记录日志
}

// 使用示例
public static void main(String[] args) {
    var example = new LoggingExample();
    example.handleRequest("王五", "req-001");  // 处理请求
    // 所有日志都会自动包含用户、追踪ID、请求ID等信息
}

日志记录的好处是,不用在每个方法里都传上下文参数,作用域值自动传递,字符串模板自动生成格式化的日志,代码更简洁,日志更统一。

场景2:错误消息生成

错误消息生成也是常见场景,可以根据上下文生成详细的错误信息:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义错误相关的作用域值
private static final ScopedValue<String> username = ScopedValue.newInstance();  // 用户名
private static final ScopedValue<String> operation = ScopedValue.newInstance();  // 操作
private static final ScopedValue<String> resource = ScopedValue.newInstance();  // 资源

// 生成错误消息的方法
String generateError(String errorCode, String errorMessage) {
    // 在字符串模板中使用作用域值,生成详细的错误消息
    return STR."""
        错误代码: \{errorCode}
        错误消息: \{errorMessage}
        用户: \{username.get()}
        操作: \{operation.get()}
        资源: \{resource.get()}
        时间: \{java.time.LocalDateTime.now()}
        """;  // 多行错误消息,包含所有上下文信息
}

// 处理操作的方法
void performOperation(String user, String op, String res) {
    // 绑定作用域值
    ScopedValue.where(username, user)  // 绑定用户名
        .where(operation, op)  // 绑定操作
        .where(resource, res)  // 绑定资源
        .run(() -> {  // 执行代码块
            try {
                // 执行操作
                validatePermission();  // 验证权限
                processResource();  // 处理资源
            } catch (SecurityException e) {  // 捕获安全异常
                // 生成详细的错误消息
                String error = generateError("SECURITY_ERROR", "权限不足");  // 生成错误消息
                System.err.println(error);  // 输出错误
                throw e;  // 重新抛出异常
            } catch (Exception e) {  // 捕获其他异常
                // 生成详细的错误消息
                String error = generateError("OPERATION_ERROR", e.getMessage());  // 生成错误消息
                System.err.println(error);  // 输出错误
                throw e;  // 重新抛出异常
            }
        });
}

// 验证权限的方法
void validatePermission() {
    String user = username.get();  // 获取用户名
    if (!"admin".equals(user)) {  // 检查权限
        throw new SecurityException("权限不足");  // 抛出安全异常
    }
}

// 处理资源的方法
void processResource() {
    String res = resource.get();  // 获取资源
    if (res == null || res.isEmpty()) {  // 检查资源
        throw new IllegalArgumentException("资源不能为空");  // 抛出参数异常
    }
    // 处理逻辑...
}

错误消息生成的好处是,可以根据上下文自动生成详细的错误信息,不用手动拼接,代码更清晰,错误信息更完整。

场景3:用户信息展示

用户信息展示也是常见场景,可以根据上下文生成个性化的信息:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义用户信息记录
record UserInfo(String username, String role, String department) {}  // 用户信息记录

// 定义作用域值
private static final ScopedValue<UserInfo> userContext = ScopedValue.newInstance();  // 用户上下文

// 生成欢迎消息的方法
String generateWelcomeMessage() {
    UserInfo user = userContext.get();  // 获取用户信息
    // 在字符串模板中使用作用域值,生成个性化消息
    return STR."""
        欢迎, \{user.username()}!
        角色: \{user.role()}
        部门: \{user.department()}
        登录时间: \{java.time.LocalDateTime.now()}
        """;  // 多行欢迎消息,包含用户信息
}

// 生成操作提示的方法
String generateOperationHint(String operation) {
    UserInfo user = userContext.get();  // 获取用户信息
    // 根据用户角色生成不同的提示
    String hint = switch (user.role()) {  // 根据角色生成提示
        case "admin" -> "您拥有管理员权限,可以执行所有操作";  // 管理员提示
        case "user" -> "您拥有普通用户权限,可以执行基本操作";  // 普通用户提示
        default -> "您的权限有限,请谨慎操作";  // 默认提示
    };
    
    // 在字符串模板中使用作用域值和提示
    return STR."""
        操作: \{operation}
        用户: \{user.username()}
        提示: \{hint}
        """;  // 操作提示,包含用户信息和角色提示
}

// 处理用户请求的方法
void handleUserRequest(UserInfo user, String operation) {
    // 绑定用户上下文
    ScopedValue.where(userContext, user).run(() -> {  // 绑定用户信息
        // 显示欢迎消息
        String welcome = generateWelcomeMessage();  // 生成欢迎消息
        System.out.println(welcome);  // 输出欢迎消息
        
        // 显示操作提示
        String hint = generateOperationHint(operation);  // 生成操作提示
        System.out.println(hint);  // 输出操作提示
        
        // 执行操作
        performOperation(operation);  // 执行操作
    });
}

// 执行操作的方法
void performOperation(String operation) {
    UserInfo user = userContext.get();  // 获取用户信息
    // 在字符串模板中使用作用域值,记录操作
    String log = STR."用户 \{user.username()} 执行了操作 \{operation}";  // 记录操作
    System.out.println(log);  // 输出日志
}

用户信息展示的好处是,可以根据用户上下文自动生成个性化的信息,不用到处传用户参数,代码更简洁,用户体验更好。

场景4:请求追踪

请求追踪也是常见场景,可以在日志中自动包含追踪信息:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义追踪相关的作用域值
private static final ScopedValue<String> traceId = ScopedValue.newInstance();  // 追踪ID
private static final ScopedValue<String> spanId = ScopedValue.newInstance();  // 跨度ID
private static final ScopedValue<String> parentSpanId = ScopedValue.newInstance();  // 父跨度ID

// 生成追踪日志的方法
void traceLog(String level, String message) {
    // 在字符串模板中使用作用域值,生成追踪日志
    String log = STR."""
        [\{level}] \{java.time.LocalDateTime.now()}
        TraceID: \{traceId.get()}
        SpanID: \{spanId.get()}
        ParentSpanID: \{parentSpanId.orElse("N/A")}
        Message: \{message}
        """;  // 追踪日志,包含所有追踪信息
    System.out.println(log);
}

// 开始新的跨度
void startSpan(String spanName) {
    // 生成新的跨度ID
    String newSpanId = "span-" + System.currentTimeMillis();  // 生成跨度ID
    String currentSpanId = spanId.isBound() ? spanId.get() : null;  // 获取当前跨度ID
    
    // 绑定新的跨度信息
    ScopedValue.where(traceId, traceId.isBound() ? traceId.get() : newSpanId)  // 追踪ID
        .where(spanId, newSpanId)  // 新的跨度ID
        .where(parentSpanId, currentSpanId != null ? currentSpanId : "root")  // 父跨度ID
        .run(() -> {  // 执行代码块
            traceLog("INFO", "开始跨度: " + spanName);  // 记录开始
            // 执行跨度逻辑...
            traceLog("INFO", "结束跨度: " + spanName);  // 记录结束
        });
}

// 处理请求的方法
void processRequest(String requestId) {
    // 生成追踪ID
    String trace = "trace-" + System.currentTimeMillis();  // 生成追踪ID
    
    // 绑定追踪信息
    ScopedValue.where(traceId, trace)  // 绑定追踪ID
        .where(spanId, "root")  // 根跨度
        .where(parentSpanId, "N/A")  // 无父跨度
        .run(() -> {  // 执行代码块
            traceLog("INFO", "开始处理请求: " + requestId);  // 记录开始
            startSpan("validate");  // 开始验证跨度
            startSpan("process");  // 开始处理跨度
            traceLog("INFO", "请求处理完成: " + requestId);  // 记录完成
        });
}

请求追踪的好处是,可以在日志中自动包含追踪信息,不用手动传递,代码更简洁,追踪更完整。

高级用法

自定义模板处理器

可以自定义模板处理器,结合作用域值实现更复杂的功能:

import java.util.concurrent.ScopedValue;  // 导入作用域值类
import java.util.function.Function;  // 导入函数接口

// 定义日志级别作用域值
private static final ScopedValue<String> logLevel = ScopedValue.newInstance();  // 日志级别

// 自定义日志模板处理器
static Function<StringTemplate, String> LOG = template -> {  // 自定义处理器
    String level = logLevel.isBound() ? logLevel.get() : "INFO";  // 获取日志级别
    String message = template.interpolate();  // 处理模板
    return STR."[\{level}] \{java.time.LocalDateTime.now()} - \{message}";  // 生成日志
};

// 使用自定义处理器
void logWithLevel(String level, String message) {
    // 绑定日志级别
    ScopedValue.where(logLevel, level).run(() -> {  // 绑定日志级别
        // 使用自定义处理器
        String log = LOG."\{message}";  // 使用自定义处理器
        System.out.println(log);  // 输出日志
    });
}

// 使用示例
public static void main(String[] args) {
    var example = new AdvancedExample();
    example.logWithLevel("ERROR", "发生错误");  // 记录错误日志
    example.logWithLevel("WARN", "警告信息");  // 记录警告日志
    example.logWithLevel("INFO", "普通信息");  // 记录信息日志
}

自定义模板处理器的好处是,可以实现更复杂的字符串处理逻辑,结合作用域值实现更灵活的功能。

嵌套作用域

可以嵌套使用作用域值,在不同层级传递不同的上下文:

import java.util.concurrent.ScopedValue;  // 导入作用域值类

// 定义不同层级的作用域值
private static final ScopedValue<String> requestId = ScopedValue.newInstance();  // 请求ID
private static final ScopedValue<String> operationId = ScopedValue.newInstance();  // 操作ID
private static final ScopedValue<String> stepId = ScopedValue.newInstance();  // 步骤ID

// 处理请求
void handleRequest(String reqId) {
    // 外层作用域:请求级别
    ScopedValue.where(requestId, reqId).run(() -> {  // 绑定请求ID
        String log1 = STR."请求开始: \{requestId.get()}";  // 使用请求ID
        System.out.println(log1);
        
        // 内层作用域:操作级别
        ScopedValue.where(operationId, "op-001").run(() -> {  // 绑定操作ID
            String log2 = STR."操作开始: \{requestId.get()} -> \{operationId.get()}";  // 组合使用
            System.out.println(log2);
            
            // 更内层作用域:步骤级别
            ScopedValue.where(stepId, "step-001").run(() -> {  // 绑定步骤ID
                String log3 = STR."步骤执行: \{requestId.get()} -> \{operationId.get()} -> \{stepId.get()}";  // 组合使用
                System.out.println(log3);
            });
        });
    });
}

嵌套作用域的好处是,可以在不同层级传递不同的上下文,在字符串模板中灵活组合,生成更详细的追踪信息。

注意事项和最佳实践

1. 作用域值要在作用域内使用

作用域值只能在绑定的作用域内使用,否则会抛出异常:

// 错误示例:在作用域外使用
ScopedValue.where(username, "张三").run(() -> {
    // 在作用域内可以使用
    String msg = STR."用户: \{username.get()}";  // 正确
});

// 在作用域外使用会抛出异常
// String msg = STR."用户: \{username.get()}";  // 错误!会抛出异常

// 正确示例:检查是否绑定
if (username.isBound()) {  // 检查是否绑定
    String msg = STR."用户: \{username.get()}";  // 安全使用
} else {
    String msg = STR."用户: 未知";  // 使用默认值
}

2. 字符串模板是预览特性

字符串模板是预览特性,需要启用预览特性:

# 编译时启用预览特性
javac --enable-preview --release 22 Main.java

# 运行时启用预览特性
java --enable-preview Main

3. 作用域值是不可变的

作用域值一旦绑定就不能修改,这保证了数据安全:

// 作用域值是不可变的
ScopedValue.where(username, "张三").run(() -> {
    // 不能修改绑定的值
    // username.set("李四");  // 错误!没有 set 方法
    
    // 可以创建新的作用域,绑定新值
    ScopedValue.where(username, "李四").run(() -> {  // 新作用域,新值
        String msg = STR."用户: \{username.get()}";  // 使用新值
    });
});

4. 性能考虑

作用域值的性能很好,读取速度跟本地变量差不多,但字符串模板处理需要一些开销:

// 性能考虑:
// - 作用域值读取很快,跟本地变量差不多
// - 字符串模板处理需要一些开销,但对于大多数场景可以忽略
// - 如果性能敏感,可以考虑缓存处理结果

总结

字符串模板和作用域值组合使用,可以解决很多实际开发中的问题。字符串模板让字符串生成更简单,作用域值让上下文传递更安全,组合起来就是:在字符串模板中直接使用作用域值,生成包含上下文信息的字符串,不用到处传参数,代码更简洁,逻辑更清晰。

这玩意儿特别适合需要根据上下文生成字符串的场景,比如日志记录、错误消息、用户信息展示、请求追踪啥的。而且作用域值是不可变的,保证了数据安全,字符串模板可以防止注入攻击,组合起来用既安全又方便。

不过要注意,字符串模板是预览特性,需要启用预览特性才能用,而且可能会在未来的版本中变化。作用域值也只能在绑定的作用域内使用,否则会抛出异常。

好了,今天就聊到这里,兄弟们有啥问题可以在评论区留言,鹏磊看到会回复的。下次咱们聊聊 Java 22 的其他新特性,比如结构化并发与作用域值协同应用,这玩意儿也挺有意思的。

本文章最后更新于 2025-11-27