Java 22 字符串模板实战:告别臃肿的字符串拼接和SQL注入风险

2026-07-31 0 128

写Java的人对字符串拼接再熟悉不过了。早些年用加号一个字段一个字段地拼,后来用String.format或者StringBuilder,再后来有了文本块,写SQL和JSON总算不用在一行里塞几十个转义符了。但文本块只解决了格式问题,变量还得靠String.format的占位符往里填,参数多了顺序就容易搞错,而且String.format完全不管内容是什么,直接把变量塞进字符串——如果变量里夹带了SQL注入代码,它也照塞不误。

Java 21引入了字符串模板的第一次预览,22做了第二次预览并趋于稳定。它把字符串和表达式的嵌入统一在了一个语法里,而且可以通过不同的模板处理器来决定最终生成什么——既能生成普通字符串,也能生成预编译的SQL语句、JSON对象,甚至HTML文档。这篇文章用一个订单查询系统的重构案例,把字符串模板从基础语法到自定义处理器走一遍,重点放在它怎么解决实际的安全和可读性问题。

过去拼接字符串的几种方式与它们的痛点

先看一个典型的场景:根据用户输入动态构建SQL查询条件。传统的写法大概是这样:

String customerName = request.getParameter("customerName");
String status = request.getParameter("status");
String startDate = request.getParameter("startDate");

String sql = "SELECT o.order_id, o.amount, o.status, c.name " +
             "FROM orders o JOIN customers c ON o.customer_id = c.id " +
             "WHERE 1=1 ";
if (customerName != null && !customerName.isEmpty()) {
    sql += "AND c.name LIKE '%" + customerName + "%' ";
}
if (status != null && !status.isEmpty()) {
    sql += "AND o.status = '" + status + "' ";
}
if (startDate != null && !startDate.isEmpty()) {
    sql += "AND o.created_at >= '" + startDate + "' ";
}
sql += "ORDER BY o.created_at DESC";

这段代码有至少三个问题。首先,SQL注入风险直接暴露——customerNamestatus被原封不动地拼进SQL字符串里,一个有恶意企图的用户可以在参数里写' OR '1'='1来绕过所有条件。其次,可读性差,字符串拼接的语法噪音盖过了SQL语句本身的结构。第三,维护成本高,每次新增查询条件都要在if判断和拼串两个地方改,容易漏掉单引号或百分号。

后来的改进方案是用PreparedStatement加参数占位符?,能解决SQL注入,但问题又来了——动态条件(WHERE子句的数量不固定)让PreparedStatement的参数索引变得脆弱,多一个条件少一个条件就得调整所有参数的位置,很容易对不上号。

文本块解决了一部分格式问题,但它本质还是字符串,往里填变量依然需要String.formatString::formatted

String sql = """
        SELECT o.order_id, o.amount, o.status, c.name
        FROM orders o JOIN customers c ON o.customer_id = c.id
        WHERE 1=1
        %s
        %s
        %s
        ORDER BY o.created_at DESC
        """.formatted(nameCondition, statusCondition, dateCondition);

占位符和参数分离,顺序容易错,而且依然没有解决安全问题——它只是在拼字符串,不知道这段字符串最终要交给数据库执行。

字符串模板的语法与内置处理器

字符串模板的核心语法是{表达式},配合一个模板处理器来定义如何处理这些嵌入值。Java 22提供了三个内置处理器:STRFMTRAW

STR是最基本的处理器,它把嵌入的表达式求值后直接替换成字符串,相当于一个强类型的String.format替代品:

import static java.lang.StringTemplate.STR;

String customerName = "张三";
int orderCount = 5;
String message = STR."客户{customerName}有{orderCount}笔订单";
// 结果: "客户张三有5笔订单"

String.format最大的不同是,表达式直接写在字符串里面,不再需要离开字符串去参数列表里找对应的值。变量的位置一目了然,不用数%s的个数。而且表达式可以是任意Java表达式,包括方法调用:

String summary = STR."总金额:{order.getAmount().setScale(2, RoundingMode.HALF_UP)}元";

FMT处理器在STR的基础上支持格式化,类似printf的格式说明符:

import static java.util.FormatProcessor.FMT;

double rate = 0.875;
String report = FMT."完成率:%.2f{rate}";
// 结果: "完成率:0.88"

RAW处理器不处理嵌入值,返回一个包含原始模板片段和表达式的StringTemplate对象,通常用来给自定义处理器做中间步骤。

语法上要注意两点:模板表达式必须以处理器开头(STR.FMT.等),中间没有空格;表达式用{}包裹,和JavaScript的模板字符串反引号`不同,Java用的是双引号"配合处理器前缀。

自定义模板处理器:安全地构建SQL

字符串模板真正强大的地方在于可以自己写处理器。不同的处理器可以把同一个模板转换成不同类型的结果。对于SQL场景,我们可以写一个处理器,让它不直接拼字符串,而是生成一个包含参数化SQL和参数列表的对象,彻底杜绝注入风险。

先定义一个存储预编译SQL和参数的数据结构:

public record ParameterizedSql(String sql, List parameters) {
    public ParameterizedSql {
        parameters = List.copyOf(parameters);
    }
}

然后实现一个SQL模板处理器,实现StringTemplate.Processor接口:

import java.util.ArrayList;
import java.util.List;
import java.lang.StringTemplate;

public class SqlProcessor implements StringTemplate.Processor {

    public static final SqlProcessor SQL = new SqlProcessor();

    private SqlProcessor() {}

    @Override
    public ParameterizedSql process(StringTemplate st) {
        StringBuilder sb = new StringBuilder();
        List params = new ArrayList();

        // st.fragments() 是模板的静态文本片段
        // st.values() 是嵌入表达式的值列表
        List fragments = st.fragments();
        List values = st.values();

        for (int i = 0; i < fragments.size(); i++) {
            sb.append(fragments.get(i));
            if (i < values.size()) {
                // 用 ? 占位符替代直接拼接值
                sb.append("?");
                params.add(values.get(i));
            }
        }

        return new ParameterizedSql(sb.toString(), params);
    }
}

这个处理器的原理很简单:遍历模板的静态片段和动态值,静态部分原样保留,动态值不拼进SQL,而是换成?占位符,值本身存入参数列表。返回的ParameterizedSql可以直接喂给PreparedStatement

使用起来就很直观了。重构前面的订单查询:

import static com.example.template.SqlProcessor.SQL;

public List queryOrders(String customerName, String status, String startDate) {
    String baseSql = """
        SELECT o.order_id, o.amount, o.status, c.name
        FROM orders o JOIN customers c ON o.customer_id = c.id
        WHERE 1=1
        """;

    // 动态条件
    String nameCondition = customerName != null && !customerName.isEmpty()
        ? "AND c.name LIKE " + SQL."%{customerName}%"
        : "";
    // 这里注意:nameCondition 本身已经是 SQL 模板处理后的结果的一部分
    // 实际使用时可以把条件也纳入模板中

    // 更优雅的做法是把整个SQL写在一个模板里
    // 为了让动态条件也能处理,可以分段构建
    ParameterizedSql sql;
    if (customerName != null && status != null) {
        sql = SQL."""
            SELECT o.order_id, o.amount, o.status, c.name
            FROM orders o JOIN customers c ON o.customer_id = c.id
            WHERE c.name LIKE {'%' + customerName + '%'}
            AND o.status = {status}
            ORDER BY o.created_at DESC
            """;
    } else if (customerName != null) {
        sql = SQL."""
            SELECT o.order_id, o.amount, o.status, c.name
            FROM orders o JOIN customers c ON o.customer_id = c.id
            WHERE c.name LIKE {'%' + customerName + '%'}
            ORDER BY o.created_at DESC
            """;
    } else {
        sql = SQL."""
            SELECT o.order_id, o.amount, o.status, c.name
            FROM orders o JOIN customers c ON o.customer_id = c.id
            ORDER BY o.created_at DESC
            """;
    }

    // 用 PreparedStatement 执行
    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql.sql())) {
        for (int i = 0; i < sql.parameters().size(); i++) {
            ps.setObject(i + 1, sql.parameters().get(i));
        }
        ResultSet rs = ps.executeQuery();
        // 映射结果集...
    }
}

现在SQL语句的结构清晰明了,变量通过{}直接嵌入在SQL文本中,可读性大幅提升。而且变量值是通过?占位符传入PreparedStatement的,和SQL语句本身完全分离,注入风险从语法层面被消除了。就算customerName传了'; DROP TABLE orders; --,它也只被当作一个普通的字符串参数值,永远不会变成SQL的一部分。

完整案例:一个带模板的订单报表系统

现在把字符串模板用到更完整的场景里。假设我们需要生成一份订单报表,包含HTML格式的邮件正文和JSON格式的日志记录。

先写一个HTML模板处理器,对嵌入值做转义防止XSS:

public class HtmlProcessor implements StringTemplate.Processor {

    public static final HtmlProcessor HTML = new HtmlProcessor();

    private HtmlProcessor() {}

    @Override
    public String process(StringTemplate st) {
        StringBuilder sb = new StringBuilder();
        List fragments = st.fragments();
        List values = st.values();

        for (int i = 0; i < fragments.size(); i++) {
            sb.append(fragments.get(i));
            if (i < values.size()) {
                // 对嵌入值做HTML转义
                String escaped = escapeHtml(String.valueOf(values.get(i)));
                sb.append(escaped);
            }
        }
        return sb.toString();
    }

    private String escapeHtml(String input) {
        return input
            .replace("&", "&")
            .replace("", ">")
            .replace(""", """);
    }
}

接下来是报表生成的业务代码:

import static com.example.template.HtmlProcessor.HTML;
import static java.lang.StringTemplate.STR;

public class ReportService {

    public String generateEmailBody(Order order, Customer customer) {
        return HTML."""
            <html>
            <body>
                <h2>订单确认</h2>
                <p>尊敬的 {customer.getName()}:</p>
                <p>您的订单 <strong>{order.getOrderId()}</strong> 已确认。</p>
                <table border="1">
                    <tr><th>商品</th><th>数量</th><th>单价</th></tr>
                    {generateOrderRows(order)}
                </table>
                <p>总金额:<strong>{order.getTotalAmount()}元</strong></p>
                <p>预计发货时间:{order.getEstimatedShipDate()}</p>
            </body>
            </html>
            """;
    }

    private String generateOrderRows(Order order) {
        StringBuilder rows = new StringBuilder();
        for (OrderItem item : order.getItems()) {
            rows.append(HTML."""
                <tr>
                    <td>{item.getProductName()}</td>
                    <td>{item.getQuantity()}</td>
                    <td>{item.getUnitPrice()}元</td>
                </tr>
                """);
        }
        return rows.toString();
    }

    public String generateJsonLog(Order order) {
        // 对于JSON,值不需要转义HTML,但需要处理引号
        return STR."""
            {
                "orderId": "{order.getOrderId()}",
                "customer": "{order.getCustomerName()}",
                "amount": {order.getTotalAmount()},
                "status": "{order.getStatus()}",
                "createdAt": "{order.getCreatedAt()}"
            }
            """;
    }
}

这个报表模块用到了三种处理器:HTML负责邮件正文的安全输出,STR负责日志JSON的简单拼接,SQL负责底层的数据查询。每种处理器封装了不同的拼接策略,但业务代码的写法完全一致——都是处理器."模板文本{表达式}"

与传统方式的对比

回顾一下用字符串模板前后的代码变化。传统的StringBuilder拼HTML:

StringBuilder sb = new StringBuilder();
sb.append("

订单确认

"); sb.append("

尊敬的"); sb.append(escapeHtml(customer.getName())); sb.append(":

"); sb.append("

您的订单 "); sb.append(escapeHtml(order.getOrderId())); sb.append(" 已确认。

"); // 十几行下去...

不仅写起来繁琐,还容易漏掉escapeHtml调用,留下XSS隐患。字符串模板把HTML结构原样呈现,转义逻辑封装在处理器内部,写代码的人不用记住“这里该转义、那里不用”——处理器替你做决定。

SQL的例子同样明显。用PreparedStatement手动管理参数索引,一旦SQL结构调整,参数的序号就得重新编排,维护起来很痛苦。字符串模板的SQL处理器把这个索引管理自动化了,你只管把变量放在该放的位置,占位符?和参数列表的对应关系由处理器保证。

性能方面的考量

字符串模板在编译时会尽量优化。对于只包含常量的模板,编译器可以在编译期就完成拼接。STRFMT作为内置处理器,JVM对其有专门优化路径,性能与StringBuilder接近。自定义处理器由于涉及额外的对象创建和循环处理,在高频调用场景下会有一定开销,但通常远小于网络IO或数据库查询的耗时,在绝大多数业务代码里不会成为瓶颈。

如果确实需要在极热路径上使用自定义处理器,可以考虑把StringTemplate对象缓存下来,只替换每次不同的值,避免重复解析模板片段。

需要注意的几个地方

预览特性需要编译参数。 在Java 22中字符串模板仍处于第二次预览,编译和运行时需要加--enable-preview参数。Maven和Gradle都有对应的配置方式。预计在后续版本中转正,但目前在产线使用需要评估风险。

自定义处理器的职责划分。 处理器应该专注于“如何组合片段和值”,不要在里面写业务逻辑。业务逻辑放在模板外面的表达式里,处理器只负责格式转换和安全过滤。保持这个边界,代码才好测试和维护。

不要滥用。 不是所有的字符串拼接都需要模板。简单的两个字符串拼接用+就行,STR的优势在于多变量、有结构文本的场景。一句话拼两个变量用模板反而显得杀鸡用牛刀。

总结

字符串模板并不是一个革命性的性能突破,它做的事情StringBuilder也能做。但它在三个维度上实实在在地改善了Java开发者的体验:一是可读性,模板文本和表达式放在一起,不用跳来跳去;二是安全性,通过自定义处理器把SQL注入和XSS防御固化为框架级别的规则,而不是依赖于每个开发者的安全意识;三是一致性,SQL、JSON、HTML、普通日志都用同一套语法,处理器不同但写法相同。

订单查询和报表生成的案例已经把日常开发中最常见的几种字符串处理场景覆盖了。如果你的项目已经在用Java 22做实验性构建,不妨把这些自定义处理器放到公共库里,团队里所有人都能享受到模板表达式带来的整洁与安全。

Java 22 字符串模板实战:告别臃肿的字符串拼接和SQL注入风险
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java 22 字符串模板实战:告别臃肿的字符串拼接和SQL注入风险 https://www.taomawang.com/server/java/2461.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务

声明:本站免费开源项目仅学习使用商用及产生法律纠纷本站概不负责!如果侵犯了您的权益请发送邮件1506151422@qq.com将立刻删除 || © 2022 淘吗网 -TAOMAWANG.COM 网站地图 蜀ICP备2024093326号

今日推荐码支付平台:https://mpay.xbwlkj.top 稳定多通道

关闭