spring prototype bean 获取处理
参考链接:http://dolszewski.com/spring/accessing-prototype-bean-in-singleton/amp/
When to use prototype beans?
Before we fall into the pitfalls of injecting prototype into singleton beans, stop for a moment to think when you actually need such relation.
Just because you use dependency injection in your project, it doesn’t mean you should avoid the new keyword like the plague. There is nothing wrong if you instantiate a stateful object on your own.
Usually, we use the prototype scope for a stateful object when it has dependencies on other Spring beans. Why? Because the framework will autowire these dependencies for us.
Let’s see this case in a practicle sample.
Prototype in singleton bean example
Now consider the following compoistion of Spring beans.
Let’s start with the right side of the diagram where a prototype bean depends on a singleton. No matter how many instances of MessageBuilders Spring creates, we expect they will always get the reference to the same ContentProcessor object. This part is easy.
Next, the left side. By contrast, here a singleton bean depends on a prototype. When Spring creates the MessageService bean, it will also create a single instance of MessageBuilder. But just one. That’s all MessageService needs to be created.
But hang on a minute.
For this particular graph of objects, Spring creates only one instance for each bean definition. Even though one of them is a prototype. If we had another bean depending on MessageBuilder, it would get another instance of the prototype bean. But in this case, Spring creates only one.
So what if you want a new instance of the prototype bean on every call to a particular method of the singleton bean? Not only when Spring creates this singleton?
Don’t worry. There’re multiple solutions.
You will see all possible options in a moment. But before I share them with you, we need a sample dependency between a prototype and a singleton for demonstration purpose.
Prototype in singleton dependency demo
We need a prototype, a singleton which depends on the prototype, and an automated test to verify the number of prototype instances.
Let’s write each step by step.
Prototype with instance counter
First, the prototype.
We want to know how many objects the Spring framework creates. We’re going to use a simple static counter incremented in the class constructor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Service @Scope (value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) class MessageBuilder { private static final AtomicInteger instanceCounter = new AtomicInteger( 0 ); MessageBuilder() { instanceCounter.incrementAndGet(); } static int getInstanceCounter() { return instanceCounter.get(); } } |
We need the counter only for testing. The main task of the class is to build some immutable Message object. Our prototype will use the Builder pattern as presented below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class MessageBuilder { //... private String content; private String receiver; MessageBuilder withContent(String content) { this .content = content; return this ; } MessageBuilder withReceiver(String receiver) { this .receiver = receiver; return this ; } Message build() { return new Message(content, receiver); } } |
Singleton dependent on prototype bean
Next, the singleton implementation.
The MesssageService class is a simple singleton with only one method. It calls all three methods of the prototype.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Service class MessageService { private final MessageBuilder messageBuilder; MessageService(MessageBuilder messageBuilder) { this .messageBuilder = messageBuilder; } Message createMessage(String content, String receiver) { return messageBuilder .withContent(content) .withReceiver(receiver) .build(); } } |
As a reminder, since Spring 4.3 you do not have to put the @Autowired annotation on a constructor if there is only one in a class. The framework uses it automatically.
Checking prototype instance count
Finally, we write a test that will confirm that on every call to the singleton, Spring creates a new prototype instance. That’s our goal. We call the singleton twice so we expect two objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@RunWith (SpringRunner. class ) @SpringBootTest public class MessageServiceTest { @Autowired private MessageService messageService; @Test public void shouldCreateTwoBuilders() throws Exception { //when messageService.createMessage( "text" , "alice" ); messageService.createMessage( "msg" , "bob" ); //then int prototypeCounter = MessageBuilder.getInstanceCounter(); assertEquals( "Wrong number of instances" , 2 , prototypeCounter); } } |
Now we’re ready to examine all possible solutions to inject prototype bean into a singleton.
How to inject prototype bean into singleton bean?
When you work with a prototype bean in a singleton, you have three options to get a new instance of the prototype:
- Spring can autowire a single prototype instance when it creates the singleton. It’s the default framework behavior.
- Spring can create a new prototype instance on every call to any method of this prototype.
- You can programmatically ask the Spring context to create a new prototype instance at the moment you need a new object.
Option 1: Injecting on singleton construction
As I already mentioned, injecting on construction is default behavior of the framework. If a singleton bean depends on a prototype, Spring creates a single prototype instance dedicated for this particular singleton.
The singleton resuses its single prototype instance through its whole lifecycle.
Let’s run the test we wrote in the previous paragraph and confirm that Spring creates only one prototype.
Option 2: Injecting on prototype method call
Another possibility is to force Spring to create a new prototype instance when every call on prototype’s method.
To achieve this, you need to modify the prototype bean and set its proxy mode. You can do this by modifying the @Scope annotation as follows:
1
2
3
4
5
6
|
@Service @Scope (value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS) class MessageBuilder { // ... } |
By default, Spring doesn’t create a proxy object for a bean and uses the reference to the real bean when injection occurs. However, when you change the proxy mode, Spring creates and injects a special proxy object instead of the real object. The proxy decides on its own when to create the real object.
By setting the proxy mode to ScopedProxyMode.TARGET_CLASS, Spring will create a new instance of the prototype whenever you call its method.
What is the outcome of our test?
We didn’t get one instance, as without the proxy, but as many as six.
Why that happened?
In our test, we call the createMessage() method on the MessageService object twice and this, in turn, executes three methods of the proxied MessageBuilder. Two times three gives us six in total and that is what our test proved.
However, that is not what we wanted and it is even worse. Not only got we more instance than we expected, but also the final result is different than wanted because each instance has a separate state.
The createMessage() method relies on the state that is stored between sequential calls on the prototype. With the given setup, the state is lost on every call to prototype methods. Therefore, setting a proxy mode is a dead end for our problem.
Option 3: Creating prototype on demand with ObjectFactory
The last option is the most flexible because it allows you to create a new prototype instance manually. Thanks to bean definition, Spring knows how to create a prototype. You just need to tell the framework when to do it.
That’s when ObjectFactory comes in.
ObjectFactory is a functional interface from Spring framework designed to provide a reference to beans managed by the application context. The interface defines only one method which returns an instance of the selected bean.
How to use ObjectFactory with prototype beans?
It’s very simple. You just need to define a dependency on the ObjectFactory for the chosen bean. Spring will generate and inject the appropriate implementation of the interface.
Let’s update the MessageService singleton to see the factory in action.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Service class MessageService { private final ObjectFactory<MessageBuilder> messageBuilder; MessageService(ObjectFactory<MessageBuilder> messageBuilder) { this .messageBuilder = messageBuilder; } Message createMessage(String content, String receiver) { return messageBuilder.getObject() .withContent(content) .withReceiver(receiver) .build(); } } |
Thanks to ObjectFactory you can create a prototype instance exactly when you need it.
If we run our test again, it will pass. Every call to the createMessage() method gets its own instance of the prototype.
ObjectFactory in unit test
One of the reasons to use dependency injection is to simplify unit testing. It’s really simple to replace a real dependency with a stub or a mock.
Because ObjectFactory is a function interface, you can easily write its stub implementation with the lambda expression.
1
2
3
4
5
6
|
@Test public void shouldStubFactory() throws Exception { ObjectFactory<MessageBuilder> factory = () -> new MessageBuilder() MessageService sut = new MessageService(factory); //... } |
A few words of conclusion
At this point, you should know about all the possible options for creating prototype beans. You learned that injecting a prototype bean into singleton may be a bit tricky. After reading the article, you know which injecting option is best for your requirements.
Do you like the post and find it useful? Consider subscribing to my site. I’ll let you know about similar topics.