简体   繁体   中英

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

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'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 = "/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 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 = "Value for seoCode (the slug) is mandatory")
    @Column(unique=true)
    private String seoCode;

    @JsonDeserialize(using = LocalDateDeserializer.class)
    @JsonSerialize(using = LocalDateSerializer.class)
    private LocalDate publishedDate;

    @NotBlank(message = "Value for title is mandatory")
    private String title;

    @NotBlank(message = "Value for description is mandatory")
    private String description;

    @NotBlank(message = "Value for content is mandatory")
    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("[^0-9a-zA-Z_-]","");
    }

    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) &&
                seoCode.equals(card.seoCode) &&
                publishedDate.equals(card.publishedDate) &&
                title.equals(card.title) &&
                description.equals(card.description) &&
                content.equals(card.content);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, seoCode, publishedDate, title, description, content);
    }

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

    public Either<JsonProcessingException,String> safeJsonSerialize(
            ObjectMapper objectMapper
    ) {
        try {
            return Right(objectMapper.writeValueAsString(this));
        } catch (JsonProcessingException e) {
            logger.error(e.getMessage());
            return Left(e);
        }
    }

    public Either<JsonProcessingException,String> 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'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 ?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM