Allow user to provide own Gson or ObjectMapper

This commit is contained in:
Dave Syer
2018-06-22 16:06:39 +01:00
parent 4c9627aee3
commit 94b1e56d55
4 changed files with 232 additions and 211 deletions

View File

@@ -1,205 +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.context.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.context.catalog.InMemoryFunctionCatalog;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* @author Dave Syer
*
*/
public class ContextFunctionCatalogBeanRegistrar implements ApplicationContextAware,
ApplicationContextInitializer<GenericApplicationContext> {
private GenericApplicationContext context;
@Override
public void initialize(GenericApplicationContext applicationContext) {
if (applicationContext.getEnvironment().getProperty("spring.functional.enabled",
Boolean.class, false)) {
register(applicationContext);
}
}
public void register(BeanDefinitionRegistry registry) throws BeansException {
try {
register(registry, beanFactory(registry));
}
catch (BeansException e) {
throw e;
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new BeanCreationException("Cannot register from " + getClass(), e);
}
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
Assert.isInstanceOf(GenericApplicationContext.class, context,
"Expecting a GenericApplicationContext");
this.context = (GenericApplicationContext) context;
}
private ConfigurableListableBeanFactory beanFactory(BeanDefinitionRegistry registry) {
if (this.context == null && registry instanceof AutowireCapableBeanFactory) {
((AutowireCapableBeanFactory) registry).initializeBean(this,
getClass().getSimpleName());
}
if (this.context == null && registry instanceof GenericApplicationContext) {
this.context = (GenericApplicationContext) registry;
}
if (this.context != null) {
return this.context.getDefaultListableBeanFactory();
}
if (registry instanceof ConfigurableListableBeanFactory) {
return (ConfigurableListableBeanFactory) registry;
}
if (registry instanceof ConfigurableApplicationContext) {
return ((ConfigurableApplicationContext) registry).getBeanFactory();
}
throw new IllegalStateException(
"Cannot locate ConfigurableListableBeanFactory in " + registry);
}
protected void register(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory factory) throws Exception {
performPreinitialization();
context.registerBean(PropertySourcesPlaceholderConfigurer.class,
() -> PropertyPlaceholderAutoConfiguration
.propertySourcesPlaceholderConfigurer());
context.registerBean(
AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME,
AutowiredAnnotationBeanPostProcessor.class);
context.registerBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
ConfigurationBeanFactoryMetadata.class,
() -> new ConfigurationBeanFactoryMetadata());
context.registerBean(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME,
ConfigurationPropertiesBindingPostProcessor.class,
() -> new ConfigurationPropertiesBindingPostProcessor());
// TODO: use Boot to create Gson and ObjectMapper
if (ClassUtils.isPresent("com.google.gson.Gson", null)
&& !"gson".equals(context.getEnvironment().getProperty(
ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
"gson"))) {
context.registerBean(Gson.class, () -> new Gson());
context.registerBean(JsonMapper.class,
() -> new ContextFunctionCatalogAutoConfiguration.GsonConfiguration()
.jsonMapper(context.getBean(Gson.class)));
}
else if (ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper", null)) {
context.registerBean(ObjectMapper.class, () -> new ObjectMapper());
context.registerBean(JsonMapper.class,
() -> new ContextFunctionCatalogAutoConfiguration.JacksonConfiguration()
.jsonMapper(context.getBean(ObjectMapper.class)));
}
context.registerBean(InMemoryFunctionCatalog.class,
() -> new InMemoryFunctionCatalog());
context.registerBean(FunctionRegistrationPostProcessor.class,
() -> new FunctionRegistrationPostProcessor(
context.getBean(FunctionRegistry.class)));
}
private void performPreinitialization() {
try {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
runSafely(() -> new DefaultFormattingConversionService());
}
public void runSafely(Runnable runnable) {
try {
runnable.run();
}
catch (Throwable ex) {
// Ignore
}
}
}, "background-preinit");
thread.start();
}
catch (Exception ex) {
}
}
private class FunctionRegistrationPostProcessor implements BeanPostProcessor {
private final FunctionRegistry catalog;
public FunctionRegistrationPostProcessor(FunctionRegistry catalog) {
this.catalog = catalog;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof FunctionRegistration) {
FunctionRegistration<?> registration = (FunctionRegistration<?>) bean;
if (registration.getNames().isEmpty()) {
registration = registration.name(beanName);
}
if (registration.getType() == null) {
throw new IllegalStateException(
"You need an explicit type for the function: " + beanName);
// TODO: in principle Spring could know how to extract this from the
// supplier, but in practice there is no functional bean registration
// with parametric types.
}
catalog.register(registration);
}
return bean;
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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.context.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.context.catalog.InMemoryFunctionCatalog;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.ClassUtils;
/**
* @author Dave Syer
*
*/
public class ContextFunctionCatalogInitializer
implements ApplicationContextInitializer<GenericApplicationContext> {
@Override
public void initialize(GenericApplicationContext applicationContext) {
if (applicationContext.getEnvironment().getProperty("spring.functional.enabled",
Boolean.class, false)) {
ContextFunctionCatalogBeanRegistrar registrar = new ContextFunctionCatalogBeanRegistrar(
applicationContext);
applicationContext.addBeanFactoryPostProcessor(registrar);
}
}
static class ContextFunctionCatalogBeanRegistrar
implements BeanDefinitionRegistryPostProcessor {
private GenericApplicationContext context;
public ContextFunctionCatalogBeanRegistrar(
GenericApplicationContext applicationContext) {
this.context = applicationContext;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
throws BeansException {
try {
register(registry, this.context.getDefaultListableBeanFactory());
}
catch (BeansException e) {
throw e;
}
catch (RuntimeException e) {
throw e;
}
catch (Exception e) {
throw new BeanCreationException("Cannot register from " + getClass(), e);
}
}
protected void register(BeanDefinitionRegistry registry,
ConfigurableListableBeanFactory factory) throws Exception {
performPreinitialization();
context.registerBean(PropertySourcesPlaceholderConfigurer.class,
() -> PropertyPlaceholderAutoConfiguration
.propertySourcesPlaceholderConfigurer());
context.registerBean(
AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME,
AutowiredAnnotationBeanPostProcessor.class);
context.registerBean(ConfigurationBeanFactoryMetadata.BEAN_NAME,
ConfigurationBeanFactoryMetadata.class,
() -> new ConfigurationBeanFactoryMetadata());
context.registerBean(ConfigurationPropertiesBindingPostProcessor.BEAN_NAME,
ConfigurationPropertiesBindingPostProcessor.class,
() -> new ConfigurationPropertiesBindingPostProcessor());
if (ClassUtils.isPresent("com.google.gson.Gson", null)
&& !"gson".equals(context.getEnvironment().getProperty(
ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY,
"gson"))) {
if (context.getBeanNamesForType(Gson.class, false, false).length == 0) {
context.registerBean(Gson.class, () -> new Gson());
}
context.registerBean(JsonMapper.class,
() -> new ContextFunctionCatalogAutoConfiguration.GsonConfiguration()
.jsonMapper(context.getBean(Gson.class)));
}
else if (ClassUtils.isPresent(
"com.fasterxml.jackson.databind.ObjectMapper.ObjectMapper", null)) {
if (context.getBeanNamesForType(ObjectMapper.class, false,
false).length == 0) {
context.registerBean(ObjectMapper.class, () -> new ObjectMapper());
}
context.registerBean(JsonMapper.class,
() -> new ContextFunctionCatalogAutoConfiguration.JacksonConfiguration()
.jsonMapper(context.getBean(ObjectMapper.class)));
}
context.registerBean(InMemoryFunctionCatalog.class,
() -> new InMemoryFunctionCatalog());
context.registerBean(FunctionRegistrationPostProcessor.class,
() -> new FunctionRegistrationPostProcessor(
context.getBean(FunctionRegistry.class)));
}
private void performPreinitialization() {
try {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
runSafely(() -> new DefaultFormattingConversionService());
}
public void runSafely(Runnable runnable) {
try {
runnable.run();
}
catch (Throwable ex) {
// Ignore
}
}
}, "background-preinit");
thread.start();
}
catch (Exception ex) {
}
}
private class FunctionRegistrationPostProcessor implements BeanPostProcessor {
private final FunctionRegistry catalog;
public FunctionRegistrationPostProcessor(FunctionRegistry catalog) {
this.catalog = catalog;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof FunctionRegistration) {
FunctionRegistration<?> registration = (FunctionRegistration<?>) bean;
if (registration.getNames().isEmpty()) {
registration = registration.name(beanName);
}
if (registration.getType() == null) {
throw new IllegalStateException(
"You need an explicit type for the function: "
+ beanName);
// TODO: in principle Spring could know how to extract this from
// the supplier, but in practice there is no functional bean
// registration with parametric types.
}
catalog.register(registration);
}
return bean;
}
}
}
}

View File

@@ -5,4 +5,4 @@ org.springframework.cloud.function.context.WrapperDetector=\
org.springframework.cloud.function.context.config.FluxWrapperDetector
org.springframework.context.ApplicationContextInitializer=\
org.springframework.cloud.function.context.config.ContextFunctionCatalogBeanRegistrar
org.springframework.cloud.function.context.config.ContextFunctionCatalogInitializer

View File

@@ -23,6 +23,8 @@ import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import com.google.gson.Gson;
import org.junit.After;
import org.junit.Test;
@@ -45,7 +47,7 @@ import reactor.core.publisher.Mono;
* @author Dave Syert
*
*/
public class ContextFunctionCatalogBeanRegistrarTests {
public class ContextFunctionCatalogInitializerTests {
private GenericApplicationContext context;
private FunctionCatalog catalog;
@@ -67,7 +69,7 @@ public class ContextFunctionCatalogBeanRegistrarTests {
// TODO: support for function composition
}
@Test(expected=BeanCreationException.class)
@Test(expected = BeanCreationException.class)
public void missingType() {
create(MissingTypeConfiguration.class);
assertThat(context.getBean("function")).isInstanceOf(FunctionRegistration.class);
@@ -110,8 +112,8 @@ public class ContextFunctionCatalogBeanRegistrarTests {
assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO");
assertThat(bean).isNotSameAs(function);
assertThat(inspector.getRegistration(function)).isNotNull();
assertThat(inspector.getRegistration(function).getType())
.isEqualTo(FunctionType.from(String.class).to(String.class).wrap(Flux.class));
assertThat(inspector.getRegistration(function).getType()).isEqualTo(
FunctionType.from(String.class).to(String.class).wrap(Flux.class));
}
@Test
@@ -132,6 +134,14 @@ public class ContextFunctionCatalogBeanRegistrarTests {
assertThat(context.getBean(SimpleConfiguration.class).list).hasSize(2);
}
@Test
public void overrideGson() {
create(GsonConfiguration.class);
Gson user = context.getBean(GsonConfiguration.class).gson();
Gson bean = context.getBean(Gson.class);
assertThat(user).isSameAs(bean);
}
@SuppressWarnings("unchecked")
private void create(
Class<? extends ApplicationContextInitializer<GenericApplicationContext>> type,
@@ -146,7 +156,8 @@ public class ContextFunctionCatalogBeanRegistrarTests {
for (ApplicationContextInitializer<GenericApplicationContext> type : types) {
type.initialize(context);
}
new ContextFunctionCatalogBeanRegistrar().register(context);
new ContextFunctionCatalogInitializer.ContextFunctionCatalogBeanRegistrar(context)
.postProcessBeanDefinitionRegistry(context);
context.refresh();
catalog = context.getBean(FunctionCatalog.class);
inspector = context.getBean(FunctionInspector.class);
@@ -212,6 +223,24 @@ public class ContextFunctionCatalogBeanRegistrarTests {
}
}
protected static class GsonConfiguration
implements ApplicationContextInitializer<GenericApplicationContext> {
private Gson gson = new Gson();
@Override
public void initialize(GenericApplicationContext context) {
context.registerBean("gson", Gson.class, this::gson);
context.registerBean(GsonConfiguration.class, () -> this);
}
@Bean
public Gson gson() {
return this.gson;
}
}
protected static class DependencyInjectionConfiguration
implements ApplicationContextInitializer<GenericApplicationContext> {