Skip to content

Commit 10fd6dd

Browse files
committed
io.swagger.v3.oas.annotations.Webhook does not work when defined on the method level. Fixes #2998
1 parent 31ed191 commit 10fd6dd

File tree

3 files changed

+116
-30
lines changed

3 files changed

+116
-30
lines changed

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/service/OpenAPIService.java

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
package org.springdoc.core.service;
2828

29+
import java.lang.annotation.Annotation;
30+
import java.lang.reflect.AnnotatedElement;
2931
import java.lang.reflect.Method;
3032
import java.util.ArrayList;
3133
import java.util.Arrays;
@@ -51,6 +53,7 @@
5153
import io.swagger.v3.core.util.AnnotationsUtils;
5254
import io.swagger.v3.oas.annotations.Hidden;
5355
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
56+
import io.swagger.v3.oas.annotations.Webhook;
5457
import io.swagger.v3.oas.annotations.Webhooks;
5558
import io.swagger.v3.oas.annotations.security.SecuritySchemes;
5659
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -534,64 +537,96 @@ private Optional<OpenAPIDefinition> getOpenAPIDefinition() {
534537
return Optional.ofNullable(apiDef);
535538
}
536539

540+
537541
/**
538542
* Get webhooks webhooks [ ].
539543
*
540544
* @return the webhooks [ ]
541545
*/
542546
public Webhooks[] getWebhooks() {
543-
// List to collect all Webhooks annotations
544547
List<Webhooks> allWebhooks = new ArrayList<>();
545548

546-
// Get beans with @Webhooks annotation managed by Spring
547-
Map<String, Object> beansWithWebhooksAnnotation = context.getBeansWithAnnotation(Webhooks.class);
549+
// First: scan Spring-managed beans
550+
Map<String, Object> beans = context.getBeansWithAnnotation(Webhooks.class);
548551

549-
// Process Spring-managed beans
550-
if (!beansWithWebhooksAnnotation.isEmpty()) {
551-
beansWithWebhooksAnnotation.values().forEach(controller -> {
552-
// Get the @Webhooks annotation(s) from each bean
553-
Webhooks[] webhooksAnnotations = controller.getClass().getAnnotationsByType(Webhooks.class);
554-
allWebhooks.addAll(Arrays.asList(webhooksAnnotations));
555-
});
552+
for (Object bean : beans.values()) {
553+
Class<?> beanClass = bean.getClass();
554+
555+
// Collect @Webhooks or @Webhook on class level
556+
collectWebhooksFromElement(beanClass, allWebhooks);
557+
558+
// Collect from methods
559+
for (Method method : beanClass.getDeclaredMethods()) {
560+
collectWebhooksFromElement(method, allWebhooks);
561+
}
556562
}
557563

558-
// If no beans with @Webhooks annotation found, perform classpath scanning
564+
// Fallback: classpath scanning if nothing found
559565
if (allWebhooks.isEmpty()) {
560-
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
566+
ClassPathScanningCandidateComponentProvider scanner =
567+
new ClassPathScanningCandidateComponentProvider(false);
561568
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhooks.class));
569+
scanner.addIncludeFilter(new AnnotationTypeFilter(Webhook.class));
562570

563-
// Scan base packages if available
564571
if (AutoConfigurationPackages.has(context)) {
565-
List<String> packagesToScan = AutoConfigurationPackages.get(context);
572+
for (String basePackage : AutoConfigurationPackages.get(context)) {
573+
Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
566574

567-
for (String basePackage : packagesToScan) {
568-
// Perform the scan and get candidate components
569-
Set<BeanDefinition> components = scanner.findCandidateComponents(basePackage);
570-
571-
// Loop through the components
572-
for (BeanDefinition beanDefinition : components) {
575+
for (BeanDefinition bd : candidates) {
573576
try {
574-
// Get the class name and load the class
575-
String className = beanDefinition.getBeanClassName();
576-
Class<?> clazz = Class.forName(className);
577+
Class<?> clazz = Class.forName(bd.getBeanClassName());
578+
579+
// Class-level annotations
580+
collectWebhooksFromElement(clazz, allWebhooks);
577581

578-
// Get @Webhooks annotation from the class
579-
Webhooks[] webhooksAnnotations = clazz.getAnnotationsByType(Webhooks.class);
580-
allWebhooks.addAll(Arrays.asList(webhooksAnnotations));
582+
// Method-level annotations
583+
for (Method method : clazz.getDeclaredMethods()) {
584+
collectWebhooksFromElement(method, allWebhooks);
585+
}
581586

582587
} catch (ClassNotFoundException e) {
583-
// Log the error if the class is not found
584588
LOGGER.error("Class not found in classpath: {}", e.getMessage());
585589
}
586590
}
587591
}
588592
}
589593
}
590594

591-
// Convert the list of Webhooks annotations to an array and return
592595
return allWebhooks.toArray(new Webhooks[0]);
593596
}
594597

598+
/**
599+
* Collect webhooks from element.
600+
*
601+
* @param element the element
602+
* @param collector the collector
603+
*/
604+
private void collectWebhooksFromElement(AnnotatedElement element, List<Webhooks> collector) {
605+
// If @Webhooks is present (container)
606+
Webhooks container = element.getAnnotation(Webhooks.class);
607+
if (container != null) {
608+
collector.add(container);
609+
}
610+
611+
// If individual @Webhook annotations are present
612+
Webhook[] individualWebhooks = element.getAnnotationsByType(Webhook.class);
613+
if (individualWebhooks.length > 0) {
614+
collector.add(new Webhooks() {
615+
@Override
616+
public Webhook[] value() {
617+
return individualWebhooks;
618+
}
619+
620+
@Override
621+
public Class<? extends Annotation> annotationType() {
622+
return Webhooks.class;
623+
}
624+
});
625+
}
626+
}
627+
628+
629+
595630
/**
596631
* Build open api with open api definition.
597632
*

springdoc-openapi-starter-webmvc-api/src/test/java/test/org/springdoc/api/v31/app9/WebHookResource.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
@Webhooks({
1414
@Webhook(
15-
name = "newPet",
15+
name = "newPet1",
1616
operation = @Operation(
1717
requestBody = @RequestBody(
1818
description = "Information about a new pet in the system",
@@ -35,7 +35,38 @@
3535
)
3636
})
3737
@Component
38-
public class WebHookResource {}
38+
public class WebHookResource {
39+
40+
@Webhook(
41+
name = "newPet",
42+
operation = @Operation(
43+
requestBody = @RequestBody(
44+
description = "Information about a new pet in the system",
45+
content = {
46+
@Content(
47+
mediaType = "application/json",
48+
schema = @Schema(
49+
description = "Webhook Pet",
50+
implementation = RequestDto.class
51+
)
52+
)
53+
}
54+
),
55+
method = "post",
56+
responses = @ApiResponse(
57+
responseCode = "200",
58+
description = "Return a 200 status to indicate that the data was received successfully"
59+
)
60+
)
61+
)
62+
public void newPet(RequestDto requestDto) {
63+
// This method is intentionally left empty.
64+
// The actual processing of the webhook data would be implemented here.
65+
System.out.println("Received new pet with personal number: " + requestDto.getPersonalNumber());
66+
}
67+
68+
69+
}
3970

4071

4172
class RequestDto {

springdoc-openapi-starter-webmvc-api/src/test/resources/results/3.1.0/app9.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@
2424
}
2525
},
2626
"webhooks": {
27+
"newPet1": {
28+
"post": {
29+
"requestBody": {
30+
"description": "Information about a new pet in the system",
31+
"content": {
32+
"application/json": {
33+
"schema": {
34+
"$ref": "#/components/schemas/RequestDto",
35+
"description": "Webhook Pet"
36+
}
37+
}
38+
}
39+
},
40+
"responses": {
41+
"200": {
42+
"description": "Return a 200 status to indicate that the data was received successfully"
43+
}
44+
}
45+
}
46+
},
2747
"newPet": {
2848
"post": {
2949
"requestBody": {

0 commit comments

Comments
 (0)