HATEOAS RepresentationModelAssembler,POST with JSON body returns 500: IllegalArgumentException: 没有足够的变量值可用于扩展 'id'

HATEOAS RepresentationModelAssembler, POST with JSON body returns 500: IllegalArgumentException: Not enough variable values available to expand 'id'

我遵循了 Spring 关于使用 HATEOAS 构建 REST 的教程:https://spring.io/guides/tutorials/rest/ 并将其与 JPA 和 MySQL DB (Maven) 混合使用。当我 运行 应用程序时,我可以看到 MySQL workbench 中的前 2 个表很好(尽管有第 3 个表突然出现?)。如果我执行 GET /players,它工作正常。当我在 Postman 中执行 POST 请求 (http://localhost:8080/players) 时正文为 JSON: { "playerName":"Pedro" },我得到一个 500 状态并且我从 Spring

Not enough variable values available to expand 'id'] with root cause...

我想实现完整的CRUD操作。这里出现了很多疑问,因为 playerId 是自增的,参数 registrariondate 是一个 TIMESTAMP。这是次要的,因为我猜问题出在我的应用程序中使用 RepresentationModelAssembler,但不太确定如何处理响应和请愿。

这里是项目结构:

错误日志:

Hibernate: 
    insert 
    into
        player
        (player_name, registration_date) 
    values
        (?, ?)
2021-04-02 11:20:03.781 ERROR 2312 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: java.lang.IllegalArgumentException: Not enough variable values available to expand 'id'] with root cause

java.lang.IllegalArgumentException: Not enough variable values available to expand 'id'
    at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:370) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:263) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.HierarchicalUriComponents$FullPathComponent.expand(HierarchicalUriComponents.java:917) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:434) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:52) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.UriComponents.expand(UriComponents.java:172) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.util.DefaultUriBuilderFactory$DefaultUriBuilder.build(DefaultUriBuilderFactory.java:403) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.hateoas.UriTemplate.expand(UriTemplate.java:272) ~[spring-hateoas-1.2.5.jar:1.2.5]
    at org.springframework.hateoas.Link.expand(Link.java:361) ~[spring-hateoas-1.2.5.jar:1.2.5]
    at org.springframework.hateoas.Link.toUri(Link.java:434) ~[spring-hateoas-1.2.5.jar:1.2.5]
    at RESTApiJWTAuthMySQL.controllers.PlayerController.createNewPlayer(PlayerController.java:66) ~[classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:197) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:141) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1060) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:962) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:652) ~[tomcat-embed-core-9.0.44.jar:4.0.FR]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.5.jar:5.3.5]
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.44.jar:4.0.FR]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.5.jar:5.3.5]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.5.jar:5.3.5]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.5.jar:5.3.5]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.5.jar:5.3.5]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1707) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630) ~[na:na]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.44.jar:9.0.44]
    at java.base/java.lang.Thread.run(Thread.java:832) ~[na:na]

控制器class:

@RestController
//@RequestMapping("/")
public class PlayerController {
    
    @Autowired
    private final PlayerRepository repository;
    @Autowired
    private final PlayerModelAssembler assembler;

    PlayerController(PlayerRepository repository, PlayerModelAssembler assembler) {
        this.repository = repository;
        this.assembler = assembler;
    }
        
    @GetMapping("/players/")
    public CollectionModel<EntityModel<Player>> all() {

      List<EntityModel<Player>> players = repository.findAll().stream().map
              (assembler::toModel).collect(Collectors.toList());

      return CollectionModel.of(players, linkTo(methodOn(PlayerController.class).all()).withSelfRel());
    }
    
    @GetMapping("/players/{id}") 
    public EntityModel<Player> one(@PathVariable Long playerId) {

      Player player = repository.findById(playerId).orElseThrow(() -> new PlayerNotFoundException(playerId));

      return assembler.toModel(player);
    }
    
    @PostMapping(path="/players", consumes="application/json") 
    public ResponseEntity<?> createNewPlayer(@RequestBody Player newPlayer) {

      EntityModel<Player> entityModel = assembler.toModel(repository.save(newPlayer));

      return ResponseEntity.created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()).body(entityModel);
      
    }        
            
    @PutMapping("/players/{id}")
    public ResponseEntity<?> replacePlayer(@RequestBody Player newPlayer, @PathVariable Long playerId) {

        Player updatedPlayer = repository.findById(playerId) //
          .map(player -> {
            player.setPlayerName(newPlayer.getPlayerName());
            return repository.save(player);
          }) //
          .orElseGet(() -> {
              newPlayer.setPlayerId(playerId);
            return repository.save(newPlayer);
          });

      EntityModel<Player> entityModel = assembler.toModel(updatedPlayer);

      return ResponseEntity //
          .created(entityModel.getRequiredLink(IanaLinkRelations.SELF).toUri()) //
          .body(entityModel);
    }
    
    @DeleteMapping("/players/{id}")
    public ResponseEntity<?> deleteEmployee(@PathVariable Long playerId) {

          repository.deleteById(playerId);

          return ResponseEntity.noContent().build();
    }
}

PlayerModelAssembler class:

@Component
public class PlayerModelAssembler implements RepresentationModelAssembler<Player, EntityModel<Player>> {

  @Override
  public EntityModel<Player> toModel(Player player) {

      return EntityModel.of(player, //
            linkTo(methodOn(PlayerController.class).one(player.getPlayerId())).withSelfRel(),
            linkTo(methodOn(PlayerController.class).all()).withRel("players"));
  }
}

玩家实体class:

@Entity
@Table(name = "Player")
public class Player {

    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)    
    private Long playerId;
    
    @Column (name = "player_name")
    private String playerName;
    
    @CreationTimestamp
    @Temporal(TemporalType.TIMESTAMP)   
    @Column (name = "registration_date", updatable = false)
    private Date registrationDate;
    
    @OneToMany (mappedBy="player", cascade = CascadeType.ALL, orphanRemoval=true)
    private List<DiceRoll> diceRolls = new ArrayList<>();
    
    public Player() {
        
    }  
    
    public Player(Long playerId, String playerName, Date registrationDate) {
        this.playerId=playerId;
        this.playerName = playerName;
        this.registrationDate = registrationDate;
    }
    
    //getter&setters//  
    
    @Override
      public boolean equals(Object o) {

        if (this == o)
          return true;
        if (!(o instanceof Player))
          return false;
        Player player = (Player) o;
        return Objects.equals(this.playerId, player.playerId) && Objects.equals(this.playerName, player.playerName)
            && Objects.equals(this.registrationDate, player.registrationDate);
      }

      @Override
      public int hashCode() {
        return Objects.hash(this.playerId, this.playerName, this.registrationDate);
      }

      @Override
      public String toString() {
        return "Player{" + "id=" + this.playerId + ", name='" + this.playerName + '\'' + ", date of registration='" + this.registrationDate + '\'' + '}';
      }
}

POM 文件:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.4.10.Final</version><!--$NO-MVN-MAN-VER$-->
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-hateoas</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.hateoas</groupId>
            <artifactId>spring-hateoas</artifactId>
        </dependency>
               
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Schema.sql:

USE `dicegame`;

DROP TABLE IF EXISTS `Player`;

CREATE TABLE IF NOT EXISTS `Player` 
(
    `player_id` BIGINT PRIMARY KEY  AUTO_INCREMENT,
    `player_name` VARCHAR(45) NOT NULL,
    `registration_date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

DROP TABLE IF EXISTS `DiceRoll`;

CREATE TABLE IF NOT EXISTS `DiceRoll` 
(
    `diceRoll_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `d1` INT(55) NOT NULL,
    `d2` INT(55) NOT NULL,
    `result` VARCHAR(45) NOT NULL,
    `diceRoll_registration` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `player_id` BIGINT NOT NULL
);
  
ALTER TABLE DiceRoll
ADD constraint FK_PLAYER_ID  FOREIGN KEY (player_id) 
      REFERENCES Player (player_id);

如有任何帮助或指导,我们将不胜感激。我在教程中看到他们将 curl petiton 用作:

$ curl -v -X POST localhost:8080/players -H 'Content-Type:application/json' -d '{"playerName": "Pedro"}' 

它应该可以工作。非常感谢。

方法PlayerModelAssembler.toModel(Player player)使用PlayerController.one(@PathVariable Long playerId)生成自link。

如果注解@PathVariablename属性没有提供,Spring期望参数名与[=18=17=]包围的名字相同=].

在您的原始代码中,参数名称 playerIdid 不同。所以要修复它,

@GetMapping("/players/{id}") 
public EntityModel<Player> one(@PathVariable("id") Long playerId) {
}

@GetMapping("/players/{id}") 
public EntityModel<Player> one(@PathVariable Long id) {
}

@GetMapping("/players/{playerId}") 
public EntityModel<Player> one(@PathVariable Long playerId) {
}