实体ID放URL还是表单隐藏字段?Spring MVC验证困境求解
解决Spring MVC中URL PathVariable与表单验证器共享ID的困境
这个问题我做后台开发时也碰到过,完全懂你的纠结——既要遵循REST规范把实体ID放在URL里,又要让表单的自定义验证器拿到这个ID做唯一名称校验,可验证逻辑偏偏跑在控制器方法执行之前,@PathVariable的ID根本传不过去。你现在把校验逻辑挪到控制器里虽然能跑,但确实不够优雅,下面给你几个更合理的解决方案:
方案一:通过@ModelAttribute提前把ID绑定到表单对象
在控制器里加一个@ModelAttribute注解的方法,这个方法会在每个请求处理方法之前执行,正好可以把URL里的channelId注入到ChannelEditForm中,这样验证器就能直接从表单对象里拿到ID了:
@Controller @RequestMapping("/channels") @RoleRestricted(resource = RoleResource.CHANNEL_ADMIN) public class ChannelAdminController { // ... 其他注入和代码 // 提前填充表单对象的ID @ModelAttribute("channelForm") public ChannelEditForm initChannelForm(@PathVariable(required = false) Long channelId) { ChannelEditForm form = new ChannelEditForm(); if (channelId != null && channelId > 0) { form.setId(channelId); // 如果需要的话,这里也可以提前从service拉取实体数据填充表单 // Channel channel = channelService.getById(channelId); // BeanUtils.copyProperties(channel, form); } return form; } // GET方法可以简化,因为form已经被@ModelAttribute初始化了 @RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET) public String editChannel(@ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model) { // 如果是新增(channelId=0),form已经有默认值;如果是编辑,已经填充了数据 return "channel/admin/channel-edit"; } // PUT方法里不用再处理ID的传递,直接用form里的ID就行 @RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT) public String saveChannel(@ModelAttribute("channelForm") @Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { return "channel/admin/channel-edit"; } // 后续直接用form.getId()获取channelId即可 // ... 保存逻辑 } // 验证器里直接从form拿ID做校验 @InitBinder("channelForm") protected void initBinder(WebDataBinder binder) { binder.setValidator(formValidator); } }
对应的验证器修改:
public class ChannelEditFormValidator implements Validator { private ChannelService channelService; @Autowired public ChannelEditFormValidator(ChannelService channelService) { this.channelService = channelService; } @Override public boolean supports(Class<?> clazz) { return ChannelEditForm.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ChannelEditForm form = (ChannelEditForm) target; Long currentChannelId = form.getId(); // 唯一名称校验 try { Long existingChannelId = channelService.getChannelIdByName(form.getName()); if (existingChannelId != null && !existingChannelId.equals(currentChannelId)) { errors.rejectValue("name", "channel:admin.f1.error.name"); } } catch (EmptyResultDataAccessException e) { // 名称不存在,没问题 } // 其他表单字段验证逻辑... } }
方案二:通过@InitBinder把PathVariable传递给验证器(需注意线程安全)
如果不想修改表单对象的结构,可以在初始化绑定器的时候,直接把URL里的channelId传递给验证器。不过要注意:Spring的验证器默认是单例的,所以必须用ThreadLocal来存储ID,避免多线程请求下的冲突:
首先修改验证器:
public class ChannelEditFormValidator implements Validator { private ChannelService channelService; private ThreadLocal<Long> currentChannelIdHolder = new ThreadLocal<>(); @Autowired public ChannelEditFormValidator(ChannelService channelService) { this.channelService = channelService; } // 用于设置当前请求的channelId public void setCurrentChannelId(Long channelId) { currentChannelIdHolder.set(channelId); } @Override public boolean supports(Class<?> clazz) { return ChannelEditForm.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ChannelEditForm form = (ChannelEditForm) target; Long currentChannelId = currentChannelIdHolder.get(); if (currentChannelId != null) { // 执行唯一名称校验 try { Long existingChannelId = channelService.getChannelIdByName(form.getName()); if (existingChannelId != null && !existingChannelId.equals(currentChannelId)) { errors.rejectValue("name", "channel:admin.f1.error.name"); } } catch (EmptyResultDataAccessException e) { // 名称可用 } } // 其他验证逻辑... } }
然后在控制器的@InitBinder方法里传递ID:
@InitBinder("channelForm") protected void initBinder(WebDataBinder binder, @PathVariable Long channelId) { formValidator.setCurrentChannelId(channelId); binder.setValidator(formValidator); }
这样验证器在执行校验时,就能拿到当前请求的channelId了,而且ThreadLocal保证了多线程下的安全性。
对比你当前的实现
你现在把校验逻辑放到控制器里的做法虽然可行,但会让控制器承担额外的验证职责,违反了单一职责原则。上面的两个方案都能把验证逻辑放回验证器中,让代码结构更清晰,也符合Spring MVC的设计理念。
内容的提问来源于stack exchange,提问作者marc82ch
相关产品推荐
相关产品推荐

