漫谈 Java 轻量级的模板技术:从字符串替换到复杂模板

2025-12-12 0 829

提起模板技术相信每个 Java 开发者都不会陌生。虽然目前已经很少在 Java 后端开发前端页面了,但是在不同场合还是会用到模板或者应用模板的相关概念,例如把 SQL 写在 XML 文件中(MyBatis 的做法),里面使用了大量表达式和/流程判断,也属于模板的一种变种。更不用说早期 MVC 时代,各种 Java 模板技术百花齐放(如 JSP、FreeMarker、Velocity 等等)。本质上讲,模板就是把不变的内容固定好,然后预留特定的位置给变化的内容,这个位置相当于一种插值(有时候称为“占位符”,本质也是引入第三方变量的概念),等到待确定的时候替换为真实的值,形成最终的字符串内容。

无索引的替换

上述的这一原理一点都不复杂,可以说在日常字符串中都经常使用,例如每个初学者都是接触的String.format()方法:

System.out.println(String.format(\"你好 %s\", \"张三\"));

又或者使用 System.out.println 已为我们考虑了的方法:

System.out.printf(\"你好 %s%n\", \"张三\"); // 功能等价

本质上仍是字符串的替换:

System.out.println(\"你好 %s\".replace(\"%s\", \"张三\"));

类似地,SLF4J 日志框架也采用了占位符机制:

log.info(\"你好,{}。\", \"李四\");

slf4j 的方式怎么实现的呢?也不复杂,下面是一个简化版的实现:

private static final String DELIM_STR = \"{}\";

/**
 * 格式化字符串模板,将模板中的占位符替换为对应的参数值
 *
 * @param tpl  字符串模板,其中包含占位符
 * @param args 可变参数列表,用于替换模板中的占位符
 * @return 格式化后的字符串,占位符被对应的参数值替换
 */
public static String print(String tpl, Object... args) {
    StringBuilder buffer = new StringBuilder(tpl.length() + 64);
    int beginIndex = 0, endIndex, count = 0;

    while ((endIndex = tpl.indexOf(DELIM_STR, beginIndex)) >= 0) {
        buffer.append(tpl, beginIndex, endIndex);

        try {
            buffer.append(args[count++]);
        } catch (IndexOutOfBoundsException e) {
            buffer.append(\"null\"); // 数组越界时对应占位符填 null
        }

        beginIndex = endIndex + DELIM_STR.length();
    }

    buffer.append(tpl.substring(beginIndex));

    return buffer.toString();
}

// 测试
System.out.println(print(\"{} {} {}\", \"a\", \"b\", \"c\"));// 输出: a b c

带数字索引的替换

使用%s或者{}由于没有次序,参数顺序完全依赖位置,参数多了话容易混乱,为此,可以使用MessageFormat,支持通过索引引用参数:

import java.text.MessageFormat;

String template = \"\"\"
    Hello {0},
    You have {1} new messages.
    \"\"\";

String result = MessageFormat.format(template, \"Alice\", 5);

注意:这里应用到 Java 11 开始支持的多行文本,但还不支持类似 js/ts 那种字符串模板,否则爽多了。

可进一步封装为通用方法:

public static String render(String template, Object... args) {
	return MessageFormat.format(template, args);
}

// 测试
String result = MessageFormat.format(\"您好{0},晚上好!您目前余额:{1,number,#.##}元,积分:{2}\", \"张三\", 10.155, 10);
System.out.println(result);
// 输出: 您好张三,晚上好!余额:10.16元,积分:10

基于命名占位符的替换

有了数字 index 好了一点,但还是不够直观清晰,应该是可以把字符串作为索引来插值的,例如你好 ${who}这样的。这个通常被认为是“表达式 Expression”,不仅仅是取值的占位符,还能够和编程语言那样参与运算,功能强大。这样的话就与正式的表达式概念无异。不过,如果只是为了作为模板里面的取值,我们用下面的一个函数就可以实现。

private static final Pattern TPL_PATTERN = Pattern.compile(\"\\$\\{\\w+}\");

/**
 * 简单模板替换方法。根据 Map 中的数据进行替换
 *
 * @param template 待替换的字符串模板
 * @param params   存放替换数据的 Map
 * @return 替换后的字符串
 */
public static String simpleTpl(String template, Map params) {
    StringBuffer sb = new StringBuffer();
    Matcher m = TPL_PATTERN.matcher(template);

    while (m.find()) {
        String param = m.group();
        // 获取要替换的键名,即去除 \'${\' 和 \'}\' 后的部分
        Object value = params.get(param.substring(2, param.length() - 1));
        m.appendReplacement(sb, value == null ? CommonConstant.EMPTY_STRING : value.toString());// 替换键值对应的值,若值为 null,则置为空字符串
    }

    m.appendTail(sb);

    return sb.toString();
}

这里约定占位符的格式为${xxx},与我们常见的一致。这里使用了正则来实现,而下面的一个例子则没有使用正则去匹配,而是反过来用值加上标签去替换值,思路又不一样了。

/**
 * 简单模板替换方法。根据 Map 中的数据进行替换。
 * 与 simpleTpl 方法的区别在于这里将 null 值替换为字符串 \"null\"。
 *
 * @param template 待替换的字符串模板
 * @param data     存放替换数据的 Map
 * @return 替换后的字符串
 */
public static String simpleTpl2(String template, Map data) {
    String result = template;

    for (Map.Entry entry : data.entrySet()) {
        String key = entry.getKey();
        Object value = entry.getValue();

        if (value == null)
            value = \"null\";

        String placeholder = \"#{\" + key + \"}\";
        result = result.replace(placeholder, value.toString());
    }

    return result;
}

当前函数第二个入参为 Map,如果要改为 Java Bean 呢?也可以,通过反射获取属性值,请看看下面:

/**
 * 简单模板替换方法。根据 JavaBean 中的数据进行替换。
 *
 * @param template 待替换的字符串模板
 * @param data     存放替换数据的 JavaBean 对象
 * @return 替换后的字符串
 */
public static String simpleTpl(String template, Object data) {
    String result = template;

    try {
        for (PropertyDescriptor descriptor : Introspector.getBeanInfo(data.getClass()).getPropertyDescriptors()) {
            String name = descriptor.getName();
            Object value = descriptor.getReadMethod().invoke(data);

            if (value == null)
                value = \"null\";

            String placeholder = \"#{\" + name + \"}\";
            result = result.replace(placeholder, value.toString());
        }
    } catch (InvocationTargetException | IllegalAccessException | IntrospectionException e) {
        throw new RuntimeException(e);
    }

    return result;
}

Spring 中借助 PropertyPlaceholderHelper 亦可达成,几行代码就可以了,非常简练。话说 Spring 自带许多工具类的,直接拿去用就可以了。

private static final PropertyPlaceholderHelper HELPER = new PropertyPlaceholderHelper(\"${\", \"}\");

public static String render(String template, Map model) {
    return HELPER.replacePlaceholders(template, key -> {
        Object obj = model.get(key);
        return obj == null ? \"\" : obj.toString();
    });
}

带表达式的替换

前面我们提到的 ${xxx}里面为表达式的处理,如${score > 60 ? \'及格\' : \'不及格\'},这个可不是简单替换内容那么简单,而是涉及表达式多种情况的处理,需要利用编译器的知识来解决,远非简单的字符串替换所能处理。通常是用一个库去解决,例如 JSP 时代的 EL(Expression Language)表达式。

关于表达式库的选型,推荐京东这篇文章。

Spring MVC 许多场合都依赖表达式,于是也内置了一个表达式引擎:Spring Expression Language (SpEL) 。我们调用它,实现一个简单的模板功能:

/**
 * 编译模板,支持复杂的逻辑
 * Spring Expression Language (SpEL) 来实现模板替换
 *
 * @param tpl    模板
 * @param values 值
 */
public static String simpleTemplate(String tpl, Map values) {
    EvaluationContext context = new StandardEvaluationContext(); // 通过 evaluationContext.setVariable 可以在上下文中设定变量。
    for (String key : values.keySet())
        context.setVariable(key, values.get(key));

    // 解析表达式,如果表达式是一个模板表达式,需要为解析传入模板解析器上下文。
    Expression expression = new SpelExpressionParser().parseExpression(tpl, new TemplateParserContext(\"${\", \"}\"));

    // 使用 Expression.getValue() 获取表达式的值,这里传入了 Evaluation 上下文,第二个参数是类型参数,表示返回值的类型。
    return expression.getValue(context, String.class);
}

// 示例
Map ctx = Map.of(\"name\", \"Alice\", \"score\", 85);
String output = renderTemplate(\"Hello ${name}, your score is ${score}.\", ctx);
// 结果: Hello Alice, your score is 85.

关于表达式本身的运算,无论其内部运行如何的逻辑,其实最终都返回一个boolean结果,我们看看 SpEL 如何单独解析表达式:

/**
 * 计算表达式
 *
 * @param express EL 表达式
 * @param map     EL 表达式动态参数
 * @return 表达式结果
 */
public static boolean parse(String express, Map map) {
    // 设置动态参数
    StandardEvaluationContext cxt = new StandardEvaluationContext();
    cxt.setVariables(map);
    cxt.setPropertyAccessors(Collections.singletonList(new MapAccessor()));

    // 创建一个 EL 解析器
    ExpressionParser parser = new SpelExpressionParser();
    SpelExpression expr = (SpelExpression) parser.parseExpression(express, new TemplateParserContext(\"${\", \"}\"));
    expr.setEvaluationContext(cxt);

    return Boolean.TRUE.equals(expr.getValue(map, Boolean.class));
}

// 例子
Map map = new HashMap(16);
map.put(\"exp\", 4);

String result = parse(\"jjjj${exp>2}jkj\", map);
System.out.println(\"result:\" + result);

更强大的模板

毫无疑问就是那些专业的模板系统,如 Thymeleaf、FreeMarker、Velocity、JSP。本文讨论的是轻量级议题,所以那些就不展开讨论了。

小结

总结了这么多种轻量级的模板技术,有没有推荐一种方式呢?其实笔者感觉也不好说,它们都不相伯仲,难说哪一种最好。当然从功能来说 SpELl 是最强大的,但是相应消耗的资源肯定也多。至于其他几种方式,功能差别不大,有条件的可以跑跑它们的性能比较,这样就比较容易有结论了。

收藏 (0) 打赏

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

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

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 编程相关 漫谈 Java 轻量级的模板技术:从字符串替换到复杂模板 https://www.zuozi.net/35900.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

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

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