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 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 ( ... ) { }
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 fuenteBinarios