Emulando literais de propriedade com o Java 8 Method Reference


De um tradutor: a ofensa pela falta de um operador nameOf em Java me levou a traduzir este artigo. Para os impacientes - no final do artigo, há uma implementação pronta nas fontes e binários.

Uma das coisas que os desenvolvedores de bibliotecas em Java geralmente não têm são os literais de propriedade. Neste post, mostrarei como você pode usar criativamente a Referência de método do Java 8 para emular literais de propriedade usando a geração de bytecode.

Semelhante aos literais de classe (por exemplo, Customer.class ), os literais de propriedade tornariam possível fazer referência às propriedades das classes de bean seguras para o tipo. Isso seria útil para projetar uma API em que é necessário executar ações nas propriedades ou configurá-las de alguma forma.

Do tradutor: Sob o corte, analisamos como implementar isso a partir de meios improvisados.

Por exemplo, considere a API de configuração do mapeamento de índice na Pesquisa do Hibernate:

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

Ou o método validateValue() da API de validação de bean, que permite verificar o valor com relação às restrições na propriedade:

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

Nos dois casos, o tipo String é usado para se referir à propriedade da city do objeto Address .

Isso pode levar a erros:
  • a classe Address pode não ter uma propriedade da city . Ou alguém pode esquecer de atualizar o nome da string da propriedade depois de renomear os métodos get / set ao refatorar.
  • no caso de validateValue() , não temos como verificar se o tipo do valor passado corresponde ao tipo da propriedade.

Os usuários desta API só podem aprender sobre esses problemas iniciando o aplicativo. Não seria legal se o sistema de compilação e tipo impedisse esse uso desde o início? Se Java tivesse literais de propriedade, poderíamos fazer isso (este código não compila):

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

E:

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

Podemos evitar os problemas mencionados acima: qualquer erro de digitação no nome da propriedade levaria a um erro de compilação, que pode ser percebido diretamente no seu IDE. Isso nos permitiria projetar a API de configuração do Hibernate Search para que ela só aceite as propriedades da classe Address quando configuramos a entidade Address. E no caso de Validação de Bean validateValue() literais de propriedade ajudariam a garantir que passamos um valor do tipo correto.

Referência do método Java 8


O Java 8 não suporta literais de propriedade (e não está planejado para suportá-los no Java 11), mas, ao mesmo tempo, fornece uma maneira interessante de emulá-los: Referência de método (referência de método). Inicialmente, a Referência do método foi adicionada para simplificar o trabalho com expressões lambda, mas elas podem ser usadas como literais de propriedade para os pobres.

Considere a ideia de usar uma referência ao método getter como um literal de propriedade:

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

Obviamente, isso só funcionará se você tiver um getter. Mas se suas aulas já seguem a convenção do JavaBeans, que costuma ser o caso, tudo bem.

Como seria uma declaração do método validateValue() ? O ponto principal é o uso do novo tipo de Function :

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

Usando dois parâmetros de digitação, podemos verificar se o tipo de lixeira, propriedades e valor passado estão corretos. Do ponto de vista da API, conseguimos o que precisamos: é seguro usá-la e o IDE complementará automaticamente os nomes dos métodos começando com Address:: . Mas como derivar o nome da propriedade do objeto Function na implementação do método validateValue() ?

E então a diversão começa, já que a interface funcional Function apenas declara um método - apply() , que executa o código da função para a instância T passada. Isso não parece ser o que precisávamos.

ByteBuddy para o resgate


Como se vê, o truque está em aplicar a função! Ao criar uma instância de proxy do tipo T, temos o objetivo de chamar o método e obter seu nome no manipulador de chamadas do Proxy. (Do tradutor: a seguir, estamos falando sobre proxies Java dinâmicos - java.lang.reflect.Proxy).

Java suporta proxies dinâmicos prontos para uso, mas esse suporte é limitado apenas a interfaces. Como nossa API deve funcionar com qualquer bean, incluindo classes reais, vou usar uma ótima ferramenta, ByteBuddy, em vez de Proxy. O ByteBuddy fornece uma DSL simples para criar classes em tempo real, e é disso que precisamos.

Vamos começar definindo uma interface que nos permita armazenar e recuperar o nome da propriedade extraído da Referência de método.

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

Agora usamos o ByteBuddy para criar programaticamente classes de proxy compatíveis com os tipos de interesse para nós (por exemplo: Endereço) 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 ); } } 

O código pode parecer um pouco confuso, então deixe-me explicar. Primeiro, obtemos uma instância do ByteBuddy (1), que é o ponto de entrada DSL. É usado para criar tipos dinâmicos que estendem o tipo desejado (se for uma classe) ou herdam Object e implementam o tipo desejado (se for uma interface) (2).

Em seguida, indicamos que o tipo implementa a interface PropertyNameCapturer e adicionamos um campo para armazenar o nome da propriedade desejada (3). Então dizemos que as chamadas para todos os métodos devem ser interceptadas pelo PropertyNameCapturingInterceptor (4). Somente setPropertyName () e getPropertyName () (da interface PropertyNameCapturer) devem acessar a propriedade real criada anteriormente (5). Finalmente, a classe é criada, carregada (6) e instanciada (7).

É tudo o que precisamos para criar tipos de proxy, graças ao ByteBuddy, isso pode ser feito em algumas linhas de código. Agora, vejamos o interceptador de chamadas:

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

O método intercept () aceita o método chamado e o destino para a chamada (1). As @This @Origin e @Origin são usadas para especificar parâmetros apropriados para que o ByteBuddy possa gerar as chamadas intercept () corretas em um proxy dinâmico.

Observe que não há uma dependência estrita do receptor nos tipos ByteBuddy, pois o ByteBuddy é usado apenas para criar um proxy dinâmico, mas não ao usá-lo.

Ao chamar getPropertyName() (4), podemos obter o nome da propriedade correspondente à Referência de método passada e salvá-la em PropertyNameCapturer (2). Se o método não for um getter, o código emitirá uma exceção (5). O tipo de retorno do getter não importa, então retornamos nulos considerando o tipo de propriedade (3).

Agora estamos prontos para obter o nome da propriedade no 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(); //      } 

Depois de aplicar a função ao proxy criado, convertemos o tipo em PropertyNameCapturer e obtemos o nome de Method.

Portanto, usando parte da mágica de gerar bytecode, usamos a Referência de método do Java 8 para emular literais de propriedades.

Obviamente, se tivéssemos literais de propriedades reais na linguagem, todos estaríamos melhor. Eu até permitiria trabalhar com propriedades privadas e, provavelmente, propriedades poderiam ser referenciadas a partir de anotações. Literais de propriedade real seriam mais organizados (sem o prefixo "get") e não pareceriam um hack.

Do tradutor


Vale ressaltar que outras boas linguagens já suportam (ou quase) um mecanismo semelhante:


Se você usar repentinamente o projeto Lombok com Java, um gerador de tempo de compilação de bytecodes será gravado para ele.

Inspirado na abordagem descrita no artigo, seu humilde servidor montou uma pequena biblioteca que implementa nameOfProperty () para Java 8:

Código fonte
Binários

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


All Articles