Emulación de literales de propiedad con la referencia de método Java 8


De un traductor: la ofensa por la falta de un operador nameOf en Java me empujó a traducir este artículo. Para los impacientes: al final del artículo hay una implementación preparada en las fuentes y los binarios.

Una de las cosas que los desarrolladores de bibliotecas en Java a menudo carecen son los literales de propiedad. En esta publicación, le mostraré cómo puede usar creativamente la Referencia de métodos de Java 8 para emular literales de propiedad mediante la generación de bytecode.

Al igual que los literales de clase (por ejemplo, Customer.class ), los literales de propiedad permitirían hacer referencia a las propiedades de las clases de bean seguras. Esto sería útil para diseñar una API donde sea necesario realizar acciones en las propiedades o de alguna manera configurarlas.

Del traductor: Debajo del corte, analizamos cómo implementar esto desde medios improvisados.

Por ejemplo, considere la API de configuración de asignación de índice en Hibernate Search:

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

O el método validateValue() de Bean Validation API, que le permite verificar el valor contra las restricciones de la propiedad:

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

En ambos casos, el tipo de String se usa para referirse a la propiedad de city del objeto Address .

Esto puede conducir a errores:
  • la clase de dirección puede no tener una propiedad de la city en absoluto. O, alguien podría olvidar actualizar el nombre de la cadena de la propiedad después de cambiar el nombre de los métodos get / set al refactorizar.
  • en el caso de validateValue() , no tenemos forma de verificar que el tipo del valor pasado coincida con el tipo de la propiedad.

Los usuarios de esta API solo pueden conocer estos problemas al iniciar la aplicación. ¿No sería genial si el compilador y el sistema de tipos impidieran tal uso desde el principio? Si Java tuviera literales de propiedad, entonces podríamos hacer esto (este código no se compila):

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

Y:

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

Podríamos evitar los problemas mencionados anteriormente: cualquier error tipográfico en el nombre de la propiedad provocaría un error de compilación, que puede notarse directamente en su IDE. Esto nos permitiría diseñar la API de configuración de Hibernate Search para que solo acepte las propiedades de la clase Address cuando configuramos la entidad Address. Y en el caso de Bean Validation validateValue() los literales de propiedad ayudarían a asegurarnos de que estamos pasando un valor del tipo correcto.

Referencia del método Java 8


Java 8 no admite literales de propiedad (y no está previsto admitirlos en Java 11), pero al mismo tiempo, proporciona una forma interesante de emularlos: Referencia de método (referencia de método). Inicialmente, la Referencia del método se agregó para simplificar el trabajo con expresiones lambda, pero se pueden usar como literales de propiedad para los pobres.

Considere la idea de usar una referencia al método getter como un literal de propiedad:

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

Obviamente, esto solo funcionará si tienes un captador. Pero si sus clases ya siguen la convención JavaBeans, que suele ser el caso, entonces está bien.

¿Cómo sería una declaración del método validateValue() ? El punto clave es el uso del nuevo tipo de Function :

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

Usando dos parámetros de tipeo, podemos verificar que el tipo de contenedor, las propiedades y el valor pasado sean correctos. Desde el punto de vista de la API, obtuvimos lo que necesitamos: es seguro usarlo y el IDE incluso complementará automáticamente los nombres de métodos que comienzan con Address:: . Pero, ¿cómo derivar el nombre de la propiedad del objeto Function en la implementación del método validateValue() ?

Y luego comienza la diversión, ya que la interfaz funcional de Function solo declara un método: apply() , que ejecuta el código de función para la instancia T pasada. Esto no parece ser lo que necesitábamos.

ByteBuddy al rescate


¡Resulta que el truco está en aplicar la función! Al crear una instancia de proxy de tipo T, tenemos el objetivo de llamar al método y obtener su nombre en el controlador de llamadas de proxy. (Del traductor: en adelante estamos hablando de proxies dinámicos de Java - java.lang.reflect.Proxy).

Java admite servidores proxy dinámicos listos para usar, pero este soporte está limitado solo a las interfaces. Como nuestra API debería funcionar con cualquier bean, incluidas las clases reales, voy a usar una gran herramienta, ByteBuddy, en lugar de Proxy. ByteBuddy proporciona un DSL simple para crear clases sobre la marcha, que es lo que necesitamos.

Comencemos por definir una interfaz que nos permita almacenar y recuperar el nombre de propiedad extraído de la Referencia del método.

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

Ahora usamos ByteBuddy para crear mediante programación clases proxy que sean compatibles con los tipos que nos interesan (por ejemplo: Dirección) e implementar 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 ); } } 

El código puede parecer un poco confuso, así que déjenme explicarlo. Primero obtenemos una instancia de ByteBuddy (1), que es el punto de entrada DSL. Se utiliza para crear tipos dinámicos que extienden el tipo deseado (si es una clase) o heredan Object e implementan el tipo deseado (si es una interfaz) (2).

Luego, indicamos que el tipo implementa la interfaz PropertyNameCapturer y agregamos un campo para almacenar el nombre de la propiedad deseada (3). Luego decimos que las llamadas a todos los métodos deben ser interceptadas por PropertyNameCapturingInterceptor (4). Solo setPropertyName () y getPropertyName () (desde la interfaz PropertyNameCapturer) deben acceder a la propiedad real creada anteriormente (5). Finalmente, la clase se crea, se carga (6) y se instancia (7).

Eso es todo lo que necesitamos para crear tipos de proxy, gracias ByteBuddy, esto se puede hacer en unas pocas líneas de código. Ahora echemos un vistazo al interceptor de llamadas:

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

El método intercept () acepta el método llamado y el objetivo de la llamada (1). Las @This @Origin y @This se usan para especificar los parámetros apropiados para que ByteBuddy pueda generar las llamadas correctas intercept () en un proxy dinámico.

Tenga en cuenta que no existe una dependencia estricta del receptor de los tipos ByteBuddy, ya que ByteBuddy se usa solo para crear un proxy dinámico, pero no cuando se usa.

Al llamar a getPropertyName() (4) podemos obtener el nombre de propiedad correspondiente a la referencia de método pasada y guardarlo en PropertyNameCapturer (2). Si el método no es un captador, entonces el código arroja una excepción (5). El tipo de retorno del captador no importa, por lo que devolvemos nulo considerando el tipo de propiedad (3).

Ahora estamos listos para obtener el nombre de la propiedad en el método 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(); //      } 

Después de aplicar la función al proxy creado, enviamos el tipo a PropertyNameCapturer y obtenemos el nombre de Method.

Entonces, usando algo de la magia de generar bytecode, usamos la Referencia de métodos de Java 8 para emular literales de propiedad.

Por supuesto, si tuviéramos literales de propiedad real en el lenguaje, todos estaríamos mejor. Incluso permitiría trabajar con propiedades privadas y, probablemente, se podría hacer referencia a las propiedades a partir de anotaciones. Los literales de propiedad real serían más ordenados (sin el prefijo "get") y no se verían como un truco.

Del traductor


Vale la pena señalar que otros buenos idiomas ya admiten (o casi) un mecanismo similar:


Si de repente usa el proyecto Lombok con Java, entonces se escribe un generador de tiempo de compilación de bytecode .

Inspirado por el enfoque descrito en el artículo, su humilde servidor creó una pequeña biblioteca que implementa nameOfProperty () para Java 8:

Código fuente
Binarios

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


All Articles