嘿,朋友。如果你正在被 Spring MVC 里的参数绑定搞得头大,或者在 JSP/Thymeleaf 里用 OGNL 取 Map 值时总是 NullPointerException,那这篇文章就是为你写的。别慌,我们不说那些晦涩的理论,直接看代码,直接看坑,直接看怎么填。
为什么我们要聊这个?
先说个真实场景。上周我一个刚转 Java 的同事问我:“老大,我前端传过来一个 Map,为什么后端收不到?” 我一看代码,好家伙,他在 Controller 里写的是 Map<String, Object> map,然后在 Service 层用 ${map['key']} 取值,结果页面一片空白。
另一个场景是,你在 JSP 里想用 OGNL 表达式从 request 域或者 session 域里取 Map 数据,写了 ${map.key} 却取不到值,得写成 ${map['key']} 才行。为啥?因为 Map 没有 getter 方法啊!
今天我们就把 OGNL 取 Map 集合的 3 种写法 和 Spring MVC 参数接收与数据绑定 这两块硬骨头啃下来。保证你看完能避开 90% 的坑。
第一部分:OGNL 取 Map 集合的三种写法
OGNL(Object-Graph Navigation Language)是 Struts 2 和 Spring 框架中常用的表达式语言。当你需要在 JSP、Thymeleaf 或者 Spring EL 中访问一个 Map 对象时,有三种主要写法。我们来一个一个拆解,并配上真实代码。
写法一:点号访问法 map.key
这是最像 Java 对象属性访问的写法。但注意,它只适用于 Map 中的 key 是简单字符串且不包含特殊字符的情况。
代码示例:
// 假设你在 Controller 中这样准备数据
Map<String, String> userMap = new HashMap<>();
userMap.put("name", "张三");
userMap.put("age", "25");
model.addAttribute("userInfo", userMap);
在 JSP 中使用 OGNL:
<%@ taglib prefix="s" uri="/struts-tags" %>
<!-- 或者在 Spring MVC 的 JSP 中 -->
${userInfo.name}
${userInfo.age}
关键点:
name和age是直接作为属性访问的。- 陷阱:如果你的 key 包含点号、空格、连字符,比如
user.name或first-name,这种写法就会失败或者只取到第一部分的值。 - 例子:如果 key 是
user.name,${userInfo.user.name}会报错或返回 null,因为 OGNL 会尝试在 Map 中找 key 为user的对象,然后取它的name属性,而不是把user.name当作一个整体 key。
写法二:方括号字符串访问法 map['key']
这是最推荐、最安全的写法,尤其是当 key 包含特殊字符时。
代码示例:
Map<String, String> configMap = new HashMap<>();
configMap.put("server.host", "192.168.1.1");
configMap.put("db.password", "secret123");
configMap.put("app.name", "MyApp");
model.addAttribute("config", configMap);
在 JSP 中使用 OGNL:
${config['server.host']}
${config['db.password']}
${config['app.name']}
关键点:
- 单引号包裹的字符串会作为整个 key 来查找。
- 无论 key 里有什么字符,只要用单引号括起来,OGNL 就会把它当成一个完整的 key 去 Map 里找。
- 真实经历:有一次我给客户部署系统,他们的配置 key 是
jdbc.url,我用点号访问法死活取不到,换成方括号法就好了。客户看了我半天,说我“神了”。其实只是基本功。
写法三:方括号变量访问法 map[keyVariable]
当你的 key 不是一个固定字符串,而是一个动态变量时,用这种写法。
代码示例:
Map<String, String> dynamicMap = new HashMap<>();
dynamicMap.put("field1", "value1");
dynamicMap.put("field2", "value2");
model.addAttribute("dynamicMap", dynamicMap);
model.addAttribute("currentKey", "field1"); // 假设这个值来自用户选择或业务逻辑
在 JSP 中使用 OGNL:
${dynamicMap[currentKey]}
关键点:
currentKey是一个变量,OGNL 会先计算这个变量的值("field1"),然后用这个值作为 key 去 Map 中查找。- 陷阱:如果变量名写错,或者变量为 null,会返回 null 而不是报错。这容易导致后续代码 NPE。
- 实战技巧:在 Thymeleaf 中,这种写法也很常见,比如
[[${dynamicMap[currentKey]}]]。
三种写法对比总结
| 写法 | 适用场景 | 优点 | 缺点 | 推荐指数 |
|---|---|---|---|---|
map.key |
key 为简单标识符 | 简洁,像属性访问 | key 含特殊字符时失效 | ⭐⭐ |
map['key'] |
key 含特殊字符或固定字符串 | 最安全,支持任意 key | 稍显繁琐 | ⭐⭐⭐⭐⭐ |
map[keyVar] |
key 动态变化 | 灵活,支持变量 | 变量为 null 时无提示失败 | ⭐⭐⭐⭐ |
给小朋友的解释:想象 Map 是一个储物柜,key 是柜子的编号。
- 点号访问法就像你直接喊“3号柜子”,但如果你喊“3-1号柜子”,它可能听不懂。
- 方括号字符串访问法就像你写一张纸条“3-1号柜子”,交给管理员,他肯定能找对。
- 方括号变量访问法就像你让另一个小朋友告诉你编号,然后你拿着这个编号去找柜子。
第二部分:Spring MVC 参数接收与数据绑定实战技巧
Spring MVC 的参数绑定非常强大,但也很容易用错。我们从一个最简单的例子开始,然后逐步深入到复杂场景。
基础:基本类型和 String 的绑定
Controller 代码:
@GetMapping("/user/register")
public String register(
@RequestParam("username") String username,
@RequestParam("age") int age,
@RequestParam(value = "email", required = false) String email
) {
// 处理注册逻辑
return "success";
}
请求示例:
GET /user/register?username=zhangsan&age=25&email=zhangsan@example.com
关键点:
@RequestParam用于接收 URL 查询参数或表单提交参数。required = false表示该参数可选,不传不会报错。- 陷阱:如果
age传了非数字字符串,会直接抛MethodArgumentTypeMismatchException。解决办法是在参数前加@InitBinder或者用Integer包装类并做空判断。
中级:POJO 对象绑定
这是最常用的方式。Spring MVC 会自动将请求参数绑定到 Java 对象的属性上。
Java 类:
public class UserForm {
private String username;
private Integer age;
private String email;
private Address address; // 嵌套对象
// getters and setters
}
public class Address {
private String province;
private String city;
// getters and setters
}
Controller 代码:
@PostMapping("/user/save")
public String saveUser(@ModelAttribute UserForm userForm) {
System.out.println("用户名: " + userForm.getUsername());
System.out.println("省: " + userForm.getAddress().getProvince());
return "redirect:/user/list";
}
HTML 表单示例:
<form action="/user/save" method="post">
用户名: <input type="text" name="username"/><br/>
年龄: <input type="text" name="age"/><br/>
省份: <input type="text" name="address.province"/><br/>
城市: <input type="text" name="address.city"/><br/>
<input type="submit" value="保存"/>
</form>
关键点:
- 表单中的
name属性必须与 Java 对象的属性名对应。 - 嵌套对象用
.分隔,如address.province。 - 真实坑点:如果
address对象没有实例化,直接访问会 NPE。解决办法是在UserForm的构造函数或初始化块中实例化address,或者在 Controller 中手动实例化。
高级:Map 和 List 的绑定
这是很多人困惑的地方。如何接收前端传来的 Map 或 List?
接收 Map 参数
场景:前端传来一个 Map,key 是参数名,value 是参数值。
Controller 代码:
@PostMapping("/config/save")
public String saveConfig(@RequestParam Map<String, String> configMap) {
// 假设前端传来?configMap.server.host=192.168.1.1&configMap.db.name=mysql
// 但这样写是不对的!
// 正确做法:直接接收 Map,Spring 会把所有参数绑进去
// 但你需要手动处理 key 的映射
System.out.println(configMap);
return "success";
}
更常用的方式:用 @RequestParam Map 接收所有查询参数。
@GetMapping("/search")
public String search(@RequestParam Map<String, String> params) {
String keyword = params.get("keyword");
String category = params.get("category");
// 处理搜索逻辑
return "result";
}
请求示例:
GET /search?keyword=java&category=tutorial
关键点:
- 这种方式适合动态参数的场景,比如高级搜索,参数不固定。
- 陷阱:如果参数名和 Map 的 key 不一致,需要自己处理。
接收 List 参数
场景:前端传来一个列表,比如多个复选框或动态表单。
Controller 代码:
@PostMapping("/items/batch")
public String batchSave(@RequestParam List<String> itemIds) {
// 接收多个同名参数: ?itemIds=1&itemIds=2&itemIds=3
System.out.println("选中了 " + itemIds.size() + " 个物品");
return "success";
}
HTML 表单示例:
<form action="/items/batch" method="post">
<input type="checkbox" name="itemIds" value="1"/> 物品1<br/>
<input type="checkbox" name="itemIds" value="2"/> 物品2<br/>
<input type="checkbox" name="itemIds" value="3"/> 物品3<br/>
<input type="submit" value="批量删除"/>
</form>
关键点:
- 多个同名参数会自动绑定到 List。
- 陷阱:如果 List 元素是对象,需要这样写:
public class Item {
private String id;
private String name;
// getters and setters
}
@PostMapping("/items/batch")
public String batchSave(@RequestParam List<Item> items) {
// 前端需要这样传: ?items[0].id=1&items[0].name=a&items[1].id=2&items[1].name=b
return "success";
}
HTML 示例(复杂表单):
<input type="text" name="items[0].id" value="1"/>
<input type="text" name="items[0].name" value="物品A"/>
<input type="text" name="items[1].id" value="2"/>
<input type="text" name="items[1].name" value="物品B"/>
真实经历:有一次做批量导入功能,前端用 jQuery 动态生成表单,我用 items[0].name 这种写法死活绑不上,后来发现是因为动态生成的 name 属性没有正确拼写。debug 了两个小时,最后加个 console.log 才发现是引号的问题。
进阶:自定义类型转换器
有时候,Spring MVC 的默认绑定不够用。比如前端传来一个日期字符串 "2024-01-15",但你的 Java 对象用的是 LocalDate,默认绑定会失败。
解决方案:注册自定义转换器
第一步:定义转换器
public class LocalDateConverter implements Converter<String, LocalDate> {
@Override
public LocalDate convert(String source) {
if (source.isEmpty()) {
return null;
}
return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
第二步:注册转换器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new LocalDateConverter());
}
}
Controller 代码:
@PostMapping("/event/create")
public String createEvent(@ModelAttribute EventForm eventForm) {
// eventForm 中的 startDate 是 LocalDate 类型,现在可以自动绑定了
return "redirect:/event/list";
}
关键点:
- 这种方式比
@InitBinder更优雅,适合全局注册。 - 陷阱:如果转换器抛异常,会直接返回 400 错误。建议在转换器中做好异常处理,返回 null 或默认值。
实战技巧:如何处理 Optional 参数
在实际项目中,很多参数是可选的。比如搜索功能,用户可能只输入关键词,也可能只选择分类。
错误做法:
@GetMapping("/search")
public String search(
@RequestParam String keyword, // 必填,但用户可能不传
@RequestParam String category // 必填,但用户可能不传
) {
// 如果这两个参数不传,直接报错
}
正确做法:
@GetMapping("/search")
public String search(
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size
) {
// keyword 和 category 不传时为 null
// page 默认为 1,size 默认为 10
return "result";
}
更优雅的做法:使用对象接收
public class SearchRequest {
@RequestParam(required = false)
private String keyword;
@RequestParam(required = false)
private String category;
@RequestParam(defaultValue = "1")
private Integer page = 1;
@RequestParam(defaultValue = "10")
private Integer size = 10;
// getters and setters
}
@GetMapping("/search")
public String search(@ModelAttribute SearchRequest request) {
// 所有参数都有默认值或可为 null
return "result";
}
关键点:
- 使用对象接收可以让 Controller 方法更简洁。
- 陷阱:如果对象中的字段有类型约束(如
Integer),必须提供默认值或使用包装类,否则默认绑定会失败。
第三部分:OGNL 与 Spring MVC 参数绑定的结合
有时候,你会在 Spring MVC 中用到 OGNL。比如在 Thymeleaf 模板中绑定数据,或者在 Spring Security 的配置中使用表达式。
场景一:Thymeleaf 中展示 Map 数据
Controller 代码:
@GetMapping("/dashboard")
public String dashboard(Model model) {
Map<String, Object> stats = new HashMap<>();
stats.put("totalUsers", 1000);
stats.put("activeUsers", 500);
stats.put("newUsers", 100);
model.addAttribute("stats", stats);
return "dashboard";
}
Thymeleaf 模板:
<div th:object="${stats}">
<p>总用户数:<span th:text="*{totalUsers}">0</span></p>
<p>活跃用户数:<span th:text="*{activeUsers}">0</span></p>
<p>新增用户数:<span th:text="*{newUsers}">0</span></p>
</div>
<!-- 或者直接访问 -->
<p>总用户数:<span th:text="${stats.totalUsers}">0</span></p>
<p>总用户数(安全写法):<span th:text="${stats['totalUsers']}">0</span></p>
关键点:
- Thymeleaf 默认使用 SpEL(Spring Expression Language),但语法和 OGNL 类似。
*{}语法需要在th:object绑定后使用。[]语法更安全,支持任意 key。
场景二:Spring Security 中的 OGNL/SpEL 表达式
Security 配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/api/*/orders").access("@authService.hasAccess(#userId)")
.anyRequest().authenticated();
}
}
AuthService 代码:
”`java @Component(“authService”)