
Kita semua senang menangkap kesalahan pada tahap kompilasi, alih-alih pengecualian runtime. Cara termudah untuk memperbaikinya adalah bahwa kompiler itu sendiri menunjukkan semua tempat yang perlu diperbaiki. Meskipun sebagian besar masalah hanya dapat dideteksi ketika program dimulai, kami masih berusaha melakukan ini sesegera mungkin.
Dalam blok inisialisasi kelas, dalam konstruktor objek, pada panggilan pertama metode, dll. Dan kadang-kadang kita beruntung, dan bahkan pada tahap kompilasi kita cukup tahu untuk memeriksa program untuk kesalahan tertentu.
Pada artikel ini saya ingin berbagi pengalaman menulis satu tes tersebut. Lebih tepatnya, membuat anotasi yang dapat membuang kesalahan, seperti yang dilakukan kompiler. Menilai oleh kenyataan bahwa tidak ada begitu banyak informasi tentang topik ini di RuNet, situasi bahagia yang dijelaskan di atas tidak sering.
Saya akan menjelaskan algoritma verifikasi umum, serta semua langkah dan nuansa yang saya habiskan waktu dan sel saraf.
Pernyataan masalah
Di bagian ini, saya akan memberikan contoh menggunakan anotasi ini. Jika Anda sudah tahu apa yang ingin Anda lakukan, Anda dapat melewatinya dengan aman. Saya yakin ini tidak akan mempengaruhi kelengkapan presentasi.
Sekarang kita akan berbicara lebih banyak tentang meningkatkan keterbacaan kode daripada memperbaiki bug. Sebuah contoh, bisa dikatakan, dari kehidupan, atau lebih tepatnya dari proyek hobi saya.
Misalkan ada kelas UnitManager, yang, pada kenyataannya, adalah kumpulan unit. Ini memiliki metode untuk menambah, menghapus, mendapatkan unit, dll. Saat menambahkan unit baru, manajer memberinya id. Generasi id didelegasikan ke kelas RotateCounter, yang mengembalikan angka dalam rentang yang diberikan. Dan ada masalah kecil, RotateCounter tidak bisa tahu apakah id yang dipilih gratis. Menurut prinsip inversi dependensi, Anda dapat membuat antarmuka, dalam kasus saya ini adalah RotateCounter.IClient, yang memiliki metode tunggal isValueFree (), yang menerima id dan mengembalikan true jika id gratis. Dan UnitManager mengimplementasikan antarmuka ini, membuat instance dari RotateCounter dan meneruskannya sendiri sebagai klien.
Saya melakukan hal itu. Tetapi, setelah membuka sumber UnitManager beberapa hari setelah menulis, saya menjadi orang yang mudah bodoh setelah melihat metode isValueFree (), yang tidak benar-benar cocok dengan logika untuk UnitManager. Akan lebih sederhana jika memungkinkan untuk menentukan antarmuka mana yang mengimplementasikan metode ini. Sebagai contoh, dalam C #, dari mana saya datang ke Jawa, implementasi antarmuka eksplisit membantu untuk mengatasi masalah ini. Dalam hal ini, pertama, Anda dapat memanggil metode hanya dengan gips eksplisit ke antarmuka. Kedua, dan yang lebih penting dalam hal ini, nama antarmuka (dan tanpa pengubah akses) secara eksplisit ditunjukkan dalam tanda tangan metode, misalnya:
IClient.isValueFree(int value) { }
Salah satu solusinya adalah menambahkan anotasi dengan nama antarmuka yang mengimplementasikan metode ini. Sesuatu seperti
@Override
, hanya dengan antarmuka. Saya setuju, Anda dapat menggunakan kelas batin anonim. Dalam kasus ini, sama seperti di C #, metode tidak bisa dipanggil pada objek, dan Anda dapat langsung melihat antarmuka mana yang diimplementasikan. Tetapi, ini akan meningkatkan jumlah kode, oleh karena itu, menurunkan keterbacaan. Ya, dan Anda perlu mendapatkannya dari kelas - buat getter atau bidang publik (toh, tidak ada overload pernyataan cor di Jawa juga). Bukan pilihan yang buruk, tapi saya tidak suka itu.
Pada awalnya, saya berpikir bahwa di Jawa, seperti dalam C #, anotasi adalah kelas lengkap dan dapat diwarisi darinya. Dalam hal ini, Anda hanya perlu membuat anotasi yang diwarisi dari
@Override
. Tetapi ini tidak benar, dan saya harus terjun ke dunia cek yang menakjubkan dan menakutkan pada tahap kompilasi.
Kode sampel UnitManager public class Unit { private int id; } public class UnitManager implements RotateCounter.IClient { private final Unit[] units; private final RotateCounter idGenerator; public UnitManager(int size) { units = new Unit[size]; idGenerator = new RotateCounter(0, size, this); } public void addUnit(Unit unit) { int id = idGenerator.findFree(); units[id] = unit; } @Implement(RotateCounter.IClient.class) public boolean isValueFree(int value) { return units[value] == null; } public void removeUnit(int id) { units[id] = null; } } public class RotateCounter { private final IClient client; private int next; private int minValue; private int maxValue; public RotateCounter(int minValue, int maxValue, IClient client) { this.client = client; this.minValue = minValue; this.maxValue = maxValue; next = minValue; } public int incrementAndGet() { int current = next; if (next >= maxValue) { next = minValue; return current; } next++; return current; } public int range() { return maxValue - minValue + 1; } public int findFree() { int range = range(); int trysCounter = 0; int id; do { if (++trysCounter > range) { throw new IllegalStateException("No free values."); } id = incrementAndGet(); } while (!client.isValueFree(id)); return id; } public static interface IClient { boolean isValueFree(int value); } }
Sedikit teori
Saya akan segera melakukan reservasi, semua metode di atas adalah turunan, oleh karena itu, untuk singkatnya, saya akan menunjukkan nama metode dengan nama jenis dan tanpa parameter:
<_>.<_>()
.
Pemrosesan elemen pada tahap kompilasi melibatkan kelas prosesor khusus. Ini adalah kelas-kelas yang mewarisi dari
javax.annotation.processing.AbstractProcessor
(Anda hanya dapat mengimplementasikan antarmuka
javax.annotation.processing.Processor
). Anda dapat membaca lebih lanjut tentang prosesor di
sini dan di
sini . Metode yang paling penting di dalamnya adalah proses. Di mana kita bisa mendapatkan daftar semua elemen beranotasi dan melakukan pemeriksaan yang diperlukan.
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { return false; }
Pada awalnya, sungguh naif, saya berpikir bahwa bekerja dengan tipe pada tahap kompilasi dilakukan dalam hal refleksi, tapi ... tidak. Semuanya berdasarkan elemen di sana.
Elemen (
javax.lang.model.element.Element ) - antarmuka utama untuk bekerja dengan sebagian besar elemen struktural bahasa. Suatu elemen memiliki turunan yang lebih tepat menentukan sifat-sifat elemen tertentu (untuk detail, lihat di
sini ):
package ds.magic.example.implement;
TypeMirror (
javax.lang.model.type.TypeMirror ) adalah sesuatu seperti Kelas <?> Dikembalikan oleh metode getClass (). Misalnya, mereka dapat dibandingkan untuk mengetahui apakah jenis elemen cocok. Anda bisa mendapatkannya menggunakan metode
Element.asType()
. Tipe ini juga mengembalikan beberapa jenis operasi, seperti
TypeElement.getSuperclass()
atau
TypeElement.getInterfaces()
.
Jenis (
javax.lang.model.util.Types ) - Saya menyarankan Anda untuk melihat lebih dekat pada kelas ini. Anda dapat menemukan banyak hal menarik di sana. Intinya, ini adalah seperangkat utilitas untuk bekerja dengan tipe. Misalnya, ini memungkinkan Anda untuk mendapatkan kembali TypeElement dari TypeMirror.
private TypeElement asTypeElement(TypeMirror typeMirror) { return (TypeElement)processingEnv.getTypeUtils().asElement(typeMirror); }
TypeKind (
javax.lang.model.type.TypeKind ) - sebuah enumerasi yang memungkinkan Anda untuk mengklarifikasi informasi jenis, periksa apakah jenisnya adalah array (ARRAY), tipe khusus (DEKLAR), jenis variabel (TYPEVAR), dll. Anda bisa mendapatkannya melalui
TypeMirror.getKind()
ElementKind (
javax.lang.model.element.ElementKind ) - enumerasi, memungkinkan Anda untuk mengklarifikasi informasi tentang elemen, memeriksa apakah elemen tersebut adalah paket (PAKET), kelas (KELAS), metode (METHOD), metode (METHOD), antarmuka (ANTARMUKA), dll.
Nama (
javax.lang.model.element.Name ) - antarmuka untuk bekerja dengan nama elemen, dapat diperoleh melalui
Element.getSimpleName()
.
Pada dasarnya, jenis ini cukup bagi saya untuk menulis algoritma verifikasi.
Saya ingin mencatat fitur menarik lainnya. Implementasi antarmuka Elemen di Eclipse ada di paket org.eclipse, misalnya, elemen yang mewakili metode bertipe
org.eclipse.jdt.internal.compiler.apt.model.ExecutableElementImpl
. Ini memberi saya ide bahwa antarmuka ini diimplementasikan oleh setiap IDE secara mandiri.
Algoritma validasi
Pertama, Anda perlu membuat anotasi itu sendiri. Sudah banyak yang ditulis tentang itu (misalnya, di
sini ), jadi saya tidak akan membahasnya secara rinci. Saya hanya bisa mengatakan bahwa untuk contoh kita, kita perlu menambahkan dua anotasi
@Target
dan
@Retention
. Yang pertama menunjukkan bahwa anotasi kami hanya dapat diterapkan pada metode, dan yang kedua bahwa anotasi hanya akan ada dalam kode sumber.
Anotasi harus ditentukan antarmuka mana yang mengimplementasikan metode beranotasi (metode di mana anotasi diterapkan). Ini dapat dilakukan dengan dua cara: tentukan nama lengkap antarmuka dengan string, misalnya
@Implement("com.ds.IInterface")
, atau lulus langsung kelas antarmuka:
@Implement(IInterface.class)
. Cara kedua jelas lebih baik. Dalam hal ini, kompiler akan memantau nama antarmuka yang benar. Omong-omong, jika Anda memanggil
nilai anggota ini
(), maka saat menambahkan anotasi ke metode, Anda tidak perlu menentukan nama parameter ini secara eksplisit.
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Implement { Class<?> value(); }
Kemudian kesenangan dimulai - penciptaan prosesor. Dalam metode proses, kami mendapatkan daftar semua elemen beranotasi. Kemudian kita mendapatkan anotasi itu sendiri dan artinya - antarmuka yang ditentukan. Secara umum, kerangka kerja kelas prosesor terlihat seperti ini:
@SupportedAnnotationTypes({"ds.magic.annotations.compileTime.Implement"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ImplementProcessor extends AbstractProcessor { private Types typeUtils; @Override public void init(ProcessingEnvironment procEnv) { super.init(procEnv); typeUtils = this.processingEnv.getTypeUtils(); } @Override public boolean process(Set<? extends TypeElement> annos, RoundEnvironment env) { Set<? extends Element> annotatedElements = env.getElementsAnnotatedWith(Implement.class); for(Element annotated : annotatedElements) { Implement annotation = annotatedElement.getAnnotation(Implement.class); TypeMirror interfaceMirror = getValueMirror(annotation); TypeElement interfaceType = asTypeElement(interfaceMirror);
Saya ingin mencatat bahwa Anda tidak bisa hanya mendapatkan dan mendapatkan nilai anotasi. Saat Anda mencoba memanggil
annotation.value()
,
MirroredTypeException akan dilempar, tetapi dari sana Anda bisa mendapatkan TypeMirror. Metode curang ini, serta penerimaan nilai yang benar, saya temukan di
sini :
private TypeMirror getValueMirror(Implement annotation) { try { annotation.value(); } catch(MirroredTypeException e) { return e.getTypeMirror(); } return null; }
Pemeriksaan itu sendiri terdiri dari tiga bagian, jika setidaknya salah satu dari mereka gagal, maka Anda perlu menampilkan pesan kesalahan dan melanjutkan ke anotasi berikutnya. Omong-omong, Anda dapat menampilkan pesan kesalahan menggunakan metode berikut:
private void printError(String message, Element annotatedElement) { Messager messager = processingEnv.getMessager(); messager.printMessage(Kind.ERROR, message, annotatedElement); }
Langkah pertama adalah memeriksa apakah anotasi nilai adalah antarmuka. Semuanya sederhana di sini:
if (interfaceType.getKind() != ElementKind.INTERFACE) { String name = Implement.class.getSimpleName(); printError("Value of @" + name + " must be an interface", annotated); continue; }
Selanjutnya, Anda perlu memeriksa apakah kelas tempat metode beranotasi benar-benar mengimplementasikan antarmuka yang ditentukan. Awalnya saya dengan bodoh menerapkan tes ini dengan tangan saya. Tetapi kemudian, dengan menggunakan saran yang bagus, saya melihat
Type dan menemukan metode
Types.isSubtype()
sana, yang akan memeriksa seluruh pohon warisan dan mengembalikan true jika antarmuka yang ditentukan ada. Yang penting, ini dapat bekerja dengan tipe generik, tidak seperti opsi pertama.
TypeElement enclosingType = (TypeElement)annotatedElement.getEnclosingElement(); if (!typeUtils.isSubtype(enclosingType.asType(), interfaceMirror)) { Name className = enclosingType.getSimpleName(); Name interfaceName = interfaceType.getSimpleName(); printError(className + " must implemet " + interfaceName, annotated); continue; }
Terakhir, Anda perlu memastikan bahwa antarmuka memiliki metode dengan tanda tangan yang sama dengan yang dianotasi. Saya ingin menggunakan metode
Types.isSubsignature()
, tetapi, sayangnya, itu tidak berfungsi dengan benar jika metode memiliki parameter tipe. Jadi kami menyingsingkan lengan baju kami dan menulis semua cek dengan tangan kami. Dan kami memiliki tiga dari mereka lagi. Nah, lebih tepatnya, tanda tangan metode terdiri dari tiga bagian: nama metode, jenis nilai balik, dan daftar parameter. Anda harus melalui semua metode antarmuka dan menemukan satu yang melewati ketiga pemeriksaan. Akan menyenangkan untuk tidak lupa bahwa metode ini dapat diwarisi dari antarmuka lain dan secara rekursif melakukan pemeriksaan yang sama untuk antarmuka yang mendasarinya.
Panggilan harus ditempatkan di akhir loop dalam metode proses, seperti ini:
if (!haveMethod(interfaceType, (ExecutableElement)annotatedElement)) { Name name = interfaceType.getSimpleName(); printError(name + " don't have \"" + annotated + "\" method", annotated); continue; }
Dan metode haveMethod () sendiri terlihat seperti ini:
private boolean haveMethod(TypeElement interfaceType, ExecutableElement method) { Name methodName = method.getSimpleName(); for (Element interfaceElement : interfaceType.getEnclosedElements()) { if (interfaceElement instanceof ExecutableElement) { ExecutableElement interfaceMethod = (ExecutableElement)interfaceElement;
Lihat masalahnya? Tidak Dan dia ada di sana. Faktanya adalah bahwa saya tidak dapat menemukan cara untuk mendapatkan parameter tipe aktual untuk antarmuka umum. Sebagai contoh, saya memiliki kelas yang mengimplementasikan antarmuka
Predicate :
MyPredicate implements Predicate<String> { @Implement(Predicate.class) boolean test(String t) { return false; } }
Saat menganalisis metode di kelas, tipe parameternya adalah
String
, dan di antarmuka itu adalah
T
, dan semua upaya untuk mendapatkan
String
bukannya mengarah ke apa pun. Pada akhirnya, saya datang dengan tidak ada yang lebih baik daripada hanya mengabaikan parameter tipe. Pemeriksaan akan diteruskan dengan parameter tipe aktual apa pun, meskipun tidak cocok. Untungnya, kompiler akan melempar kesalahan jika metode tidak memiliki implementasi standar dan tidak diimplementasikan di kelas dasar. Tapi tetap saja, jika ada yang tahu bagaimana menyiasati ini, saya akan sangat berterima kasih atas petunjuknya.
Terhubung ke Eclipse
Secara pribadi, saya suka Eclipce dan dalam latihan saya hanya menggunakannya. Oleh karena itu, saya akan menjelaskan cara menghubungkan prosesor ke IDE ini. Agar Eclipse dapat melihat prosesor, Anda harus memasukkannya ke dalam. JAR yang terpisah, di mana anotasi itu sendiri juga akan. Dalam hal ini, Anda perlu membuat folder
META-INF / services di proyek dan membuat file
javax.annotation.processing.Processor di sana dan menunjukkan nama lengkap kelas prosesor:
ds.magic.annotations.compileTime.ImplementProcessor
, dalam kasus saya. Untuk berjaga-jaga, saya akan memberikan tangkapan layar, tetapi ketika tidak ada yang berhasil untuk saya, saya hampir mulai berbuat dosa pada struktur proyek.

Selanjutnya, kumpulkan .JAR dan hubungkan ke proyek Anda, pertama sebagai pustaka biasa, sehingga anotasi terlihat di kode. Lalu kami menghubungkan prosesor (di
sini lebih rinci). Untuk melakukan ini, buka
properti proyek dan pilih:
- Java Compiler -> Annotation Processing dan centang kotak "Enable annotation processing".
- Kompiler Java -> Pemrosesan Anotasi -> Jalur Pabrik centang kotak centang "Aktifkan pengaturan spesifik proyek". Kemudian klik Tambah JAR ... dan pilih file JAR yang dibuat sebelumnya.
- Setuju untuk membangun kembali proyek.
Ringkasan
Semua bersama dan dalam proyek Eclipse dapat dilihat di
GitHub . Pada saat penulisan, hanya ada dua kelas, jika anotasi dapat disebut sebagai: Implement.java dan ImplementProcessor.java. Saya pikir Anda sudah menebak tujuan mereka.
Mungkin penjelasan ini tampaknya tidak berguna bagi sebagian orang. Mungkin itu. Tetapi secara pribadi, saya sendiri menggunakannya sebagai ganti
@Override
, ketika nama metode tidak cocok dengan tujuan kelas. Dan sejauh ini, saya tidak punya keinginan untuk menyingkirkannya. Secara umum, saya membuat anotasi untuk diri saya sendiri, dan tujuan artikel itu adalah untuk menunjukkan apa yang saya serang. Saya harap saya berhasil. Terima kasih atas perhatian anda
PS. Terima kasih kepada pengguna
ohotNik_alex dan
Comdiv untuk bantuan mereka dalam memperbaiki bug.