Agregando funcionalidad a interfaces con springboot

Published on
🕒 14 mins
👁️ -- visitas

¿Haz usado Feign para crear un cliente http?

Te habras dado cuenta que solo necesitas crear una interfaz y decorarla con la anotacion @FeingClient, con esto Feign hace la implementación de manera magica sin necesidad de nosotros escribir código o implementar dicha esa interfaz.

¿Como puedo hacer algo similar?

En este post explicare como crear una anotación personalizada con la que poder dar funcionalidad a interfaces sin necesidad de implementar dicha interfaz, esto lo shraremos con Proxies utilizando cglib (que ya viene incluida en springboot)


¿Que es un Proxy?

Proxy es un patrón de diseño estructural que proporciona un objeto que actúa como sustituto de un objeto de servicio real utilizado por un cliente. Un proxy recibe solicitudes del cliente, realiza parte del trabajo (control de acceso, almacenamiento en caché, etc.) y después pasa la solicitud a un objeto de servicio

Proxy en Java / Patrones de diseño. (2014). Refactoring.guru 🔗.

¿Que es Cglib?

Cglib es una libreria que permite generar y transformar bytecode de Java en tiempo de ejecución, gracias a esta libreria podemos crear la implementación de las interfaces en tiempo de ejecución y agregar nuestra funcionalidad requerida.


Guia

Bueno, luego de explicar a grandes razgos eso vamos a explicar como podemos realizar nuestra propia anotacion y implementación.

Creando un proyecto nuevo

Cree un proyecto nuevo solo agregando la dependencia base de springboot

1
dependencies {
2
implementation 'org.springframework.boot:spring-boot-starter'
3
}

Creando la anotación para decorar las interfaces

Ahora procedemos a crear nuestra anotación que marcara las interfaces que debemos capturar y agregar el proxy.

1
package dev.fneira.interfaceprocessor.resourceprovider;
2
3
import java.lang.annotation.ElementType;
4
import java.lang.annotation.Retention;
5
import java.lang.annotation.RetentionPolicy;
6
import java.lang.annotation.Target;
7
8
@Target(ElementType.TYPE)
9
@Retention(RetentionPolicy.RUNTIME)
10
public @interface ResourceProvider {
11
}

la anotación será @ResourceProvider se usará para marcar las interfaces que deben ser capturadas

@Target(ElementType.TYPE): esto define el tipo de target al que apunta la anotación, aquí definimos si queremos que se use a nivel de clase (type), metodo, campo, etc.

@Retention(RetentionPolicy.RUNTIME): esto define la politica de retencion de la anotación, hay 3 valores: RetentionPolicy.SOURCE, RetentionPolicy.CLASS, RetentionPolicy.RUNTIME, nosotros usaremos RUNTIME ya que será usada en tiempo de ejecución

Creando una interfaz con la anotación que creamos

vamos a crearemos una interfaz que este decorada con la anotación @ResourceProvider

IMyResourceProvider.java
1
package dev.fneira.interfaceprocessor.interfaces;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
5
@ResourceProvider
6
public interface IMyResourceProvider {
7
String getResource();
8
}

Como se puede ver es una interfaz simple con un método que retorna un string y se decora con nuestra anotación.

Realizando prueba

Intentemos inyectar esta interfaz en el main con un @Autowired:

InterfaceProcessorApplication.java
1
package dev.fneira.interfaceprocessor;
2
3
import dev.fneira.interfaceprocessor.interfaces.IMyResourceProvider;
4
import org.springframework.beans.factory.annotation.Autowired;
5
import org.springframework.boot.CommandLineRunner;
6
import org.springframework.boot.SpringApplication;
7
import org.springframework.boot.autoconfigure.SpringBootApplication;
8
9
@SpringBootApplication
10
public class InterfaceProcessorApplication implements CommandLineRunner {
11
12
private final IMyResourceProvider myResourceProvider;
13
14
@Autowired
15
public InterfaceProcessorApplication(final IMyResourceProvider myResourceProvider) {
16
this.myResourceProvider = myResourceProvider;
17
}
18
19
public static void main(final String[] args) {
20
SpringApplication.run(InterfaceProcessorApplication.class, args);
21
}
22
23
@Override
24
public void run(final String... args) throws Exception {
25
System.out.println(myResourceProvider.getResource());
26
}
27
}

en este caso implemente la clase CommandLineRunner para poder ejecutar de manera directa el metodo getResource() de mi interfaz.

si lo ejecutamos obtendremos un error de spring ya que no puede encontrar un bean para la interfaz IMyResourceProvider:

Terminal window
***************************
APPLICATION FAILED TO START
***************************
Description:
Field myResourceProvider in dev.fneira.interfaceprocessor.InterfaceProcessorApplication required a bean of type 'dev.fneira.interfaceprocessor.interfaces.IMyResourceProvider' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)

Agregando el bean IMyResourceProvider al contenedor de spring

creamos la clase que se encarga de agregar el bean de la interfaz al contenedor de spring y inyectarle funcionalidad con cglib

BeanDefinitionRegistryPostProcessor.java
1
package dev.fneira.interfaceprocessor.imp;
2
3
import dev.fneira.interfaceprocessor.interfaces.IMyResourceProvider;
4
import org.springframework.beans.factory.config.BeanDefinition;
5
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
6
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
7
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
8
import org.springframework.cglib.proxy.Enhancer;
9
import org.springframework.cglib.proxy.MethodInterceptor;
10
import org.springframework.context.annotation.Configuration;
11
12
@Configuration
13
public class ResourceProviderBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
14
15
@Override
16
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) {
17
final BeanDefinitionBuilder builder =
18
BeanDefinitionBuilder.genericBeanDefinition(
19
IMyResourceProvider.class, () -> createProxy(IMyResourceProvider.class));
20
final BeanDefinition beanDefinition = builder.getRawBeanDefinition();
21
registry.registerBeanDefinition("myResourceProviderProxy", beanDefinition);
22
}
23
24
private <T> T createProxy(final Class<T> targetClass) {
25
final Enhancer enhancer = new Enhancer();
26
enhancer.setSuperclass(targetClass);
27
enhancer.setCallback(
28
(MethodInterceptor)
29
(obj, method, args, proxy) ->
30
targetClass.getSimpleName() + "." + method.getName() + " -> Hello from CGLIB!");
31
32
return (T) enhancer.create();
33
}
34
}

Creamos la clase que implementa BeanDefinitionRegistryPostProcessor, aqui sobreescribir el método postProcessBeanDefinitionRegistry: es el metodo que permite modificar el registro de definición de bean interno del contexto de la aplicación después de su inicialización estándar. Se habrán cargado todas las definiciones de beans normales, pero todavía no se habrá creado una instancia de ningún bean. Esto permite agregar más definiciones de beans antes de que comience la siguiente fase de posprocesamiento.

también tenemos el método createProxy que es donde creamos el proxy para nuestra interfaz y le damos funcionalidad, en este caso retorna un String con el nombre de la clase y el nombre del metodo.

Realizando prueba

ahora si lo ejecutamos vemos que tenemos el valor que le declaramos en nuestro proxy

Terminal window
> Task :InterfaceProcessorApplication.main()
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.2)
2024-02-05T21:06:46.750-03:00 INFO 26335 --- [ main] d.f.i.InterfaceProcessorApplication : Starting InterfaceProcessorApplication using Java 17.0.3 with PID 26335 (/Users/fernando/Downloads/bsh/interfaceprocessor/build/classes/java/main started by fernando in /Users/fernando/Downloads/bsh)
2024-02-05T21:06:46.752-03:00 INFO 26335 --- [ main] d.f.i.InterfaceProcessorApplication : No active profile set, falling back to 1 default profile: "default"
2024-02-05T21:06:46.938-03:00 WARN 26335 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance @Configuration bean definition 'resourceProviderBeanDefinitionRegistryPostProcessor' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static' and/or marking the containing configuration class as 'proxyBeanMethods=false'.
2024-02-05T21:06:47.068-03:00 INFO 26335 --- [ main] d.f.i.InterfaceProcessorApplication : Started InterfaceProcessorApplication in 0.524 seconds (process running for 0.676)
IMyResourceProvider.getResource -> Hello from CGLIB!

ya estamos generando un bean para nuestra interfaz y agregando funcionalidad mediante un proxy con Cglib.

Creando mas interfaces

¿Pero que pasa si creamos otra interfaz?

IMyResource2Provider.java
1
package dev.fneira.interfaceprocessor.interfaces;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
5
@ResourceProvider
6
public interface IMyResource2Provider {
7
8
String getResource();
9
}

la agregamos a nuestra clase main y ejecutamos:

Terminal window
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 1 of constructor in dev.fneira.interfaceprocessor.InterfaceProcessorApplication required a bean of type 'dev.fneira.interfaceprocessor.interfaces.IMyResource2Provider' that could not be found.
Action:
Consider defining a bean of type 'dev.fneira.interfaceprocessor.interfaces.IMyResource2Provider' in your configuration.

vemos que falla, porque ahora no encuentra nuesto bean para IMyResource2Provider, ya que de la forma que lo definimos no queda dinamico detectando las interfaces decoradas con @ResourceProvider y autogenerando los beans con su implementación.

Agregando beans de manera dinamica

para solucionar esto de forma dinamica y automatica, debemos modificar la clase ResourceProviderBeanDefinitionRegistryPostProcessor para que nos permita escanear el proyecto y buscar las interfaces decoradas con la anotacion @ResourceProvider:

BeanDefinitionRegistryPostProcessor.java
1
package dev.fneira.interfaceprocessor.imp;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
import java.util.Set;
5
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
6
import org.springframework.beans.factory.config.BeanDefinition;
7
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
8
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
9
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
10
import org.springframework.cglib.proxy.Enhancer;
11
import org.springframework.cglib.proxy.MethodInterceptor;
12
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
13
import org.springframework.context.annotation.Configuration;
14
import org.springframework.core.type.filter.AnnotationTypeFilter;
15
16
@Configuration
17
public class ResourceProviderBeanDefinitionRegistryPostProcessor
18
implements BeanDefinitionRegistryPostProcessor {
19
20
@Override
21
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) {
22
final String basePackage = "dev.fneira.interfaceprocessor";
23
24
final Set<BeanDefinition> beanDefinitions = getBeanDefinition(basePackage);
25
26
for (final BeanDefinition beanDefinition : beanDefinitions) {
27
String beanClassName = beanDefinition.getBeanClassName();
28
try {
29
Class<?> targetClass = Class.forName(beanClassName);
30
registerBeanForInterface(registry, targetClass);
31
} catch (ClassNotFoundException e) {
32
throw new RuntimeException(e);
33
}
34
}
35
}
36
37
private Set<BeanDefinition> getBeanDefinition(final String basePackage) {
38
final ClassPathScanningCandidateComponentProvider provider =
39
new ClassPathScanningCandidateComponentProvider(false) {
40
@Override
41
protected boolean isCandidateComponent(final AnnotatedBeanDefinition beanDefinition) {
42
return super.isCandidateComponent(beanDefinition)
43
|| beanDefinition.getMetadata().isAbstract();
44
}
45
};
46
47
provider.addIncludeFilter(new AnnotationTypeFilter(ResourceProvider.class, true, true));
48
49
return provider.findCandidateComponents(basePackage);
50
}
51
52
private <T> void registerBeanForInterface(
53
final BeanDefinitionRegistry registry, final Class<T> targetClass) {
54
final BeanDefinitionBuilder builder =
55
BeanDefinitionBuilder.genericBeanDefinition(targetClass, () -> createProxy(targetClass));
56
final BeanDefinition beanDefinition = builder.getRawBeanDefinition();
57
registry.registerBeanDefinition(targetClass.getSimpleName() + "Proxy", beanDefinition);
58
}
59
60
private <T> T createProxy(final Class<T> targetClass) {
61
final Enhancer enhancer = new Enhancer();
62
enhancer.setSuperclass(targetClass);
63
enhancer.setCallback(
64
(MethodInterceptor)
65
(obj, method, args, proxy) ->
66
targetClass.getSimpleName() + "." + method.getName() + " -> Hello from CGLIB!");
67
68
return (T) enhancer.create();
69
}
70
}

Agregamos dos metodos

getBeanDefinition: es el encargado de escanear el classpath y buscar las interfaces decoradas con @ResourceProvider

registerBeanForInterface: se encarga de crear el registrar la definicion del bean creando el proxy para la interfaz que viene en la variable targetClass

ahora si ejecutamos obtendremos lo siguiente:

Terminal window
> Task :InterfaceProcessorApplication.main()
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.2)
2024-02-05T21:10:49.924-03:00 INFO 26611 --- [ main] d.f.i.InterfaceProcessorApplication : Starting InterfaceProcessorApplication using Java 17.0.3 with PID 26611 (/Users/fernando/Downloads/bsh/interfaceprocessor/build/classes/java/main started by fernando in /Users/fernando/Downloads/bsh)
2024-02-05T21:10:49.926-03:00 INFO 26611 --- [ main] d.f.i.InterfaceProcessorApplication : No active profile set, falling back to 1 default profile: "default"
2024-02-05T21:10:50.092-03:00 WARN 26611 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance @Configuration bean definition 'resourceProviderBeanDefinitionRegistryPostProcessor' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static' and/or marking the containing configuration class as 'proxyBeanMethods=false'.
2024-02-05T21:10:50.214-03:00 INFO 26611 --- [ main] d.f.i.InterfaceProcessorApplication : Started InterfaceProcessorApplication in 0.485 seconds (process running for 0.656)
IMyResourceProvider.getResource -> Hello from CGLIB!
IMyResource2Provider.getResource -> Hello from CGLIB!

como se puede apreciar se shran inyectar ambas interfaces y cada una devuelve el String que definimos en el metodo createProxy()

ahora podemos crear mas interfaces y todas se les agregara el proxy, pero que pasa si queremos generar mas de un metodo en las interfaces?, y tambien queremos agregarle valores dinamicos a cada metodo?

Creando anotación @ResourceValue

Ahora creamos una anotación que marcara los metodos y poder agregar funcionalidad de manera dinamica a cada uno.

ResourceValue.java
1
package dev.fneira.interfaceprocessor.resourceprovider;
2
3
import java.lang.annotation.ElementType;
4
import java.lang.annotation.Retention;
5
import java.lang.annotation.RetentionPolicy;
6
import java.lang.annotation.Target;
7
8
@Retention(RetentionPolicy.RUNTIME)
9
@Target(ElementType.METHOD)
10
public @interface ResourceValue {
11
String key();
12
13
String source();
14
}

en este caso el target sera: @Target(ElementType.METHOD) ya que queremos aplicar este decorador solo a los metodos, no a las clases ni interfaces.

la anotación tiene dos campos obligatorios: key y source que son necesarios para la implementación

Modificando interfaces

Agregamos a las interfaces el decorador @ResourceValue

IMyResourceProvider.java
1
package dev.fneira.interfaceprocessor.interfaces;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
import dev.fneira.interfaceprocessor.resourceprovider.ResourceValue;
5
6
@ResourceProvider
7
public interface IMyResourceProvider {
8
9
@ResourceValue(key = "my-key-1", source = "datagrid")
10
String getResource();
11
12
@ResourceValue(key = "my-other-resource-key", source = "redis")
13
String getOtherResource();
14
}
IMyResource2Provider.java
1
package dev.fneira.interfaceprocessor.interfaces;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
import dev.fneira.interfaceprocessor.resourceprovider.ResourceValue;
5
6
@ResourceProvider
7
public interface IMyResource2Provider {
8
9
@ResourceValue(key = "os.name", source = "environment")
10
String getResource();
11
}

en este caso tendre 3 fuentes: redis, datagrid y environment

Modificando metodo createProxy

ahora debemos modificar el metodo createProxy de la clase postProcessBeanDefinitionRegistry para mostrar el los valores de la anotacion @ResourceValue:

1
private <T> T createProxy(final Class<T> targetClass) {
2
final Enhancer enhancer = new Enhancer();
3
enhancer.setSuperclass(targetClass);
4
enhancer.setCallback(
5
(MethodInterceptor)
6
(obj, method, args, proxy) -> {
7
final ResourceValue resourceValue = method.getAnnotation(ResourceValue.class);
8
9
return targetClass.getSimpleName()
10
+ "."
11
+ method.getName()
12
+ " -> "
13
+ resourceValue.key()
14
+ " - "
15
+ resourceValue.source();
16
});
17
return (T) enhancer.create();
18
}

mediante el api de Reflection de java podemos obtener la anotación del método ejecutado mediante el proxy y asi obtener sus valores

si ejecutamos obtendremos lo siguiente:

Terminal window
> Task :InterfaceProcessorApplication.main()
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.2)
2024-02-05T21:18:31.302-03:00 INFO 27325 --- [ main] d.f.i.InterfaceProcessorApplication : Starting InterfaceProcessorApplication using Java 17.0.3 with PID 27325 (/Users/fernando/Downloads/bsh/interfaceprocessor/build/classes/java/main started by fernando in /Users/fernando/Downloads/bsh)
2024-02-05T21:18:31.304-03:00 INFO 27325 --- [ main] d.f.i.InterfaceProcessorApplication : No active profile set, falling back to 1 default profile: "default"
2024-02-05T21:18:31.472-03:00 WARN 27325 --- [ main] o.s.c.a.ConfigurationClassPostProcessor : Cannot enhance @Configuration bean definition 'resourceProviderBeanDefinitionRegistryPostProcessor' since its singleton instance has been created too early. The typical cause is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor return type: Consider declaring such methods as 'static' and/or marking the containing configuration class as 'proxyBeanMethods=false'.
2024-02-05T21:18:31.593-03:00 INFO 27325 --- [ main] d.f.i.InterfaceProcessorApplication : Started InterfaceProcessorApplication in 0.455 seconds (process running for 0.623)
IMyResourceProvider.getResource -> my-key-1 - redis
IMyResourceProvider.getOtherResource -> my-other-resource-key - redis
IMyResource2Provider.getResource -> my-key-2 - environment

ahora el inconveniente es que no podemos agregar otras dependencias ya que en este punto aun no se inicializado los beans de springboot, para eso vamos a crear otras clases:

InterfaceProxyFactoryBean.java
1
package dev.fneira.interfaceprocessor.resourceprovider;
2
3
import dev.fneira.interfaceprocessor.FakeDataProviderStub;
4
import java.lang.reflect.Method;
5
import org.springframework.beans.factory.annotation.Autowired;
6
import org.springframework.cglib.proxy.MethodInterceptor;
7
import org.springframework.cglib.proxy.MethodProxy;
8
import org.springframework.context.annotation.Configuration;
9
import org.springframework.core.env.Environment;
10
11
@Configuration
12
public class ResourceProviderHandler implements MethodInterceptor {
13
14
private final FakeDataProviderStub redisProvider;
15
private final FakeDataProviderStub datagridProvider;
16
private final Environment environment;
17
18
@Autowired
19
public ResourceProviderHandler(
20
FakeDataProviderStub redisProvider,
21
FakeDataProviderStub datagridProvider,
22
Environment environment) {
23
this.redisProvider = redisProvider;
24
this.datagridProvider = datagridProvider;
25
this.environment = environment;
26
}
27
28
@Override
29
public Object intercept(
30
final Object obj, final Method method, final Object[] args, MethodProxy proxy)
31
throws Throwable {
32
final ResourceValue resourceValue = method.getAnnotation(ResourceValue.class);
33
34
System.out.println(
35
obj.getClass().getSimpleName()
36
+ "."
37
+ method.getName()
38
+ " -> "
39
+ resourceValue.key()
40
+ " - "
41
+ resourceValue.source());
42
43
if (resourceValue.source().equals("redis")) {
44
return this.redisProvider.getResource(resourceValue.key());
45
} else if (resourceValue.source().equals("datagrid")) {
46
return this.datagridProvider.getResource(resourceValue.key());
47
} else if (resourceValue.source().equals("environment")) {
48
return this.environment.getProperty(resourceValue.key());
49
} else {
50
throw new IllegalArgumentException("Invalid source: " + resourceValue.source());
51
}
52
}
53
}

esta clase es la encargada de interceptar las ejecuciones de nuestro proxy esto lo hace implementando la clase MethodInterceptor y sobreescribiendo el metodo intercept, aqui ya podemos inyectar otras dependencias

ahora toca crear InterfaceProxyFactoryBean.java que es la encargara de la creación del proxy

InterfaceProxyFactoryBean.java
1
package dev.fneira.interfaceprocessor.imp;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProviderHandler;
4
import org.springframework.beans.BeansException;
5
import org.springframework.beans.factory.BeanFactory;
6
import org.springframework.beans.factory.BeanFactoryAware;
7
import org.springframework.beans.factory.FactoryBean;
8
import org.springframework.beans.factory.InitializingBean;
9
import org.springframework.cglib.proxy.Enhancer;
10
11
public class ResourceHandlerProxyFactoryBean
12
implements FactoryBean<Object>, InitializingBean, BeanFactoryAware {
13
private BeanFactory beanFactory;
14
private Class<?> type;
15
16
@Override
17
public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
18
this.beanFactory = beanFactory;
19
}
20
21
<T> T createProxy() {
22
return (T) Enhancer.create(type, beanFactory.getBean(ResourceProviderHandler.class));
23
}
24
25
@Override
26
public Object getObject() {
27
return createProxy();
28
}
29
30
@Override
31
public Class<?> getObjectType() {
32
return type;
33
}
34
35
@Override
36
public void afterPropertiesSet() {
37
if (type == null) {
38
throw new IllegalArgumentException("Property 'type' is required");
39
}
40
}
41
42
public void setType(final Class<?> type) {
43
this.type = type;
44
}
45
46
}

ahora modificamos la clase que implementa BeanDefinitionRegistryPostProcessor.java para indicar cual clase es nuestra factory:

BeanDefinitionRegistryPostProcessor.java
1
package dev.fneira.interfaceprocessor.imp;
2
3
import dev.fneira.interfaceprocessor.resourceprovider.ResourceProvider;
4
import java.util.Set;
5
6
import org.springframework.beans.factory.FactoryBean;
7
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
8
import org.springframework.beans.factory.config.BeanDefinition;
9
import org.springframework.beans.factory.config.BeanDefinitionHolder;
10
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
11
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
12
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
13
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
14
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
15
import org.springframework.context.annotation.Configuration;
16
import org.springframework.core.type.filter.AnnotationTypeFilter;
17
18
@Configuration
19
public class ResourceProviderBeanDefinitionRegistryPostProcessor
20
implements BeanDefinitionRegistryPostProcessor {
21
22
@Override
23
public void postProcessBeanDefinitionRegistry(final BeanDefinitionRegistry registry) {
24
final String basePackage = "dev.fneira.interfaceprocessor";
25
26
final Set<BeanDefinition> beanDefinitions = getBeanDefinition(basePackage);
27
28
for (final BeanDefinition beanDefinition : beanDefinitions) {
29
String beanClassName = beanDefinition.getBeanClassName();
30
try {
31
Class<?> targetClass = Class.forName(beanClassName);
32
registerBeanForInterface(registry, targetClass);
33
} catch (ClassNotFoundException e) {
34
throw new RuntimeException(e);
35
}
36
}
37
}
38
39
private Set<BeanDefinition> getBeanDefinition(final String basePackage) {
40
final ClassPathScanningCandidateComponentProvider provider =
41
new ClassPathScanningCandidateComponentProvider(false) {
42
@Override
43
protected boolean isCandidateComponent(final AnnotatedBeanDefinition beanDefinition) {
44
return super.isCandidateComponent(beanDefinition)
45
|| beanDefinition.getMetadata().isAbstract();
46
}
47
};
48
49
provider.addIncludeFilter(new AnnotationTypeFilter(ResourceProvider.class, true, true));
50
51
return provider.findCandidateComponents(basePackage);
52
}
53
54
private <T> void registerBeanForInterface(
55
final BeanDefinitionRegistry registry, final Class<T> targetClass) {
56
final BeanDefinitionBuilder definition =
57
BeanDefinitionBuilder.genericBeanDefinition(ResourceHandlerProxyFactoryBean.class)
58
.addPropertyValue("type", targetClass);
59
60
final BeanDefinition beanDefinition = definition.getRawBeanDefinition();
61
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, targetClass);
62
beanDefinition.setPrimary(true);
63
64
final BeanDefinitionHolder beanDefinitionHolder =
65
new BeanDefinitionHolder(beanDefinition, targetClass.getSimpleName());
66
67
BeanDefinitionReaderUtils.registerBeanDefinition(beanDefinitionHolder, registry);
68
}
69
}

ahora si ejecutamos nuestro programa:

Terminal window
> Task :InterfaceProcessorApplication.main()
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.2)
2024-02-05T21:33:27.933-03:00 INFO 28442 --- [ main] d.f.i.InterfaceProcessorApplication : Starting InterfaceProcessorApplication using Java 17.0.3 with PID 28442 (/Users/fernando/Downloads/bsh/interfaceprocessor/build/classes/java/main started by fernando in /Users/fernando/Downloads/bsh)
2024-02-05T21:33:27.934-03:00 INFO 28442 --- [ main] d.f.i.InterfaceProcessorApplication : No active profile set, falling back to 1 default profile: "default"
2024-02-05T21:33:28.210-03:00 INFO 28442 --- [ main] d.f.i.InterfaceProcessorApplication : Started InterfaceProcessorApplication in 0.458 seconds (process running for 0.646)
IMyResourceProvider$$EnhancerByCGLIB$$d852720d.getResource -> my-key-1 - redis
Resource: my-key-1
IMyResourceProvider$$EnhancerByCGLIB$$d852720d.getOtherResource -> my-other-resource-key - redis
Resource: my-other-resource-key
IMyResource2Provider$$EnhancerByCGLIB$$26f2ceaf.getResource -> os.name - environment
Mac OS X

y listo esta funcionando, ahora solo queda aplicar nuestra shica en ResourceProviderHandler para nuestras interfaces.

Resumen

  • Creamos la anotación @ResourceProvider para marcar las interfaces que queremos que se cree un proxy y se inyecte el bean al contexto de spring, de manera automatica,
  • Creamos la anotación @ResourceValue para proveer de valores a los metodos dentro de la interfaz de manera de crear una implementaciones dinamicas.
  • Creamos una clase de tipo BeanFactory que es la fabrica de nuestras interfaces y la encargada de crear el proxy

Si quieres hacer esto mas dinamico aún, como crear mas de una anotación y distintos handlers para cada una, puedes ver un ejemplo en mi Github: fneiraj/JavaSpringExamples 🔗