Skip to content

Commit 5af15ac

Browse files
committed
Support Registering Additional Classes with the ApplicationContext
Closes gh-71
1 parent aa0db05 commit 5af15ac

File tree

6 files changed

+257
-1
lines changed

6 files changed

+257
-1
lines changed

README.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,31 @@ static CommonsExecWebServerFactoryBean authorizationServer() {
141141

142142
If present, `CommonsExecWebServerFactoryBean` will add the resources `webjars/$beanName/application.yml` or `webjars/$beanName/application.properties` exists, then it is automatically added to the classpath as `application.yml` and `application.properties` respectively.
143143

144+
=== Adding Additional Classes to the ApplicationContext
145+
146+
It can often be helpful to add additional classes to the `ApplicationContext`.
147+
Simply adding them to the classpath does not necessarily add the class to the `ApplicationContext`.
148+
For example, if someone wants to start a Config Server instance, the `ConfigServerConfiguration` must be imported:
149+
150+
[source,java]
151+
----
152+
@Bean
153+
@DynamicPortUrl(name = "spring.cloud.config.uri")
154+
static CommonsExecWebServerFactoryBean configServer() {
155+
// @formatter:off
156+
return CommonsExecWebServerFactoryBean.builder()
157+
.defaultSpringBootApplicationMain()
158+
.setAdditionalBeanClassNames("org.springframework.cloud.config.server.config.ConfigServerConfiguration")
159+
.classpath((classpath) -> classpath
160+
.entries(springBootStarter("web"))
161+
.entries(new MavenClasspathEntry("org.springframework.cloud:spring-cloud-config-server:4.2.0"))
162+
);
163+
// @formatter:on
164+
}
165+
----
166+
167+
168+
144169
[[dynamicproperty]]
145170
== @DynamicProperty
146171

spring-boot-testjars/src/main/java/org/springframework/experimental/boot/server/exec/CommonsExecWebServerFactoryBean.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.experimental.boot.server.exec.imports.SpringBootApplicationMain;
3333
import org.springframework.util.Assert;
3434
import org.springframework.util.ClassUtils;
35+
import org.springframework.util.StringUtils;
3536

3637
/**
3738
* Creates a {@link CommonsExecWebServer}. If the resource
@@ -103,6 +104,17 @@ public CommonsExecWebServerFactoryBean classpath(Consumer<ClasspathBuilder> conf
103104
return this;
104105
}
105106

107+
/**
108+
* Sets additional class names that should be added to the
109+
* {@link org.springframework.context.ApplicationContext}.
110+
* @param additionalBeanClassNames the class names that should be added.
111+
* @return the {@link CommonsExecWebServerFactoryBean} for customization.
112+
*/
113+
public CommonsExecWebServerFactoryBean setAdditionalBeanClassNames(String... additionalBeanClassNames) {
114+
return systemProperties((props) -> props.put("testjars.additionalBeanClassNames",
115+
StringUtils.arrayToCommaDelimitedString(additionalBeanClassNames)));
116+
}
117+
106118
public CommonsExecWebServerFactoryBean systemProperties(Consumer<Map<String, String>> systemProperties) {
107119
systemProperties.accept(this.systemProperties);
108120
return this;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2012-2025 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.experimental.boot.server.exec.imports;
18+
19+
import org.apache.commons.logging.Log;
20+
import org.apache.commons.logging.LogFactory;
21+
22+
import org.springframework.beans.factory.support.RootBeanDefinition;
23+
import org.springframework.context.ApplicationContextInitializer;
24+
import org.springframework.context.support.GenericApplicationContext;
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* Adds beans with the class names found in the environment entry
29+
* {@link #ADDITIONAL_BEAN_CLASS_NAMES} specified as a comma-delimited list.
30+
*
31+
* @author Rob Winch
32+
*/
33+
class RegisterBeanDefinitionsInitializer implements ApplicationContextInitializer<GenericApplicationContext> {
34+
35+
private Log logger = LogFactory.getLog(getClass());
36+
37+
static final String ADDITIONAL_BEAN_CLASS_NAMES = "testjars.additionalBeanClassNames";
38+
39+
@Override
40+
public void initialize(GenericApplicationContext applicationContext) {
41+
String additionalBeanClassNames = applicationContext.getEnvironment().getProperty(ADDITIONAL_BEAN_CLASS_NAMES);
42+
if (StringUtils.hasLength(additionalBeanClassNames)) {
43+
if (this.logger.isDebugEnabled()) {
44+
this.logger.debug("Adding the following additional classes to the ApplicationContext "
45+
+ additionalBeanClassNames);
46+
}
47+
for (String beanClassName : StringUtils.delimitedListToStringArray(additionalBeanClassNames, ",")) {
48+
try {
49+
Class<?> config = applicationContext.getClassLoader().loadClass(beanClassName);
50+
applicationContext.registerBeanDefinition("testjars_" + config.getName(),
51+
new RootBeanDefinition(config));
52+
}
53+
catch (ClassNotFoundException ex) {
54+
throw new RuntimeException("Unable to register bean of type " + beanClassName, ex);
55+
}
56+
}
57+
}
58+
59+
}
60+
61+
}

spring-boot-testjars/src/main/java/org/springframework/experimental/boot/server/exec/imports/SpringBootApplicationMain.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
public class SpringBootApplicationMain {
2424

2525
public static void main(String[] args) {
26-
SpringApplication.run(SpringBootApplicationMain.class, args);
26+
SpringApplication spring = new SpringApplication(SpringBootApplicationMain.class);
27+
spring.addInitializers(new RegisterBeanDefinitionsInitializer());
28+
spring.run(args);
2729
}
2830

2931
}

spring-boot-testjars/src/test/java/org/springframework/experimental/boot/server/exec/CommonsExecWebServerFactoryBeanTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,25 @@ void classpathWhenYamlDefaults() throws Exception {
158158
});
159159
}
160160

161+
@Test
162+
void setAdditionalBeanClassNamesWhenSingleName() {
163+
String envName = "testjars.additionalBeanClassNames";
164+
String className = "org.springframework.cloud.config.server.config.ConfigServerConfiguration";
165+
CommonsExecWebServerFactoryBean factory = CommonsExecWebServerFactoryBean.builder()
166+
.setAdditionalBeanClassNames(className)
167+
.systemProperties((props) -> assertThat(props).containsEntry(envName, className));
168+
}
169+
170+
@Test
171+
void setAdditionalBeanClassNamesWhenMultipleNames() {
172+
String envName = "testjars.additionalBeanClassNames";
173+
String[] classNames = { "org.springframework.cloud.config.server.config.ConfigServerConfiguration",
174+
"other.ClassName" };
175+
CommonsExecWebServerFactoryBean factory = CommonsExecWebServerFactoryBean.builder()
176+
.setAdditionalBeanClassNames(classNames).systemProperties(
177+
(props) -> assertThat(props).containsEntry(envName, classNames[0] + "," + classNames[1]));
178+
}
179+
161180
private void assertClasspathContainsResourceWithContent(List<ClasspathEntry> classpath, String resourceName,
162181
String expectedContent) {
163182
ClasspathEntry lastEntry = classpath.get(classpath.size() - 1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright 2012-2023 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.experimental.boot.server.exec.imports;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.extension.ExtendWith;
21+
import org.mockito.ArgumentCaptor;
22+
import org.mockito.Mock;
23+
import org.mockito.junit.jupiter.MockitoExtension;
24+
25+
import org.springframework.beans.factory.support.RootBeanDefinition;
26+
import org.springframework.context.support.GenericApplicationContext;
27+
import org.springframework.core.env.ConfigurableEnvironment;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
31+
import static org.mockito.ArgumentMatchers.eq;
32+
import static org.mockito.BDDMockito.given;
33+
import static org.mockito.Mockito.verify;
34+
import static org.mockito.Mockito.verifyNoMoreInteractions;
35+
36+
/**
37+
* Tests {@link RegisterBeanDefinitionsInitializer}.
38+
*
39+
* @author Rob Winch
40+
*/
41+
@ExtendWith(MockitoExtension.class)
42+
class RegisterBeanDefinitionsInitializerTests {
43+
44+
@Mock
45+
GenericApplicationContext context;
46+
47+
@Mock
48+
ConfigurableEnvironment environment;
49+
50+
@Mock
51+
ClassLoader loader;
52+
53+
RegisterBeanDefinitionsInitializer initializer = new RegisterBeanDefinitionsInitializer();
54+
55+
@Test
56+
void initializeWhenNullEnvironmentEntry() {
57+
given(this.context.getEnvironment()).willReturn(this.environment);
58+
59+
this.initializer.initialize(this.context);
60+
61+
verify(this.context).getEnvironment();
62+
verifyNoMoreInteractions(this.context);
63+
}
64+
65+
@Test
66+
void initializeWhenEmptyEnvironmentEntry() {
67+
given(this.context.getEnvironment()).willReturn(this.environment);
68+
given(this.environment.getProperty(RegisterBeanDefinitionsInitializer.ADDITIONAL_BEAN_CLASS_NAMES))
69+
.willReturn("");
70+
71+
this.initializer.initialize(this.context);
72+
73+
verify(this.context).getEnvironment();
74+
verifyNoMoreInteractions(this.context);
75+
}
76+
77+
@Test
78+
void initializeWhenClassNotFoundException() throws Exception {
79+
String classNames = ClassA.class.getName();
80+
given(this.context.getEnvironment()).willReturn(this.environment);
81+
given(this.environment.getProperty(RegisterBeanDefinitionsInitializer.ADDITIONAL_BEAN_CLASS_NAMES))
82+
.willReturn(classNames);
83+
given(this.context.getClassLoader()).willReturn(this.loader);
84+
given(this.loader.loadClass(ClassA.class.getName())).willThrow(new ClassNotFoundException("Not found"));
85+
86+
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> this.initializer.initialize(this.context))
87+
.withMessageContaining("Unable to register bean of type " + ClassA.class.getName());
88+
}
89+
90+
@Test
91+
void initializeWhenSingleClass() throws Exception {
92+
String classNames = ClassA.class.getName();
93+
given(this.context.getEnvironment()).willReturn(this.environment);
94+
given(this.environment.getProperty(RegisterBeanDefinitionsInitializer.ADDITIONAL_BEAN_CLASS_NAMES))
95+
.willReturn(classNames);
96+
given(this.context.getClassLoader()).willReturn(this.loader);
97+
Class beanClass = ClassA.class;
98+
given(this.loader.loadClass(ClassA.class.getName())).willReturn(beanClass);
99+
ArgumentCaptor<RootBeanDefinition> beanDefCaptor = ArgumentCaptor.forClass(RootBeanDefinition.class);
100+
101+
this.initializer.initialize(this.context);
102+
103+
verify(this.context).registerBeanDefinition(eq("testjars_" + ClassA.class.getName()), beanDefCaptor.capture());
104+
assertThat(beanDefCaptor.getValue().getBeanClass()).isEqualTo(ClassA.class);
105+
}
106+
107+
@Test
108+
void initializeWhenMultiClass() throws Exception {
109+
String classNames = ClassA.class.getName() + "," + ClassB.class.getName();
110+
RegisterBeanDefinitionsInitializer initializer = new RegisterBeanDefinitionsInitializer();
111+
given(this.context.getEnvironment()).willReturn(this.environment);
112+
given(this.environment.getProperty(RegisterBeanDefinitionsInitializer.ADDITIONAL_BEAN_CLASS_NAMES))
113+
.willReturn(classNames);
114+
given(this.context.getClassLoader()).willReturn(this.loader);
115+
Class beanClassA = ClassA.class;
116+
given(this.loader.loadClass(ClassA.class.getName())).willReturn(beanClassA);
117+
Class beanClassB = ClassB.class;
118+
given(this.loader.loadClass(ClassB.class.getName())).willReturn(beanClassB);
119+
ArgumentCaptor<RootBeanDefinition> beanDefCaptor = ArgumentCaptor.forClass(RootBeanDefinition.class);
120+
121+
initializer.initialize(this.context);
122+
123+
verify(this.context).registerBeanDefinition(eq("testjars_" + ClassA.class.getName()), beanDefCaptor.capture());
124+
assertThat(beanDefCaptor.getValue().getBeanClass()).isEqualTo(ClassA.class);
125+
verify(this.context).registerBeanDefinition(eq("testjars_" + ClassB.class.getName()), beanDefCaptor.capture());
126+
assertThat(beanDefCaptor.getValue().getBeanClass()).isEqualTo(ClassB.class);
127+
}
128+
129+
static class ClassA {
130+
131+
}
132+
133+
static class ClassB {
134+
135+
}
136+
137+
}

0 commit comments

Comments
 (0)