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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user