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;
}

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には、呼び出すコントローラーメソッドを選択するための多くのロジックが既にあります。


編集2

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



1
@flupコメントが理解できません。これは、ヘッダーを使用できることを言っているだけで、先ほど言ったように、Springがそのまま提供するものでは、常に更新されるAPIをサポートするには不十分です。さらに悪いことに、その回答のリンクはURLのバージョンを使用しています。
アウグスト

たぶんあなたが探しているものではないかもしれませんが、Spring 3.2はRequestMappingの "produces"パラメータをサポートしています。注意点の1つは、バージョンリストを明示的にする必要があることです。例えば、produces={"application/json-1.0", "application/json-1.1"}など
bimsapi

1
APIのいくつかのバージョンをサポートする必要があります。これらの違いは通常、一部のクライアントからの一部の呼び出しを互換性のないものにするマイナーな変更です(4つのマイナーバージョンをサポートする必要があり、一部のエンドポイントが互換性がない場合は不思議ではありません)。私はそれをURLに入れる提案を感謝しますが、URLにバージョンが含まれるアプリがいくつかあり、バンプする必要があるたびに多くの作業が伴うため、これは間違った方向への一歩であることがわかりますバージョン。
アウグスト

1
@Augusto、あなたは実際にはあなたもそうではありません。下位互換性を損なわない方法でAPIの変更を設計するだけです。互換性を損なう変更の例を教えてください。これらの変更を互換性を損なうことなく行う方法を示します。
Alexey Andreev 2013年

回答:


62

下位互換性のある変更を行うことでバージョン管理を回避できるかどうかに関係なく(企業ガイドラインに拘束されている場合や、APIクライアントがバグのある方法で実装されていて、それが不可能な場合でも機能しない場合)、抽象化された要件は興味深いものです。 1:

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

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

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

これは、Springコンポーネントの面倒な置き換えを必要としませんが、Springの構成と拡張メカニズムを使用しているため、Springバージョンを更新しても機能します(新しいバージョンがこれらのメカニズムをサポートしている限り)。


回答xwokerとしてコメントを追加していただきありがとうございます。今までが最高です。私はあなたが言及したリンクに基づいてソリューションを実装しました、そしてそれはそれほど悪くはありません。Springの新しいバージョンにアップグレードすると、背後にあるロジックの変更をチェックする必要があるため、最大の問題が発生しますmvc:annotation-driven。うまくいけば、Springがmvc:annotation-drivenカスタム条件を定義できるバージョンを提供するでしょう。
アウグスト

@Augusto、半年後、これはどのようにうまくいきましたか?また、私は好奇心旺盛ですが、本当にメソッドごとにバージョン管理していますか?この時点で、クラスごと/コントローラごとのレベルの細分性でバージョンを明確にできないかどうか疑問に思っていますか?
Sander Verhagen 2014年

1
@SanderVerhagenは機能しますが、メソッドやコントローラーごとではなく、API全体をバージョン管理します(APIはビジネスの1つの側面に焦点を当てているため、非常に小さいです)。リソースごとに異なるバージョンを使用し、URLで指定することを選択したかなり大きなプロジェクトがあります(/ v1 / sessionsにエンドポイントを設定し、/ v4 / ordersなどの完全に異なるバージョンに別のリソースを設定できます)。 ...少し柔軟ですが、各エンドポイントのどのバージョンを呼び出すかを知るようにクライアントに圧力をかけます。
アウグスト

1
残念ながら、これはSwaggerではうまく機能しません。WebMvcConfigurationSupportを拡張すると、多くの自動構成がオフになるためです。
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」のようなバージョンを許可するためにint []をString []に変更し、「最新」のようなキーワードを処理できるようにしました
Maelig

3
はい、それはかなり合理的です。将来のプロジェクトのために、私はいくつかの理由のために別の道を行くと思います:1. URLはリソースを表します。/v1/aResourceそして、/v2/aResource異なるリソースのように見える、しかし、それは同じリソースのちょうど別の表現です!2. HTTPヘッダを使用すると良く見える、しかし URLはヘッダーが含まれていないので、あなたは、URLに誰かを与えることはできません。3. URLパラメータを使用する/aResource?v=2.1(つまり、Googleがバージョン管理を行う方法です)。...オプション23のどちらを使用するかはまだわかりませんが、上記の理由により、1を再び使用することはありません。
ベンジャミンM

5
あなたがあなた自身を注入したい場合はRequestMappingHandlerMapping、あなたの中にWebMvcConfiguration、あなたは上書きするcreateRequestMappingHandlerMapping代わりにrequestMappingHandlerMapping!そうしないと、奇妙な問題が発生します(セッションが閉じられたため、Hibernateの遅延初期化で突然問題が発生しました)
stuXnet

1
アプローチはよさそうですが、どういうわけかそれはjuntiテストケース(SpringRunner)では機能しないようです。テストケースを操作するアプローチをとっている可能性があります
JDev 2017

1
これを機能させる方法があります。拡張せずにWebMvcConfigurationSupport 拡張しDelegatingWebMvcConfigurationます。これは私にとって
うまくいき

17

@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;
    }
}

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

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

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


14

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

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

  • クライアントが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のリソースをコピー&ペーストする必要があります

2番目のアプローチの問題は、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";
    }
 }

要件は、私が要求した場合であるV1私が取らなければならないユーザーリソースの「ユーザーV1」私が要求しそうでない場合は、repsonseをV2V3をので、私は取らなければならない上、「ユーザー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を使用して、リソースの最新バージョンが見つかるまで試行します。。

この実装の利点を確認するために、ユーザーと会社の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

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

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

ここに画像の説明を入力してください

ギット

ソリューションの実装: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. *など)をサポートしている場合は、ワイルドカードを使用しても問題ありません。

以前にこの要素を実際に使用したことはないと思います(覚えていない場合)、次のURLでドキュメントを作成します。

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」ではなく「エンドポイント」をバージョン管理します。各エンドポイントは異なるバージョンを持つことができます。私にとっては、APIバージョンと比較 して、APIをバージョン管理する最も簡単な方法です。Swaggerのセットアップ簡単です。この戦略は、コンテンツネゴシエーションによるバージョン管理と呼ばれます。
デリック

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。ただし、継承はそれを密結合します。代わりに。Controllers(Test1とTest2)は単なるパススルーでなければなりません...ロジック実装ではありません。すべてのロジックは、サービスクラスsomeServiceにある必要があります。その場合は、単に他のコントローラから継承するシンプルな構成と決して使用
ダン・ヒューネックス

1
@ dan-hunex Ceekayは継承を使用して異なるバージョンのAPIを管理しているようです。継承を削除した場合、解決策は何ですか?そして、なぜこの例では密結合が問題なのでしょうか?私の観点から見ると、Test2はTest1を拡張したものです(同じ役割と同じ責任を持つ)。
jeremieca

2

次のように、URIバージョン管理を使用してAPIをバージョン管理しようとしました。

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

しかし、これを機能させるにはいくつかの課題があります。異なるバージョンでコードをどのように整理するのですか?2つ(またはそれ以上)のバージョンを同時に管理する方法は?一部のバージョンを削除した場合、どのような影響がありますか?

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

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

春の実装

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

@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

農産物では否定することができます。つまり、method1についてはproduces="!...1.7"、method2については肯定的です。

プロデュースも配列なので、method1の場合は次のように言うことができますproduces={"...1.6","!...1.7","...1.8"}(1.7を除くすべてを受け入れます)

もちろん、あなたが考えている範囲ほど理想的ではありませんが、これがシステムで一般的でない場合は、他のカスタムのものよりも維持しやすいと思います。幸運を!


codesalsaに感謝します。保守しやすく、バージョンを上げる必要があるたびに各エンドポイントを更新する必要がない方法を見つけようとしています。
アウグスト

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を使用せずに呼び出すメソッドを選択しているので、これを行うための「Spring」フレンドリーな方法を探しています。私の見解では、AOPは、回避したい新しいレベルのコードの複雑さを追加します。
アウグスト

@ Augusto、SpringはAOPを強力にサポートしています。試してみてください。:)
Konstantin Yovkov
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.