Emulieren von Eigenschaftsliteralen mit der Java 8-Methodenreferenz


Von einem Übersetzer: Das Vergehen aufgrund des Fehlens eines nameOf-Operators in Java hat mich dazu veranlasst, diesen Artikel zu übersetzen. Für Ungeduldige - am Ende des Artikels gibt es eine vorgefertigte Implementierung in den Quellen und Binärdateien.

Eines der Dinge, die Bibliotheksentwicklern in Java häufig fehlen, sind Eigenschaftsliterale. In diesem Beitrag werde ich zeigen, wie Sie die Methodenreferenz von Java 8 kreativ verwenden können, um Eigenschaftsliterale mithilfe der Bytecode-Generierung zu emulieren.

Ähnlich wie bei Klassenliteralen (z. B. Customer.class ) würden Eigenschaftsliterale es ermöglichen, auf Eigenschaften von Bean-Klassen zu verweisen, die typsicher sind. Dies ist nützlich, um eine API zu entwerfen, bei der Aktionen für Eigenschaften ausgeführt oder auf irgendeine Weise konfiguriert werden müssen.

Vom Übersetzer: Unter dem Schnitt analysieren wir, wie dies mit improvisierten Mitteln umgesetzt werden kann.

Betrachten Sie beispielsweise die API für die Indexzuordnungskonfiguration in der Suche im Ruhezustand:

 new SearchMapping().entity(Address.class) .indexed() .property("city", ElementType.METHOD) .field(); 

Oder die validateValue() -Methode aus der Bean Validation API, mit der Sie den Wert anhand der Einschränkungen für die Eigenschaft überprüfen können:

 Set<ConstraintViolation<Address>> violations = validator.validateValue(Address.class, "city", "Purbeck" ); 

In beiden Fällen wird der String Typ verwendet, um auf die city Eigenschaft des Address Objekts zu verweisen.

Dies kann zu Fehlern führen:
  • Die Adressklasse verfügt möglicherweise überhaupt nicht über eine Stadteigenschaft. Oder jemand vergisst möglicherweise, den Zeichenfolgennamen der Eigenschaft zu aktualisieren, nachdem er die Methoden get / set beim Refactoring umbenannt hat.
  • Im Fall von validateValue() können wir nicht überprüfen, ob der Typ des übergebenen Werts mit dem Typ der Eigenschaft übereinstimmt.

Benutzer dieser API können diese Probleme nur durch Starten der Anwendung kennenlernen. Wäre es nicht cool, wenn der Compiler und das Typsystem eine solche Verwendung von Anfang an verhindern würden? Wenn Java Eigenschaftsliterale hätte, könnten wir dies tun (dieser Code wird nicht kompiliert):

 mapping.entity(Address.class) .indexed() .property(Address::city, ElementType.METHOD ) .field(); 

Und:

 validator.validateValue(Address.class, Address::city, "Purbeck"); 

Wir könnten die oben genannten Probleme vermeiden: Jeder Tippfehler im Eigenschaftsnamen würde zu einem Kompilierungsfehler führen, der direkt in Ihrer IDE festgestellt werden kann. Auf diese Weise können wir die Konfigurations-API für den Ruhezustand der Suche so gestalten, dass sie nur die Eigenschaften der Adressklasse akzeptiert, wenn wir die Adressentität konfigurieren. Im Fall von Bean Validation validateValue() Eigenschaftsliterale sicherstellen, dass ein Wert des richtigen Typs übergeben wird.

Java 8-Methodenreferenz


Java 8 unterstützt keine Eigenschaftsliterale (und es ist nicht geplant, sie in Java 11 zu unterstützen), bietet jedoch gleichzeitig eine interessante Möglichkeit, sie zu emulieren: Methodenreferenz (Methodenreferenz). Ursprünglich wurde die Methodenreferenz hinzugefügt, um die Arbeit mit Lambda-Ausdrücken zu vereinfachen. Sie können jedoch als Eigenschaftsliterale für die Armen verwendet werden.

Betrachten Sie die Idee, einen Verweis auf die Getter-Methode als Eigenschaftsliteral zu verwenden:

 validator.validateValue(Address.class, Address::getCity, "Purbeck"); 

Dies funktioniert natürlich nur, wenn Sie einen Getter haben. Wenn Ihre Klassen jedoch bereits der JavaBeans-Konvention folgen, was meistens der Fall ist, ist das in Ordnung.

Wie würde eine Deklaration der validateValue() -Methode aussehen? Der entscheidende Punkt ist die Verwendung des neuen Function :

 public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value); 

Mithilfe von zwei Tippparametern können wir überprüfen, ob der Bin-Typ, die Eigenschaften und der übergebene Wert korrekt sind. Aus Sicht der API haben wir das, was wir brauchen: Es ist sicher, es zu verwenden, und die IDE ergänzt sogar automatisch die Methodennamen, die mit Address:: . Aber wie leitet man den Namen der Eigenschaft aus dem Function Objekt bei der Implementierung der validateValue() -Methode ab?

Und dann beginnt der Spaß, da die Funktionsfunktionsschnittstelle nur eine Methode deklariert - apply() , die den Funktionscode für die übergebene T Instanz ausführt. Dies scheint nicht das zu sein, was wir brauchten.

ByteBuddy zur Rettung


Wie sich herausstellt, besteht der Trick darin, die Funktion anzuwenden! Durch das Erstellen einer Proxy-Instanz vom Typ T haben wir das Ziel, die Methode aufzurufen und ihren Namen im Proxy-Aufruf-Handler abzurufen. (Vom Übersetzer: Im Folgenden geht es um dynamische Java-Proxys - java.lang.reflect.Proxy).

Java unterstützt standardmäßig dynamische Proxys, diese Unterstützung ist jedoch nur auf Schnittstellen beschränkt. Da unsere API mit allen Beans funktionieren sollte, einschließlich realer Klassen, werde ich anstelle von Proxy ein großartiges Tool, ByteBuddy, verwenden. ByteBuddy bietet ein einfaches DSL zum Erstellen von Klassen im laufenden Betrieb, was wir brauchen.

Beginnen wir mit der Definition einer Schnittstelle, über die wir den aus der Methodenreferenz extrahierten Eigenschaftsnamen speichern und abrufen können.

 public interface PropertyNameCapturer { String getPropertyName(); void setPropertyName(String propertyName); } 

Jetzt verwenden wir ByteBuddy, um programmgesteuert Proxy-Klassen zu erstellen, die mit den für uns interessanten Typen kompatibel sind (z. B. Adresse), und PropertyNameCapturer implementieren:

 public <T> T /* & PropertyNameCapturer */ getPropertyNameCapturer(Class<T> type) { DynamicType.Builder<?> builder = new ByteBuddy() (1) .subclass( type.isInterface() ? Object.class : type ); if (type.isInterface()) { (2) builder = builder.implement(type); } Class<?> proxyType = builder .implement(PropertyNameCapturer.class) (3) .defineField("propertyName", String.class, Visibility.PRIVATE) .method( ElementMatchers.any()) (4) .intercept(MethodDelegation.to( PropertyNameCapturingInterceptor.class )) .method(named("setPropertyName").or(named("getPropertyName"))) (5) .intercept(FieldAccessor.ofBeanProperty()) .make() .load( (6) PropertyNameCapturer.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER ) .getLoaded(); try { @SuppressWarnings("unchecked") Class<T> typed = (Class<T>) proxyType; return typed.newInstance(); (7) } catch (InstantiationException | IllegalAccessException e) { throw new HibernateException( "Couldn't instantiate proxy for method name retrieval", e ); } } 

Der Code mag etwas verwirrend erscheinen, also lassen Sie es mich erklären. Zuerst erhalten wir eine Instanz von ByteBuddy (1), dem DSL-Einstiegspunkt. Es wird verwendet, um dynamische Typen zu erstellen, die entweder den gewünschten Typ erweitern (wenn es sich um eine Klasse handelt) oder Object erben und den gewünschten Typ implementieren (wenn es sich um eine Schnittstelle handelt) (2).

Dann geben wir an, dass der Typ die PropertyNameCapturer-Schnittstelle implementiert, und fügen ein Feld hinzu, in dem der Name der gewünschten Eigenschaft gespeichert wird (3). Dann sagen wir, dass Aufrufe aller Methoden von PropertyNameCapturingInterceptor (4) abgefangen werden sollten. Nur setPropertyName () und getPropertyName () (über die PropertyNameCapturer-Schnittstelle) sollten auf die zuvor erstellte Immobilie zugreifen (5). Schließlich wird die Klasse erstellt, geladen (6) und instanziiert (7).

Das ist alles, was wir brauchen, um Proxy-Typen zu erstellen. Dank ByteBuddy kann dies in wenigen Codezeilen erfolgen. Schauen wir uns nun den Anrufabfangjäger an:

 public class PropertyNameCapturingInterceptor { @RuntimeType public static Object intercept(@This PropertyNameCapturer capturer, @Origin Method method) { (1) capturer.setPropertyName(getPropertyName(method)); (2) if (method.getReturnType() == byte.class) { (3) return (byte) 0; } else if ( ... ) { } // ... handle all primitve types // ... } else { return null; } } private static String getPropertyName(Method method) { (4) final boolean hasGetterSignature = method.getParameterTypes().length == 0 && method.getReturnType() != null; String name = method.getName(); String propName = null; if (hasGetterSignature) { if (name.startsWith("get") && hasGetterSignature) { propName = name.substring(3, 4).toLowerCase() + name.substring(4); } else if (name.startsWith("is") && hasGetterSignature) { propName = name.substring(2, 3).toLowerCase() + name.substring(3); } } else { throw new HibernateException( "Only property getter methods are expected to be passed"); (5) } return propName; } } 

Die intercept () -Methode akzeptiert die aufgerufene Methode und das Ziel für den Aufruf (1). Die @This @Origin und @This werden verwendet, um geeignete Parameter anzugeben, damit ByteBuddy die richtigen intercept () -Aufrufe in einem dynamischen Proxy generieren kann.

Beachten Sie, dass es keine strikte Abhängigkeit des Rezeptors von ByteBuddy-Typen gibt, da ByteBuddy nur zum Erstellen eines dynamischen Proxys verwendet wird, nicht jedoch bei Verwendung.

Durch Aufrufen von getPropertyName() (4) können wir den Eigenschaftsnamen getPropertyName() , der der übergebenen Methodenreferenz entspricht, und ihn in PropertyNameCapturer (2) speichern. Wenn die Methode kein Getter ist, löst der Code eine Ausnahme aus (5). Der Rückgabetyp des Getters spielt keine Rolle, daher geben wir unter Berücksichtigung des Eigenschaftstyps (3) null zurück.

Jetzt können wir den Eigenschaftsnamen in der validateValue() -Methode abrufen:

 public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value) { T capturer = getPropertyNameCapturer(type); property.apply(capturer); String propertyName = ((PropertyLiteralCapturer) capturer).getPropertyName(); //      } 

Nachdem wir die Funktion auf den erstellten Proxy angewendet haben, wandeln wir den Typ in PropertyNameCapturer um und erhalten den Namen von Method.

Unter Verwendung der Magie des Generierens von Bytecode haben wir die Methodenreferenz aus Java 8 verwendet, um Eigenschaftsliterale zu emulieren.

Wenn wir Immobilienliterale in der Sprache hätten, wären wir natürlich alle besser dran. Ich würde sogar erlauben, mit privaten Eigenschaften zu arbeiten, und wahrscheinlich könnten Eigenschaften aus Anmerkungen referenziert werden. Immobilienliterale wären aufgeräumter (ohne das Präfix "get") und würden nicht wie ein Hack aussehen.

Vom Übersetzer


Es ist erwähnenswert, dass andere gute Sprachen bereits (oder fast) einen ähnlichen Mechanismus unterstützen:


Wenn Sie das Lombok-Projekt plötzlich mit Java verwenden, wird ein Bytecode-Kompilierungszeitgenerator dafür geschrieben.

Inspiriert von dem im Artikel beschriebenen Ansatz hat Ihr bescheidener Diener eine kleine Bibliothek zusammengestellt, die nameOfProperty () für Java 8 implementiert:

Quellcode
Binärdateien

Source: https://habr.com/ru/post/de420533/


All Articles