Spring-Boot OpenAPI - @RestControllerAdvice 不限于抛出异常的方法

问题描述 投票:0回答:2

我需要用 OpenAPI 记录我的 SpringBoot API 及其可能的异常情况, 我正在使用 SpringDoc-OpenAPI https://springdoc.org/.

为了处理 NotFound 情况,我创建了这个异常类:

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

public class NotFoundException extends ResponseStatusException {
    public NotFoundException() {
        super(HttpStatus.NOT_FOUND);
    }
}

还有这个@RestControllerAdvice

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalControllerExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleNotFoundException(RuntimeException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
}

我面临的问题是生成的OpenAPI yaml文件有

  responses:
    "404":
      description: Not Found
      content:
        '*/*':
          schema:
            type: string

适用于所有

@RestController
端点,而不是仅适用于具有
throws NotFoundException
的方法。

如何限制@ControllerAdvice(或OpenAPI),仅针对具有抛出签名的方法生成404响应文档?

除了 @RestControllerAdvice 之外,我还需要使用其他东西吗? 我想避免必须注释每个方法。

spring-boot openapi springdoc
2个回答
3
投票

一个可能的解决方案是:

  1. 制作
    @RestControllerAdvice
    @Hidden
  2. 提供
    OperationCustomizer
    @Bean
import io.swagger.v3.oas.annotations.Hidden;
import it.eng.cysec.ot.risk.assessment.api.exceptions.NotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Hidden
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
    @ExceptionHandler(NotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ResponseEntity<String> handleNotFoundException(NotFoundException exception) {
        return new ResponseEntity<>(exception.getMessage(), HttpStatus.NOT_FOUND);
    }
}
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Content;
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.responses.ApiResponses;
import it.eng.cysec.ot.risk.assessment.api.exceptions.NotFoundException;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.web.method.HandlerMethod;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class OperationResponseCustomizer implements OperationCustomizer {
    public static final ApiResponse NOT_FOUND_API_RESPONSE;


    static {
        MediaType mediaType = new MediaType();
        mediaType.setSchema(new StringSchema());

        Content content = new Content();
        content.addMediaType("*/*", mediaType);

        NOT_FOUND_API_RESPONSE = new ApiResponse()
                .description("Not Found")
                .content(content);
    }

    /**
     * Customize operation.
     *
     * @param operation     input operation
     * @param handlerMethod original handler method
     * @return customized operation
     */
    @Override
    public Operation customize(Operation operation, HandlerMethod handlerMethod) {
        Method method = handlerMethod.getMethod();
        List<Class<?>> exceptions = Arrays.asList(method.getExceptionTypes());

        if(exceptions.contains(NotFoundException.class)){
            ApiResponses apiResponses = operation.getResponses();
            apiResponses.addApiResponse("404", NOT_FOUND_API_RESPONSE);
        }

        return operation;
    }
}

0
投票

根据 1Z10 的回答,我为 Spring 6 构建了一个解决方案,可以满足您的要求。

我将

@Hidden
注释添加到我的
RestControllerAdvice
类中,然后实现以下
OperationCustomizer
,它会自动扫描控制器中的方法声明中的 throw 子句,并为从
ErrorResponseException
继承的异常添加相应的 ApiResponses(响应)带有 ProblemDetail 主体)并用
ResponseStatus
:

进行注释
@Component
public class OperationResponseCustomizer implements OperationCustomizer {

    @Override
    public Operation customize(Operation operation, HandlerMethod handlerMethod) {
        Method method = handlerMethod.getMethod();
        List<Class<?>> exceptions = Arrays.asList(method.getExceptionTypes());

        ApiResponses apiResponses = operation.getResponses();

        Map<String, List<ResponseStatus>> responseCodeToAnnotations = new TreeMap<>();
        for (Class<?> exception : exceptions) {
            if (!ErrorResponseException.class.isAssignableFrom(exception)) {
                continue;
            }
            for (Annotation annotation : exception.getAnnotations()) {
                if (annotation.annotationType().equals(ResponseStatus.class)) {
                    ResponseStatus responseStatus = exception.getAnnotation(ResponseStatus.class);

                    String responseCode = responseStatus.value().value() + "";
                    responseCodeToAnnotations.putIfAbsent(responseCode,
                            new ArrayList<>());
                    responseCodeToAnnotations.get(responseCode).add(responseStatus);
                    break;
                }
            }
        }

        responseCodeToAnnotations.forEach((responseCode, responseStatuses) -> {
            if (responseStatuses.isEmpty())
                return;

            // generate a joined reason per responseCode
            String joinedReason = null;
            if (responseStatuses.size() > 1) {
                joinedReason = responseStatuses.stream()
                        .map(s -> String.format("- %s", s.reason()))
                        .collect(Collectors.joining("\n"));
            } else {
                joinedReason = responseStatuses.get(0).reason();
            }

            MediaType mediaType = new MediaType();
            ResolvedSchema resolvedSchema = ModelConverters.getInstance().readAllAsResolvedSchema(new AnnotatedType().type(ProblemDetail.class));
            if (resolvedSchema != null) {
                Schema schema = resolvedSchema.schema;
                schema.setTitle("ProblemDetail");
                mediaType.setSchema(schema);
            }

            Content content = new Content();
            content.addMediaType("*/*", mediaType);

            ApiResponse apiResponse = new ApiResponse()
                    .description(joinedReason)
                    .content(content);

            apiResponses.addApiResponse(responseCode,
                    apiResponse);
        });

        return operation;
    }
}

拾取的异常示例:


@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = StartTimeInPastException.REASON)
public class StartTimeInPastException extends ResponseStatusException {
    static final String REASON = "startTimeEpoch cannot lie in the past";
    public StartTimeInPastException() {
        super(HttpStatus.BAD_REQUEST, REASON);
    }
}

可以删除 ErrorResponseExceptions 的过滤,但我认为必须以其他方式处理 ApiResponses 的架构。

© www.soinside.com 2019 - 2024. All rights reserved.