Problem
Many real-world components have to perform certain types of initialization tasks before they are ready to be used. Such tasks include opening a file, opening a network/database connection, allocating memory, and so on. Also, they have to perform the corresponding destruction tasks at the end of their life cycle. So, you have a need to customize bean initialization and destruction in the Spring IoC container.
Solution
In addition to bean registration, the Spring IoC container is also responsible for managing the life cycle of your beans, and it allows you to perform custom tasks at particular points of their life cycle. Your tasks should be encapsulated in callback methods for the Spring IoC container to call at a suitable time.
The following list shows the steps through which the Spring IoC container manages the life cycle of a bean. This list will be expanded as more features of the IoC container are introduced.
1. Create the bean instance either by a constructor or by a factory method.
2. Set the values and bean references to the bean properties.
3. Call the initialization callback methods.
4. The bean is ready to be used.
5. When the container is shut down, call the destruction callback methods.
There are three ways that Spring can recognize your initialization and destruction callback methods. First, your bean can implement the InitializingBean and DisposableBean life cycle interfaces and implement the afterPropertiesSet() and destroy() methods for initializationand destruction. Second, you can set the init-method and destroy-method attributes in the bean declaration and specify the callback method names. In Spring 2.5, you can also annotate the initialization and destruction callback methods with the life cycle annotations @PostConstruct and @PreDestroy, which are defined in JSR-250: Common Annotations for the Java Platform. Then you can register a CommonAnnotationBeanPostProcessor instance in the IoC container to call these callback methods.
How It Works
To understand how the Spring IoC container manages the life cycle of your beans, let’s consider an example involving the checkout function. The following Cashier class can be used to check out the products in a shopping cart. It records the time and the amount of each checkout in a text file.
package com.shop; … public class Cashier { private String name; private String path; private BufferedWriter writer; public void setName(String name) { this.name = name; } public void setPath(String path) { this.path = path; } public void openFile() throws IOException { File logFile = new File(path, name + ".txt"); writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } public void checkout(ShoppingCart cart) throws IOException { double total = 0; for (Product product : cart.getItems()) { total += product.getPrice(); } writer.write(new Date() + "\t" + total + "\r\n"); writer.flush(); } public void closeFile() throws IOException { writer.close(); } }
In the Cashier class, the openFile() method opens the text file with the cashier name as the file name in the specified system path. Each time you call the checkout() method, a checkoutrecord will be appended to the text file. Finally, the closeFile() method closes the file to release its system resources. Then you declare a cashier bean with the name cashier1 in the IoC container. This cashier’s checkout records will be recorded in the file c:/cashier/cashier1.txt. You should create this directory in advance or specify another existing directory.
<beans …> … <bean id="cashier1" class="com.shop.Cashier"> <property name="name" value="cashier1" /> <property name="path" value="c:/cashier" /> </bean> </beans>
However, in the Main class, if you try to check out a shopping cart with this cashier, it will result in a NullPointerException. The reason for this exception is that no one has called the openFile() method for initialization beforehand.
package com.shop; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class Main { public static void main(String[] args) throws Exception { ApplicationContext context = new FileSystemXmlApplicationContext("beans.xml"); … Cashier cashier1 = (Cashier) context.getBean("cashier1"); cashier1.checkout(cart1); } }
Where should you make a call to the openFile() method for initialization? In Java, the initialization tasks should be performed in the constructor. But would it work here if you call the openFile() method in the default constructor of the Cashier class? No, because the openFile() method requires both the name and path properties to be set before it can determine which file to open.
package com.shop; … public class Cashier { … public void openFile() throws IOException { File logFile = new File(path, name + ".txt"); writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } }
When the default constructor is invoked, these properties have not been set yet. So you may add a constructor that accepts the two properties as arguments, and call the openFile() method at the end of this constructor. However, sometimes you may not be allowed to do so, or you might prefer to inject your properties via setter injection. Actually, the best time to call the openFile() method is after all properties have been set by the Spring IoC container.
Implementing the InitializingBean and DisposableBean Interfaces
Spring allows your bean to perform initialization and destruction tasks in the callback methods afterPropertiesSet() and destroy() by implementing the InitializingBean and DisposableBean interfaces. During bean construction, Spring will notice that your bean implements these interfaces and call the callback methods at a suitable time.
package com.shop; … import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; public class Cashier implements InitializingBean, DisposableBean { … public void afterPropertiesSet() throws Exception { openFile(); } public void destroy() throws Exception { closeFile(); } }
Now if you run your Main class again, you will see that a checkout record is appended tothe text file c:/cashier/cashier1.txt. However, implementing such proprietary interfaces will make your beans Spring-specific and thus unable to be reused outside the Spring IoC container.
Setting the init-method and destroy-method Attributes
A better approach of specifying the initialization and destruction callback methods is by setting the init-method and destroy-method attributes in your bean declaration.
<bean id="cashier1" class="com.shop.Cashier" init-method="openFile" destroy-method="closeFile"> <property name="name" value="cashier1" /> <property name="path" value="c:/cashier" /> </bean>
With these two attributes set in the bean declaration, your Cashier class no longer needs to implement the InitializingBean and DisposableBean interfaces. You can also delete the afterPropertiesSet() and destroy() methods as well.
Annotating the @PostConstruct and @PreDestroy Annotations (You can skip the top of this page, this method is easiest.)
In Spring 2.5, you can annotate the initialization and destruction callback methods with the JSR-250 life cycle annotations @PostConstruct and @PreDestroy.
Note To use the JSR-250 annotations, you have to include common-annotations.jar (located in the lib/j2ee directory of the Spring installation) in your classpath. However, if your application is running on Java SE 6 or Java EE 5, you needn’t include this JAR file.
package com.shop; … import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; public class Cashier { … @PostConstruct public void openFile() throws IOException { File logFile = new File(path, name + ".txt"); writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); } @PreDestroy public void closeFile() throws IOException { writer.close(); } }
Then you register a CommonAnnotationBeanPostProcessor instance in the IoC container to call the initialization and destruction callback methods with the life cycle annotations. In this way, you no longer need to specify the init-method and destroy-method attributes for your bean.
<beans …> … <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor" /> <bean id="cashier1" class="com.shop.Cashier"> <property name="name" value="cashier1" /> <property name="path" value="c:/cashier" /> </bean> </beans>
You must include the <context:annotation-config> element in your bean configuration file and a CommonAnnotationBeanPostProcessor instance will automatically get registered. But before this tag can work, you must add the context schema definition to your <beans> root element.
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> … <context:annotation-config /> … </beans>