PolarSPARC

Spring Framework Core Essentials - Part 3


Bhaskar S 07/19/2019


Overview

In Part 2 of this series, we got our hands dirty with constructor injection using the 3 different approaches.

In this part, we will explore the use-case where an application bean is initialized and/or destroyed via a method call using the 3 different approaches.

There will be situations where we want the state of the collaborator (dependee) object(s) to be initialized with data from external resource(s), such as file(s) or a database. Typically, in those situations there will be a separate method defined to initialize the bean and a separate method to clean-up (on shutdown).

Hands-on with Spring Framework Core - 3

We will demonstrate each of the 3 approaches using the simple Hello greeter (in different languages) example with a little twist - the Hello greetings for the different languages will be initialized from a file.

XML based Approach

The simple Hello greeter standalone application will output a greetings message for a chosen language (English, French, etc) and a specified name. In addition, it will tag the sales tax for the country of the chosen language. The hello message (for a chosen language) and the sales tax (for the country of the chosen language) are abstracted in the data package, while the greeter is abstracted in the service package.

The interface WorldHelloDAO from the data package has no change and remains the same.

The following is the POJO SimpleFileWorldHelloDAO from the data package that implements the interface WorldHelloDAO. It reads 'Hello' for a predefined set of languages (English, French, German, Italian, and Spanish) from a file and stores the data in a java.util.Map:

SimpleFileWorldHelloDAO.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Simple File World Hello DAO
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework.data;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class SimpleFileWorldHelloDAO implements WorldHelloDAO {
    private static String COMMA = ",";
    
    private static Log LOG = LogFactory.getLog(SimpleFileWorldHelloDAO.class);
    
    private String fileName = null;
    
    private Map helloByLang = null;
    
    private Stream stream = null;
    
    public SimpleFileWorldHelloDAO() {
        LOG.info("Created new instance of SimpleFileWorldHelloDAO");
    }
    
    public String getFileName() {
        return fileName;
    }

    public void setFileName(String filename) {
        this.fileName = filename;
    }

    public void init() throws Exception {
        LOG.info("Initializing cache from the file " + fileName);
        
        Path path = Paths.get(getClass().getClassLoader().getResource(fileName).toURI());
        
        stream = Files.lines(path);
        
        helloByLang = stream.map(line -> line.split(COMMA))
                            .collect(Collectors.toMap(tokens -> tokens[0].toLowerCase(), 
                                                      tokens -> tokens[1]));
        
        LOG.info("Cache successfully initialized with " + helloByLang.toString());
    }
    
    public void destroy() {
        LOG.info("Closing the stream resource");
        
        if (stream != null) {
            stream.close();
        }
        
        LOG.info("Successfully closed the stream resource");
    }
    
    @Override
    public String fetch(String lang) {
        String msg = null;
        
        if (lang != null && lang.trim().isEmpty() == false) {
            msg = helloByLang.get(lang.toLowerCase());
        }
        
        if (msg == null) {
            msg = helloByLang.get("english");
        }
        
        return msg;
    }
}

The method init() opens and reads from the I/O stream for the specified file with the 'Hello' data for a predefined set of languages. To work in Spring, the method must take no parameter arguments and return a void.

The method destroy() closes the I/O stream when the application shutsdown. To work in Spring, the method must take no parameter arguments and return a void.

The interface HelloGreeter as well as its implementation POJO SimpleHelloGreeter from the service package have no changes and remains the same.

The following are the contents of the data file sample7.csv that contains the 'Hello' data for a predefined set of languages:

sample7.csv
English,H3ll0
French,B0nj0ur
German,Gut3n T4g
Italian,S4lv3
Spanish,H0l4

The following XML file sample7-beans.xml specifies the configuration metadata for the Spring Framework IoC container:

sample7-beans.xml
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">
                        
    <bean id="simpleWorldHello" class="com.polarsparc.springframework.data.SimpleFileWorldHelloDAO"
          init-method="init" destroy-method="destroy">
        <property name="fileName" value="sample7.csv" />
    </bean>
    
    <bean name="simpleHelloGreeter" class="com.polarsparc.springframework.service.SimpleHelloGreeter">
        <property name="separator" value=":" />
        <property name="worldHello" ref="simpleWorldHello" />
    </bean>
</beans>

To indicate the initialization method to invoke, use the init-method attribute on the corresponding <bean> element.

Similarly, to indicate the clean-up method to invoke, use the destroy-method attribute on the corresponding <bean> element.

See Figure.1 below.

Init and Destroy
Figure.1

Note that one cannot pass any parameter arguments to either the initialization or the clean-up methods.

The initialization method will be invoked by the Spring Framework IoC container *ONLY* after all the bean properties have been autowired.

Now that we have got the application POJOs and the configuration metadata defined in an XML file, it is time to bring them together into the Spring Framework IoC container as a standalone Java application.

The following is the Spring Framework application Sample7:

Sample7.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Sample 7
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.polarsparc.springframework.service.HelloGreeter;

public class Sample7 {
    private static Log LOG = LogFactory.getLog(Sample7.class);

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new ClassPathXmlApplicationContext("/sample7-beans.xml");
        
        HelloGreeter greeter = applicationContext.getBean("simpleHelloGreeter", HelloGreeter.class);
        
        LOG.info(greeter.greetings("German", "Alligator"));
        LOG.info(greeter.greetings("French", "Bear"));
        
        ((ClassPathXmlApplicationContext)applicationContext).close();
    }
}

Notice that we are calling the close() method on the instance of the Spring Framework container (applicationContext). This will shutdown the container in an appropriate way.

Executing the Java program Sample07 listed above should generate an output similar to the following:

Output.1

Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO 
INFO: Created new instance of SimpleFileWorldHelloDAO
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO init
INFO: Initializing cache from the file sample7.csv
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO init
INFO: Cache successfully initialized with {german=Gut3n T4g, spanish=H0l4, english=H3ll0, italian=S4lv3, french=B0nj0ur}
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.Sample7 main
INFO: Gut3n T4g: Alligator
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.Sample7 main
INFO: B0nj0ur: Bear
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO destroy
INFO: Closing the stream resource
Jul 19, 2019 8:09:20 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO destroy
INFO: Successfully closed the stream resource
Annotation based Approach

The interface WorldHelloDAO from the data package has no change and remains the same.

The following is the POJO SimpleFileWorldHelloDAO2 from the data package that implements the interface WorldHelloDAO. It reads 'Hello' for a predefined set of languages (English, French, German, Italian, and Spanish) from a file and stores the data in a java.util.Map:

SimpleFileWorldHelloDAO2.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Simple File World Hello DAO 2
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework.data;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@Repository("fileWorldHello")
public class SimpleFileWorldHelloDAO2 implements WorldHelloDAO {
    private static String COMMA = ",";
    
    private static Log LOG = LogFactory.getLog(SimpleFileWorldHelloDAO2.class);
    
    private String fileName = null;
    
    private Map helloByLang = null;
    
    private Stream stream = null;
    
    public SimpleFileWorldHelloDAO2() {
        LOG.info("Created new instance of SimpleFileWorldHelloDAO");
    }
    
    public String getFileName() {
        return fileName;
    }

    @Value("${env.csv.file:sample7.csv}")
    public void setFileName(String filename) {
        this.fileName = filename;
    }

    @PostConstruct
    public void init() throws Exception {
        LOG.info("Initializing cache from the file " + fileName);
        
        Path path = Paths.get(getClass().getClassLoader().getResource(fileName).toURI());
        
        stream = Files.lines(path);
        
        helloByLang = stream.map(line -> line.split(COMMA))
                            .collect(Collectors.toMap(tokens -> tokens[0].toLowerCase(), 
                                                      tokens -> tokens[1]));
        
        LOG.info("Cache successfully initialized with " + helloByLang.toString());
    }
    
    @PreDestroy
    public void destroy() {
        LOG.info("Closing the stream resource");
        
        if (stream != null) {
            stream.close();
        }
        
        LOG.info("Successfully closed the stream resource");
    }
    
    @Override
    public String fetch(String lang) {
        String msg = null;
        
        if (lang != null && lang.trim().isEmpty() == false) {
            msg = helloByLang.get(lang.toLowerCase());
        }
        
        if (msg == null) {
            msg = helloByLang.get("english");
        }
        
        return msg;
    }
}

Notice the use of the annotation @Repository in the code SimpleFileWorldHelloDAO2 above. The value inside the @Repository annotation specifies the bean id.

Notice the use of the @PostConstruct annotation in the code SimpleFileWorldHelloDAO2 above on the method init(). This annotation signals the container to invoke the corresponding method after the bean creation and autowiring of the bean properties.

Also, notice the use of the @PreDestroy annotation in the code SimpleFileWorldHelloDAO2 above on the method destroy(). This annotation signals the container to invoke the corresponding method before the container shutdown.

See Figure.2 below.

PostConstruct and PreDestroy
Figure.2

The interface HelloGreeter from the service package has no change and remains the same.

The following is the POJO SimpleHelloGreeter7 from the service package that implements the interface HelloGreeter. It returns a greetings message that consists of the 'Hello' for the chosen language and the specified name:

SimpleHelloGreeter7.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Simple Hello Greeter 7
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.polarsparc.springframework.data.WorldHelloDAO;

@Service("fileHelloGreeter")
public class SimpleHelloGreeter7 implements HelloGreeter {
    private static String SPACE = " ";
    private static String STRANGER = "Stranger";
    
    private String separator = null;
    
    private WorldHelloDAO worldHello = null;
    
    public SimpleHelloGreeter7() {
    }
    
    public String getSeparator() {
        return separator;
    }

    @Value("${env.greeter.sep:-}")
    public void setSeparator(String separator) {
        this.separator = separator;
    }

    public WorldHelloDAO getWorldHello() {
        return worldHello;
    }

    @Autowired
    @Qualifier("fileWorldHello")
    public void setWorldHello(WorldHelloDAO worldHello) {
        this.worldHello = worldHello;
    }

    @Override
    public String greetings(String lang, String name) {
        String msg = null;
        
        if (name != null && name.trim().isEmpty() == false) {
            msg = new StringBuilder()
                      .append(worldHello.fetch(lang.toLowerCase()))
                      .append(separator)
                      .append(SPACE)
                      .append(name)
                      .toString();
        }
        else {
            msg = new StringBuilder()
                      .append(worldHello.fetch(lang.toLowerCase()))
                      .append(separator)
                      .append(SPACE)
                      .append(STRANGER)
                      .toString();
        }
        
        return msg;
    }
}

Notice the use of the @Qualifier annotation on the method setWorldHello in the code SimpleHelloGreeter7 above. The string value inside should match the name of the bean specified inside the @Repository annotation in the code SimpleFileWorldHelloDAO2. The @Qualifier annotation is used in conjunction with the @Autowired annotation to indicate which bean to use for autowiring.

We will reuse the XML file sample2-beans.xml from Part 1 of this series.

The following is the Spring Framework application Sample8:

Sample8.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Sample 8
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import com.polarsparc.springframework.service.HelloGreeter;

public class Sample8 {
    private static Log LOG = LogFactory.getLog(Sample8.class);

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new GenericXmlApplicationContext("/sample2-beans.xml");
        
        HelloGreeter greeter = applicationContext.getBean("fileHelloGreeter", HelloGreeter.class);
        
        LOG.info(greeter.greetings("English", "Zebra"));
        LOG.info(greeter.greetings("Spanish", "Mule"));
        
        ((GenericXmlApplicationContext)applicationContext).close();
    }
}

Executing the Java program Sample08 listed above should generate an output similar to the following:

Output.2

Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO2 
INFO: Created new instance of SimpleFileWorldHelloDAO
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO2 init
INFO: Initializing cache from the file sample7.csv
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO2 init
INFO: Cache successfully initialized with {german=Gut3n T4g, spanish=H0l4, english=H3ll0, italian=S4lv3, french=B0nj0ur}
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleMemoryWorldHelloDAO2 
INFO: Initialized a new instance of SimpleMemoryWorldHelloDAO2
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleMemoryWorldHelloDAO3 
INFO: Initialized a new instance of SimpleMemoryWorldHelloDAO3
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleMemoryWorldSalesTaxDAO2 
INFO: Initialized a new instance of SimpleMemoryWorldSalesTaxDAO2
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.Sample8 main
INFO: H3ll0- Zebra
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.Sample8 main
INFO: H0l4- Mule
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO2 destroy
INFO: Closing the stream resource
Jul 19, 2019 8:52:21 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO2 destroy
INFO: Successfully closed the stream resource
JavaConfig based Approach

The interface WorldHelloDAO as well as its implementation POJO SimpleFileWorldHelloDAO from the data package have no changes and remains the same.

The interface HelloGreeter as well as its implementation POJO SimpleHelloGreeter3 from the service package have no changes and remains the same.

The following is the JavaConfig POJO Sample9Config from the config package:

Sample9Config.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Sample 9 Config
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.polarsparc.springframework.data.SimpleFileWorldHelloDAO;
import com.polarsparc.springframework.data.WorldHelloDAO;
import com.polarsparc.springframework.service.HelloGreeter;
import com.polarsparc.springframework.service.SimpleHelloGreeter3;

@Configuration
public class Sample9Config {
    @Bean(initMethod="init", destroyMethod="destroy")
    public WorldHelloDAO simpleWorldHello(@Value("${env.csv.file:sample7.csv}") String filename) {
        SimpleFileWorldHelloDAO worldHello = new SimpleFileWorldHelloDAO();
        worldHello.setFileName(filename);
        
        return worldHello;
    }
    
    @Bean
    public HelloGreeter simpleHelloGreeter() {
        return new SimpleHelloGreeter3();
    }
}

Notice the use of two attributes initMethod and destroyMethod inside the @Bean annotation. These attributes indicate the initialization and clean-up methods respectively.

See Figure.3 below.

initMethod and destroyMethod
Figure.3

The following is the Spring Framework application Sample9:

Sample9.java
/*
 * Topic:  Spring Framework Core Essentials
 * 
 * Name:   Sample 9
 * 
 * Author: Bhaskar S
 * 
 * URL:    https://www.polarsparc.com
 */

package com.polarsparc.springframework;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.polarsparc.springframework.config.Sample9Config;
import com.polarsparc.springframework.service.HelloGreeter;

public class Sample9 {
    private static Log LOG = LogFactory.getLog(Sample9.class);

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(Sample9Config.class);
        
        HelloGreeter greeter = applicationContext.getBean("simpleHelloGreeter", HelloGreeter.class);
        
        LOG.info(greeter.greetings("french", "Dog"));
        LOG.info(greeter.greetings("italian", "Snake"));
        
        ((AnnotationConfigApplicationContext)applicationContext).close();
    }
}

Executing the Java program Sample09 listed above should generate an output similar to the following:

Output.3

Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO 
INFO: Created new instance of SimpleFileWorldHelloDAO
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO init
INFO: Initializing cache from the file sample7.csv
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO init
INFO: Cache successfully initialized with {german=Gut3n T4g, spanish=H0l4, english=H3ll0, italian=S4lv3, french=B0nj0ur}
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.Sample9 main
INFO: B0nj0ur- Dog
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.Sample9 main
INFO: S4lv3- Snake
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO destroy
INFO: Closing the stream resource
Jul 19, 2019 9:07:28 PM com.polarsparc.springframework.data.SimpleFileWorldHelloDAO destroy
INFO: Successfully closed the stream resource

More to be covered in the next part of this series ...

References

[1] Spring Framework Core Essentials - Part 1

[2] Spring Framework Core Essentials - Part 2

[3] Spring Framework

[4] Spring Framework Core

[5] Spring Framework API



© PolarSPARC