From 3c203e9b0d28344719c316dd02650a879ee4fb1d Mon Sep 17 00:00:00 2001 From: Madhura Bhave Date: Fri, 5 Apr 2019 17:51:33 -0700 Subject: [PATCH] Update devtools to use @Lazy(false) Fixes gh-16184 --- .../EagerInitializationAutoConfiguration.java | 66 ------- .../LocalDevToolsAutoConfiguration.java | 2 + .../main/resources/META-INF/spring.factories | 1 - .../devtools/tests/ApplicationLauncher.java | 4 + ...ithLazyInitializationIntegrationTests.java | 183 ++++++++++++++++++ .../tests/LocalApplicationLauncher.java | 15 ++ .../tests/RemoteApplicationLauncher.java | 19 ++ 7 files changed, 223 insertions(+), 67 deletions(-) delete mode 100644 spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/EagerInitializationAutoConfiguration.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/EagerInitializationAutoConfiguration.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/EagerInitializationAutoConfiguration.java deleted file mode 100644 index 568fb9121b1..00000000000 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/EagerInitializationAutoConfiguration.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2012-2019 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 - * - * https://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.devtools.autoconfigure; - -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; - -/** - * {@link EnableAutoConfiguration Auto-configuration} to ensure that DevTools' beans are - * eagerly initialized when the application is otherwise being initialized lazily. - * - * @author Andy Wilkinson - * @since 2.2.0 - */ -@Configuration(proxyBeanMethods = false) -public class EagerInitializationAutoConfiguration { - - @Bean - public static AlwaysEagerBeanFactoryPostProcessor alwaysEagerBeanFactoryPostProcessor() { - return new AlwaysEagerBeanFactoryPostProcessor(); - } - - private static final class AlwaysEagerBeanFactoryPostProcessor - implements BeanFactoryPostProcessor, Ordered { - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - for (String name : beanFactory.getBeanDefinitionNames()) { - BeanDefinition beanDefinition = beanFactory.getBeanDefinition(name); - String factoryBeanName = beanDefinition.getFactoryBeanName(); - if (factoryBeanName != null && factoryBeanName - .startsWith("org.springframework.boot.devtools")) { - beanDefinition.setLazyInit(false); - } - } - } - - @Override - public int getOrder() { - return Ordered.HIGHEST_PRECEDENCE + 1; - } - - } - -} diff --git a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java index 75db9d9a0db..81ad00d9bbc 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/LocalDevToolsAutoConfiguration.java @@ -39,6 +39,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.GenericApplicationListener; import org.springframework.core.ResolvableType; @@ -90,6 +91,7 @@ public class LocalDevToolsAutoConfiguration { /** * Local Restart Configuration. */ + @Lazy(false) @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = "spring.devtools.restart", name = "enabled", matchIfMissing = true) diff --git a/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories index d7d340dbea1..958c3dfa63c 100644 --- a/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-devtools/src/main/resources/META-INF/spring.factories @@ -9,7 +9,6 @@ org.springframework.boot.devtools.logger.DevToolsLogFactory.Listener # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.boot.devtools.autoconfigure.EagerInitializationAutoConfiguration,\ org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration,\ org.springframework.boot.devtools.autoconfigure.LocalDevToolsAutoConfiguration,\ org.springframework.boot.devtools.autoconfigure.RemoteDevToolsAutoConfiguration diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java index e7dde84ae54..c5c30c3e0c8 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/ApplicationLauncher.java @@ -22,10 +22,14 @@ import java.io.File; * Launches an application with DevTools. * * @author Andy Wilkinson + * @author Madhura Bhave */ public interface ApplicationLauncher { LaunchedApplication launchApplication(JvmLauncher javaLauncher, File serverPortFile) throws Exception; + LaunchedApplication launchApplication(JvmLauncher jvmLauncher, File serverPortFile, + String... additionalArgs) throws Exception; + } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java new file mode 100644 index 00000000000..9db19ef5fdd --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/DevToolsWithLazyInitializationIntegrationTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2012-2019 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 + * + * https://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.devtools.tests; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.FixedValue; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.testsupport.BuildOutput; +import org.springframework.http.HttpStatus; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for DevTools with lazy initialization enabled. + * + * @author Madhura Bhave + */ +@RunWith(Parameterized.class) +public class DevToolsWithLazyInitializationIntegrationTests { + + @ClassRule + public static final TemporaryFolder temp = new TemporaryFolder(); + + private static final BuildOutput buildOutput = new BuildOutput( + DevToolsIntegrationTests.class); + + private LaunchedApplication launchedApplication; + + private final File serverPortFile; + + private final ApplicationLauncher applicationLauncher; + + private String[] args; + + @Rule + public JvmLauncher javaLauncher = new JvmLauncher(); + + public DevToolsWithLazyInitializationIntegrationTests( + ApplicationLauncher applicationLauncher) { + this.applicationLauncher = applicationLauncher; + this.serverPortFile = new File(buildOutput.getRootLocation(), "server.port"); + } + + @Before + public void launchApplication() throws Exception { + this.serverPortFile.delete(); + this.launchedApplication = this.applicationLauncher.launchApplication( + this.javaLauncher, this.serverPortFile, + "--spring.main.lazy-initialization=true"); + } + + @After + public void stopApplication() throws InterruptedException { + this.launchedApplication.stop(); + } + + @Test + public void addARequestMappingToAnExistingControllerWhenLazyInit() throws Exception { + TestRestTemplate template = new TestRestTemplate(); + String urlBase = "http://localhost:" + awaitServerPort(); + assertThat(template.getForObject(urlBase + "/one", String.class)) + .isEqualTo("one"); + assertThat(template.getForEntity(urlBase + "/two", String.class).getStatusCode()) + .isEqualTo(HttpStatus.NOT_FOUND); + controller("com.example.ControllerOne").withRequestMapping("one") + .withRequestMapping("two").build(); + urlBase = "http://localhost:" + awaitServerPort(); + assertThat(template.getForObject(urlBase + "/one", String.class)) + .isEqualTo("one"); + assertThat(template.getForObject(urlBase + "/two", String.class)) + .isEqualTo("two"); + } + + private int awaitServerPort() throws Exception { + Duration timeToWait = Duration.ofSeconds(40); + long end = System.currentTimeMillis() + timeToWait.toMillis(); + System.out.println("Reading server port from '" + this.serverPortFile + "'"); + while (this.serverPortFile.length() == 0) { + if (System.currentTimeMillis() > end) { + throw new IllegalStateException(String.format( + "server.port file '" + this.serverPortFile + + "' was not written within " + timeToWait.toMillis() + + "ms. " + "Application output:%n%s%s", + FileCopyUtils.copyToString(new FileReader( + this.launchedApplication.getStandardOut())), + FileCopyUtils.copyToString(new FileReader( + this.launchedApplication.getStandardError())))); + } + Thread.sleep(100); + } + FileReader portReader = new FileReader(this.serverPortFile); + int port = Integer.valueOf(FileCopyUtils.copyToString(portReader)); + this.serverPortFile.delete(); + System.out.println("Got port " + port); + this.launchedApplication.restartRemote(port); + Thread.sleep(1000); + return port; + } + + private ControllerBuilder controller(String name) { + return new ControllerBuilder(name, + this.launchedApplication.getClassesDirectory()); + } + + @Parameterized.Parameters(name = "{0}") + public static Object[] parameters() throws IOException { + Directories directories = new Directories(buildOutput, temp); + return new Object[] { new Object[] { new LocalApplicationLauncher(directories) }, + new Object[] { new ExplodedRemoteApplicationLauncher(directories) }, + new Object[] { new JarFileRemoteApplicationLauncher(directories) } }; + + } + + private static final class ControllerBuilder { + + private final List mappings = new ArrayList<>(); + + private final String name; + + private final File classesDirectory; + + private ControllerBuilder(String name, File classesDirectory) { + this.name = name; + this.classesDirectory = classesDirectory; + } + + public ControllerBuilder withRequestMapping(String mapping) { + this.mappings.add(mapping); + return this; + } + + public void build() throws Exception { + DynamicType.Builder builder = new ByteBuddy().subclass(Object.class) + .name(this.name).annotateType(AnnotationDescription.Builder + .ofType(RestController.class).build()); + for (String mapping : this.mappings) { + builder = builder.defineMethod(mapping, String.class, Visibility.PUBLIC) + .intercept(FixedValue.value(mapping)).annotateMethod( + AnnotationDescription.Builder.ofType(RequestMapping.class) + .defineArray("value", mapping).build()); + } + builder.make().saveIn(this.classesDirectory); + } + + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java index 48bbffd4adc..645d6abdb0f 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/LocalApplicationLauncher.java @@ -18,6 +18,7 @@ package org.springframework.boot.devtools.tests; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.springframework.boot.devtools.tests.JvmLauncher.LaunchedJvm; @@ -45,6 +46,20 @@ public class LocalApplicationLauncher extends AbstractApplicationLauncher { null); } + @Override + public LaunchedApplication launchApplication(JvmLauncher jvmLauncher, + File serverPortFile, String... additionalArgs) throws Exception { + List args = new ArrayList<>( + Arrays.asList("com.example.DevToolsTestApplication", + serverPortFile.getAbsolutePath(), "--server.port=0")); + args.addAll(Arrays.asList(additionalArgs)); + LaunchedJvm jvm = jvmLauncher.launch("local", createApplicationClassPath(), + args.toArray(new String[] {})); + return new LaunchedApplication(getDirectories().getAppDirectory(), + jvm.getStandardOut(), jvm.getStandardError(), jvm.getProcess(), null, + null); + } + protected String createApplicationClassPath() throws Exception { File appDirectory = getDirectories().getAppDirectory(); copyApplicationTo(appDirectory); diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java index 6a6356360b8..5c0ac25aa24 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-devtools-tests/src/test/java/org/springframework/boot/devtools/tests/RemoteApplicationLauncher.java @@ -19,6 +19,7 @@ package org.springframework.boot.devtools.tests; import java.io.File; import java.io.FileReader; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.BiFunction; @@ -55,6 +56,24 @@ abstract class RemoteApplicationLauncher extends AbstractApplicationLauncher { remoteRestarter); } + @Override + public LaunchedApplication launchApplication(JvmLauncher javaLauncher, + File serverPortFile, String... additionalArgs) throws Exception { + List args = new ArrayList<>(Arrays.asList( + "com.example.DevToolsTestApplication", serverPortFile.getAbsolutePath(), + "--server.port=0", "--spring.devtools.remote.secret=secret")); + args.addAll(Arrays.asList(additionalArgs)); + LaunchedJvm applicationJvm = javaLauncher.launch("app", + createApplicationClassPath(), args.toArray(new String[] {})); + int port = awaitServerPort(applicationJvm.getStandardOut(), serverPortFile); + BiFunction remoteRestarter = getRemoteRestarter( + javaLauncher); + return new LaunchedApplication(getDirectories().getRemoteAppDirectory(), + applicationJvm.getStandardOut(), applicationJvm.getStandardError(), + applicationJvm.getProcess(), remoteRestarter.apply(port, null), + remoteRestarter); + } + private BiFunction getRemoteRestarter( JvmLauncher javaLauncher) { return (port, classesDirectory) -> {