Support for isolated class loaders extended to cover more functions

Functions with Flux and Message (as well as POJOs and Flux of POJO
which were already supported) should now work if they are created in
an isolated class loader. Preconditions:

* The class loaders must have the reactor-core (and reactive-streams)
shared between the app and the function. Practically speaking this means
there has to be a parent class loader with just reactive types, and
sibling children for the app and the function. This is not a new
requirement (it was needed for Flux of POJO anyway).

* Message types are handled reflectively, so they don't have to be in a
shared class loader. But they do have to be  on the class path on
both sides (obviously).
This commit is contained in:
Dave Syer
2018-02-16 08:16:55 +00:00
parent ccd3953163
commit 1b624c3531
17 changed files with 615 additions and 33 deletions

View File

@@ -27,6 +27,7 @@ import java.util.function.Function;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.context.message.MessageUtils;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
@@ -91,6 +92,9 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
return flux.publish(values -> {
Flux<?> result = function
.apply(values.map(message -> convertInput(function).apply(message)));
if (this.functionInspector.isMessage(function)) {
result = result.map(message -> MessageUtils.unpack(function, message));
}
Flux<Map<String, Object>> aggregate = headers(values);
return result.withLatestFrom(aggregate, (p, m) -> message(p, m));
});
@@ -141,7 +145,7 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
.get(StreamConfigurationProperties.ROUTE_KEY);
name = stash(key);
}
if (name==null && defaultRoute != null) {
if (name == null && defaultRoute != null) {
name = stash(defaultRoute);
}
if (name == null) {
@@ -155,10 +159,10 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
else {
for (String candidate : names) {
Object function = functionCatalog.lookupFunction(candidate);
if (function==null) {
if (function == null) {
function = functionCatalog.lookupConsumer(candidate);
}
if (function==null) {
if (function == null) {
continue;
}
Class<?> inputType = functionInspector.getInputType(function);
@@ -202,8 +206,8 @@ public class StreamListeningFunctionInvoker implements SmartInitializingSingleto
Class<?> inputType = functionInspector.getInputType(function);
return m -> {
if (functionInspector.isMessage(function)) {
return MessageBuilder.withPayload(convertPayload(inputType, m))
.copyHeaders(m.getHeaders()).build();
return MessageUtils.create(function, convertPayload(inputType, m),
m.getHeaders());
}
else {
return convertPayload(inputType, m);

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.cloud.function.context.message.MessageUtils;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.endpoint.MessageProducerSupport;
@@ -86,7 +87,8 @@ public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
if (supplier != null) {
suppliers.add(name);
disposables.put(name,
supplier.get().subscribeOn(Schedulers.elastic()).subscribe(m -> send(name, m)));
supplier.get().subscribeOn(Schedulers.elastic())
.subscribe(m -> send(name, m)));
}
}
}
@@ -94,16 +96,10 @@ public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
}
private void send(String name, Object payload) {
Message<?> message;
if (payload instanceof Message) {
message = MessageBuilder.fromMessage((Message<?>) payload)
.setHeaderIfAbsent(StreamConfigurationProperties.ROUTE_KEY, name)
.build();
}
else {
message = MessageBuilder.withPayload(payload)
.setHeader(StreamConfigurationProperties.ROUTE_KEY, name).build();
}
Supplier<Flux<?>> supplier = functionCatalog.lookupSupplier(name);
Message<?> message = MessageUtils.unpack(supplier, payload);
message = MessageBuilder.fromMessage(message)
.setHeaderIfAbsent(StreamConfigurationProperties.ROUTE_KEY, name).build();
getOutputChannel().send(message);
}

View File

@@ -0,0 +1,154 @@
/*
* 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.stream.function;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*
*/
public class ClassLoaderUtils {
public static ClassLoader createClassLoader() {
URL[] urls = findClassPath();
if (urls.length == 1) {
URL[] classpath = extractClasspath(urls[0]);
if (classpath != null) {
urls = classpath;
}
}
List<URL> child = new ArrayList<>();
for (URL url : urls) {
child.add(url);
}
for (URL url : urls) {
if (isRoot(StringUtils.getFilename(clean(url.toString())))) {
child.remove(url);
}
}
ClassLoader base = ClassLoaderUtils.class.getClassLoader();
return new ParentLastURLClassLoader(child.toArray(new URL[0]), base);
}
private static URL[] extractClasspath(URL url) {
// This works for a jar indirection like in surefire and IntelliJ
if (url.toString().endsWith(".jar")) {
JarFile jar;
try {
jar = new JarFile(new File(url.toURI()));
String path = jar.getManifest().getMainAttributes()
.getValue("Class-Path");
if (path != null) {
List<URL> result = new ArrayList<>();
for (String element : path.split(" ")) {
result.add(new URL(element));
}
return result.toArray(new URL[0]);
}
}
catch (Exception e) {
}
}
return null;
}
private static String clean(String jar) {
// This works with fat jars like Spring Boot where the path elements look like
// jar:file:...something.jar!/.
return jar.endsWith("!/") ? jar.substring(0, jar.length() - 2) : jar;
}
private static URL[] findClassPath() {
return ((URLClassLoader) ClassLoaderUtils.class.getClassLoader()).getURLs();
}
private static boolean isRoot(String file) {
return file.startsWith("reactor-core") || file.startsWith("reactive-streams");
}
private static class ParentLastURLClassLoader extends ClassLoader {
private ChildURLClassLoader childClassLoader;
/**
* This class allows me to call findClass on a classloader
*/
private static class FindClassClassLoader extends ClassLoader {
public FindClassClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
}
/**
* This class delegates (child then parent) for the findClass method for a
* URLClassLoader. We need this because findClass is protected in URLClassLoader
*/
private static class ChildURLClassLoader extends URLClassLoader {
private FindClassClassLoader realParent;
public ChildURLClassLoader(URL[] urls, FindClassClassLoader realParent) {
super(urls, null);
this.realParent = realParent;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
// first try to use the URLClassLoader findClass
return super.findClass(name);
}
catch (ClassNotFoundException e) {
// if that fails, we ask our real parent classloader to load the class
// (we give up)
return realParent.loadClass(name);
}
}
}
public ParentLastURLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
childClassLoader = new ChildURLClassLoader(urls,
new FindClassClassLoader(this.getParent()));
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
try {
// first we try to find a class inside the child classloader
return childClassLoader.findClass(name);
}
catch (ClassNotFoundException e) {
// didn't find it, try the parent
return super.loadClass(name, resolve);
}
}
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.stream.function;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.annotation.PostConstruct;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = IsolatedFluxMessagePojoStreamingFunctionTests.StreamingFunctionApplication.class)
public class IsolatedFluxMessagePojoStreamingFunctionTests {
@Autowired
Processor processor;
@Autowired
MessageCollector messageCollector;
@Test
public void test() throws Exception {
processor.input().send(
MessageBuilder.withPayload(new String("{\"name\":\"foo\"}")).build());
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
TimeUnit.MILLISECONDS);
assertThat(result).isInstanceOf(Message.class);
}
@SpringBootApplication
public static class StreamingFunctionApplication {
@Autowired
private FunctionRegistry registry;
@PostConstruct
public void register() {
// TODO: this class loader doesn't really test the isolation properly. Not
// sure why, but if you remove the reflection in MessageUtils the test is
// still green.
ClassLoader loader = ClassLoaderUtils.createClassLoader();
Class<?> type = ClassUtils.resolveClassName(Uppercase.class.getName(),
loader);
registry.register(
new FunctionRegistration<Object>(BeanUtils.instantiate(type))
.name("uppercase"));
}
}
public static class Uppercase
implements Function<Flux<Message<Foo>>, Flux<Message<Foo>>> {
@Override
public Flux<Message<Foo>> apply(Flux<Message<Foo>> flux) {
return flux.map(message -> MessageBuilder
.withPayload(new Foo(message.getPayload().getName().toUpperCase()))
.build());
}
}
protected static class Foo {
private String name;
Foo() {
}
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.stream.function;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import javax.annotation.PostConstruct;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.cloud.stream.test.binder.MessageCollector;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Dave Syer
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = IsolatedMessagePojoStreamingFunctionTests.StreamingFunctionApplication.class)
public class IsolatedMessagePojoStreamingFunctionTests {
@Autowired
Processor processor;
@Autowired
MessageCollector messageCollector;
@Test
public void test() throws Exception {
processor.input().send(
MessageBuilder.withPayload(new String("{\"name\":\"foo\"}")).build());
Message<?> result = messageCollector.forChannel(processor.output()).poll(1000,
TimeUnit.MILLISECONDS);
assertThat(result.getPayload().getClass().getName())
.isEqualTo(Foo.class.getName());
}
@SpringBootApplication
public static class StreamingFunctionApplication {
@Autowired
private FunctionRegistry registry;
@PostConstruct
public void register() {
ClassLoader loader = ClassLoaderUtils.createClassLoader();
Class<?> type = ClassUtils.resolveClassName(Uppercase.class.getName(),
loader);
registry.register(
new FunctionRegistration<Object>(BeanUtils.instantiate(type))
.name("uppercase"));
}
}
public static class Uppercase implements Function<Message<Foo>, Message<Foo>> {
@Override
public Message<Foo> apply(Message<Foo> flux) {
return MessageBuilder
.withPayload(new Foo(flux.getPayload().getName().toUpperCase()))
.build();
}
}
protected static class Foo {
private String name;
Foo() {
}
public Foo(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}