Tại sao ApplicationContext.getBean của Spring bị coi là xấu?


270

Tôi đã hỏi một câu hỏi chung về mùa xuân: Đậu mùa xuân tự động và có nhiều người trả lời rằng nên gọi Spring ApplicationContext.getBean()là càng nhiều càng tốt. Tại sao vậy?

Làm thế nào khác tôi có thể truy cập vào các bean tôi đã cấu hình Spring để tạo?

Tôi đang sử dụng Spring trong một ứng dụng không phải web và đã lên kế hoạch truy cập một ApplicationContextđối tượng được chia sẻ theo mô tả của LiorH .

Sửa đổi

Tôi chấp nhận câu trả lời dưới đây, nhưng đây là một thay thế của Martin Fowler, người thảo luận về giá trị của Dependency Injection so với sử dụng Bộ định vị dịch vụ (về cơ bản giống như gọi là gói ApplicationContext.getBean()).

Một phần, Fowler nói, " Với trình định vị dịch vụ, lớp ứng dụng yêu cầu [dịch vụ] rõ ràng bằng một thông báo cho trình định vị. Với tiêm không có yêu cầu rõ ràng, dịch vụ xuất hiện trong lớp ứng dụng - do đó đảo ngược điều khiển. Đảo ngược điều khiển là một tính năng phổ biến của các khung, nhưng nó là một thứ có giá. Nó có xu hướng khó hiểu và dẫn đến các vấn đề khi bạn đang cố gắng gỡ lỗi. Vì vậy, trên toàn bộ tôi thích tránh nó [Đảo ngược kiểm soát ] trừ khi tôi cần nó. Điều này không có nghĩa là nó là một điều xấu, chỉ là tôi nghĩ rằng nó cần phải tự biện minh cho sự thay thế đơn giản hơn. "

Câu trả lời:


202

Tôi đã đề cập điều này trong một bình luận về câu hỏi khác, nhưng toàn bộ ý tưởng của Inversion of Control là không có ai trong lớp bạn biết hoặc quan tâm làm thế nào họ có được các đối tượng mà họ phụ thuộc . Điều này giúp dễ dàng thay đổi loại triển khai của một phụ thuộc nhất định mà bạn sử dụng bất cứ lúc nào. Nó cũng làm cho các lớp dễ kiểm tra, vì bạn có thể cung cấp các triển khai giả định của các phụ thuộc. Cuối cùng, nó làm cho các lớp đơn giản hơn và tập trung hơn vào trách nhiệm cốt lõi của họ.

Gọi ApplicationContext.getBean()không phải là đảo ngược của kiểm soát! Mặc dù vẫn dễ dàng thay đổi cách cài đặt được cấu hình cho tên bean đã cho, lớp hiện đang dựa trực tiếp vào Spring để cung cấp sự phụ thuộc đó và không thể có được bất kỳ cách nào khác. Bạn không thể tự thực hiện giả của mình trong một lớp kiểm tra và tự mình chuyển nó cho nó. Điều này về cơ bản đánh bại mục đích của Spring như là một thùng chứa phụ thuộc.

Ở mọi nơi bạn muốn nói:

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

thay vào đó, bạn nên khai báo một phương thức:

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

Và sau đó trong cấu hình của bạn:

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

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

Mùa xuân sau đó sẽ tự động tiêm myClassvào myOtherClass.

Khai báo mọi thứ theo cách này, và tận gốc của nó đều có một cái gì đó như:

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

MyApplicationlà lớp trung tâm nhất và ít nhất phụ thuộc gián tiếp vào mọi dịch vụ khác trong chương trình của bạn. Khi bootstrapping, trong mainphương thức của bạn , bạn có thể gọi applicationContext.getBean("myApplication")nhưng bạn không cần phải gọi getBean()bất cứ nơi nào khác!


3
Có bất cứ điều gì liên quan đến điều này hoạt động chỉ với các chú thích khi tạo một new MyOtherClass()đối tượng? Tôi biết về @Autowired, nhưng tôi chỉ từng sử dụng nó trên các lĩnh vực và nó bị hỏng vào new MyOtherClass()..
Tim

70
Không đúng khi ApplicationContext.getBean () không phải là IoC. Niether bắt buộc phải có tất cả các lớp học của bạn được khởi tạo bởi Spring. Đó là giáo điều không phù hợp. Nếu ApplicationContext được tiêm chính nó, thì bạn hoàn toàn có thể yêu cầu nó khởi tạo một bean theo cách này - và các bean mà nó tạo ra có thể là các triển khai khác nhau dựa trên ApplicationContext ban đầu được tiêm. Chẳng hạn, tôi có một kịch bản trong đó tôi tự động tạo các cá thể bean mới dựa trên tên bean không xác định tại thời điểm biên dịch nhưng khớp với một trong các triển khai được xác định trong tệp spring.xml của tôi.
Alex Worden

3
Đồng ý với Alex, tôi có cùng một vấn đề, trong đó một lớp nhà máy sẽ chỉ biết sử dụng bean hoặc triển khai nào trong thời gian chạy thông qua tương tác người dùng, tôi nghĩ đây là nơi giao diện ContextAware xuất hiện
Ben

3
@elbek: applicationContext.getBeankhông phải là phụ thuộc tiêm: nó truy cập trực tiếp vào khung, sử dụng nó làm công cụ định vị dịch vụ .
ColinD

6
@herman: Tôi không biết về Spring vì tôi đã không sử dụng nó trong một thời gian dài, nhưng trong JSR-330 / Guice / Dagger, bạn sẽ làm điều này bằng cách tiêm Provider<Foo>thay vì Foovà gọi provider.get()mỗi khi bạn cần ví dụ mới. Không có tham chiếu đến chính container và bạn có thể dễ dàng tạo một Providerthử nghiệm.
ColinD

64

Các lý do để thích Bộ định vị dịch vụ hơn Đảo ngược điều khiển (IoC) là:

  1. Trình định vị dịch vụ dễ dàng hơn nhiều đối với những người khác theo dõi mã của bạn. IoC là 'ma thuật' nhưng các lập trình viên bảo trì phải hiểu các cấu hình Spring phức tạp của bạn và tất cả vô số vị trí để tìm ra cách bạn nối dây các đối tượng của mình.

  2. IoC là khủng khiếp cho việc gỡ lỗi các vấn đề cấu hình. Trong một số loại ứng dụng nhất định, ứng dụng sẽ không khởi động khi bị định cấu hình sai và bạn có thể không có cơ hội bước qua những gì đang xảy ra với trình gỡ lỗi.

  3. IoC chủ yếu dựa trên XML (Chú thích cải thiện mọi thứ nhưng vẫn còn rất nhiều XML ngoài kia). Điều đó có nghĩa là các nhà phát triển không thể làm việc trên chương trình của bạn trừ khi họ biết tất cả các thẻ ma thuật được xác định bởi Spring. Nó không đủ tốt để biết Java nữa. Điều này cản trở các lập trình viên ít kinh nghiệm hơn (nghĩa là thực sự thiết kế kém để sử dụng một giải pháp phức tạp hơn khi một giải pháp đơn giản hơn, chẳng hạn như Bộ định vị dịch vụ, sẽ đáp ứng các yêu cầu tương tự). Thêm vào đó, hỗ trợ chẩn đoán các vấn đề XML yếu hơn nhiều so với hỗ trợ cho các vấn đề Java.

  4. Phụ thuộc tiêm phù hợp hơn với các chương trình lớn hơn. Hầu hết thời gian sự phức tạp bổ sung là không đáng.

  5. Thường thì Spring được sử dụng trong trường hợp bạn "có thể muốn thay đổi việc thực hiện sau". Có nhiều cách khác để đạt được điều này mà không cần sự phức tạp của Spring IoC.

  6. Đối với các ứng dụng web (Java EE WARs), bối cảnh Mùa xuân bị ràng buộc một cách hiệu quả vào thời gian biên dịch (trừ khi bạn muốn các nhà khai thác tìm hiểu xung quanh bối cảnh trong cuộc chiến bùng nổ). Bạn có thể khiến Spring sử dụng các tệp thuộc tính, nhưng với các tệp thuộc tính của servlets sẽ cần phải ở một vị trí được xác định trước, điều đó có nghĩa là bạn không thể triển khai nhiều máy chủ cùng một lúc trên cùng một hộp. Bạn có thể sử dụng Spring với JNDI để thay đổi các thuộc tính khi khởi động servlet, nhưng nếu bạn đang sử dụng JNDI cho các tham số có thể sửa đổi của quản trị viên, thì nhu cầu đối với Spring sẽ giảm đi (vì JNDI thực sự là Trình định vị dịch vụ).

  7. Với Spring, bạn có thể mất Kiểm soát chương trình nếu Spring đang gửi đến các phương thức của bạn. Điều này thuận tiện và hoạt động cho nhiều loại ứng dụng, nhưng không phải tất cả. Bạn có thể cần kiểm soát luồng chương trình khi bạn cần tạo các tác vụ (luồng, v.v.) trong quá trình khởi tạo hoặc cần các tài nguyên có thể sửa đổi mà Spring không biết khi nào nội dung được ràng buộc với WAR của bạn.

Mùa xuân rất tốt cho việc quản lý giao dịch và có một số lợi thế. Chỉ là IoC có thể được thiết kế quá mức trong nhiều tình huống và đưa ra sự phức tạp không đáng có cho các nhà bảo trì. Không tự động sử dụng IoC mà không nghĩ đến cách không sử dụng nó trước.


7
Thêm vào đó - ServiceLocator của bạn luôn có thể sử dụng IoC từ Spring, trừu tượng hóa mã của bạn khỏi bị phụ thuộc vào Spring, tràn ngập các chú thích Spring và magik không thể mã hóa. Gần đây tôi đã chuyển một loạt mã tới GoogleAppEngine nơi Spring không được hỗ trợ. Tôi ước rằng tôi đã ẩn tất cả IoC đằng sau một ServiceFactory ngay từ đầu!
Alex Worden

IoC khuyến khích một mô hình miền thiếu máu, điều mà tôi coi thường. Đậu thực thể cần một cách để tra cứu dịch vụ của họ để họ có thể thực hiện hành vi của chính họ. Ở lớp đó, bạn không thể thực sự cần một bộ định vị dịch vụ.
Joel

4
Kỳ quái. Tôi sử dụng Spring TẤT CẢ thời gian với các chú thích. Mặc dù thực sự có một đường cong học tập nhất định, nhưng bây giờ, tôi không có vấn đề gì trong việc bảo trì, gỡ lỗi, rõ ràng, dễ đọc .... Tôi đoán cách bạn cấu trúc mọi thứ là mẹo.
Lawrence

25

Đúng là bao gồm cả lớp trong application-context.xml tránh sự cần thiết phải sử dụng getBean. Tuy nhiên, thậm chí điều đó thực sự không cần thiết. Nếu bạn đang viết một ứng dụng độc lập và bạn KHÔNG muốn đưa lớp trình điều khiển của mình vào ứng dụng-bối cảnh, bạn có thể sử dụng mã sau đây để Spring tự động phụ thuộc vào trình điều khiển:

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

Tôi đã cần phải làm điều này một vài lần khi tôi có một loại lớp độc lập cần sử dụng một số khía cạnh của ứng dụng của mình (ví dụ để thử nghiệm) nhưng tôi không muốn đưa nó vào ngữ cảnh ứng dụng vì nó không phải là thực sự là một phần của ứng dụng Cũng lưu ý rằng điều này tránh sự cần thiết phải tra cứu bean bằng tên String, điều mà tôi luôn nghĩ là xấu.


Tôi đã có thể sử dụng phương pháp này thành công với @Autowiredchú thích.
blong 26/03/13

21

Một trong những lợi ích tuyệt vời nhất của việc sử dụng thứ gì đó như Spring là bạn không cần phải nối các vật thể của mình lại với nhau. Đầu của Zeus mở ra và các lớp của bạn xuất hiện, được hình thành đầy đủ với tất cả các phụ thuộc của chúng được tạo và nối dây, khi cần thiết. Thật kỳ diệu và tuyệt vời.

Bạn càng nói ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, bạn càng nhận được ít phép thuật. Ít mã hơn hầu như luôn luôn tốt hơn. Nếu lớp của bạn thực sự cần một ClassINeed bean, tại sao bạn không nối nó vào?

Điều đó nói rằng, một cái gì đó rõ ràng cần phải tạo ra đối tượng đầu tiên. Không có gì sai với phương thức chính của bạn có được một hoặc hai bean thông qua getBean (), nhưng bạn nên tránh nó bởi vì bất cứ khi nào bạn sử dụng nó, bạn không thực sự sử dụng tất cả phép thuật của Spring.


1
Nhưng OP không nói "ClassINeed" mà anh ấy nói "BeanNameINeed" - cho phép bộ chứa IoC tạo một cá thể trên bất kỳ lớp nào được cấu hình theo bất kỳ cách nào. Có lẽ nó giống mô hình "định vị dịch vụ" hơn IoC, nhưng nó vẫn dẫn đến khớp nối lỏng lẻo.
HDave

16

Động lực là viết mã không phụ thuộc rõ ràng vào Spring. Theo cách đó, nếu bạn chọn chuyển đổi vùng chứa, bạn không phải viết lại bất kỳ mã nào.

Hãy nghĩ về container như một cái gì đó vô hình với mã của bạn, cung cấp một cách kỳ diệu cho nhu cầu của nó, mà không được yêu cầu.

Tiêm phụ thuộc là một đối trọng với mẫu "định vị dịch vụ". Nếu bạn định tra cứu phụ thuộc theo tên, bạn cũng có thể thoát khỏi thùng chứa DI và sử dụng cái gì đó như JNDI.


11

Sử dụng @Autowiredhoặc ApplicationContext.getBean()thực sự là điều tương tự. Trong cả hai cách, bạn nhận được bean được cấu hình trong ngữ cảnh của bạn và theo cả hai cách mã của bạn phụ thuộc vào mùa xuân. Điều duy nhất bạn nên tránh là khởi tạo ApplicationContext của bạn. Làm điều này chỉ một lần! Nói cách khác, một dòng như

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

chỉ nên được sử dụng một lần trong ứng dụng của bạn.


Không. Đôi khi @Autowired hoặc ApplicationContext.getBean () có thể tạo ra tổng số các loại đậu khác nhau. Tôi không chắc nó đã xảy ra như thế nào, nhưng tôi đang phải vật lộn với vấn đề này ngay bây giờ.
Oleksandr_DJ

4

Ý tưởng là bạn dựa vào tiêm phụ thuộc ( đảo ngược điều khiển , hoặc IoC). Đó là, các thành phần của bạn được cấu hình với các thành phần họ cần. Các phụ thuộc này được thêm vào (thông qua hàm tạo hoặc setters) - bạn không tự mình nhận được.

ApplicationContext.getBean()yêu cầu bạn đặt tên một cách rõ ràng trong thành phần của bạn. Thay vào đó, bằng cách sử dụng IoC, cấu hình của bạn có thể xác định thành phần nào sẽ được sử dụng.

Điều này cho phép bạn dễ dàng cài đặt lại ứng dụng của mình với các cài đặt thành phần khác nhau hoặc định cấu hình các đối tượng để thử nghiệm theo cách đơn giản bằng cách cung cấp các biến thể giả định (ví dụ: DAO bị giả để bạn không truy cập cơ sở dữ liệu trong khi thử nghiệm)


4

Những người khác đã chỉ ra vấn đề chung (và là câu trả lời hợp lệ), nhưng tôi sẽ chỉ đưa ra một nhận xét bổ sung: không phải là bạn KHÔNG BAO GIỜ làm điều đó, mà là làm điều đó càng ít càng tốt.

Thông thường điều này có nghĩa là nó được thực hiện chính xác một lần: trong quá trình bootstrapping. Và sau đó chỉ cần truy cập vào bean "root", thông qua đó các phụ thuộc khác có thể được giải quyết. Đây có thể là mã có thể tái sử dụng, như cơ sở servlet (nếu phát triển ứng dụng web).


4

Một trong những cơ sở mùa xuân là tránh khớp nối . Xác định và sử dụng Giao diện, DI, AOP và tránh sử dụng ApplicationContext.getBean () :-)


4

Một trong những lý do là khả năng kiểm tra. Nói rằng bạn có lớp này:

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

Làm thế nào bạn có thể kiểm tra đậu này? Ví dụ như thế này:

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

Dễ thôi phải không?

Mặc dù bạn vẫn phụ thuộc vào Spring (do các chú thích), bạn có thể loại bỏ sự phụ thuộc của mình vào mùa xuân mà không thay đổi bất kỳ mã nào (chỉ các định nghĩa chú thích) và nhà phát triển thử nghiệm không cần biết gì về cách hoạt động của mùa xuân nó cho phép xem xét và kiểm tra mã riêng biệt với những gì mùa xuân làm).

Vẫn có thể làm tương tự khi sử dụng ApplicationContext. Tuy nhiên, sau đó bạn cần phải giả định ApplicationContextđó là một giao diện lớn. Bạn có thể cần một triển khai giả hoặc bạn có thể sử dụng khung mô phỏng như 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);
    }
}

Đây là một khả năng hoàn toàn, nhưng tôi nghĩ hầu hết mọi người sẽ đồng ý rằng tùy chọn đầu tiên thanh lịch hơn và làm cho thử nghiệm đơn giản hơn.

Tùy chọn duy nhất thực sự là một vấn đề là cái này:

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

Kiểm tra điều này đòi hỏi những nỗ lực lớn hoặc bean của bạn sẽ cố gắng kết nối với stackoverflow trên mỗi thử nghiệm. Và ngay khi bạn gặp lỗi mạng (hoặc quản trị viên tại stackoverflow chặn bạn do tốc độ truy cập quá cao), bạn sẽ có các thử nghiệm thất bại ngẫu nhiên.

Vì vậy, như một kết luận, tôi sẽ không nói rằng sử dụng ApplicationContexttrực tiếp là tự động sai và nên tránh bằng mọi giá. Tuy nhiên nếu có các tùy chọn tốt hơn (và trong hầu hết các trường hợp), thì hãy sử dụng các tùy chọn tốt hơn.


3

Tôi chỉ tìm thấy hai tình huống yêu cầu getBean ():

Những người khác đã đề cập đến việc sử dụng getBean () trong main () để tìm nạp bean "chính" cho một chương trình độc lập.

Một cách sử dụng khác mà tôi đã thực hiện với getBean () là trong các tình huống trong đó cấu hình người dùng tương tác xác định cấu trúc bean cho một tình huống cụ thể. Vì vậy, ví dụ, một phần của hệ thống khởi động lặp qua bảng cơ sở dữ liệu bằng cách sử dụng getBean () với định nghĩa bean scope = 'nguyên mẫu' và sau đó đặt các thuộc tính bổ sung. Có lẽ, có một giao diện người dùng điều chỉnh bảng cơ sở dữ liệu sẽ thân thiện hơn so với việc cố gắng (viết lại) bối cảnh ứng dụng XML.


3

Có một thời điểm khác khi sử dụng getBean có ý nghĩa. Nếu bạn đang cấu hình lại một hệ thống đã tồn tại, trong đó các phụ thuộc không được gọi rõ ràng trong các tệp ngữ cảnh mùa xuân. Bạn có thể bắt đầu quá trình bằng cách thực hiện các cuộc gọi đến getBean, để bạn không phải kết nối tất cả cùng một lúc. Bằng cách này, bạn có thể từ từ xây dựng cấu hình lò xo của mình, đặt từng mảnh vào vị trí theo thời gian và sắp xếp các bit đúng cách. Các lệnh gọi getBean cuối cùng sẽ được thay thế, nhưng khi bạn hiểu cấu trúc của mã, hoặc thiếu ở đó, bạn có thể bắt đầu quá trình nối dây ngày càng nhiều đậu và sử dụng ngày càng ít cuộc gọi đến getBean.


2

tuy nhiên, vẫn có trường hợp bạn cần mẫu định vị dịch vụ. ví dụ, tôi có một bean điều khiển, bộ điều khiển này có thể có một số bean dịch vụ mặc định, có thể phụ thuộc vào cấu hình. mặc dù cũng có thể có nhiều dịch vụ bổ sung hoặc mới, bộ điều khiển này có thể gọi ngay bây giờ hoặc sau đó, sau đó cần bộ định vị dịch vụ để lấy các hạt dịch vụ.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.