Làm cách nào để quản lý lập phiên bản API REST với Spring?


118

Tôi đã tìm kiếm cách quản lý các phiên bản API REST bằng Spring 3.2.x, nhưng tôi không tìm thấy bất kỳ thứ gì dễ bảo trì. Đầu tiên tôi sẽ giải thích vấn đề tôi gặp phải, sau đó là giải pháp ... nhưng tôi tự hỏi liệu tôi có đang phát minh lại bánh xe ở đây không.

Tôi muốn quản lý phiên bản dựa trên tiêu đề Chấp nhận và ví dụ: nếu một yêu cầu có tiêu đề Chấp nhận application/vnd.company.app-1.1+json, tôi muốn Spring MVC chuyển tiếp điều này tới phương thức xử lý phiên bản này. Và vì không phải tất cả các phương thức trong API đều thay đổi trong cùng một bản phát hành, tôi không muốn đi đến từng bộ điều khiển của mình và thay đổi bất kỳ điều gì cho một trình xử lý không thay đổi giữa các phiên bản. Tôi cũng không muốn có logic để tìm ra phiên bản nào sẽ sử dụng trong chính bộ điều khiển (sử dụng bộ định vị dịch vụ) vì Spring đã khám phá ra phương thức nào để gọi.

Vì vậy, đã sử dụng một API với các phiên bản 1.0 đến 1.8 trong đó trình xử lý được giới thiệu trong phiên bản 1.0 và được sửa đổi trong v1.7, tôi muốn xử lý điều này theo cách sau. Hãy tưởng tượng rằng mã bên trong một bộ điều khiển và có một số mã có thể trích xuất phiên bản từ tiêu đề. (Phần sau không hợp lệ trong 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;
}

Điều này không thể xảy ra trong Spring vì 2 phương pháp có cùng RequestMappingchú thích và Spring không tải được. Ý tưởng là VersionRangechú thích có thể xác định phạm vi phiên bản mở hoặc đóng. Phương pháp đầu tiên có hiệu lực từ phiên bản 1.0 đến 1.6, trong khi phương pháp thứ hai dành cho phiên bản 1.7 trở đi (bao gồm cả phiên bản 1.8 mới nhất). Tôi biết rằng cách tiếp cận này sẽ bị phá vỡ nếu ai đó quyết định vượt qua phiên bản 99.99, nhưng đó là điều tôi có thể sống cùng.

Bây giờ, vì phần trên không thể thực hiện được nếu không có sự làm lại nghiêm túc về cách hoạt động của Spring, tôi đã nghĩ đến việc mày mò cách xử lý phù hợp với yêu cầu, đặc biệt là viết của riêng tôi ProducesRequestConditionvà có phạm vi phiên bản trong đó. Ví dụ

Mã:

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

Bằng cách này, tôi có thể xác định phạm vi phiên bản đóng hoặc mở trong phần sản xuất của chú thích. Tôi đang làm việc trên giải pháp này ngay bây giờ, với những vấn đề mà tôi vẫn phải thay thế một số lớp lõi Spring MVC ( RequestMappingInfoHandlerMapping, RequestMappingHandlerMappingRequestMappingInfo), mà tôi không thích, bởi vì nó có nghĩa là thêm công việc bất cứ khi nào tôi quyết định nâng cấp lên một phiên bản mới hơn của mùa xuân.

Tôi sẽ đánh giá cao bất kỳ suy nghĩ nào ... và đặc biệt, bất kỳ đề xuất nào để thực hiện điều này theo cách đơn giản hơn, dễ duy trì hơn.


Biên tập

Thêm tiền thưởng. Để nhận được tiền thưởng, vui lòng trả lời câu hỏi ở trên mà không đề nghị có logic này trong chính bộ điều khiển. Spring đã có rất nhiều logic để chọn phương thức điều khiển nào sẽ gọi, và tôi muốn dựa trên đó.


Chỉnh sửa 2

Tôi đã chia sẻ POC gốc (với một số cải tiến) trong github: https://github.com/augusto/restVersinstall



1
@flup Tôi không hiểu nhận xét của bạn. Điều đó chỉ nói rằng bạn có thể sử dụng các tiêu đề và, như tôi đã nói, những gì mùa xuân cung cấp ngoài hộp không đủ để hỗ trợ các API được cập nhật liên tục. Thậm chí tệ hơn, liên kết trên câu trả lời đó sử dụng phiên bản trong URL.
Augusto,

Có thể không chính xác những gì bạn đang tìm kiếm, nhưng Spring 3.2 hỗ trợ tham số "sản xuất" trên RequestMapping. Một lưu ý là danh sách phiên bản phải rõ ràng. Ví dụ produces={"application/json-1.0", "application/json-1.1"}
:,

1
Chúng tôi cần hỗ trợ một số phiên bản API của mình, những khác biệt này thường là những thay đổi nhỏ khiến một số cuộc gọi từ một số ứng dụng khách không tương thích (sẽ không lạ nếu chúng tôi cần hỗ trợ 4 phiên bản nhỏ, trong đó một số điểm cuối không tương thích). Tôi đánh giá cao đề xuất đưa nó vào url, nhưng chúng tôi biết rằng đó là một bước đi sai hướng, vì chúng tôi có một vài ứng dụng có phiên bản trong URL và có rất nhiều công việc liên quan mỗi khi chúng tôi cần phiên bản.
Augusto

1
@Augusto, bạn thực sự bạn cũng vậy. Chỉ cần thiết kế các thay đổi API của bạn theo cách không phá vỡ khả năng tương thích ngược. Chỉ cho tôi ví dụ về những thay đổi phá vỡ khả năng tương thích và tôi chỉ cho bạn cách thực hiện những thay đổi này theo cách không vi phạm.
Alexey Andreev

Câu trả lời:


62

Bất kể việc lập phiên bản có thể tránh được bằng cách thực hiện các thay đổi tương thích ngược (điều này không phải lúc nào cũng có thể thực hiện được khi bạn bị ràng buộc bởi một số nguyên tắc của công ty hoặc ứng dụng khách API của bạn được triển khai theo cách có lỗi và sẽ phá vỡ ngay cả khi không nên thực hiện) yêu cầu tóm tắt là một điều thú vị một:

Làm cách nào để thực hiện ánh xạ yêu cầu tùy chỉnh thực hiện đánh giá tùy ý các giá trị tiêu đề từ yêu cầu mà không thực hiện đánh giá trong phần thân phương thức?

Như được mô tả trong câu trả lời SO này, bạn thực sự có thể có giống nhau @RequestMappingvà sử dụng một chú thích khác để phân biệt trong quá trình định tuyến thực sự xảy ra trong thời gian chạy. Để làm như vậy, bạn sẽ phải:

  1. Tạo một chú thích mới VersionRange .
  2. Thực hiện a RequestCondition<VersionRange>. Vì bạn sẽ có một cái gì đó giống như một thuật toán phù hợp nhất, bạn sẽ phải kiểm tra xem các phương thức được chú thích với các VersionRangegiá trị khác có cung cấp kết quả phù hợp hơn cho yêu cầu hiện tại hay không.
  3. Triển khai VersionRangeRequestMappingHandlerMappingđiều kiện dựa trên chú thích và yêu cầu (như được mô tả trong bài đăng Cách triển khai thuộc tính tùy chỉnh @RequestMapping ).
  4. Định cấu hình mùa xuân để đánh giá của bạn VersionRangeRequestMappingHandlerMappingtrước khi sử dụng mặc định RequestMappingHandlerMapping(ví dụ: bằng cách đặt thứ tự của nó thành 0).

Điều này sẽ không yêu cầu bất kỳ sự thay thế hacky nào của các thành phần Spring nhưng sử dụng cấu hình Spring và cơ chế mở rộng, vì vậy nó sẽ hoạt động ngay cả khi bạn cập nhật phiên bản Spring của mình (miễn là phiên bản mới hỗ trợ các cơ chế này).


Cảm ơn đã thêm nhận xét của bạn như một câu trả lời xwoker. Cho đến nay là một trong những tốt nhất. Tôi đã triển khai giải pháp dựa trên các liên kết mà bạn đã đề cập và nó không tệ lắm. Vấn đề lớn nhất sẽ xuất hiện khi nâng cấp lên phiên bản Spring mới vì nó sẽ yêu cầu kiểm tra bất kỳ thay đổi nào đối với logic phía sau mvc:annotation-driven. Hy vọng rằng Spring sẽ cung cấp một phiên bản mvc:annotation-driventrong đó người ta có thể xác định các điều kiện tùy chỉnh.
Augusto

@Augusto, nửa năm sau, điều này diễn ra như thế nào đối với bạn? Ngoài ra, tôi tò mò, bạn có thực sự lập phiên bản trên cơ sở từng phương pháp không? Tại thời điểm này, tôi tự hỏi nếu phiên bản trên mức độ chi tiết của mỗi lớp / mỗi bộ điều khiển sẽ không rõ ràng hơn?
Sander Verhagen

1
@SanderVerhagen nó đang hoạt động, nhưng chúng tôi tạo phiên bản cho toàn bộ API, không theo phương thức hoặc bộ điều khiển (API khá nhỏ vì nó tập trung vào một khía cạnh của doanh nghiệp). Chúng tôi có một dự án lớn hơn đáng kể trong đó họ đã chọn sử dụng một phiên bản khác cho mỗi tài nguyên và chỉ định phiên bản đó trên URL (vì vậy bạn có thể có một điểm cuối trên / v1 / session và một tài nguyên khác trên một phiên bản hoàn toàn khác, ví dụ: / v4 / order) ... nó linh hoạt hơn một chút, nhưng nó gây áp lực nhiều hơn cho khách hàng trong việc biết phiên bản nào cần gọi của mỗi điểm cuối.
Augusto

1
Thật không may, điều này không tốt với Swagger, vì nhiều cấu hình tự động bị tắt khi mở rộng WebMvcConfigurationSupport.
Rick

Tôi đã thử giải pháp này nhưng thực sự nó không hoạt động với 2.3.2.RELEASE. Bạn có dự án ví dụ nào đó để hiển thị không?
Patrick

54

Tôi vừa tạo một giải pháp tùy chỉnh. Tôi đang sử dụng @ApiVersionchú thích kết hợp với @RequestMappingchú thích bên trong @Controllercác lớp.

Thí dụ:

@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

}

Thực hiện:

Chú thích ApiVersion.java :

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

ApiVersionRequestMappingHandlerMapping.java (đây chủ yếu là sao chép và dán từ 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);
    }

}

Tiêm vào WebMvcConfigurationSupport:

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

4
Tôi đã thay đổi int [] thành String [] để cho phép các phiên bản như "1.2" và vì vậy tôi có thể xử lý các từ khóa như "mới nhất"
Maelig

3
Vâng, điều đó khá hợp lý. Đối với các dự án trong tương lai, tôi sẽ đi theo một cách khác vì một số lý do: 1. URL đại diện cho tài nguyên. /v1/aResource/v2/aResourcetrông giống như các tài nguyên khác nhau, nhưng nó chỉ là một đại diện khác nhau của cùng một tài nguyên! 2. Sử dụng tiêu đề HTTP trông đẹp hơn, nhưng bạn không thể cung cấp cho ai đó một URL, vì URL không chứa tiêu đề. 3. Sử dụng tham số URL, tức là /aResource?v=2.1(btw: đó là cách Google thực hiện lập phiên bản). ...Tôi vẫn không chắc liệu mình có chọn tùy chọn 2 hay 3 hay không , nhưng tôi sẽ không bao giờ sử dụng tùy chọn 1 nữa vì những lý do đã đề cập ở trên.
Benjamin M

5
Nếu bạn muốn tiêm của riêng RequestMappingHandlerMappingbạn vào của bạn WebMvcConfiguration, bạn nên ghi đè createRequestMappingHandlerMappingthay vì requestMappingHandlerMapping! Nếu không, bạn sẽ gặp phải vấn đề lạ (tôi đột nhiên có vấn đề với Hibernates khởi lười biếng vì một phiên họp kín)
Stuxnet

1
Cách tiếp cận có vẻ tốt nhưng bằng cách nào đó có vẻ như nó không hoạt động với các trường hợp thử nghiệm junti (SpringRunner). Bất kỳ cơ hội nào mà bạn có được cách tiếp cận hoạt động với các trường hợp thử nghiệm
JDev

1
Có một cách để thực hiện việc này, đừng kéo dài WebMvcConfigurationSupport mà hãy kéo dài DelegatingWebMvcConfiguration. Điều này đã hiệu quả với tôi (xem stackoverflow.com/questions/22267191/… )
SeB.Fr

17

Tôi vẫn khuyên bạn nên sử dụng URL để lập phiên bản vì trong URL @RequestMapping hỗ trợ các mẫu và tham số đường dẫn, định dạng nào có thể được chỉ định bằng regexp.

Và để xử lý các nâng cấp máy khách (mà bạn đã đề cập trong bình luận), bạn có thể sử dụng các bí danh như 'mới nhất'. Hoặc có phiên bản api chưa phiên bản sử dụng phiên bản mới nhất (vâng).

Ngoài ra bằng cách sử dụng các tham số đường dẫn, bạn có thể triển khai bất kỳ logic xử lý phiên bản phức tạp nào và nếu bạn đã muốn có phạm vi, bạn rất có thể muốn một cái gì đó đủ sớm hơn.

Dưới đây là một số ví dụ:

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

Dựa trên cách tiếp cận cuối cùng, bạn thực sự có thể triển khai một cái gì đó giống như những gì bạn muốn.

Ví dụ, bạn có thể có một bộ điều khiển chỉ chứa các lỗi phương thức với xử lý phiên bản.

Trong quá trình xử lý đó, bạn nhìn (sử dụng phản chiếu / AOP / thư viện tạo mã) trong một số dịch vụ / thành phần mùa xuân hoặc trong cùng một lớp đối với phương thức có cùng tên / chữ ký và @VersionRange được yêu cầu và gọi nó chuyển tất cả các tham số.


14

Tôi đã triển khai một giải pháp xử lý HOÀN HẢO vấn đề với lập phiên bản còn lại.

Nói chung, có 3 cách tiếp cận chính để lập phiên bản phần còn lại:

  • Phương pháp tiếp cận dựa trên đường dẫn , trong đó khách hàng xác định phiên bản trong URL:

    http://localhost:9001/api/v1/user
    http://localhost:9001/api/v2/user
  • Tiêu đề Loại-Nội dung , trong đó khách hàng xác định phiên bản trong tiêu đề Chấp nhận :

    http://localhost:9001/api/v1/user with 
    Accept: application/vnd.app-1.0+json OR application/vnd.app-2.0+json
  • Tiêu đề tùy chỉnh , trong đó khách hàng xác định phiên bản trong tiêu đề tùy chỉnh.

Các vấn đề với đầu tiếp cận là nếu bạn thay đổi phiên bản giả sử từ v1 -> v2, có lẽ bạn cần phải sao chép-dán các nguồn lực v1 chưa thay đổi con đường v2

Các vấn đề với thứ hai cách tiếp cận là một số công cụ như http://swagger.io/ không thể phân biệt giữa hoạt động với cùng một con đường nhưng khác nhau Content-Type (vấn đề kiểm tra https://github.com/OAI/OpenAPI-Specification/issues/ 146 )

Giải pháp

Vì tôi đang làm việc rất nhiều với các công cụ tài liệu nghỉ ngơi, tôi thích sử dụng cách tiếp cận đầu tiên. Giải pháp của tôi xử lý sự cố với cách tiếp cận đầu tiên, vì vậy bạn không cần phải sao chép-dán điểm cuối vào phiên bản mới.

Giả sử chúng ta có phiên bản v1 và v2 cho 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";
    }
 }

Các yêu cầu là nếu tôi yêu cầu v1 cho tài nguyên người dùng i có để có những "tài V1" repsonse, nếu không nếu tôi yêu cầu v2 , v3 và vân vân tôi phải lấy "tài V2" phản ứng.

nhập mô tả hình ảnh ở đây

Để thực hiện điều này vào mùa xuân, chúng tôi cần ghi đè hành vi RequestMappingHandlerMapping mặc định :

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

}

Quá trình triển khai đọc phiên bản trong URL và yêu cầu từ mùa xuân giải quyết URL. Trong trường hợp URL này không tồn tại (ví dụ: khách hàng đã yêu cầu v3 ) thì chúng tôi thử với v2 và cứ như vậy cho đến khi chúng tôi tìm thấy phiên bản mới nhất cho tài nguyên .

Để thấy được lợi ích từ việc triển khai này, giả sử chúng ta có hai tài nguyên: Người dùng và Công ty:

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

Giả sử chúng tôi đã thực hiện một thay đổi trong "hợp đồng" của công ty khiến khách hàng bị phá vỡ. Vì vậy, chúng tôi thực hiện http://localhost:9001/api/v2/companyvà chúng tôi yêu cầu từ khách hàng thay đổi thành v2 thay vì v1.

Vì vậy, các yêu cầu mới từ khách hàng là:

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

thay vì:

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

Phần tốt nhất ở đây là với giải pháp này, khách hàng sẽ nhận được thông tin người dùng từ v1 và thông tin công ty từ v2 mà không cần phải tạo một điểm cuối mới (giống nhau) từ người dùng v2!

Tài liệu phần còn lại Như tôi đã nói trước đây lý do tôi chọn phương pháp lập phiên bản dựa trên URL là một số công cụ như swagger không ghi lại các điểm cuối có cùng URL nhưng loại nội dung khác nhau. Với giải pháp này, cả hai điểm cuối đều được hiển thị vì có URL khác nhau:

nhập mô tả hình ảnh ở đây

GIT

Triển khai giải pháp tại: https://github.com/mspapant/restVersinstallExample/


9

Các @RequestMappingchú thích hỗ trợ một headersphần cho phép bạn thu hẹp các yêu cầu phù hợp. Đặc biệt bạn có thể sử dụng Accepttiêu đề tại đây.

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

Đây không phải là chính xác những gì bạn đang mô tả, vì nó không trực tiếp xử lý các phạm vi, nhưng phần tử cũng hỗ trợ ký tự đại diện *! =. Vì vậy, ít nhất bạn có thể tránh được việc sử dụng ký tự đại diện cho các trường hợp trong đó tất cả các phiên bản đều hỗ trợ điểm cuối được đề cập hoặc thậm chí tất cả các phiên bản nhỏ của một phiên bản chính nhất định (ví dụ: 1. *).

Tôi không nghĩ rằng tôi đã thực sự sử dụng phần tử này trước đây (nếu tôi có thì tôi không nhớ), vì vậy tôi chỉ tắt tài liệu tại

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


2
Tôi biết về điều đó, nhưng như bạn đã lưu ý, trên mỗi phiên bản, tôi cần phải truy cập vào tất cả các bộ điều khiển của mình và thêm một phiên bản, ngay cả khi chúng không thay đổi. Ví dụ application/*, phạm vi mà bạn đã đề cập chỉ hoạt động trên loại đầy đủ chứ không phải các bộ phận của loại. Ví dụ sau đây là không hợp lệ trong Spring "Accept=application/vnd.company.app-1.*+json". Điều này liên quan đến cách MediaTypehoạt động của lớp học mùa xuân
Augusto

@Augusto bạn không nhất thiết phải làm điều này. Với cách tiếp cận này, bạn không lập phiên bản cho "API" mà là "Điểm cuối". Mỗi điểm cuối có thể có một phiên bản khác nhau. Đối với tôi, đó là cách dễ nhất để tạo phiên bản API so với phiên bản API . Swagger cũng đơn giản hơn để thiết lập . Chiến lược này được gọi là Tạo phiên bản thông qua thương lượng nội dung.
Dherik

3

Điều gì về việc chỉ sử dụng kế thừa để lập phiên bản mô hình? Đó là những gì tôi đang sử dụng trong dự án của mình và nó không yêu cầu cấu hình lò xo đặc biệt và mang lại cho tôi chính xác những gì tôi muốn.

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

Việc thiết lập này cho phép ít trùng lặp mã và khả năng ghi đè các phương thức vào các phiên bản api mới với rất ít thao tác. Nó cũng tiết kiệm nhu cầu phức tạp mã nguồn của bạn với logic chuyển đổi phiên bản. Nếu bạn không viết mã một điểm cuối trong một phiên bản, nó sẽ lấy phiên bản trước đó theo mặc định.

So với những gì người khác đang làm, cách này có vẻ dễ dàng hơn. Có điều gì tôi đang thiếu?


1
+1 để chia sẻ mã. Tuy nhiên, sự kế thừa gắn chặt nó. Thay thế. Bộ điều khiển (Test1 và Test2) chỉ nên là một thông qua ... không có logic. Mọi thứ logic nên nằm trong lớp dịch vụ, someService. Trong trường hợp đó, chỉ cần sử dụng thành phần đơn giản và không bao giờ kế thừa từ bộ điều khiển khác
Dan Hunex

1
@ dan-hunex Có vẻ như Ceekay sử dụng quyền thừa kế để quản lý các phiên bản khác nhau của api. Nếu bạn loại bỏ thừa kế, giải pháp là gì? Và tại sao cặp đôi chặt chẽ lại là một vấn đề trong ví dụ này? Theo quan điểm của tôi, Test2 mở rộng Test1 vì nó là một cải tiến của nó (với cùng vai trò và cùng trách nhiệm), phải không?
jeremieca

2

Tôi đã cố gắng phiên bản API của mình bằng cách sử dụng Phiên bản URI , như:

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

Nhưng có một số thách thức khi cố gắng thực hiện điều này: làm thế nào để tổ chức mã của bạn với các phiên bản khác nhau? Làm thế nào để quản lý hai (hoặc nhiều) phiên bản cùng một lúc? Tác động khi xóa một số phiên bản?

Giải pháp thay thế tốt nhất mà tôi tìm thấy không phải là phiên bản toàn bộ API, mà là kiểm soát phiên bản trên mỗi điểm cuối . Mẫu này được gọi là Tạo phiên bản sử dụng tiêu đề Chấp nhận hoặc Tạo phiên bản thông qua thương lượng nội dung :

Cách tiếp cận này cho phép chúng tôi tạo phiên bản cho một biểu diễn tài nguyên duy nhất thay vì lập phiên bản cho toàn bộ API, điều này cho phép chúng tôi kiểm soát chi tiết hơn đối với việc lập phiên bản. Nó cũng tạo ra một dấu chân nhỏ hơn trong cơ sở mã vì chúng ta không phải chia nhỏ toàn bộ ứng dụng khi tạo phiên bản mới. Một ưu điểm khác của phương pháp này là nó không yêu cầu thực hiện các quy tắc định tuyến URI được giới thiệu bằng cách lập phiên bản thông qua đường dẫn URI.

Thực hiện vào mùa xuân

Đầu tiên, bạn tạo Bộ điều khiển với thuộc tính sản xuất cơ bản, thuộc tính này sẽ áp dụng theo mặc định cho mỗi điểm cuối bên trong lớp.

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

}

Sau đó, hãy tạo một tình huống có thể xảy ra trong đó bạn có hai phiên bản của một điểm cuối để tạo đơn hàng:

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

Làm xong! Chỉ cần gọi mỗi điểm cuối bằng phiên bản Tiêu đề Http mong muốn :

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

Hoặc, để gọi phiên bản hai:

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

Về những lo lắng của bạn:

Và vì không phải tất cả các phương thức trong API đều thay đổi trong cùng một bản phát hành, tôi không muốn đi đến từng bộ điều khiển của mình và thay đổi bất kỳ điều gì cho một trình xử lý không thay đổi giữa các phiên bản

Như đã giải thích, chiến lược này duy trì mỗi Bộ điều khiển và điểm cuối với phiên bản thực của anh ta. Bạn chỉ sửa đổi điểm cuối có sửa đổi và cần phiên bản mới.

Và Swagger?

Thiết lập Swagger với các phiên bản khác nhau cũng rất dễ dàng bằng cách sử dụng chiến lược này. Xem câu trả lời này để biết thêm chi tiết.


1

Trong sản xuất bạn có thể có phủ định. Vì vậy, đối với method1 nóiproduces="!...1.7" và method2 đều có giá trị tích cực.

Sản xuất cũng là một mảng vì vậy bạn có thể nói với method1 produces={"...1.6","!...1.7","...1.8"}vv (chấp nhận tất cả ngoại trừ 1.7)

Tất nhiên không lý tưởng như phạm vi mà bạn nghĩ đến nhưng tôi nghĩ dễ bảo trì hơn những thứ tùy chỉnh khác nếu đây là điều không phổ biến trong hệ thống của bạn. Chúc may mắn!


Cảm ơn Codealsa, tôi đang cố gắng tìm một cách dễ bảo trì và không yêu cầu một số cập nhật mỗi điểm cuối mỗi khi chúng tôi cần thay đổi phiên bản.
Augusto

0

Bạn có thể sử dụng AOP, xung quanh việc đánh chặn

Hãy xem xét có một ánh xạ yêu cầu nhận được tất cả /**/public_api/*và trong phương thức này không làm gì cả;

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

Sau

@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


}

Hạn chế duy nhất là tất cả phải ở trong cùng một bộ điều khiển.

Để biết cấu hình AOP, hãy xem tại http://www.mkyong.com/spring/spring-aop-examples-advice/


Cảm ơn hevi, tôi đang tìm một cách thân thiện hơn với "mùa xuân" để thực hiện việc này, vì Spring đã chọn phương thức nào để gọi mà không cần sử dụng AOP. Tôi là quan điểm của tôi AOP thêm một mức độ phức tạp mã mới mà tôi muốn tránh.
Augusto

@Augusto, Spring có hỗ trợ AOP tuyệt vời. Bạn nên dùng thử. :)
Konstantin Yovkov
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.