Added additional features to launcehr loader to ensure it is java 11 aware

Added integration tests to validate java 11 awareness (specifically about some javax packages)
Added integration tests to validate Function-Class discovery
Added spring.factories for FunctionDeployerConfiguration
This commit is contained in:
Oleg Zhurakousky
2019-08-12 12:35:28 +02:00
parent ba05872a01
commit 3bc03a640a
8 changed files with 250 additions and 19 deletions

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>function.example</groupId>
<artifactId>bootapp-with-javax</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath />
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud-function.version>3.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<wrapper.version>1.0.17.RELEASE</wrapper.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack</id>
<phase>package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<classifier>exec</classifier>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,57 @@
package function.example;
import java.util.function.Function;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SimpleFunctionAppApplication {
public static void main(String[] args) {
SpringApplication.run(SimpleFunctionAppApplication.class, args);
}
@Bean
public Function<String, String> uppercase() {
System.out.println("==> CREATING 'uppercase' FUNCTION bean");
return new UpperCaseFunction();
}
@Bean
public Function<Person, Person> uppercasePerson() {
System.out.println("==> CREATING 'uppercasePerson' FUNCTION bean");
return person -> {
Person p = new Person();
p.setId(person.getId());
p.setName(person.getName().toUpperCase());
return p;
};
}
public static class Person {
private String name;
private int id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
}

View File

@@ -0,0 +1,23 @@
package function.example;
import java.util.function.Function;
import javax.mail.Address;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
public class UpperCaseFunction implements Function<String, String> {
@Override
public String apply(String value) {
System.out.println("Uppercasing " + value);
try {
Address address = new InternetAddress(value);
}
catch (AddressException e) {
throw new IllegalStateException("Failed to create and address: ", e);
}
return value.toUpperCase();
}
}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>function.example</groupId>
@@ -13,7 +13,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath/>
<relativePath />
</parent>
<properties>
@@ -39,6 +39,17 @@
<classifier>exec</classifier>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Function-Class>function.example.UpperCaseFunction</Function-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>

View File

@@ -92,8 +92,9 @@ class FunctionArchiveDeployer extends JarLauncher {
}
}
if (!StringUtils.isEmpty(functionProperties.getFunctionClass())) {
FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionProperties.getFunctionClass());
String functionClassName = discoverFunctionClassName(functionProperties);
if (!StringUtils.isEmpty(functionClassName)) {
FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionClassName);
if (registration != null) {
functionRegistry.register(registration);
}
@@ -125,20 +126,28 @@ class FunctionArchiveDeployer extends JarLauncher {
.copyToByteArray(DeployerContextUtils.class.getClassLoader().getResourceAsStream(classAsPath));
/*
* While LaunchedURLClassLoader is completely disconnected with the current
* class loader, this will ensure that classes in org.reactivestreams.* and reactor.*
* are shared across two class loaders since they are effectively used as transport.
* class loader, this will ensure that certain classes (e.g., org.reactivestreams.* see #shouldLoadViaDeployerLoader() )
* are shared across two class loaders.
*/
this.archiveLoader = new LaunchedURLClassLoader(urls, null) {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clazz = null;
if (shouldLoadViaDeployerLoader(name)) {
clazz = getClass().getClassLoader().loadClass(name);
try {
clazz = getClass().getClassLoader().loadClass(name);
}
catch (Exception e) {
if (logger.isDebugEnabled()) {
logger.debug("Class '" + name + "' is not available in the current class loader. Loading it from the deployed archive.");
}
clazz = super.loadClass(name, false);
}
}
else if (name.equals(DeployerContextUtils.class.getName())) {
/*
* This will ensure that `DeployerContextUtils` is available to
* foreign class loader even in cases where foreign JAR does not
* foreign class loader for cases where foreign JAR does not
* have SCF dependencies.
*/
try {
@@ -164,6 +173,17 @@ class FunctionArchiveDeployer extends JarLauncher {
|| name.startsWith("com.sun");
}
private String discoverFunctionClassName(FunctionProperties functionProperties) {
try {
return StringUtils.hasText(functionProperties.getFunctionClass())
? functionProperties.getFunctionClass()
: this.getArchive().getManifest().getMainAttributes().getValue("Function-Class");
}
catch (Exception e) {
throw new IllegalStateException("Failed to discover function class name", e);
}
}
private boolean isBootApplicationWithMain() {
try {
return StringUtils.hasText(this.getArchive().getManifest().getMainAttributes().getValue("Start-Class"));

View File

@@ -23,13 +23,13 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
@@ -38,9 +38,9 @@ import org.springframework.context.annotation.Bean;
* @since 3.0
*
*/
@EnableAutoConfiguration
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(FunctionProperties.class)
public final class FunctionDeployerConfiguration {
public class FunctionDeployerConfiguration {
private static Log logger = LogFactory.getLog(FunctionDeployerConfiguration.class);

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.function.deployer.FunctionDeployerConfiguration

View File

@@ -27,6 +27,7 @@ import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.Message;
@@ -51,7 +52,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjar/target/bootjar-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.UpperCaseFunction" };
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<String, String> function = catalog.lookup("upperCaseFunction");
@@ -75,7 +76,32 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.UpperCaseFunction" };
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<String, String> function = catalog.lookup("upperCaseFunction");
assertThat(function.apply("bob")).isEqualTo("BOB");
assertThat(function.apply("stacy")).isEqualTo("STACY");
Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction");
List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block();
assertThat(results.get(0)).isEqualTo("BOB");
assertThat(results.get(1)).isEqualTo("STACY");
}
/*
* Target function `class UpperCaseFunction implements Function<String, String>`
* No Main/Start class present, no Spring configuration
*
* Function class is discovered via 'Function-Class` manifest entry
*/
@Test
public void testNoMainAndNoStartClassAndNoSpringConfigurationDiscoverClassFromManifest() throws Exception {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-0.0.1.BUILD-SNAPSHOT-exec.jar" };
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<String, String> function = catalog.lookup("upperCaseFunction");
@@ -99,7 +125,7 @@ public class FunctionDeployerTests {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootapp/target/bootapp-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercase" };
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json");
@@ -108,6 +134,25 @@ public class FunctionDeployerTests {
assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"BOB\"");
}
/*
* Same as above but:
* Given that Java 11 does not include 'javax' packages, this test simply validates that
* the delegation will be made to archive loader where it is available
*/
@Test
public void testWithMainAndStartClassAndSpringConfigurationJavax() throws Exception {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootapp-with-javax/target/bootapp-with-javax-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercase" };
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json");
Message<byte[]> result = function
.apply(MessageBuilder.withPayload("\"foo@bar.com\"".getBytes(StandardCharsets.UTF_8)).build());
assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"FOO@BAR.COM\"");
}
/*
* Target function:
*
@@ -120,7 +165,7 @@ public class FunctionDeployerTests {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootapp-with-scf/target/bootapp-with-scf-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercase" };
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercase", "application/json");
@@ -140,7 +185,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootapp/target/bootapp-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercasePerson" };
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Message<byte[]>, Message<byte[]>> function = catalog.lookup("uppercasePerson", "application/json");
@@ -160,7 +205,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootapp-multi/target/bootapp-multi-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=fn"
};
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>, Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>> function =
@@ -202,7 +247,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjar-multi/target/bootjar-multi-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.Repeater"
};
ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function<Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>, Tuple2<Flux<Message<byte[]>>, Flux<Message<byte[]>>>> function =
@@ -233,4 +278,7 @@ public class FunctionDeployerTests {
assertThat(result2.get(1)).isEqualTo("2");
}
@SpringBootApplication(proxyBeanMethods = false)
private static class DeployerApplication {
}
}