Como gerenciar o controle de versão da API REST com spring?

119

Tenho pesquisado como gerenciar versões da API REST usando Spring 3.2.x, mas não encontrei nada que seja fácil de manter. Vou explicar primeiro o problema que tenho e depois uma solução ... mas me pergunto se estou reinventando a roda aqui.

Quero gerenciar a versão com base no cabeçalho Aceitar e, por exemplo, se uma solicitação tiver o cabeçalho Aceitar application/vnd.company.app-1.1+json, quero que o Spring MVC encaminhe isso para o método que lida com esta versão. E como nem todos os métodos em uma API mudam na mesma versão, não quero ir a cada um dos meus controladores e mudar nada para um manipulador que não mudou entre as versões. Eu também não quero ter a lógica para descobrir qual versão usar no próprio controlador (usando localizadores de serviço), pois o Spring já está descobrindo qual método chamar.

Então, pegando uma API com as versões 1.0, até 1.8, onde um manipulador foi introduzido na versão 1.0 e modificado na v1.7, gostaria de lidar com isso da seguinte maneira. Imagine que o código está dentro de um controlador e que existe algum código capaz de extrair a versão do cabeçalho. (O seguinte é inválido na primavera)

@RequestMapping(...)
@VersionRange(1.0,1.6)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(...) //same Request mapping annotation
@VersionRange(1.7)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

Isso não é possível no Spring, pois os 2 métodos têm a mesma RequestMappinganotação e o Spring falha ao carregar. A ideia é que a VersionRangeanotação pode definir um intervalo de versão aberta ou fechada. O primeiro método é válido das versões 1.0 a 1.6, enquanto o segundo para a versão 1.7 em diante (incluindo a versão mais recente 1.8). Eu sei que essa abordagem é interrompida se alguém decidir passar a versão 99.99, mas estou bem em conviver com isso.

Agora, uma vez que o acima não é possível sem um retrabalho sério de como funciona o spring, eu estava pensando em consertar a maneira como os manipuladores combinam com as solicitações, em particular para escrever meu próprio ProducesRequestCondition, e ter o intervalo de versão lá. Por exemplo

Código:

@RequestMapping(..., produces = "application/vnd.company.app-[1.0-1.6]+json)
@ResponseBody
public Object method1() {
   // so something
   return object;
}

@RequestMapping(..., produces = "application/vnd.company.app-[1.7-]+json)
@ResponseBody
public Object method2() {
   // so something
   return object;
}

Desse modo, posso fechar ou abrir intervalos de versão definidos na parte de produtos da anotação. Estou trabalhando nessa solução agora, com o problema de que ainda tenho que substituir algumas classes principais do Spring MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappinge RequestMappingInfo), das quais não gosto, porque isso significa trabalho extra sempre que decido atualizar para uma versão mais recente do Primavera.

Eu agradeceria qualquer pensamento ... e especialmente, qualquer sugestão para fazer isso de uma forma mais simples e fácil de manter.


Editar

Adicionando uma recompensa. Para obter a recompensa, responda à pergunta acima sem sugerir ter essa lógica no próprio controlador. O Spring já tem muita lógica para selecionar qual método de controlador chamar, e eu quero pegar carona nisso.


Editar 2

Eu compartilhei o POC original (com algumas melhorias) no github: https://github.com/augusto/restVersioning

Augusto
fonte
1
@flup Não entendo seu comentário. Isso apenas diz que você pode usar cabeçalhos e, como eu disse, o que o spring fornece fora da caixa não é suficiente para suportar APIs que são atualizadas constantemente. Pior ainda, o link dessa resposta usa a versão no URL.
Augusto
Talvez não seja exatamente o que você está procurando, mas o Spring 3.2 suporta um parâmetro "produz" no RequestMapping. A única ressalva é que a lista de versões deve ser explícita. Por exemplo, produces={"application/json-1.0", "application/json-1.1"}etc
bimsapi
1
Precisamos oferecer suporte a várias versões de nossas APIs, essas diferenças geralmente são pequenas alterações que tornariam algumas chamadas de alguns clientes incompatíveis (não será estranho se precisarmos oferecer suporte a 4 versões secundárias, nas quais alguns terminais são incompatíveis). Agradeço a sugestão de colocá-lo no url, mas sabemos que é um passo na direção errada, pois temos alguns aplicativos com a versão no URL e há muito trabalho envolvido toda vez que precisamos superar o versão.
Augusto
1
@Augusto, na verdade você também não. Basta projetar as mudanças de API de uma maneira que não quebre a compatibilidade com versões anteriores. Apenas me dê um exemplo de mudanças que quebram a compatibilidade e eu mostro como fazer essas mudanças de maneira ininterrupta.
Alexey Andreev

Respostas:

62

Independentemente de saber se o controle de versão pode ser evitado ao fazer alterações compatíveis com versões anteriores (o que nem sempre é possível quando você está sujeito a algumas diretrizes corporativas ou seus clientes de API são implementados de uma forma com bugs e quebrariam mesmo se não deveriam), o requisito abstraído é interessante 1:

Como posso fazer um mapeamento de solicitação personalizada que faz avaliações arbitrárias de valores de cabeçalho da solicitação sem fazer a avaliação no corpo do método?

Conforme descrito nesta resposta do SO, você realmente pode ter o mesmo @RequestMappinge usar uma anotação diferente para diferenciar durante o roteamento real que acontece durante o tempo de execução. Para fazer isso, você terá que:

  1. Crie uma nova anotação VersionRange.
  2. Implementar a RequestCondition<VersionRange>. Como você terá algo como um algoritmo de melhor correspondência, terá que verificar se os métodos anotados com outros VersionRangevalores fornecem uma correspondência melhor para a solicitação atual.
  3. Implemente um com VersionRangeRequestMappingHandlerMappingbase na anotação e na condição de solicitação (conforme descrito na postagem Como implementar as propriedades personalizadas @RequestMapping ).
  4. Configure a mola para avaliar seu VersionRangeRequestMappingHandlerMappingantes de usar o padrão RequestMappingHandlerMapping(por exemplo, definindo sua ordem para 0).

Isso não exigiria nenhuma substituição de componentes do Spring, mas usa os mecanismos de configuração e extensão do Spring, então deve funcionar mesmo se você atualizar sua versão do Spring (contanto que a nova versão suporte esses mecanismos).

xwoker
fonte
Obrigado por adicionar seu comentário como uma resposta xwoker. Até agora é o melhor. Implementei a solução com base nos links que você mencionou e não é tão ruim assim. O maior problema se manifestará ao atualizar para uma nova versão do Spring, pois será necessário verificar quaisquer alterações na lógica por trás mvc:annotation-driven. Esperançosamente, o Spring fornecerá uma versão mvc:annotation-drivenem que se pode definir condições personalizadas.
Augusto
@Augusto, meio ano depois, como isso tem funcionado para você? Além disso, estou curioso, você está realmente criando versões por método? Neste ponto, estou me perguntando se não seria mais claro a versão em uma granularidade de nível por classe / por controlador?
Sander Verhagen
1
@SanderVerhagen está funcionando, mas criamos a versão de toda a API, não por método ou controlador (a API é bem pequena, pois está focada em um aspecto do negócio). Temos um projeto consideravelmente maior, onde eles escolheram usar uma versão diferente por recurso e especificar isso no URL (para que você possa ter um endpoint em / v1 / sessions e outro recurso em uma versão completamente diferente, por exemplo, / v4 / orders) ... é um pouco mais flexível, mas coloca mais pressão sobre os clientes para saber qual versão chamar de cada terminal.
Augusto
1
Infelizmente, isso não funciona bem com o Swagger, pois muitas configurações automáticas são desativadas ao estender o WebMvcConfigurationSupport.
Rick
Tentei esta solução, mas na verdade não está funcionando com 2.3.2.RELEASE. Você tem algum projeto de exemplo para mostrar?
Patrick
54

Acabei de criar uma solução personalizada. Estou usando a @ApiVersionanotação em combinação com a @RequestMappinganotação dentro das @Controllerclasses.

Exemplo:

@Controller
@RequestMapping("x")
@ApiVersion(1)
class MyController {

    @RequestMapping("a")
    void a() {}         // maps to /v1/x/a

    @RequestMapping("b")
    @ApiVersion(2)
    void b() {}         // maps to /v2/x/b

    @RequestMapping("c")
    @ApiVersion({1,3})
    void c() {}         // maps to /v1/x/c
                        //  and to /v3/x/c

}

Implementação:

Anotação ApiVersion.java :

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (geralmente copia e cola de RequestMappingHandlerMapping):

public class ApiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private final String prefix;

    public ApiVersionRequestMappingHandlerMapping(String prefix) {
        this.prefix = prefix;
    }

    @Override
    protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = super.getMappingForMethod(method, handlerType);
        if(info == null) return null;

        ApiVersion methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        if(methodAnnotation != null) {
            RequestCondition<?> methodCondition = getCustomMethodCondition(method);
            // Concatenate our ApiVersion with the usual request mapping
            info = createApiVersionInfo(methodAnnotation, methodCondition).combine(info);
        } else {
            ApiVersion typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
            if(typeAnnotation != null) {
                RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
                // Concatenate our ApiVersion with the usual request mapping
                info = createApiVersionInfo(typeAnnotation, typeCondition).combine(info);
            }
        }

        return info;
    }

    private RequestMappingInfo createApiVersionInfo(ApiVersion annotation, RequestCondition<?> customCondition) {
        int[] values = annotation.value();
        String[] patterns = new String[values.length];
        for(int i=0; i<values.length; i++) {
            // Build the URL prefix
            patterns[i] = prefix+values[i]; 
        }

        return new RequestMappingInfo(
                new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), useSuffixPatternMatch(), useTrailingSlashMatch(), getFileExtensions()),
                new RequestMethodsRequestCondition(),
                new ParamsRequestCondition(),
                new HeadersRequestCondition(),
                new ConsumesRequestCondition(),
                new ProducesRequestCondition(),
                customCondition);
    }

}

Injeção em WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Benjamin M
fonte
4
Mudei o int [] para String [] para permitir versões como "1.2", e assim posso lidar com palavras-chave como "mais recente"
Maelig
3
Sim, isso é bastante razoável. Para projetos futuros, eu seguiria por um caminho diferente por alguns motivos: 1. URLs representam recursos. /v1/aResourcee /v2/aResourceparecem recursos diferentes, mas é apenas uma representação diferente do mesmo recurso! 2. Usar cabeçalhos HTTP parece melhor, mas você não pode fornecer um URL a alguém, porque o URL não contém o cabeçalho. 3. Usando um parâmetro de URL, ou seja /aResource?v=2.1(aliás: é assim que o Google faz versionamento). ...Ainda não tenho certeza se escolheria a opção 2 ou 3 , mas nunca mais usarei 1 pelos motivos mencionados acima.
Benjamin M
5
Se quiser injetar o seu próprio RequestMappingHandlerMappingno seu WebMvcConfiguration, você deve sobrescrever em createRequestMappingHandlerMappingvez de requestMappingHandlerMapping! Caso contrário, você encontrará problemas estranhos (de repente tive problemas com a inicialização lenta do Hibernates por causa de uma sessão fechada)
stuXnet
1
A abordagem parece boa, mas de alguma forma parece que não funciona com casos de teste junti (SpringRunner). Qualquer chance de você ter a abordagem funcionando com casos de teste
JDev
1
Existe uma maneira de fazer este trabalho, não estender, WebMvcConfigurationSupport mas estender DelegatingWebMvcConfiguration. Isso funcionou para mim (consulte stackoverflow.com/questions/22267191/… )
SeB.Fr
17

Eu ainda recomendaria usar URLs para controle de versão porque em URLs @RequestMapping oferece suporte a padrões e parâmetros de caminho, cujo formato pode ser especificado com regexp.

E para lidar com as atualizações do cliente (que você mencionou no comentário), você pode usar apelidos como 'mais recentes'. Ou ter uma versão não versionada do api que usa a versão mais recente (sim).

Também usando parâmetros de caminho, você pode implementar qualquer lógica de tratamento de versão complexa e, se você já quiser ter intervalos, pode querer algo mais em breve.

Aqui estão alguns exemplos:

@RequestMapping({
    "/**/public_api/1.1/method",
    "/**/public_api/1.2/method",
})
public void method1(){
}

@RequestMapping({
    "/**/public_api/1.3/method"
    "/**/public_api/latest/method"
    "/**/public_api/method" 
})
public void method2(){
}

@RequestMapping({
    "/**/public_api/1.4/method"
    "/**/public_api/beta/method"
})
public void method2(){
}

//handles all 1.* requests
@RequestMapping({
    "/**/public_api/{version:1\\.\\d+}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//handles 1.0-1.6 range, but somewhat ugly
@RequestMapping({
    "/**/public_api/{version:1\\.[0123456]?}/method"
})
public void methodManual1(@PathVariable("version") String version){
}

//fully manual version handling
@RequestMapping({
    "/**/public_api/{version}/method"
})
public void methodManual2(@PathVariable("version") String version){
    int[] versionParts = getVersionParts(version);
    //manual handling of versions
}

public int[] getVersionParts(String version){
    try{
        String[] versionParts = version.split("\\.");
        int[] result = new int[versionParts.length];
        for(int i=0;i<versionParts.length;i++){
            result[i] = Integer.parseInt(versionParts[i]);
        }
        return result;
    }catch (Exception ex) {
        return null;
    }
}

Com base na última abordagem, você pode realmente implementar algo como o que deseja.

Por exemplo, você pode ter um controlador que contém apenas stabs de método com manipulação de versão.

Nessa manipulação, você procura (usando bibliotecas de reflexão / AOP / geração de código) em algum serviço / componente de primavera ou na mesma classe para o método com o mesmo nome / assinatura e @VersionRange necessário e o invoca passando todos os parâmetros.

código evasivo
fonte
14

Eu implementei uma solução que lida PERFEITAMENTE com o problema de controle de versão restante.

Em termos gerais, existem 3 abordagens principais para o controle de versões restantes:

  • Aproximação baseada no caminho , em que o cliente define a versão no URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Cabeçalho Content-Type , no qual o cliente define a versão no cabeçalho Accept :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Cabeçalho personalizado , no qual o cliente define a versão em um cabeçalho personalizado.

O problema com a primeira abordagem é que se você alterar a versão, digamos de v1 -> v2, provavelmente você precisará copiar e colar os recursos v1 que não mudaram para o caminho v2

O problema com a segunda abordagem é que algumas ferramentas como http://swagger.io/ não podem distinguir entre operações com o mesmo caminho, mas com tipo de conteúdo diferente (verifique o problema https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

A solução

Como estou trabalhando muito com ferramentas de documentação restantes, prefiro usar a primeira abordagem. Minha solução lida com o problema com a primeira abordagem, então você não precisa copiar e colar o endpoint para a nova versão.

Digamos que temos as versões v1 e v2 para o controlador de usuário:

package com.mspapant.example.restVersion.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * The user controller.
 *
 * @author : Manos Papantonakos on 19/8/2016.
 */
@Controller
@Api(value = "user", description = "Operations about users")
public class UserController {

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v1/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV1() {
         return "User V1";
    }

    /**
     * Return the user.
     *
     * @return the user
     */
    @ResponseBody
    @RequestMapping(method = RequestMethod.GET, value = "/api/v2/user")
    @ApiOperation(value = "Returns user", notes = "Returns the user", tags = {"GET", "User"})
    public String getUserV2() {
         return "User V2";
    }
 }

O requisito é que, se eu solicitar a v1 para o recurso do usuário, eu tenho que responder "User V1" , caso contrário, se eu solicitar a v2 , v3 e assim por diante, eu tenho que tomar a resposta "User V2" .

insira a descrição da imagem aqui

Para implementar isso na primavera, precisamos substituir o comportamento padrão de RequestMappingHandlerMapping :

package com.mspapant.example.restVersion.conf.mapping;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

public class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Value("${server.apiContext}")
    private String apiContext;

    @Value("${server.versionContext}")
    private String versionContext;

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod method = super.lookupHandlerMethod(lookupPath, request);
        if (method == null && lookupPath.contains(getApiAndVersionContext())) {
            String afterAPIURL = lookupPath.substring(lookupPath.indexOf(getApiAndVersionContext()) + getApiAndVersionContext().length());
            String version = afterAPIURL.substring(0, afterAPIURL.indexOf("/"));
            String path = afterAPIURL.substring(version.length() + 1);

            int previousVersion = getPreviousVersion(version);
            if (previousVersion != 0) {
                lookupPath = getApiAndVersionContext() + previousVersion + "/" + path;
                final String lookupFinal = lookupPath;
                return lookupHandlerMethod(lookupPath, new HttpServletRequestWrapper(request) {
                    @Override
                    public String getRequestURI() {
                        return lookupFinal;
                    }

                    @Override
                    public String getServletPath() {
                        return lookupFinal;
                    }});
            }
        }
        return method;
    }

    private String getApiAndVersionContext() {
        return "/" + apiContext + "/" + versionContext;
    }

    private int getPreviousVersion(final String version) {
        return new Integer(version) - 1 ;
    }

}

A implementação lê a versão no URL e pede desde a primavera para resolver o URL. Caso este URL não exista (por exemplo, o cliente solicitou v3 ), então tentamos com v2 e assim um até encontrarmos a versão mais recente para o recurso .

Para ver os benefícios desta implementação, digamos que temos dois recursos: Usuário e Empresa:

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

Digamos que fizemos uma mudança no "contrato" da empresa que quebra o cliente. Portanto, implementamos o http://localhost:9001/api/v2/companye pedimos ao cliente para mudar para v2 em vez de v1.

Portanto, as novas solicitações do cliente são:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

ao invés de:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

A melhor parte aqui é que com esta solução o cliente obterá as informações do usuário da v1 e as informações da empresa da v2 sem a necessidade de criar um novo (mesmo) endpoint a partir do usuário v2!

Rest Documentation Como eu disse antes, o motivo pelo qual escolhi a abordagem de versão baseada em URL é que algumas ferramentas como o swagger não documentam de forma diferente os terminais com a mesma URL, mas diferentes tipos de conteúdo. Com esta solução, ambos os endpoints são exibidos, pois têm URLs diferentes:

insira a descrição da imagem aqui

GIT

Implementação da solução em: https://github.com/mspapant/restVersioningExample/

mspapant
fonte
9

A @RequestMappinganotação oferece suporte a um headerselemento que permite restringir as solicitações correspondentes. Em particular, você pode usar o Acceptcabeçalho aqui.

@RequestMapping(headers = {
    "Accept=application/vnd.company.app-1.0+json",
    "Accept=application/vnd.company.app-1.1+json"
})

Isso não é exatamente o que você está descrevendo, uma vez que não trata diretamente de intervalos, mas o elemento suporta o caractere * curinga, bem como! =. Portanto, pelo menos você poderia usar um curinga para casos em que todas as versões suportam o endpoint em questão, ou mesmo todas as versões secundárias de uma determinada versão principal (por exemplo, 1. *).

Não acho que já usei esse elemento antes (se o usei, não me lembro), então estou apenas saindo da documentação em

http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html


fonte
2
Eu sei disso, mas como você observou, em cada versão eu precisaria ir a todos os meus controladores e adicionar uma versão, mesmo que eles não tenham mudado. O intervalo que você mencionou funciona apenas no tipo completo, por exemplo, application/*e não em partes do tipo. Por exemplo, o seguinte é inválido no Spring "Accept=application/vnd.company.app-1.*+json". Isso está relacionado a como MediaTypefunciona a aula de primavera
Augusto
@Augusto você não precisa necessariamente fazer isso. Com essa abordagem, você não está controlando a versão da "API", mas do "Endpoint". Cada terminal pode ter uma versão diferente. Para mim, é a maneira mais fácil de criar uma versão da API em comparação com a versão da API . Swagger também é mais simples de configurar . Essa estratégia é chamada de controle de versão por meio da negociação de conteúdo.
Dherik
3

Que tal usar apenas a herança para modelar versões? Isso é o que estou usando no meu projeto e não requer nenhuma configuração de mola especial e me dá exatamente o que desejo.

@RestController
@RequestMapping(value = "/test/1")
@Deprecated
public class Test1 {
...Fields Getters Setters...
    @RequestMapping(method = RequestMethod.GET)
    @Deprecated
    public Test getTest(Long id) {
        return serviceClass.getTestById(id);
    }
    @RequestMapping(method = RequestMethod.PUT)
    public Test getTest(Test test) {
        return serviceClass.updateTest(test);
    }

}

@RestController
@RequestMapping(value = "/test/2")
public class Test2 extends Test1 {
...Fields Getters Setters...
    @Override
    @RequestMapping(method = RequestMethod.GET)
    public Test getTest(Long id) {
        return serviceClass.getAUpdated(id);
    }

    @RequestMapping(method = RequestMethod.DELETE)
    public Test deleteTest(Long id) {
        return serviceClass.deleteTestById(id);
    }
}

Essa configuração permite pouca duplicação de código e a capacidade de sobrescrever métodos em novas versões da API com pouco trabalho. Também evita a necessidade de complicar seu código-fonte com a lógica de troca de versão. Se você não codificar um endpoint em uma versão, ele obterá a versão anterior por padrão.

Comparado com o que os outros estão fazendo, isso parece muito mais fácil. Tem algo que estou perdendo?

Ceekay
fonte
1
+1 para compartilhar o código. No entanto, a herança acopla-o fortemente. Em vez de. os controladores (Teste1 e Teste2) devem ser apenas uma passagem ... sem implementação de lógica. Toda a lógica deve estar na classe de serviço, algum serviço. Nesse caso, basta usar a composição simples e nunca herdar do outro controlador
Dan Hunex
1
@dan-hunex Parece que Ceekay usa a herança para gerenciar as diferentes versões da API. Se você remover a herança, qual é a solução? E por que acoplar firmemente é um problema neste exemplo? Do meu ponto de vista, Test2 estende Test1 porque é uma melhoria dele (com a mesma função e mesmas responsabilidades), não é?
jeremieca
2

Já tentei criar uma versão da minha API usando o controle de versão de URI , como:

/api/v1/orders
/api/v2/orders

Mas existem alguns desafios ao tentar fazer isso funcionar: como organizar seu código com versões diferentes? Como gerenciar duas (ou mais) versões ao mesmo tempo? Qual é o impacto ao remover alguma versão?

A melhor alternativa que encontrei não foi a versão de toda a API, mas controlar a versão em cada terminal . Esse padrão é chamado de Controle de versão usando o cabeçalho Aceitar ou Controle de versão por meio da negociação de conteúdo :

Essa abordagem nos permite criar uma versão de uma representação de recurso único em vez de controlar a versão de toda a API, o que nos dá um controle mais granular sobre o controle de versão. Ele também cria uma pegada menor na base de código, pois não precisamos bifurcar o aplicativo inteiro ao criar uma nova versão. Outra vantagem dessa abordagem é que ela não requer a implementação de regras de roteamento de URI introduzidas pelo controle de versão por meio do caminho de URI.

Implementação na primavera

Primeiro, você cria um Controlador com um atributo de produção básico, que se aplicará por padrão a cada terminal dentro da classe.

@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {

}

Depois disso, crie um cenário possível em que você tenha duas versões de um endpoint para criar um pedido:

@Deprecated
@PostMapping
public ResponseEntity<OrderResponse> createV1(
        @RequestBody OrderRequest orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

@PostMapping(
        produces = "application/vnd.company.etc.v2+json",
        consumes = "application/vnd.company.etc.v2+json")
public ResponseEntity<OrderResponseV2> createV2(
        @RequestBody OrderRequestV2 orderRequest) {

    OrderResponse response = createOrderService.createOrder(orderRequest);
    return new ResponseEntity<>(response, HttpStatus.CREATED);
}

Feito! Basta chamar cada endpoint usando a versão desejada do cabeçalho Http :

Content-Type: application/vnd.company.etc.v1+json

Ou, para chamar a versão dois:

Content-Type: application/vnd.company.etc.v2+json

Sobre suas preocupações:

E uma vez que nem todos os métodos em uma API mudam na mesma versão, não quero ir para cada um dos meus controladores e mudar nada para um manipulador que não mudou entre as versões

Conforme explicado, esta estratégia mantém cada controlador e terminal com sua versão real. Você apenas modifica o terminal que tem modificações e precisa de uma nova versão.

E a arrogância?

Configurar o Swagger com diferentes versões também é muito fácil usando essa estratégia. Veja esta resposta para mais detalhes.

Dherik
fonte
1

Nos produtos você pode ter negação. Portanto, para o método1, diga produces="!...1.7"e no método2, tenha o positivo.

O produz também é uma matriz para que você, para o método 1, possa dizer produces={"...1.6","!...1.7","...1.8"}etc (aceitar todos, exceto 1.7)

Claro, não tão ideal quanto os intervalos que você tem em mente, mas acho mais fácil de manter do que outras coisas personalizadas se isso for algo incomum em seu sistema. Boa sorte!

codesalsa
fonte
Obrigado, codesalsa, estou tentando encontrar uma maneira que seja fácil de manter e não exija atualização de cada endpoint toda vez que precisamos alterar a versão.
Augusto
0

Você pode usar AOP, em torno da interceptação

Considere ter um mapeamento de solicitação que recebe todos os /**/public_api/*e neste método não faz nada;

@RequestMapping({
    "/**/public_api/*"
})
public void method2(Model model){
}

Depois de

@Override
public void around(Method method, Object[] args, Object target)
    throws Throwable {
       // look for the requested version from model parameter, call it desired range
       // check the target object for @VersionRange annotation with reflection and acquire version ranges, call the function if it is in the desired range


}

A única restrição é que tudo deve estar no mesmo controlador.

Para configuração de AOP, dê uma olhada em http://www.mkyong.com/spring/spring-aop-examples-advice/

hevi
fonte
Obrigado hevi, estou procurando uma maneira mais amigável do "spring" de fazer isso, já que o Spring já seleciona qual método chamar sem usar AOP. Eu sou minha opinião AOP adiciona um novo nível de complexidade de código que eu gostaria de evitar.
Augusto
@Augusto, Spring tem um grande suporte AOP. Você deveria experimentar. :)
Konstantin Yovkov