字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
白歌:上周线上出 P0——日志把用户密码明文打印出来了。 大翔:根因是
"...WHERE pwd = '" + pwd + "'"拼 SQL 被注入。 小崔:我之前String.format("age=%d", getName())也是编译过、运行炸。 孔蓝:那有没有让编译器提前抓错、还能防注入的写法? 大翔:有,JDK 22 引入的字符串模板(String Templates) 就是答案。
一、为什么需要字符串模板
| 痛点 | 描述 | 后果 |
|---|---|---|
| 性能差 | + 拼接在循环中产生大量中间对象 | GC 压力、内存抖动 |
| 类型不安全 | String.format("age=%d", "abc") 编译通过 | 运行期才抛异常 |
| 注入风险 | 字符串拼接构造 SQL/HTML | SQL 注入、XSS 攻击 |
二、字符串模板定义
| 术语 | 定义 |
|---|---|
| 字符串模板 | 由模板表达式 + 模板处理器组成的预览特性 |
| 模板表达式 | 含 \{expression} 嵌入表达式的字符串字面量 |
| 模板处理器 | 接收片段与嵌入值并生成结果的函数对象 |
| 嵌入表达式 | \{...} 内的 Java 表达式,编译期求值并类型检查 |
三、基本语法
3.1 STR 处理器(字符串插值)
// === 场景说明:使用 STR 处理器进行最基础的字符串插值 ===
public class StringTemplateDemo {
public static void main(String[] args) {
String name = "白歌";
int age = 28;
System.out.println(STR."我叫 \{name},今年 \{age} 岁");
// null 自动处理为 "null",不会抛 NPE
System.out.println(STR."昵称:\{(String) null}");
}
}
我叫 白歌,今年 28 岁
昵称:null
3.2 FMT 处理器(格式化)
// === 场景说明:使用 FMT 处理器对数值进行格式化 ===
import static java.util.FormatProcessor.FMT;
public class FmtDemo {
public static void main(String[] args) {
double price = 199.9956;
int qty = 3;
System.out.println(FMT."订单:%.2f\{price} 元 × %04d\{qty} 件");
}
}
订单:200.00 元 × 0003 件
3.3 RAW 处理器(原始模板)
// === 场景说明:使用 RAW 处理器获取原始模板片段 ===
import static java.util.FormatProcessor.RAW;
public class RawDemo {
public static void main(String[] args) {
StringTemplate st = RAW."价格 = \{199.99}";
System.out.println("片段:" + st.fragments() + ",值:" + st.values().get(0));
}
}
片段:[价格 = , ],值:199.99
四、工作原理
五、自定义处理器:SQL 参数化
// === 场景说明:自定义 SQL 处理器,把拼接转换为 ? 占位符 + 参数列表 ===
import java.util.List;
public class SqlProcessor {
public record Result(String sql, List<Object> params) {}
public static Result safe(StringTemplate st) {
return new Result(String.join("?", st.fragments()), st.values());
}
}
public class SqlTemplateDemo {
public static void main(String[] args) {
String name = "白歌' OR '1'='1"; // 注入攻击字符串
var r = SqlProcessor.safe(STR."SELECT * FROM users WHERE name = '\{name}' AND age = \{28}");
System.out.println("SQL: " + r.sql());
System.out.println("Params: " + r.params());
}
}
SQL: SELECT * FROM users WHERE name = '?' AND age = ?
Params: [白歌' OR '1'='1, 28]
攻击字符串作为参数绑定,永不进入 SQL 文本,从源头杜绝注入。
六、字符串模板与文本块结合
// === 场景说明:文本块 + 字符串模板构造多行 JSON ===
public class JsonTemplateDemo {
record User(String name, int age) {}
public static void main(String[] args) {
User u = new User("孔蓝", 25);
String json = STR."""
{
"name": "\{u.name()}",
"age": \{u.age()}
}""";
System.out.println(json);
}
}
{
"name": "孔蓝",
"age": 25
}
七、实战:飞翔科技类型安全日志
// === 场景说明:类型安全日志,避免泄露敏感字段 ===
import java.util.logging.Logger;
public class SafeLogDemo {
static final Logger LOG = Logger.getLogger("fly");
public static void main(String[] args) {
String userId = "u001";
// 只引用 userId,编译期就能看到不会误打 pwd
LOG.info(STR."用户 \{userId} 登录成功");
}
}
信息: 用户 u001 登录成功
八、易错场景
// === 场景说明:常见错误 vs 正确写法对比 ===
public class CommonMistakes {
public static void main(String[] args) {
int age = 18;
// ❌ 忘记 \{...},age 当字面量
// String s1 = STR."age=age";
// ✅ 必须用 \{...} 包裹
System.out.println(STR."age=\{age}");
// ❌ 嵌入表达式里写分号(是表达式不是语句)
// ✅ 表达式不能有分号
System.out.println(STR."next=\{age + 1}");
// ❌ 忘记加 --enable-preview,编译报错
}
}
age=18
next=19
九、面试考点
Q1:字符串模板相比 String.format 的核心优势是什么?
- 类型安全:
\{expr}编译期检查类型,不匹配直接编译失败。- 可扩展:自定义处理器可拦截拼接过程,实现 SQL 参数化、JSON 转义、HTML 编码。
- 可读:插值与原文本同处一行,不像
%d %s那样按位置对暗号。
Q2:什么是模板处理器?内置哪几个?
实现
StringTemplate.Processor接口的函数对象,接收片段与值并输出结果。JDK 内置:STR(字符串插值)、FMT(带String.format格式化)、RAW(原样返回模板对象供自定义处理器使用)。
Q3:字符串模板如何防止 SQL 注入?
传统
+拼接让用户输入直接进入 SQL 文本。字符串模板通过自定义处理器把模板转为「?占位符 + 参数列表」,由 JDBC 预编译语句把参数作为数据绑定而非 SQL 语法的一部分,从而彻底阻断注入。
"好的 API 不是让正确的事变容易,而是让错误的事变困难。" —— 字符串模板正是这句话的写照。