SpringBoot 應用篇 實現後端的接口版本支持

SpringBoot 應用篇 實現後端的接口版本支持java

做爲一個主職的後端開發者,在平時的工做中,最討厭的作的事情能夠說是參數校驗和接口的版本支持了。對於客戶端的同窗來講,業務的歷史包袱會小不少,當出現不兼容的業務變更時,直接開發新的就好;然然後端就沒有這麼簡單了,歷史的接口得支持,新的業務也得支持,吭哧吭哧的新加一個服務接口,url 又不能和以前的相同,怎麼辦?只能在某個地方加一個相似v1, v2...git

那麼有沒有一種不改變 url,經過其餘的方式來支持版本管理的方式呢?github

本文將介紹一種,利用請求頭來傳遞客戶端版本,在相同的 url 中尋找最適合的這個版本請求的接口的實例 caseweb

主要用到的知識點爲:spring

  • RequestCondition
  • RequestMappingHandlerMapping

<!-- more -->後端

I. 應用場景

咱們但願同一個業務始終用相同的 url,即使不一樣的版本之間業務徹底不兼容,經過請求參數中的版本選擇最合適的後端接口來響應這個請求api

1. 約定

須要實現上面的 case,首先有兩個約定app

  • 每一個請求中必須攜帶版本參數
  • 每一個接口都定義有一個支持的版本

2. 規則

明確上面兩點前提以後,就是基本規則了框架

版本定義ide

根據常見的三段式版本設計,版本格式定義以下

x.x.x
  • 其中第一個 x:對應的是大版本,通常來講只有較大的改動升級,纔會改變
  • 其中第二個 x:表示正常的業務迭代版本號,每發佈一個常規的 app 升級,這個數值+1
  • 最後一個 x:主要針對 bugfix,好比發佈了一個 app,結果發生了異常,須要一個緊急修復,須要再發佈一個版本,這個時候能夠將這個數值+1

接口選擇

一般的 web 請求都是經過 url 匹配規則來選擇對應響應接口,可是在咱們這裏,一個 url,可能會有多個不一樣的接口,該怎麼選擇呢?

  • 首先從請求中,獲取版本參數 version
  • 從全部相同的 url 接口中,根據接口上定義的版本,找到全部小於等於 version 的接口
  • 在上面知足條件的接口中,選擇版本最大的接口來響應請求

II. 應用實現

明確上面的應用場景以後,開始設計與實現

1. 接口定義

首先咱們須要一個版本定義的註解,用於標記 web 服務接口的版本,默認版本好爲 1.0.0

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Api {

    /**
     * 版本
     *
     * @return
     */
    String value() default "1.0.0";
}

其次須要一個版本對應的實體類,注意下面的實現中,默認版本爲1.0.0,並實現了Comparable接口,支持版本之間的比較

@Data
public class ApiItem implements Comparable<ApiItem> {

    private int high = 1;

    private int mid = 0;

    private int low = 0;

    public ApiItem() {
    }

    @Override
    public int compareTo(ApiItem right) {
        if (this.getHigh() > right.getHigh()) {
            return 1;
        } else if (this.getHigh() < right.getHigh()) {
            return -1;
        }

        if (this.getMid() > right.getMid()) {
            return 1;
        } else if (this.getMid() < right.getMid()) {
            return -1;
        }

        if (this.getLow() > right.getLow()) {
            return 1;
        } else if (this.getLow() < right.getLow()) {
            return -1;
        }
        return 0;
    }
}

須要一個將 string 格式的版本轉換爲 ApiItem 的轉換類,而且支持了默認版本爲1.0.0的設定

public class ApiConverter {
    public static ApiItem convert(String api) {
        ApiItem apiItem = new ApiItem();
        if (StringUtils.isBlank(api)) {
            return apiItem;
        }

        String[] cells = StringUtils.split(api, ".");
        apiItem.setHigh(Integer.parseInt(cells[0]));
        if (cells.length > 1) {
            apiItem.setMid(Integer.parseInt(cells[1]));
        }

        if (cells.length > 2) {
            apiItem.setLow(Integer.parseInt(cells[2]));
        }
        return apiItem;
    }
}

2. HandlerMapping 接口選擇

須要一個 url,支持多個請求接口,能夠考慮經過RequestCondition來實現,下面是具體的實現類

public class ApiCondition implements RequestCondition<ApiCondition> {

    private ApiItem version;

    public ApiCondition(ApiItem version) {
        this.version = version;
    }

    @Override
    public ApiCondition combine(ApiCondition other) {
        // 選擇版本最大的接口
        return version.compareTo(other.version) >= 0 ? new ApiCondition(version) : new ApiCondition(other.version);
    }

    @Override
    public ApiCondition getMatchingCondition(HttpServletRequest request) {
        String version = request.getHeader("x-api");
        ApiItem item = ApiConverter.convert(version);
        // 獲取全部小於等於版本的接口
        if (item.compareTo(this.version) >= 0) {
            return this;
        }

        return null;
    }

    @Override
    public int compareTo(ApiCondition other, HttpServletRequest request) {
        // 獲取最大版本對應的接口
        return other.version.compareTo(this.version);
    }
}

雖然上面的實現比較簡單,可是有必要注意一下兩個邏輯

  • getMatchingCondition方法中,控制了只有版本小於等於請求參數中的版本的 ApiCondition 才知足規則
  • compareTo 指定了當有多個ApiCoondition知足這個請求時,選擇最大的版本

自定義RequestMappingHandlerMapping實現類ApiHandlerMapping

public class ApiHandlerMapping extends RequestMappingHandlerMapping {
    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        return buildFrom(AnnotationUtils.findAnnotation(handlerType, Api.class));
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        return buildFrom(AnnotationUtils.findAnnotation(method, Api.class));
    }

    private ApiCondition buildFrom(Api platform) {
        return platform == null ? new ApiCondition(new ApiItem()) :
                new ApiCondition(ApiConverter.convert(platform.value()));
    }
}

註冊

@Configuration
public class ApiAutoConfiguration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiHandlerMapping();
    }
}

基於此,一個實現接口版本管理的微框架已經完成;接下來進入測試環節

III. 測試

case1. 方法上添加版本

設計三個接口,一個不加上註解,兩外兩個添加不一樣版本的註解

@RestController
@RequestMapping(path = "v1")
public class V1Rest {

    @GetMapping(path = "show")
    public String show1() {
        return "v1/show 1.0.0";
    }

    @Api("1.1.2")
    @GetMapping(path = "show")
    public String show2() {
        return "v1/show 1.1.2";
    }

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v1/show 1.1.0";
    }
}

在發起請求時,分別不帶上版本,帶指定版本,來測試對應的響應

  • 從上面的截圖能夠看出,請求頭中沒有版本時,默認給一個1.0.0的版本
  • 響應的是小於請求版本的接口中,版本最大的哪個

case2. 類版本+方法版本

每一個方法上添加版本有點蛋疼,在上面的註解定義中,就支持了類上註解,從實現上也能夠看出,當方法和類上都有註解時,選擇最大的版本

@Api("2.0.0")
@RestController
@RequestMapping(path = "v2")
public class V2Rest {

    @Api("1.1.0")
    @GetMapping(path = "show")
    public String show0() {
        return "v2/show0 1.1.0";
    }

    @GetMapping(path = "show")
    public String show1() {
        return "v2/show1 2.0.0";
    }

    @Api("2.1.1")
    @GetMapping(path = "show")
    public String show2() {
        return "v2/show2 2.1.1";
    }

    @Api("2.2.0")
    @GetMapping(path = "show")
    public String show3() {
        return "v2/show3 2.2.0";
    }
}

根據咱們的實現規則,show0 和 show1 都會相應 <2.1.1 的版本請求,這個時候會出現衝突;

  • 從上面的截圖中,能夠看出來版本小於 2.0.0 的請求,報的是 404 錯誤
  • 請求版本小於 2.1.1 的請求,報的是衝突異常

IV. 其餘

0. 項目&相關博文

相關博文

1. 一灰灰 Blog

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激

下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

一灰灰blog

相關文章
相關標籤/搜索