[FIXED] Wie generiert man eine Service-Implementierung aus mehreren Implementierungen zur Laufzeit?

Ausgabe

Nehmen wir an, ich habe folgende Schnittstellen:

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

public interface SaveExtension {
    void save();
}

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

Und ich habe folgende Implementierungen:

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 + ")");
    }
}

Und jetzt möchte ich diese Implementierungen mischen und sagen wir, ich möchte diesen Dienst:

public interface MyService extends FindExtension, SaveExtension, DeleteExtension {
}

Jetzt möchte ich in meiner Spring Boot-Anwendung die MyServiceImplementierung in einen meiner Controller einfügen. Normalerweise würde ich eine MyServiceImplKlasse erstellen, Schnittstellenmethoden implementieren und mit kommentieren @Service. Auf diese Weise scannt Spring Boot meinen Code und erstellt eine Instanz dieses Dienstes und stellt sie mir jederzeit zur Verfügung, wenn ich sie benötige.

Allerdings möchte ich nicht erstellen MyServiceImpl, sondern das vom Framework zur Laufzeit generiert, also aus kleinen Stücken zusammensetzen. Wie sage ich Spring Boot, dass es das automatisch macht? Oder muss ich meine eigene Anmerkung und meinen eigenen Anmerkungsprozessor erstellen, die irgendwie eine Implementierung generieren würden?

Dies ist etwas Ähnliches wie Spring Boot-Repositories, wo ich diese Schnittstelle hätte:

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

Und Spring Boot würde “magisch” eine Implementierung dieser Schnittstelle erstellen und Methoden aus JpaRepository, QuerydslPredicateExecutor und meiner eigenen PostCustomRepositoryExtension injizieren …

Da Spring Boot bereits eine ähnliche Logik wie meine hat, frage ich mich, ob ich das wiederverwenden kann und wie?

Lösung

Haftungsausschluss

Ich bin kein Frühlingsexperte, besonders nicht, wenn es darum geht, Bohnen zu kreieren usw., also gehen Sie mit einem Körnchen Salz vor. Ihre Frage hat mich jedoch zum Nachdenken gebracht, also habe ich mit Spring herumgespielt und konnte auf den folgenden grundlegenden Ansatz kommen.

Bitte beachten Sie, dass dies nur als Einstieg dient und noch lange nicht produktionsbereit ist. Weitere Forschung wäre erforderlich, und andere könnten sogar hinzukommen und zeigen, dass dies ein falscher Ansatz ist.

Sich nähern

Hier nun der Ansatz:

  • Erstellen Sie eine benutzerdefinierte Anmerkung, um Ihre Schnittstellen zu markieren, an denen Sie interessiert sind.
  • Registrieren Sie Factory-Methoden, die Proxys für diese Schnittstellen erstellen. Diese Proxys delegieren die Aufrufe an die jeweiligen “Fragment”-Implementierungen.
  • Aktivieren Sie alles mit einer Anmerkung zu Ihrer Anwendungsklasse.

Code (nur zusätzliche relevante Teile)

Die Schnittstellenanmerkung:

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

Die Aktivierungsanmerkung:

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

Der Hauptregisterführer der Fabrik (mit Inline-Kommentaren):

@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();
    }
}

Beachten Sie, dass dies eine sehr einfache Implementierung ist und einige Nachteile hat, z

  • Es behandelt keine Überladungen und Parameterkonvertierungen
  • es kann Schwierigkeiten mit Überschreibungen haben -> braucht etwas mehr Reflexionsmagie
  • Fragmente sind fest codiert
  • (ich vermute viele mehr)


Beantwortet von –
Thomas


Antwort geprüft von –
Terry (FixError Volunteer)

0 Shares:
Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like