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 RequestMapping
anotação e o Spring falha ao carregar. A ideia é que a VersionRange
anotaçã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
, RequestMappingHandlerMapping
e 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
fonte
produces={"application/json-1.0", "application/json-1.1"}
etcRespostas:
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
@RequestMapping
e 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:VersionRange
.RequestCondition<VersionRange>
. Como você terá algo como um algoritmo de melhor correspondência, terá que verificar se os métodos anotados com outrosVersionRange
valores fornecem uma correspondência melhor para a solicitação atual.VersionRangeRequestMappingHandlerMapping
base na anotação e na condição de solicitação (conforme descrito na postagem Como implementar as propriedades personalizadas @RequestMapping ).VersionRangeRequestMappingHandlerMapping
antes de usar o padrãoRequestMappingHandlerMapping
(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).
fonte
mvc:annotation-driven
. Esperançosamente, o Spring fornecerá uma versãomvc:annotation-driven
em que se pode definir condições personalizadas.Acabei de criar uma solução personalizada. Estou usando a
@ApiVersion
anotação em combinação com a@RequestMapping
anotação dentro das@Controller
classes.Exemplo:
Implementação:
Anotação ApiVersion.java :
ApiVersionRequestMappingHandlerMapping.java (geralmente copia e cola de
RequestMappingHandlerMapping
):Injeção em WebMvcConfigurationSupport:
fonte
/v1/aResource
e/v2/aResource
parecem 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.RequestMappingHandlerMapping
no seuWebMvcConfiguration
, você deve sobrescrever emcreateRequestMappingHandlerMapping
vez derequestMappingHandlerMapping
! 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)WebMvcConfigurationSupport
mas estenderDelegatingWebMvcConfiguration
. Isso funcionou para mim (consulte stackoverflow.com/questions/22267191/… )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:
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.
fonte
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:
Cabeçalho Content-Type , no qual o cliente define a versão no cabeçalho Accept :
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:
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" .
Para implementar isso na primavera, precisamos substituir o comportamento padrão de RequestMappingHandlerMapping :
}
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:
Digamos que fizemos uma mudança no "contrato" da empresa que quebra o cliente. Portanto, implementamos o
http://localhost:9001/api/v2/company
e pedimos ao cliente para mudar para v2 em vez de v1.Portanto, as novas solicitações do cliente são:
ao invés de:
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:
GIT
Implementação da solução em: https://github.com/mspapant/restVersioningExample/
fonte
A
@RequestMapping
anotação oferece suporte a umheaders
elemento que permite restringir as solicitações correspondentes. Em particular, você pode usar oAccept
cabeçalho aqui.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
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 comoMediaType
funciona a aula de primaveraQue 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.
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?
fonte
Já tentei criar uma versão da minha API usando o controle de versão de URI , como:
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 :
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.
Depois disso, crie um cenário possível em que você tenha duas versões de um endpoint para criar um pedido:
Feito! Basta chamar cada endpoint usando a versão desejada do cabeçalho Http :
Ou, para chamar a versão dois:
Sobre suas preocupaçõ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.
fonte
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!
fonte
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;Depois de
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/
fonte