Skip to content

Commit 843ae67

Browse files
committed
Application won't compile when OpenApi and spring-boot-data-rest/spring-boot-starter-data-rest is present. Fixes #3195
1 parent b403f82 commit 843ae67

4 files changed

Lines changed: 175 additions & 19 deletions

File tree

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/configuration/SpringDocDataRestConfiguration.java

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
5252
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
5353
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
54+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
5455
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
5556
import org.springframework.boot.hateoas.autoconfigure.HateoasProperties;
5657
import org.springframework.context.annotation.Bean;
@@ -84,19 +85,63 @@
8485
public class SpringDocDataRestConfiguration {
8586

8687
/**
87-
* Hal provider data rest hal provider.
88+
* Configuration for DataRestHalProvider when HateoasProperties is on the classpath.
8889
*
89-
* @param repositoryRestConfiguration the repository rest configuration
90-
* @param hateoasPropertiesOptional the hateoas properties optional
91-
* @param objectMapperProvider the object mapper provider
92-
* @return the data rest hal provider
90+
* @author bnasslahsen
9391
*/
94-
@Bean
95-
@ConditionalOnMissingBean
96-
@Primary
97-
@Lazy(false)
98-
DataRestHalProvider halProvider(Optional<RepositoryRestConfiguration> repositoryRestConfiguration, Optional<HateoasProperties> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
99-
return new DataRestHalProvider(repositoryRestConfiguration, hateoasPropertiesOptional, objectMapperProvider);
92+
@Configuration(proxyBeanMethods = false)
93+
@ConditionalOnClass(name = {
94+
"org.springframework.boot.hateoas.autoconfigure.HateoasProperties",
95+
"org.springframework.data.rest.core.config.RepositoryRestConfiguration"
96+
})
97+
static class DataRestHateoasPropertiesConfiguration {
98+
99+
/**
100+
* Hal provider data rest hal provider.
101+
*
102+
* @param repositoryRestConfiguration the repository rest configuration
103+
* @param hateoasPropertiesOptional the hateoas properties optional
104+
* @param objectMapperProvider the object mapper provider
105+
* @return the data rest hal provider
106+
*/
107+
@Bean
108+
@ConditionalOnMissingBean
109+
@Primary
110+
@Lazy(false)
111+
DataRestHalProvider halProvider(Optional<RepositoryRestConfiguration> repositoryRestConfiguration,
112+
Optional<HateoasProperties> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
113+
return new DataRestHalProvider(repositoryRestConfiguration, hateoasPropertiesOptional,
114+
objectMapperProvider);
115+
}
116+
117+
}
118+
119+
/**
120+
* Fallback configuration for DataRestHalProvider when HateoasProperties is absent.
121+
*
122+
* @author bnasslahsen
123+
*/
124+
@Configuration(proxyBeanMethods = false)
125+
@ConditionalOnClass(name = "org.springframework.data.rest.core.config.RepositoryRestConfiguration")
126+
@ConditionalOnMissingClass("org.springframework.boot.hateoas.autoconfigure.HateoasProperties")
127+
static class DataRestNoHateoasPropertiesConfiguration {
128+
129+
/**
130+
* Hal provider data rest hal provider.
131+
*
132+
* @param repositoryRestConfiguration the repository rest configuration
133+
* @param objectMapperProvider the object mapper provider
134+
* @return the data rest hal provider
135+
*/
136+
@Bean
137+
@ConditionalOnMissingBean
138+
@Primary
139+
@Lazy(false)
140+
DataRestHalProvider halProvider(Optional<RepositoryRestConfiguration> repositoryRestConfiguration,
141+
ObjectMapperProvider objectMapperProvider) {
142+
return new DataRestHalProvider(repositoryRestConfiguration, Optional.empty(), objectMapperProvider);
143+
}
144+
100145
}
101146

102147
/**

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/DataRestHalProvider.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
import org.springdoc.core.data.SpringDocJackson2HalModule;
3232

3333
import org.springframework.beans.factory.InitializingBean;
34-
import org.springframework.boot.hateoas.autoconfigure.HateoasProperties;
3534
import org.springframework.data.rest.core.config.RepositoryRestConfiguration;
3635

3736
/**
@@ -53,7 +52,7 @@ public class DataRestHalProvider extends HateoasHalProvider implements Initializ
5352
* @param hateoasPropertiesOptional the hateoas properties optional
5453
* @param objectMapperProvider the object mapper provider
5554
*/
56-
public DataRestHalProvider(Optional<RepositoryRestConfiguration> repositoryRestConfigurationOptional, Optional<HateoasProperties> hateoasPropertiesOptional,
55+
public DataRestHalProvider(Optional<RepositoryRestConfiguration> repositoryRestConfigurationOptional, Optional<?> hateoasPropertiesOptional,
5756
ObjectMapperProvider objectMapperProvider) {
5857
super(hateoasPropertiesOptional, objectMapperProvider);
5958
this.repositoryRestConfigurationOptional = repositoryRestConfigurationOptional;

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/providers/HateoasHalProvider.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232
import org.springdoc.core.data.SpringDocJackson2HalModule;
3333

3434
import org.springframework.beans.factory.InitializingBean;
35-
import org.springframework.boot.hateoas.autoconfigure.HateoasProperties;
36-
import org.springframework.lang.NonNull;
3735
import org.springframework.util.ReflectionUtils;
3836

3937
/**
@@ -51,25 +49,25 @@ public class HateoasHalProvider implements InitializingBean {
5149
/**
5250
* The Hateoas properties optional.
5351
*/
54-
private final Optional<HateoasProperties> hateoasPropertiesOptional;
52+
private final Optional<?> hateoasPropertiesOptional;
5553

5654
/**
5755
* Instantiates a new Hateoas hal provider.
5856
*
5957
* @param hateoasPropertiesOptional the hateoas properties optional
6058
* @param objectMapperProvider the object mapper provider
6159
*/
62-
public HateoasHalProvider(Optional<HateoasProperties> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
60+
public HateoasHalProvider(Optional<?> hateoasPropertiesOptional, ObjectMapperProvider objectMapperProvider) {
6361
this.hateoasPropertiesOptional = hateoasPropertiesOptional;
6462
this.objectMapperProvider = objectMapperProvider;
6563
}
6664

67-
private static boolean isHalEnabled(@NonNull HateoasProperties hateoasProperties) {
65+
private static boolean isHalEnabled(Object hateoasProperties) {
6866
// In spring-boot 3.5, the method name was changed from getUseHalAsDefaultJsonMediaType to isUseHalAsDefaultJsonMediaType
6967
var possibleMethodNames = List.of("isUseHalAsDefaultJsonMediaType", "getUseHalAsDefaultJsonMediaType");
7068

7169
for (var methodName : possibleMethodNames) {
72-
var method = ReflectionUtils.findMethod(HateoasProperties.class, methodName);
70+
var method = ReflectionUtils.findMethod(hateoasProperties.getClass(), methodName);
7371
if (method != null) {
7472
var result = ReflectionUtils.invokeMethod(method, hateoasProperties);
7573
if (result instanceof Boolean halEnabled) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * *
7+
* * * * * * Copyright 2019-2025 the original author or authors.
8+
* * * * * *
9+
* * * * * * Licensed under the Apache License, Version 2.0 (the "License");
10+
* * * * * * you may not use this file except in compliance with the License.
11+
* * * * * * You may obtain a copy of the License at
12+
* * * * * *
13+
* * * * * * https://www.apache.org/licenses/LICENSE-2.0
14+
* * * * * *
15+
* * * * * * Unless required by applicable law or agreed to in writing, software
16+
* * * * * * distributed under the License is distributed on an "AS IS" BASIS,
17+
* * * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* * * * * * See the License for the specific language governing permissions and
19+
* * * * * * limitations under the License.
20+
* * * * *
21+
* * * *
22+
* * *
23+
* *
24+
*
25+
*/
26+
27+
package test.org.springdoc.api.v31.app39;
28+
29+
import java.lang.reflect.Method;
30+
import java.lang.reflect.Type;
31+
import java.util.Arrays;
32+
33+
import org.junit.jupiter.api.Test;
34+
import org.springdoc.core.configuration.SpringDocDataRestConfiguration;
35+
36+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
37+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
38+
import org.springframework.context.annotation.Bean;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
42+
/**
43+
* Verifies that HateoasProperties-dependent bean methods are isolated in nested
44+
* configuration classes guarded by {@code @ConditionalOnClass} /
45+
* {@code @ConditionalOnMissingClass}, so that when HateoasProperties is absent
46+
* from the classpath, Spring never loads the class that references it.
47+
*
48+
* @author bnasslahsen
49+
*/
50+
class SpringDocApp39Test {
51+
52+
private static final String HATEOAS_PROPERTIES_CLASS = "org.springframework.boot.hateoas.autoconfigure.HateoasProperties";
53+
54+
@Test
55+
void dataRestConfigOuterClassDoesNotReferenceHateoasProperties() {
56+
assertOuterBeanMethodsDoNotReferenceHateoasProperties(SpringDocDataRestConfiguration.class);
57+
}
58+
59+
@Test
60+
void dataRestConfigHasGuardedNestedClasses() {
61+
assertHasGuardedNestedClasses(SpringDocDataRestConfiguration.class);
62+
}
63+
64+
/**
65+
* Asserts that no {@code @Bean} method declared directly on the outer
66+
* configuration class has {@code HateoasProperties} in its generic
67+
* parameter types. Before the fix, the outer class had methods like
68+
* {@code halProvider(Optional<HateoasProperties>...)} which caused
69+
* {@code TypeNotPresentException} when HateoasProperties was absent.
70+
*/
71+
private void assertOuterBeanMethodsDoNotReferenceHateoasProperties(Class<?> configClass) {
72+
for (Method method : configClass.getDeclaredMethods()) {
73+
if (method.isAnnotationPresent(Bean.class)) {
74+
for (Type type : method.getGenericParameterTypes()) {
75+
assertThat(type.getTypeName())
76+
.as("@Bean method %s.%s() should not reference HateoasProperties directly",
77+
configClass.getSimpleName(), method.getName())
78+
.doesNotContain("HateoasProperties");
79+
}
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Asserts that the configuration class has at least one nested class
86+
* guarded by {@code @ConditionalOnClass(name = "...HateoasProperties")}
87+
* and one by {@code @ConditionalOnMissingClass("...HateoasProperties")}.
88+
*/
89+
private void assertHasGuardedNestedClasses(Class<?> configClass) {
90+
boolean hasConditionalOnClass = false;
91+
boolean hasConditionalOnMissingClass = false;
92+
93+
for (Class<?> inner : configClass.getDeclaredClasses()) {
94+
ConditionalOnClass onClass = inner.getAnnotation(ConditionalOnClass.class);
95+
if (onClass != null && Arrays.asList(onClass.name()).contains(HATEOAS_PROPERTIES_CLASS)) {
96+
hasConditionalOnClass = true;
97+
}
98+
ConditionalOnMissingClass onMissingClass = inner.getAnnotation(ConditionalOnMissingClass.class);
99+
if (onMissingClass != null && Arrays.asList(onMissingClass.value()).contains(HATEOAS_PROPERTIES_CLASS)) {
100+
hasConditionalOnMissingClass = true;
101+
}
102+
}
103+
104+
assertThat(hasConditionalOnClass)
105+
.as("%s should have a nested class with @ConditionalOnClass for HateoasProperties",
106+
configClass.getSimpleName())
107+
.isTrue();
108+
assertThat(hasConditionalOnMissingClass)
109+
.as("%s should have a nested class with @ConditionalOnMissingClass for HateoasProperties",
110+
configClass.getSimpleName())
111+
.isTrue();
112+
}
113+
114+
}

0 commit comments

Comments
 (0)