From a97bcfe3cded186ac56b7a727f5c7cc09e3c3ba0 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 31 Jan 2014 21:56:21 -0800 Subject: [PATCH] Automatically detect 'development' profile Detect when an application is running in development (by the presence of a build file) and automatically add a 'development' profile. Additional detectors can be developed by implementing the `ProfileDetector` interface and registering with the `SpringApplication` Fixes gh-296 --- .../simple/SampleSimpleApplication.java | 5 +- .../boot/DevelopmentProfileDetector.java | 95 +++++++++++++ .../springframework/boot/ProfileDetector.java | 34 +++++ .../boot/SpringApplication.java | 36 ++++- .../builder/SpringApplicationBuilder.java | 17 ++- .../test/SpringApplicationContextLoader.java | 3 +- .../boot/DevelopmentProfileDetectorTests.java | 128 ++++++++++++++++++ .../boot/SpringApplicationTests.java | 23 ++++ 8 files changed, 330 insertions(+), 11 deletions(-) create mode 100644 spring-boot/src/main/java/org/springframework/boot/DevelopmentProfileDetector.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/ProfileDetector.java create mode 100644 spring-boot/src/test/java/org/springframework/boot/DevelopmentProfileDetectorTests.java diff --git a/spring-boot-samples/spring-boot-sample-simple/src/main/java/sample/simple/SampleSimpleApplication.java b/spring-boot-samples/spring-boot-sample-simple/src/main/java/sample/simple/SampleSimpleApplication.java index 830f95f15af..db8eb105c98 100644 --- a/spring-boot-samples/spring-boot-sample-simple/src/main/java/sample/simple/SampleSimpleApplication.java +++ b/spring-boot-samples/spring-boot-sample-simple/src/main/java/sample/simple/SampleSimpleApplication.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -43,6 +44,8 @@ public class SampleSimpleApplication implements CommandLineRunner { } public static void main(String[] args) throws Exception { - SpringApplication.run(SampleSimpleApplication.class, args); + ConfigurableApplicationContext context = SpringApplication.run( + SampleSimpleApplication.class, args); + System.out.println(context.getEnvironment().acceptsProfiles("development")); } } diff --git a/spring-boot/src/main/java/org/springframework/boot/DevelopmentProfileDetector.java b/spring-boot/src/main/java/org/springframework/boot/DevelopmentProfileDetector.java new file mode 100644 index 00000000000..c322b32d2bf --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/DevelopmentProfileDetector.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.io.File; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link ProfileDetector} that attempts to detect when the application is being developed + * and adds a 'development' profile. + * + * @author Phillip Webb + */ +public class DevelopmentProfileDetector implements ProfileDetector { + + private final Log logger = LogFactory.getLog(getClass()); + + private static final String DEFAULT_PROFILE_NAME = "development"; + + private static final String EXECUTABLE_JAR_CLASS = "org.springframework.boot.loader.Launcher"; + + private static final String[] DEVELOPMENT_TIME_FILES = { "pom.xml", "build.gradle", + "build.xml" }; + + private final String profileName; + + public DevelopmentProfileDetector() { + this(DEFAULT_PROFILE_NAME); + } + + public DevelopmentProfileDetector(String profileName) { + Assert.notNull(profileName, "ProfileName must not be null"); + this.profileName = profileName; + } + + @Override + public void addDetectedProfiles(ConfigurableEnvironment environment) { + if (!isPackageAsJar() && isRunningInDevelopmentDirectory()) { + environment.addActiveProfile(this.profileName); + } + } + + protected boolean isPackageAsJar() { + if (ClassUtils.isPresent(EXECUTABLE_JAR_CLASS, null)) { + this.logger.debug("Development profile not detected: " + + "running inside executable jar"); + return true; + } + String command = System.getProperty("sun.java.command"); + if (StringUtils.hasLength(command) && command.toLowerCase().contains(".jar")) { + this.logger.debug("Development profile not detected: started from a jar"); + return true; + } + return false; + } + + private boolean isRunningInDevelopmentDirectory() { + File userDir = getUserDir(); + if (userDir != null && userDir.exists()) { + for (String developementTimeFile : DEVELOPMENT_TIME_FILES) { + if (new File(userDir, developementTimeFile).exists()) { + this.logger.debug("Development profile detected: file " + + developementTimeFile + " present"); + return true; + } + } + } + return false; + } + + protected File getUserDir() { + String userDir = System.getProperty("user.dir"); + return (userDir == null ? null : new File(userDir)); + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/ProfileDetector.java b/spring-boot/src/main/java/org/springframework/boot/ProfileDetector.java new file mode 100644 index 00000000000..d40edac2720 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ProfileDetector.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * Strategy interface that can be used to detect active profiles. + * + * @author Phillip Webb + */ +public interface ProfileDetector { + + /** + * Add any detected profiles to the specified environment. + * @param environment the environment + */ + void addDetectedProfiles(ConfigurableEnvironment environment); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 0be2509be46..58eb4744f9d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -158,6 +158,9 @@ public class SpringApplication { private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }; + private static final List DEFAULT_PROFILE_DETECTORS = Arrays + . asList(new DevelopmentProfileDetector()); + private final Log log = LogFactory.getLog(getClass()); private final Set sources = new LinkedHashSet(); @@ -190,6 +193,8 @@ public class SpringApplication { private Set profiles = new HashSet(); + private List profileDetectors = DEFAULT_PROFILE_DETECTORS; + /** * Crate a new {@link SpringApplication} instance. The application context will load * beans from the specified sources (see {@link SpringApplication class-level} @@ -308,9 +313,7 @@ public class SpringApplication { // Create and configure the environment ConfigurableEnvironment environment = getOrCreateEnvironment(); addPropertySources(environment, args); - for (String profile : this.profiles) { - environment.addActiveProfile(profile); - } + setupProfiles(environment); // Notify listeners of the environment creation multicaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(this, @@ -461,6 +464,15 @@ public class SpringApplication { } } + protected void setupProfiles(ConfigurableEnvironment environment) { + for (String profile : this.profiles) { + environment.addActiveProfile(profile); + } + for (ProfileDetector profileDetector : this.profileDetectors) { + profileDetector.addDetectedProfiles(environment); + } + } + /** * Print a simple banner message to the console. Subclasses can override this method * to provide additional or alternative banners. @@ -740,11 +752,23 @@ public class SpringApplication { /** * Set additional profile values to use (on top of those set in system or command line * properties). - * * @param profiles the additional profiles to set */ - public void setAdditionalProfiles(Collection profiles) { - this.profiles = new LinkedHashSet(profiles); + public void setAdditionalProfiles(String... profiles) { + this.profiles = new LinkedHashSet(Arrays.asList(profiles)); + } + + /** + * Sets the {@link ProfileDetector}s that will be used to attempt to detect + * {@link Environment#acceptsProfiles(String...) active profiles}. + * {@link DevelopmentProfileDetector} is used. + * @param profileDetectors the profile detectors + * @see #setAdditionalProfiles(String...) + */ + public void setProfileDetectors(ProfileDetector... profileDetectors) { + Assert.notNull(profileDetectors, "Profile detectors must not be null"); + this.profileDetectors = new ArrayList( + Arrays.asList(profileDetectors)); } /** diff --git a/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java b/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java index 7f0157adc16..4a802e7104e 100644 --- a/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java +++ b/spring-boot/src/main/java/org/springframework/boot/builder/SpringApplicationBuilder.java @@ -28,6 +28,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.boot.ProfileDetector; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.initializer.ParentContextApplicationContextInitializer; import org.springframework.boot.context.initializer.ServletContextApplicationContextInitializer; @@ -389,14 +390,26 @@ public class SpringApplicationBuilder { */ public SpringApplicationBuilder profiles(String... profiles) { this.additionalProfiles.addAll(Arrays.asList(profiles)); - this.application.setAdditionalProfiles(this.additionalProfiles); + this.application.setAdditionalProfiles(this.additionalProfiles + .toArray(new String[this.additionalProfiles.size()])); + return this; + } + + /** + * Set the {@link ProfileDetector}s that should be used for this app. + * @param profileDetectors the profile detectors to set + * @return the current builder + */ + public SpringApplicationBuilder profileDetectors(ProfileDetector... profileDetectors) { + this.application.setProfileDetectors(profileDetectors); return this; } private SpringApplicationBuilder additionalProfiles( Collection additionalProfiles) { this.additionalProfiles = new LinkedHashSet(additionalProfiles); - this.application.setAdditionalProfiles(additionalProfiles); + this.application.setAdditionalProfiles(additionalProfiles + .toArray(new String[additionalProfiles.size()])); return this; } diff --git a/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java b/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java index a37bf401b04..256552d67ff 100644 --- a/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/test/SpringApplicationContextLoader.java @@ -61,8 +61,7 @@ public class SpringApplicationContextLoader extends AbstractContextLoader { SpringApplication application = new SpringApplication(); application.setSources(getSources(mergedConfig)); if (!ObjectUtils.isEmpty(mergedConfig.getActiveProfiles())) { - application.setAdditionalProfiles(Arrays.asList(mergedConfig - .getActiveProfiles())); + application.setAdditionalProfiles(mergedConfig.getActiveProfiles()); } application.setDefaultProperties(getArgs(mergedConfig)); List> initializers = getInitializers( diff --git a/spring-boot/src/test/java/org/springframework/boot/DevelopmentProfileDetectorTests.java b/spring-boot/src/test/java/org/springframework/boot/DevelopmentProfileDetectorTests.java new file mode 100644 index 00000000000..e313eb693d0 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/DevelopmentProfileDetectorTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot; + +import java.io.File; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.core.env.ConfigurableEnvironment; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link DevelopmentProfileDetector}. + * + * @author Phillip Webb + */ +public class DevelopmentProfileDetectorTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private TestDevelopmentProfileDetector detector; + + private ConfigurableEnvironment environment; + + @Before + public void setup() { + this.detector = new TestDevelopmentProfileDetector(); + this.environment = mock(ConfigurableEnvironment.class); + } + + @Test + public void notFound() { + this.detector.addDetectedProfiles(this.environment); + verifyZeroInteractions(this.environment); + } + + @Test + public void foundDueToMavenBuild() throws Exception { + this.temporaryFolder.newFile("pom.xml").createNewFile(); + this.detector.addDetectedProfiles(this.environment); + verify(this.environment).addActiveProfile("development"); + } + + @Test + public void foundDueToGradleBuild() throws Exception { + this.temporaryFolder.newFile("build.gradle").createNewFile(); + this.detector.addDetectedProfiles(this.environment); + verify(this.environment).addActiveProfile("development"); + } + + @Test + public void foundDueToAntBuild() throws Exception { + this.temporaryFolder.newFile("build.xml").createNewFile(); + this.detector.addDetectedProfiles(this.environment); + verify(this.environment).addActiveProfile("development"); + } + + @Test + public void differentProfileName() throws Exception { + this.detector = new TestDevelopmentProfileDetector("different"); + this.temporaryFolder.newFile("pom.xml").createNewFile(); + this.detector.addDetectedProfiles(this.environment); + verify(this.environment).addActiveProfile("different"); + verifyNoMoreInteractions(this.environment); + } + + @Test + public void notFoundWhenStartedFromJar() throws Exception { + System.setProperty("sun.java.command", "something.jar"); + this.temporaryFolder.newFile("pom.xml").createNewFile(); + this.detector.setSkipPackageAsJar(false); + this.detector.addDetectedProfiles(this.environment); + verifyZeroInteractions(this.environment); + } + + private class TestDevelopmentProfileDetector extends DevelopmentProfileDetector { + + private boolean skipPackageAsJar = true; + + public TestDevelopmentProfileDetector() { + super(); + } + + public TestDevelopmentProfileDetector(String profileName) { + super(profileName); + } + + public void setSkipPackageAsJar(boolean skipPackageAsJar) { + this.skipPackageAsJar = skipPackageAsJar; + } + + @Override + protected boolean isPackageAsJar() { + if (this.skipPackageAsJar) { + // Unfortunately surefire uses a jar so we need to stub this out + return false; + } + return super.isPackageAsJar(); + } + + @Override + protected File getUserDir() { + return DevelopmentProfileDetectorTests.this.temporaryFolder.getRoot(); + } + + } +} diff --git a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java index 4cefcbf2a48..68c0abeb5b3 100644 --- a/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/SpringApplicationTests.java @@ -409,6 +409,29 @@ public class SpringApplicationTests { assertThat(System.getProperty("java.awt.headless"), equalTo("false")); } + @Test + public void setAdditionalProfiles() throws Exception { + TestSpringApplication application = new TestSpringApplication(ExampleConfig.class); + application.setWebEnvironment(false); + application.setAdditionalProfiles("mine"); + this.context = application.run(); + assertThat(this.context.getEnvironment().acceptsProfiles("mine"), equalTo(true)); + } + + @Test + public void setProfileDetectors() throws Exception { + TestSpringApplication application = new TestSpringApplication(ExampleConfig.class); + application.setWebEnvironment(false); + application.setProfileDetectors(new ProfileDetector() { + @Override + public void addDetectedProfiles(ConfigurableEnvironment environment) { + environment.addActiveProfile("mine"); + } + }); + this.context = application.run(); + assertThat(this.context.getEnvironment().acceptsProfiles("mine"), equalTo(true)); + } + private boolean hasPropertySource(ConfigurableEnvironment environment, Class propertySourceClass, String name) { for (PropertySource source : environment.getPropertySources()) {