当前位置: 首页 > news >正文

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

提起模板技术相信每个 Java 开发者都不会陌生。虽然目前已经很少在 Java 后端开发前端页面了,但是在不同场合还是会用到模板或者应用模板的相关概念,例如把 SQL 写在 XML 文件中(MyBatis 的做法),里面使用了大量表达式和<if>/<foreach>流程判断,也属于模板的一种变种。更不用说早期 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<String, Object> 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<String, Object> data) {String result = template;for (Map.Entry<String, Object> 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<String, ?> 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<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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 是最强大的,但是相应消耗的资源肯定也多。至于其他几种方式,功能差别不大,有条件的可以跑跑它们的性能比较,这样就比较容易有结论了。

http://www.dtcms.com/a/585913.html

相关文章:

  • 免费网站空间有哪些mdx wordpress
  • 第九章 生成树
  • UniApp 全局使用字体教程
  • 404网站怎么做网站开发费用明细
  • python爬虫学习笔记
  • 【算法】递归算法实战:汉诺塔问题详解与代码实现
  • js 网站首页下拉广告南宁市网站开发建设
  • SolarEdge和英飞凌合作开发人工智能数据中心
  • asp.net core webapi------3.AutoMapper的使用
  • CCF LMCC人工智能大模型认证 青少年组 第一轮样题
  • 百度搜索不到asp做的网站全球知名购物网站有哪些
  • Android Studio 中 Gradle 同步慢 / 失败:清理、配置全攻略
  • Makefile极简指南
  • 信息系统项目管理师--论文case
  • win7 iis网站无法显示该页面网站上线准备
  • 华为防火墙基础功能详解:构建网络安全的基石
  • 北京网站定制设计开发公司宁波专业定制网站建设
  • 网站的后台怎么做调查问卷设计之家广告设计
  • WebRtc语音通话前置铃声处理
  • 使用XSHELL远程操作数据库
  • 淘宝客网站域名宜昌做网站哪家最便宜
  • 微信小程序中使用 MQTT 实现实时通信:技术难点与实践指南
  • Java computeIfAbsent() 方法详解
  • 做网站市场报价免费企业网站开源系统
  • 天元建设集团有限公司企业代码东莞做网站seo
  • Web前端摄像头调用安全性分析
  • 绵阳网站建设怎么做免费查公司
  • std之list
  • 前端:前端/浏览器 可以录屏吗 / 实践 / 录制 Microsoft Edge 标签页、应用窗口、整个屏幕
  • 做网站像美团一样多少钱中国最新军事消息