#java #forms #spring-boot #post #freemarker
#java #формы #spring-boot #Публикация #freemarker
Вопрос:
Описание проблемы
Spring boot не может найти данные, отправленные в теле запроса.
Как указано ниже, в извлечениях кода я отправляю форму с application/x-www-form-urlencoded
типом содержимого в конечную POST /cards
точку. Хороший метод вызывается Spring boot, но данные из тела запроса не загружаются в объект card, который передается как параметр (см. Вывод консоли ниже).
Версии:
- Весенняя загрузка: 2.3.4.ВЫПУСК
- 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. Может быть, вы могли бы сказать ему использовать конструктор вместо использования пустого общедоступного конструктора и установщиков