Разрешение URI пользовательского контроллера (Spring HATEOAS)
У меня есть проект на основе spring -data-rest, а также он имеет некоторые пользовательские конечные точки.
Для отправки данных POST я использую json как
{
"action": "REMOVE",
"customer": "http://localhost:8080/api/rest/customers/7"
}
Это нормально для spring -data-rest, но не работает с настраиваемым контроллером.
например:
public class Action {
public ActionType action;
public Customer customer;
}
@RestController
public class ActionController(){
@Autowired
private ActionService actionService;
@RestController
public class ActionController {
@Autowired
private ActionService actionService;
@RequestMapping(value = "/customer/action", method = RequestMethod.POST)
public ResponseEntity<ActionResult> doAction(@RequestBody Action action){
ActionType actionType = action.action;
Customer customer = action.customer;//<------There is a problem
ActionResult result = actionService.doCustomerAction(actionType, customer);
return ResponseEntity.ok(result);
}
}
Когда я звоню
curl -v -X POST -H "Content-Type: application/json" -d '{"action": "REMOVE","customer": "http://localhost:8080/api/rest/customers/7"}' http://localhost:8080/customer/action
У меня есть ответ
{
"timestamp" : "2016-05-12T11:55:41.237+0000",
"status" : 400,
"error" : "Bad Request",
"exception" : "org.springframework.http.converter.HttpMessageNotReadableException",
"message" : "Could not read document: Can not instantiate value of type [simple type, class model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: [email protected]; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class logic.model.user.Customer] from String value ('http://localhost:8080/api/rest/customers/7'); no single-String constructor/factory method\n at [Source: [email protected]; line: 1, column: 33] (through reference chain: api.controller.Action[\"customer\"])",
"path" : "/customer/action"
* Closing connection 0
}
bacause case spring не может преобразовать URI в объект Customer.
Можно ли использовать механизм spring -data-rest для разрешения объектов по их URI?
У меня есть только одна идея - использовать пользовательский JsonDeserializer с разбором URI для извлечения entityId и запроса в репозиторий. Но эта стратегия не помогает мне, если у меня URI, например " http://localhost:8080/api/rest/customers/8/product", в этом случае у меня нет product.Id.
Ответы
Ответ 1
У меня была такая же проблема слишком долгое время и она была решена следующим образом. @Florian был на правильном пути, и благодаря его предложению я нашел способ сделать преобразование автоматически. Требуется несколько штук:
- Служба преобразования, чтобы включить преобразование из URI в объект (используя UriToEntityConverter, снабженный каркасом).
- Deserializer, чтобы определить, когда это необходимо для вызова преобразователя (мы не хотим испортить поведение SDR по умолчанию)
- Пользовательский модуль Jackson для ввода всех значений в SDR
Для точки 1 реализация может быть сужена до следующего
import org.springframework.context.ApplicationContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.format.support.DefaultFormattingConversionService;
public class UriToEntityConversionService extends DefaultFormattingConversionService {
private UriToEntityConverter converter;
public UriToEntityConversionService(ApplicationContext applicationContext, PersistentEntities entities) {
new DomainClassConverter<>(this).setApplicationContext(applicationContext);
converter = new UriToEntityConverter(entities, this);
addConverter(converter);
}
public UriToEntityConverter getConverter() {
return converter;
}
}
Для точки 2 это мое решение
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator;
import your.domain.RootEntity; // <-- replace this with the import of the root class (or marker interface) of your domain
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.util.Assert;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
public class RootEntityFromUriDeserializer extends BeanDeserializerModifier {
private final UriToEntityConverter converter;
private final PersistentEntities repositories;
public RootEntityFromUriDeserializer(PersistentEntities repositories, UriToEntityConverter converter) {
Assert.notNull(repositories, "Repositories must not be null!");
Assert.notNull(converter, "UriToEntityConverter must not be null!");
this.repositories = repositories;
this.converter = converter;
}
@Override
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, BeanDeserializerBuilder builder) {
PersistentEntity<?, ?> entity = repositories.getPersistentEntity(beanDesc.getBeanClass());
boolean deserializingARootEntity = entity != null && RootEntity.class.isAssignableFrom(entity.getType());
if (deserializingARootEntity) {
replaceValueInstantiator(builder, entity);
}
return builder;
}
private void replaceValueInstantiator(BeanDeserializerBuilder builder, PersistentEntity<?, ?> entity) {
ValueInstantiator currentValueInstantiator = builder.getValueInstantiator();
if (currentValueInstantiator instanceof StdValueInstantiator) {
EntityFromUriInstantiator entityFromUriInstantiator =
new EntityFromUriInstantiator((StdValueInstantiator) currentValueInstantiator, entity.getType(), converter);
builder.setValueInstantiator(entityFromUriInstantiator);
}
}
private class EntityFromUriInstantiator extends StdValueInstantiator {
private final Class entityType;
private final UriToEntityConverter converter;
private EntityFromUriInstantiator(StdValueInstantiator src, Class entityType, UriToEntityConverter converter) {
super(src);
this.entityType = entityType;
this.converter = converter;
}
@Override
public Object createFromString(DeserializationContext ctxt, String value) throws IOException {
URI uri;
try {
uri = new URI(value);
} catch (URISyntaxException e) {
return super.createFromString(ctxt, value);
}
return converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(entityType));
}
}
}
Затем для точки 3 в пользовательском RepositoryRestConfigurerAdapter
public class MyRepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.registerModule(new SimpleModule("URIDeserializationModule"){
@Override
public void setupModule(SetupContext context) {
UriToEntityConverter converter = conversionService.getConverter();
RootEntityFromUriDeserializer rootEntityFromUriDeserializer = new RootEntityFromUriDeserializer(persistentEntities, converter);
context.addBeanDeserializerModifier(rootEntityFromUriDeserializer);
}
});
}
}
Это работает плавно для меня и не мешает никакому преобразованию из фреймворка (у нас много настраиваемых конечных точек). В пункте 2 целью было включить создание экземпляра из URI только в тех случаях, когда:
- Объект, десериализованный, является корневым объектом (поэтому никаких свойств)
- Предоставленная строка - это фактический URI (в противном случае она просто возвращается к поведению по умолчанию)
Ответ 2
Это скорее побочная заметка, чем реальный ответ, но некоторое время назад мне удалось скопировать и вставить себе класс для разрешения объектов из URL-адреса, используя методы, используемые в SDR (только более грубые). Вероятно, есть намного лучший способ, но до тех пор, возможно, это помогает...
@Service
public class EntityConverter {
@Autowired
private MappingContext<?, ?> mappingContext;
@Autowired
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryRestConfigurer> configurers = Collections.emptyList();
public <T> T convert(Link link, Class<T> target) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
PersistentEntities entities = new PersistentEntities(Arrays.asList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, conversionService);
conversionService.addConverter(converter);
addFormatters(conversionService);
for (RepositoryRestConfigurer configurer : configurers) {
configurer.configureConversionService(conversionService);
}
URI uri = convert(link);
T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target)));
if (object == null) {
throw new IllegalArgumentException(String.format("%s '%s' was not found.", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref());
} catch (Exception e) {
throw new IllegalArgumentException("URI from link is invalid", e);
}
}
private void addFormatters(FormatterRegistry registry) {
registry.addFormatter(DistanceFormatter.INSTANCE);
registry.addFormatter(PointFormatter.INSTANCE);
if (!(registry instanceof FormattingConversionService)) {
return;
}
FormattingConversionService conversionService = (FormattingConversionService) registry;
DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
conversionService);
converter.setApplicationContext(applicationContext);
}
}
И да, скорее всего, части этого класса просто бесполезны. В моей защите это был всего лишь короткий хак, и я никогда не обошел его, потому что сначала я нашел другие проблемы: -)
Ответ 3
Для HAL с @RequestBody
используйте Resource<T>
в качестве параметра метода вместо entity Action
, чтобы разрешить конвертировать связанные ресурсы URI
public ResponseEntity<ActionResult> doAction(@RequestBody Resource<Action> action){
Ответ 4
Я не могу в это поверить. Обдумав это в течение МЕСЯЦА (!), Мне удалось РЕШИТЬ ЭТО !
Несколько слов введения:
Spring HATEOAS использует URI в качестве ссылок на сущности. И это обеспечивает большую поддержку для получения этих ссылок URI для данного объекта. Например, когда клиент запрашивает объект, который ссылается на другие дочерние объекты, клиент получит эти URI. Приятно работать.
GET /users/1
{
"username": "foobar",
"_links": {
"self": {
"href": "http://localhost:8080/user/1" //<<<== HATEOAS Link
}
}
}
Клиент REST работает только с этими Uris. Клиент REST НЕ ДОЛЖЕН знать структуру этих URI. Клиент REST не знает, что в конце строки URI находится внутренний идентификатор БД.
Все идет нормально. НО пружинные данные HATEOAS не предлагает никаких функций для преобразования URI обратно в соответствующий объект (загруженный из БД). Это нужно каждому в пользовательских контроллерах REST. (См. Вопрос выше)
Вспомните пример, где вы хотите работать с пользователем в пользовательском контроллере REST. Клиент отправит этот запрос
POST /checkAdress
{
user: "/users/1"
someMoreOtherParams: "...",
[...]
}
Как пользовательский контроллер REST должен десериализоваться из (String) URI в UserModel? Я нашел способ: Вы должны настроить десериализацию Джексона в вашем RepositoryRestConfigurer:
RepositoryRestConfigurer.java
public class RepositoryRestConfigurer extends RepositoryRestConfigurerAdapter {
@Autowired
UserRepo userRepo;
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.addDeserializer(UserModel.class, new JsonDeserializer<UserModel>() {
@Override
public UserModel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String uri = p.getValueAsString();
//extract ID from URI, with regular expression (1)
Pattern regex = Pattern.compile(".*\\/" + entityName + "\\/(\\d+)");
Matcher matcher = regex.matcher(uri);
if (!matcher.matches()) throw new RuntimeException("This does not seem to be an URI for an '"+entityName+"': "+uri);
String userId = matcher.group(1);
UserModel user = userRepo.findById(userId)
.orElseThrow(() -> new RuntimeException("User with id "+userId+" does not exist."))
return user;
}
});
objectMapper.registerModule(module);
}
}
(1) Разбор этой строки ужасен. Я знаю. Но это просто обратная сторона org.springframework.hateoas.EntityLinks и его реализаций. И автор пружинных hateos упорно отказывается предлагать служебные методы для обоих направлений.
Ответ 5
Я пришел к следующему решению. Это немного хакерский, но работает.
Во-первых, сервис для преобразования URI в сущности.
EntityConverter
import java.net.URI;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.geo.format.DistanceFormatter;
import org.springframework.data.geo.format.PointFormatter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.DefaultRepositoryInvokerFactory;
import org.springframework.data.repository.support.DomainClassConverter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
@Service
public class EntityConverter {
@Autowired
private MappingContext<?, ?> mappingContext;
@Autowired
private ApplicationContext applicationContext;
@Autowired(required = false)
private List<RepositoryRestConfigurer> configurers = Collections.emptyList();
public <T> T convert(Link link, Class<T> target) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
Repositories repositories = new Repositories(applicationContext);
UriToEntityConverter converter = new UriToEntityConverter(
new PersistentEntities(Collections.singleton(mappingContext)),
new DefaultRepositoryInvokerFactory(repositories),
repositories);
conversionService.addConverter(converter);
addFormatters(conversionService);
for (RepositoryRestConfigurer configurer : configurers) {
configurer.configureConversionService(conversionService);
}
URI uri = convert(link);
T object = target.cast(conversionService.convert(uri, TypeDescriptor.valueOf(target)));
if (object == null) {
throw new IllegalArgumentException(String.format("registerNotFound", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref().replace("{?projection}", ""));
} catch (Exception e) {
throw new IllegalArgumentException("invalidURI", e);
}
}
private void addFormatters(FormatterRegistry registry) {
registry.addFormatter(DistanceFormatter.INSTANCE);
registry.addFormatter(PointFormatter.INSTANCE);
if (!(registry instanceof FormattingConversionService)) {
return;
}
FormattingConversionService conversionService = (FormattingConversionService) registry;
DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<FormattingConversionService>(
conversionService);
converter.setApplicationContext(applicationContext);
}
}
Во-вторых, компонент, позволяющий использовать EntityConverter
вне контекста Spring.
ApplicationContextHolder
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
}
В-третьих, конструктор сущностей, который принимает другую сущность в качестве входных данных.
MyEntity
public MyEntity(MyEntity entity) {
property1 = entity.property1;
property2 = entity.property2;
property3 = entity.property3;
// ...
}
В-четвертых, конструктор сущности, который принимает в качестве входных данных String
, который должен быть URI.
MyEntity
public MyEntity(String URI) {
this(ApplicationContextHolder.getContext().getBean(EntityConverter.class).convert(new Link(URI.replace("{?projection}", "")), MyEntity.class));
}
По желанию, я переместил часть кода выше в класс Utils
.
Я пришел к этому решению, посмотрев сообщение об ошибке в вопросе, которое я тоже получал. Spring не знает, как построить объект из String
? Я покажу как...
Однако, как сказано в комментарии, это не работает для URI вложенных объектов.
Ответ 6
Мое решение будет несколько компактным. Не уверен, что это может быть полезно для всех случаев, но для простых отношений, таких как .../entity/{id}
он может быть проанализирован. Я протестировал его на SDR и Spring Boot 2.0.3.RELEASE
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.core.UriToEntityConverter;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.Collections;
@Service
public class UriToEntityConversionService {
@Autowired
private MappingContext<?, ?> mappingContext; // OOTB
@Autowired
private RepositoryInvokerFactory invokerFactory; // OOTB
@Autowired
private Repositories repositories; // OOTB
public <T> T convert(Link link, Class<T> target) {
PersistentEntities entities = new PersistentEntities(Collections.singletonList(mappingContext));
UriToEntityConverter converter = new UriToEntityConverter(entities, invokerFactory, repositories);
URI uri = convert(link);
Object o = converter.convert(uri, TypeDescriptor.valueOf(URI.class), TypeDescriptor.valueOf(target));
T object = target.cast(o);
if (object == null) {
throw new IllegalArgumentException(String.format("%s '%s' was not found.", target.getSimpleName(), uri));
}
return object;
}
private URI convert(Link link) {
try {
return new URI(link.getHref());
} catch (Exception e) {
throw new IllegalArgumentException("URI from link is invalid", e);
}
}
}
Использование:
@Component
public class CategoryConverter implements Converter<CategoryForm, Category> {
private UriToEntityConversionService conversionService;
@Autowired
public CategoryConverter(UriToEntityConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Category convert(CategoryForm source) {
Category category = new Category();
category.setId(source.getId());
category.setName(source.getName());
category.setOptions(source.getOptions());
if (source.getParent() != null) {
Category parent = conversionService.convert(new Link(source.getParent()), Category.class);
category.setParent(parent);
}
return category;
}
}
Запрос JSON, как:
{
...
"parent": "http://localhost:8080/categories/{id}",
...
}
Ответ 7
К сожалению, UriToEntityConverter (универсальный конвертер, который может преобразовать URI в сущность), который использует Spring Data REST, не экспортируется в виде компонента или сервиса.
Таким образом, мы не можем @Autowired
его напрямую, но он зарегистрирован как конвертер в службе преобразования форматирования по умолчанию.
Таким образом, мы управляем @Autowired
службой преобразования форматирования по умолчанию и используем их для преобразования URI в сущность, например:
@RestController
@RequiredArgsConstructor
public class InstanceController {
private final DefaultFormattingConversionService formattingConversionService;
@RequestMapping(path = "/api/instances", method = {RequestMethod.POST})
public ResponseEntity<?> create(@RequestBody @Valid InstanceDTO instanceDTO) { // get something what you want from request
// ...
// extract URI from any string what you want to process
final URI uri = "..."; // http://localhost:8080/api/instances/1
// convert URI to Entity
final Instance instance = formattingConversionService.convert(uri, Instance.class); // Instance(id=1, ...)
// ...
}
}