Spring boot не привязывает данные, отправленные формой, к конечной точке POST

#java #forms #spring-boot #post #freemarker

#java #формы #spring-boot #Публикация #freemarker

Вопрос:

Описание проблемы

Spring boot не может найти данные, отправленные в теле запроса.

Как указано ниже, в извлечениях кода я отправляю форму с application/x-www-form-urlencoded типом содержимого в конечную POST /cards точку. Хороший метод вызывается Spring boot, но данные из тела запроса не загружаются в объект card, который передается как параметр (см. Вывод консоли ниже).

Версии:

  1. Весенняя загрузка: 2.3.4.ВЫПУСК
  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 cardamp;seoCode=first-cardamp;description=This is the first card of the blogamp;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> {

    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) 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 "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;
    }
}

  

Редактировать

Я получил хороший ответ. Это работает при добавлении пустого конструктора и установщиков к объекту Card. Однако это не тот класс, который мне нужен. Я хочу, чтобы card создавался только с помощью конструктора, имеющего все параметры. У вас есть идея о том, как этого добиться? Должен ли я создать другой класс для представления формы? Или есть способ разрешить Spring использовать только такие сеттеры?

Ответ №1:

Вы убедились, что у вас Card.java есть соответствующие получатели и установщики? Таким образом, spring может фактически заполнить данные в объекте, который он пытается создать.

Комментарии:

1. Очень интересно… Я добавил общедоступный конструктор и установщик для Card, тогда он работает. Спасибо вам, Марк! Но я не хочу разрешать сеттеру создавать подобную карту. Я хотел бы убедиться, что конструктор вызывается со всеми параметрами. Каковы будут мои варианты? Новый класс используется только для заполнения формы? Скопировать карту, сгенерированную Spring, с помощью моего конструктора?

2. Возможно, вам следует посмотреть, как Джексон десериализует ваш dto. Может быть, вы могли бы сказать ему использовать конструктор вместо использования пустого общедоступного конструктора и установщиков