You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

原因分析

  1. 表单参数无法自动封装为Map:模板中复选框的name="ingredientsMap"无法被Spring MVC自动转换为Map<String, Boolean>并放入Model,导致POST请求时map属性为null。
  2. 冗余中间表实体:DishIngredient实体完全多余,Dish和Ingredient的@ManyToMany注解已经自动维护中间表,手动映射会引发Hibernate冲突。
  3. 控制器逻辑错误: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端维护关联关系,避免双向映射冲突

验证修复

  1. 启动应用进入菜品编辑页面,确认复选框正确显示已选配料
  2. 修改配料选择后提交表单,查看数据库dish_ingredient表,确认关联记录已更新

内容的提问来源于stack exchange,提问作者Seweryn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 08:27:26