Meniru literal properti dengan Java 8 Method Reference


Dari seorang penerjemah: pelanggaran karena kurangnya operator nameOf di Jawa mendorong saya untuk menerjemahkan artikel ini. Untuk yang tidak sabar - di akhir artikel ada implementasi yang sudah jadi di sumber dan binari.

Salah satu hal yang kurang dimiliki oleh pengembang perpustakaan di Jawa adalah literal properti. Dalam posting ini, saya akan menunjukkan bagaimana Anda dapat secara kreatif menggunakan Referensi Metode dari Java 8 untuk mengemulasi literal properti menggunakan generasi bytecode.

Sejalan dengan literal kelas (mis., Customer.class ), literal properti memungkinkan untuk merujuk ke properti safe-type kelas kacang. Ini akan berguna untuk mendesain API di mana ada kebutuhan untuk melakukan tindakan pada properti atau mengonfigurasinya.

Dari penerjemah: Di bawah potongan, kami menganalisis bagaimana menerapkan ini dari cara improvisasi.

Misalnya, pertimbangkan API konfigurasi pemetaan indeks di Pencarian Hibernate:

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

Atau metode validateValue() dari API Validasi Bean, yang memungkinkan Anda memeriksa nilainya terhadap pembatasan properti:

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

Dalam kedua kasus, tipe String digunakan untuk merujuk ke properti city dari objek Address .

Ini dapat menyebabkan kesalahan:
  • kelas Address mungkin tidak memiliki properti city sama sekali. Atau, seseorang mungkin lupa memperbarui nama string properti setelah mengganti nama metode get / set saat refactoring.
  • dalam kasus validateValue() , kami tidak memiliki cara untuk memverifikasi bahwa jenis nilai yang diteruskan cocok dengan jenis properti.

Pengguna API ini hanya dapat mempelajari tentang masalah ini dengan meluncurkan aplikasi. Bukankah lebih keren jika sistem kompiler dan tipe mencegah penggunaan seperti itu dari awal? Jika Java memiliki literal properti, maka kita bisa melakukan ini (kode ini tidak dikompilasi):

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

Dan:

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

Kami dapat menghindari masalah yang disebutkan di atas: kesalahan ketik apa pun pada nama properti akan menyebabkan kesalahan kompilasi, yang dapat dilihat langsung di IDE Anda. Ini akan memungkinkan kami untuk merancang API konfigurasi Pencarian Hibernate sehingga hanya menerima properti dari kelas Address ketika kami mengkonfigurasi entitas Address. Dan dalam kasus Validasi validateValue() literal properti akan membantu memastikan bahwa kami memberikan nilai dari tipe yang benar.

Referensi Metode Java 8


Java 8 tidak mendukung literal properti (dan tidak direncanakan untuk mendukungnya di Java 11), tetapi pada saat yang sama, Java menyediakan cara yang menarik untuk meniru mereka: Metode Referensi (metode referensi). Awalnya, Referensi Metode ditambahkan untuk menyederhanakan bekerja dengan ekspresi lambda, tetapi mereka dapat digunakan sebagai literal properti untuk orang miskin.

Pertimbangkan gagasan untuk menggunakan referensi ke metode pengambil sebagai literal properti:

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

Jelas, ini hanya akan berfungsi jika Anda memiliki seorang pengambil. Tetapi jika kelas Anda sudah mengikuti konvensi JavaBeans, yang paling sering terjadi, maka itu tidak masalah.

Seperti apa deklarasi metode validateValue() ? Poin kuncinya adalah penggunaan tipe Function baru:

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

Dengan menggunakan dua parameter pengetikan, kita dapat memverifikasi bahwa tipe bin, properti, dan nilai yang diteruskan sudah benar. Dari sudut pandang API, kami mendapatkan apa yang kami butuhkan: aman untuk menggunakannya dan IDE bahkan akan secara otomatis menambah nama metode yang dimulai dengan Address:: . Tetapi bagaimana cara mendapatkan nama properti dari objek Function dalam implementasi metode validateValue() ?

Dan kemudian kesenangan dimulai, karena antarmuka fungsional Fungsi hanya menyatakan satu metode - apply() , yang mengeksekusi kode fungsi untuk instance T diteruskan. Ini sepertinya bukan yang kami butuhkan.

ByteBuddy untuk menyelamatkan


Ternyata, triknya adalah dalam menerapkan fungsi! Dengan membuat instance proxy dari tipe T, kami memiliki tujuan memanggil metode dan mendapatkan namanya di pengendali panggilan Proxy. (Dari penerjemah: selanjutnya kita berbicara tentang proksi Java dinamis - java.lang.reflect.Proxy).

Java mendukung proxy dinamis di luar kotak, tetapi dukungan ini hanya terbatas pada antarmuka. Karena API kami harus bekerja dengan kacang apa pun, termasuk kelas nyata, saya akan menggunakan alat yang hebat, ByteBuddy, bukan Proxy. ByteBuddy menyediakan DSL sederhana untuk membuat kelas dengan cepat, itulah yang kami butuhkan.

Mari kita mulai dengan mendefinisikan antarmuka yang memungkinkan kita untuk menyimpan dan mengambil nama properti yang diekstrak dari Referensi Metode.

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

Sekarang kita menggunakan ByteBuddy untuk secara terprogram membuat kelas proksi yang kompatibel dengan jenis minat kepada kita (misalnya: Alamat) dan mengimplementasikan 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 ); } } 

Kode mungkin tampak sedikit membingungkan, jadi izinkan saya menjelaskannya. Pertama kita mendapatkan instance ByteBuddy (1), yang merupakan titik masuk DSL. Ini digunakan untuk membuat tipe dinamis yang memperluas tipe yang diinginkan (jika itu adalah kelas) atau mewarisi objek dan mengimplementasikan tipe yang diinginkan (jika itu adalah antarmuka) (2).

Kemudian, kami menunjukkan bahwa tipe mengimplementasikan antarmuka PropertyNameCapturer dan menambahkan bidang untuk menyimpan nama properti yang diinginkan (3). Kemudian kami mengatakan bahwa panggilan ke semua metode harus dicegat oleh PropertyNameCapturingInterceptor (4). Hanya setPropertyName () dan getPropertyName () (dari antarmuka PropertyNameCapturer) yang dapat mengakses properti real yang dibuat sebelumnya (5). Akhirnya, kelas dibuat, dimuat (6) dan instantiated (7).

Itu saja yang kita butuhkan untuk membuat tipe proxy, terima kasih ByteBuddy, ini dapat dilakukan dalam beberapa baris kode. Sekarang mari kita lihat pencegat panggilan:

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

Metode intersept () menerima Metode yang dipanggil dan target untuk panggilan (1). @Origin dan @This digunakan untuk menentukan parameter yang sesuai sehingga ByteBuddy dapat menghasilkan panggilan intersep () yang benar dalam proxy dinamis.

Perhatikan bahwa tidak ada ketergantungan ketat dari reseptor pada tipe ByteBuddy, karena ByteBuddy hanya digunakan untuk membuat proksi dinamis, tetapi tidak ketika menggunakannya.

Dengan memanggil getPropertyName() (4) kita bisa mendapatkan nama properti yang sesuai dengan Referensi Metode yang diteruskan, dan menyimpannya di PropertyNameCapturer (2). Jika metode ini bukan pengambil, maka kode akan melempar pengecualian (5). Jenis pengembalian pengambil tidak masalah, jadi kami mengembalikan nol dengan mempertimbangkan jenis properti (3).

Sekarang kita siap untuk mendapatkan nama properti dalam metode 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(); //      } 

Setelah menerapkan fungsi ke proksi yang dibuat, kami melemparkan tipe ke PropertyNameCapturer dan mendapatkan nama dari Metode.

Jadi, menggunakan beberapa keajaiban menghasilkan bytecode, kami menggunakan Referensi Metode dari Java 8 untuk meniru properti literal.

Tentu saja, jika kita memiliki literal properti nyata dalam bahasa tersebut, kita semua akan lebih baik. Saya bahkan akan mengizinkan bekerja dengan properti pribadi dan, mungkin, properti dapat dirujuk dari anotasi. Literal real estate akan lebih rapi (tanpa awalan "get") dan tidak akan terlihat seperti retasan.

Dari penerjemah


Perlu dicatat bahwa bahasa-bahasa bagus lain sudah mendukung (atau hampir) mekanisme serupa:


Jika Anda tiba-tiba menggunakan proyek Lombok dengan Java, maka generator waktu kompilasi bytecode ditulis untuk itu.

Terinspirasi oleh pendekatan yang dijelaskan dalam artikel, hamba Anda yang sederhana mengumpulkan perpustakaan kecil yang mengimplementasikan nameOfProperty () untuk Java 8:

Kode sumber
Binari

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


All Articles