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

     

  • 相关阅读:
    失眠食療
    学会妥协学会弯腰(下册)
    软件工程(第4版·修订版)
    GitHub入门与实践
    iOS开发:从零基础到精通
    Premiere Pro CS6高手成长之路
    网页设计与前端开发从入门到精通Dreamweaver+Flash+Photoshop+HTML+CSS+JavaScript
    CINEMA 4D R17 完全学习手册(第2版)
    1040. 二叉树层次遍历
    1041. 二哥打飞机
  • 原文地址:https://www.cnblogs.com/tonymu/p/7147172.html
Copyright © 2020-2023  润新知