Рассмотрим случай передачи из spring контроллера списка сущностей, когда использование создаваемых на лету методов интерфейса spring data почему-то не подходит. Использовать будем hateoas. В данном примере мы будем передавать на вход методу сущность с параметрами поиска в виде JSON, на основе этой сущности потом будет формироваться JPA спецификация для передачи в метод репозитория.
Метод контроллера:
@MonitoredWithSpring @RepositoryRestController @RequestMapping(value = "topics") public class TopicController implements ResourceProcessor<PersistentEntityResource> { ... @RequestMapping(value = "search/searchWithCriteria", method = RequestMethod.POST, consumes = APPLICATION_JSON_VALUE, produces = MediaTypes.HAL_JSON_VALUE) public @ResponseBody Resources<PersistentEntityResource> searchTopics(@RequestBody Resource<TopicSearchCriteria> topicSearchCriteria, PersistentEntityResourceAssembler persistentEntityResourceAssembler) { List<Topic> entities = topicService.searchTopics(topicSearchCriteria.getContent()); List<PersistentEntityResource> resources = entities .stream() .map(persistentEntityResourceAssembler::toResource) .collect(Collectors.toList()); return new Resources<PersistentEntityResource>(resources); } ... } |
Запомним, что это метод обслуживает ссылку «topics/search/searchWithCriteria».
Действия в службе примерно такие:
@Override @Transactional public List<Topic> searchTopics(TopicSearchCriteria criteria) { TopicSpecification spec = new TopicSpecification(criteria); List<Topic> topics = topicRepo.findAll(spec); return topics; } |
По JPA спецификациям много статей, поэтому рассматривать их не буду.
Для полной совместимости с HATEOAS вам нужно будет добиться того, чтобы в разделе ссылок «search» у сущности Topic появилась ссылка «searchWithCriteria». Я делал это в отдельном компоненте (это даже не контроллер — ни одной ссылки не обслуживает), который даже не обслуживает ссылки:
@Component public class TopicSearchComponent implements ResourceProcessor<RepositorySearchesResource> { @Autowired private EntityLinks entityLinks; @Override public RepositorySearchesResource process(RepositorySearchesResource resource) { if (resource.getDomainType().equals(Topic.class)) { LinkBuilder lb = entityLinks.linkFor(Topic.class).slash("search"); resource.add(new Link(lb.toString() + "/searchWithCriteria", "searchWithCriteria")); } return resource; } } |
Для того, чтобы получить доступ к разделу search, нужно реализовать шаблонный интерфейс ResourceProcessor
При десериализации ответа контроллера со списком сущностей вы увидите, что он составлен достаточно неудобно: список является
{ "_embedded": { "topics": [...] } } |
Следовательно, нужно приготовить классы, которые позволят из такой сериализации достать список сущностей.
@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(NON_NULL) public class TopicSearchResultHalRepresentation { @JsonProperty(value = "_embedded") private volatile TopicSearchResultEmbedded embedded; public TopicSearchResultEmbedded getEmbedded() { return embedded; } public void setEmbedded(TopicSearchResultEmbedded embedded) { this.embedded = embedded; } } public class TopicSearchResultEmbedded { private List<Topic> topics; public TopicSearchResultEmbedded() { topics = null; } public List<Topic> getTopics() { return topics; } public void setTopics(List<Topic> topics) { this.topics = topics; } } |
Теперь на стороне фронт-энда можно получить список так:
List<Topic> result = new ArrayList<>(); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<TopicSearchCriteria> request = new HttpEntity<>(tsc, httpHeaders); Traverson traverson = null; try { traverson = new Traverson(new URI(<your back end's URL>), MediaTypes.HAL_JSON); } catch (URISyntaxException e) { LOG.error(ExceptionUtils.getStackTrace(e)); } Link link = traverson.follow(TOPICS_REL).asLink(); ResponseEntity<TopicSearchResultHalRepresentation> topicResponse = restOperations.exchange(link.getHref() + "/" + "search" + "/" + "searchWithCriteria", HttpMethod.POST, request, TopicSearchResultHalRepresentation.class); if (topicResponse.getBody().getEmbedded() != null) { result = topicResponse .getBody() .getEmbedded() .getTopics(); } return result; |