Registro dinámico de beans en el contexto de Spring.

0
12424

0. Índice de contenidos.


1. Introducción.

En este tutorial veremos cómo registrar de forma dinámica beans dentro del contexto de Spring, en tiempo de inicialización.

Lo normal es que tengamos el control sobre las instancias de los beans a registrar dentro del contexto de inyección de dependencias de Spring, pero podemos encontrarnos con situaciones en las que el número de beans del mismo tipo sea dinámico y venga dado por la propia configuración de entorno.

Con el soporte de Spring Boot podemos ver ejemplos de dicho registro dinámico, como en el soporte que proporciona de fábrica para definir múltiples datasources.

Pero, ¿cómo lo hace internamente?, ¿cómo podríamos nosotros crear beans, de forma dinámica, en función de nuestras necesidades?, en este tutorial veremos cómo hacerlo.


2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Oracle Java: 1.8.0_25
  • Spring Boot 1.5.4.RELEASE
  • Spring 4.3.9


3. Registro dinámico basado en el escaneo de clases.

En este primer ejemplo vamos a ver cómo podemos registrar un número indeterminado de beans basándonos en el escaneo de clases y, en concreto,
en la búsqueda por reflexión de clases anotadas con una anotación de clientes de servicios web de jax-ws.

Imaginad que tenemos una librería de clientes de servicios web generada o autogenerada en base a una definición de contrato de servicios web
y queremos registrar un proxy de apache-cxf en el contexto de Spring para hacer uso de los mismos de forma transparente.

Podríamos tener un post procesador con un código como el siguiente:

package com.sanchezsuarezjm.tutos.spring;

import java.beans.Introspector;
import java.util.List;

import javax.jws.WebService;

import org.apache.cxf.feature.LoggingFeature;
import org.apache.cxf.interceptor.Interceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.message.Message;
import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class WebServiceContextInitializer implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware  {

    private static final String BEAN_NAME_PREFIX = "jaxws:";

    private static final String BEAN_NAME_SUFFIX = "Client";

    private List> interceptors;

    private Interceptor faultInterceptor;

	private ApplicationContext applicationContext;

    public WebServiceContextInitializer(List> interceptors,
            Interceptor faultInterceptor) {
        this.interceptors = interceptors;
        this.faultInterceptor = faultInterceptor;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

		String packageName = applicationContext.getEnvironment().getProperty("jaxws.clients.basePackage");

        if (packageName == null) {
            packageName = "";
		}

        log.info("Scanning {} ... ", packageName);

        final Reflections ref = new Reflections(new ConfigurationBuilder()
            .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner(),
                new ResourcesScanner())
            .setUrls(ClasspathHelper.forPackage(packageName))
            .filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix(packageName))));
        	ref.getTypesAnnotatedWith(WebService.class)
            .forEach(clazz -> {

                final String beanName = Introspector.decapitalize(clazz.getSimpleName()).concat(BEAN_NAME_SUFFIX);
                log.info("Defining ws-client bean: {} of {}", beanName, clazz.getCanonicalName());
        		final JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean();
        		jaxWsProxyFactoryBean.setServiceClass(clazz);
        		final String endPointProperty = "jaxws.clients."+ beanName +".endPoint";
        		final String address = applicationContext.getEnvironment().getProperty(endPointProperty);

                if (address == null) {
                    throw new IllegalStateException(
        					"EndPoint property definition not found. Please define a value for ".concat(endPointProperty));
        		}

        		jaxWsProxyFactoryBean.setAddress(address);
                if (interceptors != null) {
                    jaxWsProxyFactoryBean.getOutInterceptors().addAll(interceptors);
                }

                if (faultInterceptor != null){
                    jaxWsProxyFactoryBean.getInFaultInterceptors().add(faultInterceptor);
                }

                jaxWsProxyFactoryBean.getFeatures().add(new LoggingFeature());

                final Object runtimeService = jaxWsProxyFactoryBean.create();

                beanFactory.registerSingleton(BEAN_NAME_PREFIX.concat(beanName), runtimeService);

            });
    }

	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		// do nothing through the registry
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

}

El procesador espera tener definidas únicamente un par de propiedades:

  • jaxws.clients.basePackage: con el paquete base de escaneo, sino lo escanea todo,
  • jaxws.clients.<webServiceClientName>.endPoint: con la url del endPoint en el entorno correspondiente.

y estas propiedades pueden estar definidas en cualquier fichero dentro de la jerarquía de lectura de propiedades de Spring y/o Spring Boot.

Se ejecuta en el arranque del contexto de Spring justo después de la configuración convencional, pero sin haber instanciado aun ningún bean,
es por ello que para la lectura de propiedades debemos acceder a las mismas programáticamente.

Solo nos quedaría configurar el procesador como cualquier otro bean en una clase de @Configuration, pero con un modificador static.

@Configuration
@ConditionalOnProperty(name = "jaxws.clients.enabled", matchIfMissing = true)
public class WebServiceClientConfig {

    @Bean
    public static WebServiceContextInitializer webServiceContextInitializer(List> interceptors) {
        return new WebServiceContextInitializer(interceptors, new SoapFaultInterceptor());
    }
    
}


4. Registro dinámico basado en la lectura de propiedades.

Podemos usar otra técnica que nos permitirá el registro múltiple de beans de forma dinámica basado en la lectura de propiedades.

Imaginad que ahora queremos declarar e instanciar tantos beans de un mismo tipo como propiedades tengamos definidas con un prefijo concreto. Podrían ser clientes de mensajería y necesitaríamos crear tantas plantillas de jms como número de ocurrencias en nuestras properties.

Lo primero que podemos hacer es definir una interfaz para la definición dinámica de múltiples beans, como la siguiente:

package com.sanchezsuarezjm.tutos.spring;

import java.util.Collection;

public interface MultiBeanFactory {
	
	String getBeanName(String name);
		
	T getObject(String name) throws Exception;

	Class getObjectType();

	Collection getNames();

}

A continuación podemos registrar un postprocesador que recuperando instancias de la factoría de beans ya registrados del tipo de interfaz anterior,
registre de forma dinámica instancias de los mismos.

package com.sanchezsuarezjm.tutos.spring;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;

import java.util.Map;

@Slf4j
public class MultiBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

	@SuppressWarnings({ "rawtypes"})
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
		Map factories = beanFactory.getBeansOfType(MultiBeanFactory.class);
		registerBeans(registry, factories);
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private void registerBeans(BeanDefinitionRegistry registry, Map factories) {
        factories.forEach((factoryName, factory) -> {
            factory.getNames().forEach(bean -> {
                final String beanName = factory.getBeanName( (String) bean);
				BeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition(factory.getObjectType())
						.setScope(BeanDefinition.SCOPE_SINGLETON).setFactoryMethod("getObject")
						.addConstructorArgValue(bean).getBeanDefinition();
				definition.setFactoryBeanName(factoryName);
				log.info("Registering {} of {}", beanName, definition);
				registry.registerBeanDefinition(beanName, definition);

            });
		});
	}

}

Quedaría declarar el procesador como sigue:

@Configuration
@ConditionalOnProperty(name = "jaxws.clients.enabled", matchIfMissing = true)
public class MultiBeanFactoryConfig {

    @Bean
    public static MultiBeanFactoryPostProcessor multiBeanFactoryPostProcessor() {
        return new MultiBeanFactoryPostProcessor();
    }
    
}

Y, a partir de ese momento, podemos definir en cualquier @Configuration beans de tipo MultiBeanFactory, para declarar el posterior registro
de forma dinámica de beans de un tipo concreto.


import java.util.Arrays;
import java.util.Collection;

import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Session;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.bind.PropertySourcesPropertyValues;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.AbstractEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.connection.CachingConnectionFactory;
import org.springframework.jms.connection.JmsTransactionManager;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.util.Assert;

import lombok.extern.slf4j.Slf4j;

@Configuration
@ConditionalOnProperty(name = JmsAutoConfiguration.JMS_CONFIG_PREFIX + ".enabled", havingValue="true", matchIfMissing = false)
@EnableJms
@Slf4j
public class JmsAutoConfiguration implements EnvironmentAware{

	protected static final String JMS_CONFIG_PREFIX = "jms";
	
	private Environment environment;
	
	@Bean
	public JmsProperties jmsProperties(){
		final JmsProperties jmsProperties = new JmsProperties();
		new RelaxedDataBinder(jmsProperties, JMS_CONFIG_PREFIX).bind(new PropertySourcesPropertyValues(((AbstractEnvironment) getEnvironment()).getPropertySources()));
		if (jmsProperties.getChannel() == null){
			throw new IllegalStateException("No " + JMS_CONFIG_PREFIX + ".channel.* found in properties");
		}
		log.trace("{} channels found in properties." , jmsProperties.getChannel().size());
		return jmsProperties;
	}
	
	@Primary
	@Bean
	public MultiBeanFactory connectionManagerFactory(
			jmsProperties jmsProperties, BeanFactory beanFactory) {
		return new MultiBeanFactory() {

            @Override
            public ConnectionFactory getObject(String name) throws Exception {
 				// generación de la factoría de conexiones para jms, con el nombre del canal y las propiedades del mismo
	           	return buildConnectionFactory(name, jmsProperties);
			}

            @Override
            public Class getObjectType() {
				return ConnectionFactory.class;
			}

			@Override
			public String getBeanName(String name) {
				return (getNames().size() == 1) ? "connectionFactory" : name.concat(ConnectionFactory.class.getSimpleName());
			}

            @Override
            public Collection getNames() {
				return Arrays.asList(jmsProperties.getChannel().keySet().toArray(new String[0]));
			}
		};
	}
	
	@Primary
	@Bean
	public MultiBeanFactory jmsTemplateFactory  (
			JmsProperties jmsProperties, BeanFactory beanFactory) {
		return new MultiBeanFactory() {

            @Override
            public JmsTemplate getObject(String name) throws Exception {
                final JmsTemplate jmsTemplate = new JmsTemplate(beanFactory.getBean((getNames().size() == 1) ? "connectionFactory" : name.concat(ConnectionFactory.class.getSimpleName()), ConnectionFactory.class));
        		final Integer receiveTimeout = jmsProperties.getChannel().get(name) != null && jmsProperties.getChannel().get(name).getReceiveTimeout() != null ? jmsProperties.getChannel().get(name).getReceiveTimeout() : jmsProperties.getReceiveTimeout();
        		if (receiveTimeout != null) {
        			jmsTemplate.setReceiveTimeout(receiveTimeout);
        		}
        		// configuración de la plantilla conforme al valor de las propiedades leídas
        		
                return jmsTemplate;
			}

            @Override
            public Class getObjectType() {
				return JmsTemplate.class;
			}

			@Override
			public String getBeanName(String name) {
				return (getNames().size() == 1) ? "jmsTemplate" : name.concat(JmsTemplate.class.getSimpleName());
			}

            @Override
            public Collection getNames() {
            	return Arrays.asList(jmsProperties.getChannel().keySet().toArray(new String[0]));
			}
		};
	}
}

Lo más interesante de todo es la lectura de propiedades para, en función del array encontrado se declaren tantos beans
como sea necesario. Para ello nos apoyamos en una clase de propiedades que se enriquece gracias al soporte de la clase RelaxedDataBinder
de Spring Boot.

@Setter
@Getter
public class JmsProperties {
	
	private String hostName;
	
	private Integer port;
	
	private String channelName;
	
	private Map channel;

}

Cualquier fichero de propiedades podría tener el siguiente contenido y se generarían tantas plantillas como «channel» se definan:

jms.enabled=true
jms.channel.testChannel1.channelName=TEST1
jms.channel.testChannel1.hostName=localhost
jms.channel.testChannel1.port=9999

jms.channel.testChannel2.channelName=TEST2
jms.channel.testChannel2.hostName=localhost
jms.channel.testChannel2.port=9998


5. Referencias.


6. Conclusiones.

Hace más de 10 años que escribíamos el primer tutorial sobre registro dinámico de beans en el contexto de Spring, el ecosistema de Spring se ha ampliado
mucho, pero la esencia sigue siendo la misma.

Un saludo.

Jose

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad