Spring으로 REST API 버전 관리를 관리하는 방법은 무엇입니까?


118

Spring 3.2.x를 사용하여 REST API 버전을 관리하는 방법을 찾고 있었지만 유지 관리하기 쉬운 것을 찾지 못했습니다. 먼저 내가 가진 문제를 설명하고 그 다음 해결책을 설명하겠습니다.하지만 여기서 바퀴를 다시 발명하고 있는지 궁금합니다.

Accept 헤더를 기반으로 버전을 관리하고 싶습니다. 예를 들어 요청에 Accept 헤더가있는 경우 application/vnd.company.app-1.1+jsonSpring 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, RequestMappingHandlerMappingRequestMappingInfo) 를 교체해야하는 문제가 있습니다 . 새로운 버전으로 업그레이드하기로 결정할 때마다 추가 작업을 의미하기 때문입니다. 봄.

나는 어떤 생각이라도 고맙게 생각하고 ... 특히 더 간단하고 유지하기 쉬운 방법으로 이것을 수행하는 제안.


편집하다

현상금 추가. 현상금을 받으려면 컨트롤러 자체에이 로직이 있다는 제안없이 위의 질문에 답하십시오. Spring은 이미 호출 할 컨트롤러 메서드를 선택하는 많은 논리를 가지고 있으며 이에 대해 피기 백하고 싶습니다.


편집 2

github에서 원본 POC (일부 개선 사항 포함)를 공유했습니다 : https://github.com/augusto/restVersioning



1
@flup 귀하의 의견을 이해하지 못합니다. 그것은 헤더를 사용할 수 있다는 것을 의미하며 내가 말했듯이 스프링이 즉시 제공하는 것은 지속적으로 업데이트되는 API를 지원하기에 충분하지 않습니다. 그 대답에 대한 링크는 URL의 버전을 사용합니다.
Augusto

찾고있는 것이 정확히 아닐 수도 있지만 Spring 3.2는 RequestMapping에서 "produces"매개 변수를 지원합니다. 한 가지주의 할 점은 버전 목록이 명시 적이어야한다는 것입니다. 예, produces={"application/json-1.0", "application/json-1.1"}
bimsapi 2011

1
여러 버전의 API를 지원해야합니다. 이러한 차이점은 일반적으로 일부 클라이언트의 일부 호출이 호환되지 않는 사소한 변경입니다 (일부 엔드 포인트가 호환되지 않는 4 개의 부 버전을 지원해야하는 경우 이상하지 않습니다). URL에 넣으라는 제안은 감사하지만 URL에 버전이 포함 된 앱이 몇 개 있고 URL을 올릴 때마다 많은 작업이 필요하기 때문에 잘못된 방향으로 나아가는 단계라는 것을 알고 있습니다. 버전.
Augusto

1
@Augusto, 당신은 실제로 당신도 그렇게하지 않았습니다. 이전 버전과의 호환성을 깨지 않는 방식으로 API 변경을 설계하십시오. 호환성을 깨는 변경 사항의 예를 들어 보시고 이러한 변경 사항을 깨지지 않는 방식으로 변경하는 방법을 보여 드리겠습니다.
Alexey Andreev 2013

답변:


62

이전 버전과 호환되는 변경을 수행하여 버전 관리를 피할 수 있는지 여부에 관계없이 (일부 기업 지침에 묶여 있거나 API 클라이언트가 버그가있는 방식으로 구현되어서는 안 되더라도 깨질 수있는 경우 항상 가능하지는 않을 수 있음) 추상화 된 요구 사항은 흥미 롭습니다. 하나:

메서드 본문에서 평가를 수행하지 않고 요청에서 헤더 값을 임의로 평가하는 사용자 지정 요청 매핑을 수행하려면 어떻게해야합니까?

이 SO 답변 에서 설명한 것처럼 실제로는 동일 @RequestMapping하고 다른 주석을 사용하여 런타임 중에 발생하는 실제 라우팅 중에 구별 할 수 있습니다 . 이렇게하려면 다음을 수행해야합니다.

  1. 새 주석을 만듭니다 VersionRange.
  2. 을 구현합니다 RequestCondition<VersionRange>. 가장 일치하는 알고리즘과 같은 것이 있으므로 다른 VersionRange값으로 주석이 달린 메서드 가 현재 요청에 대해 더 나은 일치를 제공 하는지 확인해야합니다 .
  3. 구현 VersionRangeRequestMappingHandlerMapping(포스트에 설명 된대로 주석과 요구 조건에 따라 @RequestMapping 사용자 지정 속성을 구현하는 방법 ).
  4. VersionRangeRequestMappingHandlerMapping기본값을 사용하기 전에 평가하도록 spring을 구성하십시오 RequestMappingHandlerMapping(예 : 순서를 0으로 설정).

이것은 Spring 컴포넌트의 해키 교체가 필요하지 않지만 Spring 구성 및 확장 메커니즘을 사용하므로 Spring 버전을 업데이트하더라도 작동합니다 (새 버전이 이러한 메커니즘을 지원하는 한).


답변 xwoker로 귀하의 의견을 추가해 주셔서 감사합니다. 지금까지 최고입니다. 언급 한 링크를 기반으로 솔루션을 구현했으며 그다지 나쁘지 않습니다. 가장 큰 문제는 Spring의 새 버전으로 업그레이드 할 때 나타납니다 mvc:annotation-driven. 이면의 논리에 대한 변경 사항을 확인해야하기 때문입니다 . 바라건대 Spring은 mvc:annotation-driven커스텀 조건을 정의 할 수 있는 버전을 제공 할 것이다 .
아우 구스토

@Augusto, 반년 후,이게 당신에게 어떻게 도움이 되었습니까? 또한 궁금합니다. 실제로 메서드별로 버전을 관리하고 있습니까? 이 시점에서 클래스 별 / 컨트롤러 별 수준 세분화로 버전을 지정하는 것이 더 명확하지 않은지 궁금합니다.
Sander Verhagen 2014-06-23

1
@SanderVerhagen은 작동하지만 메서드 나 컨트롤러별로가 아닌 전체 API를 버전 화합니다 (API는 비즈니스의 한 측면에 초점을 맞추기 때문에 매우 작습니다). 리소스별로 다른 버전을 사용하고 URL에 지정하는 상당히 큰 프로젝트가 있습니다 (따라서 / v1 / sessions에 엔드 포인트를, / v4 / orders와 같이 완전히 다른 버전에 다른 리소스를 가질 수 있습니다). ... 조금 더 유연하지만 클라이언트가 각 엔드 포인트의 어떤 버전을 호출해야하는지 알기 위해 더 많은 압력을가합니다.
Augusto

1
불행히도 이것은 WebMvcConfigurationSupport를 확장 할 때 많은 자동 구성이 꺼져 있기 때문에 Swagger에서는 잘 작동하지 않습니다.
Rick

이 솔루션을 시도했지만 실제로 2.3.2.RELEASE에서 작동하지 않습니다. 보여줄 예제 프로젝트가 있습니까?
Patrick

54

방금 맞춤형 솔루션을 만들었습니다. 클래스 내부의 주석 @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");
    }
}

4
나는 "1.2"와 같은 버전을 허용하는 문자열 []에 지능 [] 변경, 나는 "최신"와 같은 키워드를 처리 할 수 있도록
Maelig

3
예, 꽤 합리적입니다. 향후 프로젝트에서는 몇 가지 이유로 다른 방법을 사용하겠습니다. 1. URL은 리소스를 나타냅니다. /v1/aResource/v2/aResource다른 자원처럼, 하지만 이 같은 자원의 단지 다른 표현입니다! 2. HTTP 헤더를 사용하면 좋아 보이는, 하지만 URL이 헤더가 포함되어 있지 않기 때문에 당신은 URL 사람을 줄 수 없다. 3. URL 매개 변수 사용, 즉 /aResource?v=2.1(btw : Google이 버전 관리를 수행하는 방식입니다). ...옵션 2 또는 3을 사용할지 아직 확실하지 않지만 위에서 언급 한 이유로 1을 다시 사용하지 않을 것 입니다.
Benjamin M

5
당신이 당신의 자신을 주입 할 경우 RequestMappingHandlerMapping당신에 WebMvcConfiguration, 당신은 덮어 쓰기한다 createRequestMappingHandlerMapping대신 requestMappingHandlerMapping! 그렇지 않으면 당신은 이상한 문제가 발생합니다 (I 갑자기 때문에 비공개 회의의 최대 절전 모드 초기화 지연에 문제가 있었다)
스턱 스넷

1
접근 방식은 좋아 보이지만 junti 테스트 케이스 (SpringRunner)에서는 작동하지 않는 것 같습니다. 테스트 케이스에 대한 접근 방식이있을 가능성이 있습니다.
JDev

1
이 작업을 수행하는 방법이 WebMvcConfigurationSupport 있습니다 DelegatingWebMvcConfiguration. 확장하지 말고 확장하십시오 . 이것은 나를 위해 일했습니다 ( stackoverflow.com/questions/22267191/… 참조 )
SeB.Fr

17

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 / 코드 생성 라이브러리 사용)보고 모든 매개 변수를 전달하여 호출합니다.


14

완벽 하게 처리하는 솔루션을 구현했습니다.나머지 버전 관리 문제를 .

일반 말하기 나머지 버전 관리에는 세 가지 주요 접근 방식이 있습니다.

  • 클라이언트가 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

솔루션 구현 : https://github.com/mspapant/restVersioningExample/


9

@RequestMapping주석은 지원 headers이 일치하는 요청을 좁힐 수 있습니다 요소. 특히 Accept여기 에서 헤더를 사용할 수 있습니다 .

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

이것은 범위를 직접 처리하지 않기 때문에 정확히 설명하는 것은 아니지만 요소는 * 와일드 카드와! =를 지원합니다. 따라서 최소한 모든 버전이 해당 엔드 포인트를 지원하는 경우 또는 특정 주 버전의 모든 부 버전 (예 : 1. *)을 지원하는 경우 와일드 카드를 사용하는 것은 피할 수 있습니다.

이전에이 요소를 실제로 사용하지 않았다고 생각합니다 (기억하지 않는 경우).

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


2
나는 그것에 대해 알고 있지만 언급했듯이 각 버전에서 변경되지 않은 경우에도 모든 컨트롤러로 이동하여 버전을 추가해야합니다. 언급 한 범위는 예를 들어 application/*유형의 일부가 아닌 전체 유형에서만 작동 합니다. 예를 들어 다음은 Spring에서 유효하지 않습니다 "Accept=application/vnd.company.app-1.*+json". 이것은 봄 수업의 MediaType작동 방식과 관련이 있습니다
Augusto

@Augusto 당신은 반드시 이것을 할 필요가 없습니다. 이 접근 방식을 사용하면 "API"가 아니라 "Endpoint"의 버전을 관리합니다. 각 엔드 포인트는 다른 버전을 가질 수 있습니다. 저에게는 API 버전에 비해 API 버전을 만드는 가장 쉬운 방법 입니다. Swagger는 설치 가 더 간단합니다 . 이 전략을 콘텐츠 협상을 통한 버전 관리라고합니다.
Dherik

3

모델 버전 관리에 상속을 사용하는 것은 어떻습니까? 그것이 내 프로젝트에서 사용하고 있으며 특별한 스프링 구성이 필요하지 않으며 내가 원하는 것을 정확하게 얻습니다.

@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로 메서드를 덮어 쓸 수있는 기능을 제공합니다. 또한 버전 전환 논리로 소스 코드를 복잡하게 만들 필요가 없습니다. 버전에서 엔드 포인트를 코딩하지 않으면 기본적으로 이전 버전을 가져옵니다.

다른 사람들이하는 일에 비해 훨씬 쉬운 것 같습니다. 내가 놓친 것이 있습니까?


1
코드 공유를위한 +1. 그러나 상속은 밀접하게 연결됩니다. 대신. 컨트롤러 (Test1 및 Test2)는 논리 구현없이 통과해야합니다. 모든 로직은 서비스 클래스 someService에 있어야합니다. 이 경우, 단지 다른 컨트롤러에서 상속 간단한 구성과 결코 사용
댄 휴 넥스

1
@ dan-hunex Ceekay가 상속을 사용하여 API의 다른 버전을 관리하는 것 같습니다. 상속을 제거하면 해결책은 무엇입니까? 그리고이 예에서 왜 밀접한 커플이 문제가 되는가? 내 관점에서 Test2는 (같은 역할과 동일한 책임을 가진) 개선이기 때문에 Test1을 확장합니다.
jeremieca

2

이미 다음과 같이 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를 설정하는 것도 매우 쉽습니다. 자세한 내용은이 답변 을 참조하십시오.


1

생산물에서는 부정을 가질 수 있습니다. 그래서 method1의 경우produces="!...1.7" method2는 긍정을 가지고 있습니다.

생산물도 ​​배열이므로 method1에 대해 produces={"...1.6","!...1.7","...1.8"}etc (1.7을 제외한 모두 허용) 라고 말할 수 있습니다.

물론 당신이 염두에 둔 범위만큼 이상적이지는 않지만 이것이 당신의 시스템에서 흔하지 않은 경우 다른 사용자 정의보다 유지하기가 더 쉽다고 생각합니다. 행운을 빕니다!


Codealsa 감사합니다. 유지 관리가 쉽고 버전을 변경해야 할 때마다 각 엔드 포인트를 업데이트 할 필요가없는 방법을 찾으려고합니다.
Augusto

0

AOP를 사용할 수 있습니다.

/**/public_api/*이 메소드에서 모든 및이 메소드 를 수신하는 요청 맵핑 이 아무 작업도 수행하지 않는 것을 고려하십시오 .

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

@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


}

유일한 제약은 모든 것이 동일한 컨트롤러에 있어야한다는 것입니다.

AOP 구성의 경우 http://www.mkyong.com/spring/spring-aop-examples-advice/를 살펴보십시오 .


감사합니다 hevi, Spring은 이미 AOP를 사용하지 않고 호출 할 메소드를 선택하기 때문에 더 "봄"친화적 인 방법을 찾고 있습니다. 나는 AOP가 내가 피하고 싶은 새로운 수준의 코드 복잡성을 추가한다고 생각합니다.
Augusto

@Augusto, Spring은 훌륭한 AOP 지원을 제공합니다. 당신은 그것을 시도해야합니다. :)
Konstantin Yovkov
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.