Home > Enterprise >  How to generate service implementation from multiple implementations at runtime?
How to generate service implementation from multiple implementations at runtime?

Time:10-05

Let's say I have following interfaces:

public interface FindExtension {
    void findById(long id);
    void findAll();
}

public interface SaveExtension {
    void save();
}

public interface DeleteExtension {
    void deleteById(long id);
}

And I have following implementations:

public class FindExtensionImpl implements FindExtension {

    @Override
    public void findById(long id) {
        System.out.println("FindExtensionImpl::findById("   id   ")");
    }

    @Override
    public void findAll() {
        System.out.println("FindExtensionImpl::findAll()");
    }
}

public class SaveExtensionImpl implements SaveExtension {

    @Override
    public void save() {
        System.out.println("SaveExtensionImpl::save()");
    }
}

public class DeleteExtensionImpl implements DeleteExtension {
    @Override
    public void deleteById(long id) {
        System.out.println("DeleteExtensionImpl::deleteById("   id   ")");
    }
}

And now I want to mix these implementations and let's say I want this service:

public interface MyService extends FindExtension, SaveExtension, DeleteExtension {
}

Now, in my Spring Boot Application I would like to inject MyService implementation into one of my controllers. Usually what I would do, I would create MyServiceImpl class, implement interface methods and annotate with @Service. That way Spring Boot will scan my code and create instance of this service and provide it to me anytime I need it.

However, I would like not to create MyServiceImpl, but rather that to be generated by the framework at runtime, in otherwords composed from little pieces. How do I tell Spring Boot to do that automatically? Or do I need to create my own Annotation and Annotation Processor that would somehow generate implementation?

This is something similar to Spring Boot repositories where I would have this interface:

@Repository
interface IPostRepository implements JpaRepository<Post,Long>, QuerydslPredicateExecutor<Post>, PostCustomRepositoryExtension { }

And Spring Boot would "magically" create implementation of this interface and inject methods from JpaRepository, QuerydslPredicateExecutor and my own PostCustomRepositoryExtension...

Since Spring Boot already does similar logic to that of mine, I am wondering can I reuse that and how?

CodePudding user response:

The easiest way is you put all interface into one interface :

public interface CommonService<T, Z> {
    void findById(long id);
    void findAll();
    void save();
    void deleteById(long id);
}

then make your service that extends CommonService

public interface ObjectService extends CommonService<Object, long> {

}

and finally you can make impl that implements ObjectService

public class ObjectImpl implements ObjectService {
@Override
    public void findById(long id) {
        System.out.println("ObjectImpl::findById("   id   ")");
    }

    @Override
    public void findAll() {
        System.out.println("ObjectImpl::findAll()");
    }

    @Override
    public void save() {
        System.out.println("ObjectImpl::save()");
    }

    @Override
    public void deleteById(long id) {
        System.out.println("ObjectImpl::deleteById("   id   ")");
    }

}

CodePudding user response:

Disclaimer

I'm not a Spring expert, especially not in messing with bean creation etc. so take the following approach with a grain of salt. Your question got me thinking though so I played around with Spring and could come up with the following basic approach.

Please note that this is just meant to get you started and is nowhere near production ready. More research would be needed and others might even come in and show this is a wrong approach (to those who like to downvote: please leave a comment on why this approach deserves it so everyone can learn).

Approach

Now here's the approach:

  • Create a custom annotation to mark your interfaces that you're interested in.
  • Register factory methods that create proxies for those interfaces. These proxies will delegate the calls to respective "fragment" implementations.
  • Enable everything with an annotation on your application class.

Code (only additional relevant portions)

The interface annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CompositeService {}

The enablement annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({CompositeServiceFactoryRegistrar.class})
public @interface EnableCompositeServices {}

The main factory registrar (with inline comments):

@Component
public class CompositeServiceFactoryRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {

    private Map<Class<?>, Supplier<Object>> fragmentFactories = new HashMap<>();

    private Environment environment;
   
    public CompositeServiceFactoryRegistrar() {
        //hard coded fragment registration, you'll probably want to do a lookup of all interfaces and their implementation on the classpath instead
        fragmentFactories.put(SaveExtension.class, () -> new SaveExtensionImpl());
        fragmentFactories.put(DeleteExtension.class, () -> new DeleteExtensionImpl());      
        fragmentFactories.put(FindExtension.class, () -> new FindExtensionImpl());
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        
        //get the enablement annotation and set up package scan
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableCompositeServices.class.getCanonicalName());

        if (annotationAttributes != null) {
            String[] basePackages = (String[]) annotationAttributes.get("value");

            if (basePackages == null || basePackages.length == 0) {
                // If value attribute is not set, fallback to the package of the annotated class
                basePackages = new String[] {
                        ((StandardAnnotationMetadata) metadata).getIntrospectedClass().getPackage().getName() };
            }

            // using these packages, scan for interface annotated with MyCustomBean
            ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(
                    false, environment) {
                // Override isCandidateComponent to only scan for interface
                @Override
                protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                    AnnotationMetadata metadata = beanDefinition.getMetadata();
                    return metadata.isIndependent() && metadata.isInterface();
                }
            };
            provider.addIncludeFilter(new AnnotationTypeFilter(CompositeService.class));

            // Scan all packages
            for (String basePackage : basePackages) {
                for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {                   

                    GenericBeanDefinition genericDef = new GenericBeanDefinition(beanDefinition);
                    
                    //resolve the interface class if not done yet
                    if( !genericDef.hasBeanClass()) {
                        try {
                            genericDef.resolveBeanClass(getClassLoader());
                        } catch(ClassNotFoundException e) {
                            //simple logging, replace that with something more appropriate
                            e.printStackTrace();
                        }
                    }
                    
                    Class<?> interfaceType = genericDef.getBeanClass();                                 

                    //add the factory to the bean definition and then register it
                    genericDef.setInstanceSupplier(() -> createProxy(interfaceType) );                  
                    registry.registerBeanDefinition(interfaceType.getSimpleName(), genericDef);
                }
            }
        }
    }

    /*
     * Main factory method
     */
    @SuppressWarnings("unchecked")
    private <T> T createProxy(Class<T> type) {
        //create the factory and set the interface type
        ProxyFactory factory = new ProxyFactory();
        factory.setInterfaces(type);

        //add the advice that actually delegates to the fragments
        factory.addAdvice(new MethodInterceptor() {
            @Override
            public Object invoke(MethodInvocation invocation) throws Throwable {
                Method invokedMethod = invocation.getMethod();

                Class<?> invokedClass = invokedMethod.getDeclaringClass();
                if (invokedClass.isInterface()) {

                    //create the fragment for this method, if not possible continue with the next interceptor
                    Supplier<Object> supplier = fragmentFactories.get(invokedClass);
                    if (supplier == null) {
                        return invocation.proceed();
                    }

                    Object fragment = supplier.get();

                    //get the fragment method and invoke it
                    Method targetMethod = fragment.getClass().getDeclaredMethod(invokedMethod.getName(),
                            invokedMethod.getParameterTypes());

                    return targetMethod.invoke(fragment, invocation.getArguments());
                } else {
                    return invocation.proceed();
                }
            }
        });

        return (T) factory.getProxy(getClassLoader());
    }
    
    private ClassLoader getClassLoader() {
        return getClass().getClassLoader();
    }
}

Note that this is a very simple implementation and has a couple of drawbacks, e.g.

  • it doesn't handle overloads and parameter conversion
  • it may have difficulties with overrides -> needs some more reflection magic
  • fragments are hard coded
  • (I suspect many more)
  • Related