Spring+Thymeleaf菜品配料复选框更新空指针问题求助
问题描述
使用Spring Framework 5.3.20、Hibernate 6.1.0、Thymeleaf 3.1.0M2开发菜品配料修改功能时遇到以下问题:
- 能显示数据库中已保存的菜品配料,但无法通过Update Dish模板修改配料
- 提交表单时抛出空指针异常:
java.lang.NullPointerException: Cannot invoke "java.util.Map.keySet()" because "dishIngredients" is null at project.service.DishController.getIngredientSetFromMap(DishController.java:104) ~[classes/:na]
核心问题:视图能生成配料选择的Map,但提交表单时无法将选中状态回传给控制器。
相关代码
Dish实体类
@Entity @Table(name="Dish") public class Dish implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; @ManyToMany(cascade = CascadeType.MERGE) @JoinTable( name = "dish_ingredient", joinColumns = { @JoinColumn(name = "fk_dish_id")}, inverseJoinColumns = { @JoinColumn(name = "fk_ingredient_id")} ) private Set<Ingredient> ingredients = new HashSet<>(); public Set<Ingredient> getIngredients() { return ingredients; } public String getPictureLink() { return pictureLink; } public void setIngredients(Set<Ingredient> ingredients) { this.ingredients = ingredients; } }
Ingredient实体类
@Entity @Table(name="ingredient") public class Ingredient implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long ingredientId; @Column(name = "name") private String name; @ManyToMany(cascade = CascadeType.MERGE) @JoinTable( name = "dish_ingredient", joinColumns = { @JoinColumn(name = "fk_ingredient_id")}, inverseJoinColumns = { @JoinColumn(name = "fk_dish_id")} ) private Set<Dish> dishes; // getter和setter省略 }
DishIngredient实体类
@Entity @Table(name = "dish_ingredient") public class DishIngredient { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; @ManyToOne private Dish dish; @ManyToOne private Ingredient ingredient; // getter和setter省略 }
DishController控制器
@GetMapping("/dish/update/{id}") public String updateDishForm(@PathVariable("id") long id, Model model) { Dish dish = dishService.getDish(id); model.addAttribute("dish", dish); List<Ingredient> dishIngredients = new ArrayList<>(dishService.getIngredients(id)); List<Ingredient> allIngredients = new ArrayList<>(ingredientService.getAllIngredients()); getIngredientSetFromMap(getCheckedIngredientsMap(dishIngredients, allIngredients)); model.addAttribute("map", getCheckedIngredientsMap(dishIngredients, allIngredients)); return "updatedish"; } @PostMapping("dish/save") public String saveDish(@ModelAttribute("dish") Dish dish, Model model){ Map<String, Boolean> dishIngredientsMap = (Map<String, Boolean>) model.getAttribute("map"); Set<Ingredient> ingredients = getIngredientSetFromMap(dishIngredientsMap); model.addAttribute("map", dishIngredientsMap); dish.setIngredients(ingredients); dishService.saveDish(dish); return "redirect:/"; } public Map<String, Boolean> getCheckedIngredientsMap(List<Ingredient> dishIngredients, List<Ingredient> allIngredients) { Map<String,Boolean> ingredientsPresentInDish = new TreeMap<>(); Set<String> dishIngredientsNames = dishIngredients.stream().map(Ingredient::getName).collect(Collectors.toSet()); Set<String> allIngredientsNames = allIngredients.stream().map(Ingredient::getName).collect(Collectors.toSet()); for (String ingredientName : allIngredientsNames) ingredientsPresentInDish.put(ingredientName, false); for (String dishIngredientsName : dishIngredientsNames) { for (String allIngredientsName : allIngredientsNames) { if (allIngredientsName.equals(dishIngredientsName)) { ingredientsPresentInDish.put(allIngredientsName, true); } } } return ingredientsPresentInDish; } public Set<Ingredient> getIngredientSetFromMap(Map<String, Boolean> dishIngredients) { Set<Ingredient> ingredientsSet = new HashSet<>(); for (String key : dishIngredients.keySet()) { if (dishIngredients.get(key)) ingredientsSet.add(ingredientService.getIngredientByName(key)); } return ingredientsSet; }
updatedish模板
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <head> <meta charset="UTF-8"> </head> <body> <div th:if="${dish} != null"> <div class="container"> <h1>Edit dish</h1> <hr> <form action="#" th:action="@{/dish/save}" th:object="${dish}" method="POST"> Dish name: <input type="text" th:field="*{name}" placeholder="Dish name"><br> <div id ="map"> <span>MAP:</span> <input type="checkbox" class="ingredientsMap" name="ingredientsMap" th:each="key: ${map.keySet()}" th:text="${key}" th:value="${key}" th:checked="${map.get(key)}"> </div> <button type="submit">Save Dish</button> </form> </div> </div> <div th:unless="${dish} != null"> Dish not found! </div> <a th:href="@{/}">Back to Ingredients List</a> </body> </html>
原因分析
- 表单参数无法自动封装为Map:模板中复选框的
name="ingredientsMap"无法被Spring MVC自动转换为Map<String, Boolean>并放入Model,导致POST请求时map属性为null。 - 冗余中间表实体:
DishIngredient实体完全多余,Dish和Ingredient的@ManyToMany注解已经自动维护中间表,手动映射会引发Hibernate冲突。 - 控制器逻辑错误:
saveDish方法尝试从Model中获取不存在的map属性,直接触发空指针异常。
修复步骤
1. 删除冗余的DishIngredient实体
直接删除该类,@ManyToMany注解会自动管理dish_ingredient中间表。
2. 优化Thymeleaf模板
调整复选框的命名和绑定逻辑,让Spring MVC能直接接收选中的配料名称:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" lang="en"> <head> <meta charset="UTF-8"> </head> <body> <div th:if="${dish} != null"> <div class="container"> <h1>编辑菜品</h1> <hr> <form action="#" th:action="@{/dish/save}" th:object="${dish}" method="POST"> <input type="hidden" th:field="*{id}"/> 菜品名称: <input type="text" th:field="*{name}" placeholder="菜品名称"><br> <div id="ingredients"> <span>配料选择:</span> <input type="checkbox" th:each="ingredient : ${allIngredients}" th:name="selectedIngredients" th:value="${ingredient.name}" th:checked="${dishIngredientNames.contains(ingredient.name)}" th:text="${ingredient.name}"> </div> <button type="submit">保存菜品</button> </form> </div> </div> <div th:unless="${dish} != null"> 未找到菜品! </div> <a th:href="@{/}">返回配料列表</a> </body> </html>
- 添加隐藏的
id字段,确保Spring MVC能识别要修改的菜品 - 用
selectedIngredients作为复选框名称,提交时会以字符串数组形式传递选中的配料名称 - 传入已选配料名称集合,用于渲染复选框的选中状态
3. 重构DishController逻辑
移除Map相关代码,直接处理表单提交的配料名称:
@GetMapping("/dish/update/{id}") public String updateDishForm(@PathVariable("id") long id, Model model) { Dish dish = dishService.getDish(id); model.addAttribute("dish", dish); // 传递所有配料列表 List<Ingredient> allIngredients = new ArrayList<>(ingredientService.getAllIngredients()); model.addAttribute("allIngredients", allIngredients); // 传递当前菜品已选配料的名称集合 Set<String> dishIngredientNames = dish.getIngredients().stream() .map(Ingredient::getName) .collect(Collectors.toSet()); model.addAttribute("dishIngredientNames", dishIngredientNames); return "updatedish"; } @PostMapping("/dish/save") public String saveDish(@ModelAttribute("dish") Dish dish, @RequestParam(value = "selectedIngredients", required = false) List<String> selectedIngredientNames) { Set<Ingredient> selectedIngredients = new HashSet<>(); // 处理空选择的情况,避免NPE if (selectedIngredientNames != null) { for (String name : selectedIngredientNames) { Ingredient ingredient = ingredientService.getIngredientByName(name); if (ingredient != null) { selectedIngredients.add(ingredient); } } } dish.setIngredients(selectedIngredients); dishService.saveDish(dish); return "redirect:/"; } // 删除原来的getCheckedIngredientsMap和getIngredientSetFromMap方法
4. 修正Ingredient实体的双向映射
调整@ManyToMany注解,避免重复维护中间表:
@Entity @Table(name="ingredient") public class Ingredient implements Serializable { // 其他字段不变 @ManyToMany(mappedBy = "ingredients", cascade = CascadeType.MERGE) private Set<Dish> dishes = new HashSet<>(); // getter和setter不变 }
- 用
mappedBy指定由Dish端维护关联关系,避免双向映射冲突
验证修复
- 启动应用进入菜品编辑页面,确认复选框正确显示已选配料
- 修改配料选择后提交表单,查看数据库
dish_ingredient表,确认关联记录已更新
内容的提问来源于stack exchange,提问作者Seweryn
相关产品推荐
相关产品推荐

