Spring Boot无法将表单发送的数据绑定到POST端点。

huangapple go评论105阅读模式
英文:

Spring boot doesn't bind the data sent by form to POST endpoint

问题

问题描述

Spring Boot 无法在请求体中找到发送的数据。

如下所示,在代码片段中,我使用 application/x-www-form-urlencoded 内容类型将表单发送到端点 POST /cards
Spring Boot 调用了正确的方法,但请求体中的数据没有加载到 card 实体中,该实体作为参数传递(请参阅下面的控制台输出)。

版本:

  1. Spring Boot: 2.3.4.RELEASE
  2. spring-boot-starter-freemarker: 2.3.4.RELEASE

控制台输出(手动在请求过滤器中读取请求体):

  1. 2020-10-21 00:26:58.594 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : New request method=POST path=/cards content-type=application/x-www-form-urlencoded
  2. 2020-10-21 00:26:58.595 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : RequestBody: title=First+card&seoCode=first-card&description=This+is+the+first+card+of+the+blog&content=I+think+I+need+help+about+this+one...
  3. ### createNewCard ###
  4. card: Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}
  5. result: org.springframework.validation.BeanPropertyBindingResult: 0 errors
  6. model: {card=Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}, org.springframework.validation.BindingResult.card=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
  7. 2020-10-21 00:26:58.790 TRACE 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : Response to request method=POST path=/cards status=200 elapsedTime=196ms

(在这里我使用 req.getReader() 读取了请求体,但通常我会将其注释掉以避免消耗缓冲区。)

控制器

  1. @Controller
  2. public class CardController implements ControllerHelper {
  3. @PostMapping(value = "/cards", consumes = MediaType.ALL_VALUE)
  4. public String createNewCard(
  5. @ModelAttribute Card card,
  6. BindingResult result,
  7. ModelMap model
  8. ) {
  9. System.out.println("\n### createNewCard ###\n");
  10. System.out.println("card: "+card);
  11. System.out.println("result: "+result);
  12. System.out.println("model: "+model);
  13. return "/cards/editor";
  14. }
  15. @GetMapping(value = "/cards/form")
  16. public String newPost(
  17. Model model
  18. ) {
  19. model.addAttribute("card", Card.defaultEmptyCard);
  20. return "/cards/editor";
  21. }
  22. }

HTML 表单(使用 Freemarker 模板编写):

  1. <form action="/cards"
  2. method="POST"
  3. modelAttribute="card"
  4. enctype="application/x-www-form-urlencoded"
  5. >
  6. <div class="form-group">
  7. <label for="title">Title &amp; SEO slug code</label>
  8. <div class="form-row">
  9. <div class="col-9">
  10. <@spring.formInput
  11. "card.title"
  12. "class='form-control' placeholder='Title'"
  13. />
  14. <@spring.showErrors "<br>"/>
  15. </div>
  16. <div class="col-2">
  17. <@spring.formInput
  18. "card.seoCode"
  19. "class='form-control' placeholder='SEO slug code' aria-describedby='seoCodeHelp'"
  20. />
  21. <@spring.showErrors "<br>"/>
  22. </div>
  23. <div class="col-1">
  24. <@spring.formInput
  25. "card.id"
  26. "DISABLED class='form-control' placeholder='ID'"
  27. />
  28. </div>
  29. </div>
  30. <div class="form-row">
  31. <small id="seoCodeHelp" class="form-text text-muted">
  32. Keep SEO slug very small and remove useless words.
  33. </small>
  34. </div>
  35. </div>
  36. <div class="form-group">
  37. <label for="description">Description</label>
  38. <@spring.formInput
  39. "card.description"
  40. "class='form-control' placeholder='Short description of this card..' aria-describedby='descriptionHelp'"
  41. />
  42. <small id="descriptionHelp" class="form-text text-muted">
  43. Keep this description as small as possible.
  44. </small>
  45. </div>
  46. <div class="form-group">
  47. <label for="content">Content</label>
  48. <@spring.formTextarea
  49. "card.content"
  50. "class='form-control' rows='5'"
  51. />
  52. </div>
  53. <button type="submit" class="btn btn-primary">Save</button>
  54. </form>

Card 实体

  1. @Entity
  2. public class Card implements Comparable<Card> {
  3. // ...(省略了部分代码)
  4. private Card(
  5. @NonNull String rawSeoCode,
  6. @NonNull String title,
  7. @NonNull String description,
  8. @NonNull String content,
  9. @NonNull LocalDate publishedDate
  10. ) {
  11. this.seoCode = formatSeoCode(rawSeoCode);
  12. this.title = title;
  13. this.description = description;
  14. this.content = content;
  15. this.publishedDate = publishedDate;
  16. }
  17. public static Card createCard(
  18. @NonNull String seoCode,
  19. @NonNull String title,
  20. @NonNull String description,
  21. @NonNull String content,
  22. @NonNull LocalDate publishedDate
  23. ) {
  24. return new Card(
  25. seoCode,
  26. title,
  27. description,
  28. content,
  29. publishedDate
  30. );
  31. }
  32. public static Card createCard(
  33. @NonNull String seoCode,
  34. @NonNull String title,
  35. @NonNull String description,
  36. @NonNull String content
  37. ) {
  38. LocalDate publishedDate = LocalDate.now();
  39. return new Card(
  40. seoCode,
  41. title,
  42. description,
  43. content,
  44. publishedDate
  45. );
  46. }
  47. // ...(省略了部分代码)
  48. @Override
  49. public String toString() {
  50. return "Card<" + super.toString() + ">{" +
  51. "id=" + id +
  52. ", seoCode='" + seoCode + '\'' +
  53. ", publishedDate=" + publishedDate +
  54. ", title='" + title + '\'' +
  55. ", description='" + description + '\'' +
  56. ", content='" + content + '\'' +
  57. '}';
  58. }
  59. // ...(省略了部分代码)
  60. <details>
  61. <summary>英文:</summary>
  62. ## Description of the problem
  63. Spring boot cannot find the data sent in request body.
  64. As specified below, in code extracts, I send form with `application/x-www-form-urlencoded` content-type to the endpoint `POST /cards`.
  65. The good method is called by Spring boot but data from the request body aren&#39;t loaded in card entity, which is passed as parameter (see console output below).
  66. Versions:
  67. 1. Spring boot: 2.3.4.RELEASE
  68. 2. spring-boot-starter-freemarker: 2.3.4.RELEASE
  69. ### Console output (with request body read manually in request filter):

2020-10-21 00:26:58.594 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : New request method=POST path=/cards content-type=application/x-www-form-urlencoded
2020-10-21 00:26:58.595 DEBUG 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : RequestBody: title=First+card&seoCode=first-card&description=This+is+the+first+card+of+the+blog&content=I+think+I+need+help+about+this+one...

createNewCard

card: Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}
result: org.springframework.validation.BeanPropertyBindingResult: 0 errors
model: {card=Card<com.brunierterry.cards.models.Card@34e63b41>{id=null, seoCode='null', publishedDate=null, title='null', description='null', content='null'}, org.springframework.validation.BindingResult.card=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
2020-10-21 00:26:58.790 TRACE 38768 --- [nio-8080-exec-1] c.b.c.c.f.RequestResponseLoggingFilter : Response to request method=POST path=/cards status=200 elapsedTime=196ms

  1. (Here I read body with `req.getReader()`, but I comment it usually to not consume the buffer.)
  2. ### Controller

@Controller
public class CardController implements ControllerHelper {

  1. @PostMapping(value = &quot;/cards&quot;, consumes = MediaType.ALL_VALUE)
  2. public String createNewCard(
  3. @ModelAttribute Card card,
  4. BindingResult result,
  5. ModelMap model
  6. ) {
  7. System.out.println(&quot;\n### createNewCard ###\n&quot;);
  8. System.out.println(&quot;card: &quot;+card);
  9. System.out.println(&quot;result: &quot;+result);
  10. System.out.println(&quot;model: &quot;+model);
  11. return &quot;/cards/editor&quot;;
  12. }
  13. @GetMapping(value = &quot;/cards/form&quot;)
  14. public String newPost(
  15. Model model
  16. ) {
  17. model.addAttribute(&quot;card&quot;, Card.defaultEmptyCard);
  18. return &quot;/cards/editor&quot;;
  19. }

}

  1. ### HTML form (wrote with freemarker template):

<form action="/cards"
method="POST"
modelAttribute="card"
enctype="application/x-www-form-urlencoded"
>
<div class="form-group">
<label for="title">Title & SEO slug code</label>
<div class="form-row">
<div class="col-9">
<@spring.formInput
"card.title"
"class='form-control' placeholder='Title'"
/>
<@spring.showErrors "<br>"/>
</div>
<div class="col-2">
<@spring.formInput
"card.seoCode"
"class='form-control' placeholder='SEO slug code' aria-describedby='seoCodeHelp'"
/>
<@spring.showErrors "<br>"/>
</div>
<div class="col-1">
<@spring.formInput
"card.id"
"DISABLED class='form-control' placeholder='ID'"
/>
</div>
</div>
<div class="form-row">
<small id="seoCodeHelp" class="form-text text-muted">
Keep SEO slug very small and remove useless words.
</small>
</div>
</div>
<div class="form-group">
<label for="description">Description</label>
<@spring.formInput
"card.description"
"class='form-control' placeholder='Short description of this card..' aria-describedby='descriptionHelp'"
/>
<small id="descriptionHelp" class="form-text text-muted">
Keep this description as small as possible.
</small>
</div>
<div class="form-group">
<label for="content">Content</label>
<@spring.formTextarea
"card.content"
"class='form-control' rows='5'"
/>
</div>
<button type="submit" class="btn btn-primary">Save</button>
</form>

  1. ### Card entity

@Entity
public class Card implements Comparable<Card> {

  1. protected Card() {}
  2. public static final Card defaultEmptyCard = new Card();
  3. private final static Logger logger = LoggerFactory.getLogger(Card.class);
  4. @Autowired
  5. private ObjectMapper objectMapper;
  6. @Id
  7. @GeneratedValue(strategy=GenerationType.AUTO)
  8. private Long id;
  9. @NotBlank(message = &quot;Value for seoCode (the slug) is mandatory&quot;)
  10. @Column(unique=true)
  11. private String seoCode;
  12. @JsonDeserialize(using = LocalDateDeserializer.class)
  13. @JsonSerialize(using = LocalDateSerializer.class)
  14. private LocalDate publishedDate;
  15. @NotBlank(message = &quot;Value for title is mandatory&quot;)
  16. private String title;
  17. @NotBlank(message = &quot;Value for description is mandatory&quot;)
  18. private String description;
  19. @NotBlank(message = &quot;Value for content is mandatory&quot;)
  20. private String content;
  21. public boolean hasIdUndefine() {
  22. return null == id;
  23. }
  24. public boolean hasIdDefined() {
  25. return null != id;
  26. }
  27. public Long getId() {
  28. return id;
  29. }
  30. public String getSeoCode() {
  31. return seoCode;
  32. }
  33. public LocalDate getPublishedDate() {
  34. return publishedDate;
  35. }
  36. public String getTitle() {
  37. return title;
  38. }
  39. public String getDescription() {
  40. return description;
  41. }
  42. public String getContent() {
  43. return content;
  44. }
  45. private String formatSeoCode(String candidateSeoCode) {
  46. return candidateSeoCode.replaceAll(&quot;[^0-9a-zA-Z_-]&quot;,&quot;&quot;);
  47. }
  48. private Card(
  49. @NonNull String rawSeoCode,
  50. @NonNull String title,
  51. @NonNull String description,
  52. @NonNull String content,
  53. @NonNull LocalDate publishedDate
  54. ) {
  55. this.seoCode = formatSeoCode(rawSeoCode);
  56. this.title = title;
  57. this.description = description;
  58. this.content = content;
  59. this.publishedDate = publishedDate;
  60. }
  61. public static Card createCard(
  62. @NonNull String seoCode,
  63. @NonNull String title,
  64. @NonNull String description,
  65. @NonNull String content,
  66. @NonNull LocalDate publishedDate
  67. ) {
  68. return new Card(
  69. seoCode,
  70. title,
  71. description,
  72. content,
  73. publishedDate
  74. );
  75. }
  76. public static Card createCard(
  77. @NonNull String seoCode,
  78. @NonNull String title,
  79. @NonNull String description,
  80. @NonNull String content
  81. ) {
  82. LocalDate publishedDate = LocalDate.now();
  83. return new Card(
  84. seoCode,
  85. title,
  86. description,
  87. content,
  88. publishedDate
  89. );
  90. }
  91. @Override
  92. public boolean equals(Object o) {
  93. if (this == o) return true;
  94. if (o == null || getClass() != o.getClass()) return false;
  95. Card card = (Card) o;
  96. return Objects.equals(id, card.id) &amp;&amp;
  97. seoCode.equals(card.seoCode) &amp;&amp;
  98. publishedDate.equals(card.publishedDate) &amp;&amp;
  99. title.equals(card.title) &amp;&amp;
  100. description.equals(card.description) &amp;&amp;
  101. content.equals(card.content);
  102. }
  103. @Override
  104. public int hashCode() {
  105. return Objects.hash(id, seoCode, publishedDate, title, description, content);
  106. }
  107. @Override
  108. public String toString() {
  109. return &quot;Card&lt;&quot;+ super.toString() +&quot;&gt;{&quot; +
  110. &quot;id=&quot; + id +
  111. &quot;, seoCode=&#39;&quot; + seoCode + &#39;\&#39;&#39; +
  112. &quot;, publishedDate=&quot; + publishedDate +
  113. &quot;, title=&#39;&quot; + title + &#39;\&#39;&#39; +
  114. &quot;, description=&#39;&quot; + description + &#39;\&#39;&#39; +
  115. &quot;, content=&#39;&quot; + content + &#39;\&#39;&#39; +
  116. &#39;}&#39;;
  117. }
  118. public Either&lt;JsonProcessingException,String&gt; safeJsonSerialize(
  119. ObjectMapper objectMapper
  120. ) {
  121. try {
  122. return Right(objectMapper.writeValueAsString(this));
  123. } catch (JsonProcessingException e) {
  124. logger.error(e.getMessage());
  125. return Left(e);
  126. }
  127. }
  128. public Either&lt;JsonProcessingException,String&gt; safeJsonSerialize() {
  129. try {
  130. return Right(objectMapper.writeValueAsString(this));
  131. } catch (JsonProcessingException e) {
  132. logger.error(e.getMessage());
  133. return Left(e);
  134. }
  135. }
  136. @Override
  137. public int compareTo(@NotNull Card o) {
  138. int publicationOrder = this.publishedDate.compareTo(o.publishedDate);
  139. int defaultOrder = this.seoCode.compareTo(o.seoCode);
  140. return publicationOrder == 0 ? defaultOrder : publicationOrder;
  141. }

}

  1. ## Edit
  2. I got a good answer.
  3. It works when adding empty constructor and setters to the Card entity.
  4. However, it&#39;s not the class I want.
  5. I want card to be only instantiated with a constructor that have all parameters.
  6. Do you have an idea about how to achieve that ?
  7. Should I create another class to represent the form ?
  8. Oris there a way to only allow Spring to use such setters ?
  9. </details>
  10. # 答案1
  11. **得分**: 1
  12. 你确定你的 `Card.java` 文件有适当的 getter setter 吗?这样 Spring 才能够真正地填充数据到它正在尝试创建的对象中。
  13. <details>
  14. <summary>英文:</summary>
  15. Did you make sure that you `Card.java` has the appropriate getters and setters? This way spring can actually populate the data in the object it is trying to create.
  16. </details>

huangapple
  • 本文由 发表于 2020年10月21日 00:12:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/64449149.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定