让SpringMVC Restful API优雅地支持多版本

好久没有更新博客,难得有空,记录一下今天写的一个小工具,供有需要的朋友参考。

在移动APP开发中,多版本接口同时存在的情况经常发生,通常接口支持多版本,有以下两种方式:

1.通过不同路径区分不同版本

如:

http://www.xxx.com/api/v1/product/detail?id=100 (版本1)
http://www.xxx.com/api/v2/product/detail?id=100 (版本2)

这种情况,可以通过建立多个文件的方式实现,优点是结构清晰、实现简单,缺点是大量重复工作导致实现不优雅。


2.通过不同调用参数区分不同版本

如:
http://www.xxx.com/api/v1/product/detail?id=100&@version=1(版本1)
http://www.xxx.com/api/v1/product/detail?id=100&@version=2(版本2)

【version还可以通过http请求头的header提供】

这种方式相对灵活且优雅,这篇文章主要讨论这种方式,直接上代码!

首先定义一个注解,用于在控制器的方法中标记API的版本号:

/**
 * Annotation for support Multi-version Restful API
 *
 * @author Tony Mu(tonymu@qq.com)
 * @since 2017-07-07
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface ApiVersion {

    /**
     * api version code
     */
    double value() default 1.0;

}

然后扩展SpringMVC的RequestMappingHandlerMapping,以便于根据不同的版本号,调用不同的实现逻辑:

 

/**
 * Custom RequestMappingHandlerMapping for support multi-version of spring mvc restful api with same url.
 * Version code provide by {@code ApiVersionCodeDiscoverer}.
 * <p>
 *
 * How to use ?
 *
 * Spring mvc config case:
 *
 * <pre class="code">
 * @Configuration
 * public class WebConfig extends WebMvcConfigurationSupport {
 *      @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
 *          MultiVersionRequestMappingHandlerMapping requestMappingHandlerMapping = new MultiVersionRequestMappingHandlerMapping();
 *          requestMappingHandlerMapping.registerApiVersionCodeDiscoverer(new DefaultApiVersionCodeDiscoverer());
 *          return requestMappingHandlerMapping;
 *      }
 * }</pre>
 *
 * Controller/action case:
 *
 * <pre class="code">
 * @RestController
 * @RequestMapping(value = "/api/product")
 * public class ProductController {
 *
 *      @RequestMapping(value = "detail", method = GET)
 *      public something detailDefault(int id) {
 *          return something;
 *      }
 *
 *      @RequestMapping(value = "detail", method = GET)
 *      @ApiVersion(value = 1.1)
 *      public something detailV11(int id) {
 *          return something;
 *      }
 *
 *      @RequestMapping(value = "detail", method = GET)
 *      @ApiVersion(value = 1.2)
 *      public something detailV12(int id) {
 *          return something;
 *      }
 * }</pre>
 *
 * Client case:
 *
 * <pre class="code">
 * $.ajax({
 *      type: "GET",
 *      url: "http://www.xxx.com/api/product/detail?id=100",
 *      headers: {
 *          value: 1.1
 *      },
 *      success: function(data){
 *          do something
 *      }
 * });</pre>
 *
 * @since 2017-07-07
 */
public class MultiVersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    private static final Logger logger = LoggerFactory.getLogger(MultiVersionRequestMappingHandlerMapping.class);

    private final static Map<String, HandlerMethod> HANDLER_METHOD_MAP = new HashMap<>();

    /**
     * key pattern,such as:/api/product/detail[GET]@1.1
     */
    private final static String HANDLER_METHOD_KEY_PATTERN = "%s[%s]@%s";

    private List<ApiVersionCodeDiscoverer> apiVersionCodeDiscoverers = new ArrayList<>();

    @Override
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        ApiVersion apiVersionAnnotation = method.getAnnotation(ApiVersion.class);
        if (apiVersionAnnotation != null) {
            registerMultiVersionApiHandlerMethod(handler, method, mapping, apiVersionAnnotation);
            return;
        }
        super.registerHandlerMethod(handler, method, mapping);
    }

    @Override
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        HandlerMethod restApiHandlerMethod = lookupMultiVersionApiHandlerMethod(lookupPath, request);
        if (restApiHandlerMethod != null)
            return restApiHandlerMethod;
        return super.lookupHandlerMethod(lookupPath, request);
    }

    public void registerApiVersionCodeDiscoverer(ApiVersionCodeDiscoverer apiVersionCodeDiscoverer){
        if(!apiVersionCodeDiscoverers.contains(apiVersionCodeDiscoverer)){
            apiVersionCodeDiscoverers.add(apiVersionCodeDiscoverer);
        }
    }

    private void registerMultiVersionApiHandlerMethod(Object handler, Method method, RequestMappingInfo mapping, ApiVersion apiVersionAnnotation) {
        PatternsRequestCondition patternsCondition = mapping.getPatternsCondition();
        RequestMethodsRequestCondition methodsCondition = mapping.getMethodsCondition();
        if (patternsCondition == null
                || methodsCondition == null
                || patternsCondition.getPatterns().size() == 0
                || methodsCondition.getMethods().size() == 0) {
            return;
        }
        Iterator<String> patternIterator = patternsCondition.getPatterns().iterator();
        Iterator<RequestMethod> methodIterator = methodsCondition.getMethods().iterator();
        while (patternIterator.hasNext() && methodIterator.hasNext()) {
            String patternItem = patternIterator.next();
            RequestMethod methodItem = methodIterator.next();
            String key = String.format(HANDLER_METHOD_KEY_PATTERN, patternItem, methodItem.name(), apiVersionAnnotation.value());
            HandlerMethod handlerMethod = super.createHandlerMethod(handler, method);
            if (!HANDLER_METHOD_MAP.containsKey(key)) {
                HANDLER_METHOD_MAP.put(key, handlerMethod);
                if (logger.isDebugEnabled()) {
                    logger.debug("register ApiVersion HandlerMethod of %s %s", key, handlerMethod);
                }
            }
        }
    }

    private HandlerMethod lookupMultiVersionApiHandlerMethod(String lookupPath, HttpServletRequest request) {
        String version = tryResolveApiVersion(request);
        if (StringUtils.hasText(version)) {
            String key = String.format(HANDLER_METHOD_KEY_PATTERN, lookupPath, request.getMethod(), version);
            HandlerMethod handlerMethod = HANDLER_METHOD_MAP.get(key);
            if (handlerMethod != null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("lookup ApiVersion HandlerMethod of %s %s", key, handlerMethod);
                }
                return handlerMethod;
            }
            logger.debug("lookup ApiVersion HandlerMethod of %s failed", key);
        }
        return null;
    }

    private String tryResolveApiVersion(HttpServletRequest request) {
        for (int i = 0; i < apiVersionCodeDiscoverers.size(); i++) {
            ApiVersionCodeDiscoverer apiVersionCodeDiscoverer = apiVersionCodeDiscoverers.get(i);
            String versionCode = apiVersionCodeDiscoverer.getVersionCode(request);
            if(StringUtils.hasText(versionCode))
                return versionCode;
        }
        return null;
    }
}

 

使用方式参考代码注释。

 

以下是用到的相关代码:

/**
 * Interface to discover api version code in http request.
 *
 * @author Tony Mu(tonymu@qq.com)
 * @since 2017-07-11
 */
public interface ApiVersionCodeDiscoverer {

    /**
     * Return an api version code that can indicate the version of current api.
     *
     * @param request current HTTP request
     * @return an api version code that can indicate the version of current api or {@code null}.
     */
    String getVersionCode(HttpServletRequest request);

}

 

/**
 * Default implementation of the {@link ApiVersionCodeDiscoverer} interface, get api version code
 * named "version" in headers or named "@version" in parameters.
 *
 * @author Tony Mu(tonymu@qq.com)
 * @since 2017-07-11
 */
public class DefaultApiVersionCodeDiscoverer implements ApiVersionCodeDiscoverer {

    /**
     * Get api version code named "version" in headers or named "@version" in parameters.
     *
     * @param request current HTTP request
     * @return api version code named "version" in headers or named "@version" in parameters.
     */
    @Override
    public String getVersionCode(HttpServletRequest request) {
        String version = request.getHeader("version");
        if (!StringUtils.hasText(version)) {
            String versionFromUrl = request.getParameter("@version");//for debug
            if (StringUtils.hasText(versionFromUrl)) {
                version = versionFromUrl;
            }
        }
        return version;
    }
}

 

 

posted @ 2017-07-10 18:33  小八路  阅读(6894)  评论(3编辑  收藏  举报