Émulation de littéraux de propriété avec la référence de méthode Java 8


D'un traducteur: l'offense du manque d'opérateur nameOf en Java m'a poussé à traduire cet article. Pour les impatients - à la fin de l'article, il y a une implémentation prête à l'emploi dans les sources et les binaires.

Les littéraux de propriété manquent souvent aux développeurs de bibliothèques en Java. Dans cet article, je montrerai comment vous pouvez utiliser de manière créative la référence de méthode de Java 8 pour émuler des littéraux de propriété en utilisant la génération de bytecode.

À l'instar des littéraux de classe (par exemple, Customer.class ), les littéraux de propriété permettraient de faire référence aux propriétés des classes de bean en toute sécurité. Cela serait utile pour concevoir une API où il est nécessaire d'effectuer des actions sur les propriétés ou de les configurer d'une manière ou d'une autre.

Du traducteur: Sous la coupe, nous analysons comment mettre en œuvre cela à partir de moyens improvisés.

Par exemple, considérez l'API de configuration de mappage d'index dans Hibernate Search:

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

Ou la méthode validateValue() de l'API Bean Validation, qui vous permet de vérifier la valeur par rapport aux restrictions sur la propriété:

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

Dans les deux cas, le type String est utilisé pour faire référence à la propriété city de l'objet Address .

Cela peut entraîner des erreurs:
  • la classe Address peut ne pas avoir du tout de propriété de city . Ou, quelqu'un peut oublier de mettre à jour le nom de chaîne de la propriété après avoir renommé les méthodes get / set lors de la refactorisation.
  • dans le cas de validateValue() , nous n'avons aucun moyen de vérifier que le type de la valeur passée correspond au type de la propriété.

Les utilisateurs de cette API ne peuvent connaître ces problèmes qu'en lançant l'application. Ne serait-ce pas cool si le compilateur et le système de type empêchaient une telle utilisation dès le début? Si Java avait des littéraux de propriété, nous pourrions le faire (ce code ne compile pas):

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

Et:

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

Nous pourrions éviter les problèmes mentionnés ci-dessus: toute faute de frappe dans le nom de la propriété entraînerait une erreur de compilation, qui peut être remarquée directement dans votre IDE. Cela nous permettrait de concevoir l'API de configuration Hibernate Search de sorte qu'elle n'accepte les propriétés de la classe Address que lorsque nous configurons l'entité Address. Et dans le cas de Bean Validation validateValue() les littéraux de propriété permettraient de s'assurer que nous transmettons une valeur du type correct.

Référence des méthodes Java 8


Java 8 ne prend pas en charge les littéraux de propriété (et il n'est pas prévu de les prendre en charge dans Java 11), mais en même temps, il fournit un moyen intéressant de les émuler: Référence de méthode (référence de méthode). Initialement, la référence de méthode a été ajoutée pour simplifier le travail avec les expressions lambda, mais elles peuvent être utilisées comme littéraux de propriété pour les pauvres.

Considérez l'idée d'utiliser une référence à la méthode getter comme littéral de propriété:

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

De toute évidence, cela ne fonctionnera que si vous avez un getter. Mais si vos classes suivent déjà la convention JavaBeans, ce qui est le plus souvent le cas, alors ça va.

À quoi ressemblerait une déclaration de la méthode validateValue() ? Le point clé est l'utilisation du nouveau type de Function :

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

En utilisant deux paramètres de frappe, nous pouvons vérifier que le type de bac, les propriétés et la valeur transmise sont corrects. Du point de vue de l'API, nous avons obtenu ce dont nous avons besoin: il est sûr à utiliser et l'EDI complétera même automatiquement les noms de méthode commençant par Address:: . Mais comment dériver le nom de la propriété de l'objet Function dans l'implémentation de la méthode validateValue() ?

Et puis le plaisir commence, puisque l'interface fonctionnelle Function ne déclare qu'une seule méthode - apply() , qui exécute le code de fonction pour l'instance T passée. Cela ne semble pas être ce dont nous avions besoin.

ByteBuddy à la rescousse


En fait, l'astuce consiste à appliquer la fonction! En créant une instance proxy de type T, nous avons pour objectif d'appeler la méthode et d'obtenir son nom dans le gestionnaire d'appels proxy. (Du traducteur: ci-après, nous parlons de proxys Java dynamiques - java.lang.reflect.Proxy).

Java prend en charge les proxys dynamiques prêts à l'emploi, mais cette prise en charge est limitée uniquement aux interfaces. Étant donné que notre API devrait fonctionner avec tous les beans, y compris les classes réelles, je vais utiliser un excellent outil, ByteBuddy, au lieu de Proxy. ByteBuddy fournit un DSL simple pour créer des classes à la volée, ce dont nous avons besoin.

Commençons par définir une interface qui nous permettrait de stocker et de récupérer le nom de propriété extrait de la référence de méthode.

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

Maintenant, nous utilisons ByteBuddy pour créer par programme des classes proxy qui sont compatibles avec les types qui nous intéressent (par exemple: Adresse) et implémenter PropertyNameCapturer :

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

Le code peut sembler un peu déroutant, alors laissez-moi vous l'expliquer. Nous obtenons d'abord une instance de ByteBuddy (1), qui est le point d'entrée DSL. Il est utilisé pour créer des types dynamiques qui étendent le type souhaité (s'il s'agit d'une classe) ou héritent Object et implémentent le type souhaité (s'il s'agit d'une interface) (2).

Ensuite, nous indiquons que le type implémente l'interface PropertyNameCapturer et ajoutons un champ pour stocker le nom de la propriété souhaitée (3). Ensuite, nous disons que les appels à toutes les méthodes doivent être interceptés par PropertyNameCapturingInterceptor (4). Seuls setPropertyName () et getPropertyName () (à partir de l'interface PropertyNameCapturer) doivent accéder à la propriété réelle créée précédemment (5). Enfin, la classe est créée, chargée (6) et instanciée (7).

C'est tout ce dont nous avons besoin pour créer des types de proxy, merci ByteBuddy, cela peut être fait en quelques lignes de code. Voyons maintenant l'intercepteur d'appel:

 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; } } 

La méthode intercept () accepte la méthode appelée et la cible de l'appel (1). Les @This @Origin et @This sont utilisées pour spécifier les paramètres appropriés afin que ByteBuddy puisse générer les appels intercept () corrects dans un proxy dynamique.

Notez qu'il n'y a pas de dépendance stricte du récepteur sur les types ByteBuddy, puisque ByteBuddy est utilisé uniquement pour créer un proxy dynamique, mais pas lors de son utilisation.

En appelant getPropertyName() (4), nous pouvons obtenir le nom de propriété correspondant à la référence de méthode passée et l'enregistrer dans PropertyNameCapturer (2). Si la méthode n'est pas un getter, le code lève une exception (5). Le type de retour du getter n'a pas d'importance, nous retournons donc null en considérant le type de propriété (3).

Nous sommes maintenant prêts à obtenir le nom de la propriété dans la méthode validateValue() :

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

Après avoir appliqué la fonction au proxy créé, nous convertissons le type en PropertyNameCapturer et obtenons le nom de Method.

Ainsi, en utilisant une partie de la magie de la génération de bytecode, nous avons utilisé la référence de méthode de Java 8 pour émuler les littéraux de propriété.

Bien sûr, si nous avions des littéraux immobiliers dans la langue, nous serions tous mieux lotis. Je permettrais même de travailler avec des propriétés privées et, probablement, les propriétés pourraient être référencées à partir d'annotations. Les littéraux des biens immobiliers seraient plus ordonnés (sans le préfixe "get") et ne ressembleraient pas à un hack.

Du traducteur


Il convient de noter que d'autres bonnes langues prennent déjà en charge (ou presque) un mécanisme similaire:


Si vous utilisez soudainement le projet Lombok avec Java, un générateur de temps de compilation de bytecode est écrit pour cela.

Inspiré par l'approche décrite dans l'article, votre humble serviteur a créé une petite bibliothèque qui implémente nameOfProperty () pour Java 8:

Code source
Binaires

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


All Articles