在Spring Boot中为自定义版本mediatype返回406

时间:2017-03-16 09:05:59

标签: spring rest spring-boot spring-restcontroller http-status-code-406

我在Spring Boot(1.5.2)中有许多类可以通过自定义mediatype标签进行版本控制。 mediatype的格式为application/vnd.<application>.<version>+json

ApiVersionedResource

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

    /**
     * Media type without the version for e.g. application/vnd.orchestral.onboarding
     *
     * @return
     */
    String media() default Constants.DEFAULT_API_MEDIA_TYPE;

    /**
     * Version of the API as defined by {@link Version}
     *
     * @return
     */
    String version() default Constants.DEFAULT_API_VERSION;
}

ApiVersionedResourceRequestCondition

public class ApiVersionedResourceRequestCondition extends AbstractRequestCondition<ApiVersionedResourceRequestCondition> {

    private final static Logger log = LoggerFactory.getLogger(ApiVersionedResourceRequestCondition.class);

    private final Set<Version> versions;
    private final String acceptedMediaType;
    private final static Version MAX_API_VERSION = new Version(Constants.MAX_API_VERSION);

    public ApiVersionedResourceRequestCondition(String acceptedMediaType, String version) {
        this(acceptedMediaType, versionGenerator(version));
    }

    public ApiVersionedResourceRequestCondition(String acceptedMediaType, Collection<Version> versions) {
        this.acceptedMediaType = acceptedMediaType;
        this.versions = Collections.unmodifiableSet(new HashSet<>(versions));
    }

    @Override
    public ApiVersionedResourceRequestCondition combine(ApiVersionedResourceRequestCondition other) {
        log.debug("Combining:\n{}\n{}", this, other);
        Set<Version> newVersions = new LinkedHashSet<>(this.versions);
        newVersions.addAll(other.versions);
        String newMediaType;
        if (StringUtils.hasText(this.acceptedMediaType) && StringUtils.hasText(other.acceptedMediaType)
                && !this.acceptedMediaType.equals(other.acceptedMediaType)) {
            throw new IllegalArgumentException(String.format("Both conditions should have the same media type however its %s != %s",
                    this.acceptedMediaType, other.acceptedMediaType));
        } else if (StringUtils.hasText(this.acceptedMediaType)) {
            newMediaType = this.acceptedMediaType;
        } else {
            newMediaType = other.acceptedMediaType;
        }
        return new ApiVersionedResourceRequestCondition(newMediaType, newVersions);
    }

    @Override
    public ApiVersionedResourceRequestCondition getMatchingCondition(HttpServletRequest request) {
        final String accept = request.getHeader("Accept");
        log.debug("Accept header = {}", accept);
        if (StringUtils.hasText(accept)) {
            final Pattern regexPattern = Pattern.compile("(.*)\\.(\\d+\\.\\d+).*");
            final Matcher matcher = regexPattern.matcher(accept);
            if (matcher.matches()) {
                final String actualMediaType = matcher.group(1);
                final Version version = new Version(matcher.group(2));
                log.debug("Version={}", version);

                if (acceptedMediaType.startsWith(actualMediaType)) {
                    for (Version definedVersion : versions) {
                        if (definedVersion.compareTo(version) == 0) {
                            return this;
                        }
                    }
                    log.debug("Unable to find a matching version");
                } else {
                    log.debug("Unable to find a valid media type {}", acceptedMediaType);
                }
            }
        }
        return null;
    }

    @Override
    protected Collection<?> getContent() {
        return versions;
    }

    @Override
    protected String getToStringInfix() {
        return " && ";
    }

    @Override
    public int compareTo(ApiVersionedResourceRequestCondition other, HttpServletRequest request) {
        return 0;
    }

    /**
     * Converts a given version string to {@link Version}
     *
     * @param commaDelimitedVersioning
     * @return
     */
    private static Set<Version> versionGenerator(final String commaDelimitedVersioning) {
        HashSet<Version> versionRanges = new HashSet<>();

        if (StringUtils.hasText(commaDelimitedVersioning)) {
            final String[] versions = StringUtils.tokenizeToStringArray(commaDelimitedVersioning.trim(), ",");
            Arrays.stream(versions).forEach(i -> {
                Version v = new Version(i);
                if (v.compareTo(MAX_API_VERSION) > 1) {
                    throw new IllegalArgumentException(
                            String.format("Specified version %s is highest than the max version %s", v, MAX_API_VERSION));
                }
                versionRanges.add(v);
            });
        }

        return versionRanges;
    }
}

WebConfiguration

@Configuration
@ConditionalOnClass({ ApiVersionedResource.class })
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(final ContentNegotiationConfigurer configurer) {
        configurer.favorPathExtension(true)
                .favorParameter(false)
                .ignoreAcceptHeader(false)
                .useJaf(false)
                .defaultContentType(MediaType.APPLICATION_JSON)
                .mediaType("json", MediaType.APPLICATION_JSON);
    }

    @Bean
    public WebMvcRegistrationsAdapter webMvcRegistrationsHandlerMapping() {
        return new WebMvcRegistrationsAdapter() {
            @Override
            public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
                return new CustomRequestMappingHandlerMapping();
            }
        };
    }

    private static final class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

        @Override
        protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
            ApiVersionedResource typeAnnotation = AnnotationUtils.findAnnotation(handlerType, ApiVersionedResource.class);
            return createCondition(typeAnnotation);
        }

        @Override
        protected RequestCondition<?> getCustomMethodCondition(Method method) {
            ApiVersionedResource methodAnnotation = AnnotationUtils.findAnnotation(method, ApiVersionedResource.class);
            return createCondition(methodAnnotation);
        }

        @Override
        protected boolean isHandler(Class<?> beanType) {
            return super.isHandler(beanType) && (AnnotationUtils.findAnnotation(beanType, ApiVersionedResource.class) != null);
        }

        private RequestCondition<?> createCondition(ApiVersionedResource versionMapping) {
            if (versionMapping != null) {
                return new ApiVersionedResourceRequestCondition(versionMapping.media(), versionMapping.version());
            }

            return null;
        }
    }
}

在我的 ResourceController 中,我将注释设置为,

@RestController
@RequestMapping(Constants.DEFAULT_API_PREFIX)
@ApiVersionedResource
@Api(description = "Operations to onboard Organizations",
        tags = {"organizations"},
        produces = "application/json")
public class OrganizationResource {
    .....
}

如果我将 接受 标头指定为未知的媒体类型,例如application/vnd.test.blah+json,我希望服务器返回 406 错误回到客户端。

不幸的是,返回的是 404 错误。

我需要更正此问题才能返回 406 错误?

1 个答案:

答案 0 :(得分:1)

您可以添加控制器建议:

    @ControllerAdvice
public class MediaNotSupportedExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(value = { HttpMediaTypeNotSupportedException.class })
    protected ResponseEntity<Void> handleMediaNotSupported(RuntimeException ex, WebRequest request) {

    return new ResponseEntity<Void>(HttpStatus.NOT_ACCEPTABLE);
}

加上拦截器:

public class MediaInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler) throws Exception {

        if(request.getHeader("Accept") == null || compare against your desired media type)
        throw new HttpMediaTypeNotSupportedException();
        return super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }
}
然而,抛出的404令人困惑。