Spring MVC中DTO接收表单参数时类型转换失败问题求助
问题描述
我有一对多关联的Tabel与Person实体类,同时定义了对应的TabelDto和PersonDto数据传输对象。在Spring MVC控制器中使用@ModelAttribute注解接收TabelDto类型的表单参数时,触发类型转换错误:无法将String类型的Person ID值转换为PersonDto类型。直接使用Tabel实体类接收参数功能正常,但我希望通过DTO处理请求而非直接操作实体,请问该如何解决此问题?
实体类代码
@Entity @Data @Builder @AllArgsConstructor @NoArgsConstructor @Table(name = "tabel") public class Tabel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; private String comments; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "person_id") private Person person; } @Entity @Builder @Data @NoArgsConstructor @AllArgsConstructor public class Person { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String lastname; private String surname; }
DTO代码
@Data @Builder @EqualsAndHashCode @Jacksonized public class TabelDto implements Serializable { private Long id; private String comments; private PersonDto person; } @Value @Builder public class PersonDto implements Serializable { Long id; String name; String lastname; String surname; }
控制器代码
@PostMapping("/addTabel") public String addTabel(@ModelAttribute TabelDto tabelDto) { tabelService.save(tabelDto); return "redirect:/addTabel"; } @GetMapping("/addTabel") public String addTabel(Model model) { TabelDto tabelDto = TabelDto.builder().build(); model.addAttribute("persons", personService.findAllPerson()); model.addAttribute("tabelDTO", tabelDto); return "addTabel"; }
Thymeleaf表单代码
<form action="#" th:action="@{/tabel/addTabel}" method="post" th:object="${tabelDTO}"> <label>person:</label> <label> <select th:field="*{person}" class="form-control"> <option th:each="person: ${persons}" th:value="${person.id}" th:text="${person.name}"></option> </select> </label> <button type="submit">Register</button> </form>
错误信息
[tabel] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String by.gus.tabel.controller.TabelController.addTabel(by.gus.tabel.dto.TabelDto): [Field error in object 'tabelDto' on field 'person': rejected value [1]; codes [typeMismatch.tabelDto.person,typeMismatch.person,typeMismatch.by.gus.tabel.dto.PersonDto,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [tabelDto.person,person]; arguments []; default message [person]]; default message [Failed to convert value of type 'java.lang.String' to required type 'by.gus.tabel.dto.PersonDto'; Cannot convert value of type 'java.lang.String' to required type 'by.gus.tabel.dto.PersonDto' for property 'person': no matching editors or conversion strategy found]] ]
解决方案
Spring MVC默认无法自动将String类型的ID转换为自定义DTO对象,以下是三种可行的解决方式:
方式一:自定义PropertyEditor
为PersonDto类型注册自定义属性编辑器,告诉Spring如何将String类型的ID转换为PersonDto。
- 实现PropertyEditor类
import by.gus.tabel.dto.PersonDto; import by.gus.tabel.entity.Person; import by.gus.tabel.service.PersonService; import java.beans.PropertyEditorSupport; import org.springframework.beans.factory.annotation.Autowired; public class PersonDtoEditor extends PropertyEditorSupport { private final PersonService personService; @Autowired public PersonDtoEditor(PersonService personService) { this.personService = personService; } @Override public void setAsText(String text) throws IllegalArgumentException { Long personId = Long.parseLong(text); Person person = personService.findPersonById(personId); PersonDto personDto = PersonDto.builder() .id(person.getId()) .name(person.getName()) .lastname(person.getLastname()) .surname(person.getSurname()) .build(); setValue(personDto); } }
- 在控制器中注册编辑器
添加@InitBinder注解的方法,将自定义编辑器绑定到PersonDto类型:
@Controller @RequestMapping("/tabel") public class TabelController { private final PersonService personService; @Autowired public TabelController(PersonService personService) { this.personService = personService; } @InitBinder public void initBinder(WebDataBinder binder) { binder.registerCustomEditor(PersonDto.class, new PersonDtoEditor(personService)); } // 原有GetMapping和PostMapping方法保持不变 }
方式二:实现Converter接口
实现Spring的Converter<String, PersonDto>接口,这种方式是全局生效的,适用于多个控制器需要转换的场景。
- 编写Converter类
import by.gus.tabel.dto.PersonDto; import by.gus.tabel.entity.Person; import by.gus.tabel.service.PersonService; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class StringToPersonDtoConverter implements Converter<String, PersonDto> { private final PersonService personService; public StringToPersonDtoConverter(PersonService personService) { this.personService = personService; } @Override public PersonDto convert(String source) { Long personId = Long.parseLong(source); Person person = personService.findPersonById(personId); return PersonDto.builder() .id(person.getId()) .name(person.getName()) .lastname(person.getLastname()) .surname(person.getSurname()) .build(); } }
- 注册Converter(Spring Boot自动扫描@Component,无需额外配置)
如果是传统Spring MVC,需在Web配置类中手动注册:
import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { private final StringToPersonDtoConverter converter; public WebConfig(StringToPersonDtoConverter converter) { this.converter = converter; } @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(converter); } }
方式三:修改DTO结构与表单绑定
简化DTO结构,直接接收Person的ID,在服务层完成ID到DTO/实体的转换。
- 修改TabelDto
将PersonDto person替换为Long personId:
@Data @Builder @EqualsAndHashCode @Jacksonized public class TabelDto implements Serializable { private Long id; private String comments; private Long personId; // 替换原有的PersonDto字段 }
- 调整Thymeleaf表单的字段绑定
<select th:field="*{personId}" class="form-control"> <option th:each="person: ${persons}" th:value="${person.id}" th:text="${person.name}"></option> </select>
- 在服务层处理转换
@Service public class TabelService { private final TabelRepository tabelRepository; private final PersonService personService; @Autowired public TabelService(TabelRepository tabelRepository, PersonService personService) { this.tabelRepository = tabelRepository; this.personService = personService; } public void save(TabelDto tabelDto) { Person person = personService.findPersonById(tabelDto.getPersonId()); Tabel tabel = Tabel.builder() .comments(tabelDto.getComments()) .person(person) .build(); tabelRepository.save(tabel); } }
方案对比
- 方式一、二适合多场景复用
PersonDto接收参数的情况,方式一仅作用于当前控制器,方式二全局生效。 - 方式三实现简单,无需额外转换类,适合仅少数场景使用的情况。
内容的提问来源于stack exchange,提问作者Сяргей Пяшэвіч
相关产品推荐
相关产品推荐

