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

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

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

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

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

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

控制器

@Controller
public class CardController implements ControllerHelper {

    @PostMapping(value = "/cards", consumes = MediaType.ALL_VALUE)
    public String createNewCard(
            @ModelAttribute Card card,
            BindingResult result,
            ModelMap model
    ) {
        System.out.println("\n### createNewCard ###\n");
        System.out.println("card: "+card);
        System.out.println("result: "+result);
        System.out.println("model: "+model);

        return "/cards/editor";
    }

    @GetMapping(value = "/cards/form")
    public String newPost(
            Model model
    ) {
        model.addAttribute("card", Card.defaultEmptyCard);
        return "/cards/editor";
    }
}

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

<form action="/cards"
      method="POST"
      modelAttribute="card"
      enctype="application/x-www-form-urlencoded"
>
    <div class="form-group">
        <label for="title">Title &amp; 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>

Card 实体

@Entity
public class Card implements Comparable<Card> {

    // ...(省略了部分代码)

    private Card(
            @NonNull String rawSeoCode,
            @NonNull String title,
            @NonNull String description,
            @NonNull String content,
            @NonNull LocalDate publishedDate
    ) {
        this.seoCode = formatSeoCode(rawSeoCode);
        this.title = title;
        this.description = description;
        this.content = content;
        this.publishedDate = publishedDate;
    }

    public static Card createCard(
            @NonNull String seoCode,
            @NonNull String title,
            @NonNull String description,
            @NonNull String content,
            @NonNull LocalDate publishedDate
    ) {
        return new Card(
                seoCode,
                title,
                description,
                content,
                publishedDate
        );
    }

    public static Card createCard(
            @NonNull String seoCode,
            @NonNull String title,
            @NonNull String description,
            @NonNull String content
    ) {
        LocalDate publishedDate = LocalDate.now();
        return new Card(
                seoCode,
                title,
                description,
                content,
                publishedDate
        );
    }

    // ...(省略了部分代码)

    @Override
    public String toString() {
        return "Card<" + super.toString() + ">{" +
                "id=" + id +
                ", seoCode='" + seoCode + '\'' +
                ", publishedDate=" + publishedDate +
                ", title='" + title + '\'' +
                ", description='" + description + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    // ...(省略了部分代码)


<details>
<summary>英文:</summary>

## Description of the problem

Spring boot cannot find the data sent in request body.

As specified below, in code extracts, I send form with `application/x-www-form-urlencoded` content-type to the endpoint `POST /cards`.
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).

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



### 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

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

@Controller
public class CardController implements ControllerHelper {

@PostMapping(value = &quot;/cards&quot;, consumes = MediaType.ALL_VALUE)
public String createNewCard(
@ModelAttribute Card card,
BindingResult result,
ModelMap model
) {
System.out.println(&quot;\n### createNewCard ###\n&quot;);
System.out.println(&quot;card: &quot;+card);
System.out.println(&quot;result: &quot;+result);
System.out.println(&quot;model: &quot;+model);
return &quot;/cards/editor&quot;;
}
@GetMapping(value = &quot;/cards/form&quot;)
public String newPost(
Model model
) {
model.addAttribute(&quot;card&quot;, Card.defaultEmptyCard);
return &quot;/cards/editor&quot;;
}

}


### 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>


### Card entity

@Entity
public class Card implements Comparable<Card> {

protected Card() {}
public static final Card defaultEmptyCard = new Card();
private final static Logger logger = LoggerFactory.getLogger(Card.class);
@Autowired
private ObjectMapper objectMapper;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
@NotBlank(message = &quot;Value for seoCode (the slug) is mandatory&quot;)
@Column(unique=true)
private String seoCode;
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate publishedDate;
@NotBlank(message = &quot;Value for title is mandatory&quot;)
private String title;
@NotBlank(message = &quot;Value for description is mandatory&quot;)
private String description;
@NotBlank(message = &quot;Value for content is mandatory&quot;)
private String content;
public boolean hasIdUndefine() {
return null == id;
}
public boolean hasIdDefined() {
return null != id;
}
public Long getId() {
return id;
}
public String getSeoCode() {
return seoCode;
}
public LocalDate getPublishedDate() {
return publishedDate;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public String getContent() {
return content;
}
private String formatSeoCode(String candidateSeoCode) {
return candidateSeoCode.replaceAll(&quot;[^0-9a-zA-Z_-]&quot;,&quot;&quot;);
}
private Card(
@NonNull String rawSeoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content,
@NonNull LocalDate publishedDate
) {
this.seoCode = formatSeoCode(rawSeoCode);
this.title = title;
this.description = description;
this.content = content;
this.publishedDate = publishedDate;
}
public static Card createCard(
@NonNull String seoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content,
@NonNull LocalDate publishedDate
) {
return new Card(
seoCode,
title,
description,
content,
publishedDate
);
}
public static Card createCard(
@NonNull String seoCode,
@NonNull String title,
@NonNull String description,
@NonNull String content
) {
LocalDate publishedDate = LocalDate.now();
return new Card(
seoCode,
title,
description,
content,
publishedDate
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Card card = (Card) o;
return Objects.equals(id, card.id) &amp;&amp;
seoCode.equals(card.seoCode) &amp;&amp;
publishedDate.equals(card.publishedDate) &amp;&amp;
title.equals(card.title) &amp;&amp;
description.equals(card.description) &amp;&amp;
content.equals(card.content);
}
@Override
public int hashCode() {
return Objects.hash(id, seoCode, publishedDate, title, description, content);
}
@Override
public String toString() {
return &quot;Card&lt;&quot;+ super.toString() +&quot;&gt;{&quot; +
&quot;id=&quot; + id +
&quot;, seoCode=&#39;&quot; + seoCode + &#39;\&#39;&#39; +
&quot;, publishedDate=&quot; + publishedDate +
&quot;, title=&#39;&quot; + title + &#39;\&#39;&#39; +
&quot;, description=&#39;&quot; + description + &#39;\&#39;&#39; +
&quot;, content=&#39;&quot; + content + &#39;\&#39;&#39; +
&#39;}&#39;;
}
public Either&lt;JsonProcessingException,String&gt; safeJsonSerialize(
ObjectMapper objectMapper
) {
try {
return Right(objectMapper.writeValueAsString(this));
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
return Left(e);
}
}
public Either&lt;JsonProcessingException,String&gt; safeJsonSerialize() {
try {
return Right(objectMapper.writeValueAsString(this));
} catch (JsonProcessingException e) {
logger.error(e.getMessage());
return Left(e);
}
}
@Override
public int compareTo(@NotNull Card o) {
int publicationOrder  = this.publishedDate.compareTo(o.publishedDate);
int defaultOrder  = this.seoCode.compareTo(o.seoCode);
return publicationOrder == 0 ? defaultOrder : publicationOrder;
}

}


## Edit
I got a good answer.
It works when adding empty constructor and setters to the Card entity.
However, it&#39;s not the class I want. 
I want card to be only instantiated with a constructor that have all parameters.
Do you have an idea about how to achieve that ?
Should I create another class to represent the form ?
Oris there a way to only allow Spring to use such setters ?
</details>
# 答案1
**得分**: 1
你确定你的 `Card.java` 文件有适当的 getter 和 setter 吗?这样 Spring 才能够真正地填充数据到它正在尝试创建的对象中。
<details>
<summary>英文:</summary>
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.
</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:

确定