From ebd1646308b1f89d48cf25d6750951d8ee62d491 Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 25 Apr 2018 17:33:24 +0100 Subject: [PATCH] Push deployer configuration out of autoconfig It tends to pop back into function apps where it is not needed otherwise. Users that want to use the library need to import the FunctionConfiguration directly using the @EnableFunctionDeployer convenience annotation.. --- spring-cloud-function-deployer/README.md | 1 + spring-cloud-function-deployer/pom.xml | 10 +- .../BeanCountingApplicationListener.java | 118 --------------- .../function/deployer/ContextRunner.java | 5 - .../deployer/EnableFunctionDeployer.java | 39 +++++ .../deployer/FunctionApplication.java | 1 + ...java => FunctionCreatorConfiguration.java} | 77 +++++----- .../FunctionDeployerConfiguration.java | 65 +++++++++ .../main/resources/META-INF/spring.factories | 2 - .../function/deployer/AdhocTestSuite.java | 43 ++++++ .../deployer/ApplicationRunnerTests.java | 40 +++++ .../function/deployer/ContextRunnerTests.java | 40 +++++ .../deployer/FunctionConfigurationTests.java | 120 --------------- .../FunctionCreatorConfigurationTests.java | 138 ++++++++++++++++++ .../SpringFunctionAppConfigurationTests.java | 64 ++++---- .../cloud/function/test/Doubler.java | 26 ++++ .../cloud/function/test/Frenchizer.java | 43 ++++++ .../cloud/function/test/FunctionApp.java | 42 ++++++ .../cloud/function/test/NumberEmitter.java | 26 ++++ .../cloud/function/test/Printer.java | 27 ++++ .../cloud/function/test/SpringDoubler.java | 53 +++++++ .../FluxHandlerMethodArgumentResolver.java | 4 +- 22 files changed, 656 insertions(+), 328 deletions(-) delete mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java rename spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/{FunctionConfiguration.java => FunctionCreatorConfiguration.java} (89%) create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java delete mode 100644 spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java delete mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java diff --git a/spring-cloud-function-deployer/README.md b/spring-cloud-function-deployer/README.md index 252cf6fe8..a2e4b0201 100644 --- a/spring-cloud-function-deployer/README.md +++ b/spring-cloud-function-deployer/README.md @@ -2,6 +2,7 @@ Spring Cloud Function Deployer is an library for building apps that can deploy f ```java @SpringBootApplication +@EnableFunctionDeployer public class FunctionApplication { public static void main(String[] args) throws IOException { diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml index 0981676a3..c64b3be00 100644 --- a/spring-cloud-function-deployer/pom.xml +++ b/spring-cloud-function-deployer/pom.xml @@ -24,11 +24,6 @@ org.springframework.cloud spring-cloud-function-context - - org.springframework.cloud - spring-cloud-function-stream - test - org.springframework.boot spring-boot-configuration-processor @@ -54,9 +49,8 @@ test - org.springframework.cloud - spring-cloud-stream-test-support - true + org.springframework.boot + spring-boot-starter-logging test diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java deleted file mode 100644 index 72aca30ca..000000000 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2016-2017 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.cloud.function.deployer; - -import java.lang.management.ManagementFactory; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.BeansException; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; - -/** - * @author Dave Syer - * - */ -public class BeanCountingApplicationListener - implements ApplicationListener, ApplicationContextAware { - - public static final String MARKER = "Invoker app started"; - private static Log logger = LogFactory.getLog(BeanCountingApplicationListener.class); - private ApplicationContext context; - - @Override - public void setApplicationContext(ApplicationContext context) throws BeansException { - this.context = context; - } - - @SuppressWarnings("resource") - @Override - public void onApplicationEvent(ApplicationReadyEvent event) { - if (!event.getApplicationContext().equals(this.context)) { - return; - } - int count = 0; - ConfigurableApplicationContext context = event.getApplicationContext(); - String id = context.getId(); - List names = new ArrayList<>(); - while (context != null) { - count += context.getBeanDefinitionCount(); - names.addAll(Arrays.asList(context.getBeanDefinitionNames())); - context = (ConfigurableApplicationContext) context.getParent(); - } - logger.info("Bean count: " + id + "=" + count); - logger.debug("Bean names: " + id + "=" + names); - try { - logger.info("Class count: " + id + "=" + ManagementFactory - .getClassLoadingMXBean().getTotalLoadedClassCount()); - } - catch (Exception e) { - } - if (isSpringBootApplication(sources(event))) { - try { - logger.info(MARKER); - } - catch (Exception e) { - } - } - } - - private boolean isSpringBootApplication(Set> sources) { - for (Class source : sources) { - if (AnnotatedElementUtils.hasAnnotation(source, - SpringBootConfiguration.class)) { - return true; - } - } - return false; - } - - private Set> sources(ApplicationReadyEvent event) { - Method method = ReflectionUtils.findMethod(SpringApplication.class, - "getAllSources"); - if (method == null) { - method = ReflectionUtils.findMethod(SpringApplication.class, "getSources"); - } - ReflectionUtils.makeAccessible(method); - @SuppressWarnings("unchecked") - Set objects = (Set) ReflectionUtils.invokeMethod(method, - event.getSpringApplication()); - Set> result = new LinkedHashSet<>(); - for (Object object : objects) { - if (object instanceof String) { - object = ClassUtils.resolveClassName((String) object, null); - } - result.add((Class) object); - } - return result; - } - -} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java index 718556c65..c54e7ec7b 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java @@ -57,11 +57,6 @@ public class ContextRunner { running = true; SpringApplicationBuilder builder = builder( ClassUtils.resolveClassName(source, null)); - if (ClassUtils.isPresent( - "org.springframework.cloud.stream.app.function.app.BeanCountingApplicationListener.BeanCountingApplicationListener()", - null)) { - builder.listeners(new BeanCountingApplicationListener()); - } context = builder.environment(environment).registerShutdownHook(false) .run(args); } diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java new file mode 100644 index 000000000..355b0a296 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2016-2017 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.cloud.function.deployer; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Annotation to be used on a Spring Boot application if it wants to deploy a jar file + * containing a function definition. + * + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(FunctionDeployerConfiguration.class) +public @interface EnableFunctionDeployer { + +} \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java index a6b929f31..0d58bb3d6 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; * @author Dave Syer */ @SpringBootApplication +@EnableFunctionDeployer public class FunctionApplication { public static void main(String[] args) throws IOException { diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java similarity index 89% rename from spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java rename to spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java index 87af9ca01..97f5f3edf 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java @@ -25,13 +25,12 @@ import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.jar.JarFile; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -46,25 +45,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.loader.JarLauncher; import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.JarFileArchive; -import org.springframework.cloud.deployer.resource.maven.MavenProperties; -import org.springframework.cloud.deployer.resource.maven.MavenResource; -import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.FunctionType; import org.springframework.cloud.function.context.catalog.FunctionInspector; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; import org.springframework.util.StreamUtils; @@ -83,10 +74,9 @@ import org.springframework.util.StreamUtils; * @author Dave Syer */ @Configuration -@EnableConfigurationProperties -public class FunctionConfiguration { +class FunctionCreatorConfiguration { - private static Log logger = LogFactory.getLog(FunctionConfiguration.class); + private static Log logger = LogFactory.getLog(FunctionCreatorConfiguration.class); @Autowired private FunctionRegistry registry; @@ -104,27 +94,6 @@ public class FunctionConfiguration { private BeanCreator creator; - @Bean - @ConfigurationProperties("maven") - public MavenProperties mavenProperties() { - return new MavenProperties(); - } - - @Bean - @ConfigurationProperties("function") - public FunctionProperties functionProperties() { - return new FunctionProperties(); - } - - @Bean - @ConditionalOnMissingBean(DelegatingResourceLoader.class) - public DelegatingResourceLoader delegatingResourceLoader( - MavenProperties mavenProperties) { - Map loaders = new HashMap<>(); - loaders.put(MavenResource.URI_SCHEME, new MavenResourceLoader(mavenProperties)); - return new DelegatingResourceLoader(loaders); - } - /** * Registers a function for each of the function classes passed into the * {@link FunctionProperties}. They are named sequentially "function0", "function1", @@ -222,6 +191,13 @@ public class FunctionConfiguration { public URL[] getClassLoaderUrls() throws Exception { List archives = getClassPathArchives(); if (archives.isEmpty()) { + URL url = getArchive().getUrl(); + if (url.toString().contains(".jar")) { // Surefire or IntelliJ? + URL[] classpath = extractClasspath(url.toString()); + if (classpath != null) { + return classpath; + } + } return new URL[] { getArchive().getUrl() }; } return archives.stream().map(archive -> { @@ -234,6 +210,36 @@ public class FunctionConfiguration { }).collect(Collectors.toList()).toArray(new URL[0]); } + private URL[] extractClasspath(String url) { + // This works for a jar indirection like in surefire and IntelliJ + if (url.endsWith(".jar!/")) { + url = url.substring(0, url.length() - "!/".length()); + if (url.startsWith("jar:")) { + url = url.substring("jar:".length()); + } + if (url.startsWith("file:")) { + url = url.substring("file:".length()); + } + } + if (url.endsWith(".jar")) { + JarFile jar; + try { + jar = new JarFile(new File(url)); + String path = jar.getManifest().getMainAttributes() + .getValue("Class-Path"); + if (path != null) { + List result = new ArrayList<>(); + for (String element : path.split(" ")) { + result.add(new URL(element)); + } + return result.toArray(new URL[0]); + } + } + catch (Exception e) { + } + } + return null; + } } /** @@ -269,7 +275,8 @@ public class FunctionConfiguration { runner.run("--spring.main.webEnvironment=false", "--spring.cloud.stream.enabled=false", "--spring.main.bannerMode=OFF", - "--spring.main.webApplicationType=none"); + "--spring.main.webApplicationType=none", + "--function.deployer.enabled=false"); this.runner = runner; } finally { diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java new file mode 100644 index 000000000..f4749a167 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-2017 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.cloud.function.deployer; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.deployer.resource.maven.MavenProperties; +import org.springframework.cloud.deployer.resource.maven.MavenResource; +import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; +import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.ResourceLoader; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnProperty(prefix = "function.deployer", name = "enabled", matchIfMissing = true) +@EnableConfigurationProperties +@Import(FunctionCreatorConfiguration.class) +public class FunctionDeployerConfiguration { + + @Bean + @ConfigurationProperties("maven") + public MavenProperties mavenProperties() { + return new MavenProperties(); + } + + @Bean + @ConfigurationProperties("function") + public FunctionProperties functionProperties() { + return new FunctionProperties(); + } + + @Bean + @ConditionalOnMissingBean(DelegatingResourceLoader.class) + public DelegatingResourceLoader delegatingResourceLoader( + MavenProperties mavenProperties) { + Map loaders = new HashMap<>(); + loaders.put(MavenResource.URI_SCHEME, new MavenResourceLoader(mavenProperties)); + return new DelegatingResourceLoader(loaders); + } + +} diff --git a/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories b/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 4486ed0a4..000000000 --- a/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.function.deployer.FunctionConfiguration \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java new file mode 100644 index 000000000..c7f0d5737 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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.cloud.function.deployer; + +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * A test suite for probing weird ordering problems in the tests. + * + * @author Dave Syer + */ +@RunWith(Suite.class) +@SuiteClasses({ FunctionCreatorConfigurationTests.FunctionCompositionTests.class, + FunctionCreatorConfigurationTests.SingleFunctionTests.class, + FunctionCreatorConfigurationTests.ManualSpringFunctionTests.class, + ContextRunnerTests.class, + SpringFunctionAppConfigurationTests.ProcessorTests.class, + SpringFunctionAppConfigurationTests.SourceTests.class, + FunctionCreatorConfigurationTests.ConsumerCompositionTests.class, + SpringFunctionAppConfigurationTests.CompositeTests.class, + ApplicationRunnerTests.class, SpringFunctionAppConfigurationTests.SinkTests.class, + FunctionCreatorConfigurationTests.SupplierCompositionTests.class }) +@Ignore +public class AdhocTestSuite { + +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java new file mode 100644 index 000000000..a28904004 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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.cloud.function.deployer; + +import org.junit.Test; + +import org.springframework.cloud.function.test.Doubler; +import org.springframework.cloud.function.test.FunctionApp; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + */ +public class ApplicationRunnerTests { + + @Test + public void startEvaluateAndStop() { + ApplicationRunner runner = new ApplicationRunner(getClass().getClassLoader(), + FunctionApp.class.getName()); + runner.run("--spring.main.webEnvironment=false"); + assertThat(runner.containsBean(Doubler.class.getName())).isTrue(); + assertThat(runner.getBean(Doubler.class.getName())).isNotNull(); + runner.close(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java new file mode 100644 index 000000000..a3e3a7a59 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 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.cloud.function.deployer; + +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.cloud.function.test.Doubler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + */ +public class ContextRunnerTests { + + @Test + public void startEvaluateAndStop() { + ContextRunner runner = new ContextRunner(); + runner.run(Doubler.class.getName(), Collections.emptyMap(), + "--spring.main.webEnvironment=false"); + assertThat(runner.getContext()).isNotNull(); + runner.close(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java deleted file mode 100644 index 3809eeecd..000000000 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2017 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.cloud.function.deployer; - -import java.util.concurrent.TimeUnit; - -import org.hamcrest.Matchers; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.messaging.Processor; -import org.springframework.cloud.stream.messaging.Sink; -import org.springframework.cloud.stream.messaging.Source; -import org.springframework.cloud.stream.test.binder.MessageCollector; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.junit.Assert.assertThat; - -@RunWith(SpringRunner.class) -@SpringBootTest(classes = FunctionConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) -@TestPropertySource(properties = { - "function.location=file:target/it/support/target/function-sample-1.0.0.M1.jar", }) -public abstract class FunctionConfigurationTests { - - @Autowired - protected MessageCollector messageCollector; - - @EnableAutoConfiguration - @TestPropertySource(properties = { "function.bean=com.example.functions.Emitter" }) - public static class SourceTests extends FunctionConfigurationTests { - - @Autowired - private Source source; - - @Test - public void test() throws Exception { - - Message received = messageCollector.forChannel(source.output()).poll(2, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is("one")); - - } - - } - - @EnableAutoConfiguration - @TestPropertySource(properties = { - "function.bean=com.example.functions.Emitter,com.example.functions.LengthCounter" }) - public static class CompositeTests extends FunctionConfigurationTests { - - @Autowired - private Source source; - - @Test - public void test() throws Exception { - - Message received = messageCollector.forChannel(source.output()).poll(2, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is(3)); - - } - - } - - @EnableAutoConfiguration - @TestPropertySource(properties = { - "function.bean=com.example.functions.LengthCounter" }) - public static class ProcessorTests extends FunctionConfigurationTests { - - @Autowired - private Processor processor; - - @Test - public void test() throws Exception { - processor.input().send(MessageBuilder.withPayload("hello").build()); - Message received = messageCollector.forChannel(processor.output()).poll(1, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is("hello".length())); - - } - - } - - @EnableAutoConfiguration - @TestPropertySource(properties = { - "function.bean=com.example.functions.DoubleLogger" }) - public static class SinkTests extends FunctionConfigurationTests { - - @Autowired - private Sink sink; - - @Test - public void test() throws Exception { - // Can't assert side effects. - sink.input().send(MessageBuilder.withPayload(5).build()); - } - - } - -} \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java new file mode 100644 index 000000000..f9bebf7f6 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2018 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.cloud.function.deployer; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { FunctionDeployerConfiguration.class }) +@DirtiesContext +public abstract class FunctionCreatorConfigurationTests { + + @Autowired + protected FunctionCatalog catalog; + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Doubler" }) + public static class SingleFunctionTests extends FunctionCreatorConfigurationTests { + + @Test + public void testDouble() { + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo(4); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=app:classpath", + "function.bean=org.springframework.cloud.function.test.SpringDoubler" }) + public static class ManualSpringFunctionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testDouble() { + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo(4); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.NumberEmitter," + + "org.springframework.cloud.function.test.Frenchizer" }) + public static class SupplierCompositionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testSupplier() { + Supplier function = catalog.lookup(Supplier.class, "function0"); + assertThat(function).isNull(); + } + + @Test + public void testFunction() { + Supplier> function = catalog.lookup(Supplier.class, + "function0|function1"); + assertThat(function.get().blockFirst()).isEqualTo("un"); + } + + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Doubler," + + "org.springframework.cloud.function.test.Frenchizer" }) + public static class FunctionCompositionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testFunction() { + Function, Flux> function = catalog + .lookup(Function.class, "function0|function1"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo("quatre"); + } + + @Test + public void testThen() { + Function function = catalog.lookup(Function.class, + "function1"); + assertThat(function).isNull(); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Frenchizer," + + "org.springframework.cloud.function.test.Printer" }) + public static class ConsumerCompositionTests + extends FunctionCreatorConfigurationTests { + + @Rule + public OutputCapture capture = new OutputCapture(); + + @Test + public void testConsumer() { + Function, Mono> function = catalog.lookup(Function.class, + "function0|function1"); + function.apply(Flux.just(2)).block(); + capture.expect(containsString("Seen deux")); + } + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java index 175fc6fb5..0716ee552 100644 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java @@ -16,50 +16,45 @@ package org.springframework.cloud.function.deployer; -import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; -import org.hamcrest.Matchers; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.stream.messaging.Processor; -import org.springframework.cloud.stream.messaging.Sink; -import org.springframework.cloud.stream.messaging.Source; -import org.springframework.cloud.stream.test.binder.MessageCollector; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @RunWith(SpringRunner.class) -@SpringBootTest(classes = FunctionConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(classes = FunctionDeployerConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) @TestPropertySource(properties = { "function.location=file:target/it/support/target/function-sample-1.0.0.M1-exec.jar", }) public abstract class SpringFunctionAppConfigurationTests { @Autowired - protected MessageCollector messageCollector; + protected FunctionCatalog catalog; @EnableAutoConfiguration @TestPropertySource(properties = { "function.bean=myEmitter", "function.main=com.example.functions.FunctionApp" }) public static class SourceTests extends SpringFunctionAppConfigurationTests { - @Autowired - private Source source; - @Test public void test() throws Exception { - - Message received = messageCollector.forChannel(source.output()).poll(2, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is("one")); - + Supplier> function = catalog.lookup(Supplier.class, "function0"); + assertThat(function.get().blockFirst()).isEqualTo("one"); } } @@ -69,16 +64,11 @@ public abstract class SpringFunctionAppConfigurationTests { "function.main=com.example.functions.FunctionApp" }) public static class CompositeTests extends SpringFunctionAppConfigurationTests { - @Autowired - private Source source; - @Test public void test() throws Exception { - - Message received = messageCollector.forChannel(source.output()).poll(2, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is(3)); - + Supplier> function = catalog.lookup(Supplier.class, + "function0|function1"); + assertThat(function.get().blockFirst()).isEqualTo(3); } } @@ -88,16 +78,11 @@ public abstract class SpringFunctionAppConfigurationTests { "function.main=com.example.functions.FunctionApp" }) public static class ProcessorTests extends SpringFunctionAppConfigurationTests { - @Autowired - private Processor processor; - @Test public void test() throws Exception { - processor.input().send(MessageBuilder.withPayload("hello").build()); - Message received = messageCollector.forChannel(processor.output()).poll(1, - TimeUnit.SECONDS); - assertThat(received.getPayload(), Matchers.is("hello".length())); - + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just("spam")).blockFirst()).isEqualTo(4); } } @@ -107,13 +92,16 @@ public abstract class SpringFunctionAppConfigurationTests { "function.main=com.example.functions.FunctionApp" }) public static class SinkTests extends SpringFunctionAppConfigurationTests { - @Autowired - private Sink sink; + @Rule + public OutputCapture capture = new OutputCapture(); @Test public void test() throws Exception { // Can't assert side effects. - sink.input().send(MessageBuilder.withPayload(5).build()); + Function, Mono> function = catalog.lookup(Function.class, + "function0"); + function.apply(Flux.just(5)).block(); + capture.expect(containsString(String.format("10%n"))); } } diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java new file mode 100644 index 000000000..bd5310951 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import java.util.function.Function; + +public class Doubler implements Function { + @Override + public Integer apply(Integer integer) { + return 2 * integer; + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java new file mode 100644 index 000000000..7c9eb3fbd --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import java.util.function.Function; + +import javax.annotation.PostConstruct; + +public class Frenchizer implements Function { + + private String[] numbers; + + @PostConstruct + public void init() { + this.numbers = new String[4]; + numbers[0] = "un"; + numbers[1] = "deux"; + numbers[2] = "trois"; + numbers[3] = "quatre"; + } + + @Override + public String apply(Integer integer) { + if (integer < this.numbers.length + 1) { + return this.numbers[integer - 1]; + } + throw new RuntimeException(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java new file mode 100644 index 000000000..d0972702e --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java @@ -0,0 +1,42 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * @author Dave Syer + */ +@SpringBootConfiguration +public class FunctionApp { + + @Bean + public Doubler myDoubler() { + return new Doubler(); + } + + @Bean + public Frenchizer myFrenchizer() { + return new Frenchizer(); + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(FunctionApp.class, args); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java new file mode 100644 index 000000000..2bfde2188 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import java.util.function.Supplier; + +public class NumberEmitter implements Supplier { + @Override + public Integer get() { + return 1; + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java new file mode 100644 index 000000000..99e64502a --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import java.util.function.Consumer; + +public class Printer implements Consumer { + + @Override + public void accept(Object o) { + System.err.println("Seen " + o); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java new file mode 100644 index 000000000..20a779800 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 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.cloud.function.test; + +import java.util.function.Function; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.Banner.Mode; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; + +public class SpringDoubler implements Function { + + @Autowired + private ConfigurableApplicationContext context; + + @PostConstruct + public void init() { + if (this.context == null) { + context = new SpringApplicationBuilder(FunctionApp.class).bannerMode(Mode.OFF).registerShutdownHook(false) + .web(false).run(); + } + } + + @PreDestroy + public void close() { + if (context != null) { + context.close(); + } + } + + @Override + public Integer apply(Integer integer) { + return 2 * integer; + } +} diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java index 7a90fb165..525c17ac7 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java @@ -99,10 +99,10 @@ public class FluxHandlerMethodArgumentResolver body = null; } else { - try { + if (json.startsWith("[")) { body = mapper.toList(json, type); } - catch (IllegalArgumentException e) { + else { nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true); body = Arrays.asList(mapper.toSingle(json, type)); }