web-dev-qa-db-fra.com

Spring Data REST: remplacer la méthode de référentiel sur le contrôleur

J'ai le référentiel REST, dont l'implémentation est générée au moment de l'exécution par Spring.

@RepositoryRestResource
public interface FooRepository extends CrudRepository<Foo, Long> {

}

Cela signifie que j'aurai save (), find (), exist () et d'autres méthodes disponibles et exposées via REST.

Maintenant, je voudrais remplacer une des méthodes; par exemple, save (). Pour cela, je créerais un contrôleur exposant cette méthode, comme ceci:

@RepositoryRestController
@RequestMapping("/foo")
public class FooController {

    @Autowired
    FooService fooService;


    @RequestMapping(value = "/{fooId}", method = RequestMethod.PUT)
    public void updateFoo(@PathVariable Long fooId) {
        fooService.updateProperly(fooId);
    }

}

Le problème: Si j'active ce contrôleur, alors toutes les autres méthodes implémentées par Spring ne sont plus exposées. Ainsi, par exemple, je ne peux plus faire de requête GET à/foo/1

Question: Existe-t-il un moyen de remplacer les méthodes REST tout en conservant les autres méthodes Spring générées automatiquement?

Informations supplémentaires:

  1. Cette question semble très similaire: Spring Data Rest: Override Method dans RestController avec le même request-mapping-path ... mais je ne veux pas changer le chemin en quelque chose comme/foo/1/save

  2. J'ai pensé à utiliser un @RepositoryEventHandler mais je n'aime pas beaucoup cette idée car je voudrais l'encapsuler sous un service. En outre, vous semblez perdre le contrôle du contexte de transaction.

  3. Cette partie de la documentation de Spring Data dit ce qui suit:

    Parfois, vous souhaiterez peut-être écrire un gestionnaire personnalisé pour une ressource spécifique. Pour tirer parti des paramètres de Spring Data REST, des convertisseurs de messages, de la gestion des exceptions, etc., utilisez l'annotation @RepositoryRestController au lieu d'une Spring MVC @Controller ou @RestController standard

il semble donc que cela devrait fonctionner hors de la boîte, mais malheureusement pas.

16
Nicolas

Existe-t-il un moyen de remplacer les méthodes REST tout en conservant les autres méthodes Spring générées automatiquement?

Examinez attentivement l'exemple de la documentation: bien qu'il n'interdise pas explicitement le mappage de demandes au niveau de la classe, il utilise le mappage de demandes au niveau de la méthode. Je ne sais pas si c'est le comportement souhaité ou un bug, mais pour autant que je sache, c'est la seule façon de le faire fonctionner, comme indiqué ici .

Changez simplement votre contrôleur en:

@RepositoryRestController
public class FooController {

    @Autowired
    FooService fooService;

    @RequestMapping(value = "/foo/{fooId}", method = RequestMethod.PUT)
    public void updateFoo(@PathVariable Long fooId) {
        fooService.updateProperly(fooId);
    }

    // edited after Sergey's comment
    @RequestMapping(value = "/foo/{fooId}", method = RequestMethod.PUT)
    public RequestEntity<Void> updateFoo(@PathVariable Long fooId) {
        fooService.updateProperly(fooId);

        return ResponseEntity.ok().build(); // simplest use of a ResponseEntity
    }
}
12
Marc Tarin

Imaginons que nous ayons une entité Account:

@Entity
public class Account implements Identifiable<Integer>, Serializable {

    private static final long serialVersionUID = -3187480027431265380L;

    @Id
    private Integer id;
    private String name;

    public Account(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Avec un AccountRepository exposant ses points de terminaison CRUD sur /accounts:

@RepositoryRestResource(collectionResourceRel = "accounts", path = "accounts")
public interface AccountRepository extends CrudRepository<Account, Integer> {
} 

Et un AccountController qui remplace la forme par défaut GET endpoint AccountRepository .:

@RepositoryRestController
public class AccountController {
    private PagedResourcesAssembler<Account> pagedAssembler;

    @Autowired
    public AccountController(PagedResourcesAssembler<Account> pagedAssembler) {
        this.pagedAssembler = pagedAssembler;
    }

    private Page<Account> getAccounts(Pageable pageRequest){
        int totalAccounts= 50;
        List<Account> accountList = IntStream.rangeClosed(1, totalAccounts)
                                             .boxed()
                                             .map( value -> new Account(value, value.toString()))
                                             .skip(pageRequest.getOffset())
                                             .limit(pageRequest.getPageSize())
                                             .collect(Collectors.toList());
        return new PageImpl(accountList, pageRequest, totalAccounts);
    }

    @RequestMapping(method= RequestMethod.GET, path="/accounts", produces = "application/hal+json")
    public ResponseEntity<Page<Account>> getAccountsHal(Pageable pageRequest, PersistentEntityResourceAssembler assembler){
        return new ResponseEntity(pagedAssembler.toResource(getAccounts(pageRequest), (ResourceAssembler) assembler), HttpStatus.OK);
    }

Si vous invoquez le GET /accounts?size=5&page=0 vous obtiendrez la sortie suivante qui utilise l'implémentation fictive:

{
  "_embedded": {
    "accounts": [
      {
        "name": "1",
        "_links": {
          "self": {
            "href": "http://localhost:8080/accounts/1"
          },
          "account": {
            "href": "http://localhost:8080/accounts/1"
          }
        }
      },
      {
        "name": "2",
        "_links": {
          "self": {
            "href": "http://localhost:8080/accounts/2"
          },
          "account": {
            "href": "http://localhost:8080/accounts/2"
          }
        }
      },
      {
        "name": "3",
        "_links": {
          "self": {
            "href": "http://localhost:8080/accounts/3"
          },
          "account": {
            "href": "http://localhost:8080/accounts/3"
          }
        }
      },
      {
        "name": "4",
        "_links": {
          "self": {
            "href": "http://localhost:8080/accounts/4"
          },
          "account": {
            "href": "http://localhost:8080/accounts/4"
          }
        }
      },
      {
        "name": "5",
        "_links": {
          "self": {
            "href": "http://localhost:8080/accounts/5"
          },
          "account": {
            "href": "http://localhost:8080/accounts/5"
          }
        }
      }
    ]
  },
  "_links": {
    "first": {
      "href": "http://localhost:8080/accounts?page=0&size=5"
    },
    "self": {
      "href": "http://localhost:8080/accounts?page=0&size=5"
    },
    "next": {
      "href": "http://localhost:8080/accounts?page=1&size=5"
    },
    "last": {
      "href": "http://localhost:8080/accounts?page=9&size=5"
    }
  },
  "page": {
    "size": 5,
    "totalElements": 50,
    "totalPages": 10,
    "number": 0
  }
}

Dans un souci d'exhaustivité, le POM peut être configuré avec le parent et les dépendances suivants:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-webmvc</artifactId>
            <version>2.6.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>
8
Daniel Cerecedo

J'ai trouvé une solution intéressante si vous utilisez Java 8 - utilisez simplement les méthodes par défaut dans l'interface

@RepositoryRestResource
public interface FooRepository extends CrudRepository<Foo, Long> {
    default <S extends T> S save(S var1) {
        //do some work here
    }
}
2
jsannn

Juste une mise à jour que j'ai trouvée qui m'a sauvé la vie. Comme l'a dit brillamment @ mathias-dpunkt dans cette réponse https://stackoverflow.com/a/34518166/2836627

Plus important encore, RepositoryRestController connaît le chemin de la base de repos des données de printemps et sera servi sous ce chemin de base.

Donc, si votre chemin de base est "/ api" et que vous utilisez @RepositoryRestController

vous devez supprimer "/ api" de @RequestMapping

1
azous