Aber was wäre, wenn Sie beispielsweise eine Schnittstelle wie diese erstellen könnten:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); }
Und dann injizieren Sie es einfach und rufen Sie seine Methoden auf:
@SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); } public static void main(String[] args) { SpringApplication.run(App.class, args); } }
Dies ist durchaus möglich (und nicht sehr schwierig) zu implementieren. Als nächstes werde ich zeigen, wie und warum es geht.
Vor kurzem hatte ich die Aufgabe, die Interaktion der Entwickler mit einem der verwendeten Frameworks zu vereinfachen. Es war notwendig, ihnen eine noch einfachere und bequemere Möglichkeit zu geben, mit ihm zu arbeiten, als die, die bereits implementiert worden war.
Eigenschaften, die ich mit einer solchen Lösung erreichen wollte:
- deklarative Beschreibung der gewünschten Aktion
- Mindestmenge an Code erforderlich
- Integration in das verwendete Abhängigkeitsinjektions-Framework (in unserem Fall Spring)
Dies ist in den Bibliotheken Spring Data Repository und Retrofit implementiert. In ihnen beschreibt der Benutzer die gewünschte Interaktion in Form einer Java-Oberfläche, ergänzt durch Anmerkungen. Der Benutzer muss die Implementierung nicht selbst schreiben - die Bibliothek generiert sie zur Laufzeit basierend auf den Signaturen von Methoden, Anmerkungen und Typen.
Als ich das Thema studierte, hatte ich viele Fragen, deren Antworten im Internet verstreut waren. In diesem Moment würde mich ein Artikel wie dieser nicht verletzen. Deshalb habe ich hier versucht, alle Informationen und meine Erfahrungen an einem Ort zu sammeln.
In diesem Beitrag werde ich am Beispiel eines Wrappers für einen http-Client zeigen, wie Sie diese Idee implementieren können. Ein Beispiel für ein Spielzeug, das nicht für den realen Gebrauch gedacht ist, sondern den Ansatz demonstriert. Der Quellcode des Projekts kann auf Bitbucket studiert werden .
Wie sieht es für den Benutzer aus?
Der Benutzer beschreibt den von ihm benötigten Dienst in Form einer Schnittstelle. So führen Sie beispielsweise http-Anforderungen bei Google aus:
@Service public interface GoogleSearchApi { @Uri("https://www.google.com") int mainPageStatus(); @Uri("https://www.google.com") HttpGet mainPageRequest(); @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); @Uri("https://www.google.com/doodles/?q={query}&hl={language}") int searchDoodleStatus(String query, String language); }
Was die Implementierung dieser Schnittstelle letztendlich bewirken wird, wird durch die Signatur bestimmt. Wenn der Rückgabetyp int ist, wird eine http-Anforderung ausgeführt und der Status als Ergebniscode zurückgegeben. Wenn der Rückgabetyp CloseableHttpResponse ist, wird die gesamte Antwort zurückgegeben und so weiter. Wenn die Anfrage gestellt wird, nehmen wir den Uri aus der Anmerkung und ersetzen die gleichen übertragenen Werte anstelle von Platzhaltern in ihrem Inhalt.
In diesem Beispiel habe ich mich darauf beschränkt, drei Rückgabetypen und eine Anmerkung zu unterstützen. Sie können auch Methodennamen und Parametertypen verwenden, um eine Implementierung auszuwählen, und alle Arten von Kombinationen davon verwenden, aber ich werde dieses Thema in diesem Beitrag nicht öffnen.
Wenn ein Benutzer diese Schnittstelle verwenden möchte, bettet er sie mit Spring in seinen Code ein:
@SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override @SneakyThrows public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); LOG.info("Main page request: " + api.mainPageRequest()); LOG.info("Doodle search status: " + api.searchDoodleStatus("tesla", "en")); try (CloseableHttpResponse response = api.searchSomething("qweqwe")) { LOG.info("Search result " + response); } } public static void main(String[] args) { SpringApplication.run(App.class, args); } }
Die Integration mit Spring war in meinem Arbeitsprojekt erforderlich, aber es ist natürlich nicht das einzig mögliche. Wenn Sie die Abhängigkeitsinjektion nicht verwenden, können Sie die Implementierung beispielsweise über die statische Factory-Methode abrufen. Aber in diesem Artikel werde ich den Frühling betrachten.
Dieser Ansatz ist sehr praktisch: Markieren Sie Ihre Schnittstelle einfach als Bestandteil von Spring (in diesem Fall Service-Annotation) und sie kann implementiert und verwendet werden.
Wie man den Frühling dazu bringt, diese Magie zu unterstützen
Eine typische Spring-Anwendung durchsucht den Klassenpfad beim Start und sucht nach allen Komponenten, die mit speziellen Anmerkungen gekennzeichnet sind. Für sie registriert es BeanDefinitions, Rezepte, mit denen diese Komponenten erstellt werden. Wenn Spring jedoch bei konkreten Klassen weiß, wie sie erstellt werden, welche Konstruktoren aufgerufen und was sie übergeben werden müssen, verfügt Spring für abstrakte Klassen und Schnittstellen nicht über solche Informationen. Daher wird für unsere GoogleSearchApi Spring keine BeanDefinition erstellt. Dabei wird er Hilfe von uns brauchen.
Um die Logik der Verarbeitung von BeanDefinitions abzuschließen, gibt es im Frühjahr eine BeanDefinitionRegistryPostProcessor-Schnittstelle. Damit können wir der BeanDefinitionRegistry jede gewünschte Definition von Beans hinzufügen.
Leider habe ich keine Möglichkeit gefunden, mich in die Spring-Logik des Klassenpfad-Scans zu integrieren, um sowohl normale Beans als auch unsere Schnittstellen in einem einzigen Durchgang zu verarbeiten. Daher habe ich den Nachkommen der ClassPathScanningCandidateComponentProvider-Klasse erstellt und verwendet, um alle mit der Service-Annotation gekennzeichneten Schnittstellen zu finden:
Vollständiger Paket-Scan-Code und Registrierung von BeanDefinitions:
DynamicProxyBeanDefinitionRegistryPostProcessor @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
Fertig! Zu Beginn der Anwendung führt Spring diesen Code aus und registriert alle erforderlichen Schnittstellen wie Beans.
Das Erstellen einer Implementierung der gefundenen Beans wird an eine separate Komponente von DynamicProxyBeanFactory delegiert:
@Component(DYNAMIC_PROXY_BEAN_FACTORY) public class DynamicProxyBeanFactory { public static final String DYNAMIC_PROXY_BEAN_FACTORY = "repositoryProxyBeanFactory"; private final DynamicProxyInvocationHandlerDispatcher proxy; public DynamicProxyBeanFactory(DynamicProxyInvocationHandlerDispatcher proxy) { this.proxy = proxy; } @SuppressWarnings("unused") public <T> T createDynamicProxyBean(Class<T> beanClass) {
Zum Erstellen der Implementierung wird der gute alte Dynamic Proxy-Mechanismus verwendet. Eine Implementierung wird im laufenden Betrieb mit der Proxy.newProxyInstance-Methode erstellt. Es wurden bereits viele Artikel über ihn geschrieben, daher werde ich hier nicht im Detail darauf eingehen.
Den richtigen Handler finden und Anrufe bearbeiten
Wie Sie sehen können, leitet DynamicProxyBeanFactory die Methodenverarbeitung an DynamicProxyInvocationHandlerDispatcher um. Da wir möglicherweise viele Implementierungen von Handlern haben (für jede Annotation, für jeden zurückgegebenen Typ usw.), ist es logisch, einen zentralen Ort für deren Speicherung und Suche einzurichten.
Um festzustellen, ob der Handler für die Verarbeitung der aufgerufenen Methode geeignet ist, habe ich die Standard-InvocationHandler-Schnittstelle um eine neue Methode erweitert
public interface HandlerMatcher { boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { }
Das Ergebnis ist die ProxyInvocationHandler-Schnittstelle, deren Implementierungen unsere Handler sein werden. Außerdem werden Handler-Implementierungen als Komponente markiert, damit Spring sie für uns in einer großen Liste in DynamicProxyInvocationHandlerDispatcher sammeln kann:
DynamicProxyInvocationHandlerDispatcher package com.bachkovsky.dynproxy.lib.proxy; import lombok.SneakyThrows; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) {
In der findHandler-Methode gehen wir alle Handler durch und geben den ersten zurück, der die übergebene Methode verarbeiten kann. Dieser Suchmechanismus ist möglicherweise nicht sehr effektiv, wenn viele Handler-Implementierungen vorhanden sind. Vielleicht müssen Sie dann über eine geeignetere Struktur zum Speichern nachdenken als über eine Liste.
Handler-Implementierung
Die Aufgaben der Handler umfassen das Lesen von Informationen über die aufgerufene Methode der Schnittstelle und das Verarbeiten des Aufrufs selbst.
Was soll der Handler in diesem Fall tun:
- Lesen Sie die Uri-Anmerkung und holen Sie sich den Inhalt
- Ersetzen Sie Uri-Platzhalter in der Zeichenfolge durch echte Werte
- Rückgabetyp der Lesemethode
- Wenn der Rückgabetyp geeignet ist, verarbeiten Sie die Methode und geben Sie das Ergebnis zurück.
Die ersten drei Punkte werden für alle zurückgegebenen Typen benötigt, daher habe ich den allgemeinen Code in eine abstrakte Oberklasse eingefügt
HttpInvocationHandler:
public abstract class HttpInvocationHandler implements ProxyInvocationHandler { final HttpClient client; private final UriHandler uriHandler; HttpInvocationHandler(HttpClient client, UriHandler uriHandler) { this.client = client; this.uriHandler = uriHandler; } @Override public boolean canHandle(Method method) { return uriHandler.canHandle(method); } final String getUri(Method method, Object[] args) { return uriHandler.getUriString(method, args); } }
Die UriHandler-Hilfsklasse implementiert die Arbeit mit der Uri-Annotation: Lesen von Werten, Ersetzen von Platzhaltern. Ich werde den Code hier nicht geben, weil es ist ziemlich nützlich.
Es ist jedoch zu beachten, dass Sie zum Kompilieren der Parameternamen aus der Signatur der Java-Methode beim Kompilieren die Option "-parameters" hinzufügen müssen .
HttpClient - ein Wrapper über Apachevsky CloseableHttpClient, ist ein Backend für diese Bibliothek.
Als Beispiel für einen bestimmten Handler werde ich einen Handler angeben, der einen Statusantwortcode zurückgibt:
@Component public class HttpCodeInvocationHandler extends HttpInvocationHandler { public HttpCodeInvocationHandler(HttpClient client, UriHandler uriHandler) { super(client, uriHandler); } @Override @SneakyThrows public Integer invoke(Object proxy, Method method, Object[] args) { try (CloseableHttpResponse resp = client.execute(new HttpGet(getUri(method, args)))) { return resp.getStatusLine().getStatusCode(); } } @Override public boolean canHandle(Method method) { return super.canHandle(method) && method.getReturnType().equals(int.class); } }
Andere Handler werden ähnlich hergestellt. Das Hinzufügen neuer Handler ist einfach und erfordert keine Änderung des vorhandenen Codes. Erstellen Sie einfach einen neuen Handler und markieren Sie ihn als Spring-Komponente.
Das ist alles. Der Code ist geschrieben und bereit zu gehen.
Fazit
Je mehr ich über ein solches Design nachdenke, desto mehr sehe ich Fehler darin. Schwächen, die ich sehe:
- Typ Sicherheit, was nicht ist. Stellen Sie die Anmerkung falsch ein - bevor Sie sich mit RuntimeException treffen. Verwendete die falsche Kombination aus Rückgabetyp und Anmerkung - dasselbe.
- Schwache Unterstützung durch die IDE. Fehlende automatische Vervollständigung. Der Benutzer kann nicht sehen, welche Aktionen ihm in seiner Situation zur Verfügung stehen (als ob er einen „Punkt“ hinter das Objekt setzen und eine Liste der verfügbaren Methoden sehen würde).
- Es gibt nur wenige Anwendungsmöglichkeiten. Der bereits erwähnte http-Client fällt mir ein und der Client wechselt zur Datenbank. Aber warum kann dies sonst angewendet werden?
In meinem Arbeitsentwurf hat der Ansatz jedoch Wurzeln geschlagen und ist beliebt. Die Vorteile, die ich bereits erwähnt habe - Einfachheit, wenig Code, Deklarativität - ermöglichen es Entwicklern, sich auf das Schreiben von wichtigerem Code zu konzentrieren.
Was denkst du über diesen Ansatz? Lohnt sich die Mühe? Welche Probleme sehen Sie bei diesem Ansatz? Während ich immer noch versuche, es zu verstehen, während es in unserer Produktion herumgerollt wird, würde ich gerne hören, was andere Leute darüber denken. Ich hoffe, dieses Material war für jemanden nützlich.