首页 > 代码库 > 让SpringMVC Restful优雅地支持多版本

让SpringMVC Restful优雅地支持多版本

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

在移动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的版本号:

/** * Multi-version Restful API support annotation * * @author Tony Mu(tonymu@qq.com) * @since 2017-07-07 */@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documented@Mappingpublic @interface ApiVersion {    /**     * version code     */    double value() default 1.0;}

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

/** * Custom HandlerMapping for support multi-value of spring mvc restful api with same url. * Version code put into request header. * <p> * * spring mvc config case: * * @Configuration * public class WebConfig extends WebMvcConfigurationSupport { *      @Override protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { *          return new MultiVersionRequestMappingHandlerMapping(); *      } * } * * controller/action case: * * @RestController * @RequestMapping(value = "http://www.mamicode.com/api/product") * public class ProductController { * *      @RequestMapping(value = "http://www.mamicode.com/detail", method = GET) *      public something detailDefault(int id) { *          return something; *      } * *      @RequestMapping(value = "http://www.mamicode.com/detail", method = GET) *      @RestApi(version = 1.1) *      public something detailV11(int id) { *          return something; *      } * *      @RequestMapping(value = "http://www.mamicode.com/detail", method = GET) *      @RestApi(version = 1.2) *      public something detailV12(int id) { *          return something; *      } * } * * client case: * * $.ajax({ *      type: "GET", *      url: "http://www.xxx.com/api/product/detail?id=100", *      headers: { *          value: 1.1 *      }, *      success: function(data){ *          do something *      } * }); * * @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";    @Override    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {        ApiVersion apiVersionAnnotation = method.getAnnotation(ApiVersion.class);        if (apiVersionAnnotation != null) {            registerRestApiHandlerMethod(handler, method, mapping, apiVersionAnnotation);            return;        }        super.registerHandlerMethod(handler, method, mapping);    }    @Override    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {        HandlerMethod restApiHandlerMethod = lookupRestApiHandlerMethod(lookupPath, request);        if (restApiHandlerMethod != null)            return restApiHandlerMethod;        return super.lookupHandlerMethod(lookupPath, request);    }    private void registerRestApiHandlerMethod(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 lookupRestApiHandlerMethod(String lookupPath, HttpServletRequest request) {        String version = tryResolveRestApiVersion(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 tryResolveRestApiVersion(HttpServletRequest request) {        String version = request.getHeader("version");        if (!StringUtils.hasText(version)) {            String versionFromUrl = request.getParameter("@value");//for debug            if (StringUtils.hasText(versionFromUrl)) {                version = versionFromUrl;            }        }        return version;    }}

使用方式参考代码注释。

让SpringMVC Restful优雅地支持多版本