Alih-alih "Didedikasikan untuk ..."
Tugas yang dijelaskan di bawah ini tidak inovatif atau sangat berguna, perusahaan tempat saya bekerja tidak akan menerima untung, tetapi saya akan mendapat bonus.
Tetapi tugas ini, dan karena itu harus diselesaikan.
Intro
Dalam artikel Anda akan sering bertemu kata Lombok, saya meminta para pembenci untuk tidak terburu-buru mengambil kesimpulan.
Saya tidak akan "tenggelam" untuk Lombok atau ketidakhadirannya, saya, seperti Geralt Sapkovsky, mencoba bersikap netral, dan saya bisa membaca kode dengan atau tanpa Lombok dengan tenang dan tanpa gemetar di abad ini.
Tetapi pada proyek saat ini, perpustakaan yang disebutkan ada, dan sesuatu memberitahu saya bahwa proyek kami bukan satu-satunya.
Jadi disini.
Terakhir kali di java pasti ada kecenderungan ke arah annotashki. Untuk kemuliaan konsep gagal cepat, parameter metode sering dijelaskan dengan penjelasan @NonNull (sehingga jika ada yang salah, maka ia pergi).
Ada banyak opsi impor untuk ini (atau anotasi serupa dalam ideologi), tetapi, karena sudah menjadi jelas, kami akan fokus pada versi
import lombok.NonNull;
Jika Anda menggunakan anotasi ini (atau yang serupa), maka Anda memiliki kontrak yang perlu Anda periksa dengan tes dan setiap penganalisa kode statis akan memberi tahu Anda (Sonar memberi tahu Anda dengan tepat).
Menguji anotasi ini dengan uji unit cukup sederhana, masalahnya adalah tes tersebut akan berlipat ganda dalam proyek Anda dengan kecepatan kelinci di musim semi, dan kelinci, seperti yang Anda tahu, melanggar prinsip KERING.
Dalam artikel ini kami akan menulis kerangka uji kecil untuk menguji kontrak anotasi @NonNull (dan agar Sonar tidak bersinar di mata Anda dengan lampu merah yang tidak menyenangkan).
PS Nama lagu itu terinspirasi oleh lagu dari band PowerWolf, yang dimainkan (oleh astaga) ketika saya menulis nama (dalam aslinya, namanya terdengar lebih positif)
Tubuh utama
Awalnya, kami menguji anotasi seperti ini:
@Test void methodNameWithNullArgumentThrowException() { try { instance.getAnyType(null); fail("Exception not thrown"); } catch (final NullPointerException e) { assertNotNull(e); } }
Mereka memanggil metode dan lulus nol sebagai parameter yang dijelaskan dengan penjelasan @NonNull.
Mereka mendapat NPE dan puas (Sonar juga senang).
Kemudian mereka mulai melakukan hal yang sama, tetapi dengan pernyataan yang lebih modis. Throw yang bekerja melalui Pemasok (kami suka lambdas):
@TestUnitRepeatOnce void methodNameWithNullArgumentThrowException() { assertThrows(NullPointerException.class, () -> instance.getAnyType(null)); }
Bergaya. Fashionable. Pemuda
Tampaknya mungkin untuk selesai, anotasi diuji, lalu apa lagi?
Masalahnya (bukan itu masalahnya, tapi tetap saja) dari metode tes ini "muncul" ketika suatu hari saya menulis tes untuk suatu metode, itu berhasil, dan kemudian saya perhatikan bahwa tidak ada penjelasan pada @NonNull pada parameter.
Dapat dimengerti: Anda memanggil metode pengujian, sambil tidak menggambarkan perilaku kelas moque, sampai kapan () / lalu (). Utas pelaksana memasuki metode dengan aman, di suatu tempat di dalamnya menangkap NPE, pada objek yang tidak dikunci (atau dikunci, tetapi tanpa saat () / lalu ()), dan terhempas, namun dengan NPE, seperti yang Anda peringatkan, yang berarti pengujian berwarna hijau
Ternyata kami sedang menguji dalam kasus ini, bukan anotasi, tetapi tidak jelas apa. Dengan tes yang bekerja dengan benar, kita bahkan tidak harus masuk lebih dalam ke metode (jatuh di ambang pintu).
@NonNull anotasi Lombok memiliki satu fitur: jika kita beralih dari NPE ke anotasi, nama parameter ditulis ke galat.
Kami akan terlibat dalam hal ini, setelah kami jatuh dari NPE, kami juga akan memeriksa teks stacktrace, seperti ini:
exception.getCause().getMessage().equals(parameter.getName())
Dan jika tiba-tiba ...Jika Lombok tiba-tiba menyegarkan dan berhenti menulis nama parameter yang menerima null di stacktrace, maka kami akan meninjau kuliah Andrei Pangin tentang
JVM TI dan menulis plug-in untuk JVM, di mana kami mentransfer nama parameter.
Semuanya sepertinya tidak ada apa-apanya, sekarang kita benar-benar memeriksa apa yang dibutuhkan, tetapi masalah "kelinci" tidak terpecahkan.
Saya ingin memiliki alat yang bisa dikatakan, misalnya, seperti ini:
@TestUnitRepeatOnce @SneakyThrows void nonNullAnnotationTest() { assertNonNullAnnotation(YourPerfectClass.class); }
dan dia sendiri akan pergi dan memindai semua metode publik dari kelas yang ditentukan dan memeriksa semua parameter @NonNull mereka dengan tes.
Anda akan berkata, dapatkan refleksi, dan periksa apakah metode @NonNull aktif dan apakah ada peluru di dalamnya nol.
Semua tidak akan menjadi apa-apa, tetapi RetentionPolicy tidak sama.
Semua anotasi memiliki parameter RetentionPolicy, yang dapat terdiri dari 3 jenis: SOURCE, CLASS dan RUNTIME, sehingga Lombok memiliki RetentionPolicy.SOURCE secara default, yang berarti bahwa anotasi ini tidak terlihat di Runtime dan Anda tidak akan menemukannya melalui refleksi.
Dalam proyek kami, semua parameter metode publik diberi keterangan (tidak termasuk primitif), jika dipahami bahwa parameter tidak boleh nol, jika kebalikannya tersirat, maka parameter tersebut dijelaskan oleh pegas @Nullable. Anda dapat terlibat dalam hal ini, kami akan mencari semua metode publik, dan semua parameter di dalamnya yang tidak ditandai @Nullable dan bukan primitif.
Kami bermaksud bahwa untuk semua kasus lain, @NonNull anotasi harus pada parameter.
Untuk kenyamanan, jika memungkinkan, kami akan menyebarkan logika dengan metode pribadi, untuk memulai kami akan mendapatkan semua metode publik:
private List<Method> getPublicMethods(final Class clazz) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(METHOD_FILTER) .collect(toList()); }
di mana METHOD_FILTER adalah predikat reguler di mana kami mengatakan bahwa:
- Metode harus publik
- Seharusnya tidak sintetik (dan ini terjadi ketika Anda memiliki metode dengan parameter mentah)
- Seharusnya tidak abstrak (tentang kelas abstrak secara terpisah dan di bawah)
- Nama metode tidak boleh sama (dalam kasus semacam orang jahat memutuskan untuk mengisi kelas dengan yang sama dengan ditimpa () pada input kerangka kerja POJO kami)
Setelah kami mendapatkan semua metode yang kami butuhkan, kami mulai memilahnya dalam satu lingkaran,
jika metode ini tidak memiliki parameter sama sekali, maka ini bukan kandidat kami:
if (method.getParameterCount() == 0) { continue; }
Jika ada parameter, kita perlu memahami apakah mereka beranotasi @NonNull (harus lebih tepat, menurut
logika- metode publik
- tidak bisa dihapus
- tidak primitif
Untuk melakukan ini, buat peta dan letakkan parameter kami di dalamnya sesuai dengan urutan dalam metode, dan sebaliknya, kita beri tanda yang menyatakan apakah anotasi @NonNull harus di atas atau tidak:
int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap<Integer, Boolean>(); for (val parameter : parameterCurrentMethodArray) { if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) { notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; } else { notNullAnnotationParameterMap.put(index++, false); } } if (nonNullAnnotationCount == 0) { continue; }
peta ini berguna bagi kita untuk kemudian memanggil metode dan meneruskannya nol ke semua parameter dengan penjelasan @NonNull pada gilirannya, dan bukan hanya yang pertama.
Parameter nonNullAnnotationCount menghitung berapa banyak parameter dalam metode yang harus dianotasi @NonNull, itu akan menentukan jumlah interaksi integrasi panggilan untuk setiap metode.
Ngomong-ngomong, jika tidak ada anotasi @NonNull (ada parameter, tetapi semuanya primitif atau @Nullable), maka tidak ada yang perlu dibicarakan:
if (nonNullAnnotationCount == 0) { continue; }
Kami memiliki peta parameter. Kita tahu berapa kali memanggil metode dan di mana posisi nol, masalahnya kecil (seperti yang saya pikir naif tanpa memahami), kita perlu membuat instance kelas dan memanggil metode pada mereka.
Masalah dimulai ketika Anda menyadari betapa berbedanya sebuah instance: itu bisa menjadi kelas privat, itu bisa menjadi kelas dengan satu konstruktor default, dengan satu konstruktor dengan parameter, dengan konstruktor ini dan itu, kelas abstrak, antarmuka (dengan metode default, yang juga publik dan yang juga perlu diuji).
Dan ketika kita membangun instance dengan cara kait atau dengan penjahat, kita perlu melewatkan parameter ke metode invoke dan di sini juga membentang: bagaimana cara membuat instance dari kelas akhir? dan Enum? dan primitif? dan array primitif (yang juga merupakan objek dan juga dapat dijelaskan).
Baiklah, mari kita lakukan secara berurutan.
Kasus pertama adalah kelas dengan satu konstruktor pribadi:
if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(clazz, invokeMethodParameterArray); makeErrorMessage(method); }
lalu kita panggil metode invoke kita, berikan clazz yang datang dari luar ke tes dan array parameter di mana null sudah dibebankan ke posisi pertama dengan bendera untuk anotasi @NonNull (ingat, di atas kita buat peta @ NonNulls) kita mulai jalankan dalam satu lingkaran dan buat array parameter, secara bergantian ubah posisi parameter nol, dan beri tanda flag sebelum memanggil metode, sehingga pada integrasi berikutnya parameter lainnya menjadi nol.
Dalam kode, tampilannya seperti ini:
val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) { if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) { currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; } else { mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); } }
Opsi pertama instantiasi diselesaikan.
Antarmuka lebih lanjut, Anda tidak dapat mengambil dan membuat instance antarmuka (bahkan tidak memiliki konstruktor).
Oleh karena itu, dengan antarmuka akan seperti ini:
if (INTERFACE_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(createInstanceByDynamicProxy(clazz, invokeMethodParameterArray), invokeMethodParameterArray); makeErrorMessage(method); }
createInstanceByDynamicProxy memungkinkan kita membuat instance di kelas jika mengimplementasikan setidaknya satu antarmuka, atau itu sendiri antarmuka
Nuansaperlu diingat bahwa di sini secara mendasar yang menghubungkan implementasi kelas, tipe antarmuka (dan bukan beberapa yang sebanding) adalah penting, di mana ada metode yang Anda implementasikan di kelas target, jika tidak contoh akan mengejutkan Anda dengan tipenya
tapi di dalamnya seperti ini:
private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) { return newProxyInstance( currentThread().getContextClassLoader(), new Class[]{clazz}, (proxy, method1, args) -> { Constructor<Lookup> constructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; } ); }
MenyapuNgomong-ngomong, ada juga beberapa garu di sini, saya tidak ingat yang mana lagi, ada banyak, tetapi Anda perlu membuat proxy melalui Lookup.class
Contoh berikutnya (favorit saya) adalah kelas abstrak. Dan di sini proxy dinamis tidak akan membantu kita lagi, karena jika kelas abstrak mengimplementasikan beberapa jenis antarmuka, maka ini jelas bukan tipe yang kita inginkan. Dan seperti itu, kita tidak bisa mengambil dan membuat newInstance () dari kelas abstrak. Di sini CGLIB akan datang ke bantuan kami, lib pegas yang membuat proxy berdasarkan pewarisan, tetapi masalahnya adalah, kelas target harus memiliki konstruktor default (tanpa parameter)
GosipMeskipun dinilai oleh gosip di Internet sejak Musim Semi 4, CGLIB dapat bekerja tanpanya, dan karenanya: Jadi itu tidak bekerja!
Opsi untuk membuat instance kelas abstrak adalah sebagai berikut:
if (isAbstract(clazz.getModifiers())) { createInstanceByCGLIB(clazz, method, invokeMethodParameterArray); makeErrorMessage(); }
makeErrorMessage (), yang sudah terlihat dalam contoh kode, menjatuhkan tes, jika kita memanggil metode dengan parameter @NonNull beranotasi lewat nol dan tidak jatuh, maka tes tidak bekerja, maka tes tidak bekerja, Anda harus turun.
Untuk pemetaan parameter, kami memiliki satu metode umum yang dapat memetakan dan mengunci parameter konstruktor dan metode, seperti ini:
private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException { if (isFinal(parameter.getType().getModifiers())) { if (parameter.getType().isEnum()) { methodParam[index] = Enum.valueOf( (Class<Enum>) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); } else if (parameter.getType().isPrimitive()) { mappingPrimitiveName(parameter, methodParam, index); } else if (parameter.getType().getTypeName().equals("byte[]")) { methodParam[index] = new byte[0]; } else { methodParam[index] = parameter.getType().newInstance(); } } else { methodParam[index] = mock(parameter.getType()); } }
Perhatikan pembuatan Enum (cherry on the cake), secara umum, Anda tidak bisa hanya mengambil dan membuat Enum.
Di sini untuk parameter akhir pemetaan Anda sendiri, untuk non-final Anda sendiri, dan kemudian hanya dalam teks (kode).
Nah, setelah kami membuat parameter untuk konstruktor dan untuk metode, kami membentuk contoh kami:
val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); } notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method);
Kita sudah tahu pasti bahwa karena kita telah mencapai tahap kode ini, itu berarti bahwa kita memiliki setidaknya satu konstruktor, kita dapat mengambil apa pun untuk membuat sebuah instance, jadi kita mengambil yang pertama yang kita lihat, melihat apakah ia memiliki parameter dalam konstruktor dan jika tidak, maka panggil seperti ini:
method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray);
Nah, jika ada yang seperti ini:
method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray);
Ini adalah logika yang terjadi dalam metode createAndInvoke () yang Anda lihat sedikit lebih tinggi.
Versi lengkap dari kelas tes di bawah spoiler, saya tidak mengunggah ke git, seperti yang saya tulis di proyek kerja, tetapi sebenarnya itu hanya satu kelas yang dapat diwarisi dalam tes Anda dan digunakan.
Kode sumber public class TestUtil { private static final Predicate<Method> METHOD_FILTER = method -> isPublic(method.getModifiers()) && isFalse(method.isSynthetic()) && isFalse(isAbstract(method.getModifiers())) && isFalse(method.getName().equals("equals")); private static final Predicate<Class> ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER = clazz -> clazz.getConstructors().length == 0 && isFalse(clazz.isInterface()); private static final Predicate<Class> INTERFACE_FILTER = clazz -> clazz.getConstructors().length == 0; private static final BiPredicate<Exception, Parameter> LOMBOK_ERROR_FILTER = (exception, parameter) -> isNull(exception.getCause().getMessage()) || isFalse(exception.getCause().getMessage().equals(parameter.getName())); protected void assertNonNullAnnotation(final Class clazz) throws Throwable { for (val method : getPublicMethods(clazz)) { if (method.getParameterCount() == 0) { continue; } int nonNullAnnotationCount = 0; int index = 0; val parameterCurrentMethodArray = method.getParameters(); val notNullAnnotationParameterMap = new HashMap<Integer, Boolean>(); for (val parameter : parameterCurrentMethodArray) { if (isNull(parameter.getAnnotation(Nullable.class)) && isFalse(parameter.getType().isPrimitive())) { notNullAnnotationParameterMap.put(index++, true); nonNullAnnotationCount++; } else { notNullAnnotationParameterMap.put(index++, false); } } if (nonNullAnnotationCount == 0) { continue; } for (int j = 0; j < nonNullAnnotationCount; j++) { val invokeMethodParameterArray = new Object[parameterCurrentMethodArray.length]; boolean hasNullParameter = false; int currentNullableIndex = 0; for (int i = 0; i < invokeMethodParameterArray.length; i++) { if (notNullAnnotationParameterMap.get(i) && isFalse(hasNullParameter)) { currentNullableIndex = i; invokeMethodParameterArray[i] = null; hasNullParameter = true; } else { mappingParameter(parameterCurrentMethodArray[i], invokeMethodParameterArray, i); } } try { if (ONLY_ONE_PRIVATE_CONSTRUCTOR_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(clazz, invokeMethodParameterArray); makeErrorMessage(method); } if (INTERFACE_FILTER.test(clazz)) { notNullAnnotationParameterMap.put(currentNullableIndex, false); method.invoke(createInstanceByDynamicProxy(clazz, invokeMethodParameterArray), invokeMethodParameterArray); makeErrorMessage(method); } if (isAbstract(clazz.getModifiers())) { createInstanceByCGLIB(clazz, method, invokeMethodParameterArray); makeErrorMessage(); } val firstFindConstructor = clazz.getConstructors()[0]; val constructorParameterArray = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParameterArray.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParameterArray, i); } notNullAnnotationParameterMap.put(currentNullableIndex, false); createAndInvoke(clazz, method, invokeMethodParameterArray, firstFindConstructor, constructorParameterArray); makeErrorMessage(method); } catch (final Exception e) { if (LOMBOK_ERROR_FILTER.test(e, parameterCurrentMethodArray[currentNullableIndex])) { makeErrorMessage(method); } } } } } @SneakyThrows private void createAndInvoke( final Class clazz, final Method method, final Object[] invokeMethodParameterArray, final Constructor firstFindConstructor, final Object[] constructorParameterArray ) { if (firstFindConstructor.getParameters().length == 0) { method.invoke(spy(clazz.getConstructors()[0].newInstance()), invokeMethodParameterArray); } else { method.invoke(spy(clazz.getConstructors()[0].newInstance(constructorParameterArray)), invokeMethodParameterArray); } } @SneakyThrows private void createInstanceByCGLIB(final Class clazz, final Method method, final Object[] invokeMethodParameterArray) { MethodInterceptor handler = (obj, method1, args, proxy) -> proxy.invoke(clazz, args); if (clazz.getConstructors().length > 0) { val firstFindConstructor = clazz.getConstructors()[0]; val constructorParam = new Object[firstFindConstructor.getParameters().length]; for (int i = 0; i < constructorParam.length; i++) { mappingParameter(firstFindConstructor.getParameters()[i], constructorParam, i); } for (val constructor : clazz.getConstructors()) { if (constructor.getParameters().length == 0) { val proxy = Enhancer.create(clazz, handler); method.invoke(proxy.getClass().newInstance(), invokeMethodParameterArray); } } } } private Object createInstanceByDynamicProxy(final Class clazz, final Object[] invokeMethodParameterArray) { return newProxyInstance( currentThread().getContextClassLoader(), new Class[]{clazz}, (proxy, method1, args) -> { Constructor<Lookup> constructor = Lookup.class .getDeclaredConstructor(Class.class); constructor.setAccessible(true); constructor.newInstance(clazz) .in(clazz) .unreflectSpecial(method1, clazz) .bindTo(proxy) .invokeWithArguments(invokeMethodParameterArray); return null; } ); } private void makeErrorMessage() { fail(" @NonNull DefaultConstructor "); } private void makeErrorMessage(final Method method) { fail(" " + method.getName() + " @NonNull"); } private List<Method> getPublicMethods(final Class clazz) { return Arrays.stream(clazz.getDeclaredMethods()) .filter(METHOD_FILTER) .collect(toList()); } private void mappingParameter(final Parameter parameter, final Object[] methodParam, final int index) throws InstantiationException, IllegalAccessException { if (isFinal(parameter.getType().getModifiers())) { if (parameter.getType().isEnum()) { methodParam[index] = Enum.valueOf( (Class<Enum>) (parameter.getType()), parameter.getType().getEnumConstants()[0].toString() ); } else if (parameter.getType().isPrimitive()) { mappingPrimitiveName(parameter, methodParam, index); } else if (parameter.getType().getTypeName().equals("byte[]")) { methodParam[index] = new byte[0]; } else { methodParam[index] = parameter.getType().newInstance(); } } else { methodParam[index] = mock(parameter.getType()); } } private void mappingPrimitiveName(final Parameter parameter, final Object[] methodParam, final int index) { val name = parameter.getType().getName(); if ("long".equals(name)) { methodParam[index] = 0L; } else if ("int".equals(name)) { methodParam[index] = 0; } else if ("byte".equals(name)) { methodParam[index] = (byte) 0; } else if ("short".equals(name)) { methodParam[index] = (short) 0; } else if ("double".equals(name)) { methodParam[index] = 0.0d; } else if ("float".equals(name)) { methodParam[index] = 0.0f; } else if ("boolean".equals(name)) { methodParam[index] = false; } else if ("char".equals(name)) { methodParam[index] = 'A'; } } }
Kesimpulan
Kode ini berfungsi dan menguji anotasi dalam proyek nyata, saat ini hanya ada satu opsi yang memungkinkan, ketika semua yang dikatakan dapat diciutkan.
Nyatakan setter Lombock di kelas (jika ada spesialis yang tidak mengatur setter di kelas Pojo, meskipun itu tidak terjadi) dan bidang yang ditetapkan setter tidak akan final.
Maka kerangka kerja akan dengan ramah mengatakan bahwa ada metode publik, dan ia memiliki parameter di mana tidak ada penjelasan @NonNull, solusinya sederhana: nyatakan setter secara eksplisit dan anotasi parameternya berdasarkan konteks logika @ NonNull / @ Nullable.
Perhatikan bahwa jika Anda ingin saya terikat dengan nama parameter metode dalam pengujian Anda (atau sesuatu yang lain), dalam Runtime nama-nama variabel dalam metode tidak tersedia secara default, Anda akan menemukan arg [0] dan arg [1], dll. .
Untuk mengaktifkan tampilan nama metode di Runtime, gunakan plugin Maven:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>${maven.compiler.plugin.version}</version> <configuration> <source>${compile.target.source}</source/> <target>${compile.target.source}</target> <encoding>${project.build.sourceEncoding}</encoding> <compilerArgs><arg>-parameters</arg></compilerArgs> </configuration> </plugin>
dan khususnya kunci ini:
<compilerArgs><arg>-parameters</arg></compilerArgs>
Saya harap Anda tertarik.