Включите сериализацию HAL в Spring Boot для пользовательского метода контроллера
Я пытаюсь создать RESTful API с помощью Spring Boot, используя spring-boot-starter-data-rest. Есть несколько сущностей: аккаунты, транзакции, категории и пользователи - просто обычные вещи.
Когда я получаю объекты в http://localhost:8080/transactions через API, сгенерированный по умолчанию, все идет хорошо, и я получаю список со всеми транзакциями в виде объектов JSON, как этот:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"_links": {
"self": {
"href": "http://localhost:8080/transactions/5"
},
"category": {
"href": "http://localhost:8080/transactions/5/category"
},
"account": {
"href": "http://localhost:8080/transactions/5/account"
}
}
}
Но теперь цель состоит в том, чтобы получить только последние транзакции по этому URL, так как я не хочу сериализовать всю таблицу базы данных. Итак, я написал контроллер:
@Controller
public class TransactionController {
private final TransactionRepository transactionRepository;
@Autowired
public TransactionController(TransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
}
// return the 5 latest transactions
@RequestMapping(value = "/transactions", method = RequestMethod.GET)
public @ResponseBody List<Transaction> getLastTransactions() {
return transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent();
}
}
Когда я сейчас пытаюсь получить доступ к http://localhost:8080/transactions, есть
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
из-за круговой ссылки между пользователями и учетными записями. Когда я решу эту проблему, добавив аннотацию @JsonBackReference в список учетных записей пользователя, я смогу получить список транзакций, но только с этим "классическим" форматом:
{
"id": 5,
"amount": -4.5,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza",
"account": {
"id": 2,
"name": "Account Tilman",
"owner": {
"id": 1,
"name": "Tilman"
},
"categories": [
{
"id": 1,
"name": "Groceries"
},
{
"id": 2,
"name": "Restaurant"
}
],
"users": [
{
"id": 1,
"name": "Tilman"
}
]
},
"category": {
"id": 2,
"name": "Restaurant"
}
}
Больше нет HAL-ссылок, все сериализуется напрямую Джексоном. Я пытался добавить
@EnableHypermediaSupport(type = HypermediaType.HAL)
к классам сущностей, но это никуда меня не привело. Я просто хочу, чтобы мой контроллер возвращал те же объекты, что и сгенерированный API, с HAL _links вместо каждой ссылки, которая сериализуется. Какие-нибудь мысли?
РЕДАКТИРОВАТЬ: ОК, подумав дважды, я понял, что аннотацию @EnableHypermediaSupport необходимо добавить в конфигурацию, конечно. Это решает проблему циклических ссылок, и я могу удалить @JsonBackReference от пользователя. Но сериализуются только атрибуты самого объекта, нет секции _links:
{
"amount": -4.81,
"date": "2014-06-17T21:18:00.000+0000",
"description": "Pizza"
}
Я знаю, что мог бы написать классы-обертки, расширяющие ResourceSupport для всех моих сущностей, но это кажется довольно бессмысленным. Поскольку spring-hateoas может волшебным образом генерировать представления с разделом _link для интерфейса REST, который создается автоматически, должен быть способ вернуть те же представления из пользовательского контроллера, верно?
3 ответа
Здесь много аспектов:
Я сомневаюсь, что ресурс коллекции на
/transactions
действительно возвращает отдельную транзакцию, как вы описали. Эти представления возвращаются для ресурсов элемента.Если
TransactionRepository
уже являетсяPageableAndSortingRepository
ресурс коллекции может быть изменен путем расширения шаблона URI, представленного в корне API для ссылки с именемtransactions
, По умолчанию этоpage
,size
а такжеsort
параметр. Это означает, что клиенты могут запросить то, что вы хотите выставить уже.Если вы хотите по умолчанию использовать параметры подкачки и сортировки, реализация контроллера является правильным способом. Однако, чтобы получить представление, подобное представлению Spring Data REST, вам нужно вернуть как минимум несколько экземпляров
ResourceSupport
поскольку это тип, для которого зарегистрировано отображение HAL.Здесь нет ничего волшебного, если вы думаете об этом. Простая сущность не имеет никаких ссылок,
ResourcesSupport
и типа, какResource<T>
позволяет вам обернуть сущность и обогатить ее ссылками, как вы считаете нужным. Spring Data REST в основном делает это для вас, используя множество знаний о домене и структуре хранилища, которые доступны неявно. Вы можете использовать много, как показано ниже.Есть несколько помощников, о которых вам нужно знать:
PersistentEntityResourceAssembler
- который обычно вводится в метод контроллера. Он отображает одну сущность Spring REST, что означает, что ассоциации, указывающие на управляемые типы, будут отображаться как ссылки и т. Д.PagedResourcesAssembler
- обычно вводится в экземпляр контроллера. Заботится о подготовке элементов, содержащихся на странице, по желанию с помощью выделенногоResourceAssembler
,
Spring Data REST в основном делает для страниц следующее:
PersistentEntityResourceAssembler entityAssembler = …; Resources<?> … = pagedResourcesAssembler.toResources(page, entityAssembler);
Это в основном с использованием
PagedResourcesAssembler
сPersistentEntityResourceAssembler
для визуализации предметов.Возвращая это
Resources
Экземпляр должен дать вам дизайн представления, который вы ожидали.
Вам не нужно создавать свой собственный контроллер для ограничения результатов запроса или сортировки результатов. Просто создайте метод запроса в вашем хранилище:
public interface TransactionRepository extends MongoRepository<Transaction, String> {
List<Transaction> findFirst10ByOrderByDateDesc();
}
Spring Data REST автоматически экспортирует его как ресурс метода в /transactions/search/findFirst10ByOrderByDateDesc
,
Чтобы использовать PersistentEntityResourceAssembler в контроллере, мы должны пометить его как @RepositoryRestController
@RestController
@RequestMapping("/categories")
@RepositoryRestController
public class CategoryController implements ValidableController {
// dependencies
@RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<PersistentEntityResource> create(@Valid @RequestBody CategoryForm category,
BindingResult validation,
PersistentEntityResourceAssembler resourceAssembler)
{
validate(validation);
Category entity = categoryConverter.convert(category);
entity = categoryService.save(entity);
return ResponseEntity.ok(resourceAssembler.toFullResource(entity));
}
Он создает довольно хороший ответ в стиле HAL
{
"createdTime": "2018-07-24T00:55:32.854",
"updatedTime": "2018-07-24T00:55:32.855",
"name": "cfvfcdfgdfdfdfs32",
"options": [
"aaa",
"bbb"
],
"_links": {
"self": {
"href": "http://localhost:8080/shop/categories/34"
},
"category": {
"href": "http://localhost:8080/shop/categories/34{?projection}",
"templated": true
},
"products": {
"href": "http://localhost:8080/shop/categories/34/products"
},
"categories": {
"href": "http://localhost:8080/shop/categories/34/categories{?projection}",
"templated": true
},
"parent": {
"href": "http://localhost:8080/shop/categories/34/parent{?projection}",
"templated": true
}
}
}