Skip to content

Commit 56cf581

Browse files
czpilarfmbenhassine
authored andcommitted
Fix command level profile registration
Resolves #1328 Signed-off-by: David Pilar <david@czpilar.net>
1 parent 4f8bd1e commit 56cf581

4 files changed

Lines changed: 257 additions & 3 deletions

File tree

spring-shell-core-autoconfigure/src/main/java/org/springframework/shell/core/autoconfigure/CommandRegistryAutoConfiguration.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@
2626
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2727
import org.springframework.context.ApplicationContext;
2828
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Profile;
2930
import org.springframework.core.MethodIntrospector;
3031
import org.springframework.core.annotation.AnnotatedElementUtils;
32+
import org.springframework.core.env.Environment;
33+
import org.springframework.core.env.Profiles;
3134
import org.springframework.shell.core.command.Command;
3235
import org.springframework.shell.core.command.CommandRegistry;
3336
import org.springframework.shell.core.command.annotation.support.CommandFactoryBean;
@@ -65,7 +68,8 @@ private void registerAnnotatedCommands(ApplicationContext applicationContext, Co
6568
}
6669
log.debug("Registering commands from component: " + className);
6770
ReflectionUtils.MethodFilter filter = method -> AnnotatedElementUtils.hasAnnotation(method,
68-
org.springframework.shell.core.command.annotation.Command.class);
71+
org.springframework.shell.core.command.annotation.Command.class)
72+
&& isProfileActive(method, applicationContext.getEnvironment());
6973
Set<Method> methods = MethodIntrospector.selectMethods(type, filter);
7074
for (Method method : methods) {
7175
CommandFactoryBean factoryBean = new CommandFactoryBean(method);
@@ -75,4 +79,9 @@ private void registerAnnotatedCommands(ApplicationContext applicationContext, Co
7579
}
7680
}
7781

82+
private boolean isProfileActive(Method method, Environment environment) {
83+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
84+
return profile == null || environment.acceptsProfiles(Profiles.of(profile.value()));
85+
}
86+
7887
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2026-present 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+
package org.springframework.shell.core.autoconfigure;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
import org.springframework.boot.autoconfigure.AutoConfigurations;
21+
import org.springframework.boot.autoconfigure.SpringBootApplication;
22+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
23+
import org.springframework.context.annotation.Profile;
24+
import org.springframework.shell.core.command.Command;
25+
import org.springframework.shell.core.command.CommandRegistry;
26+
27+
import static org.junit.jupiter.api.Assertions.assertFalse;
28+
import static org.junit.jupiter.api.Assertions.assertTrue;
29+
30+
/**
31+
* Tests for {@link CommandRegistryAutoConfiguration} profile filtering support on
32+
* {@code @Command} methods.
33+
*
34+
* @author David Pilar
35+
*/
36+
class CommandRegistryAutoConfigurationProfileTests {
37+
38+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
39+
.withUserConfiguration(TestShellApplication.class)
40+
.withConfiguration(AutoConfigurations.of(SpringShellAutoConfiguration.class));
41+
42+
@Test
43+
void commandWithoutProfileShouldAlwaysBeRegistered() {
44+
this.contextRunner.run(context -> {
45+
CommandRegistry registry = context.getBean(CommandRegistry.class);
46+
assertTrue(hasCommand(registry, "always-available"),
47+
"Command without @Profile should always be registered");
48+
});
49+
}
50+
51+
@Test
52+
void commandWithProfileShouldNotBeRegisteredWhenProfileIsNotActive() {
53+
this.contextRunner.run(context -> {
54+
CommandRegistry registry = context.getBean(CommandRegistry.class);
55+
assertFalse(hasCommand(registry, "hello"),
56+
"Command with @Profile('greetings') should not be registered when profile is not active");
57+
});
58+
}
59+
60+
@Test
61+
void commandWithProfileShouldBeRegisteredWhenProfileIsActive() {
62+
this.contextRunner.withPropertyValues("spring.profiles.active=greetings").run(context -> {
63+
CommandRegistry registry = context.getBean(CommandRegistry.class);
64+
assertTrue(hasCommand(registry, "hello"),
65+
"Command with @Profile('greetings') should be registered when profile is active");
66+
});
67+
}
68+
69+
@Test
70+
void commandWithNegatedProfileShouldBeRegisteredWhenProfileIsNotActive() {
71+
this.contextRunner.run(context -> {
72+
CommandRegistry registry = context.getBean(CommandRegistry.class);
73+
assertTrue(hasCommand(registry, "debug-info"),
74+
"Command with @Profile('!production') should be registered when 'production' is not active");
75+
});
76+
}
77+
78+
@Test
79+
void commandWithNegatedProfileShouldNotBeRegisteredWhenProfileIsActive() {
80+
this.contextRunner.withPropertyValues("spring.profiles.active=production").run(context -> {
81+
CommandRegistry registry = context.getBean(CommandRegistry.class);
82+
assertFalse(hasCommand(registry, "debug-info"),
83+
"Command with @Profile('!production') should not be registered when 'production' is active");
84+
});
85+
}
86+
87+
private boolean hasCommand(CommandRegistry registry, String commandName) {
88+
return registry.getCommands().stream().anyMatch(command -> command.getName().equals(commandName));
89+
}
90+
91+
@SpringBootApplication
92+
static class TestShellApplication {
93+
94+
@org.springframework.shell.core.command.annotation.Command(name = "always-available",
95+
description = "Always available command")
96+
public void alwaysAvailable() {
97+
}
98+
99+
@Profile("greetings")
100+
@org.springframework.shell.core.command.annotation.Command(name = "hello", description = "Say hello")
101+
public void sayHello() {
102+
}
103+
104+
@Profile("!production")
105+
@org.springframework.shell.core.command.annotation.Command(name = "debug-info",
106+
description = "Debug information")
107+
public void debugInfo() {
108+
}
109+
110+
}
111+
112+
}

spring-shell-core/src/main/java/org/springframework/shell/core/command/annotation/support/EnableCommandRegistrar.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
2323
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2424
import org.springframework.beans.factory.support.RootBeanDefinition;
25+
import org.springframework.context.EnvironmentAware;
2526
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
27+
import org.springframework.context.annotation.Profile;
2628
import org.springframework.core.MethodIntrospector;
2729
import org.springframework.core.annotation.AnnotatedElementUtils;
30+
import org.springframework.core.env.Environment;
31+
import org.springframework.core.env.Profiles;
2832
import org.springframework.core.type.AnnotationMetadata;
2933
import org.springframework.shell.core.ConsoleInputProvider;
3034
import org.springframework.shell.core.SystemShellRunner;
@@ -38,8 +42,16 @@
3842
*
3943
* @author Janne Valkealahti
4044
* @author Mahmoud Ben Hassine
45+
* @author David Pilar
4146
*/
42-
public final class EnableCommandRegistrar implements ImportBeanDefinitionRegistrar {
47+
public final class EnableCommandRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
48+
49+
private Environment environment;
50+
51+
@Override
52+
public void setEnvironment(Environment environment) {
53+
this.environment = environment;
54+
}
4355

4456
@Override
4557
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
@@ -95,7 +107,8 @@ private void registerCommands(Class<?> candidateClass, BeanDefinitionRegistry re
95107
}
96108

97109
private void registerAnnotatedMethods(Class<?> candidateClass, BeanDefinitionRegistry registry) {
98-
ReflectionUtils.MethodFilter filter = method -> AnnotatedElementUtils.hasAnnotation(method, Command.class);
110+
ReflectionUtils.MethodFilter filter = method -> AnnotatedElementUtils.hasAnnotation(method, Command.class)
111+
&& isProfileActive(method);
99112
Set<Method> methods = MethodIntrospector.selectMethods(candidateClass, filter);
100113
for (Method method : methods) {
101114
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
@@ -105,4 +118,9 @@ private void registerAnnotatedMethods(Class<?> candidateClass, BeanDefinitionReg
105118
}
106119
}
107120

121+
private boolean isProfileActive(Method method) {
122+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
123+
return profile == null || environment.acceptsProfiles(Profiles.of(profile.value()));
124+
}
125+
108126
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright 2026-present 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+
package org.springframework.shell.core.command.annotation.support;
17+
18+
import java.lang.reflect.Method;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
import org.springframework.context.annotation.Profile;
23+
import org.springframework.core.annotation.AnnotatedElementUtils;
24+
import org.springframework.core.env.Environment;
25+
import org.springframework.core.env.Profiles;
26+
import org.springframework.mock.env.MockEnvironment;
27+
import org.springframework.shell.core.command.annotation.Command;
28+
29+
import static org.junit.jupiter.api.Assertions.*;
30+
31+
/**
32+
* Tests for {@link EnableCommandRegistrar} profile filtering support.
33+
*
34+
* @author David Pilar
35+
*/
36+
class EnableCommandRegistrarTests {
37+
38+
@Test
39+
void commandWithoutProfileShouldAlwaysBeActive() throws Exception {
40+
Method method = TestCommands.class.getDeclaredMethod("noProfileCommand");
41+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
42+
assertNull(profile, "Method without @Profile should not have a Profile annotation");
43+
}
44+
45+
@Test
46+
void commandWithProfileShouldBeActiveWhenProfileMatches() throws Exception {
47+
MockEnvironment environment = new MockEnvironment();
48+
environment.setActiveProfiles("greetings");
49+
50+
Method method = TestCommands.class.getDeclaredMethod("greetingsProfileCommand");
51+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
52+
53+
assertNotNull(profile, "Method should have @Profile annotation");
54+
assertTrue(environment.acceptsProfiles(Profiles.of(profile.value())),
55+
"Command should be active when 'greetings' profile is active");
56+
}
57+
58+
@Test
59+
void commandWithProfileShouldNotBeActiveWhenProfileDoesNotMatch() throws Exception {
60+
MockEnvironment environment = new MockEnvironment();
61+
// no active profiles
62+
63+
Method method = TestCommands.class.getDeclaredMethod("greetingsProfileCommand");
64+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
65+
66+
assertNotNull(profile, "Method should have @Profile annotation");
67+
assertFalse(environment.acceptsProfiles(Profiles.of(profile.value())),
68+
"Command should not be active when 'greetings' profile is not active");
69+
}
70+
71+
@Test
72+
void commandWithNegatedProfileShouldBeActiveWhenProfileIsNotSet() throws Exception {
73+
MockEnvironment environment = new MockEnvironment();
74+
// no active profiles
75+
76+
Method method = TestCommands.class.getDeclaredMethod("notProductionCommand");
77+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
78+
79+
assertNotNull(profile, "Method should have @Profile annotation");
80+
assertTrue(environment.acceptsProfiles(Profiles.of(profile.value())),
81+
"Command with '!production' profile should be active when 'production' is not active");
82+
}
83+
84+
@Test
85+
void commandWithNegatedProfileShouldNotBeActiveWhenProfileIsSet() throws Exception {
86+
MockEnvironment environment = new MockEnvironment();
87+
environment.setActiveProfiles("production");
88+
89+
Method method = TestCommands.class.getDeclaredMethod("notProductionCommand");
90+
Profile profile = AnnotatedElementUtils.findMergedAnnotation(method, Profile.class);
91+
92+
assertNotNull(profile, "Method should have @Profile annotation");
93+
assertFalse(environment.acceptsProfiles(Profiles.of(profile.value())),
94+
"Command with '!production' profile should not be active when 'production' is active");
95+
}
96+
97+
static class TestCommands {
98+
99+
@Command(name = "no-profile")
100+
public void noProfileCommand() {
101+
}
102+
103+
@Profile("greetings")
104+
@Command(name = "hello")
105+
public void greetingsProfileCommand() {
106+
}
107+
108+
@Profile("!production")
109+
@Command(name = "debug-info")
110+
public void notProductionCommand() {
111+
}
112+
113+
}
114+
115+
}

0 commit comments

Comments
 (0)