JavaRush /Java Blog /Random-ID /Sekilas tentang injeksi ketergantungan atau "Apa lagi itu...
Viacheslav
Level 3

Sekilas tentang injeksi ketergantungan atau "Apa lagi itu CDI?"

Dipublikasikan di grup Random-ID
Fondasi di mana kerangka kerja paling populer saat ini dibangun adalah injeksi ketergantungan. Saya sarankan untuk melihat apa yang dikatakan spesifikasi CDI tentang hal ini, kemampuan dasar apa yang kita miliki dan bagaimana kita dapat menggunakannya.
Perjalanan singkat ke injeksi ketergantungan atau

Perkenalan

Saya ingin mengabdikan ulasan singkat ini untuk hal seperti CDI. Apa ini? CDI adalah singkatan dari Konteks dan Injeksi Ketergantungan. Ini adalah spesifikasi Java EE yang menjelaskan Injeksi Ketergantungan dan konteksnya. Untuk informasinya, Anda dapat melihat website http://cdi-spec.org . Karena CDI adalah sebuah spesifikasi (deskripsi cara kerjanya, sekumpulan antarmuka), kita juga memerlukan implementasi untuk menggunakannya. Salah satu implementasinya adalah Weld - http://weld.cdi-spec.org/ Untuk mengelola dependensi dan membuat proyek, kita akan menggunakan Maven - https://maven.apache.org Jadi, kita sudah menginstal Maven, sekarang kita akan memahaminya secara praktik, sehingga tidak memahami yang abstrak. Untuk melakukan ini, kita akan membuat proyek menggunakan Maven. Mari kita buka baris perintah (di Windows, Anda dapat menggunakan Win+R untuk membuka jendela "Run" dan menjalankan cmd) dan meminta Maven melakukan semuanya untuk kita. Untuk ini, Maven memiliki konsep yang disebut arketipe: Maven Archetype .
Perjalanan singkat ke injeksi ketergantungan atau
Setelah itu, pada pertanyaan “ Choose a number or apply filter ” dan “ Choose org.apache.maven.archetypes:maven-archetype-quickstart version ” cukup tekan Enter. Selanjutnya, masukkan pengidentifikasi proyek, yang disebut GAV (lihat Panduan Konvensi Penamaan ).
Perjalanan singkat ke injeksi ketergantungan atau
Setelah pembuatan proyek berhasil, kita akan melihat tulisan “BUILD SUCCESS”. Sekarang kita dapat membuka proyek kita di IDE favorit kita.

Menambahkan CDI ke Proyek

Dalam pendahuluan, kita melihat bahwa CDI memiliki situs web yang menarik - http://www.cdi-spec.org/ . Ada bagian download yang berisi tabel yang berisi data yang kita butuhkan:
Perjalanan singkat ke injeksi ketergantungan atau
Di sini kita dapat melihat bagaimana Maven menjelaskan fakta bahwa kita menggunakan CDI API dalam proyek tersebut. API adalah antarmuka pemrograman aplikasi, yaitu beberapa antarmuka pemrograman. Kami bekerja dengan antarmuka tanpa khawatir tentang apa dan bagaimana cara kerjanya di balik antarmuka ini. API adalah arsip jar yang akan mulai kita gunakan dalam proyek kita, artinya proyek kita mulai bergantung pada toples ini. Oleh karena itu, CDI API untuk proyek kami adalah ketergantungan. Di Maven, sebuah proyek dijelaskan dalam file POM.xml ( POM - Project Object Model ). Dependensi dijelaskan di blok dependensi, yang mana kita perlu menambahkan entri baru:
<dependency>
	<groupId>javax.enterprise</groupId>
	<artifactId>cdi-api</artifactId>
	<version>2.0</version>
</dependency>
Seperti yang mungkin Anda ketahui, kami tidak menentukan cakupan dengan nilai yang diberikan. Mengapa ada perbedaan seperti itu? Cakupan ini berarti seseorang akan memberi kita ketergantungan. Ketika aplikasi berjalan di server Java EE, itu berarti server akan menyediakan semua teknologi JEE yang diperlukan untuk aplikasi tersebut. Demi kesederhanaan ulasan ini, kami akan bekerja di lingkungan Java SE, oleh karena itu tidak ada yang akan memberi kami ketergantungan ini. Anda dapat membaca lebih lanjut tentang Lingkup Ketergantungan di sini: " Lingkup Ketergantungan ". Oke, sekarang kita memiliki kemampuan untuk bekerja dengan antarmuka. Tapi kita juga perlu implementasi. Seingat kita, kita akan menggunakan Weld. Menariknya, ketergantungan yang berbeda diberikan di mana-mana. Tapi kami akan mengikuti dokumentasinya. Oleh karena itu, mari kita baca " 18.4.5. Mengatur Classpath " dan lakukan seperti yang dikatakan:
<dependency>
	<groupId>org.jboss.weld.se</groupId>
	<artifactId>weld-se-core</artifactId>
	<version>3.0.5.Final</version>
</dependency>
Penting bahwa versi baris ketiga Weld mendukung CDI 2.0. Oleh karena itu, kami dapat mengandalkan API versi ini. Sekarang kita siap untuk menulis kode.
Perjalanan singkat ke injeksi ketergantungan atau

Menginisialisasi wadah CDI

CDI adalah sebuah mekanisme. Seseorang harus mengendalikan mekanisme ini. Seperti yang telah kita baca di atas, pengelola seperti itu adalah sebuah wadah. Oleh karena itu, kita perlu membuatnya; itu sendiri tidak akan muncul di lingkungan SE. Mari tambahkan yang berikut ini ke metode utama kita:
public static void main(String[] args) {
	SeContainerInitializer initializer = SeContainerInitializer.newInstance();
	initializer.addPackages(App.class.getPackage());
	SeContainer container = initializer.initialize();
}
Kami membuat wadah CDI secara manual karena... Kami bekerja di lingkungan SE. Dalam proyek tempur biasa, kode berjalan di server, yang menyediakan berbagai teknologi untuk kode tersebut. Dengan demikian, jika server menyediakan CDI, berarti server tersebut sudah memiliki wadah CDI dan kita tidak perlu menambahkan apa pun. Namun untuk keperluan tutorial ini, kita akan menggunakan lingkungan SE. Selain itu, wadahnya ada di sini, dengan jelas dan dapat dimengerti. Mengapa kita membutuhkan wadah? Wadah di dalamnya berisi kacang-kacangan (CDI beans).
Perjalanan singkat ke injeksi ketergantungan atau

Kacang CDI

Jadi, kacang. Apa itu tempat CDI? Ini adalah kelas Java yang mengikuti beberapa aturan. Aturan-aturan ini dijelaskan dalam spesifikasi, dalam bab " 2.2. Jenis kelas apa yang dimaksud dengan kacang? ". Mari tambahkan CDI bean ke paket yang sama dengan kelas App:
public class Logger {
    public void print(String message) {
        System.out.println(message);
    }
}
Sekarang kita dapat memanggil kacang ini dari mainmetode kita:
Logger logger = container.select(Logger.class).get();
logger.print("Hello, World!");
Seperti yang Anda lihat, kami tidak membuat kacang menggunakan kata kunci baru. Kami bertanya pada container CDI: "Container CDI. Saya benar-benar membutuhkan sebuah instance dari kelas Logger, tolong berikan kepada saya." Metode ini disebut " Pencarian ketergantungan ", yaitu mencari ketergantungan. Sekarang mari kita buat kelas baru:
public class DateSource {
    public String getDate() {
        return new Date().toString();
    }
}
Kelas primitif yang mengembalikan representasi teks suatu tanggal. Sekarang mari tambahkan keluaran tanggal ke pesan:
public class Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(dateSource.getDate() + " : " + message);
    }
}
Anotasi @Inject yang menarik telah muncul. Sebagaimana dinyatakan dalam bab " 4.1. Titik injeksi " dari dokumentasi las cdi, dengan menggunakan anotasi ini kami mendefinisikan Titik Injeksi. Dalam bahasa Rusia, ini bisa dibaca sebagai “poin implementasi”. Mereka digunakan oleh wadah CDI untuk memasukkan dependensi saat membuat instance kacang. Seperti yang Anda lihat, kami tidak memberikan nilai apa pun ke bidang dateSource. Alasannya adalah fakta bahwa wadah CDI mengizinkan di dalam kacang CDI (hanya kacang yang dipakai sendiri, yaitu yang dikelolanya) untuk menggunakan “ Injeksi Ketergantungan ”. Ini adalah cara lain dari Inversion of Control , sebuah pendekatan di mana ketergantungan dikontrol oleh orang lain, bukan oleh kita yang secara eksplisit membuat objek. Injeksi ketergantungan dapat dilakukan melalui metode, konstruktor, atau bidang. Untuk lebih jelasnya lihat spesifikasi CDI bab " 5.5. Injeksi Ketergantungan ". Prosedur untuk menentukan apa yang perlu diterapkan disebut resolusi typesafe, itulah yang perlu kita bicarakan.
Perjalanan singkat ke injeksi ketergantungan atau

Resolusi nama atau resolusi Typesafe

Biasanya, antarmuka digunakan sebagai tipe objek yang akan diimplementasikan, dan wadah CDI sendiri yang menentukan implementasi mana yang akan dipilih. Ini berguna karena berbagai alasan, yang akan kita bahas. Jadi kami memiliki antarmuka logger:
public interface Logger {
    void print(String message);
}
Dia mengatakan bahwa jika kita memiliki beberapa logger, kita dapat mengirim pesan ke sana dan ia akan menyelesaikan tugasnya - log. Bagaimana dan di mana tidak menjadi perhatian dalam kasus ini. Sekarang mari kita buat implementasi untuk logger:
public class SystemOutLogger implements Logger {
    @Inject
    private DateSource dateSource;

    public void print(String message) {
        System.out.println(message);
    }
}
Seperti yang Anda lihat, ini adalah logger yang menulis ke System.out. Luar biasa. Sekarang, metode utama kita akan berfungsi seperti sebelumnya. Logger logger = container.select(Logger.class).get(); Baris ini masih akan diterima oleh logger. Dan keindahannya adalah kita hanya perlu mengetahui antarmukanya, dan wadah CDI sudah memikirkan implementasinya untuk kita. Katakanlah kita memiliki implementasi kedua yang seharusnya mengirim log ke suatu tempat ke penyimpanan jarak jauh:
public class NetworkLogger implements Logger {
    @Override
    public void print(String message) {
        System.out.println("Send log message to remote log system");
    }
}
Jika sekarang kita menjalankan kode kita tanpa perubahan, kita akan mendapatkan kesalahan karena Wadah CDI melihat dua implementasi antarmuka dan tidak dapat memilih di antara keduanya: org.jboss.weld.exceptions.AmbiguousResolutionException: WELD-001335: Ambiguous dependencies for type Logger Apa yang harus dilakukan? Ada beberapa variasi yang tersedia. Yang paling sederhana adalah anotasi @Vetoed untuk kacang CDI sehingga wadah CDI tidak menganggap kelas ini sebagai kacang CDI. Namun ada pendekatan yang jauh lebih menarik. Kacang CDI dapat ditandai sebagai "alternatif" menggunakan anotasi @Alternativeyang dijelaskan dalam bab " 4.7. Alternatif " dalam dokumentasi Weld CDI. Apa artinya? Artinya kecuali kami secara eksplisit mengatakan untuk menggunakannya, maka itu tidak akan dipilih. Ini adalah versi alternatif dari kacang. Mari tandai kacang NetworkLogger sebagai @Alternative dan kita dapat melihat bahwa kode tersebut dieksekusi lagi dan digunakan oleh SystemOutLogger. Untuk mengaktifkan alternatifnya, kita harus memiliki file beans.xml . Mungkin timbul pertanyaan: " beans.xml, di mana saya harus menempatkan Anda? " Oleh karena itu, mari kita letakkan file tersebut dengan benar:
Perjalanan singkat ke injeksi ketergantungan atau
Segera setelah kami memiliki file ini, artefak dengan kode kami akan disebut “ Arsip kacang eksplisit ”. Sekarang kami memiliki 2 konfigurasi terpisah: perangkat lunak dan xml. Masalahnya adalah mereka akan memuat data yang sama. Misalnya, definisi kacang DataSource akan dimuat 2 kali dan program kita akan crash saat dijalankan, karena Wadah CDI akan menganggapnya sebagai 2 kacang yang terpisah (walaupun sebenarnya keduanya adalah kelas yang sama, yang dipelajari dua kali oleh wadah CDI). Untuk menghindari hal ini ada 2 pilihan:
  • hapus baris initializer.addPackages(App.class.getPackage())dan tambahkan indikasi alternatif ke file xml:
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd">
    <alternatives>
        <class>ru.javarush.NetworkLogger</class>
    </alternatives>
</beans>
  • tambahkan atribut bean-discovery-modedengan nilai " none " ke elemen akar kacang dan tentukan alternatif secara terprogram:
initializer.addPackages(App.class.getPackage());
initializer.selectAlternatives(NetworkLogger.class);
Jadi, dengan menggunakan alternatif CDI, wadah dapat menentukan biji mana yang akan dipilih. Menariknya, jika container CDI mengetahui beberapa alternatif untuk antarmuka yang sama, maka kita dapat mengetahuinya dengan menunjukkan prioritas menggunakan anotasi @Priority(Sejak CDI 1.1).
Perjalanan singkat ke injeksi ketergantungan atau

Kualifikasi

Kita juga harus membahas konsep kualifikasi. Kualifikasi ditunjukkan dengan anotasi di atas kacang dan menyaring pencarian kacang. Dan sekarang lebih detailnya. Menariknya, setiap kacang CDI memiliki setidaknya satu kualifikasi - @Any. Jika kita tidak menentukan kualifikasi APAPUN di atas kacang, tetapi wadah CDI itu sendiri menambahkan @Anykualifikasi lain ke kualifikasi - @Default. Jika kita menentukan sesuatu (misalnya, secara eksplisit menentukan @Any), maka kualifikasi @Default tidak akan ditambahkan secara otomatis. Namun keindahan dari kualifikasi adalah Anda dapat membuat kualifikasi Anda sendiri. Kualifikasinya hampir tidak berbeda dengan anotasi, karena intinya, ini hanyalah anotasi yang ditulis dengan cara khusus. Misalnya, Anda dapat memasukkan Enum untuk jenis protokol:
public enum ProtocolType {
    HTTP, HTTPS
}
Selanjutnya kita dapat membuat kualifikasi yang akan mempertimbangkan tipe ini:
@Qualifier
@Retention(RUNTIME)
@Target({METHOD, FIELD, PARAMETER, TYPE})
public @interface Protocol {
    ProtocolType value();
    @Nonbinding String comment() default "";
}
Perlu dicatat bahwa bidang yang ditandai sebagai @Nonbindingtidak mempengaruhi penentuan kualifikasi. Sekarang Anda perlu menentukan kualifikasi. Ini ditunjukkan di atas jenis kacang (sehingga CDI mengetahui cara mendefinisikannya) dan di atas Titik Injeksi (dengan anotasi @Inject sehingga Anda memahami kacang mana yang harus dicari untuk injeksi di tempat ini). Misalnya, kita bisa menambahkan beberapa kelas dengan qualifier. Untuk mempermudah, untuk artikel ini kami akan melakukannya di dalam NetworkLogger:
public interface Sender {
	void send(byte[] data);
}

@Protocol(ProtocolType.HTTP)
public static class HTTPSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTP");
	}
}

@Protocol(ProtocolType.HTTPS)
public static class HTTPSSender implements Sender{
	public void send(byte[] data) {
		System.out.println("sended via HTTPS");
	}
}
Dan kemudian ketika kita melakukan Inject, kita akan menentukan qualifier yang akan mempengaruhi kelas mana yang akan digunakan:
@Inject
@Protocol(ProtocolType.HTTPS)
private Sender sender;
Hebat, bukan?) Kelihatannya indah, tapi tidak jelas kenapa. Sekarang bayangkan hal berikut:
Protocol protocol = new Protocol() {
	@Override
	public Class<? extends Annotation> annotationType() {
		return Protocol.class;
	}
	@Override
	public ProtocolType value() {
		String value = "HTTP";
		return ProtocolType.valueOf(value);
	}
};
container.select(NetworkLogger.Sender.class, protocol).get().send(null);
Dengan cara ini kita bisa mengganti nilai yang didapat sehingga bisa dihitung secara dinamis. Misalnya bisa diambil dari beberapa pengaturan. Kemudian kita dapat mengubah implementasinya bahkan dengan cepat, tanpa mengkompilasi ulang atau memulai ulang program/server. Ini menjadi jauh lebih menarik, bukan? )
Perjalanan singkat ke injeksi ketergantungan atau

Produser

Fitur lain yang berguna dari CDI adalah produsen. Ini adalah metode khusus (ditandai dengan anotasi khusus) yang dipanggil ketika beberapa kacang meminta injeksi ketergantungan. Rincian lebih lanjut dijelaskan dalam dokumentasi, di bagian " 2.2.3. Metode produsen ". Contoh paling sederhana:
@Produces
public Integer getRandomNumber() {
	return new Random().nextInt(100);
}
Sekarang, ketika menginjeksi ke dalam field bertipe Integer, metode ini akan dipanggil dan nilai akan diperoleh darinya. Disini kita harus segera memahami bahwa ketika kita melihat kata kunci baru, kita harus segera memahami bahwa ini BUKAN CDI bean. Artinya, sebuah instance dari kelas Random tidak akan menjadi CDI bean hanya karena ia berasal dari sesuatu yang mengontrol container CDI (dalam hal ini, produser).
Perjalanan singkat ke injeksi ketergantungan atau

Pencegat

Pencegat adalah pencegat yang “mengganggu” pekerjaan. Di CDI hal ini dilakukan dengan cukup jelas. Mari kita lihat bagaimana kita bisa melakukan logging menggunakan interpreter (atau interseptor). Pertama, kita perlu menjelaskan pengikatan pada pencegat. Seperti banyak hal lainnya, ini dilakukan dengan menggunakan anotasi:
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface ConsoleLog {
}
Hal utama di sini adalah ini merupakan pengikatan untuk interseptor ( @InterceptorBinding), yang akan diwarisi oleh extends ( @InterceptorBinding). Sekarang mari kita tulis pencegat itu sendiri:
@Interceptor
@ConsoleLog
public class LogInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("Invocation method: " + ic.getMethod().getName());
        return ic.proceed();
    }
}
Anda dapat membaca lebih lanjut tentang bagaimana pencegat ditulis dalam contoh dari spesifikasi: " 1.3.6. Contoh pencegat ". Yang harus kita lakukan hanyalah menyalakan inerceptornya. Untuk melakukannya, tentukan anotasi pengikatan di atas metode yang dijalankan:
@ConsoleLog
public void print(String message) {
Dan sekarang detail lain yang sangat penting. Pencegat dinonaktifkan secara default dan harus diaktifkan dengan cara yang sama seperti alternatifnya. Misalnya, dalam file beans.xml :
<interceptors>
	<class>ru.javarush.LogInterceptor</class>
</interceptors>
Seperti yang Anda lihat, ini cukup sederhana.
Perjalanan singkat ke injeksi ketergantungan atau

Acara & Pengamat

CDI juga menyediakan model peristiwa dan pengamat. Di sini semuanya tidak sejelas dengan pencegat. Jadi, Acara dalam hal ini bisa berupa kelas apa pun; tidak ada yang khusus yang diperlukan untuk deskripsinya. Misalnya:
public class LogEvent {
    Date date = new Date();
    public String getDate() {
        return date.toString();
    }
}
Sekarang seseorang harus menunggu acara tersebut:
public class LogEventListener {
    public void logEvent(@Observes LogEvent event){
        System.out.println("Message Date: " + event.getDate());
    }
}
Hal utama di sini adalah menentukan anotasi @Observes, yang menunjukkan bahwa ini bukan hanya sebuah metode, tetapi metode yang harus dipanggil sebagai hasil pengamatan peristiwa bertipe LogEvent. Nah, sekarang kita membutuhkan seseorang yang mau mengawasi:
public class LogObserver {
    @Inject
    private Event<LogEvent> event;
    public void observe(LogEvent logEvent) {
        event.fire(logEvent);
    }
}
Kami memiliki satu metode yang akan memberi tahu container bahwa suatu peristiwa telah terjadi untuk jenis peristiwa LogEvent. Sekarang yang tersisa hanyalah menggunakan pengamat. Misalnya, di NetworkLogger kita dapat menambahkan suntikan pengamat kita:
@Inject
private LogObserver observer;
Dan dalam metode print kita dapat memberitahukan kepada pengamat bahwa kita mempunyai event baru:
public void print(String message) {
	observer.observe(new LogEvent());
Penting untuk diketahui bahwa peristiwa dapat diproses dalam satu atau beberapa thread. Untuk pemrosesan asinkron, gunakan metode .fireAsync(bukan .fire) dan anotasi @ObservesAsync(bukan @Observes). Misalnya, jika semua event dieksekusi di thread yang berbeda, maka jika 1 thread memunculkan Exception, maka thread lainnya akan dapat melakukan tugasnya untuk event lainnya. Anda dapat membaca lebih lanjut tentang event di CDI, seperti biasa, di spesifikasi, di bab " 10. Event ".
Perjalanan singkat ke injeksi ketergantungan atau

Dekorator

Seperti yang kita lihat di atas, berbagai pola desain dikumpulkan di bawah sayap CDI. Dan ini satu lagi - seorang dekorator. Ini adalah hal yang sangat menarik. Mari kita lihat kelas ini:
@Decorator
public abstract class LoggerDecorator implements Logger {
    public final static String ANSI_GREEN = "\u001B[32m";
    public static final String ANSI_RESET = "\u001B[0m";

    @Inject
    @Delegate
    private Logger delegate;

    @Override
    public void print(String message) {
        delegate.print(ANSI_GREEN + message + ANSI_RESET);
    }
}
Dengan mendeklarasikannya sebagai dekorator, kami mengatakan bahwa ketika implementasi Logger apa pun digunakan, “add-on” ini akan digunakan, yang mengetahui implementasi sebenarnya, yang disimpan di bidang delegasi (karena ditandai dengan anotasi @Delegate). Dekorator hanya dapat dikaitkan dengan kacang CDI, yang bukan merupakan pencegat atau dekorator. Contohnya juga dapat dilihat pada spesifikasi: " 1.3.7. Contoh dekorator ". Dekorator, seperti pencegat, harus dihidupkan. Misalnya, di beans.xml :
<decorators>
	<class>ru.javarush.LoggerDecorator</class>
</decorators>
Untuk lebih jelasnya lihat referensi las : " Bab 10. Dekorator ".

Lingkaran kehidupan

Kacang memiliki siklus hidupnya sendiri. Ini terlihat seperti ini:
Perjalanan singkat ke injeksi ketergantungan atau
Seperti yang Anda lihat dari gambar, kami memiliki apa yang disebut callback siklus hidup. Ini adalah anotasi yang akan memberitahu wadah CDI untuk memanggil metode tertentu pada tahap tertentu dalam siklus hidup kacang. Misalnya:
@PostConstruct
public void init() {
	System.out.println("Inited");
}
Metode ini akan dipanggil ketika kacang CDI dipakai oleh sebuah container. Hal yang sama akan terjadi dengan @PreDestroy ketika kacang dihancurkan ketika tidak diperlukan lagi. Tak heran jika akronim CDI mengandung huruf C - Konteks. Kacang dalam CDI bersifat kontekstual, artinya siklus hidupnya bergantung pada konteks keberadaannya dalam wadah CDI. Untuk memahami hal ini dengan lebih baik, Anda harus membaca bagian spesifikasi “ 7. Siklus hidup contoh kontekstual ”. Perlu juga diketahui bahwa container itu sendiri memiliki siklus hidup, yang dapat Anda baca di “ Peristiwa siklus hidup container ”.
Perjalanan singkat ke injeksi ketergantungan atau

Total

Di atas kita melihat puncak gunung es yang disebut CDI. CDI adalah bagian dari spesifikasi JEE dan digunakan di lingkungan JavaEE. Yang pakai Spring tidak pakai CDI, tapi DI, artinya spesifikasinya sedikit berbeda. Namun mengetahui dan memahami hal di atas, Anda dapat dengan mudah berubah pikiran. Mengingat Spring mendukung anotasi dari dunia CDI (Inject yang sama). Bahan tambahan: #Viacheslav
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION