web-dev-qa-db-ja.com

SpringでREST AP​​Iバージョン管理を管理する方法

Spring 3.2.xを使用してREST AP​​Iバージョンを管理する方法を探していましたが、メンテナンスが簡単なものは見つかりませんでした。最初に私が抱えている問題を説明し、次に解決策を説明します...しかし、ここで車輪を再発明しているのではないかと思います。

Acceptヘッダーに基づいてバージョンを管理します。たとえば、リクエストにAcceptヘッダーapplication/vnd.company.app-1.1+jsonがある場合、spring MVCがこのバージョンを処理するメソッドに転送するようにします。また、APIのすべてのメソッドが同じリリースで変更されるわけではないので、各コントローラーに移動して、バージョン間で変更されていないハンドラーについては何も変更したくありません。また、Springはどのメソッドを呼び出すかを既に検出しているため、コントローラー自体で(サービスロケーターを使用して)使用するバージョンを把握するロジックも必要ありません。

そのため、バージョン1.0でAPIが1.8になり、バージョン1.0でハンドラが導入され、v1.7で変更された場合、次の方法でこれを処理したいと思います。コードがコントローラー内にあり、ヘッダーからバージョンを抽出できるコードがあると想像してください。 (以下は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;
}

2つのメソッドには同じRequestMapping注釈があり、Springはロードに失敗するため、これはSpringでは不可能です。その考え方は、VersionRangeアノテーションがオープンまたはクローズのバージョン範囲を定義できるということです。最初の方法はバージョン1.0〜1.6で有効で、2番目の方法はバージョン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のコアクラス(RequestMappingInfoHandlerMappingRequestMappingHandlerMapping、およびRequestMappingInfo)を置き換える必要がありましたが、それは好きではありませんが、なぜなら、私が新しいバージョンのSpringにアップグレードすることを決定するたびに、余分な作業が必要になるからです。

私はどんな考えでも感謝します...そして、特に、これをより簡単で維持しやすい方法で行うための提案。


編集

バウンティを追加します。賞金を得るには、コントローラー自体にこのロジックを含めることを提案せずに、上記の質問に答えてください。 Springには、どのコントローラーメソッドを呼び出すかを選択するためのロジックが既にたくさんあり、私はそれに便乗したいと思います。


編集2

Githubで元のPOC(いくつかの改良を加えた)を共有しました: https://github.com/augusto/restVersioning

103
Augusto

下位互換性のある変更を行うことでバージョン管理を回避できるかどうかにかかわらず(一部の企業ガイドラインに縛られている場合や、APIクライアントがバグのある方法で実装されており、そうでなくても壊れる場合は常に可能とは限りません)、抽象化された要件は興味深いものです1:

メソッド本体で評価を行うことなく、リクエストからのヘッダー値の任意の評価を行うカスタムリクエストマッピングを行うにはどうすればよいですか?

this SO answer で説明されているように、同じ@RequestMappingを実際に使用し、異なるアノテーションを使用して、実行時に発生する実際のルーティングを区別できます。そのためには、次のことを行う必要があります。

  1. 新しい注釈VersionRangeを作成します。
  2. RequestCondition<VersionRange>を実装します。ベストマッチアルゴリズムのようなものがあるため、他のVersionRange値で注釈されたメソッドが現在のリクエストによりよく一致するかどうかを確認する必要があります。
  3. 注釈とリクエスト条件に基づいてVersionRangeRequestMappingHandlerMappingを実装します(投稿 @ RequestMappingカスタムプロパティの実装方法 で説明されています)。
  4. デフォルトのVersionRangeRequestMappingHandlerMappingを使用する前にRequestMappingHandlerMappingを評価するようにスプリングを設定します(たとえば、順序を0に設定して)。

これには、Springコンポーネントのハック的な置き換えは必要ありませんが、Springバージョンを更新しても機能するはずです(新しいバージョンがこれらのメカニズムをサポートしている限り)。

58
xwoker

カスタムソリューションを作成しました。 @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");
    }
}
46
Benjamin M

URLでは@RequestMappingがパターンとパスパラメーターをサポートしているため、バージョン管理にURLを使用することをお勧めします。この形式はregexpで指定できます。

また、クライアントのアップグレード(コメントで言及した)を処理するには、「最新」などのエイリアスを使用できます。または、最新バージョンを使用するバージョン管理されていないバージョンの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;
    }
}

最後のアプローチに基づいて、実際に必要なものを実装できます。

たとえば、バージョン処理を備えたメソッドスタブのみを含むコントローラーを使用できます。

その処理では、いくつかのSpringサービス/コンポーネントまたは同じ名前/署名と必要な@VersionRangeを持つメソッドの同じクラスで(リフレクション/ AOP /コード生成ライブラリを使用して)見て、すべてのパラメータを渡して呼び出します。

16
elusive-code

残りのバージョン管理の問題を完全に処理するソリューションを実装しました。

一般的に言えば、残りのバージョン管理には3つの主要なアプローチがあります。

  • Pathベースのアプローチ。クライアントはURLでバージョンを定義します。

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
    
  • Content-Typeヘッダー。クライアントはAcceptヘッダーでバージョンを定義します。

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
    
  • カスタムヘッダー。クライアントはカスタムヘッダーでバージョンを定義します。

problemwithfirstアプローチは、バージョンを変更する場合、たとえばv1-> v2からコピーする必要があることです-v2パスに変更されていないv1リソースを貼り付けます

problemsecondアプローチでは、 http://swagger.io/ のようないくつかのツールが同じパスで異なるContent-Typeの操作を区別することはできません(問題を確認してください 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";
    }
 }

requirementは、ユーザーリソースに対してv1を要求した場合、 "User V1 "応答、それ以外の場合、v2v3などを要求する場合、 "User V2"応答。

enter image description here

これを春に実装するには、デフォルトの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そして、リソースの最新バージョンが見つかるまで1つ。

この実装の利点を確認するために、ユーザーと会社の2つのリソースがあるとします。

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

ここでのbest部分は、このソリューションでは、クライアントがv1からユーザー情報を取得し、v2から会社情報を取得することです必要なしユーザーv2から新しい(同じ)エンドポイントを作成します!

残りのドキュメント前に言ったように、URLベースのバージョン管理アプローチを選択する理由は、swaggerのような一部のツールは、同じURLで異なるコンテンツタイプのエンドポイントを別々にドキュメント化しないからです。このソリューションでは、URLが異なるため、両方のエンドポイントが表示されます。

enter image description here

GIT

ソリューションの実装: https://github.com/mspapant/restVersioningExample/

11
mspapant

@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

8
Willie Wheeler

継承を使用してバージョン管理をモデル化するのはどうですか?それが私のプロジェクトで使用しているものであり、特別なスプリング設定を必要とせず、まさに私が望むものを取得します。

@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に上書きすることができます。また、バージョン切り替えロジックを使用してソースコードを複雑にする必要もありません。バージョンでエンドポイントをコーディングしない場合、デフォルトで以前のバージョンを取得します。

他の人がやっていることと比較して、これはずっと簡単に思えます。私が見逃しているものはありますか?

3
Ceekay

私はすでにURIバージョン管理を使用してAPIをバージョン管理しようとしました:

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

しかし、この作業を行うにはいくつかの課題があります。異なるバージョンでコードをどのように整理するのでしょうか? 2つ(またはそれ以上)のバージョンを同時に管理するにはどうすればよいですか?一部のバージョンを削除するとどのような影響がありますか?

私が見つけた最良の代替案は、A​​PI全体のバージョンではなく、各エンドポイントのバージョンを制御することです。このパターンの名前は Acceptヘッダーを使用したバージョン管理 または コンテンツネゴシエーションによるバージョン管理

このアプローチにより、API全体をバージョニングする代わりに単一のリソース表現をバージョニングすることができ、バージョニングをよりきめ細かく制御できます。また、新しいバージョンを作成するときにアプリケーション全体をフォークする必要がないため、コードベースのフットプリントが小さくなります。このアプローチのもう1つの利点は、URIパスを介したバージョン管理によって導入されたURIルーティングルールを実装する必要がないことです。

春の実装

最初に、基本的なproduces属性を持つControllerを作成します。これは、クラス内の各エンドポイントにデフォルトで適用されます。

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

}

その後、注文を作成するためのエンドポイントの2つのバージョンがあるシナリオを作成します。

@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
Dherik

プロデュースでは否定することができます。そのため、method1の場合はproduces="!...1.7"と言い、method2の場合は肯定的です。

また、producesは配列であるため、method1ではproduces={"...1.6","!...1.7","...1.8"}などと言うことができます(1.7を除くすべてを受け入れます)

もちろん、あなたが念頭に置いている範囲ほど理想的ではありませんが、これがあなたのシステムで珍しいものであれば、私は他のカスタムのものよりも維持しやすいと思います。幸運を!

1
codesalsa

傍受を中心に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/ をご覧ください。

0
hevi