如何简单地添加指向 Spring Data REST 实体的链接
Posted
技术标签:
【中文标题】如何简单地添加指向 Spring Data REST 实体的链接【英文标题】:How can I simply add a link to a Spring Data REST Entity 【发布时间】:2016-03-21 10:48:36 【问题描述】:我的实体使用 Spring Data JPA,但为了生成关于它们的统计信息,我在 Spring @Repository
中使用 jOOQ。
由于我的方法返回实体的List
或Double
,我如何将它们公开为链接?假设我有一个User
实体,我想获得以下 JSON:
"_embedded" :
"users" : [ ]
,
"_links" :
"self" :
"href" : "http://localhost:8080/api/users"
,
"stats" :
"href" : "http://localhost:8080/api/users/stats"
"profile" :
"href" : "http://localhost:8080/api/profile/users"
,
"page" :
"size" : 20,
"totalElements" : 0,
"totalPages" : 0,
"number" : 0
在http://localhost:8080/api/users/stats 中,我想获取我在 jOOQ 存储库中声明的方法的链接列表。我将如何处理这个问题?谢谢。
【问题讨论】:
【参考方案1】:从docs看到这个
@Bean
public ResourceProcessor<Resource<Person>> personProcessor()
return new ResourceProcessor<Resource<Person>>()
@Override
public Resource<Person> process(Resource<Person> resource)
resource.add(new Link("http://localhost:8080/people", "added-link"));
return resource;
;
【讨论】:
spring-boot 2.1 中的测试。如果Resource<ManagedEntity>
和PagedResources
都出现在响应中,则会引发异常。请参阅下面的解决方案。【参考方案2】:
添加链接的最佳方式是考虑使用 Spring-HATEOAS,它可以让代码看起来更干净。
忠告:始终使用 org.springframework.http.ResponseEntity 向客户端返回响应,因为它可以轻松自定义响应。
因此,由于您的要求是在响应中发送链接,因此建议的最佳实践是使用 ResourceSupport(org.springframework.hateoas.ResourceSupport) 和 ResourceAssemblerSupport( org.springframework.hateoas.mvc.ResourceAssemblerSupport) 创建需要发送给客户端的资源。
例如: 如果您有一个像 Account 这样的模型对象,那么必须有一些字段是您不希望客户知道或包含在响应中的,因此要从响应中排除这些属性,我们可以使用 ResourceAssemblerSupport 类'
public TResource toResource(T t);
从需要作为响应发送的模型对象生成资源的方法。
例如,我们有一个 Account 类(可以直接用于所有服务器端交互和操作)
@Document(collection = "Accounts_Details")
public class Account
@Id
private String id;
private String username;
private String password;
private String firstName;
private String lastName;
private String emailAddress;
private String role;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private long accountNonLockedCounter;
private Date lastPasswordResetDate;
private Address address;
private long activationCode;
public Account()
//getters and setters
现在从这个 POJO 我们将创建一个资源对象,该对象将被发送到带有选定属性的客户端。
为此,我们将创建一个帐户资源,其中仅包含客户可以查看的必要字段。然后我们创建另一个类。
@XmlRootElement
public class AccountResource extends ResourceSupport
@XmlAttribute
private String username;
@XmlAttribute
private String firstName;
@XmlAttribute
private String lastName;
@XmlAttribute
private String emailAddress;
@XmlAttribute
private Address address;
public String getUsername()
return username;
public void setUsername(String username)
this.username = username;
public String getFirstName()
return firstName;
public void setFirstName(String firstName)
this.firstName = firstName;
public String getLastName()
return lastName;
public void setLastName(String lastName)
this.lastName = lastName;
public String getEmailAddress()
return emailAddress;
public void setEmailAddress(String emailAddress)
this.emailAddress = emailAddress;
public Address getAddress()
return address;
public void setAddress(Address address)
this.address = address;
所以现在这个资源是客户将看到或必须使用的。
创建 AccountResource 的蓝图后,我们需要一种方法将模型 POJO 转换为该资源,为此建议的最佳做法是创建 ResourceAssemblerSupport 类并覆盖 toResource(T t) 方法。
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.stereotype.Component;
import com.brx.gld.www.api.controller.RegistrationController;
import com.brx.gld.www.api.model.Account;
@Component
public class AccountResourceAssembler extends ResourceAssemblerSupport<Account, AccountResource>
public AccountResourceAssembler(Class<RegistrationController> controllerClass,
Class<AccountResource> resourceType)
super(controllerClass, resourceType);
public AccountResourceAssembler()
this(RegistrationController.class, AccountResource.class);
@Override
public AccountResource toResource(Account account)
AccountResource accountResource = instantiateResource(account); //or createResourceWithId(id, entity) canbe used which will automatically create a link to itself.
accountResource.setAddress(account.getAddress());
accountResource.setFirstName(account.getFirstName());
accountResource.setLastName(account.getLastName());
accountResource.setEmailAddress(account.getEmailAddress());
accountResource.setUsername(account.getUsername());
accountResource.removeLinks();
accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());
return accountResource;
在 toReource 方法中,我们必须使用 createdResourceWithId(id, entity) 而不是使用 instanriateReource(..),然后将 custum 链接添加到资源,这实际上也是一个值得考虑的最佳实践,但为了演示我使用了 instantiateResource(..)
现在在 Controller 中使用它:
@Controller
@RequestMapping("/api/public/accounts")
public class RegistrationController
@Autowired
private AccountService accountService;
@Autowired
private AccountResourceAssembler accountResourceAssembler;
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity<List<AccountResource>> getAllRegisteredUsers()
List<AccountResource> accountResList = new ArrayList<AccountResource>();
for (Account acnt : accountService.findAllAccounts())
accountResList.add(this.accountResourceAssembler.toResource(acnt));
return new ResponseEntity<List<AccountResource>>(accountResList, HttpStatus.OK);
/*Use the below method only if you have enabled spring data web Support or otherwise instead of using Account in @PathVariable usr String id or int id depending on what type to id you have in you db*/
@RequestMapping(value = "userID", method = RequestMethod.GET)
public ResponseEntity<AccountResource> getAccountForID(@PathVariable("userID") Account fetchedAccountForId)
return new ResponseEntity<AccountResource>(
this.accountResourceAssembler.toResource(fetchedAccountForId), HttpStatus.OK);
启用 Spring Data Web 支持,为代码添加更多功能,例如根据传递的 id 从 DB 自动获取模型数据,就像我们在之前的方法中使用的那样。
现在返回 toResource(Account account) 方法:首先初始化资源对象,然后设置所需的道具,然后使用静态 org.springframework.hateoas 将链接添加到 AccountResorce .mvc.ControllerLinkBuilder.linkTo(..) 方法,然后传入控制器类,从中选择基本 url,然后使用 slash(..) 等构建 url。指定完整路径后,我们使用 rel 方法来指定关系(就像这里我们使用 withSelfRel() 来指定关系是它自己。对于其他关系,我们可以使用 withRel(String relationship) 更具描述性。 所以在我们的 toResource 方法的代码中,我们使用了类似的东西 accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());
这会将 URL 构建为 /api/public/accounts/userID
现在在邮递员中,如果我们在这个 url 上使用 get http://localhost:8080/api/public/accounts
"username": "Arif4",
"firstName": "xyz",
"lastName": "Arif",
"emailAddress": "xyz@outlook.com",
"address":
"addressLine1": "xyz",
"addressLine2": "xyz",
"addressLine3": "xyz",
"city": "xyz",
"state": "xyz",
"zipcode": "xyz",
"country": "India"
,
"links": [
"rel": "self",
"href": "http://localhost:8080/api/public/accounts/5628b95306bf022f33f0c4f7"
]
,
"username": "Arif5",
"firstName": "xyz",
"lastName": "Arif",
"emailAddress": "xyz@gmail.com",
"address":
"addressLine1": "xyz",
"addressLine2": "xyz",
"addressLine3": "xyz",
"city": "xyz",
"state": "xyz",
"zipcode": "xyz",
"country": "India"
,
"links": [
"rel": "self",
"href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
]
单击任何链接并发送获取请求,响应将是 http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0
"username": "Arif5",
"firstName": "xyz",
"lastName": "Arif",
"emailAddress": "xyz@gmail.com",
"address":
"addressLine1": "xyz",
"addressLine2": "xyz",
"addressLine3": "xyz",
"city": "xyz",
"state": "xyz",
"zipcode": "xyz",
"country": "India"
,
"links": [
"rel": "self",
"href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
]
【讨论】:
这是不可接受的。太多样板。一句忠告:org.springframework.http.ResponseEntity 生成样板代码。你最终会在你的控制器中随处可见这些 ResponseEntity ,从而使其难以阅读和理解。如果要处理错误(未找到、冲突等)等特殊情况,可以将 HandlerExceptionResolver 添加到 Spring 配置中。因此,在您的代码中,您只需抛出一个特定的异常(例如 NotFoundException)并决定在您的 Handler 中做什么(将 HTTP 状态设置为 404),从而使 Controller 代码更加清晰。 “太多样板代码”lel 它是 Java .. 添加 2000 行 XML,其中一半是自动生成的,现在您可以使用 Java 的大量样板代码。致 OP:我喜欢这种方法,我希望更多人意识到资源(表示)应该与域模型分开,然后您可以轻松推出新版本的“帐户”【参考方案3】:有关手动创建链接,请参阅spring-hateoas-examples。
如果没有 DTO,最简单的 wai 是 via new Resource
,对于 DTO,最简单的 wai 是 extends ResourceSupport
。
我自定义的 spring-data-rest 托管实体的链接类似于links to root resource:
MyController implements ResourceProcessor<Resource<ManagedEntity>>
@Override
public Resource<Restaurant> process(Resource<ManagedEntity> resource)
resource.add(linkTo(methodOn(MyController.class)
.myMethod(resource.getContent().getId(), ...)).withRel("..."));
return resource;
对于分页资源
MyController implements ResourceProcessor<PagedResources<Resource<ManagedEntity>>>
问题是当你需要两者时,由于泛型类型擦除,你不能同时扩展这个接口。作为一个黑客,我创建了虚拟ResourceController
【讨论】:
【参考方案4】:https://docs.spring.io/spring-hateoas/docs/current/reference/html/#reference
public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>>
@Override
public EntityModel<Order> process(EntityModel<Order> model)
model.add(
Link.of("/payments/orderId").withRel(LinkRelation.of("payments")) //
.expand(model.getContent().getOrderId()));
return model;
migrate-to-1.0.changes
ResourceSupport 现在是 RepresentationModel
资源现在是 EntityModel
Resources 现在是 CollectionModel
PagedResources 现在是 PagedModel
【讨论】:
以上是关于如何简单地添加指向 Spring Data REST 实体的链接的主要内容,如果未能解决你的问题,请参考以下文章
如何正确地将资源 (*.res) 文件添加到带有组件的包中?
Spring Data JPA NamedStoredProcedureQuery 多个输出参数