Added new deployer experimental module

This commit is contained in:
Oleg Zhurakousky
2019-08-05 17:04:30 +02:00
parent 38981003b9
commit 203687e45f
15 changed files with 1203 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
/**
*
* @author Oleg Zhurakousky
* @since 3.0
*/
public abstract class ApplicationContainer {
private final FunctionCatalog functionCatalog;
private final FunctionInspector functionInspector;
private final FunctionProperties functionProperties;
public ApplicationContainer(FunctionCatalog functionCatalog,
FunctionInspector functionInspector, FunctionProperties functionProperties) {
this.functionCatalog = functionCatalog;
this.functionInspector = functionInspector;
this.functionProperties = functionProperties;
}
protected FunctionCatalog getFunctionCatalog() {
return this.functionCatalog;
}
protected FunctionInspector getFunctionInspector() {
return this.functionInspector;
}
protected FunctionProperties getFunctionProperties() {
return this.functionProperties;
}
}

View File

@@ -0,0 +1,121 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.Resource;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* @author Oleg Zhurakousky
* @since 3.0
*/
abstract class DeployerContextUtils {
public static Type findType(BeanFactory beanFactory, String name) {
ConfigurableListableBeanFactory registry = (ConfigurableListableBeanFactory) beanFactory;
AbstractBeanDefinition definition = (AbstractBeanDefinition) registry.getBeanDefinition(name);
Object source = definition.getSource();
Type param = null;
if (source instanceof MethodMetadata) {
param = findBeanType(definition, ((MethodMetadata) source).getDeclaringClassName(), ((MethodMetadata) source).getMethodName());
}
else if (source instanceof Resource) {
param = registry.getType(name);
}
else {
ResolvableType type = (ResolvableType) getField(definition, "targetType");
if (type != null) {
param = type.getType();
}
}
return param;
}
private static Type findBeanType(AbstractBeanDefinition definition, String declaringClassName, String methodName) {
Class<?> factory = ClassUtils.resolveClassName(declaringClassName, null);
Class<?>[] params = getParamTypes(factory, definition);
Method method = ReflectionUtils.findMethod(factory, methodName,
params);
Type type = method.getGenericReturnType();
return type;
}
private static Class<?>[] getParamTypes(Class<?> factory,
AbstractBeanDefinition definition) {
if (definition instanceof RootBeanDefinition) {
RootBeanDefinition root = (RootBeanDefinition) definition;
for (Method method : getCandidateMethods(factory, root)) {
if (root.isFactoryMethod(method)) {
return method.getParameterTypes();
}
}
}
List<Class<?>> params = new ArrayList<>();
for (ConstructorArgumentValues.ValueHolder holder : definition
.getConstructorArgumentValues().getIndexedArgumentValues().values()) {
params.add(ClassUtils.resolveClassName(holder.getType(), null));
}
return params.toArray(new Class<?>[0]);
}
private static Method[] getCandidateMethods(final Class<?> factoryClass,
final RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
return AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
@Override
public Method[] run() {
return (mbd.isNonPublicAccessAllowed()
? ReflectionUtils.getAllDeclaredMethods(factoryClass)
: factoryClass.getMethods());
}
});
}
else {
return (mbd.isNonPublicAccessAllowed()
? ReflectionUtils.getAllDeclaredMethods(factoryClass)
: factoryClass.getMethods());
}
}
private static Object getField(Object target, String name) {
Field field = ReflectionUtils.findField(target.getClass(), name);
if (field == null) {
return null;
}
ReflectionUtils.makeAccessible(field);
return ReflectionUtils.getField(field, target);
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.loader.JarLauncher;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.jar.JarFile;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.util.StreamUtils;
/**
*
* @author Oleg Zhurakousky
* @since 3.0
*
*/
class ExternalFunctionJarLauncher extends JarLauncher {
private static Log logger = LogFactory.getLog(ExternalFunctionJarLauncher.class);
private final StandardEvaluationContext evalContext = new StandardEvaluationContext();
private final Archive archive;
ExternalFunctionJarLauncher(Archive archive) {
super(archive);
this.archive = archive;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void deploy(ApplicationContext deployerContext, String[] args) {
ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
try {
this.launch(deployerContext, args);
Map<String, Object> functions = this.discoverFunctions();
if (logger.isInfoEnabled()) {
logger.info("Discovered functions: " + functions);
}
FunctionRegistry functionRegistry = deployerContext.getBean(FunctionRegistry.class);
for (Entry<String, Object> entry : functions.entrySet()) {
FunctionRegistration registration = new FunctionRegistration(entry.getValue(), entry.getKey());
Type type = this.findType(entry.getKey());
if (logger.isInfoEnabled()) {
logger.info("Registering function '" + entry.getKey() + "' of type '" + type
+ "' in FunctionRegistry.");
}
registration.type(type);
functionRegistry.register(registration);
}
}
catch (Exception e) {
throw new IllegalStateException("Failed to deploy archive " + archive, e);
}
finally {
Thread.currentThread().setContextClassLoader(currentLoader);
}
}
@Override
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
String className = DeployerContextUtils.class.getName();
String classAsPath = className.replace('.', '/') + ".class";
byte[] fcuBytes = StreamUtils
.copyToByteArray(DeployerContextUtils.class.getClassLoader().getResourceAsStream(classAsPath));
/*
* While LaunchedURLClassLoader is completely disconnected with the current
* class loader, this will still allow it to see FunctionContextUtils
*/
return new ClassLoader(new LaunchedURLClassLoader(urls, null)) {
boolean functionContextUtilsLoaded;
@Override
protected Class<?> findClass(final String name) throws ClassNotFoundException {
if (!functionContextUtilsLoaded && className.equals(name)) {
Class<?> fcuClass = defineClass(name, fcuBytes, 0, fcuBytes.length);
this.functionContextUtilsLoaded = true;
return fcuClass;
}
return super.findClass(name);
}
};
}
private void launch(ApplicationContext deployerContext, String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
Thread.currentThread().setContextClassLoader(createClassLoader(getClassPathArchives()));
evalContext.setTypeLocator(new StandardTypeLocator(Thread.currentThread().getContextClassLoader()));
String mainClassName = getMainClass();
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(mainClassName);
Class<?> bootAppClass = Thread.currentThread().getContextClassLoader()
.loadClass(SpringApplication.class.getName());
Method runMethod = bootAppClass.getDeclaredMethod("run", Class.class, String[].class);
Object applicationContext = runMethod.invoke(null, mainClass, (Object) args);
if (logger.isInfoEnabled()) {
logger.info("Application context for archive '" + archive.getUrl() + "' is created.");
}
evalContext.setVariable("context", applicationContext);
setBeanFactory(applicationContext);
}
private void setBeanFactory(Object applicationContext) throws Exception {
Expression parsed = new SpelExpressionParser().parseExpression("#context.getBeanFactory()");
Object beanFactory = parsed.getValue(evalContext);
evalContext.setVariable("bf", beanFactory);
}
private Type findType(String name) {
evalContext.setVariable("functionName", name);
String expr = "T(" + DeployerContextUtils.class.getName() + ").findType(#bf, #functionName)";
Expression parsed = new SpelExpressionParser().parseExpression(expr);
Object type = parsed.getValue(evalContext);
return (Type) type;
}
@SuppressWarnings("unchecked")
private Map<String, Object> discoverFunctions() throws Exception {
Map<String, Object> allFunctions = new HashMap<String, Object>();
Expression parsed = new SpelExpressionParser()
.parseExpression("#context.getBeansOfType(T(java.util.function.Function))");
allFunctions.putAll((Map<String, Object>) parsed.getValue(evalContext));
parsed = new SpelExpressionParser().parseExpression("#context.getBeansOfType(T(java.util.function.Supplier))");
allFunctions.putAll((Map<String, Object>) parsed.getValue(evalContext));
parsed = new SpelExpressionParser().parseExpression("#context.getBeansOfType(T(java.util.function.Consumer))");
allFunctions.putAll((Map<String, Object>) parsed.getValue(evalContext));
return allFunctions;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer;
import java.io.File;
import java.lang.reflect.Constructor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
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.FunctionCatalog;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
/**
*
* @author Oleg Zhurakousky
* @since 3.0
*/
@SpringBootApplication
@EnableConfigurationProperties(FunctionProperties.class)
public class FunctionDeployerBootstrap implements ApplicationContextAware {
public static FunctionDeployerBootstrap instance(String... args) {
ApplicationContext context = SpringApplication.run(FunctionDeployerBootstrap.class, args);
return context.getBean(FunctionDeployerBootstrap.class);
}
private ConfigurableApplicationContext applicationContext;
@Autowired
private FunctionProperties functionProperties;
@Autowired
private FunctionCatalog functionCatalog;
@Autowired
private FunctionInspector functionInspector;
@SuppressWarnings("unchecked")
public <T extends ApplicationContainer> T run(Class<?> configurationClass, String... args) {
try {
Archive archive = new JarFileArchive(new File(functionProperties.getLocation()));
ExternalFunctionJarLauncher launcher = new ExternalFunctionJarLauncher(archive);
launcher.deploy(this.applicationContext, args);
Constructor<? extends ApplicationContainer> applicationContainerCtr = (Constructor<? extends ApplicationContainer>) configurationClass
.getDeclaredConstructor(FunctionCatalog.class, FunctionInspector.class, FunctionProperties.class);
ApplicationContainer applicationContainer = applicationContainerCtr.newInstance(this.functionCatalog,
this.functionInspector, this.functionProperties);
return (T) applicationContainer;
}
catch (Exception e) {
throw new IllegalStateException("Failed to launch archive: " + functionProperties.getLocation(), e);
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer;
import javax.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
/**
* Configuration properties for deciding how to locate the functional class to execute.
*
* @author Eric Bottard
*/
@ConfigurationProperties("spring.cloud.function")
public class FunctionProperties {
/**
* Location(s) of jar archives containing the supplier/function/consumer class to run.
*/
private String location;
private String functionName;
public void setFunctionName(String functionName) {
this.functionName = functionName;
}
public String getName() {
return this.functionName;
}
public String getLocation() {
return this.location;
}
public void setLocation(String location) {
this.location = location;
}
@PostConstruct
public void init() {
Assert.notNull(this.location, "No archive location provided, please configure spring.cloud.function.location as a jar or directory.");
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2017-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.deployer.sample;
import java.util.function.Function;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.catalog.FunctionInspector;
import org.springframework.cloud.function.deployer.ApplicationContainer;
import org.springframework.cloud.function.deployer.FunctionDeployerBootstrap;
import org.springframework.cloud.function.deployer.FunctionProperties;
/**
*
* @author Oleg Zhurakousky
* @since 3.0
*/
public class SampleInvoker extends ApplicationContainer {
public static void main(String[] args) throws Exception {
SampleInvoker invoker = FunctionDeployerBootstrap.instance(
"--spring.cloud.function.location=/Users/olegz/Downloads/simple-function-app/target/simple-function-app-0.0.1-SNAPSHOT.jar",
"--spring.cloud.function.function-name=uppercase")
.run(SampleInvoker.class, args);
System.out.println(invoker.uppercase("eric"));
System.out.println(invoker.uppercase("oleg"));
}
private Function<String, String> function;
public SampleInvoker(FunctionCatalog functionCatalog, FunctionInspector functionInspector,
FunctionProperties functionProperties) {
super(functionCatalog, functionInspector, functionProperties);
this.function = this.getFunctionCatalog().lookup(functionProperties.getName());
}
public String uppercase(String value) {
return this.function.apply(value);
}
}