混合 Spring MVC + Spring Data Rest 会导致奇怪的 MVC 响应

Posted

技术标签:

【中文标题】混合 Spring MVC + Spring Data Rest 会导致奇怪的 MVC 响应【英文标题】:Mixing Spring MVC + Spring Data Rest results in odd MVC responses 【发布时间】:2016-09-10 23:18:51 【问题描述】:

我有两个 JPA 实体,一个带有 SDR 导出的存储库,另一个带有 Spring MVC 控制器和一个未导出的存储库。

MVC 公开实体具有对 SDR 托管实体的引用。请参阅下面的代码参考。

UserController 检索User 时,问题就出现了。 SDR 托管实体不会序列化,似乎 Spring 可能会尝试在响应中使用 HATEOAS refs。

下面是 GET 与完全填充的 User 的样子:


  "username": "foo@gmail.com",
  "enabled": true,
  "roles": [
    
      "role": "ROLE_USER",
      "content": [],
      "links": [] // why the content and links?
    
    // no places?
  ]

如何使用嵌入式 SDR 托管实体从我的控制器中明确返回 User 实体?

Spring MVC 托管

实体

@Entity
@Table(name = "users")
public class User implements Serializable 

    // UID

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    private Long id;

    @Column(unique = true)
    @NotNull
    private String username;

    @Column(name = "password_hash")
    @JsonIgnore
    @NotNull
    private String passwordHash;

    @NotNull
    private Boolean enabled;

    // No Repository
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @NotEmpty
    private Set<UserRole> roles = new HashSet<>();

    // The SDR Managed Entity
    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_place", 
        joinColumns =  @JoinColumn(name = "users_id") , 
        inverseJoinColumns =  @JoinColumn(name = "place_id"))
    private Set<Place> places = new HashSet<>();

    // getters and setters

回购

@RepositoryRestResource(exported = false)
public interface UserRepository extends PagingAndSortingRepository<User, Long> 
    // Query Methods

控制器

@RestController
public class UserController 

    // backed by UserRepository
    private final UserService userService;

    @Autowired
    public UserController(UserService userService) 
        this.userService = userService;
    

    @RequestMapping(path = "/users/username", method = RequestMethod.GET)
    public User getUser(@PathVariable String username) 
        return userService.getByUsername(username);
    

    @RequestMapping(path = "/users", method = RequestMethod.POST)
    public User createUser(@Valid @RequestBody UserCreateView user) 
        return userService.create(user);
    

    // Other MVC Methods

SDR 托管

实体

@Entity
public class Place implements Serializable 

    // UID

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @NotBlank
    private String name;

    @Column(unique = true)
    private String handle;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "address_id")
    private Address address;

    @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "contact_info_id")
    private ContactInfo contactInfo;

    // getters and setters

回购

public interface PlaceRepository extends PagingAndSortingRepository<Place, Long> 
    // Query Methods

【问题讨论】:

我假设您确实有 PlaceRepository 的 @Repository 注释 - 只是没有在此处发布?您能否也添加异常的文本? @lenach87 - SDR 不需要 @Repository 注释,除非您需要进一步配置它。也没有Exception,只是缺少序列化。 您的 JPA 实现可能有问题?如果您使用的是 Hibernate,它可以只加载一个包。您可以创建一个自定义查询来强制它立即加载,或者在将其作为响应发送之前简单地调用属性的访问器(这将强制休眠加载属性包)。 也许检查一下,假设您使用杰克逊进行序列化:***.com/questions/19580856/… 您可以添加您的存储库配置吗?是否有可能有两个 entityManager 在处理这些存储库,而一个看不到另一个中的实体? 【参考方案1】:

简而言之:Spring Data REST 和 Spring HATEOAS 劫持了 ObjectMapper,并希望将资源之间的关系表示为链接,而不是嵌入资源。

将一个实体与另一个实体建立一对一关系:

@Entity
public class Person 
    private String firstName;
    private String lastName;
    @OneToOne private Address address;

SDR/HATEOAS 将地址作为链接返回:


    "firstName": "Joe",
    "lastName": "Smith",
    "_links": 
        "self":  "href": "http://localhost:8080/persons/123123123" ,
        "address":  "href": "http://localhost:8080/addresses/9127391273" 
    

默认格式可以根据您的类路径中的内容而改变。我相信在我的示例中这是 HAL,当您包含 SDR 和 HATEOAS 时,这是默认设置。 根据上述配置,它可能不同但相似

Address 由 SDR 管理时,Spring 将执行此操作。如果它根本不受 SDR 管理,它将在响应中包含整个地址对象。我怀疑仅此一项就可以解释您所看到的行为。

角色

您尚未包含有关 UserRole 的信息,但根据您的代码,它似乎未在 User 之外进行管理,因此未注册 Spring Data 存储库。如果是这种情况,这就是它被嵌入的原因——没有其他存储库可以“链接”到。

角色下的contentlinks 看起来像Spring 试图将其序列化为Page。通常content 将有一个资源数组,links 将有诸如“自我”之类的链接或指向其他资源的链接。我不确定是什么原因造成的。

地点

Place 拥有自己的 Spring Data 存储库,因此它将被视为托管实体并链接到而不是嵌入。我怀疑你正在寻找的是一个投影。结帐春天documentation on projections。它看起来像这样:

@Projection(name = "embedPlaces", types =  User.class )
interface EmbedPlaces 
    String getUsername();
    boolean isEnabled();
    Set<Place> getPlaces();

应该序列化用户名、启用和角色并忽略其他所有内容。我还没有亲自使用过预测,所以我不能保证它的效果如何,但这是文档中的解决方案。

编辑:虽然我们正在这样做,但请注意这也适用于创建或更新资源。 Spring 将期望资源作为 URL。因此,以人/地址为例,如果我正在创建一个新人,我的身体可能看起来像:


    "firstName": "New",
    "lastName": "Person",
    "address": "http://localhost:8080/addresses/1290312039123"

很容易忘记这些事情,因为大量的“REST”API 不是 REST,而且 SDR/HATEOAS 对 REST 持固执己见的观点(例如,它应该是 REST,例如)。

【讨论】:

【参考方案2】:

您可以很好地在控制器中使用@ResponseEntity,然后在 ResponseEntity 中设置用户对象。

请看下面的例子:

ResponseEntity<User> respEntity = new ResponseEntity<User>(user, HttpStatus.OK);

然后在客户端你可以打电话,restTemplate.getForEntity

请参阅下面的 restTemplate 文档:

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html#getForObject-java.lang.String-java.lang.Class-java.lang.Object...-

【讨论】:

我不太确定这对我有什么帮助。

以上是关于混合 Spring MVC + Spring Data Rest 会导致奇怪的 MVC 响应的主要内容,如果未能解决你的问题,请参考以下文章

混合前端和服务器端技术(Spring、Thymeleaf、AngularJS)

Spring MVC基础篇4

使用JSF作为Spring MVC的视图技术

Spring MVC学习笔记---Spring MVC 的HelloWorld

Spring MVC学习笔记---Spring MVC 的HelloWorld

spring mvc怎么存cookie