Yay ile REST API versiyonlama nasıl yönetilir?


119

Spring 3.2.x kullanarak bir REST API sürümünün nasıl yönetileceğini araştırıyorum, ancak bakımı kolay bir şey bulamadım. Önce sahip olduğum sorunu açıklayacağım, sonra bir çözümü ... ama burada tekerleği yeniden mi icat ettiğimi merak ediyorum.

Sürümü Accept başlığına göre yönetmek istiyorum ve örneğin bir istek Accept başlığına sahipse, application/vnd.company.app-1.1+jsonSpring MVC'nin bunu bu sürümü işleyen yönteme iletmesini istiyorum. Ve bir API'deki tüm yöntemler aynı sürümde değişmediğinden, denetleyicilerimin her birine gitmek ve sürümler arasında değişmeyen bir işleyici için herhangi bir değişiklik yapmak istemiyorum. Ayrıca, Spring hangi yöntemi çağıracağını zaten keşfettiği için, denetleyicide hangi sürümün kullanılacağını (hizmet bulucuları kullanarak) anlamaya yönelik mantığa sahip olmak istemiyorum.

Dolayısıyla, 1.0 sürümünden 1.8'e bir işleyicinin 1.0 sürümünde tanıtıldığı ve v1.7'de değiştirildiği bir API'yi aldım, bunu aşağıdaki şekilde ele almak istiyorum. Kodun bir denetleyicinin içinde olduğunu ve sürümü başlıktan çıkarabilen bazı kodlar olduğunu hayal edin. (Aşağıdakiler İlkbaharda geçersizdir)

@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 yöntem aynı RequestMappingaçıklamaya sahip olduğundan ve Yay yüklenemediğinden ilkbaharda bu mümkün değildir . Buradaki fikir, VersionRangeek açıklamanın açık veya kapalı bir sürüm aralığını tanımlayabilmesidir. İlk yöntem 1.0'dan 1.6'ya kadar olan sürümler için geçerlidir, ikincisi ise 1.7 sürümü için geçerlidir (en son sürüm 1.8 dahil). Birisi 99.99 sürümünü geçmeye karar verirse bu yaklaşımın bozulacağını biliyorum, ancak bu benimle yaşamaya uygun bir şey.

Şimdi, baharın nasıl çalıştığına dair ciddi bir yeniden çalışma olmadan yukarıdakiler mümkün olmadığından, işleyicilerin isteklere uyma şeklini, özellikle de kendi isteklerimi yazmayı ProducesRequestConditionve orada sürüm aralığına sahip olmayı düşünüyordum. Örneğin

Kod:

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

Bu şekilde, açıklamanın üretim kısmında tanımlanmış kapalı veya açık sürüm aralıklarına sahip olabilirim. Şu anda bu çözüm üzerinde çalışıyorum, hala bazı çekirdek Spring MVC sınıflarını ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingve RequestMappingInfo) değiştirmek zorunda kalmam sorunuyla, hoşuma gitmiyor, çünkü bu, daha yeni bir sürümüne yükseltmeye karar verdiğimde fazladan iş anlamına geliyor. yay.

Herhangi bir düşünceyi ve özellikle bunu daha basit, bakımı daha kolay bir şekilde yapma önerisini takdir ediyorum.


Düzenle

Bir ödül eklemek. Ödülü almak için, lütfen yukarıdaki soruyu, bu mantığın kontrol cihazının kendisinde olmasını önermeden cevaplayın. Spring, hangi denetleyici yönteminin çağrılacağını seçmek için zaten çok fazla mantığa sahip ve bunun üzerine piggyback yapmak istiyorum.


Düzenle 2

Orijinal POC'yi (bazı iyileştirmelerle) github'da paylaştım: https://github.com/augusto/restVersioning



1
@flup Yorumunuzu anlamıyorum. Bu sadece başlıkları kullanabileceğinizi söylüyor ve dediğim gibi, yayının kutudan sağladığı şey, sürekli güncellenen API'leri desteklemek için yeterli değil. Daha da kötüsü, bu yanıttaki bağlantı URL'deki sürümü kullanıyor.
Augusto

Belki tam olarak aradığınız şey olmayabilir, ancak Spring 3.2, RequestMapping üzerinde bir "üretir" parametresini destekler. Bir uyarı, sürüm listesinin açık olması gerektiğidir. Ör. produces={"application/json-1.0", "application/json-1.1"}, Vb
bimsapi

1
API'lerimizin çeşitli sürümlerini desteklememiz gerekir, bu farklılıklar genellikle bazı istemcilerden gelen bazı çağrıları uyumsuz hale getiren küçük değişikliklerdir (bazı uç noktaların uyumsuz olduğu 4 küçük sürümü desteklememiz garip olmayacaktır). URL’ye koyma önerisine minnettarım, ancak URL’de sürümü olan birkaç uygulamamız olduğu için bunun yanlış yönde bir adım olduğunu biliyoruz ve sürümü.
Augusto

1
@Augusto, aslında sen de yapmadın. API değişikliklerinizi geriye dönük uyumluluğu bozmayacak şekilde tasarlayın. Sadece bana uyumluluğu bozan değişikliklere örnek verin ve size bu değişiklikleri kesintisiz bir şekilde nasıl yapacağınızı göstereyim.
Alexey Andreev

Yanıtlar:


62

Geriye dönük uyumlu değişiklikler yaparak sürümlemeden kaçınılıp önlenemeyeceğine bakılmaksızın (bu, bazı kurumsal yönergelere bağlı olduğunuzda veya API istemcileriniz hatalı bir şekilde uygulandığında her zaman mümkün olmayabilir ve yapmamaları gerekse bile bozulabilir), soyutlanmış gereksinim ilginçtir bir:

Yöntem gövdesinde değerlendirme yapmadan istekten gelen başlık değerlerinin keyfi değerlendirmelerini yapan özel bir istek eşlemesi nasıl yapabilirim?

Bu SO cevabında açıklandığı gibi, aslında aynı @RequestMappingşeye sahip olabilir ve çalışma zamanında gerçekleşen gerçek yönlendirme sırasında ayırt etmek için farklı bir açıklama kullanabilirsiniz. Bunu yapmak için yapmanız gerekenler:

  1. Yeni bir açıklama oluşturun VersionRange.
  2. Bir RequestCondition<VersionRange>. En iyi eşleşme algoritması gibi bir şeye sahip olacağınız için, diğer VersionRangedeğerlerle açıklanmış yöntemlerin mevcut istek için daha iyi bir eşleşme sağlayıp sağlamadığını kontrol etmeniz gerekecektir .
  3. Bir gerçeklenmemişti VersionRangeRequestMappingHandlerMapping(yayınında açıklandığı gibi açıklama ve istek durumuna göre @ RequestMapping özel özellikler nasıl uygulanır ).
  4. VersionRangeRequestMappingHandlerMappingVarsayılanı kullanmadan önce yayı değerlendirmek için yapılandırın RequestMappingHandlerMapping(örneğin sırasını 0 olarak ayarlayarak).

Bu, Spring bileşenlerinin hile ile değiştirilmesini gerektirmez, ancak Spring konfigürasyonunu ve uzatma mekanizmalarını kullanır, bu nedenle Spring sürümünüzü güncelleseniz bile çalışmalıdır (yeni sürüm bu mekanizmaları desteklediği sürece).


Yorumunuzu yanıt olarak eklediğiniz için teşekkürler xwoker. Şimdiye kadar en iyisi. Çözümü bahsettiğiniz bağlantılara göre uyguladım ve o kadar da kötü değil. Arkadaki mantıkta herhangi bir değişikliği kontrol etmeniz gerekeceğinden, Spring'in yeni bir sürümüne yükseltme yapılırken en büyük sorun ortaya çıkacaktır mvc:annotation-driven. Umarım Spring, mvc:annotation-drivenözel koşulların tanımlanabileceği bir versiyon sağlayacaktır .
Ağustos

@Augusto, yarım yıl sonra, bu senin için nasıl gidiyor? Ayrıca, merak ediyorum, gerçekten yöntem bazında sürüm mü yapıyorsunuz? Bu noktada, sınıf başına / denetleyici başına düzey ayrıntı düzeyinde sürümün daha net olup olmayacağını merak ediyorum.
Sander Verhagen

1
@SanderVerhagen çalışıyor, ancak yöntem veya denetleyici başına değil, tüm API'nin sürümünü yapıyoruz (API, işletmenin bir yönüne odaklandığı için oldukça küçüktür). Her kaynak için farklı bir sürüm kullanmayı seçtikleri ve bunu URL'de belirttikleri çok daha büyük bir projemiz var (böylece / v1 / sessions'da bir uç noktaya ve tamamen farklı bir sürümde başka bir kaynağa sahip olabilirsiniz, örneğin / v4 / orders) ... biraz daha esnektir, ancak istemcilerin her bir uç noktayı hangi sürümü arayacaklarını bilmeleri için daha fazla baskı oluşturur.
Augusto

1
Ne yazık ki, WebMvcConfigurationSupport'u genişletirken birçok otomatik yapılandırma kapatıldığı için bu Swagger ile iyi oynamıyor.
Rick

Bu çözümü denedim ama aslında 2.3.2.RELEASE ile çalışmıyor. Gösterecek örnek bir projeniz var mı?
Patrick

54

Az önce özel bir çözüm yarattım. @ApiVersionEk @RequestMappingaçıklamayı, içindeki ek açıklamayla birlikte kullanıyorum@Controller sınıfların notlarla .

Misal:

@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

}

Uygulama:

ApiVersion.java ek açıklaması:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    int[] value();
}

ApiVersionRequestMappingHandlerMapping.java (bu çoğunlukla kopyalayıp yapıştırmaktır 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'a Enjeksiyon:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}

4
"1.2" gibi sürümlere izin vermek için int [] 'yi String [] olarak değiştirdim ve böylece "en yeni" gibi anahtar kelimeleri işleyebiliyorum
Maelig

3
Evet, bu oldukça makul. Gelecekteki projeler için bazı nedenlerden dolayı farklı bir yol izlerim: 1. URL'ler kaynakları temsil eder. /v1/aResourceve /v2/aResourcefarklı kaynaklar gibi görünebilir, ancak bu yalnızca aynı kaynağın farklı bir temsilidir! 2. HTTP üstbilgilerini kullanmak daha iyi görünür, ancak birisine URL veremezsiniz çünkü URL üstbilgiyi içermiyor. 3. Bir URL parametresi kullanmak, yani /aResource?v=2.1(btw: Google'ın sürüm belirleme yöntemi budur). ...Hala seçenek 2 veya 3'ü tercih edip etmeyeceğimden emin değilim , ancak yukarıda belirtilen nedenlerden dolayı 1'i asla kullanmayacağım .
Benjamin M

5
Kendi enjekte etmek istiyorsanız RequestMappingHandlerMappingSİZİN içine WebMvcConfiguration, üzerine yazmak gerekir createRequestMappingHandlerMappingyerine requestMappingHandlerMapping! Aksi takdirde tuhaf problemlerle karşılaşacaksınız (kapalı bir oturum nedeniyle birdenbire Hazırda
Beklemede

1
Yaklaşım iyi görünüyor ama bir şekilde junti test durumlarında (SpringRunner) çalışmıyor gibi görünüyor. Test
senaryoları üzerinde

1
Bu işi yapmanın, uzatmanın WebMvcConfigurationSupport değil uzatmanın bir yolu var DelegatingWebMvcConfiguration. Bu benim için çalıştı (bkz. Stackoverflow.com/questions/22267191/… )
SeB.Fr

17

Yine de sürüm oluşturma için URL'lerin kullanılmasını tavsiye ederim, çünkü URL'lerde @RequestMapping, regexp ile belirtilebilen kalıpları ve yol parametrelerini destekler.

Ve (yorumda bahsettiğiniz) istemci yükseltmelerini işlemek için 'en son' gibi takma adlar kullanabilirsiniz. Veya api'nin en son sürümü kullanan (evet) sürümsüz sürümüne sahip olun.

Ayrıca yol parametrelerini kullanarak herhangi bir karmaşık sürüm işleme mantığını uygulayabilirsiniz ve zaten aralıklara sahip olmak istiyorsanız, çok daha kısa sürede bir şeyler isteyebilirsiniz.

İşte birkaç örnek:

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

Son yaklaşıma dayanarak, aslında istediğiniz gibi bir şey uygulayabilirsiniz.

Örneğin, yalnızca sürüm işlemeli yöntem saplamalarını içeren bir denetleyiciniz olabilir.

Bu işlemde (yansıma / AOP / kod üretme kitaplıklarını kullanarak) bazı yay hizmetlerinde / bileşenlerinde veya aynı ada / imzaya sahip yöntem için aynı sınıfta ve gerekli @ Sürüm Aralığı'na bakarsınız ve tüm parametreleri iletmeyi çağırırsınız.


14

MÜKEMMEL bir şekilde işleyen bir çözüm uyguladımGeri kalan sürüm oluşturma ile ilgili sorunu şekilde .

Genel Konuşma, dinlenme versiyonlaması için 3 ana yaklaşım vardır:

  • Yol istemci URL'de sürümü tanımladığı yargıyı, tabanlı:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • İstemcinin Accept başlığındaki sürümü tanımladığı Content-Type başlığı:

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Özel Üstbilgi istemci özel bir başlıkta versiyonunu tanımladığı,.

Sorun ile ilk yaklaşımda Sürüm değiştirirseniz en v1'den diyelim ki -> v2 muhtemelen v2 yoluna değişmedi v1 kaynaklarını kopyalayıp yapıştırmak gerekir

Sorun ile ikinci yaklaşımda olduğu gibi bazı araçlar ise http://swagger.io/ aynı yolu ancak farklı Content-Type (çekin düzenlenme ile operasyonlar arasında can belirgin değil https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

Çözüm

Dinlenme dokümantasyon araçlarıyla çok çalıştığım için, ilk yaklaşımı kullanmayı tercih ediyorum. Benim çözümüm sorunu ilk yaklaşımla ele alıyor, bu nedenle uç noktayı yeni sürüme kopyalayıp yapıştırmanıza gerek kalmıyor.

Kullanıcı denetleyicisi için v1 ve v2 sürümlerimiz olduğunu varsayalım:

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

Gereklilik i istemek durumunda olduğunu v1 i almak zorunda kullanıcı kaynak için "Kullanıcı V1" i istemek aksi takdirde repsonse v2 , v3 ve bu yüzden almak zorunda üzerinde "Kullanıcı V2" yanıtını.

görüntü açıklamasını buraya girin

Bunu ilkbaharda uygulamak için, varsayılan RequestMappingHandlerMapping davranışını geçersiz kılmamız gerekir :

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

}

Uygulama, URL'deki sürümü okur ve ilkbahardan URL'yi çözmesini ister. Bu URL'nin mevcut olmaması durumunda (örneğin, istemci v3'ü talep etti ), sonra v2 ile deneriz ve böylece kaynak için en son sürümü bulana kadar .

Bu uygulamanın faydalarını görmek için iki kaynağımız olduğunu varsayalım: Kullanıcı ve Şirket:

http://localhost:9001/api/v{version}/user
http://localhost:9001/api/v{version}/company

Diyelim ki şirket "sözleşmesinde" müşteriyi bozan bir değişiklik yaptık. Bu yüzden, 'yi uygularız http://localhost:9001/api/v2/companyve istemciden v1 yerine v2'ye geçmesini isteriz.

Yani müşteriden gelen yeni istekler:

http://localhost:9001/api/v2/user
http://localhost:9001/api/v2/company

onun yerine:

http://localhost:9001/api/v1/user
http://localhost:9001/api/v1/company

Buradaki en iyi bölüm, bu çözümle müşterinin kullanıcı bilgilerini v1'den ve şirket bilgilerini v2'den yeni (aynı) bir uç nokta oluşturmaya gerek kalmadan almasıdır!

Kalan Belgeler Daha önce de söylediğim gibi, URL tabanlı sürüm oluşturma yaklaşımını seçmemin nedeni, swagger gibi bazı araçların aynı URL'ye sahip ancak farklı içerik türüne sahip uç noktaları farklı şekilde belgelememesidir. Bu çözümde, farklı URL’lere sahip oldukları için her iki uç nokta da görüntülenir:

görüntü açıklamasını buraya girin

GYTE

Çözüm uygulaması: https://github.com/mspapant/restVersioningExample/


9

@RequestMappingEk açıklama, destekler headersEğer eşleştirme isteklerini daraltmak için izin verir elemanı. Özellikle Acceptbaşlığı burada kullanabilirsiniz .

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

Bu tam olarak açıkladığınız şey değil, çünkü aralıkları doğrudan işlemiyor, ancak öğe * joker karakterini ve! = 'İ destekliyor. Bu nedenle, en azından tüm sürümlerin söz konusu uç noktayı desteklediği durumlarda veya belirli bir ana sürümün tüm küçük sürümlerini (örneğin 1. *) desteklediği durumlarda joker karakter kullanmaktan kurtulabilirsiniz.

Bu öğeyi daha önce kullandığımı sanmıyorum (eğer varsa hatırlamıyorsam), bu yüzden sadece dokümantasyondan çıkıyorum

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


2
Bunu biliyorum, ancak sizin de belirttiğiniz gibi, her sürümde tüm denetleyicilerime gitmem ve değişmemiş olsalar bile bir sürüm eklemem gerekiyor. Bahsettiğiniz aralık, örneğin application/*türün bazı kısımlarında değil , yalnızca tam tür için çalışır . Örneğin aşağıdaki baharda geçersizdir "Accept=application/vnd.company.app-1.*+json". Bu, bahar sınıfının nasıl MediaTypeçalıştığıyla ilgili
Ağustos 13

@Augusto bunu yapmana gerek yok. Bu yaklaşımla, "API" değil, "Uç Nokta" sürümünü oluşturursunuz. Her uç noktanın farklı bir sürümü olabilir. Benim için , API sürümüne kıyasla API'yi sürümlendirmenin en kolay yolu . Swagger'ın kurulumu da daha kolaydır . Bu stratejiye içerik pazarlığı yoluyla Sürüm oluşturma adı verilir.
Dherik

3

Sürüm oluşturmayı modellemek için mirası kullanmaya ne dersiniz? Projemde kullandığım şey bu ve özel bir yay konfigürasyonu gerektirmiyor ve bana tam olarak istediğimi veriyor.

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

Bu kurulum, kodun çok az kopyalanmasına ve çok az işle api'nin yeni sürümlerine yöntemlerin üzerine yazma becerisine izin verir. Ayrıca, kaynak kodunuzu sürüm değiştirme mantığıyla karmaşıklaştırma ihtiyacını da ortadan kaldırır. Bir sürümde bir uç noktayı kodlamazsanız, varsayılan olarak önceki sürümü alır.

Başkalarının yaptıklarına kıyasla bu çok daha kolay görünüyor. Eksik bir şey mi var?


1
Kodu paylaşmak için +1. Bununla birlikte, miras onu sıkıca birleştirir. Yerine. Denetleyiciler (Test1 ve Test2) sadece bir geçiş olmalıdır ... mantık uygulaması yok. Her şeyin mantığı hizmet sınıfında olmalı, someService. Bu durumda, sadece basit bir kompozisyon kullanın ve diğer denetleyiciden asla miras
almayın

1
@ dan-hunex Görünüşe göre Ceekay, api'nin farklı sürümlerini yönetmek için kalıtımı kullanıyor. Mirası kaldırırsanız çözüm nedir? Ve neden bu örnekte sıkı çift bir problemdir? Benim bakış açıma göre, Test2 Test1'i genişletiyor çünkü onun bir iyileştirmesi (aynı role ve aynı sorumluluklara sahip), değil mi?
jeremieca

2

URI Sürüm Oluşturmayı kullanarak API'mi şu şekilde sürümlemeye çalıştım :

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

Ancak bunun çalışmasını sağlamaya çalışırken bazı zorluklar var: kodunuzu farklı sürümlerle nasıl düzenleyin? Aynı anda iki (veya daha fazla) sürümü nasıl yönetirsiniz? Bazı sürümleri kaldırmanın etkisi nedir?

Bulduğum en iyi alternatif, tüm API'nin sürümü değil, her uç noktada sürümü kontrol etmekti . Bu kalıp, Accept üstbilgisi kullanılarak Sürüm Oluşturma veya içerik pazarlığı yoluyla Sürüm Oluşturma olarak adlandırılır :

Bu yaklaşım, tüm API'yi versiyonlamak yerine tek bir kaynak gösterimini versiyonlamamıza izin verir, bu da bize versiyonlama üzerinde daha ayrıntılı bir kontrol sağlar. Ayrıca, yeni bir sürüm oluştururken tüm uygulamayı çatallamak zorunda olmadığımız için kod tabanında daha küçük bir alan oluşturur. Bu yaklaşımın bir başka avantajı, URI yolu üzerinden sürüm oluşturma ile sunulan URI yönlendirme kurallarının uygulanmasını gerektirmemesidir.

İlkbaharda Uygulama

İlk olarak, sınıf içindeki her uç nokta için varsayılan olarak geçerli olacak temel bir üretim özniteliğine sahip bir Denetleyici oluşturursunuz.

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

}

Bundan sonra, bir sipariş oluşturmak için bir uç noktanın iki sürümüne sahip olduğunuz olası bir senaryo oluşturun:

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

Bitti! İstediğiniz Http Başlık sürümünü kullanarak her uç noktayı çağırmanız yeterlidir :

Content-Type: application/vnd.company.etc.v1+json

Veya ikinci sürümü çağırmak için:

Content-Type: application/vnd.company.etc.v2+json

Endişeleriniz hakkında:

Ve bir API'deki tüm yöntemler aynı sürümde değişmediğinden, denetleyicilerimin her birine gidip sürümler arasında değişmeyen bir işleyici için herhangi bir değişiklik yapmak istemiyorum

Açıklandığı gibi, bu strateji her Denetleyiciyi ve uç noktayı gerçek sürümüyle korur. Yalnızca değişikliklere sahip olan ve yeni bir sürüme ihtiyaç duyan uç noktayı değiştirirsiniz.

Ve Swagger?

Swagger'ı farklı sürümlerle kurmak da bu stratejiyi kullanmak çok kolaydır. Daha fazla ayrıntı için bu yanıta bakın.


1

Üretimlerde olumsuzluğa sahip olabilirsiniz. Yani method1 için söyleproduces="!...1.7" ve method2'de pozitif var.

Üretimler ayrıca bir dizidir, bu nedenle yöntem1 için produces={"...1.6","!...1.7","...1.8"}etc diyebilirsiniz (1.7 hariç hepsini kabul edin)

Elbette, aklınızdaki aralıklar kadar ideal değil, ancak bu, sisteminizde yaygın olmayan bir şeyse, diğer özel şeylere göre bakımı daha kolay olduğunu düşünüyorum. İyi şanslar!


Codealsa'ya teşekkürler, sürüme her çarpmamız gerektiğinde bakımı kolay ve bazılarının her uç noktayı güncellemesini gerektirmeyen bir yol bulmaya çalışıyorum.
Augusto

0

Yakalama etrafında AOP kullanabilirsiniz

Hepsini alan /**/public_api/*ve bu yöntemde hiçbir şey yapmayan bir istek eşlemesine sahip olmayı düşünün ;

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

Sonra

@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


}

Tek kısıtlama, hepsinin aynı kontrolörde olması gerektiğidir.

AOP yapılandırması için http://www.mkyong.com/spring/spring-aop-examples-advice/ adresine bakın.


Teşekkürler hevi, Spring AOP kullanmadan hangi yöntemi arayacağını zaten seçtiği için bunu yapmanın daha "bahar" dostu bir yolunu arıyorum. Benim görüşüme göre AOP, kaçınmak istediğim yeni bir kod karmaşıklığı düzeyi ekler.
Augusto

@Augusto, Spring'in harika bir AOP desteği var. Denemelisin. :)
Konstantin Yovkov
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.