Cấu hình Spring Java: làm thế nào để bạn tạo một @Bean trong phạm vi nguyên mẫu với các đối số thời gian chạy?


134

Sử dụng Cấu hình Java của Spring, tôi cần thu thập / khởi tạo một bean có phạm vi nguyên mẫu với các đối số hàm tạo chỉ có thể nhận được khi chạy. Xem xét ví dụ mã sau (đơn giản hóa cho ngắn gọn):

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = appCtx.getBean(Thing.class, name);

    //System.out.println(thing.getName()); //prints name
}

trong đó lớp Thing được định nghĩa như sau:

public class Thing {

    private final String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

Lưu ý namefinal: nó chỉ có thể được cung cấp thông qua một nhà xây dựng và đảm bảo tính bất biến. Các phụ thuộc khác là các phụ thuộc dành riêng cho việc thực hiện của Thinglớp và không nên biết (kết hợp chặt chẽ với) việc thực hiện xử lý yêu cầu.

Mã này hoạt động hoàn toàn tốt với cấu hình Spring XML, ví dụ:

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

Làm cách nào để đạt được điều tương tự với cấu hình Java? Cách sau không hoạt động khi sử dụng Spring 3.x:

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

Bây giờ, tôi có thể tạo một Nhà máy, ví dụ:

public interface ThingFactory {
    public Thing createThing(String name);
}

Nhưng điều đó đánh bại toàn bộ quan điểm sử dụng Spring để thay thế mẫu thiết kế ServiceLocator và Factory , sẽ rất lý tưởng cho trường hợp sử dụng này.

Nếu Spring Java Config có thể làm điều này, tôi có thể tránh:

  • xác định giao diện Factory
  • xác định thực hiện Nhà máy
  • kiểm tra viết cho việc thực hiện Nhà máy

Đó là một tấn công việc (tương đối nói) cho một cái gì đó tầm thường mà Spring đã hỗ trợ thông qua cấu hình XML.


15
Câu hỏi tuyệt vời.
Sotirios Delimanolis

Tuy nhiên, có lý do gì bạn không thể tự khởi tạo lớp học và phải lấy nó từ Spring không? Nó có phụ thuộc vào các loại đậu khác không?
Sotirios Delimanolis

@SotiriosDelimanolis vâng, việc Thingthực hiện thực sự phức tạp hơn và không có sự phụ thuộc vào các loại đậu khác (tôi chỉ bỏ qua chúng cho ngắn gọn). Như vậy, tôi không muốn triển khai Trình xử lý yêu cầu biết về chúng, vì điều này sẽ kết hợp chặt chẽ trình xử lý với API / đậu mà nó không cần. Tôi sẽ cập nhật câu hỏi để phản ánh câu hỏi (xuất sắc) của bạn.
Les Hazlewood

Tôi không chắc chắn nếu Spring cho phép điều này trên một hàm tạo, nhưng tôi biết bạn có thể đặt @Qualifiercác tham số cho một setter với @Autowiredchính setter đó.
CodeChimp

2
Trong mùa xuân 4, ví dụ của bạn với @Beancác tác phẩm. Các @Beanphương pháp được gọi với các đối số thích hợp bạn truyền cho getBean(..).
Sotirios Delimanolis

Câu trả lời:


94

Trong một @Configurationlớp học, một @Beanphương thức như vậy

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

được sử dụng để đăng ký một định nghĩa bean và cung cấp cho nhà máy để tạo ra bean . Bean mà nó định nghĩa chỉ được khởi tạo theo yêu cầu bằng cách sử dụng các đối số được xác định trực tiếp hoặc thông qua quá trình quét màApplicationContext .

Trong trường hợp của một prototypebean, một đối tượng mới được tạo ra mỗi lần và do đó tương ứng@Bean phương thức cũng được thực thi.

Bạn có thể lấy một hạt từ ApplicationContextthông qua nóBeanFactory#getBean(String name, Object... args) phương thức

Cho phép chỉ định đối số hàm tạo rõ ràng / đối số phương thức nhà máy, ghi đè các đối số mặc định đã chỉ định (nếu có) trong định nghĩa bean.

Thông số:

args đối số để sử dụng nếu tạo ra một nguyên mẫu sử dụng lập luận rõ ràng để một phương thức tĩnh. Không hợp lệ để sử dụng giá trị args không null trong mọi trường hợp khác.

Nói cách khác, đối với prototypebean có phạm vi này , bạn đang cung cấp các đối số sẽ được sử dụng, không phải trong hàm tạo của lớp bean, mà trong@Bean lời gọi phương thức.

Điều này ít nhất đúng với phiên bản Spring 4+.


12
Vấn đề của tôi với phương pháp này là bạn không thể giới hạn @Beanphương thức trong việc gọi thủ công. Nếu bạn đã bao giờ @Autowire Thingcác @Beanphương pháp sẽ được gọi là khả năng chết trên không có khả năng tiêm tham số. Tương tự nếu bạn @Autowire List<Thing>. Tôi thấy điều này hơi mong manh.
Jan Zyka

@JanZyka, có cách nào tôi có thể tự động Điều khác ngoài những câu trả lời được nêu trong những câu trả lời này không (tất cả đều giống nhau nếu bạn nheo mắt). Cụ thể hơn, nếu tôi biết các đối số trả trước (tại thời gian biên dịch / cấu hình), có cách nào để tôi có thể diễn đạt các đối số này trong một số chú thích mà tôi có thể đủ điều kiện @Autowirevới không?
M. Prokhorov

52

Với Spring> 4.0 và Java 8, bạn có thể thực hiện việc này một cách an toàn hơn:

@Configuration    
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    } 

    @Bean
    @Scope(value = "prototype")
    public Thing thing(String name) {
       return new Thing(name);
    }

}

Sử dụng:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

Vì vậy, bây giờ bạn có thể nhận được bean của bạn trong thời gian chạy. Tất nhiên đây là một mô hình nhà máy, nhưng bạn có thể tiết kiệm thời gian để viết một lớp cụ thể như thế nào ThingFactory(tuy nhiên bạn sẽ phải viết tùy chỉnh @FunctionalInterfaceđể truyền nhiều hơn hai tham số).


1
Tôi thấy cách tiếp cận này rất hữu ích và sạch sẽ. Cảm ơn!
Alex Objelean

1
Vải gì vậy? Tôi hiểu cách sử dụng của bạn .. nhưng không phải là thuật ngữ .. đừng nghĩ rằng tôi đã nghe nói về "mẫu vải"
AnthonyJClink

1
@AnthonyJClink Tôi đoán tôi chỉ sử dụng fabricthay vì factory, xấu của tôi :)
Roman Golyshev

1
@AbhijitSarkar oh, tôi hiểu rồi. Nhưng bạn không thể truyền tham số cho một Providerhoặc cho một ObjectFactory, hoặc tôi sai? Và trong ví dụ của tôi, bạn có thể truyền tham số chuỗi cho nó (hoặc bất kỳ tham số nào)
Roman Golyshev

2
Nếu bạn không muốn (hoặc không cần) sử dụng các phương pháp vòng đời của Spring bean (khác với đậu nguyên mẫu ...), bạn có thể bỏ qua @BeanScopechú thích qua Thing thingphương thức. Hơn nữa, phương pháp này có thể được đặt ở chế độ riêng tư để ẩn và chỉ còn lại nhà máy.
m52509791

17

Kể từ mùa xuân 4.3, có một cách mới để làm điều đó, được may cho vấn đề đó.

Đối tượng - Nó cho phép bạn chỉ cần thêm nó như là một phụ thuộc vào bean có phạm vi Prototype "được tranh luận" của bạn và khởi tạo nó bằng cách sử dụng đối số.

Đây là một ví dụ đơn giản về cách sử dụng nó:

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

Điều này tất nhiên sẽ in chuỗi hello khi gọi usePrototype.


15

CẬP NHẬT mỗi bình luận

Đầu tiên, tôi không chắc tại sao bạn nói "điều này không hoạt động" cho một cái gì đó hoạt động tốt trong Mùa xuân 3.x. Tôi nghi ngờ một cái gì đó phải sai trong cấu hình của bạn ở đâu đó.

Những công việc này:

-- Tập tin cấu hình:

@Configuration
public class ServiceConfig {
    // only here to demo execution order
    private int count = 1;

    @Bean
    @Scope(value = "prototype")
    public TransferService myFirstService(String param) {
       System.out.println("value of count:" + count++);
       return new TransferServiceImpl(aSingletonBean(), param);
    }

    @Bean
    public AccountRepository aSingletonBean() {
        System.out.println("value of count:" + count++);
        return new InMemoryAccountRepository();
    }
}

- Kiểm tra tệp để thực thi:

@Test
public void prototypeTest() {
    // create the spring container using the ServiceConfig @Configuration class
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
    Object singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

Sử dụng Spring 3.2.8 và Java 7, cung cấp đầu ra này:

value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2

Vì vậy, Bean 'Singleton' được yêu cầu hai lần. Tuy nhiên như chúng ta mong đợi, Spring chỉ tạo ra nó một lần. Lần thứ hai nó thấy rằng nó có bean đó và chỉ trả về đối tượng hiện có. Hàm tạo (phương thức @Bean) không được gọi lần thứ hai. Để bảo vệ điều này, khi Bean 'Prototype' được yêu cầu từ cùng một đối tượng bối cảnh, chúng ta thấy rằng tham chiếu thay đổi trong đầu ra VÀ mà hàm tạo (phương thức @Bean) được gọi hai lần.

Vì vậy, câu hỏi là làm thế nào để tiêm một singleton vào một nguyên mẫu. Lớp cấu hình ở trên cho thấy làm thế nào để làm điều đó quá! Bạn nên chuyển tất cả các tham chiếu như vậy vào hàm tạo. Điều này sẽ cho phép lớp được tạo trở thành một POJO thuần túy cũng như làm cho các đối tượng tham chiếu có trong đó trở thành bất biến như chúng cần phải có. Vì vậy, dịch vụ chuyển tiền có thể trông giống như:

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String name) {
        this.name = name;
        // system out here is only because this is a dumb test usage
        System.out.println("Using name value of: " + this.name);

        this.accountRepository = accountRepository;
    }
    ....
}

Nếu bạn viết Bài kiểm tra đơn vị, bạn sẽ rất vui vì bạn đã tạo ra các lớp này mà không cần tất cả @Autowired. Nếu bạn cần các thành phần tự động, hãy giữ các tệp cục bộ đó vào các tệp cấu hình java.

Điều này sẽ gọi phương thức dưới đây trong BeanFactory. Lưu ý trong phần mô tả cách này dành cho trường hợp sử dụng chính xác của bạn.

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
 * overriding the specified default arguments (if any) in the bean definition.
 * @param name the name of the bean to retrieve
 * @param args arguments to use if creating a prototype using explicit arguments to a
 * static factory method. It is invalid to use a non-null args value in any other case.
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no such bean definition
 * @throws BeanDefinitionStoreException if arguments have been given but
 * the affected bean isn't a prototype
 * @throws BeansException if the bean could not be created
 * @since 2.5
 */
Object getBean(String name, Object... args) throws BeansException;

3
Cảm ơn vi đa trả lơi! Tuy nhiên, tôi nghĩ bạn đã hiểu nhầm câu hỏi. Phần quan trọng nhất của câu hỏi là giá trị thời gian chạy phải được cung cấp dưới dạng đối số của hàm tạo khi lấy (khởi tạo) nguyên mẫu.
Les Hazlewood

Tôi cập nhật trả lời của tôi. Trên thực tế có vẻ như việc xử lý giá trị thời gian chạy đã được thực hiện chính xác vì vậy tôi đã bỏ phần đó ra. Nó được hỗ trợ rõ ràng mặc dù bạn có thể thấy từ các bản cập nhật và đầu ra từ chương trình.
Joe

0

Bạn có thể đạt được hiệu ứng tương tự chỉ bằng cách sử dụng một lớp bên trong :

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}


-1

Trả lời muộn với một cách tiếp cận hơi khác nhau. Đó là phần tiếp theo của câu hỏi gần đây đề cập đến chính câu hỏi này.

Có, như đã nói, bạn có thể khai báo bean nguyên mẫu chấp nhận một tham số trong một @Configurationlớp cho phép tạo một bean mới ở mỗi lần tiêm.
Điều đó sẽ làm cho @Configuration lớp này trở thành một nhà máy và để không cung cấp cho nhà máy này quá nhiều trách nhiệm, điều này không nên bao gồm các loại đậu khác.

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

Nhưng bạn cũng có thể tiêm bean cấu hình đó để tạo Things:

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

Nó là cả hai loại an toàn và súc tích.


1
Cảm ơn đã trả lời, nhưng đây là một mô hình chống mùa xuân. Các đối tượng cấu hình không nên 'rò rỉ' vào mã ứng dụng - chúng tồn tại để định cấu hình biểu đồ và giao diện đối tượng ứng dụng của bạn với các cấu trúc Spring. Điều này gần giống với các lớp XML trong các bean ứng dụng của bạn (tức là một cơ chế cấu hình khác). Đó là, nếu Spring đi kèm với một cơ chế cấu hình khác, bạn sẽ phải cấu trúc lại mã ứng dụng của mình - một chỉ báo rõ ràng điều này vi phạm sự phân tách mối quan tâm. Tốt hơn là để Cấu hình của bạn tạo các phiên bản của giao diện Factory / Function và chèn Factory - không có khớp nối chặt chẽ để cấu hình.
Les Hazlewood

1) Tôi hoàn toàn đồng ý rằng trong trường hợp chung, các đối tượng cấu hình không bị rò rỉ như một trường. Nhưng trong trường hợp cụ thể này, việc tiêm một đối tượng cấu hình xác định một và chỉ một hạt để tạo ra các hạt nguyên mẫu, IHMO hoàn toàn có ý nghĩa: lớp cấu hình này trở thành một nhà máy. Đâu là sự phân tách các vấn đề quan tâm nếu nó chỉ làm điều đó? ...
davidxxx

... 2) Giới thiệu "Đó là, nếu Spring đi kèm với một cơ chế cấu hình khác", đó là một lập luận sai bởi vì khi bạn quyết định sử dụng một khung công tác trong ứng dụng của mình, bạn sẽ ghép đôi ứng dụng của mình với điều đó. Vì vậy, trong mọi trường hợp, bạn cũng sẽ phải cấu trúc lại bất kỳ ứng dụng Spring nào dựa vào @Configurationnếu cơ chế đó thay đổi.
davidxxx

1
... 3) Câu trả lời bạn chấp nhận đề xuất sử dụng BeanFactory#getBean(). Nhưng điều đó còn tệ hơn về mặt khớp nối vì đó là một nhà máy cho phép nhận / khởi tạo bất kỳ hạt đậu nào của ứng dụng và không chỉ loại đậu nào hiện tại cần. Với cách sử dụng như vậy, bạn có thể kết hợp các trách nhiệm của lớp của mình rất dễ dàng vì các phụ thuộc mà nó có thể kéo là không giới hạn, điều này thực sự không được khuyến khích nhưng trường hợp đặc biệt.
davidxxx

@ davidxxx - Tôi đã chấp nhận câu trả lời từ nhiều năm trước, trước khi JDK 8 và Spring 4 không hoạt động. Câu trả lời của Roman ở trên là đúng hơn cho các tập quán mùa xuân hiện đại. Liên quan đến tuyên bố của bạn "bởi vì khi bạn quyết định sử dụng một khung công tác trong ứng dụng của mình, bạn kết hợp ứng dụng của mình với điều đó" hoàn toàn trái ngược với các khuyến nghị của nhóm Spring và các thực tiễn tốt nhất về Cấu hình Java - hãy hỏi Josh Long hoặc Jeurgen Hoeller nếu bạn nhận được cơ hội để nói chuyện trực tiếp với họ (tôi có, và tôi có thể đảm bảo với bạn rằng họ khuyên rõ ràng không nên ghép mã ứng dụng của bạn với Spring bất cứ khi nào có thể). Chúc mừng.
Les Hazlewood
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.