Mengapa Spring's ApplicationContext.getBean dianggap buruk?

270

Saya mengajukan pertanyaan umum Musim Semi: Auto-cast Spring Beans dan meminta banyak orang menjawab bahwa menelepon Spring ApplicationContext.getBean()harus dihindari sebanyak mungkin. Mengapa demikian?

Bagaimana lagi saya bisa mendapatkan akses ke kacang yang saya konfigurasikan Spring untuk membuat?

Saya menggunakan Spring dalam aplikasi non-web dan telah merencanakan mengakses ApplicationContextobjek bersama seperti yang dijelaskan oleh LiorH .

Amandemen

Saya menerima jawaban di bawah ini, tetapi inilah pilihan lain oleh Martin Fowler yang membahas manfaat Dependency Injection vs. menggunakan Service Locator (yang pada dasarnya sama dengan memanggil dibungkus ApplicationContext.getBean()).

Sebagian, Fowler menyatakan, " Dengan locator layanan, kelas aplikasi memintanya [layanan] secara eksplisit melalui pesan ke locator. Dengan injeksi tidak ada permintaan eksplisit, layanan muncul di kelas aplikasi - maka inversi kontrol. Inversion of control adalah fitur umum dari frameworks, tapi itu adalah sesuatu yang mahal, cenderung sulit untuk dipahami dan mengarah ke masalah ketika Anda mencoba untuk debug. Jadi secara keseluruhan saya lebih suka menghindarinya [Inversion of Control ] kecuali saya membutuhkannya. Ini bukan untuk mengatakan itu adalah hal yang buruk, hanya saja saya pikir itu perlu membenarkan dirinya atas alternatif yang lebih mudah. "

Vinnie
sumber

Jawaban:

202

Saya menyebutkan hal ini dalam komentar pada pertanyaan lain, tetapi seluruh gagasan Inversion of Control adalah agar tidak ada kelas Anda yang tahu atau peduli bagaimana mereka mendapatkan objek yang mereka andalkan . Ini membuatnya mudah untuk mengubah jenis implementasi dari ketergantungan yang Anda gunakan kapan saja. Ini juga membuat kelas mudah untuk diuji, karena Anda dapat memberikan implementasi tiruan dari dependensi. Akhirnya, ini membuat kelas lebih sederhana dan lebih fokus pada tanggung jawab inti mereka.

Panggilan ApplicationContext.getBean()bukanlah Pembalikan Kontrol! Meskipun masih mudah untuk mengubah implementasi apa yang dikonfigurasi untuk nama kacang yang diberikan, kelas sekarang bergantung langsung pada Spring untuk memberikan ketergantungan itu dan tidak bisa mendapatkannya dengan cara lain. Anda tidak bisa hanya membuat implementasi tiruan Anda sendiri di kelas tes dan meneruskannya sendiri. Ini pada dasarnya mengalahkan tujuan Spring sebagai wadah injeksi ketergantungan.

Di mana pun Anda ingin mengatakan:

MyClass myClass = applicationContext.getBean("myClass");

Anda seharusnya, misalnya, mendeklarasikan metode:

public void setMyClass(MyClass myClass) {
   this.myClass = myClass;
}

Dan kemudian dalam konfigurasi Anda:

<bean id="myClass" class="MyClass">...</bean>

<bean id="myOtherClass" class="MyOtherClass">
   <property name="myClass" ref="myClass"/>
</bean>

Pegas kemudian akan secara otomatis menyuntikkan myClasske myOtherClass.

Nyatakan semuanya dengan cara ini, dan pada dasarnya semuanya memiliki sesuatu seperti:

<bean id="myApplication" class="MyApplication">
   <property name="myCentralClass" ref="myCentralClass"/>
   <property name="myOtherCentralClass" ref="myOtherCentralClass"/>
</bean>

MyApplicationadalah kelas paling sentral, dan setidaknya tergantung secara tidak langsung pada setiap layanan lain dalam program Anda. Saat bootstrap, dalam mainmetode Anda, Anda dapat menelepon applicationContext.getBean("myApplication")tetapi Anda tidak perlu menelepon di getBean()tempat lain!

ColinD
sumber
3
Adakah yang berhubungan dengan ini yang bekerja hanya dengan anotasi saat membuat new MyOtherClass()objek? Aku tahu tentang @ Autowired, tapi aku hanya pernah digunakan pada bidang dan istirahat pada new MyOtherClass()..
Tim
71
Tidak benar bahwa ApplicationContext.getBean () bukan IoC. Niether apakah itu wajib untuk memiliki semua kelas Anda instantiated oleh Spring. Itu adalah dogma yang tidak pantas. Jika ApplicationContext itu sendiri disuntikkan, maka tidak apa-apa untuk memintanya membuat instance kacang dengan cara ini - dan kacang yang dibuatnya bisa merupakan implementasi yang berbeda berdasarkan ApplicationContext yang awalnya disuntikkan. Sebagai contoh, saya memiliki skenario di mana saya secara dinamis membuat instance kacang baru berdasarkan nama kacang yang tidak dikenal pada waktu kompilasi tetapi cocok dengan salah satu implementasi yang didefinisikan dalam file spring.xml saya.
Alex Worden
3
Setuju dengan Alex, saya memiliki masalah yang sama, di mana kelas pabrik hanya akan tahu kacang mana atau implementasi yang akan digunakan saat dijalankan melalui interaksi pengguna, saya pikir ini adalah di mana antarmuka ContextAware masuk
Ben
3
@elbek: applicationContext.getBeanbukan injeksi ketergantungan: ini mengakses kerangka kerja secara langsung, menggunakannya sebagai pencari lokasi layanan .
ColinD
6
@herman: Saya tidak tahu tentang Spring karena saya belum menggunakannya dalam waktu yang lama, tetapi di JSR-330 / Guice / Dagger, Anda akan melakukan ini dengan menyuntikkan Provider<Foo>alih - alih a Foodan memanggil provider.get()setiap kali Anda membutuhkan contoh baru. Tidak ada referensi ke wadah itu sendiri, dan Anda dapat dengan mudah membuat Provideruntuk pengujian.
ColinD
64

Alasan untuk memilih Penentu Lokasi Layanan daripada Inversion of Control (IoC) adalah:

  1. Service Locator jauh, lebih mudah bagi orang lain untuk mengikuti kode Anda. IoC 'ajaib' tetapi programmer pemeliharaan harus memahami konfigurasi Spring yang berbelit-belit dan semua lokasi yang beragam untuk mengetahui bagaimana Anda menghubungkan kabel ke objek Anda.

  2. IoC sangat buruk untuk debugging masalah konfigurasi. Di kelas aplikasi tertentu, aplikasi tidak akan mulai ketika salah konfigurasi dan Anda mungkin tidak mendapatkan kesempatan untuk menelusuri apa yang terjadi dengan debugger.

  3. IoC terutama berbasis XML (Anotasi meningkatkan banyak hal tetapi masih ada banyak XML di luar sana). Itu berarti pengembang tidak dapat bekerja pada program Anda kecuali mereka tahu semua tag ajaib yang ditentukan oleh Spring. Tidak cukup baik untuk mengenal Jawa lagi. Hal ini menghambat programmer yang kurang berpengalaman (mis. Sebenarnya desain yang buruk untuk menggunakan solusi yang lebih rumit ketika solusi yang lebih sederhana, seperti Service Locator, akan memenuhi persyaratan yang sama). Plus, dukungan untuk mendiagnosis masalah XML jauh lebih lemah daripada dukungan untuk masalah Java.

  4. Injeksi ketergantungan lebih cocok untuk program yang lebih besar. Sebagian besar waktu kompleksitas tambahan tidak sepadan.

  5. Seringkali Spring digunakan jika Anda "mungkin ingin mengubah implementasinya nanti". Ada cara lain untuk mencapai ini tanpa kerumitan Spring IoC.

  6. Untuk aplikasi web (Java EE WARs) konteks Spring secara efektif terikat pada waktu kompilasi (kecuali jika Anda ingin operator untuk mengitari konteks dalam perang yang meledak). Anda dapat membuat Spring menggunakan file properti, tetapi dengan servlets file properti harus berada di lokasi yang ditentukan sebelumnya, yang berarti Anda tidak dapat menggunakan beberapa servlet dari waktu yang sama di kotak yang sama. Anda dapat menggunakan Spring dengan JNDI untuk mengubah properti pada waktu permulaan servlet, tetapi jika Anda menggunakan JNDI untuk parameter yang dapat dimodifikasi administrator, kebutuhan untuk Spring itu sendiri berkurang (karena JNDI secara efektif adalah Service Locator).

  7. Dengan Spring Anda dapat kehilangan Kontrol program jika Spring mengirim ke metode Anda. Ini nyaman dan berfungsi untuk banyak jenis aplikasi, tetapi tidak semua. Anda mungkin perlu mengontrol aliran program saat Anda perlu membuat tugas (utas dll) selama inisialisasi atau membutuhkan sumber daya yang dapat dimodifikasi yang tidak diketahui Spring saat konten terikat ke WAR Anda.

Musim semi sangat baik untuk manajemen transaksi dan memiliki beberapa keunggulan. Hanya saja IoC dapat melakukan rekayasa berlebihan dalam banyak situasi dan memperkenalkan kompleksitas yang tidak beralasan bagi para pengelola. Jangan menggunakan IoC secara otomatis tanpa memikirkan cara untuk tidak menggunakannya terlebih dahulu.

Moa
sumber
7
Plus - ServiceLocator Anda selalu dapat menggunakan IoC dari Spring, mengabstraksi kode Anda agar tidak bergantung pada Spring, dikotori dengan anotasi Spring, dan magik yang tidak dapat diuraikan. Saya baru-baru ini porting sekelompok kode ke GoogleAppEngine di mana Spring tidak didukung. Saya berharap saya menyembunyikan semua IOC di belakang ServiceFactory di tempat pertama!
Alex Worden
IoC mendorong model domain anemia, yang saya benci. Entity bean membutuhkan cara untuk mencari layanan mereka sehingga mereka dapat menerapkan perilaku mereka sendiri. Pada lapisan itu, Anda tidak bisa benar-benar membutuhkan pelacak layanan.
Joel
4
Bizar. Saya menggunakan Spring SEMUA waktu dengan anotasi. Meskipun memang ada kurva belajar tertentu yang terlibat, sekarang, saya tidak punya masalah apa pun dalam pemeliharaan, debugging, kejelasan, keterbacaan .... Saya kira bagaimana Anda menyusun berbagai hal adalah triknya.
Lawrence
25

Memang benar bahwa menyertakan kelas dalam application-context.xml menghindari kebutuhan untuk menggunakan getBean. Namun, bahkan itu sebenarnya tidak perlu. Jika Anda menulis aplikasi mandiri dan Anda TIDAK ingin memasukkan kelas driver Anda di application-context.xml, Anda dapat menggunakan kode berikut untuk memiliki Spring autowire dependensi driver:

public class AutowireThisDriver {

    private MySpringBean mySpringBean;    

    public static void main(String[] args) {
       AutowireThisDriver atd = new AutowireThisDriver(); //get instance

       ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                  "/WEB-INF/applicationContext.xml"); //get Spring context 

       //the magic: auto-wire the instance with all its dependencies:
       ctx.getAutowireCapableBeanFactory().autowireBeanProperties(atd,
                  AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);        

       // code that uses mySpringBean ...
       mySpringBean.doStuff() // no need to instantiate - thanks to Spring
    }

    public void setMySpringBean(MySpringBean bean) {
       this.mySpringBean = bean;    
    }
}

Saya perlu melakukan ini beberapa kali ketika saya memiliki semacam kelas mandiri yang perlu menggunakan beberapa aspek dari aplikasi saya (misalnya untuk pengujian) tetapi saya tidak ingin memasukkannya ke dalam konteks aplikasi karena tidak sebenarnya bagian dari aplikasi. Perhatikan juga bahwa ini menghindari kebutuhan untuk mencari kacang menggunakan nama String, yang saya selalu anggap jelek.

iftheshoefritz
sumber
Saya berhasil menggunakan metode ini dengan @Autowiredanotasi juga.
blong
21

Salah satu manfaat paling keren dari menggunakan sesuatu seperti Spring adalah Anda tidak harus menyatukan benda-benda Anda. Kepala Zeus terbelah terbuka dan kelas Anda muncul, sepenuhnya terbentuk dengan semua dependensinya dibuat dan terhubung, sesuai kebutuhan. Ajaib dan fantastis.

Semakin banyak Anda mengatakan ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, semakin sedikit sihir yang Anda dapatkan. Lebih sedikit kode hampir selalu lebih baik. Jika kelas Anda benar-benar membutuhkan kacang ClassINeed, mengapa Anda tidak langsung memasukkannya?

Yang mengatakan, sesuatu jelas perlu membuat objek pertama. Tidak ada yang salah dengan metode utama Anda mendapatkan satu atau dua kacang melalui getBean (), tetapi Anda harus menghindarinya karena setiap kali Anda menggunakannya, Anda tidak benar-benar menggunakan semua keajaiban Spring.

Brandon Yarbrough
sumber
1
Tapi OP tidak mengatakan "ClassINeed", dia mengatakan "BeanNameINeed" - yang memungkinkan wadah IoC untuk membuat instance pada setiap kelas yang dikonfigurasi dengan cara apa pun. Mungkin ini lebih seperti pola "service locator" daripada IoC, tetapi masih menghasilkan kopling yang longgar.
HDave
16

Motivasinya adalah untuk menulis kode yang tidak bergantung secara eksplisit pada Spring. Dengan begitu, jika Anda memilih untuk mengganti wadah, Anda tidak perlu menulis ulang kode apa pun.

Pikirkan wadah sebagai sesuatu yang tidak terlihat oleh kode Anda, secara ajaib memenuhi kebutuhannya, tanpa diminta.

Injeksi ketergantungan adalah titik tandingan untuk pola "pencari lokasi". Jika Anda akan mencari dependensi berdasarkan nama, Anda sebaiknya menyingkirkan wadah DI dan menggunakan sesuatu seperti JNDI.

erickson
sumber
11

Menggunakan @Autowiredatau ApplicationContext.getBean()benar-benar hal yang sama. Dalam kedua cara Anda mendapatkan kacang yang dikonfigurasi dalam konteks Anda dan dalam kedua cara kode Anda tergantung pada pegas. Satu-satunya hal yang harus Anda hindari adalah instantiating ApplicationContext Anda. Lakukan ini hanya sekali! Dengan kata lain, garis seperti

ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml");

seharusnya hanya digunakan sekali dalam aplikasi Anda.

easyplanner.cu.cc
sumber
Nggak. Terkadang @Autowired atau ApplicationContext.getBean () dapat menghasilkan kacang yang sama sekali berbeda. Saya tidak yakin bagaimana itu terjadi, tetapi saya sedang berjuang dengan masalah ini sekarang.
Oleksandr_DJ
4

Idenya adalah bahwa Anda bergantung pada injeksi ketergantungan ( inversi kontrol , atau IOC). Artinya, komponen Anda dikonfigurasikan dengan komponen yang mereka butuhkan. Ketergantungan ini disuntikkan (melalui konstruktor atau setter) - Anda tidak mendapatkannya sendiri.

ApplicationContext.getBean()mengharuskan Anda memberi nama kacang secara eksplisit di dalam komponen Anda. Sebagai gantinya, dengan menggunakan IoC, konfigurasi Anda dapat menentukan komponen apa yang akan digunakan.

Ini memungkinkan Anda untuk memasang kembali aplikasi Anda dengan implementasi komponen yang berbeda dengan mudah, atau mengonfigurasi objek untuk pengujian secara langsung dengan menyediakan varian tiruan (mis. DAO yang diolok-olok sehingga Anda tidak menekan basis data selama pengujian)

Brian Agnew
sumber
4

Orang lain telah menunjuk ke masalah umum (dan merupakan jawaban yang valid), tetapi saya hanya akan menawarkan satu komentar tambahan: bukan bahwa Anda TIDAK PERNAH melakukannya, melainkan melakukannya sesedikit mungkin.

Biasanya ini berarti bahwa hal itu dilakukan tepat sekali: saat bootstrap. Dan kemudian itu hanya untuk mengakses kacang "root", di mana dependensi lain dapat diatasi. Ini bisa menjadi kode yang dapat digunakan kembali, seperti servlet dasar (jika mengembangkan aplikasi web).

StaxMan
sumber
4

Salah satu tempat Spring adalah menghindari pemasangan . Tentukan dan gunakan Antarmuka, DI, AOP dan hindari menggunakan ApplicationContext.getBean () :-)

sourcerebels
sumber
4

Salah satu alasannya adalah testability. Katakanlah Anda memiliki kelas ini:

interface HttpLoader {
    String load(String url);
}
interface StringOutput {
    void print(String txt);
}
@Component
class MyBean {
    @Autowired
    MyBean(HttpLoader loader, StringOutput out) {
        out.print(loader.load("http://stackoverflow.com"));
    }
}

Bagaimana Anda bisa menguji kacang ini? Misal seperti ini:

class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();

        // execution
        new MyBean(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get, result::append);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

Mudah kan?

Meskipun Anda masih bergantung pada Pegas (karena anotasi), Anda dapat menghapus ketergantungan Anda pada pegas tanpa mengubah kode apa pun (hanya definisi anotasi) dan pengembang pengujian tidak perlu tahu apa pun tentang cara kerja pegas (mungkin ia harus melakukannya, tetapi memungkinkan untuk meninjau dan menguji kode secara terpisah dari apa yang dilakukan pegas).

Masih dimungkinkan untuk melakukan hal yang sama saat menggunakan ApplicationContext. Namun, Anda perlu mengejek ApplicationContextyang merupakan antarmuka besar. Anda membutuhkan implementasi dummy atau Anda dapat menggunakan kerangka kerja mengejek seperti Mockito:

@Component
class MyBean {
    @Autowired
    MyBean(ApplicationContext context) {
        HttpLoader loader = context.getBean(HttpLoader.class);
        StringOutput out = context.getBean(StringOutput.class);

        out.print(loader.load("http://stackoverflow.com"));
    }
}
class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();
        ApplicationContext context = Mockito.mock(ApplicationContext.class);
        Mockito.when(context.getBean(HttpLoader.class))
            .thenReturn(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get);
        Mockito.when(context.getBean(StringOutput.class)).thenReturn(result::append);

        // execution
        new MyBean(context);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

Ini kemungkinan yang cukup, tetapi saya pikir kebanyakan orang akan setuju bahwa opsi pertama lebih elegan dan membuat tes lebih sederhana.

Satu-satunya opsi yang benar-benar masalah adalah yang ini:

@Component
class MyBean {
    @Autowired
    MyBean(StringOutput out) {
        out.print(new HttpLoader().load("http://stackoverflow.com"));
    }
}

Menguji ini membutuhkan upaya besar atau kacang Anda akan mencoba untuk terhubung ke stackoverflow pada setiap pengujian. Dan segera setelah Anda mengalami kegagalan jaringan (atau admin di stackoverflow memblokir Anda karena tingkat akses yang berlebihan), Anda akan mengalami tes yang gagal secara acak.

Jadi sebagai kesimpulan saya tidak akan mengatakan bahwa menggunakan ApplicationContextsecara langsung itu salah dan harus dihindari di semua biaya. Namun jika ada opsi yang lebih baik (dan ada dalam banyak kasus), maka gunakan opsi yang lebih baik.

yankee
sumber
3

Saya hanya menemukan dua situasi di mana getBean () diperlukan:

Yang lain telah menyebutkan menggunakan getBean () di main () untuk mengambil kacang "utama" untuk program mandiri.

Penggunaan lain yang saya buat dari getBean () adalah dalam situasi di mana konfigurasi pengguna interaktif menentukan susunan kacang untuk situasi tertentu. Jadi, misalnya, bagian dari sistem boot loop melalui tabel database menggunakan getBean () dengan definisi bean scope = 'prototype' dan kemudian menyetel properti tambahan. Agaknya, ada UI yang menyesuaikan tabel database yang akan lebih ramah daripada mencoba (kembali) menulis konteks aplikasi XML.

tidak doa
sumber
3

Ada waktu lain ketika menggunakan getBean masuk akal. Jika Anda mengkonfigurasi ulang sistem yang sudah ada, di mana dependensi tidak secara eksplisit dipanggil dalam file konteks musim semi. Anda dapat memulai proses dengan melakukan panggilan ke getBean, sehingga Anda tidak harus mengirim semuanya sekaligus. Dengan cara ini Anda perlahan-lahan dapat membangun konfigurasi pegas Anda dengan menempatkan setiap bagian pada tempatnya dari waktu ke waktu dan membuat bit berbaris dengan benar. Panggilan untuk getBean pada akhirnya akan diganti, tetapi ketika Anda memahami struktur kode, atau tidak ada di sana, Anda dapat memulai proses kabel lebih banyak dan lebih banyak kacang dan menggunakan lebih sedikit dan lebih sedikit panggilan untuk getBean.

Tony Giaccone
sumber
2

Namun, masih ada kasus di mana Anda memerlukan pola pencari lokasi layanan. misalnya, saya memiliki kacang pengontrol, pengontrol ini mungkin memiliki beberapa kacang layanan default, yang dapat disuntikkan ketergantungan oleh konfigurasi. sementara mungkin ada banyak layanan tambahan atau baru controller ini dapat meminta sekarang atau nanti, yang kemudian membutuhkan pencari layanan untuk mengambil kacang layanan.

lwpro2
sumber
0

Anda harus menggunakan: ConfigurableApplicationContext bukan untuk ApplicationContext

Hai Nguyen Le
sumber