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 MyService
Implementierung in einen meiner Controller einfügen. Normalerweise würde ich eine MyServiceImpl
Klasse 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)