
Рассмотрим случай передачи из 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);
}
...
} |
@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;
} |
@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;
}
} |
@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
, а наш TopicController уже реализует этот же интерфейс с другим шаблонным типом. В методе TopicSearchController мы проверяем тип сущности и для нужной добавляем поисковую ссылку.
При десериализации ответа контроллера со списком сущностей вы увидите, что он составлен достаточно неудобно: список является
{
"_embedded":
{
"topics":
[...]
}
} |
{
"_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;
}
} |
@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; |
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;