Repository: springdoc/springdoc-openapi
Pull request #1156 (2021-05-06 12:50:21)

Pick up ExceptionHandlers from any parent of a class marked as @ControllerAdvice
Currently the library scans  for exception handlers only declared method of `@ControllerAdvice`s. So, if an advice has parent classes with exception handlers, handlers will not be picked up and models will not be displayed in OpenAPI spec.

Sometimes, it might be useful to have one class with common handlers (e.g. a jar library that should be imported in all services across a project/company) and extend with more specific handlers.

Something like that


```java
-----------------------------------------
public class Error {
  // ...
}
-----------------------------------------
public class CommonErrorHandler {

  @ResponseStatus(INTERNAL_SERVER_ERROR)
  @ExceptionHandler(Exception.class)
  public Error onException(Exception ex) {
    return new Error();
  }

  // other common handlers
}
-----------------------------------------
@ControllerAdvice
public class ErrorHandler extends CommonErrorHandler {
  
  //additional error handlers

  @ResponseStatus(BAD_REQUEST)
  @ExceptionHandler(IllegalArgumentException.class)
  public SpecificError onIllegalArgumentException(IllegalArgumentException ex) {
    return new SpecificError ();
  }

}
-----------------------------------------
```


Patch:
diff --git a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
index d4ea9f76a..55bd18643 100644
--- a/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
+++ b/springdoc-openapi-common/src/main/java/org/springdoc/core/GenericResponseService.java
@@ -52,6 +52,7 @@ import org.springframework.core.ResolvableType;
 import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.http.HttpStatus;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.ResponseStatus;
@@ -152,7 +153,7 @@ public class GenericResponseService {
 			if (org.springframework.aop.support.AopUtils.isAopProxy(controllerAdvice))
 				objClz = org.springframework.aop.support.AopUtils.getTargetClass(controllerAdvice);
 			ControllerAdviceInfo controllerAdviceInfo = new ControllerAdviceInfo(controllerAdvice);
-			Arrays.stream(objClz.getDeclaredMethods()).filter(m -> m.isAnnotationPresent(ExceptionHandler.class)).forEach(methods::add);
+			Arrays.stream(ReflectionUtils.getAllDeclaredMethods(objClz)).filter(m -> m.isAnnotationPresent(ExceptionHandler.class)).forEach(methods::add);
 			// for each one build ApiResponse and add it to existing responses
 			for (Method method : methods) {
 				if (!operationService.isHidden(method)) {


Test patch:
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CommonFooErrorHandler.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CommonFooErrorHandler.java
new file mode 100644
index 000000000..21e2d2ef0
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/CommonFooErrorHandler.java
@@ -0,0 +1,14 @@
+package test.org.springdoc.api.app157;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+public class CommonFooErrorHandler {
+
+    @ExceptionHandler
+    @ResponseStatus(HttpStatus.CONFLICT)
+    public ErrorDTO onException(Exception e) {
+        return new ErrorDTO("Something wrong has happened");
+    }
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/ErrorDTO.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/ErrorDTO.java
new file mode 100644
index 000000000..6d2d2ecca
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/ErrorDTO.java
@@ -0,0 +1,20 @@
+package test.org.springdoc.api.app157;
+
+public class ErrorDTO {
+    private String message;
+
+    public ErrorDTO() {
+    }
+
+    public ErrorDTO(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java
new file mode 100644
index 000000000..95da66ff7
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/HelloController.java
@@ -0,0 +1,19 @@
+package test.org.springdoc.api.app157;
+
+import io.swagger.v3.oas.annotations.OpenAPIDefinition;
+import io.swagger.v3.oas.annotations.info.Info;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api")
+@OpenAPIDefinition(info = @Info(title = "API Examples", version = "1.0"), tags = @Tag(name = "Operations"))
+public class HelloController {
+
+	@GetMapping("/foo")
+	public SimpleDTO hello() {
+		return new SimpleDTO("foo");
+	}
+}
\ No newline at end of file
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SimpleDTO.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SimpleDTO.java
new file mode 100644
index 000000000..d36b6ad68
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SimpleDTO.java
@@ -0,0 +1,22 @@
+package test.org.springdoc.api.app157;
+
+public class SimpleDTO {
+
+	private String payload;
+
+	public SimpleDTO() {
+	}
+
+	public SimpleDTO(String payload) {
+		this.payload = payload;
+	}
+
+	public String getPayload() {
+		return payload;
+	}
+
+	public void setPayload(String payload) {
+		this.payload = payload;
+	}
+
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpecificFooErrorHandler.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpecificFooErrorHandler.java
new file mode 100644
index 000000000..cdc6f3aea
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpecificFooErrorHandler.java
@@ -0,0 +1,7 @@
+package test.org.springdoc.api.app157;
+
+import org.springframework.web.bind.annotation.ControllerAdvice;
+
+@ControllerAdvice(assignableTypes = HelloController.class)
+public class SpecificFooErrorHandler extends CommonFooErrorHandler {
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java
new file mode 100644
index 000000000..ba5e98f78
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/java/test/org/springdoc/api/app157/SpringDocApp157Test.java
@@ -0,0 +1,11 @@
+package test.org.springdoc.api.app157;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import test.org.springdoc.api.AbstractSpringDocTest;
+
+public class SpringDocApp157Test extends AbstractSpringDocTest {
+
+	@SpringBootApplication
+	static class SpringDocTestApp {}
+
+}
diff --git a/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json b/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json
new file mode 100644
index 000000000..8c8e2f9be
--- /dev/null
+++ b/springdoc-openapi-webmvc-core/src/test/resources/results/app157.json
@@ -0,0 +1,70 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "API Examples",
+    "version": "1.0"
+  },
+  "servers": [
+    {
+      "url": "http://localhost",
+      "description": "Generated server url"
+    }
+  ],
+  "tags": [
+    {
+      "name": "Operations"
+    }
+  ],
+  "paths": {
+    "/api/foo": {
+      "get": {
+        "tags": [
+          "hello-controller"
+        ],
+        "operationId": "hello",
+        "responses": {
+          "409": {
+            "description": "Conflict",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/ErrorDTO"
+                }
+              }
+            }
+          },
+          "200": {
+            "description": "OK",
+            "content": {
+              "*/*": {
+                "schema": {
+                  "$ref": "#/components/schemas/SimpleDTO"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "ErrorDTO": {
+        "type": "object",
+        "properties": {
+          "message": {
+            "type": "string"
+          }
+        }
+      },
+      "SimpleDTO": {
+        "type": "object",
+        "properties": {
+          "payload": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  }
+}