Bagaimana cara mengelola versi REST API dengan spring?

119

Saya telah mencari cara mengelola versi REST API menggunakan Spring 3.2.x, tetapi saya belum menemukan apa pun yang mudah dipelihara. Saya akan menjelaskan terlebih dahulu masalah yang saya miliki, dan kemudian solusinya ... tetapi saya bertanya-tanya apakah saya menemukan kembali roda di sini.

Saya ingin mengelola versi berdasarkan header Terima, dan misalnya jika permintaan memiliki header Terima application/vnd.company.app-1.1+json, saya ingin MVC musim semi untuk meneruskan ini ke metode yang menangani versi ini. Dan karena tidak semua metode dalam API berubah dalam rilis yang sama, saya tidak ingin membuka setiap pengontrol saya dan mengubah apa pun untuk penangan yang tidak berubah antar versi. Saya juga tidak ingin memiliki logika untuk mencari tahu versi mana yang akan digunakan dalam pengontrol itu sendiri (menggunakan pelacak layanan) karena Spring sudah menemukan metode mana yang harus dipanggil.

Jadi mengambil API dengan versi 1.0, ke 1.8 di mana penangan diperkenalkan di versi 1.0 dan dimodifikasi di v1.7, saya ingin menangani ini dengan cara berikut. Bayangkan kode tersebut ada di dalam pengontrol, dan ada beberapa kode yang dapat mengekstrak versi dari header. (Berikut ini tidak valid di 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;
}

Ini tidak mungkin dilakukan pada musim semi karena 2 metode memiliki RequestMappinganotasi yang sama dan Spring gagal dimuat. Idenya adalah bahwa VersionRangeanotasi dapat menentukan rentang versi terbuka atau tertutup. Metode pertama berlaku dari versi 1.0 hingga 1.6, sedangkan yang kedua untuk versi 1.7 dan seterusnya (termasuk versi terbaru 1.8). Saya tahu bahwa pendekatan ini rusak jika seseorang memutuskan untuk mengoper versi 99,99, tetapi itu adalah sesuatu yang tidak masalah bagi saya.

Sekarang, karena hal di atas tidak mungkin dilakukan tanpa pengerjaan ulang yang serius tentang cara kerja pegas, saya berpikir untuk mengutak-atik cara penangan yang cocok dengan permintaan, khususnya untuk menulis sendiri ProducesRequestCondition, dan memiliki rentang versi di sana. Sebagai contoh

Kode:

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

Dengan cara ini, saya dapat memiliki rentang versi tertutup atau terbuka yang ditentukan di bagian produksi anotasi. Saya sedang mengerjakan solusi ini sekarang, dengan masalah bahwa saya masih harus mengganti beberapa kelas inti Spring MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingdan RequestMappingInfo), yang tidak saya sukai, karena itu berarti kerja ekstra setiap kali saya memutuskan untuk meningkatkan ke versi yang lebih baru dari musim semi.

Saya akan menghargai pemikiran apa pun ... dan terutama, saran apa pun untuk melakukan ini dengan cara yang lebih sederhana dan lebih mudah dipertahankan.


Sunting

Menambahkan hadiah. Untuk mendapatkan bounty, harap jawab pertanyaan di atas tanpa menyarankan untuk memiliki logika ini sendiri di controller. Spring sudah memiliki banyak logika untuk memilih metode pengontrol mana yang akan dipanggil, dan saya ingin mendukungnya.


Edit 2

Saya telah membagikan POC asli (dengan beberapa peningkatan) di github: https://github.com/augusto/restVersioning

Augusto
sumber
1
@flup Saya tidak mengerti komentar Anda. Itu hanya mengatakan bahwa Anda dapat menggunakan header dan, seperti yang saya katakan, apa yang disediakan spring out of the box tidak cukup untuk mendukung API yang diperbarui terus-menerus. Lebih buruk lagi tautan pada jawaban itu menggunakan versi di URL.
Agustus
Mungkin bukan yang Anda cari, tetapi Spring 3.2 mendukung parameter "hasil" di RequestMapping. Satu-satunya peringatan adalah daftar versinya harus eksplisit. Misalnya,, produces={"application/json-1.0", "application/json-1.1"}dll
bimsapi
1
Kami perlu mendukung beberapa versi API kami, perbedaan ini biasanya merupakan perubahan kecil yang akan membuat beberapa panggilan dari beberapa klien tidak kompatibel (tidak aneh jika kami perlu mendukung 4 versi kecil, di mana beberapa titik akhir tidak kompatibel). Saya menghargai saran untuk memasukkannya ke dalam url, tetapi kami tahu bahwa ini adalah langkah ke arah yang salah, karena kami memiliki beberapa aplikasi dengan versi di URL dan ada banyak pekerjaan yang terlibat setiap kali kami perlu menabrak Versi: kapan.
Augusto
1
@Augusto, Anda sebenarnya belum juga. Rancang saja perubahan API Anda dengan cara yang tidak merusak kompatibilitas ke belakang. Beri saya contoh perubahan yang merusak kompatibilitas dan saya menunjukkan kepada Anda bagaimana membuat perubahan ini dengan cara yang tidak merusak.
Alexey Andreev

Jawaban:

62

Terlepas dari apakah pembuatan versi dapat dihindari dengan melakukan perubahan yang kompatibel ke belakang (yang mungkin tidak selalu memungkinkan ketika Anda terikat oleh beberapa pedoman perusahaan atau klien API Anda diimplementasikan dengan cara buggy dan akan mencapai titik impas jika seharusnya tidak) persyaratan yang diabstraksi adalah hal yang menarik satu:

Bagaimana saya bisa melakukan pemetaan permintaan kustom yang melakukan evaluasi arbitrer nilai header dari permintaan tanpa melakukan evaluasi di badan metode?

Seperti yang dijelaskan dalam jawaban SO ini Anda sebenarnya dapat memiliki yang sama @RequestMappingdan menggunakan penjelasan yang berbeda untuk membedakan selama perutean sebenarnya yang terjadi selama runtime. Untuk melakukannya, Anda harus:

  1. Buat anotasi baru VersionRange.
  2. Menerapkan a RequestCondition<VersionRange>. Karena Anda akan memiliki sesuatu seperti algoritme yang paling cocok, Anda harus memeriksa apakah metode yang dianotasi dengan VersionRangenilai lain memberikan kecocokan yang lebih baik untuk permintaan saat ini.
  3. Implementasikan VersionRangeRequestMappingHandlerMappingberdasarkan pada anotasi dan kondisi permintaan (seperti yang dijelaskan dalam posting Cara mengimplementasikan properti khusus @RequestMapping ).
  4. Konfigurasi pegas untuk mengevaluasi Anda VersionRangeRequestMappingHandlerMappingsebelum menggunakan default RequestMappingHandlerMapping(misalnya dengan mengatur urutannya ke 0).

Ini tidak memerlukan penggantian komponen Spring yang diretas tetapi menggunakan konfigurasi Spring dan mekanisme ekstensi sehingga harus berfungsi meskipun Anda memperbarui versi Spring (selama versi baru mendukung mekanisme ini).

xwoker
sumber
Terima kasih telah menambahkan komentar Anda sebagai jawaban xwoker. Hingga saat ini adalah yang terbaik. Saya telah menerapkan solusi berdasarkan tautan yang Anda sebutkan dan itu tidak terlalu buruk. Masalah terbesar akan terwujud saat meningkatkan ke versi baru Spring karena perlu memeriksa setiap perubahan pada logika di belakang mvc:annotation-driven. Mudah-mudahan Spring akan menyediakan versi mvc:annotation-drivendi mana seseorang dapat menentukan kondisi khusus.
Augusto
@Augusto, setengah tahun kemudian, bagaimana ini berhasil untuk Anda? Juga, saya ingin tahu, apakah Anda benar-benar membuat versi berdasarkan metode? Pada titik ini saya bertanya-tanya apakah itu tidak akan lebih jelas untuk versi pada per-class / per-controller level granularity?
Sander Verhagen
1
@SanderVerhagen berfungsi, tetapi kami membuat versi untuk seluruh API, bukan per metode atau pengontrol (API cukup kecil karena berfokus pada satu aspek bisnis). Kami memiliki proyek yang jauh lebih besar di mana mereka memilih untuk menggunakan versi yang berbeda per sumber daya dan menentukannya di URL (sehingga Anda dapat memiliki titik akhir di / v1 / sesi dan sumber daya lain pada versi yang sama sekali berbeda, misalnya / v4 / orders) ... ini sedikit lebih fleksibel, tetapi memberi lebih banyak tekanan pada klien untuk mengetahui versi mana yang harus dipanggil dari setiap titik akhir.
Augusto
1
Sayangnya, ini tidak berjalan dengan baik dengan Swagger, karena banyak konfigurasi otomatis dimatikan saat memperluas WebMvcConfigurationSupport.
Rick
Saya mencoba solusi ini tetapi sebenarnya tidak berfungsi dengan 2.3.2.RELEASE. Apakah Anda memiliki beberapa proyek contoh untuk ditunjukkan?
Patrick
54

Saya baru saja membuat solusi khusus. Saya menggunakan @ApiVersionanotasi yang dikombinasikan dengan @RequestMappinganotasi di dalam @Controllerkelas.

Contoh:

@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

}

Penerapan:

Anotasi ApiVersion.java :

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

ApiVersionRequestMappingHandlerMapping.java (ini sebagian besar adalah salin dan tempel dari 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);
    }

}

Injeksi ke WebMvcConfigurationSupport:

public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    public RequestMappingHandlerMapping requestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandlerMapping("v");
    }
}
Benjamin M
sumber
4
Saya mengubah int [] ke String [] untuk memungkinkan versi seperti "1.2", dan jadi saya dapat menangani kata kunci seperti "terbaru"
Maelig
3
Ya, itu cukup masuk akal. Untuk proyek-proyek mendatang, saya akan memilih cara lain karena beberapa alasan: 1. URL mewakili sumber daya. /v1/aResourcedan /v2/aResourceterlihat seperti sumber daya yang berbeda, tetapi itu hanya representasi yang berbeda dari sumber daya yang sama! 2. Menggunakan header HTTP terlihat lebih baik, tetapi Anda tidak dapat memberikan URL kepada seseorang, karena URL tersebut tidak berisi header. 3. Menggunakan parameter URL, yaitu /aResource?v=2.1(btw: itulah cara Google membuat versi). ...Saya masih tidak yakin apakah saya akan menggunakan opsi 2 atau 3 , tetapi saya tidak akan pernah menggunakan 1 lagi karena alasan yang disebutkan di atas.
Benjamin M.
5
Jika Anda ingin menyuntikkan sendiri RequestMappingHandlerMappingke dalam WebMvcConfiguration, Anda harus menimpa createRequestMappingHandlerMappingbukan requestMappingHandlerMapping! Jika tidak, Anda akan mengalami masalah aneh (tiba-tiba saya punya masalah dengan Hibernates inisialisasi malas karena sesi tertutup)
Stuxnet
1
Pendekatannya terlihat bagus tetapi entah bagaimana sepertinya tidak berfungsi dengan kasus uji junti (SpringRunner). Setiap kemungkinan bahwa Anda telah mendapatkan pendekatan yang bekerja dengan kasus uji
JDev
1
Ada cara agar ini berhasil, jangan memperpanjang WebMvcConfigurationSupport tapi memperpanjang DelegatingWebMvcConfiguration. Ini berhasil untuk saya (lihat stackoverflow.com/questions/22267191/… )
SeB.Fr
17

Saya tetap merekomendasikan menggunakan URL untuk pembuatan versi karena di URL @RequestMapping mendukung pola dan parameter jalur, yang formatnya dapat ditentukan dengan regexp.

Dan untuk menangani peningkatan versi klien (yang Anda sebutkan dalam komentar), Anda dapat menggunakan alias seperti 'terbaru'. Atau memiliki versi api tidak berversi yang menggunakan versi terbaru (ya).

Juga menggunakan parameter jalur, Anda dapat menerapkan logika penanganan versi yang kompleks, dan jika Anda sudah ingin memiliki rentang, Anda mungkin menginginkan sesuatu yang lebih cepat.

Berikut ini beberapa contohnya:

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

Berdasarkan pendekatan terakhir, Anda sebenarnya dapat menerapkan sesuatu seperti yang Anda inginkan.

Misalnya Anda dapat memiliki pengontrol yang hanya berisi metode tusukan dengan penanganan versi.

Dalam penanganan itu Anda melihat (menggunakan pustaka pembuatan refleksi / AOP / kode) di beberapa layanan / komponen pegas atau di kelas yang sama untuk metode dengan nama / tanda tangan yang sama dan memerlukan @VersionRange dan memanggilnya dengan meneruskan semua parameter.

kode yang sulit dipahami
sumber
14

Saya telah menerapkan solusi yang menangani masalah dengan SEMPURNA dengan versi lainnya.

Berbicara Umum, ada 3 pendekatan utama untuk versi istirahat:

  • Pendekatan berbasis jalur , di mana klien menentukan versi di URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Header Tipe Konten , di mana klien menentukan versi di header Terima :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Header Kustom , di mana klien menentukan versi di header kustom.

The masalah dengan pertama pendekatan adalah bahwa jika Anda mengubah versi katakanlah dari v1 -> v2, mungkin Anda perlu copy-paste sumber v1 yang belum berubah ke jalan v2

The masalah dengan kedua pendekatan adalah bahwa beberapa alat seperti http://swagger.io/ tidak bisa berbeda antara operasi dengan jalan yang sama tetapi berbeda Content-Type (masalah cek https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

Solusinya

Karena saya banyak bekerja dengan alat dokumentasi istirahat, saya lebih suka menggunakan pendekatan pertama. Solusi saya menangani masalah dengan pendekatan pertama, jadi Anda tidak perlu menyalin-tempel titik akhir ke versi baru.

Katakanlah kita memiliki versi v1 dan v2 untuk User controller:

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

The persyaratan adalah jika saya meminta v1 bagi pengguna sumber daya saya harus mengambil "V1 Pengguna" repsonse, sebaliknya jika saya meminta v2 , v3 dan seterusnya saya harus mengambil "User V2" respon.

masukkan deskripsi gambar di sini

Untuk mengimplementasikan ini di musim semi, kita perlu mengganti perilaku RequestMappingHandlerMapping default :

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

}

Implementasi membaca versi di URL dan meminta dari spring untuk menyelesaikan URL. Jika URL ini tidak ada (misalnya klien meminta v3 ) maka kami mencoba dengan v2 dan seterusnya sampai kami menemukan versi terbaru untuk resource .

Untuk melihat manfaat dari penerapan ini, katakanlah kita memiliki dua sumber daya: Pengguna dan Perusahaan:

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

Katakanlah kita membuat perubahan dalam "kontrak" perusahaan yang melanggar klien. Jadi kami menerapkan http://localhost:9001/api/v2/companydan kami meminta dari klien untuk mengubah ke v2 sebagai gantinya di v1.

Jadi permintaan baru dari klien adalah:

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

dari pada:

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

Bagian terbaiknya di sini adalah dengan solusi ini klien akan mendapatkan informasi pengguna dari v1 dan informasi perusahaan dari v2 tanpa perlu membuat titik akhir baru (sama) dari pengguna v2!

Dokumentasi Istirahat Seperti yang saya katakan sebelumnya alasan saya memilih pendekatan versi berbasis URL adalah bahwa beberapa alat seperti kesombongan tidak mendokumentasikan secara berbeda titik akhir dengan URL yang sama tetapi jenis konten yang berbeda. Dengan solusi ini, kedua titik akhir ditampilkan karena memiliki URL yang berbeda:

masukkan deskripsi gambar di sini

GIT

Implementasi solusi di: https://github.com/mspapant/restVersioningExample/

mspapant.dll
sumber
9

The @RequestMappingpenjelasan mendukung headerselemen yang memungkinkan Anda untuk mempersempit permintaan cocok. Secara khusus, Anda dapat menggunakan Accepttajuk di sini.

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

Ini tidak persis seperti yang Anda gambarkan, karena tidak secara langsung menangani rentang, tetapi elemennya mendukung * karakter pengganti serta! =. Jadi setidaknya Anda bisa lolos dengan menggunakan wildcard untuk kasus di mana semua versi mendukung titik akhir yang dimaksud, atau bahkan semua versi minor dari versi mayor tertentu (misalnya 1. *).

Saya tidak berpikir saya benar-benar menggunakan elemen ini sebelumnya (jika saya punya saya tidak ingat), jadi saya hanya akan mendokumentasikannya di

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


sumber
2
Saya tahu tentang itu, tetapi seperti yang Anda catat, pada setiap versi saya harus pergi ke semua pengontrol saya dan menambahkan versi, bahkan jika mereka belum berubah. Rentang yang Anda sebutkan hanya berfungsi pada tipe lengkap, misalnya, application/*dan bukan bagian dari tipe. Misalnya, berikut ini tidak valid di Spring "Accept=application/vnd.company.app-1.*+json". Ini terkait dengan cara MediaTypekerja kelas musim semi
Augusto
@Augusto Anda tidak perlu melakukan ini. Dengan pendekatan ini, Anda tidak membuat versi "API" tetapi "Endpoint". Setiap titik akhir dapat memiliki versi yang berbeda. Bagi saya ini adalah cara termudah untuk membuat versi API dibandingkan dengan versi API . Swagger juga lebih mudah diatur . Strategi ini disebut Versioning melalui negosiasi konten.
Dherik
3

Bagaimana dengan hanya menggunakan pewarisan ke versi model? Itulah yang saya gunakan dalam proyek saya dan tidak memerlukan konfigurasi pegas khusus dan memberi saya apa yang saya inginkan.

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

Pengaturan ini memungkinkan duplikasi kecil kode dan kemampuan untuk menimpa metode ke versi baru api dengan sedikit kerja. Ini juga menghemat kebutuhan untuk memperumit kode sumber Anda dengan logika peralihan versi. Jika Anda tidak membuat kode titik akhir dalam suatu versi, itu akan mengambil versi sebelumnya secara default.

Dibandingkan dengan apa yang dilakukan orang lain, ini tampaknya jauh lebih mudah. Apakah ada sesuatu yang saya lewatkan?

Ceekay
sumber
1
1 untuk membagikan kode. Namun, warisan pasangan erat itu. Sebagai gantinya. Pengontrol (Test1 dan Test2) harus hanya melewati ... tidak ada implementasi logika. Semua logika harus ada di kelas layanan, someService. Dalam hal ini, cukup gunakan komposisi sederhana dan jangan pernah mewarisi dari pengontrol lain
Dan Hunex
1
@ dan-hunex Tampaknya Ceekay menggunakan warisan untuk mengelola berbagai versi api. Jika Anda menghapus warisan, apa solusinya? Dan mengapa pasangan yang erat menjadi masalah dalam contoh ini? Dari sudut pandang saya, Test2 memperluas Test1 karena ini merupakan peningkatannya (dengan peran yang sama dan tanggung jawab yang sama), bukan?
jeremieca
2

Saya sudah mencoba membuat versi API saya menggunakan URI Versioning , seperti:

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

Tetapi ada beberapa tantangan ketika mencoba membuat ini berfungsi: bagaimana mengatur kode Anda dengan versi yang berbeda? Bagaimana mengelola dua (atau lebih) versi secara bersamaan? Apa dampaknya saat menghapus beberapa versi?

Alternatif terbaik yang saya temukan bukanlah versi seluruh API, tetapi mengontrol versi pada setiap titik akhir . Pola ini disebut Versioning using Accept header atau Versioning through content negotiation :

Pendekatan ini memungkinkan kita untuk membuat versi representasi sumber daya tunggal daripada membuat versi seluruh API yang memberi kita kontrol yang lebih terperinci atas pembuatan versi. Ini juga membuat footprint yang lebih kecil di basis kode karena kita tidak perlu membagi seluruh aplikasi saat membuat versi baru. Keuntungan lain dari pendekatan ini adalah tidak memerlukan implementasi aturan perutean URI yang diperkenalkan dengan pembuatan versi melalui jalur URI.

Implementasi di Spring

Pertama, Anda membuat Controller dengan atribut produksi dasar, yang akan diterapkan secara default untuk setiap titik akhir di dalam kelas.

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

}

Setelah itu, buat skenario yang memungkinkan Anda memiliki dua versi titik akhir untuk membuat pesanan:

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

Selesai! Cukup panggil setiap titik akhir menggunakan versi Header Http yang diinginkan :

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

Atau, untuk memanggil versi dua:

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

Tentang kekhawatiran Anda:

Dan karena tidak semua metode dalam API berubah dalam rilis yang sama, saya tidak ingin membuka setiap pengontrol saya dan mengubah apa pun untuk penangan yang tidak berubah antar versi.

Seperti dijelaskan, strategi ini mempertahankan setiap Pengontrol dan titik akhir dengan versi aslinya. Anda hanya mengubah titik akhir yang memiliki modifikasi dan membutuhkan versi baru.

Dan kesombongan?

Menyiapkan Swagger dengan versi yang berbeda juga sangat mudah menggunakan strategi ini. Lihat jawaban ini untuk lebih jelasnya.

Dherik
sumber
1

Dalam menghasilkan Anda bisa mengalami negasi. Jadi untuk metode1 katakan produces="!...1.7"dan dalam metode2 memiliki positif.

Menghasilkan juga sebuah array sehingga Anda untuk metode1 Anda dapat mengatakan produces={"...1.6","!...1.7","...1.8"}dll (menerima semua kecuali 1.7)

Tentu saja tidak ideal seperti rentang yang Anda pikirkan tetapi menurut saya lebih mudah untuk dipertahankan daripada barang khusus lainnya jika ini adalah sesuatu yang tidak biasa di sistem Anda. Semoga berhasil!

kode salsa
sumber
Terima kasih codesalsa, saya mencoba menemukan cara yang mudah dipelihara dan tidak memerlukan beberapa untuk memperbarui setiap titik akhir setiap kali kita perlu mengubah versinya.
Augusto
0

Anda dapat menggunakan AOP, di sekitar intersepsi

Pertimbangkan untuk memiliki pemetaan permintaan yang menerima semua /**/public_api/*dan dalam metode ini tidak melakukan apa-apa;

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

Setelah

@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


}

Satu-satunya kendala adalah semua harus berada dalam pengontrol yang sama.

Untuk konfigurasi AOP, lihat http://www.mkyong.com/spring/spring-aop-examples-advice/

hevi
sumber
Terima kasih hevi, saya mencari cara yang lebih ramah "musim semi" untuk melakukan ini, karena Spring sudah memilih metode mana yang akan dipanggil tanpa menggunakan AOP. Saya pandangan saya AOP menambahkan tingkat kompleksitas kode baru yang ingin saya hindari.
Augusto
@Augusto, Spring memiliki dukungan AOP yang bagus. Anda harus mencobanya. :)
Konstantin Yovkov