我需要用 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 之外,我还需要使用其他东西吗? 我想避免必须注释每个方法。
一个可能的解决方案是:
@RestControllerAdvice
@Hidden
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;
}
}
根据 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 的架构。