Skip to content

Commit 3d7ab4e

Browse files
committed
Add failure analyzer for missing web factory bean
Closes gh-28926
1 parent 63e8117 commit 3d7ab4e

File tree

9 files changed

+217
-7
lines changed

9 files changed

+217
-7
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContext.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,12 +18,14 @@
1818

1919
import org.springframework.beans.BeansException;
2020
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
21+
import org.springframework.boot.WebApplicationType;
2122
import org.springframework.boot.availability.AvailabilityChangeEvent;
2223
import org.springframework.boot.availability.ReadinessState;
2324
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
2425
import org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle;
2526
import org.springframework.boot.web.reactive.server.ReactiveWebServerFactory;
2627
import org.springframework.boot.web.server.WebServer;
28+
import org.springframework.boot.web.server.context.MissingWebServerFactoryBeanException;
2729
import org.springframework.context.ApplicationContextException;
2830
import org.springframework.core.metrics.StartupStep;
2931
import org.springframework.http.server.reactive.HttpHandler;
@@ -105,8 +107,8 @@ protected String getWebServerFactoryBeanName() {
105107
// Use bean names so that we don't consider the hierarchy
106108
String[] beanNames = getBeanFactory().getBeanNamesForType(ReactiveWebServerFactory.class);
107109
if (beanNames.length == 0) {
108-
throw new ApplicationContextException(
109-
"Unable to start ReactiveWebApplicationContext due to missing ReactiveWebServerFactory bean.");
110+
throw new MissingWebServerFactoryBeanException(this.getClass(), ReactiveWebServerFactory.class,
111+
WebApplicationType.REACTIVE);
110112
}
111113
if (beanNames.length > 1) {
112114
throw new ApplicationContextException("Unable to start ReactiveWebApplicationContext due to multiple "
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.server.context;
18+
19+
import org.springframework.boot.WebApplicationType;
20+
import org.springframework.context.ApplicationContextException;
21+
import org.springframework.lang.NonNull;
22+
23+
/**
24+
* Throws exception when web server factory bean is missing.
25+
*
26+
* @author Guirong Hu
27+
* @since 2.x
28+
*/
29+
@SuppressWarnings("serial")
30+
public class MissingWebServerFactoryBeanException extends ApplicationContextException {
31+
32+
private final WebApplicationType webApplicationType;
33+
34+
/**
35+
* Create a new {@code MissingWebServerFactoryBeanException} with the given web
36+
* application context class and the given web server factory class and the given type
37+
* of web application.
38+
* @param webApplicationContextClass the web application context class
39+
* @param webServerFactoryClass the web server factory class
40+
* @param webApplicationType the web application type
41+
*/
42+
public MissingWebServerFactoryBeanException(@NonNull Class<?> webApplicationContextClass,
43+
@NonNull Class<?> webServerFactoryClass, @NonNull WebApplicationType webApplicationType) {
44+
super(String.format("Unable to start %s due to missing %s bean.", webApplicationContextClass.getSimpleName(),
45+
webServerFactoryClass.getSimpleName()));
46+
this.webApplicationType = webApplicationType;
47+
}
48+
49+
/**
50+
* Returns the type of web application that is being run.
51+
* @return the type of web application
52+
* @since 2.x
53+
*/
54+
public WebApplicationType getWebApplicationType() {
55+
return this.webApplicationType;
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.server.context;
18+
19+
import org.springframework.boot.WebApplicationType;
20+
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
21+
import org.springframework.boot.diagnostics.FailureAnalysis;
22+
import org.springframework.boot.diagnostics.FailureAnalyzer;
23+
import org.springframework.context.ApplicationContextException;
24+
25+
/**
26+
* A {@link FailureAnalyzer} that performs analysis of failures caused by an
27+
* {@link MissingWebServerFactoryBeanException}.
28+
*
29+
* @author Guirong Hu
30+
*/
31+
class MissingWebServerFactoryBeanFailureAnalyzer extends AbstractFailureAnalyzer<ApplicationContextException> {
32+
33+
@Override
34+
protected FailureAnalysis analyze(Throwable rootFailure, ApplicationContextException cause) {
35+
Throwable rootCause = cause.getCause();
36+
if (rootCause instanceof MissingWebServerFactoryBeanException) {
37+
WebApplicationType webApplicationType = ((MissingWebServerFactoryBeanException) rootCause)
38+
.getWebApplicationType();
39+
return new FailureAnalysis(String.format(
40+
"Reason: The running web application is of type %s, but the dependent class is missing.",
41+
webApplicationType.name().toLowerCase()), action(webApplicationType), cause);
42+
}
43+
return null;
44+
}
45+
46+
private String action(WebApplicationType webApplicationType) {
47+
String action = "Add `%s` dependencies to your application.";
48+
if (webApplicationType == WebApplicationType.SERVLET) {
49+
return String.format(action, "spring-boot-starter-web");
50+
}
51+
return String.format(action, "spring-boot-starter-webflux");
52+
}
53+
54+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Web server integrations with Spring's
19+
* {@link org.springframework.context.ApplicationContext ApplicationContext}.
20+
*/
21+
package org.springframework.boot.web.server.context;

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/context/ServletWebServerApplicationContext.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3737
import org.springframework.beans.factory.config.Scope;
3838
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
39+
import org.springframework.boot.WebApplicationType;
3940
import org.springframework.boot.availability.AvailabilityChangeEvent;
4041
import org.springframework.boot.availability.ReadinessState;
4142
import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext;
4243
import org.springframework.boot.web.context.WebServerGracefulShutdownLifecycle;
4344
import org.springframework.boot.web.server.WebServer;
45+
import org.springframework.boot.web.server.context.MissingWebServerFactoryBeanException;
4446
import org.springframework.boot.web.servlet.FilterRegistrationBean;
4547
import org.springframework.boot.web.servlet.ServletContextInitializer;
4648
import org.springframework.boot.web.servlet.ServletContextInitializerBeans;
@@ -206,8 +208,8 @@ protected ServletWebServerFactory getWebServerFactory() {
206208
// Use bean names so that we don't consider the hierarchy
207209
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
208210
if (beanNames.length == 0) {
209-
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
210-
+ "ServletWebServerFactory bean.");
211+
throw new MissingWebServerFactoryBeanException(this.getClass(), ServletWebServerFactory.class,
212+
WebApplicationType.SERVLET);
211213
}
212214
if (beanNames.length > 1) {
213215
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "

spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFa
7474
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer,\
7575
org.springframework.boot.diagnostics.analyzer.PatternParseFailureAnalyzer,\
7676
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer,\
77-
org.springframework.boot.web.embedded.tomcat.ConnectorStartFailureAnalyzer
77+
org.springframework.boot.web.embedded.tomcat.ConnectorStartFailureAnalyzer,\
78+
org.springframework.boot.web.server.context.MissingWebServerFactoryBeanFailureAnalyzer
7879

7980
# Failure Analysis Reporters
8081
org.springframework.boot.diagnostics.FailureAnalysisReporter=\

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/context/ReactiveWebServerApplicationContextTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ void cleanUp() {
6161
void whenThereIsNoWebServerFactoryBeanThenContextRefreshWillFail() {
6262
assertThatExceptionOfType(ApplicationContextException.class).isThrownBy(() -> this.context.refresh())
6363
.withMessageContaining(
64-
"Unable to start ReactiveWebApplicationContext due to missing ReactiveWebServerFactory bean");
64+
"Unable to start ReactiveWebServerApplicationContext due to missing ReactiveWebServerFactory bean");
6565
}
6666

6767
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2012-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.web.server.context;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.diagnostics.FailureAnalysis;
22+
import org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext;
23+
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
24+
import org.springframework.context.ApplicationContextException;
25+
import org.springframework.context.ConfigurableApplicationContext;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
/**
30+
* Tests for {@link MissingWebServerFactoryBeanFailureAnalyzer}.
31+
*
32+
* @author Guirong Hu
33+
*/
34+
class MissingWebServerFactoryBeanFailureAnalyzerTests {
35+
36+
@Test
37+
void missingServletWebServerFactoryBeanFailure() {
38+
ApplicationContextException failure = createFailure(new ServletWebServerApplicationContext());
39+
assertThat(failure).isNotNull();
40+
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
41+
assertThat(analysis).isNotNull();
42+
assertThat(analysis.getDescription()).isEqualTo(
43+
"Reason: The running web application is of type servlet, but the dependent class is missing.");
44+
assertThat(analysis.getAction()).isEqualTo("Add `spring-boot-starter-web` dependencies to your application.");
45+
}
46+
47+
@Test
48+
void missingReactiveWebServerFactoryBeanFailure() {
49+
ApplicationContextException failure = createFailure(new ReactiveWebServerApplicationContext());
50+
FailureAnalysis analysis = new MissingWebServerFactoryBeanFailureAnalyzer().analyze(failure);
51+
assertThat(analysis).isNotNull();
52+
assertThat(analysis.getDescription()).isEqualTo(
53+
"Reason: The running web application is of type reactive, but the dependent class is missing.");
54+
assertThat(analysis.getAction())
55+
.isEqualTo("Add `spring-boot-starter-webflux` dependencies to your application.");
56+
}
57+
58+
private ApplicationContextException createFailure(ConfigurableApplicationContext context) {
59+
try {
60+
context.refresh();
61+
context.close();
62+
return null;
63+
}
64+
catch (ApplicationContextException ex) {
65+
return ex;
66+
}
67+
}
68+
69+
}

src/checkstyle/import-control.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
</subpackage>
8383
<subpackage name="server">
8484
<disallow pkg="org.springframework.context" />
85+
<subpackage name="context">
86+
<allow pkg="org.springframework.context" />
87+
</subpackage>
8588
</subpackage>
8689
<subpackage name="context">
8790
<allow pkg="org.springframework.boot.web.server" />

0 commit comments

Comments
 (0)