我正在使用 Spring Boot 2、Spring Data REST、Spring HATEOAS。
假设我有一个模型:
@EntityListeners({ContactListener.class})
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Contact extends AbstractEntity {
@NotNull
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'CUSTOMER'")
private ContactType type = ContactType.CUSTOMER;
@NotNull
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "VARCHAR(30) DEFAULT 'NATURAL_PERSON'")
private PersonType personType = PersonType.NATURAL_PERSON;
private String firstName;
private String lastName;
private String companyName;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id", updatable = false)
private Store store;
和商店:
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Store extends AbstractEntity {
@NotBlank
@Column(nullable = false)
private String name;
@Username
@NotBlank
@Length(max = 16)
@Column(nullable = false/*, unique = true*/)
@ColumnTransformer(write = "UPPER(?)")
private String code;
private String address;
private String zipCode;
private String city;
private String district;
当我收到联系信息时,回复如下所示:
{
"sid": "962732c2-68a8-413b-9762-f676d42046b4",
"createdBy": "1ccf2329-4aa3-4d55-8878-25517edf1522",
"createdDate": "2019-05-28T14:06:07.011Z",
"lastModifiedDate": "2019-06-04T08:46:02.591Z",
"lastModifiedBy": "system",
"createdByName": "Rossi Mario",
"lastModifiedByName": null,
"type": "CUSTOMER",
"personType": "NATURAL_PERSON",
"firstName": "Mario",
"lastName": "Rossi",
"companyName": null,
"fullName": "Rossi Mario",
"gender": "MALE",
"birthDate": "2019-05-21T00:00:00Z",
"birthCity": null,
"job": null,
"billingAddress": "Via 123",
"billingZipCode": "14018",
"billingCity": "Roatto",
"billingDistrict": "AT",
"billingCountry": "IT",
"shippingAddress": "Via 123",
"shippingZipCode": "14018",
"shippingCity": "Roatto",
"shippingDistrict": "AT",
"shippingCountry": "IT",
"taxCode": "XXXX",
"vatNumber": null,
"landlinePhone": null,
"mobilePhone": null,
"fax": null,
"email": "[email protected]",
"certifiedEmail": null,
"survey": null,
"iban": null,
"swift": null,
"publicAdministration": false,
"sdiAccountId": "0000000",
"preset": false,
"_links": {
"self": {
"href": "http://localhost:8082/api/v1/contacts/1"
},
"contact": {
"href": "http://localhost:8082/api/v1/contacts/1{?projection}",
"templated": true
},
"store": {
"href": "http://localhost:8082/api/v1/contacts/1/store{?projection}",
"templated": true
}
}
}
正如您所看到的商店链接,它不是资源商店的self链接。 我想覆盖该链接设置自身资源。所以我创建了这个处理器:
@Component
public class DocumentRowProcessor implements ResourceProcessor<Resource<Contact>> {
@Autowired
private BasePathAwareLinks service;
@Autowired
private EntityLinks entityLinks;
@Override
public Resource<Contact> process(Resource<Contact> resource) {
Store store = resource.getContent().getStore();
if(store != null){
resource.add(entityLinks.linkToSingleResource(store.getClass(), store.getId()).withRel("store"));
}
return resource;
}
}
不幸的是,该链接现已被覆盖,但我在“商店”内找到了 2 个链接。调试我看到资源内部仅存在自我链接。我的猜测是在以下步骤中添加了相关链接。
如何干净利落地完成我的目标?
http://localhost:8082/api/v1/contacts/1/store
是端点,您可以在其中检查哪个商店链接到该内容,或者您可以删除/修改这两个对象之间的关联。
当您获得联系人的投影时,结果将包含商店
内部商店属性的自链接。因此,主要的 _links
集合仍然是常规的 hatos 链接集合,但会有一个
store._links.self.href
属性,其中包含关联商店的自链接。
LinkCollector
。以下内容有明显的缺陷,我无法测试 Java 版本(我的 Kotlin 版本工作正常),但它使用规范 URL 生成链接:
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.data.rest.core.mapping.ResourceMapping;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.core.support.SelfLinkProvider;
import org.springframework.data.rest.webmvc.mapping.Associations;
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
// https://github.com/spring-projects/spring-data-rest/issues/2042#issuecomment-1913914052
public class AbsoluteLinkCollector implements LinkCollector {
private final PersistentEntities entities;
private final Associations associationLinks;
private final SelfLinkProvider links;
public AbsoluteLinkCollector(LinkCollector oldCollector) throws NoSuchFieldException, IllegalAccessException {
Field entitiesField = oldCollector.getClass().getDeclaredField("entities");
entitiesField.setAccessible(true);
this.entities = (PersistentEntities) entitiesField.get(oldCollector);
Field associationLinksField = oldCollector.getClass().getDeclaredField("associationLinks");
associationLinksField.setAccessible(true);
this.associationLinks = (Associations) associationLinksField.get(oldCollector);
Field linksField = oldCollector.getClass().getDeclaredField("links");
linksField.setAccessible(true);
this.links = (SelfLinkProvider) linksField.get(oldCollector);
}
@Override
public Links getLinksFor(Object obj) {
return this.getLinksFor(obj, Links.NONE);
}
@Override
public Links getLinksFor(Object obj, Links existing) {
Map<String, Link> linkMap = new HashMap<>();
for (Link l : existing) {
linkMap.put(l.getRel().value(), l);
}
linkMap.put(IanaLinkRelations.SELF.value(), links.createSelfLinkFor(obj).withSelfRel());
PersistentEntity<?, ? extends PersistentProperty<?>> entity = entities.getRequiredPersistentEntity(obj.getClass());
entity.doWithAssociations((Association<?> assoc) -> {
try {
if (!associationLinks.isLinkableAssociation(assoc)) {
return;
}
ResourceMetadata ownerMetadata = associationLinks.getMetadataFor(assoc.getInverse().getOwner().getType());
ResourceMapping propertyMapping = ownerMetadata.getMappingFor(assoc.getInverse());
LinkRelation rel = propertyMapping.getRel();
// if it is a collection, we can't get the value
if (assoc.getInverse().isCollectionLike()) {
return;
}
Field fieldValueField = obj.getClass().getDeclaredField(assoc.getInverse().getName());
fieldValueField.setAccessible(true);
Object fieldValue = fieldValueField.get(obj);
if (fieldValue == null) return;
linkMap.put(rel.value(), links.createSelfLinkFor(fieldValue).withRel(rel));
} catch (Exception e) {
// without this, we get a useless and confusing "Failed to write request"
// error response.
System.out.println("Error getting links for " + obj);
e.printStackTrace();
throw new RuntimeException(e);
}
});
return Links.of(linkMap.values());
}
@Override
public Links getLinksForNested(Object obj, Links existing) {
return this.getLinksFor(obj, existing);
}
}
它可以通过覆盖 customizeLinkCollector
中的
RepositoryRestConfigurer
来启用:
@Configuration
public class RestConfiguration extends RepositoryRestConfigurerAdapter {
@Override
public LinkCollector customizeLinkCollector(LinkCollector collector) {
return new AbsoluteLinkCollector(collector);
}
}