Skip to content

Commit fa9079b

Browse files
author
bnasslahsen
committed
Initial support of Webflux with Functional Endpoints
1 parent b748a1b commit fa9079b

File tree

24 files changed

+1489
-1
lines changed

24 files changed

+1489
-1
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/core/MethodAttributes.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ else if (reqMappingMethod != null) {
132132
else if (reqMappingClass != null) {
133133
fillMethods(reqMappingClass.produces(), reqMappingClass.consumes(), reqMappingClass.headers());
134134
}
135+
else
136+
fillMethods(null, null, null);
135137
}
136138

137139
private void fillMethods(String[] produces, String[] consumes, String[] headers) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package org.springdoc.webflux.annotations;
20+
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Inherited;
24+
import java.lang.annotation.Retention;
25+
import java.lang.annotation.RetentionPolicy;
26+
import java.lang.annotation.Target;
27+
28+
import org.springframework.web.bind.annotation.RequestMapping;
29+
import org.springframework.web.bind.annotation.RequestMethod;
30+
31+
/**
32+
* The annotation may be used to define an operation method as an OpenAPI Operation, and/or to define additional
33+
* properties for the Operation.
34+
* <p>The following fields can also alternatively be defined at method level (as repeatable annotations in case of arrays),
35+
*
36+
**/
37+
@Target({ ElementType.TYPE, ElementType.METHOD})
38+
@Retention(RetentionPolicy.RUNTIME)
39+
@Inherited
40+
public @interface RouterOperation {
41+
42+
43+
/**
44+
* Alias for {@link RequestMapping#path}.
45+
*/
46+
String path();
47+
48+
/**
49+
* Alias for {@link RequestMapping#method}.
50+
*/
51+
RequestMethod[] method();
52+
53+
/**
54+
* Alias for {@link RequestMapping#consumes}.
55+
*/
56+
String[] consumes() default {};
57+
58+
/**
59+
* Alias for {@link RequestMapping#produces}.
60+
*/
61+
String[] produces() default {};
62+
63+
/**
64+
* The class of the Handler bean.
65+
*
66+
* @return the class of the Bean
67+
**/
68+
Class<?> beanClass() default Void.class;;
69+
70+
/**
71+
* The method of the handler Bean.
72+
*
73+
* @return The method of the handler Bean.
74+
**/
75+
String beanMethod() default "";
76+
77+
/**
78+
* The parameters of the handler method.
79+
*
80+
* @return The parameters of the handler method.
81+
**/
82+
Class<?>[] parameterTypes() default {};
83+
84+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package org.springdoc.webflux.annotations;
20+
21+
import java.lang.annotation.Inherited;
22+
import java.lang.annotation.Retention;
23+
import java.lang.annotation.RetentionPolicy;
24+
import java.lang.annotation.Target;
25+
26+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
27+
import static java.lang.annotation.ElementType.METHOD;
28+
29+
/**
30+
* Container for repeatable {@link RouterOperation} annotation
31+
*
32+
* @see RouterOperation
33+
*/
34+
@Target({METHOD, ANNOTATION_TYPE})
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@Inherited
37+
public @interface RouterOperations {
38+
/**
39+
* An array of RouterOperation Objects for the Router
40+
*
41+
* @return the RouterOperation
42+
*/
43+
RouterOperation[] value() default {};
44+
}

springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/OpenApiResource.java

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818

1919
package org.springdoc.webflux.api;
2020

21+
import java.lang.annotation.Annotation;
22+
import java.lang.reflect.Method;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.HashSet;
2126
import java.util.LinkedHashMap;
2227
import java.util.List;
2328
import java.util.Map;
@@ -30,6 +35,10 @@
3035
import io.swagger.v3.core.util.Yaml;
3136
import io.swagger.v3.oas.annotations.Operation;
3237
import io.swagger.v3.oas.models.OpenAPI;
38+
import org.apache.commons.lang3.ArrayUtils;
39+
import org.apache.commons.lang3.StringUtils;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
3342
import org.springdoc.api.AbstractOpenApiResource;
3443
import org.springdoc.core.AbstractRequestBuilder;
3544
import org.springdoc.core.GenericResponseBuilder;
@@ -38,16 +47,25 @@
3847
import org.springdoc.core.SpringDocConfigProperties;
3948
import org.springdoc.core.customizers.OpenApiCustomiser;
4049
import org.springdoc.core.customizers.OperationCustomizer;
50+
import org.springdoc.webflux.annotations.RouterOperation;
51+
import org.springdoc.webflux.annotations.RouterOperations;
4152
import reactor.core.publisher.Mono;
4253

4354
import org.springframework.beans.factory.annotation.Autowired;
4455
import org.springframework.beans.factory.annotation.Value;
56+
import org.springframework.beans.factory.config.BeanDefinition;
57+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
58+
import org.springframework.context.ApplicationContext;
59+
import org.springframework.core.type.StandardMethodMetadata;
4560
import org.springframework.http.MediaType;
4661
import org.springframework.http.server.reactive.ServerHttpRequest;
62+
import org.springframework.util.CollectionUtils;
4763
import org.springframework.web.bind.annotation.GetMapping;
4864
import org.springframework.web.bind.annotation.RequestMethod;
4965
import org.springframework.web.bind.annotation.RestController;
5066
import org.springframework.web.method.HandlerMethod;
67+
import org.springframework.web.reactive.HandlerMapping;
68+
import org.springframework.web.reactive.function.server.RouterFunction;
5169
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
5270
import org.springframework.web.reactive.result.method.RequestMappingInfo;
5371
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
@@ -62,8 +80,13 @@
6280
@RestController
6381
public class OpenApiResource extends AbstractOpenApiResource {
6482

83+
private static final Logger LOGGER = LoggerFactory.getLogger(OpenApiResource.class);
84+
6585
private final RequestMappingInfoHandlerMapping requestMappingHandlerMapping;
6686

87+
@Autowired
88+
private List<HandlerMapping> handlerMappings;
89+
6790
public OpenApiResource(String groupName, OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder requestBuilder,
6891
GenericResponseBuilder responseBuilder, OperationBuilder operationParser,
6992
RequestMappingInfoHandlerMapping requestMappingHandlerMapping,
@@ -79,7 +102,7 @@ public OpenApiResource(OpenAPIBuilder openAPIBuilder, AbstractRequestBuilder req
79102
RequestMappingInfoHandlerMapping requestMappingHandlerMapping,
80103
Optional<List<OperationCustomizer>> operationCustomizers,
81104
Optional<List<OpenApiCustomiser>> openApiCustomisers, SpringDocConfigProperties springDocConfigProperties) {
82-
super(DEFAULT_GROUP_NAME, openAPIBuilder, requestBuilder, responseBuilder, operationParser,operationCustomizers, openApiCustomisers, springDocConfigProperties);
105+
super(DEFAULT_GROUP_NAME, openAPIBuilder, requestBuilder, responseBuilder, operationParser, operationCustomizers, openApiCustomisers, springDocConfigProperties);
83106
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
84107
}
85108

@@ -123,6 +146,51 @@ protected void getPaths(Map<String, Object> restControllers) {
123146
}
124147
}
125148
}
149+
150+
ApplicationContext applicationContext = (ApplicationContext) requestMappingHandlerMapping.getApplicationContext();
151+
Map<String, RouterFunction> routerBeans = applicationContext.getBeansOfType(RouterFunction.class);
152+
153+
for (Map.Entry<String, RouterFunction> entry : routerBeans.entrySet()) {
154+
List<RouterOperation> routerOperationList = new ArrayList<>();
155+
RouterOperations routerOperations = applicationContext.findAnnotationOnBean(entry.getKey(), RouterOperations.class);
156+
if (routerOperations == null) {
157+
RouterOperation routerOperation = applicationContext.findAnnotationOnBean(entry.getKey(), RouterOperation.class);
158+
routerOperationList.add(routerOperation);
159+
}
160+
else
161+
routerOperationList.addAll(Arrays.asList(routerOperations.value()));
162+
163+
if (!CollectionUtils.isEmpty(routerOperationList)) {
164+
for (RouterOperation routerOperation : routerOperationList) {
165+
if (!Void.class.equals(routerOperation.beanClass())) {
166+
Object handlerBean = applicationContext.getBean(routerOperation.beanClass());
167+
HandlerMethod handlerMethod = null;
168+
if (StringUtils.isNotBlank(routerOperation.beanMethod())) {
169+
try {
170+
if (ArrayUtils.isEmpty(routerOperation.parameterTypes())) {
171+
Optional<Method> methodOptional = Arrays.stream(handlerBean.getClass().getDeclaredMethods())
172+
.filter(method1 -> routerOperation.beanMethod().equals(method1.getName()) && method1.getParameters().length == 0)
173+
.findAny();
174+
if (!methodOptional.isPresent())
175+
methodOptional = Arrays.stream(handlerBean.getClass().getDeclaredMethods())
176+
.filter(method1 -> routerOperation.beanMethod().equals(method1.getName()))
177+
.findAny();
178+
if (methodOptional.isPresent())
179+
handlerMethod = new HandlerMethod(handlerBean, methodOptional.get());
180+
}
181+
else
182+
handlerMethod = new HandlerMethod(handlerBean, routerOperation.beanMethod(), routerOperation.parameterTypes());
183+
}
184+
catch (NoSuchMethodException e) {
185+
LOGGER.error(e.getMessage());
186+
}
187+
if (handlerMethod != null)
188+
calculatePath(handlerMethod, routerOperation.path(), new HashSet<>(Arrays.asList(routerOperation.method())));
189+
}
190+
}
191+
}
192+
}
193+
}
126194
}
127195

128196
protected void calculateServerUrl(ServerHttpRequest serverHttpRequest, String apiDocsUrl) {
@@ -131,4 +199,23 @@ protected void calculateServerUrl(ServerHttpRequest serverHttpRequest, String ap
131199
openAPIBuilder.setServerBaseUrl(serverBaseUrl);
132200
}
133201

202+
public List<String> getBeansWithAnnotation(ConfigurableListableBeanFactory factory, Class<? extends Annotation> type) {
203+
204+
List<String> result = new ArrayList<>();
205+
206+
for (String name : factory.getBeanDefinitionNames()) {
207+
BeanDefinition bd = factory.getBeanDefinition(name);
208+
209+
if (bd.getSource() instanceof StandardMethodMetadata) {
210+
StandardMethodMetadata metadata = (StandardMethodMetadata) bd.getSource();
211+
212+
Map<String, Object> attributes = metadata.getAnnotationAttributes(type.getName());
213+
if (null == attributes) {
214+
continue;
215+
}
216+
}
217+
}
218+
219+
return result;
220+
}
134221
}

springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/core/RequestBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.http.codec.multipart.FilePart;
3232
import org.springframework.http.server.reactive.ServerHttpRequest;
3333
import org.springframework.http.server.reactive.ServerHttpResponse;
34+
import org.springframework.web.reactive.function.server.ServerRequest;
3435
import org.springframework.web.server.ServerWebExchange;
3536

3637
import static org.springdoc.core.SpringDocUtils.getConfig;
@@ -40,6 +41,7 @@ public class RequestBuilder extends AbstractRequestBuilder {
4041
static {
4142
getConfig().addRequestWrapperToIgnore(ServerWebExchange.class, ServerHttpRequest.class)
4243
.addRequestWrapperToIgnore(ServerHttpResponse.class)
44+
.addRequestWrapperToIgnore(ServerRequest.class)
4345
.addFileType(FilePart.class);
4446
}
4547

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package test.org.springdoc.api.app69;
2+
3+
import org.springdoc.webflux.annotations.RouterOperation;
4+
import org.springdoc.webflux.annotations.RouterOperations;
5+
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.web.bind.annotation.RequestMethod;
10+
import org.springframework.web.reactive.function.server.RouterFunction;
11+
import org.springframework.web.reactive.function.server.ServerResponse;
12+
13+
import static org.springframework.web.reactive.function.server.RequestPredicates.DELETE;
14+
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
15+
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
16+
import static org.springframework.web.reactive.function.server.RequestPredicates.PUT;
17+
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
18+
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
19+
20+
@Configuration
21+
public class RoutingConfiguration {
22+
23+
@Bean
24+
@RouterOperations({ @RouterOperation(path = "/api/user/index", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getAllUsers", consumes = MediaType.APPLICATION_JSON_VALUE),
25+
@RouterOperation(path = "/api/user/byFirstName", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getAllUsers", parameterTypes = {String.class} , consumes = MediaType.APPLICATION_JSON_VALUE),
26+
@RouterOperation(path = "/api/user/{id}", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getUserById", consumes = MediaType.APPLICATION_JSON_VALUE),
27+
@RouterOperation(path = "/api/user/post", method = RequestMethod.POST, beanClass = UserRepository.class, beanMethod = "saveUser", consumes = MediaType.APPLICATION_JSON_VALUE),
28+
@RouterOperation(path = "/api/user/put/{id}", method = RequestMethod.PUT, beanClass = UserRepository.class, beanMethod = "putUser", consumes = MediaType.APPLICATION_JSON_VALUE),
29+
@RouterOperation(path = "/api/user/delete/{id}", method = RequestMethod.DELETE, beanClass = UserRepository.class, beanMethod = "deleteUser", consumes = MediaType.APPLICATION_JSON_VALUE) })
30+
public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
31+
return route(GET("/api/user/index").and(accept(MediaType.APPLICATION_JSON)), userHandler::getAll)
32+
.andRoute(GET("/api/user/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUser)
33+
.andRoute(POST("/api/user/post").and(accept(MediaType.APPLICATION_JSON)), userHandler::postUser)
34+
.andRoute(PUT("/api/user/put/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::putUser)
35+
.andRoute(DELETE("/api/user/delete/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::deleteUser);
36+
}
37+
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app69;
20+
21+
import test.org.springdoc.api.AbstractSpringDocTest;
22+
23+
import org.springframework.boot.autoconfigure.SpringBootApplication;
24+
import org.springframework.context.annotation.ComponentScan;
25+
26+
public class SpringDocApp69Test extends AbstractSpringDocTest {
27+
28+
@SpringBootApplication
29+
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.app69" })
30+
static class SpringDocTestApp {}
31+
}

0 commit comments

Comments
 (0)