资讯动态

OGNL与EL表达式深度对比:原理、用法、选型与安全

发布时间:2026/10/9 7:32:53 来源:尧图企业网站定制
1. 从一次线上事故说起表达式选型搞错了有多痛先讲一个我早年踩过的坑。当时在某公司做一个报表权限系统需求很简单用户登录后根据其角色动态渲染菜单和按钮。前端传过来一个表达式字符串后端解析后判断“这个用户能不能点这个按钮”。我当时图省事直接用 EL 表达式去解析前端传来的字符串结果在测试环境一切正常上线第二天就出事了——某个用户居然看到了不该看的导出按钮。排查了一整天才定位到根因EL 表达式在解析时踏踏实实地“取值”而那段字符串里藏了一个方法调用。EL 根本不执行方法直接返回 null权限判断走了默认放行分支。如果当时用 OGNL这个漏洞根本不会存在——因为 OGNL 会老老实实把那个方法调用执行出来然后在权限层把它拦住。这事让我彻底明白OGNL 和 EL 根本不是“哪个更好用”的关系而是“哪个场景该用哪个”的关系。很多 Java 开发者对这两者的认知停留在“一个能取值一个也能取值”但真正落到项目里选错表达式引擎的代价往往是线上事故级别的。这篇文章我不打算从教科书定义讲起而是直接按“区别、原理、用法、踩坑”四个维度拆开揉碎把我这些年实际项目里的经验、教训、排查思路全部倒出来。不管你是刚接触 JavaWeb 的新人还是已经写过几年业务代码的老手这篇文章都能帮你把表达式这个基础但关键的地基打牢。2. OGNL 和 EL 的底层定位同样叫表达式其实是两种生物2.1 EL 的出身为 JSP 页面而生追求的是“安全地读”ELExpression Language最早是 JSP 2.0 规范引入的核心目标只有一个让页面里不要出现 Java 代码。在它出现之前JSP 页面里写% request.getAttribute(user).getName() %这种脚本片段是常态页面又臭又长还难维护。EL 出现后页面里只需要写${user.name}就够了。这个出身决定了 EL 的基因它是为“读取展示数据”设计的天然偏向保守。默认只支持属性访问、算术运算、逻辑运算、集合访问不支持方法调用除非配置 MethodExpression。遇到 null 值默认返回空字符串或 null不会抛异常也不会触发副作用。设计哲学是“容错优先”页面渲染不能因为某个数据缺失就整个 500。用生活化类比EL 像个“只读不写的图书馆管理员”你问他要什么书他给你找找不到就告诉你“没有”但他绝对不会把书拆开抄一段给你更不会帮你改书里的内容。2.2 OGNL 的出身从 Struts 2 走红天生就是“万能工具人”OGNLObject-Graph Navigation Language全称是对象图导航语言它的历史比 EL 更早最初是一个独立的开源项目后来被 Struts 2 选中作为默认表达式引擎从此在 JavaWeb 圈子里有了姓名。OGNL 的设计目标恰恰和 EL 相反它要成为“访问 Java 对象图的瑞士军刀”。什么都能干支持方法调用user.getName()、user.doSomething()支持静态方法/属性访问支持构造对象new java.util.Date()支持投影和选择users.{name}、users.{? age 18}支持赋值操作user.name xxx支持多表达式执行用逗号分隔甚至能做类型转换、 lambda 表达式还是那个类比OGNL 像个“万能工匠”不仅能帮你找书还能帮你抄书、改书、写书、甚至把书拆了重新装订。2.3 两者的核心差异对照表我把日常开发和面试里最常涉及的差异点整理成了一张表建议你收藏对比维度ELOGNL全称Expression LanguageObject-Graph Navigation Language起源JSP 规范独立开源项目Struts 2 采用核心设计目标页面数据展示安全读取对象图任意操作强能力方法调用默认不支持需特殊配置原生支持静态成员访问不支持支持java.lang.MathPI赋值操作不支持支持集合投影/选择不支持支持OGNL 特色构造对象不支持支持new 表达式null 处理策略容错返回空抛异常或按逻辑处理典型应用场景JSP、Thymeleaf 部分场景、JSFStruts 2、MyBatis 动态 SQL性能开销轻量相对较重安全性相对安全能力弱危险能力越强越危险这张表背后有一条核心结论EL 的能力边界是它的安全边界而 OGNL 的能力边界同时也是它的安全风险边界。你选的表达式引擎越强大越要小心它被恶意利用。2.4 为什么 MyBatis 选了 OGNL 而不选 EL一个很有意思的问题MyBatis 的动态 SQLif test...为什么选 OGNL而不是选更轻量的 EL我当初也困惑过后来看了 MyBatis 源码里的ExpressionEvaluator类才算想明白。MyBatis 需要的不只是“判断某个属性是否为 null”还要支持user.name ! null and user.age 18这种组合判断甚至有时候要调用实体类上的方法。EL 的保守策略在这里根本不够用——比如默认不支持方法调用这在写动态 SQL 时就是个硬伤。而 OGNL 的完整表达式能力让 MyBatis 可以优雅地支持复杂多变的动态查询条件。代价就是 MyBatis 启动时解析这些表达式有一定的性能开销和安全隐患但相对于它换来的灵活性这笔买卖值。3. 核心用法拆解EL 和 OGNL 的语法与实战示例3.1 EL 的基本用法三分钟上手但有隐藏边界EL 的基础语法非常简单核心就是${}。我直接按场景列属性访问${user.name} ${user.address.city} ${list[0]} ${map[key]}运算${count 1} ${count 10 flag} ${empty list}隐式对象这是 EL 独有的JSP 场景很常用${param.username} ${sessionScope.user} ${applicationScope.config}EL 的empty运算符是个好东西能同时判断 null、空字符串、空集合${empty userList ? 暂无数据 : 有数据}EL 的隐藏限制是时候说清楚了很多人写 EL 写着写着就想“顺手调个方法”比如${user.getFullName()}然后发现页面报错或者直接不生效。原因在于 EL 默认不允许方法调用。当然EL 2.2 之后如果你使用的是支持该规范的容器比如 Tomcat 8并且通过MethodExpression的方式是可以调用无参方法的。但这里有个很隐蔽的坑EL 的方法调用不支持传参。你想写${user.getAddress(home)}门都没有。这是 EL 的硬限制设计如此不是 bug。所以我在实际项目里的原则是EL 只做读取和展示凡是涉及逻辑处理、方法调用的一律在后台Controller/Service算好页面只拿结果。这样既绕开了 EL 的限制也让页面保持纯净。3.2 OGNL 的基本用法功能强大但要用在正确的地方OGNL 的标准用法我做 Java 开发这些年主要接触三个场景Struts 2 标签、MyBatis 动态 SQL、以及自己写代码调用 OGNL 表达式。场景一Struts 2 标签!-- 访问值栈中的属性 -- s:property valueuser.name/ !-- 调用方法 -- s:property valueuser.getFullName()/ !-- 静态访问 -- s:property valuejava.lang.MathPI/ !-- 集合投影 -- s:property valueusers.{name}/场景二MyBatis 动态 SQL这是我日常打交道最多的select idfindUsers resultTypeUser SELECT * FROM user WHERE 1 1 if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testage ! null and age 0 AND age gt; #{age} /if if testdeptIdList ! null and deptIdList.size() 0 AND dept_id IN foreach collectiondeptIdList itemdeptId open( separator, close) #{deptId} /foreach /if /select注意这里if test...里面的表达式本质就是 OGNL 在对传入的参数对象进行判断。deptIdList.size()能直接调方法这就是 OGNL 给 MyBatis 带来的便利。场景三直接用 Java 代码调用 OGNL如果你在项目中引入了 ognl 依赖可以直接这样写import ognl.Ognl; import ognl.OgnlContext; // 创建一个简单的对象 MapString, Object user new HashMap(); user.put(name, 张三); user.put(age, 25); // 构建表达式并执行 Object expression Ognl.parseExpression(name 的年龄是 age); OgnlContext context new OgnlContext(); Object result Ognl.getValue(expression, context, user); System.out.println(result); // 输出张三的年龄是25 // 支持方法调用 Object expression2 Ognl.parseExpression(name.length()); Object result2 Ognl.getValue(expression2, context, user); System.out.println(result2); // 输出2这个 API 看起来很简洁但用的时候有几个细节要注意Ognl.parseExpression返回的是编译后的表达式对象最好缓存起来复用不要每次执行都重新解析性能差距很大。OgnlContext可以塞入 Root 对象和自定义变量可以通过#变量名在表达式中引用。默认的 OgnlContext 没有开启成员访问权限控制恶意表达式可以调用任意方法、访问任意类所以如果表达式来源不可信务必先看本文第 5 节的安全措施。3.3 一个容易混淆的语法点${}和%{}和#{}很多新手会把 Struts 2 的%{}、OGNL 的#、MyBatis 的#{}搞混我当初也花了不少时间。这里用一张表彻底理清语法所属框架作用${expression}JSP EL / Spring立即求值输出结果%{ognlExpr}Struts 2 标签强制把字符串当 OGNL 表达式解析#variableOGNL访问非 Root 对象的变量如#session.user#{}MyBatis预编译占位符生成?参数${}MyBatis字符串拼接直接替换有 SQL 注入风险一个经典场景在 Struts 2 的s:property标签中value 属性默认会当作 OGNL 表达式解析所以直接写valueuser.name就行。但如果你在一个普通 HTML 属性里想动态拼接就需要%{}强制转换比如input typetext value%{user.name}/而 MyBatis 里#{}和${}的区别更是老生常谈的重点#{}是预编译参数安全${}是字符串替换危险。能用#{}的地方绝不用${}除非你明确知道你在做什么比如动态表名、动态排序字段这种无法用占位符的场景。4. 实操复盘在真实项目中如何选型和落地4.1 选型决策框架三个问题帮你避开 90% 的坑我在接手一个新项目、或者要给现有系统引入表达式能力时会先问自己三个问题问题一表达式来自哪里来自开发者自己写的代码/配置文件 → OGNL 可选只要管好代码审查。来自用户输入/前端传递 → 一律 EL 优先或者对 OGNL 做严格白名单校验。来自数据库动态配置 → 警惕这属于半可信来源必须做沙箱隔离。问题二需要什么能力只是取值展示、简单判断 → EL 足够没必要引入 OGNL 的复杂度。需要方法调用、集合操作、动态判断 → OGNL 合适比如 MyBatis 场景。需要执行复杂业务逻辑 → 都不该用表达式写成 Java 方法。问题三性能敏感吗高频调用路径比如每秒上千次的接口→ EL 更稳OGNL 需要缓存表达式避免解析开销。低频配置场景比如启动时加载规则→ OGNL 没问题。这三个问题问完基本就能定下来。我的经验是绝大数 web 项目里EL 是默认选项OGNL 只在特定框架Struts 2、MyBatis里被动出现很少需要主动引进来做业务。主动引入 OGNL 的场景一般集中在规则引擎、权限系统、动态配置中心这类偏底层的模块。4.2 案例一用 EL 做页面数据展示的完整代码一个典型的用户列表页面用 EL 把用户信息和角色状态展示到 JSP 上% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % ... table thead tr th姓名/th th年龄/th th所属部门/th th状态/th /tr /thead tbody c:forEach items${userList} varuser tr td${user.name}/td td${user.age}/td td${user.dept.name}/td td c:choose c:when test${user.status 1} span classactive正常/span /c:when c:otherwise span classdisabled禁用/span /c:otherwise /c:choose /td /tr /c:forEach /tbody /table这里有个我在生产环境踩过的细节点${user.dept.name}在 user.dept 为 null 时EL 会静默返回空字符串页面不报错看起来一切正常。但如果你在页面上判断dept.name 研发部就会得到 false而且没有任何日志。这种“静默失败”在排查问题时非常隐蔽建议在后台组装 VO 时就把关联对象处理好不要过度依赖页面兜底。4.3 案例二用 OGNL 写一个灵活的规则判断工具假设你有一个需求运营人员在后台配置优惠券的使用规则规则以字符串形式存储例如user.level 3 order.amount 100 !user.isBlacklisted()。这时候 EL 干不了不支持方法调用纯 Java if-else 又太死板OGNL 就成了最顺手的工具。我封装过的工具类大致长这样import ognl.Ognl; import ognl.OgnlContext; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class OgnlRuleEngine { // 表达式缓存避免重复解析的开销 private static final MapString, Object EXPR_CACHE new ConcurrentHashMap(); public static boolean evaluate(String rule, Object rootObject) { try { // 1. 尝试从缓存取解析后的表达式 Object expr EXPR_CACHE.computeIfAbsent(rule, key - { try { return Ognl.parseExpression(key); } catch (Exception e) { throw new RuntimeException(OGNL 表达式解析失败: key); } }); // 2. 创建上下文放入自定义变量 OgnlContext context new OgnlContext(); context.put(now, System.currentTimeMillis()); // 3. 执行表达式结果为 Boolean Object result Ognl.getValue(expr, context, rootObject); return result instanceof Boolean (Boolean) result; } catch (Exception e) { // 规则配置错误时建议记录日志并返回 false保证不阻断主流程 return false; } } }用的时候你可以把用户对象作为 root 传入User user userService.getById(123); Order order orderService.getLatestByUserId(123); // 把 user 作为 rootorder 作为变量规则里可以通过 #order 访问 MapString, Object root new HashMap(); root.put(user, user); OgnlContext context new OgnlContext(); context.put(order, order); String rule user.level 3 #order.amount 100 !user.isBlacklisted(); boolean pass OgnlRuleEngine.evaluate(rule, root);几个我在封装过程中的心得规则表达式尽量由项目组技术侧审核后再上线不要让运营直接写不然很容易写出能让系统崩溃的表达式比如死循环、内存溢出。缓存表达式一定要做我自己压测过不缓存 vs 缓存在高频调用下有几十倍的性能差距。表达式执行要加超时控制。OGNL 本身没有超时机制如果表达式里有大量对象遍历可能卡住线程。我在实战中是用线程池 Future.get(timeout) 来兜底的。4.4 案例三MyBatis 动态 SQL 里 OGNL 的隐藏陷阱MyBatis 的if test...是大家最熟悉的 OGNL 使用场景但这块有几个坑我见过无数人踩。坑一整数比较的自动类型转换if teststatus 1这个1在 OGNL 里默认是 Integer而status如果数据库返回的是 Long 类型比较结果就是 false。排查方法在 OGNL 表达式中把字面量写成status 1L或者把实体类属性类型改成 Integer。坑二字符串比较必须用引号不能省if testname 张三注意这里外层用单引号内层用双引号因为 XML 属性本身要用双引号。如果你在 XML 里写testname 张三在 OGNL 里也能识别但很多人混用的时候容易把引号写错导致 SQL 一直走不到这个分支。坑三list.size() 0的判空顺序问题if testdeptIdList ! null and deptIdList.size() 0如果把前后两个条件反过来写先调用deptIdList.size()再判断 null那 null 集合会直接抛 NPE导致 SQL 报错。OGNL 的and是短路还是非短路答案是OGNL 的 and 不保证短路这一点和 Java 的不一样。所以 MyBatis 里判空条件必须放在最前面这个顺序问题在代码审查时一定要盯紧。坑四符号要转义在 XML 中if testage 18会被 XML 解析器当成标签开始直接报错。必须写成lt;if testage lt; 18这个坑看似低级但在我见过的大多数 MyBatis 报错帖里有一半是这个问题。4.5 工具选型解析什么时候引入第三方表达式引擎除了 EL 和 OGNLJava 生态里还有几个表达式引擎我简单对比一下方便你做技术选型时有全局观表达式引擎核心特点适用场景EL轻量、安全、只读优先JSP 展示、简单模板OGNL功能全面、能调方法、有赋值能力Struts 2、MyBatisSpELSpring 家族功能全面Spring 注解、缓存 key、安全规则MVEL高性能、支持复杂逻辑规则引擎、业务编排Aviator轻量高性能、Java 语法子集公式计算、规则判断如果你的项目里已经在用 Spring且需要一个能力适中、安全可控的表达式引擎SpEL 是 OGNL 之外很好的备选。它的语法更贴近 Java对静态方法的访问支持也更规范。但如果你就是想轻量地做动态判断不想引入 SpringAviator 也是一个不错的选择。我个人的偏好是项目里已有 Spring 就用 SpEL项目是纯 MyBatis 体系就用 OGNL完全自定义场景才评估 MVEL/Aviator。不要因为“OGNL 功能强”就在所有地方硬上功能强不一定是优点安全管控成本会随着能力增强而指数级上升。5. 安全红线OGNL 威力越大越要小心被反噬5.1 为什么 OGNL 能成为攻击武器这个问题我必须放在最后压轴讲因为太重要了。OGNL 的最大卖点——能调用任意方法、能访问任意类、能构造任意对象——同时就是它最大的安全软肋。最典型的风险场景如果你的系统把用户输入的字符串直接用 OGNL 解析执行攻击者可以构造出任意代码执行效果。曾经发生过多起知名开源框架因为 OGNL 表达式注入被攻破的安全事件核心原理都是同一个不可信输入 OGNL 强大能力 攻击面。举个例子一段看似人畜无害的输入#context[x] new java.lang.ProcessBuilder({calc}).start()如果这条表达式被 OGNL 执行结果是什么直接启动一个系统进程。在服务器上攻击者可以用类似手法执行任意命令、读取任意文件、甚至反弹 shell。这不是危言耸听而是真实发生过无数次的攻击路径。5.2 三个必须遵守的安全底线根据我在安全意识培训和实践中的总结只要你的系统里出现 OGNL这三条底线必须守住底线一绝不用 OGNL 解析不可信输入这是最核心的一条。用户输入、请求参数、外部接口传来的字符串一律禁止直接传给 OGNL.parseExpression。如果业务上确实需要“动态规则”能力必须走“后台配置 白名单校验 权限审批”的流程不能让用户自助提交规则代码。底线二配置安全上下文限制成员访问OGNL 提供了一些安全相关的开关比如OgnlContext context new OgnlContext(); // 限制类解析禁止访问 Object 以外的基础类 context.setMemberAccess(new DefaultMemberAccess(false)); // 或者使用更严格的自定义 MemberAccess不过说实话这些开关我并不推荐完全依赖因为 OGNL 的沙箱机制并不完善绕过方法层出不穷。安全主要靠第一道防线输入源头治理。底线三表达式白名单 运行时沙箱如果实在无法避免使用 OGNL 解析配置文件中的规则至少要做到对表达式做静态语法检查、关键词黑名单过滤new、ProcessBuilder、Runtime、reflect、class等危险关键词。在独立线程池中执行设置超时时间防止表达式死循环。对表达式的执行结果做类型限制只输出基本类型和指定 VO。5.3 EL 是不是就一定安全也不能这么绝对。EL 虽然能力弱但历史上也出过基于 EL 的注入风险比如构造特殊表达式访问内部数据结构。但相比 OGNLEL 的攻击面小得多能力边界就是它的天然盾牌。这也是为什么在不太可控的场景里我强烈建议先用 EL 兜底。一句话总结安全策略表达式引擎的能力必须严格遵循“最小够用原则”。能用 EL 解决的问题绝不上 OGNL万不得已上 OGNL必须把输入可信任这关把得死死的。6. 个人实操经验总结表达式选型的那些事写到这里我再分享几点这些年反复验证过的经验。第一在 JavaWeb 常规业务开发中EL 的使用频率远高于 OGNL但面试和源码阅读中 OGNL 的重要性更高。如果你在看 Struts 2 或 MyBatis 源码看不懂 OGNL 就只能看懂个皮毛。所以学习优先级上我建议先精通 EL 的日常用法再深入 OGNL 的原理两条腿走路。第二遇到“页面数据没显示”这类问题先怀疑表达式本身再怀疑后端数据。我遇到过太多次了后端数据明明没问题页面就是空的最后发现是 EL 表达式大小写写错了或者属性名和 getter 对不上。EL 解析属性user.name会先找getName()如果你的方法叫getUName()EL 是识别不了的页面就静默输出空值。这种问题比 OGNL 报错更头疼因为完全没有报错信息。第三写 MyBatis 动态 SQL 时尽量避免写过于复杂的 OGNL 表达式。复杂的条件判断逻辑尽量在 Service 层提前处理成简单的布尔值或 List 参数传入这样既让 SQL 可读性高又避免 OGNL 在运行时出现类型转换等隐性问题。我在代码评审时有一条铁律if test...里只允许出现“属性判空 简单比较”超过三个条件的组合判断必须挪到 Java 里算好再传。第四如果团队里有新手一定要把 EL 和 OGNL 的差异讲透尤其是 OGNL 的 and 不短路、类型自动转换、XML 转义这几个细节。这些点看起来小但几乎每个新手都会踩一遍而且踩的时候往往毫无头绪。最后送大家一句我常对组里同学说的话表达式语言是 Java 开发里最不起眼却最容易捅娄子的部分之一选型和防注入的意识比记住语法重要十倍。希望这篇文章能让你在之后的开发里少走几步我曾走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑