Spring 3.2.x를 사용하여 REST API 버전을 관리하는 방법을 찾고 있었지만 유지 관리하기 쉬운 것을 찾지 못했습니다. 먼저 내가 가진 문제를 설명하고 그 다음 해결책을 설명하겠습니다.하지만 여기서 바퀴를 다시 발명하고 있는지 궁금합니다.
Accept 헤더를 기반으로 버전을 관리하고 싶습니다. 예를 들어 요청에 Accept 헤더가있는 경우 application/vnd.company.app-1.1+json
Spring MVC에서이 버전을 처리하는 메서드로 전달하기를 원합니다. API의 모든 메서드가 동일한 릴리스에서 변경되는 것은 아니기 때문에 각 컨트롤러로 이동하여 버전간에 변경되지 않은 핸들러에 대해 아무것도 변경하고 싶지 않습니다. 또한 Spring이 이미 호출 할 메서드를 발견하고 있으므로 컨트롤러 자체에서 사용할 버전 (서비스 로케이터 사용)을 파악하는 논리를 갖고 싶지 않습니다.
따라서 버전 1.0에서 핸들러가 도입되고 v1.7에서 수정 된 버전 1.0에서 1.8까지의 API를 가져 오면 다음과 같이 처리하고 싶습니다. 코드가 컨트롤러 내부에 있고 헤더에서 버전을 추출 할 수있는 코드가 있다고 상상해보십시오. (다음은 Spring에서 유효하지 않습니다)
@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;
}
두 메서드가 동일한 RequestMapping
주석을 가지고 있고 Spring 이로드되지 않기 때문에 이것은 Spring에서 가능하지 않습니다. 아이디어는 VersionRange
주석이 개방형 또는 폐쇄 형 버전 범위를 정의 할 수 있다는 것입니다 . 첫 번째 방법은 버전 1.0에서 1.6까지 유효하고 두 번째 방법은 버전 1.7 이상 (최신 버전 1.8 포함)에서 유효합니다. 누군가가 99.99 버전을 통과하기로 결정하면이 접근 방식이 깨진다는 것을 알고 있지만, 그게 제가 살아도 괜찮습니다.
이제 스프링 작동 방식을 심각하게 재 작업하지 않고는 위의 작업을 수행 할 수 없기 때문에 핸들러가 요청에 일치하는 방식, 특히 내를 작성 ProducesRequestCondition
하고 거기에 버전 범위를 포함 하는 방식을 수정하려고합니다 . 예를 들면
암호:
@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;
}
이런 식으로 주석의 생산 부분에 정의 된 폐쇄 또는 개방 버전 범위를 가질 수 있습니다. 현재이 솔루션을 작업 중입니다.이 솔루션을 사용하는 중입니다. 아직 마음에 들지 않는 일부 핵심 Spring MVC 클래스 ( RequestMappingInfoHandlerMapping
, RequestMappingHandlerMapping
및 RequestMappingInfo
) 를 교체해야하는 문제가 있습니다 . 새로운 버전으로 업그레이드하기로 결정할 때마다 추가 작업을 의미하기 때문입니다. 봄.
나는 어떤 생각이라도 고맙게 생각하고 … 특히 더 간단하고 유지하기 쉬운 방법으로 이것을 수행하는 제안.
편집하다
현상금 추가. 현상금을 받으려면 컨트롤러 자체에이 로직이 있다는 제안없이 위의 질문에 답하십시오. Spring은 이미 호출 할 컨트롤러 메서드를 선택하는 많은 논리를 가지고 있으며 이에 대해 피기 백하고 싶습니다.
편집 2
github에서 원본 POC (일부 개선 사항 포함)를 공유했습니다 : https://github.com/augusto/restVersioning
답변
이전 버전과 호환되는 변경을 수행하여 버전 관리를 피할 수 있는지 여부에 관계없이 (일부 기업 지침에 묶여 있거나 API 클라이언트가 버그가있는 방식으로 구현되어서는 안 되더라도 깨질 수있는 경우 항상 가능하지는 않을 수 있음) 추상화 된 요구 사항은 흥미 롭습니다. 하나:
메서드 본문에서 평가를 수행하지 않고 요청에서 헤더 값을 임의로 평가하는 사용자 지정 요청 매핑을 수행하려면 어떻게해야합니까?
이 SO 답변 에서 설명한 것처럼 실제로는 동일 @RequestMapping
하고 다른 주석을 사용하여 런타임 중에 발생하는 실제 라우팅 중에 구별 할 수 있습니다 . 이렇게하려면 다음을 수행해야합니다.
- 새 주석을 만듭니다
VersionRange
. - 을 구현합니다
RequestCondition<VersionRange>
. 가장 일치하는 알고리즘과 같은 것이 있으므로 다른VersionRange
값으로 주석이 달린 메서드 가 현재 요청에 대해 더 나은 일치를 제공 하는지 확인해야합니다 . - 구현
VersionRangeRequestMappingHandlerMapping
(포스트에 설명 된대로 주석과 요구 조건에 따라 @RequestMapping 사용자 지정 속성을 구현하는 방법
). VersionRangeRequestMappingHandlerMapping
기본값을 사용하기 전에 평가하도록 spring을 구성하십시오RequestMappingHandlerMapping
(예 : 순서를 0으로 설정).
이것은 Spring 컴포넌트의 해키 교체가 필요하지 않지만 Spring 구성 및 확장 메커니즘을 사용하므로 Spring 버전을 업데이트하더라도 작동합니다 (새 버전이 이러한 메커니즘을 지원하는 한).
답변
방금 맞춤형 솔루션을 만들었습니다. 클래스 내부의 주석 @ApiVersion
과 함께 @RequestMapping
주석을 사용하고 @Controller
있습니다.
예:
@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
}
이행:
ApiVersion.java 주석 :
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
int[] value();
}
ApiVersionRequestMappingHandlerMapping.java (대부분에서 복사 및 붙여 넣기 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);
}
}
WebMvcConfigurationSupport에 삽입 :
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
return new ApiVersionRequestMappingHandlerMapping("v");
}
}
답변
URL에서 @RequestMapping은 regexp로 지정할 수있는 형식과 경로 매개 변수를 지원하기 때문에 버전 관리에 URL을 사용하는 것이 좋습니다.
그리고 클라이언트 업그레이드 (댓글에서 언급 했음)를 처리하기 위해 ‘최신’과 같은 별칭을 사용할 수 있습니다. 또는 최신 버전을 사용하는 버전이없는 API 버전이 있습니다 (예).
또한 경로 매개 변수를 사용하여 복잡한 버전 처리 논리를 구현할 수 있으며 이미 범위를 갖고 싶다면 더 빨리 무언가를 원할 수 있습니다.
다음은 몇 가지 예입니다.
@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;
}
}
마지막 접근 방식을 기반으로 실제로 원하는 것과 같은 것을 구현할 수 있습니다.
예를 들어 버전 처리와 함께 메서드 stab 만 포함하는 컨트롤러를 가질 수 있습니다.
해당 처리에서 일부 스프링 서비스 / 컴포넌트 또는 동일한 이름 / 서명 및 필수 @VersionRange를 가진 메서드에 대한 동일한 클래스에서 (반사 / AOP / 코드 생성 라이브러리 사용)보고 모든 매개 변수를 전달하여 호출합니다.
답변
완벽 하게 처리하는 솔루션을 구현했습니다.나머지 버전 관리 문제를 .
일반 말하기 나머지 버전 관리에는 세 가지 주요 접근 방식이 있습니다.
-
클라이언트가 URL에서 버전을 정의하는 경로 기반 접근 방식 :
http://localhost:9001/api/v1/user http://localhost:9001/api/v2/user
-
클라이언트가 Accept 헤더 의 버전을 정의하는 Content-Type 헤더 :
http://localhost:9001/api/v1/user with Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
-
사용자 지정 헤더 클라이언트가 사용자 정의 헤더의 버전을 정의하는.
문제 와 첫 번째 방법은 당신이 버전을 변경하는 경우의가 V1에서 생각한 것입니다 -> V2, 아마 당신은 v2의 경로로 변경되지 않은 V1 자원을 복사하여 붙여 넣어야
문제 와 두 번째 방법은 같은 몇 가지 도구입니다 http://swagger.io/ 동일한 경로하지만 서로 다른 내용 유형 (체크 문제와 작업 사이 수 구별하지 https://github.com/OAI/OpenAPI-Specification/issues/ 146 )
해결책
나머지 문서화 도구를 많이 사용하고 있으므로 첫 번째 접근 방식을 선호합니다. 내 솔루션 은 첫 번째 접근 방식으로 문제 를 처리 하므로 엔드 포인트를 새 버전에 복사하여 붙여 넣을 필요가 없습니다.
사용자 컨트롤러 용 v1 및 v2 버전이 있다고 가정 해 보겠습니다.
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";
}
}
요구 사항은 내가 요청하는 경우이다 V1 내가 가지고 가야 사용자 리소스에 대해 “사용자 V1” 내가 요청 그렇지 않은 경우, repsonse을 V2 , V3를 그래서 내가 가지고 가야에서 “사용자 V2” 응답을.
이를 봄에 구현하려면 기본 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 ;
}
}
구현은 URL에서 버전을 읽고 URL을 확인하기 위해 봄부터 요청합니다.이 URL이 존재하지 않는 경우 (예 : 클라이언트가 v3 요청 ) v2로 시도 하고 리소스에 대한 최신 버전 을 찾을 때까지 하나를 시도 합니다. .
이 구현의 이점을 확인하기 위해 사용자와 회사의 두 가지 리소스가 있다고 가정 해 보겠습니다.
http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company
고객을 파기하는 회사 “계약”을 변경했다고 가정 해 보겠습니다. 그래서 우리는를 구현하고 http://localhost:9001/api/v2/company
클라이언트에서 v1 대신 v2로 변경하도록 요청합니다.
따라서 클라이언트의 새로운 요청은 다음과 같습니다.
http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company
대신에:
http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company
여기서 가장 좋은 점은이 솔루션을 사용하면 클라이언트가 사용자 v2에서 새 (동일한) 엔드 포인트를 만들 필요없이 v1에서 사용자 정보를 가져오고 v2에서 회사 정보를 얻을 수 있다는 것입니다 !
나머지 문서화
내가 URL 기반 버전 관리 방식을 선택한 이유 전에 말했듯이 swagger와 같은 일부 도구는 동일한 URL을 사용하지만 콘텐츠 유형이 다른 엔드 포인트를 다르게 문서화하지 않기 때문입니다. 이 솔루션을 사용하면 URL이 다르기 때문에 두 끝 점이 표시됩니다.
GIT
답변
@RequestMapping
주석은 지원 headers
이 일치하는 요청을 좁힐 수 있습니다 요소. 특히 Accept
여기 에서 헤더를 사용할 수 있습니다 .
@RequestMapping(headers = {
"Accept=application/vnd.company.app-1.0+json",
"Accept=application/vnd.company.app-1.1+json"
})
이것은 범위를 직접 처리하지 않기 때문에 정확히 설명하는 것은 아니지만 요소는 * 와일드 카드와! =를 지원합니다. 따라서 최소한 모든 버전이 해당 엔드 포인트를 지원하는 경우 또는 특정 주 버전의 모든 부 버전 (예 : 1. *)을 지원하는 경우 와일드 카드를 사용하는 것은 피할 수 있습니다.
이전에이 요소를 실제로 사용하지 않았다고 생각합니다 (기억하지 않는 경우).
답변
모델 버전 관리에 상속을 사용하는 것은 어떻습니까? 그것이 내 프로젝트에서 사용하고 있으며 특별한 스프링 구성이 필요하지 않으며 내가 원하는 것을 정확하게 얻습니다.
@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);
}
}
이 설정은 코드 복제를 거의 허용하지 않으며 거의 작업없이 새 버전의 API로 메서드를 덮어 쓸 수있는 기능을 제공합니다. 또한 버전 전환 논리로 소스 코드를 복잡하게 만들 필요가 없습니다. 버전에서 엔드 포인트를 코딩하지 않으면 기본적으로 이전 버전을 가져옵니다.
다른 사람들이하는 일에 비해 훨씬 쉬운 것 같습니다. 내가 놓친 것이 있습니까?
답변
이미 다음과 같이 URI Versioning을 사용하여 API의 버전을 지정 하려고했습니다 .
/api/v1/orders
/api/v2/orders
그러나이 작업을 수행 할 때 몇 가지 문제가 있습니다. 다른 버전으로 코드를 구성하는 방법은 무엇입니까? 동시에 두 개 이상의 버전을 관리하는 방법은 무엇입니까? 일부 버전을 제거하면 어떤 영향이 있습니까?
내가 찾은 최고의 대안은 전체 API 버전이 아니라 각 엔드 포인트의 버전을 제어하는 것 입니다. 이 패턴 을 Accept 헤더를 사용한 Versioning 또는 Content Negotiation을 통한 Versioning 이라고합니다 .
이 접근 방식을 사용하면 전체 API를 버전 관리하는 대신 단일 리소스 표현을 버전 관리 할 수 있으므로 버전 관리를보다 세밀하게 제어 할 수 있습니다. 또한 새 버전을 생성 할 때 전체 애플리케이션을 포크 할 필요가 없기 때문에 코드베이스에 더 작은 공간을 생성합니다. 이 접근 방식의 또 다른 장점은 URI 경로를 통한 버전 관리로 도입 된 URI 라우팅 규칙을 구현할 필요가 없다는 것입니다.
Spring에서 구현
먼저 기본 생성 속성을 사용하여 컨트롤러를 생성합니다.이 속성은 기본적으로 클래스 내의 각 엔드 포인트에 적용됩니다.
@RestController
@RequestMapping(value = "/api/orders/", produces = "application/vnd.company.etc.v1+json")
public class OrderController {
}
그런 다음 주문 생성을위한 두 가지 버전의 엔드 포인트가있는 가능한 시나리오를 생성합니다.
@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);
}
끝난! 원하는 Http 헤더 버전을 사용하여 각 엔드 포인트를 호출하기 만하면됩니다 .
Content-Type: application/vnd.company.etc.v1+json
또는 버전 2를 호출하려면 :
Content-Type: application/vnd.company.etc.v2+json
당신의 걱정 :
API의 모든 메서드가 동일한 릴리스에서 변경되는 것은 아니기 때문에 각 컨트롤러로 이동하여 버전간에 변경되지 않은 핸들러에 대해 아무것도 변경하고 싶지 않습니다.
설명했듯이이 전략은 실제 버전으로 각 컨트롤러와 엔드 포인트를 유지합니다. 수정 사항이 있고 새 버전이 필요한 끝점 만 수정합니다.
그리고 Swagger?
이 전략을 사용하면 다른 버전으로 Swagger를 설정하는 것도 매우 쉽습니다. 자세한 내용은이 답변 을 참조하십시오.