SpringMVC自定义RequestMappingHandlerMapping实现接口的版本控制(四)

说明

在Spring MVC项目中,如果要进行restful接口的版本控制一般有以下几个方向:

  • 基于参数的版本控制
  • 基于header的版本控制
  • 基于path的版本控制

在spring MVC下,url映射到哪个method是由RequestMappingHandlerMapping来控制的,那么我们可以通过对RequestMappingHandlerMapping 定义条件来做版本控制

环境搭建

ApiVersion 表示请求的版本

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiVersion {
    String value() default "";
}

WebMvcRegistrationsConfig.java

@Configuration
public class WebMvcRegistrationsConfig implements WebMvcRegistrations {

    private static final String VERSION_PARAM_NAME = "version";

    private static final String HEADER_VERSION = "X-VERSION";

    /**
     * RequestMappingHandlerMapping 被 VersionRequestMappingHandlerMappingHandlerMapping 替换
     * 按照VersionRequestMappingHandlerMappingHandlerMapping映射逻辑进行映射
     * @return
     */
    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new VersionRequestMappingHandlerMapping();
    }

    private class VersionRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

        @Override
        protected RequestCondition<?> getCustomMethodCondition(Method method) {
            ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
            if (apiVersion == null) {
                return null;
            }

            return new RequestCondition() {

                @Override
                public Object combine(Object o) {
                    return null;
                }

                @Override
                public Object getMatchingCondition(HttpServletRequest request) {
                    String version = resolveVersion(method, request);

                    if (Objects.equals(version, apiVersion.value())) {
                        return this;
                    }

                    return null;
                }

                @Override
                public int compareTo(Object o, HttpServletRequest request) {
                    return 0;
                }
            };
        }

        private String resolveVersion(Method method, HttpServletRequest request) {
            String version = request.getParameter(VERSION_PARAM_NAME);
            if (StringUtils.hasText(version)) {
                return version;
            }
            version = request.getHeader(HEADER_VERSION);
            if (StringUtils.hasText(version)) {
                return version;
            }
            return resolvePathVersion(method, request);
        }

        private String resolvePathVersion(Method method, HttpServletRequest request) {
            String bastPath = null;
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == RequestMapping.class) {
                    bastPath = ((RequestMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == GetMapping.class) {
                    bastPath = ((GetMapping)annotation).value()[0];
                    break;
                }  else if (annotation.annotationType() == PostMapping.class) {
                    bastPath = ((PostMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == DeleteMapping.class) {
                    bastPath = ((DeleteMapping)annotation).value()[0];
                    break;
                } else if (annotation.annotationType() == PutMapping.class) {
                    bastPath = ((PutMapping)annotation).value()[0];
                    break;
                }
            }

            Map<String, String> uriVariables = getPathMatcher().extractUriTemplateVariables(bastPath.startsWith("/") ? bastPath : "/" + bastPath,
                    request.getServletPath());
            return uriVariables.get(VERSION_PARAM_NAME);
        }
    }
}

测试

@GetMapping("{version}/test")
public String v1(@PathVariable String version) {
    return "default";
}

@GetMapping("{version}/test")
@ApiVersion("v2")
public String v2() {
    return "v2";
}

@GetMapping("{version}/test")
@ApiVersion("v3")
public String v3() {
    return "v3";
}

/**
 * version在不一定非要在第一个位置
 * @return
 */
@GetMapping("/test/{version}")
@ApiVersion("v4")
public String v4() {
    return"v4";
}

@GetMapping("/test/{version}")
public String v5() {
    return "default";
}
curl -X GET \
  'http://localhost:8080/v1/test?version=v3' \
  -H 'X-VERSION: v2'

优先级依次是:request参数 > header > path参数

path参数,变量不一定写在第一个,只要变量名称是${version}就可以
GitHub:https://github.com/jkxyx205/spring-boot-learn/tree/master/spring-mvc-mapping