By kswaughs | Thursday, September 19, 2024

Spring Boot ShedLock Example

Spring ShedLock is a distributed lock for scheduled tasks. It ensures that only one instance of a scheduled task runs at the same time in a distributed environment, such as a cluster of microservices. This is particularly useful for tasks that should not be executed concurrently, like data processing or cleanup jobs.

How ShedLock Works

Lock Provider: A lock provider is configured to manage the locks. This could be a database, Redis, etc.

Annotations: Tasks are annotated with @SchedulerLock to specify lock names and durations.

Execution: When a task is scheduled to run, ShedLock checks if a lock is available. If it is, the task runs and the lock is held for the specified duration.

Release: After the task completes or the lock duration expires, the lock is released, allowing other instances to acquire it.


Step 1. Add Dependencies


pom.xml
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>4.27.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>4.27.0</version>
</dependency>

Step 2. Create Configuration Class

Create a configuration class to set up ShedLock with a JDBC lock provider.

ShedLockConfig
package com.kswaughs.batch.config;

import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "PT30S")
public class ShedLockConfig {

    @Bean
    public LockProvider lockProvider(JdbcTemplate jdbcTemplate) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(jdbcTemplate)
                .usingDbTime() // Works with PostgreSQL, MySQL, MariaDB, MS SQL, Oracle, DB2
                .build()
        );
    }
}

Step 3: Create Scheduled Task

Create a scheduled task that uses ShedLock to ensure only one instance of the task runs at a time.

ScheduledTasks
package com.kswaughs.batch.job;

import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class ScheduledTasks {

    @Scheduled(cron = "0 0/5 * * * ?")
    @SchedulerLock(name = "TaskName", lockAtMostFor = "PT10M", lockAtLeastFor = "PT5M")
    public void performTask() {
        // Task implementation
        System.out.println("Scheduled task is running...");
    }
}

Step 4: Enable Scheduling

Ensure that scheduling is enabled in your Spring Boot application by adding @EnableScheduling to your main application class.

Main class
package com.kswaughs.batch;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Step 5: Create Database Table - ShedLock

The ShedLock library does not create the table automatically. You need to create the table manually. Here is the SQL script to create the ShedLock table.

Database Table
CREATE TABLE shedlock(
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP(3) NOT NULL,
    locked_at TIMESTAMP(3) NOT NULL,
    locked_by VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

Key Features
Distributed LockingEnsures that only one instance of a task runs at a time across multiple nodes.
Multiple Lock ProvidersSupports various lock providers like JDBC, MongoDB, Redis, ZooKeeper, etc.
AnnotationsUses annotations like @SchedulerLock to easily lock scheduled tasks.
ConfigurationCan be configured using Spring's configuration classes and properties.
IntegrationSeamlessly integrates with Spring's @Scheduled tasks.

Benefits
Prevents Duplicate ExecutionEnsures that tasks are not executed concurrently in a distributed setup.
Easy to UseSimple annotations and configuration make it easy to integrate into existing Spring applications.
FlexibleSupports various backends for lock storage, making it adaptable to different environments.

Recommend this on


By kswaughs | Monday, June 1, 2020

Spring JdbcTemplate ResultSetExtractor Junit test example

This example shows how to write junit to test spring ResultSetExtractor functionality while mocking JdbcTemplate with Mockito.

This also increases code coverage of ResultSetExtractor code.

Below is the DAO class that returns Map of employees with ResultSetExtractor using Lambdas

Spring JdbcTemplate ResultSetExtractor Example with Lambdas
package com.kswaughs.dao;

import java.sql.ResultSet;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.kswaughs.beans.Employee;

@Repository
public class EmployeeDAO {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public Map<Integer, Employee> getEmployeeMap() {

        return jdbcTemplate.query("SELECT ID, NAME, STATUS FROM EMPLOYEE",

            (ResultSet rs) -> {

                Map<Integer, Employee> employeeMap = new HashMap<>();

                while (rs.next()) {

                    Employee employee = new Employee();

                    employee.setId(rs.getInt("ID"));
                    employee.setName(rs.getString("NAME"));
                    employee.setStatus(rs.getBoolean("STATUS"));

                    employeeMap.put(employee.getId(), employee);
                }

                return employeeMap;
        });
    }
}

Below is the Junit test class to test ResultSetExtractor code by mocking JdbcTemplate to return two rows.

Spring JdbcTemplate ResultSetExtractor Junit test example
package com.kswaughs.dao;

import static org.mockito.Mockito.when;

import java.sql.ResultSet;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;

import com.kswaughs.beans.Employee;

@RunWith(MockitoJUnitRunner.class)
public class EmployeeDAOTest {

    @Mock
    private JdbcTemplate jdbcTemplate;

    @InjectMocks
    private EmployeeDAO employeeDAO;

    @SuppressWarnings("unchecked")
    @Test
    public void testGetEmployeeMap() {

        Mockito.when(jdbcTemplate.query(
            ArgumentMatchers.anyString(), ArgumentMatchers.any(ResultSetExtractor.class)))
            .thenAnswer((invocation) -> {

                ResultSetExtractor<Map<Integer, Employee>> resultSetExtractor = 
                    (ResultSetExtractor<Map<Integer, Employee>>) invocation.getArgument(1);
                
                ResultSet rs = Mockito.mock(ResultSet.class);
                
                // two times it returns true and third time returns false.
                when(rs.next()).thenReturn(true, true, false);

                // Mock ResultSet to return two rows.
                Mockito.when(rs.getInt(ArgumentMatchers.eq("ID")))
                    .thenReturn(506, 400);
                Mockito.when(rs.getString(ArgumentMatchers.eq("NAME")))
                    .thenReturn("Jim Carrey", "John Travolta");
                Mockito.when(rs.getBoolean(ArgumentMatchers.eq("STATUS")))
                    .thenReturn(true, false);

                return resultSetExtractor.extractData(rs);
        });

        Map<Integer, Employee> employeeMap = employeeDAO.getEmployeeMap();
        
        // Assert First Row
        assertFirstUser(employeeMap.get(506));

        // Assert Second Row
        assertSecondUser(employeeMap.get(400));
    }

    public void assertFirstUser(Employee employee) {
        Assert.assertEquals(Integer.valueOf(506), employee.getId());
        Assert.assertEquals("Jim Carrey", employee.getName());
        Assert.assertTrue(employee.isStatus());
    }
    
    public void assertSecondUser(Employee employee) {
        Assert.assertEquals(Integer.valueOf(400), employee.getId());
        Assert.assertEquals("John Travolta", employee.getName());
        Assert.assertFalse(employee.isStatus());
    }
}

Recommend this on


By kswaughs | Thursday, May 28, 2020

Spring JdbcTemplate RowMapper Junit Test Example

This example shows how to write junit to test Spring RowMapper functionality while mocking JdbcTemplate with Mockito.

This also increases code coverage of RowMapper code.

Below is the DAO class that returns list of users with RowMapper using Lambdas.

Spring JdbcTemplate RowMapper Example with Lambdas
package com.kswaughs.dao;

import java.sql.ResultSet;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.kswaughs.beans.User;

@Repository
public class UserDAO {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<User> getAllUsers() {

        return jdbcTemplate.query("SELECT ID, NAME, STATUS FROM USER", 
            
            (ResultSet rs, int rowNum) -> {

                User user = new User();

                user.setId(rs.getInt("ID"));
                user.setName(rs.getString("NAME"));
                user.setStatus(rs.getBoolean("STATUS"));

                return user;
            });
        }
    }

Below is the Junit test class to test RowMapper code by mocking JdbcTemplate to return two rows.

Spring JdbcTemplate RowMapper Junit test example
package com.kswaughs.dao;

import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import com.kswaughs.beans.User;

@RunWith(MockitoJUnitRunner.class)
public class UserDAOTest {

    @Mock
    private JdbcTemplate jdbcTemplate;

    @InjectMocks
    private UserDAO userDAO;

    @SuppressWarnings("unchecked")
    @Test
    public void testGetAllUsers() {

        Mockito.when(jdbcTemplate.query(
            ArgumentMatchers.anyString(), ArgumentMatchers.any(RowMapper.class)))
            .thenAnswer((invocation) -> {

                RowMapper<User> rowMapper = (RowMapper<User>) invocation.getArgument(1);
                ResultSet rs = Mockito.mock(ResultSet.class);

                // Mock ResultSet to return two rows.
                Mockito.when(rs.getInt(ArgumentMatchers.eq("ID")))
                    .thenReturn(506, 400);
                Mockito.when(rs.getString(ArgumentMatchers.eq("NAME")))
                    .thenReturn("Jim Carrey", "John Travolta");
                Mockito.when(rs.getBoolean(ArgumentMatchers.eq("STATUS")))
                    .thenReturn(true, false);

                List<User> users = new ArrayList<>();
                users.add(rowMapper.mapRow(rs, 0));
                users.add(rowMapper.mapRow(rs, 1));

                return users;
        });

        List<User> users = userDAO.getAllUsers();
        
        // Assert First Row
        assertFirstUser(users.get(0));

        // Assert Second Row
        assertSecondUser(users.get(1));
    }

    public void assertFirstUser(User user) {
        Assert.assertEquals(Integer.valueOf(506), user.getId());
        Assert.assertEquals("Jim Carrey", user.getName());
        Assert.assertTrue(user.isStatus());
    }
    
    public void assertSecondUser(User user) {
        Assert.assertEquals(Integer.valueOf(400), user.getId());
        Assert.assertEquals("John Travolta", user.getName());
        Assert.assertFalse(user.isStatus());
    }
}

Recommend this on


By kswaughs | Thursday, November 30, 2017

Spring Boot Soap Web Service Client Example

This post explains how to consume a soap web service with Spring Boot using org.springframework.ws.client.core.WebServiceTemplate class.

In this example, we will call Book Store web service which is already explained in the following post.

Spring Boot Soap Web Service Example

Step 1: Maven setup

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.kswaughs.spring</groupId>
    <artifactId>spring-boot-soap-service-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-boot-soap-service-client</name>
    <description>Demo project for Spring Boot SOAP Web service Client</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven-jaxb2-plugin.version>0.13.2</maven-jaxb2-plugin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jvnet.jaxb2.maven2</groupId>
                <artifactId>maven-jaxb2-plugin</artifactId>
                <version>${maven-jaxb2-plugin.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <schemaDirectory>
                       ${project.basedir}/src/main/resources/wsdl
                    </schemaDirectory>
                    <schemaIncludes>
                        <include>*.wsdl</include>
                    </schemaIncludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: To consume a web service, we need a WSDL file. Copy your WSDL file to resources folder.

src/main/resources/wsdl/books.wsdl
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
    xmlns:sch="http://com/kswaughs/services/bookSvc"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://com/kswaughs/services/bookSvc"
    targetNamespace="http://com/kswaughs/services/bookSvc">
    <wsdl:types>
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
            elementFormDefault="qualified"
            targetNamespace="http://com/kswaughs/services/bookSvc">
            <xs:element name="getBookRequest">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="name" type="xs:string" />
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
            <xs:element name="getBookResponse">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="book" type="tns:book" />
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
            <xs:element name="addBookRequest">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="book" type="tns:book" />
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
            <xs:element name="addBookResponse">
                <xs:complexType>
                    <xs:sequence>
                        <xs:element name="status" type="xs:string" />
                    </xs:sequence>
                </xs:complexType>
            </xs:element>
            <xs:complexType name="book">
                <xs:sequence>
                    <xs:element name="name" type="xs:string" />
                    <xs:element name="author" type="xs:string" />
                    <xs:element name="price" type="xs:string" />
                </xs:sequence>
            </xs:complexType>
        </xs:schema>
    </wsdl:types>
    <wsdl:message name="getBookRequest">
        <wsdl:part element="tns:getBookRequest" name="getBookRequest">
        </wsdl:part>
    </wsdl:message>
    <wsdl:message name="addBookRequest">
        <wsdl:part element="tns:addBookRequest" name="addBookRequest">
        </wsdl:part>
    </wsdl:message>
    <wsdl:message name="addBookResponse">
        <wsdl:part element="tns:addBookResponse" name="addBookResponse">
        </wsdl:part>
    </wsdl:message>
    <wsdl:message name="getBookResponse">
        <wsdl:part element="tns:getBookResponse" name="getBookResponse">
        </wsdl:part>
    </wsdl:message>
    <wsdl:portType name="BooksPort">
        <wsdl:operation name="getBook">
            <wsdl:input message="tns:getBookRequest" name="getBookRequest">
            </wsdl:input>
            <wsdl:output message="tns:getBookResponse" name="getBookResponse">
            </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="addBook">
            <wsdl:input message="tns:addBookRequest" name="addBookRequest">
            </wsdl:input>
            <wsdl:output message="tns:addBookResponse" name="addBookResponse">
            </wsdl:output>
        </wsdl:operation>
    </wsdl:portType>
    <wsdl:binding name="BooksPortSoap11" type="tns:BooksPort">
        <soap:binding style="document"
            transport="http://schemas.xmlsoap.org/soap/http" />
        <wsdl:operation name="getBook">
            <soap:operation soapAction="" />
            <wsdl:input name="getBookRequest">
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output name="getBookResponse">
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
        <wsdl:operation name="addBook">
            <soap:operation soapAction="" />
            <wsdl:input name="addBookRequest">
                <soap:body use="literal" />
            </wsdl:input>
            <wsdl:output name="addBookResponse">
                <soap:body use="literal" />
            </wsdl:output>
        </wsdl:operation>
    </wsdl:binding>
    <wsdl:service name="BooksPortService">
        <wsdl:port binding="tns:BooksPortSoap11" name="BooksPortSoap11">
            <soap:address location="http://localhost:8088/MyApp/ws" />
        </wsdl:port>
    </wsdl:service>
</wsdl:definitions>

Step 3: Generate Domain classes based on Schema defined. When you run Maven build, maven-jaxb2-plugin will generate the java files and stores in target/generated-sources/xjc folder. Configure this folder as source folder in your eclipse IDE.

Step 4: Create Web Service Client Configuration class.

WebSvcClientConfig.java
package com.kswaughs.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;

@Configuration
public class WebSvcClientConfig {
    
    @Value("${books.svc.url}")
    private String url;

    @Bean
    Jaxb2Marshaller jaxb2Marshaller() {
    
        Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
        jaxb2Marshaller.setContextPath("com.kswaughs.services.booksvc");

        return jaxb2Marshaller;
    }

    @Bean
    public WebServiceTemplate webServiceTemplate() {
    
        WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
        webServiceTemplate.setMarshaller(jaxb2Marshaller());
        webServiceTemplate.setUnmarshaller(jaxb2Marshaller());
        webServiceTemplate.setDefaultUri(url);

        return webServiceTemplate;
    }

}

Step 5: Create a Service Client class which will consume the web service through above configured WebServiceTemplate class.

BookServiceClient.java
package com.kswaughs.services;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ws.client.core.WebServiceTemplate;

import com.kswaughs.services.booksvc.AddBookRequest;
import com.kswaughs.services.booksvc.AddBookResponse;
import com.kswaughs.services.booksvc.Book;
import com.kswaughs.services.booksvc.GetBookRequest;
import com.kswaughs.services.booksvc.GetBookResponse;
import com.kswaughs.services.booksvc.ObjectFactory;

@Component
public class BookServiceClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(BookServiceClient.class);

    @Autowired
    private WebServiceTemplate webServiceTemplate;

    public String addBook(String name, String author, String price) {
        
        ObjectFactory factory = new ObjectFactory();
        
        AddBookRequest req = factory.createAddBookRequest();

        Book book = new Book();
        book.setAuthor(author);
        book.setName(name);
        book.setPrice(price);

        req.setBook(book);

        LOGGER.info("Client sending book[Name={},", book.getName());

        AddBookResponse resp = (AddBookResponse) webServiceTemplate.marshalSendAndReceive(req);

        LOGGER.info("Client received status='{}'", resp.getStatus());
        
        return resp.getStatus();
    }

    public Book getBook(String name) {
        
        ObjectFactory factory = new ObjectFactory();
        
        GetBookRequest req = factory.createGetBookRequest();

        req.setName(name);

        LOGGER.info("Client sending book[Name={},", name);

        GetBookResponse resp = (GetBookResponse) webServiceTemplate.marshalSendAndReceive(req);

        LOGGER.info("Client received book='{}'", resp.getBook());
        
        return resp.getBook();
    }

}

Step 6: To test this application, I am creating a sample class which will reads the user entered commands from console and calls the service client methods.

BookCommandLineListener.java
package com.kswaughs.services;

import java.util.Arrays;
import java.util.Scanner;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.kswaughs.services.booksvc.Book;

@Component
public class BookCommandLineListener  {

    private static final Logger LOGGER = LoggerFactory
        .getLogger(BookCommandLineListener.class);

    @Autowired
    private BookServiceClient bookSvcClient;

    @Scheduled(fixedDelay=1000)
    public void run() throws Exception {

        try (Scanner scanner = new Scanner(System.in)) {

            while (true) {

                LOGGER.info("Enter your command");
                String text = scanner.nextLine();

                LOGGER.info("you entered :{}", text);
                String[] args = text.split("\\s+");

                LOGGER.info("args :{}", Arrays.toString(args));

                if ("ADD".equalsIgnoreCase(args[0])) {

                    String name = args[1];
                    String author = args[2];
                    String price = args[3];

                    String status = bookSvcClient.addBook(name, author, price);

                    LOGGER.info("Book Added Status :{}", status);

                } else if ("GET".equalsIgnoreCase(args[0])) {
                    String name = args[1];

                    Book book = bookSvcClient.getBook(name);

                    if(book != null) {
                        LOGGER.info("GET Book Details : Name:{}, Author:{}, Price:{}", 
                                book.getName(), book.getAuthor(), book.getPrice());
                    }

                } else {
                    
                    LOGGER.info("Invalid Command."); 
                }
            }
        }
    }
}

Step 7: Create Spring Boot Main Application Class

SpringBootSoapServiceClientApplication.java
package com.kswaughs;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SpringBootSoapServiceClientApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSoapServiceClientApplication.class, args);
    }
    
}

Step 8: Configure web service url in application.properties

src/main/resources/application.properties
books.svc.url=http://localhost:8088/MyApp/ws

Testing the Application

Test Add method
2017-12-01 11:19:13.376  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : Enter your command
ADD SpringGuide kswaughs 10
2017-12-01 11:20:27.922  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : you entered :ADD SpringGuide kswaughs 10
2017-12-01 11:20:27.922  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : args :[ADD, SpringGuide, kswaughs, 10]
2017-12-01 11:20:27.922  INFO 10440 --- [pool-1-thread-1] com.kswaughs.services.BookServiceClient  : Client sending book[Name=SpringGuide,
2017-12-01 11:20:28.930  INFO 10440 --- [pool-1-thread-1] com.kswaughs.services.BookServiceClient  : Client received status='SUCCESS'
2017-12-01 11:20:28.930  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : Book Added Status :SUCCESS

Test Get method
2017-12-01 11:20:28.930  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : Enter your command
GET SpringGuide
2017-12-01 11:20:43.711  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : you entered :GET SpringGuide
2017-12-01 11:20:43.711  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : args :[GET, SpringGuide]
2017-12-01 11:20:43.720  INFO 10440 --- [pool-1-thread-1] com.kswaughs.services.BookServiceClient  : Client sending book[Name=SpringGuide,
2017-12-01 11:20:43.740  INFO 10440 --- [pool-1-thread-1] com.kswaughs.services.BookServiceClient  : Client received book='com.kswaughs.services.booksvc.Book@fa6b61e'
2017-12-01 11:20:43.740  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : GET Book Details : Name:SpringGuide, Author:kswaughs, Price:10

Test invalid command
2017-12-01 11:20:43.740  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : Enter your command
ABCD
2017-12-01 11:20:56.944  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : you entered :ABCD
2017-12-01 11:20:56.944  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : args :[ABCD]
2017-12-01 11:20:56.956  INFO 10440 --- [pool-1-thread-1] c.k.services.BookCommandLineListener     : Invalid Command.

Recommend this on


By kswaughs | Wednesday, March 15, 2017

Spring Boot Soap Web Service Example

This post explains how to develop a soap based web service with Spring Boot. In this Book Store example, We will create a web service that allows to add books information and get the book information.

Step 1: Maven setup

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
    http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.1.RELEASE</version>
    </parent>
    <groupId>com.kswaughs.spring</groupId>
    <artifactId>spring_boot_soapwebsvc</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>
    <name>spring_boot_soapwebsvc</name>
    <description>Spring Boot SOAP WEB Service Example</description>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web-services</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>jaxb2-maven-plugin</artifactId>
                <version>1.6</version>
                <executions>
                    <execution>
                        <id>xjc</id>
                        <goals>
                            <goal>xjc</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <schemaDirectory>${project.basedir}/src/main/resources/</schemaDirectory>
                    <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    <clearOutputDir>false</clearOutputDir>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: Create an XML Schema to define the WebService domain. This book store web service provides two operations to add & get Book details.

src/main/resources/books.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:tns="http://com/kswaughs/services/bookSvc"
    targetNamespace="http://com/kswaughs/services/bookSvc"
    elementFormDefault="qualified">

    <xs:element name="getBookRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="name" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="getBookResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="book" type="tns:book" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="addBookRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="book" type="tns:book" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    
    <xs:element name="addBookResponse">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="status" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    
    <xs:complexType name="book">
        <xs:sequence>
            <xs:element name="name" type="xs:string" />
            <xs:element name="author" type="xs:string" />
            <xs:element name="price" type="xs:string" />
        </xs:sequence>
    </xs:complexType>
    
</xs:schema>

Step 3: Generate Domain classes based on Schema defined. When you run Maven build, jaxb2-maven-plugin will generate the java files and stores in src/main/java folder.

Step 4: Create a book repository class to store books details and Initialise the list with few books.

BookRepository.java
package com.kswaughs.repo;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;

import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import com.kswaughs.services.booksvc.Book;

@Component
public class BookRepository {
    
    private static final List<Book> books = new ArrayList<Book>();

    @PostConstruct
    public void initData() {
        Book book1 = new Book();
        book1.setName("The Family Way");
        book1.setAuthor("Tony Parsons");
        book1.setPrice("10 $");
        
        books.add(book1);

        Book book2 = new Book();
        book2.setName("Count To Ten");
        book2.setAuthor("Karen Rose");
        book2.setPrice("12 $");
        
        books.add(book2);
    }

    public Book findBook(String name) {
        Assert.notNull(name);

        Book result = null;

        for (Book book : books) {
            if (name.equals(book.getName())) {
                result = book;
            }
        }

        return result;
    }
    
    public void addBook(Book book) {
        books.add(book);
    }
}

Step 5: Create BookService EndPoint class to handle the incoming SOAP requests.

BookEndPoint.java
package com.kswaughs.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

import com.kswaughs.repo.BookRepository;
import com.kswaughs.services.booksvc.AddBookRequest;
import com.kswaughs.services.booksvc.AddBookResponse;
import com.kswaughs.services.booksvc.GetBookRequest;
import com.kswaughs.services.booksvc.GetBookResponse;

@Endpoint
public class BookEndPoint {
    
    private static final String NAMESPACE_URI = "http://com/kswaughs/services/bookSvc";

    private BookRepository bookRepository;

    @Autowired
    public BookEndPoint(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    // To handle getBookRequest
    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getBookRequest")
    @ResponsePayload
    public GetBookResponse getBook(@RequestPayload GetBookRequest request) {
        GetBookResponse response = new GetBookResponse();
        response.setBook(bookRepository.findBook(request.getName()));

        return response;
    }
    
    // To handle addBookRequest
    @PayloadRoot(namespace = NAMESPACE_URI, localPart = "addBookRequest")
    @ResponsePayload
    public AddBookResponse addBook(@RequestPayload AddBookRequest request) {
        AddBookResponse response = new AddBookResponse();
        bookRepository.addBook(request.getBook());
        response.setStatus("SUCCESS");
        return response;
    }
}

Step 6: Configure Web Service Spring beans

SpringWebConfig.java
package com.kswaughs.config;

import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;

@EnableWs
@Configuration
public class SpringWebConfig extends WsConfigurerAdapter {
    
    @Bean
    public ServletRegistrationBean messageDispatcherServlet(
            ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/ws/*");
    }

    @Bean(name = "books")
    public DefaultWsdl11Definition defaultBookWsdl11Definition(XsdSchema countriesSchema) {
        DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
        wsdl11Definition.setPortTypeName("BooksPort");
        wsdl11Definition.setLocationUri("/ws");
        wsdl11Definition.setTargetNamespace("http://com/kswaughs/services/bookSvc");
        wsdl11Definition.setSchema(booksSchema());
        return wsdl11Definition;
    }

    @Bean
    public XsdSchema booksSchema() {
        return new SimpleXsdSchema(new ClassPathResource("books.xsd"));
    }
}

Step 7: Create Spring Boot Main Application Class

BootApp.java
package com.kswaughs.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan({ "com.kswaughs.*" })
public class BootApp {
    
    public static void main(String[] args) {
        SpringApplication.run(BootApp.class, args);
    }

}

Step 8: Define application Context path & port in application.properties

src/main/resources/application.properties
server.contextPath=/MyApp
server.port=8088

Testing the Application

WSDL Url : http://localhost:8088/MyApp/ws/books.wsdl

SOAP URL : http://localhost:8088/MyApp/ws

Test 1 : addBookRequest

SOAP Request:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:book="http://com/kswaughs/services/bookSvc">
   <soapenv:Header/>
   <soapenv:Body>
      <book:addBookRequest>
         <book:book>
            <book:name>Revolution 2020</book:name>
            <book:author>Chetan Bhagat</book:author>
            <book:price>11 $</book:price>
         </book:book>
      </book:addBookRequest>
   </soapenv:Body>
</soapenv:Envelope>

SOAP Response:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns2:addBookResponse xmlns:ns2="http://com/kswaughs/services/bookSvc">
         <ns2:status>SUCCESS</ns2:status>
      </ns2:addBookResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Test 2 : getBookRequest

SOAP Request:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:book="http://com/kswaughs/services/bookSvc">
   <soapenv:Header/>
   <soapenv:Body>
      <book:getBookRequest>
         <book:name>Revolution 2020</book:name>
      </book:getBookRequest>
   </soapenv:Body>
</soapenv:Envelope>

SOAP Response:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns2:getBookResponse xmlns:ns2="http://com/kswaughs/services/bookSvc">
         <ns2:book>
            <ns2:name>Revolution 2020</ns2:name>
            <ns2:author>Chetan Bhagat</ns2:author>
            <ns2:price>11 $</ns2:price>
         </ns2:book>
      </ns2:getBookResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Recommend this on


By kswaughs | Tuesday, December 13, 2016

Spring Boot WebSocket Server push + AngularJS client

WebSocket is a very thin, lightweight layer above TCP used to build interactive web applications that send messages back and forth between a browser and the server.

The best examples are live updates websites, where once user access the website neither user nor browser sends request to the server to get the latest updates. Server only keeps sending the messages to the browser.

In this example, I am building a WebSocket application with Spring Boot using STOMP Messaging to provide the cricket live score updates for every 5 secs.

Step 1 : Maven setup

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.0.RELEASE</version>
    </parent>
    <groupId>com.kswaughs.spring</groupId>
    <artifactId>springboot-websocket</artifactId>
    <version>1.0</version>
    <name>springboot-websocket</name>
    <description>Spring Boot WebSocket Application</description>
    <dependencies>
        <!-- Spring framework related jars -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>1.3.0.RELEASE</version>
        </dependency>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency> 
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 2: Model class to hold the batsman & score details

Batsman.java
package com.kswaughs.cricket.beans;

public class Batsman {
    
    private String name;
    
    private int runs;
    
    private int balls;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getRuns() {
        return runs;
    }

    public void setRuns(int runs) {
        this.runs = runs;
    }

    public int getBalls() {
        return balls;
    }

    public void setBalls(int balls) {
        this.balls = balls;
    }
    
}

Step 3: Create a Web Socket & STOMP Messaging Configuration

LiveScoreSocketConfig.java
package com.kswaughs.cricket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class LiveScoreSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/livescore-websocket").setAllowedOrigins("*"). withSockJS();
    }

}

Step 4: Create a Message handling controller

LiveCricketController.java
package com.kswaughs.cricket.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import com.kswaughs.cricket.beans.Batsman;
import com.kswaughs.cricket.service.LiveScoreService;

@Controller
public class LiveCricketController {
    
    @Autowired
    private LiveScoreService service;
    
    @MessageMapping("/score")
    @SendTo("/topic/myscores")
    public List<Batsman> getScores() {
        
        List<Batsman> scoresList = service.getScore();
        
        return scoresList;
    }
}

Step 5: Business Logic Implementation

Create a business logic component to get the live updates from back-end service. In this example, this class initializes the list of batsman objects with pre-filled data and later for every request, it will increments the runs & balls by 1 of any one batsman randomly.

LiveScoreService.java
package com.kswaughs.cricket.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.springframework.stereotype.Component;

import com.kswaughs.cricket.beans.Batsman;

@Component
public class LiveScoreService {
    
    private List<Batsman> scoresList = initialScores();
    
    public List<Batsman> getScore() {

        Random rand = new Random();

        int randomNum = rand.nextInt((2 - 1) + 1);
        Batsman batsman = scoresList.get(randomNum);

        batsman.setBalls(batsman.getBalls() + 1);
        batsman.setRuns(batsman.getRuns() + 1);

        return scoresList;
    }

    private List<Batsman> initialScores() {

        Batsman sachin = new Batsman();

        sachin.setName("Sachin Tendulkar");
        sachin.setRuns(24);
        sachin.setBalls(26);

        Batsman ganguly = new Batsman();
        ganguly.setName("Sourav Ganguly");
        ganguly.setRuns(28);
        ganguly.setBalls(30);

        List<Batsman> scoresList = new ArrayList<Batsman>();

        scoresList.add(sachin);
        scoresList.add(ganguly);

        return scoresList;
    }

}

Step 6: Scheduler Configuration

Configure a Scheduler task to send the updated scores to subscribed channel. The task is configured to run every 5 seconds.

ScoreScheduler.java
package com.kswaughs.cricket.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;

import com.kswaughs.cricket.service.LiveScoreService;

@Configuration
@EnableScheduling
public class ScoreScheduler {
        
    @Autowired
    private SimpMessagingTemplate template;
    
    @Autowired
    LiveScoreService service;

    @Scheduled(fixedRate = 5000)
    public void publishUpdates(){
       
        template.convertAndSend("/topic/myscores", service.getScore());
       
    }
 
}

Step 7 : Create a main class which will initialize and runs the spring boot application.

BootApp.java
package com.kswaughs.cricket.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan({ "com.kswaughs.cricket" })
public class BootApp {
    
    public static void main(String[] args) {
        
        SpringApplication.run(new Object[] { BootApp.class }, args);

    }
}

Testing : AngularJS WebSocket Example

Now we will connect to this application from HTML Page using AngularJS Stomp component and display the live score updates. You can download ng-stomp.standalone.min.js from https://github.com/beevelop/ng-stomp.

scores.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Client</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="ng-stomp.standalone.min.js"></script>
<script type="text/javascript">
  
  var app = angular.module('kswaughsLiveScore', ['ngStomp']);
 
  app.controller('LiveController', function ($stomp, $scope) {
  
      $scope.myres = [];
  
      $stomp.connect('http://localhost:8080/livescore-websocket', {})
            .then(function (frame) {
                var subscription = $stomp.subscribe('/topic/myscores', 
                    function (payload, headers, res) {
                        $scope.myres = payload;
                        $scope.$apply($scope.myres);
                });
            
                $stomp.send('/app/score', '');
         });
  });
  
 </script>
 <style> 
 .liveScore{
    color : blue;
 }

li{
 list-style: none;
 padding:0px 0px 10px 0px;
}
</style>
</head>
<body >
<div class="liveScore" ng-app="kswaughsLiveScore" ng-controller="LiveController">
   <p>Cricket - Live Score</p>
    <ul>
        <li ng-repeat="x in myres">{{$index+1}} - {{x.name}} - <b>{{x.runs}}</b> runs (<b>{{x.balls}}</b>balls)</li>
    </ul>
</div>    
</body>
</html>

Output

Cricket - Live Score

  • 1 - Sachin Tendulkar - 24 runs ( 26 balls)
  • 2 - Sourav Ganguly - 28 runs ( 30 balls)

The above scores gets updated every 5 secs when the server application runs.

Recommend this on


By kswaughs | Monday, October 10, 2016

Spring Stored Procedure CLOB OUT Example

This example shows how to read CLOB data returned from Stored Procedure using Spring.

Step 1: Oracle Stored Procedure Setup

Stored Procedure
create or replace PROCEDURE GETARTICLE 
(
  IN_ARTICLE_ID IN NUMBER,  
  OUT_ARTICLE_NAME OUT VARCHAR2,  
  OUT_ARTICLE_CONTENT OUT CLOB  
) AS 
BEGIN
  SELECT ARTICLE_NAME , ARTICLE_CONTENT
  INTO OUT_ARTICLE_NAME, OUT_ARTICLE_CONTENT
  from  ARTICLES WHERE ARTICLE_ID = IN_ARTICLE_ID;
END GETARTICLE;

Step 2: Implement SqlReturnType

Create a new class that implements org.springframework.jdbc.core.SqlReturnType to read CLOB data and convert it into String object.

CLOBToStringConverter.java
package com.kswaughs.util;

import java.io.IOException;
import java.io.Reader;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.SQLException;

import org.springframework.jdbc.core.SqlReturnType;

public class CLOBToStringConverter implements SqlReturnType {

    @Override
    public Object getTypeValue(CallableStatement cs,
            int paramIndex, int sqlType, String typeName) throws SQLException {
        
         Clob aClob = cs.getClob(paramIndex);

         final Reader clobReader = aClob.getCharacterStream();
         
         int length = (int) aClob.length();
         char[] inputBuffer = new char[1024];
         final StringBuilder outputBuffer = new StringBuilder();
         try {
            while ((length = clobReader.read(inputBuffer)) != -1)
             {
             outputBuffer.append(inputBuffer, 0, length);
             }
        } catch (IOException e) {
            throw new SQLException(e.getMessage());
        }
         return outputBuffer.toString();
    }
}

Step 3: DAO Implementation

Write a DAO class that extends org.springframework.jdbc.object.StoredProcedure. While declaring the OUT parameters in constructor, pass the above CLOBToStringConverter as parameter to SqlOutParameter.

ArticleDAO.java
package com.kswaughs.dao;

import java.sql.Types;
import java.util.HashMap;
import java.util.Map;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;

import com.kswaughs.util.CLOBToStringConverter;

public class ArticleDAO  extends StoredProcedure {
    
    public ArticleDAO(final JdbcTemplate jdbcTemplate) {

        super(jdbcTemplate, "GETARTICLE");
        
        // Input Parameters
        declareParameter(new SqlParameter("IN_ARTICLE_ID", Types.NUMERIC));
        
        // Output Parameters
        declareParameter(new SqlOutParameter("OUT_ARTICLE_NAME", Types.VARCHAR));
        declareParameter(new SqlOutParameter("OUT_ARTICLE_CONTENT", Types.CLOB, 
             null, new CLOBToStringConverter()));
    }
    
    
    public String getArticleContent(final String articleID) throws Exception {
        
        Map returnMap = null;
        
        Map inParams = new HashMap();
        inParams.put("IN_ARTICLE_ID", articleID);
    
        Map returnMap = super.execute(inParams);
        
        if(returnMap== null || returnMap.isEmpty()){
            
            throw new Exception("Article Not found") ;
        }
        
        String articleContent = String.valueOf(returnMap.get("OUT_ARTICLE_CONTENT"));
        return articleContent;
    }    
    
}

Common Mistakes

If we pass typeName as Empty String to SqlOutParameter as below,

 declareParameter(new SqlOutParameter("OUT_ARTICLE_CONTENT", Types.CLOB, 
        "", new CLOBToStringConverter()));

we will get below SQLException

Error occured while executing the Stored procedure. CallableStatementCallback; uncategorized SQLException for SQL [{call GETARTICLE(?, ?, ?)}]; SQL state [99999]; error code [17060]; Fail to construct descriptor: empty Object name; nested exception is java.sql.SQLException: Fail to construct descriptor: empty Object name at com.kswaughs.dao.ArticleDAO.getArticleContent.

Recommend this on


By kswaughs | Friday, April 29, 2016

Spring Pagination Example

Spring MVC Framework provides pagination capabilities for web pages with PagedListHolder class. In this example, we will see how to develop an online phone store application that displays list of the phones available with a specified number of phones and allows user to choose the next or previous list including required page index. Follow the below steps in developing this application.

1. Maven Dependencies

2. Define Service & model classes

3. Define pagination logic in Controller

4. Define Spring MVC Configuration

5. Servlet Configuration

6. JSPs implementation

1. Maven Dependencies

pom.xml
<dependencies>
   <!--Spring framework related jars-->
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>3.2.10.RELEASE</version>
   </dependency>
   <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
   </dependency>
   <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
   </dependency>
</dependencies>

2. Define Service & model classes

Create a Service class that returns the list of phone objects.

PhoneService
package com.kswaughs.web.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import com.kswaughs.web.beans.Phone;

@Service
public class PhoneService {
    
    public List<Phone> getPhoneslist() {
        
        List<Phone> phonesList = new ArrayList<Phone>();
        
        phonesList.add(buildPhone("1", "Samsung Galaxy Y"));
        phonesList.add(buildPhone("2", "Nokia Lumia"));
        phonesList.add(buildPhone("3", "Moto G"));
        phonesList.add(buildPhone("4", "Lenovo A 7000 white"));
        phonesList.add(buildPhone("5", "Sony XPeria"));
        
        return phonesList;
    }
    
    private Phone buildPhone(String id, String name) {
        
        Phone phone = new Phone(id, name);
        return phone;
    }
    
}

Below is the Data Model class to hold phone details.

Phone
package com.kswaughs.web.beans;

public class Phone {
    
    private String id;
    
    private String name;
    
    public Phone() {
        
    }
    
    public Phone(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

}

3. Define pagination logic in Controller

Create a Controller class to handle the requests and define the pagination logic.

PhoneController
package com.kswaughs.web.controller;

import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.support.PagedListHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import com.kswaughs.web.beans.Phone;
import com.kswaughs.web.service.PhoneService;

@Controller
@RequestMapping(value = "/phones")
public class PhoneController {
    
    @Autowired
    PhoneService phoneSvc;
    
    @RequestMapping(value = {"/all/{type}","/all"}, method = RequestMethod.GET)
    public ModelAndView all(
            @PathVariable Map<String, String> pathVariablesMap, 
            HttpServletRequest req) {
        
        PagedListHolder<Phone> productList = null;
        
        String type = pathVariablesMap.get("type");
        
        if(null == type) {
            // First Request, Return first set of list
            List<Phone> phonesList = phoneSvc.getPhoneslist();
            
            productList = new PagedListHolder<Phone>();
            productList.setSource(phonesList);
            productList.setPageSize(2);
            
            req.getSession().setAttribute("phonesList",  productList);
        
            printPageDetails(productList);
            
        } else if("next".equals(type)) {
            // Return next set of list
            productList = (PagedListHolder<Phone>) req.getSession()
                                .getAttribute("phonesList");
            
            productList.nextPage();
            
            printPageDetails(productList);
            
        } else if("prev".equals(type)) {
            // Return previous set of list
            productList = (PagedListHolder<Phone>) req.getSession()
                                .getAttribute("phonesList");
            
            productList.previousPage();
            
            printPageDetails(productList);
            
        } else {
            // Return specific index set of list
            System.out.println("type:" + type);
            
            productList = (PagedListHolder<Phone>) req.getSession()
                                .getAttribute("phonesList");
            
            int pageNum = Integer.parseInt(type);
            
            productList.setPage(pageNum);
            
            printPageDetails(productList);
        }
                    
        ModelAndView mv = new ModelAndView("index");
        
        return  mv;
    }

    private void printPageDetails(PagedListHolder productList) {
        
        System.out.println("curent page - productList.getPage() :"
                + productList.getPage());
        
        System.out.println("Total Num of pages - productList.getPageCount :"
                + productList.getPageCount());
        
        System.out.println("is First page - productList.isFirstPage :"
                + productList.isFirstPage());
        
        System.out.println("is Last page - productList.isLastPage :"
                + productList.isLastPage());
    }
}

4. Define Spring MVC Configuration

Spring Configuration defined with annotations.

SpringWebConfig
package com.kswaughs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@EnableWebMvc 
@Configuration
@ComponentScan({ "com.kswaughs.web" })
public class SpringWebConfig extends WebMvcConfigurerAdapter {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }
    
    @Bean
    public ViewResolver jspViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/jsps/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }        
}

5. Servlet Configuration

Create below servlet initialization class.

SpringWebInitializer
package com.kswaughs.servlet;

import org.springframework.web.servlet.support.
    AbstractAnnotationConfigDispatcherServletInitializer;

import com.kswaughs.config.SpringWebConfig;

public class SpringWebInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class[] getServletConfigClasses() {
        return new Class[] { SpringWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected Class[] getRootConfigClasses() {
        return new Class[] {};
    }
    
}

6. JSPs implementation

JSP page that displays the list of phones available.

index.jsp

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
    <head>
    <spring:url value="/resources/css/style.css" var="style" />
    <spring:url value="/phones/all" var="pageurl" />
    <link href="${style}" rel="stylesheet" />
    </head>
    <body>
    <div>
        <p>Welcome to Phones Store</p>
        <p>List of the Phones in our store.</p>
    </div>
    <div>
        <c:set var="pageListHolder" value="${phonesList}" scope="session" />
        <table cellspacing="0">
            <thead><tr><th>ID</th><th>Name</th></tr></thead>
            <tbody>
            <c:forEach var="ph" items="${pageListHolder.pageList}">
                <tr><td>${ph.id}</td><td>${ph.name}</td></tr>
            </c:forEach>
            </tbody>
        </table>
    </div>
    <div>
    <span style="float:left;">
    <c:choose>
        <c:when test="${pageListHolder.firstPage}">Prev</c:when>
        <c:otherwise><a href="${pageurl}/prev">Prev</a></c:otherwise>
    </c:choose>
    </span>
    <span>
    <c:forEach begin="0" end="${pageListHolder.pageCount-1}" varStatus="loop">
    &nbsp;&nbsp;
    <c:choose>
        <c:when test="${loop.index == pageListHolder.page}">${loop.index+1}</c:when>
        <c:otherwise><a href="${pageurl}/${loop.index}">${loop.index+1}</a></c:otherwise>
    </c:choose>
    &nbsp;&nbsp;
    </c:forEach>
    </span>
    <span>
    <c:choose>
        <c:when test="${pageListHolder.lastPage}">Next</c:when>
        <c:otherwise><a href="${pageurl}/next">Next</a></c:otherwise>
    </c:choose>
    </span>
    </div>
    </body>
</html>

css file to display the table with border

styles.css
html, body {
    height: 100%;
}
html {
    display: table;
    margin: auto;
 position: relative;
    min-width: 500px;
    min-height: 768px;
    height: 100%;
}
body {
  display: table-cell;
  font: 13px/20px 'Lucida Grande', Tahoma, Verdana, sans-serif;
  color: #404040;
  background: #ffffff;
  text-align: center;
}
div {
 margin-left: 10 px;
 text-align: left;
}
table {
text-align:center;
font-family:Arial, Helvetica, sans-serif;
color:#000;
font-size:14px;
text-shadow: 1px 1px 0px #fff;
background:#ededed;
margin:20px;
border:#ccc 1px solid;
-moz-border-radius:3px;
-webkit-border-radius:3px;
border-radius:3px;
-moz-box-shadow: 0 1px 2px #d1d1d1;
-webkit-box-shadow: 0 1px 2px #d1d1d1;
box-shadow: 0 1px 2px #d1d1d1;

}
table th {
padding:21px 25px 22px 25px;
border-top:1px solid #fafafa;
border-bottom:1px solid #e0e0e0;
background: #ededed;
background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#ebebeb));
background: -moz-linear-gradient(top,  #ededed,  #ebebeb);
}
table a:link {
color: #666;
font-weight: bold;
text-decoration:none;
}
table a:visited {
color: #999999;
font-weight:bold;
text-decoration:none;
}
table a:active,
table a:hover {
color: #bd5a35;
text-decoration:underline;
}
table tr:first-child th:first-child {
-moz-border-radius-topleft:3px;
-webkit-border-top-left-radius:3px;
border-top-left-radius:3px;
}
table tr:first-child th:last-child {
-moz-border-radius-topright:3px;
-webkit-border-top-right-radius:3px;
border-top-right-radius:3px;
}
table tr {
text-align: left;
padding-left:20px;
}
table td:first-child {
text-align: left;
padding-left:20px;
border-left: 0;
}
table td {
padding:18px;
border-top: 1px solid #ffffff;
border-bottom:1px solid #e0e0e0;
border-left: 1px solid #e0e0e0;
background: #ffffff;
background: -webkit-gradient(linear, left top, left bottom, from(#fbfbfb), to(#fafafa));
background: -moz-linear-gradient(top,  #fbfbfb,  #fafafa);
}
table tr.even td {
background: #ffffff;
background: -webkit-gradient(linear, left top, left bottom, from(#f8f8f8), to(#f6f6f6));
background: -moz-linear-gradient(top,  #f8f8f8,  #f6f6f6);
}
table tr:last-child td {
border-bottom:0;
}
table tr:last-child td:first-child {
-moz-border-radius-bottomleft:3px;
-webkit-border-bottom-left-radius:3px;
border-bottom-left-radius:3px;
}
table tr:last-child td:last-child {
-moz-border-radius-bottomright:3px;
-webkit-border-bottom-right-radius:3px;
border-bottom-right-radius:3px;
}
table tr:hover td {
background: #f2f2f2;
background: -webkit-gradient(linear, left top, left bottom, from(#f2f2f2), to(#f0f0f0));
background: -moz-linear-gradient(top,  #f2f2f2,  #f0f0f0);
}

Testing the application

First page

http://localhost:8080/spring_app/phones/all

Recommend this on


By kswaughs | Sunday, April 10, 2016

Spring MVC PDF View Example

Spring MVC Framework has built in methods and components to render the output in many documents like PDF, EXCEL along with JSPs. In this example, we will see how to configure and develop our spring based application to allow users to download the web content in PDF document.

We have an online store application where the home page displays list of the phones available and allows user to choose a phone to get its details. After submission, price details are shown in the next page along with a download link to view the page in PDF format. Now, we will follow the below steps in developing this application.

1. Maven Dependencies

2. Define Controllers, Service & model classes

3. Define Custom PDF View

4. View Configuration

5. Servlet Configuration

6. JSPs implementation

1. Maven Dependencies

pom.xml
<dependencies>
   <!--Spring framework related jars-->
   <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>3.2.10.RELEASE</version>
   </dependency>
   <!--Itext jar for PDF generation-->
   <dependency>
      <groupId>com.lowagie</groupId>
      <artifactId>itext</artifactId>
      <version>2.1.7</version>
   </dependency>
   <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
   </dependency>
   <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
   </dependency>
</dependencies>

2. Define Controllers, Service & model classes

Create a Controller class to handle the requests based on the URL mappings.

PhoneController
package com.kswaughs.web.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import com.kswaughs.web.beans.Phone;
import com.kswaughs.web.service.PhoneService;

@Controller
@RequestMapping(value = "/phones")
@SessionAttributes("phone")
public class PhoneController {
    
    @Autowired
    PhoneService phoneSvc;
    
    @RequestMapping(value = "", method = RequestMethod.GET)
    public ModelAndView home() {
        
        List<Phone> phonesList = phoneSvc.getPhoneslist();
        
        ModelAndView mv = new ModelAndView("home", "phones",  phonesList);
        mv.addObject("phone", new Phone());
            
        return  mv;
    }
    
    @RequestMapping(value = "/details", method = RequestMethod.POST)
    public ModelAndView showPrice(@ModelAttribute("phone") Phone phone ) {
                
        phone = phoneSvc.getPhoneDetails(phone);
                
        return new ModelAndView("details", "phone", phone);
    }
        
    
    @RequestMapping(value = "/details/pdf", method = RequestMethod.GET)
    public ModelAndView showPriceInPDF(@ModelAttribute("phone") Phone phone ) {
                
        List<Phone> phonesList = phoneSvc.getPhoneslist();
        
        return new ModelAndView("pdfView", "phonesList",  phonesList);
    }
}

Create a Service class to process the requests.

PhoneService
package com.kswaughs.web.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import com.kswaughs.annotations.TrackResponse;
import com.kswaughs.web.beans.Phone;

@Service
public class PhoneService {
    
    public List<Phone> getPhoneslist() {
        
        List<Phone> phonesList = new ArrayList<Phone>();
        
        phonesList.add(buildPhone("1", "Samsung Galaxy Y"));
        phonesList.add(buildPhone("2", "Nokia Lumia"));
        phonesList.add(buildPhone("3", "Moto G"));
        phonesList.add(buildPhone("4", "Lenovo A 7000 white"));
        phonesList.add(buildPhone("5", "Sony XPeria"));
        
        return phonesList;
    }
    
    public Phone getPhoneDetails(Phone phone) {
                
        final String id = phone.getId();
        String price = null;
        if("1".equals(id)) {
            phone = buildPhone("1", "Samsung Galaxy Y");
            price = "10,000";
            
        } else if("2".equals(id)) {
            phone = buildPhone("2", "Nokia Lumia");
            price = "12,000";
        
        } else if("3".equals(id)) {
            phone = buildPhone("3", "Moto G");
            price = "14,000";
        
        } else if("4".equals(id)) {
            phone = buildPhone("4", "Lenovo A 7000 white");
            price = "9,800";
        
        } else if("5".equals(id)) {
            phone = buildPhone("5", "Sony XPeria");
            price = "23,000";
        }
        
        phone.setPrice(price);
        return phone;
        
    }
    
    private Phone buildPhone(String id, String name) {
        
        Phone phone = new Phone(id, name);
        return phone;
    }
    
}

Below is the Model class to hold phone details.

Phone
package com.kswaughs.web.beans;

public class Phone {
    
    private String id;
    
    private String name;
    
    private String price;
    
    public Phone() {
        
    }
    
    public Phone(String id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price;
    }

    public void setId(String id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

}

3. Define Custom PDF View Component

Create a new class that extends AbstractPdfView and override buildPdfDocument() method with data from session object.

PDFBuilder
package com.kswaughs.web.views;

import java.awt.Color;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.document.AbstractPdfView;

import com.lowagie.text.Document;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfWriter;
import com.kswaughs.web.beans.Phone;

@Component
public class PDFBuilder extends AbstractPdfView {

    @Override
    protected void buildPdfDocument(Map<String, Object> model, Document doc,
            PdfWriter writer, HttpServletRequest req, HttpServletResponse resp)
            throws Exception {

        Phone ph = (Phone) req.getSession().getAttribute("phone");

        doc.add(new Paragraph(
                "Dear User, Following is the list of available phones in our shop."));

        List<Phone> phonesList = (List<Phone>) model.get("phonesList");

        PdfPTable table = new PdfPTable(2);
        table.setSpacingBefore(10);

        // define font for table header row
        Font font = FontFactory.getFont(FontFactory.HELVETICA);
        font.setColor(Color.WHITE);

        // define table header cell
        PdfPCell cell = new PdfPCell();
        cell.setBackgroundColor(Color.BLUE);
        cell.setPadding(5);

        cell.setPhrase(new Phrase("ID", font));
        table.addCell(cell);

        cell.setPhrase(new Phrase("Name", font));
        table.addCell(cell);

        for (Phone phone : phonesList) {
            table.addCell(phone.getId());
            table.addCell(phone.getName());
        }

        doc.add(table);
        
        doc.add(new Paragraph(
                "Please find the price details of phone you have selected."));

        Font priceTxtFont = new Font();
        priceTxtFont.setColor(Color.BLUE);
            
        doc.add(new Paragraph("Phone :" + ph.getName(), priceTxtFont));
        doc.add(new Paragraph("Price :" + ph.getPrice() + "Rs/-", priceTxtFont));

    }

}

4. View Configuration

Add below entry in 'views.properties' file which should be placed in src/main/resources folder. The view name 'pdfView' is mapped to PDFBuilder View component.

views.properties
pdfView.(class)=com.poc.web.views.PDFBuilder

Configure a new ViewResolver with this properties file in Java Configuration

SpringWebConfig
package com.kswaughs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.ResourceBundleViewResolver;

@EnableWebMvc 
@Configuration
@ComponentScan({ "com.kswaughs.web" })
public class SpringWebConfig extends WebMvcConfigurerAdapter {
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    }
    
    @Bean
    public ViewResolver jspViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setViewClass(JstlView.class);
        viewResolver.setPrefix("/jsps/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
    
    @Bean
    public ViewResolver resourceBundleViewResolver() {
        
        ResourceBundleViewResolver viewResolver = new ResourceBundleViewResolver();
        viewResolver.setBasename("views");
        viewResolver.setOrder(1);
        
        return viewResolver;
    }
        
}

5. Servlet Configuration

Create below servlet initialization class.

SpringWebInitializer
package com.kswaughs.servlet;

import org.springframework.web.servlet.support.
        AbstractAnnotationConfigDispatcherServletInitializer;

import com.kswaughs.config.SpringWebConfig;

public class SpringWebInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class[] getServletConfigClasses() {
        return new Class[] { SpringWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected Class[] getRootConfigClasses() {
        return new Class[] {};
    }
    
}

6. JSPs implementation

Home page that displays the list of phones available and allows user to choose one.

home.jsp

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
    <head>
    <spring:url value="/resources/css/style.css" var="style" />
    <spring:url value="/phones/details" var="submiturl" />
    <link href="${style}" rel="stylesheet" />
    </head>
    <body>
    <div>
        <p>Welcome to Phones Store</p>
        <p>Choose your Phone below</p>
        <div>
        <form:form method="post" action="${submiturl}" commandName="phone">
        <c:forEach var="ph" items="${phones}">
            <div><form:radiobutton path="id" value="${ph.id}"/>${ph.name}</div>
        </c:forEach>
            <input type="submit"  value="submit"/>
        </form:form>
        </div>
    </div>
    </body>
</html>

Details page that displays the price details of selected phone.

details.jsp
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<html>
    <head>
        <spring:url value="/resources/css/style.css" var="style" />
        <spring:url value="/phones/details/pdf" var="pdfLink" />
        <link href="${style}" rel="stylesheet" />
    </head>
    <body>
    <div>
        <p>Welcome to Phones Store</p>
        <p>Your phone details</p>
        <div>Phone : ${phone.name}</div>
        <div>Price : ${phone.price} Rs/<sub>-</sub></div>
    </div>
    <div>
        <a href="${pdfLink}">Click here to download in PDF.</a>
    </div>
    </body>
</html>

Testing the application

Home page

http://localhost:8080/spring_app/phones

Welcome to Phones Store
Choose your Phone below
Samsung Galaxy Y
Nokia Lumia
Moto G
Lenovo A 7000 white
Sony XPeria

Details page

http://localhost:8080/spring_app/phones/details

Welcome to Phones Store
Your phone details
Phone : Lenovo A 7000 white
Price : 9,800 Rs/-

On clicking the pdf download link, You will see below content in PDF file.

Recommend this on


By kswaughs | Tuesday, March 22, 2016

Spring Boot Batch Job + Scheduler Example

We have seen how to schedule a task using spring boot and how to run a batch job using spring boot in below posts.

In this example, We will see how to run that Batch job with spring scheduler using spring boot.

Step 1 : By default, the batch job runs immediately when we start the application. So we have to disable the auto run feature in application.properties file.

spring.batch.job.enabled=false

Step 2 : Configure JobLauncher in one more configuration class with defining required dependant components.

BatchScheduler
package com.kswaughs.config;

import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class BatchScheduler {

    @Bean
    public ResourcelessTransactionManager transactionManager() {
        return new ResourcelessTransactionManager();
    }

    @Bean
    public MapJobRepositoryFactoryBean mapJobRepositoryFactory(
            ResourcelessTransactionManager txManager) throws Exception {
        
        MapJobRepositoryFactoryBean factory = new 
                MapJobRepositoryFactoryBean(txManager);
        
        factory.afterPropertiesSet();
        
        return factory;
    }

    @Bean
    public JobRepository jobRepository(
            MapJobRepositoryFactoryBean factory) throws Exception {
        return factory.getObject();
    }

    @Bean
    public SimpleJobLauncher jobLauncher(JobRepository jobRepository) {
        SimpleJobLauncher launcher = new SimpleJobLauncher();
        launcher.setJobRepository(jobRepository);
        return launcher;
    }

}

Step 3 : Follow the below methods

Import the above Configuration class to your batch configuration class.

Get the reference of the configured JobLauncher through autowired injection.

Write a new method annotated with @Scheduled and desired cron expression.

Run the JobLauncher with passing job bean and custom job parameter.

BatchConfiguration
package com.kswaughs.config;

import java.util.Date;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.Scheduled;

import com.kswaughs.beans.Order;
import com.kswaughs.beans.SvcReq;
import com.kswaughs.order.OrderSvcInvoker;
import com.kswaughs.processor.JobCompletionNotificationListener;
import com.kswaughs.processor.OrderItemProcessor;

@Configuration
@EnableBatchProcessing
@Import({BatchScheduler.class})
public class BatchConfiguration {

    @Autowired
    private SimpleJobLauncher jobLauncher;

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Scheduled(cron = "1 53/3 17 * * ?")
    public void perform() throws Exception {

        System.out.println("Job Started at :" + new Date());

        JobParameters param = new JobParametersBuilder().addString("JobID",
                String.valueOf(System.currentTimeMillis())).toJobParameters();

        JobExecution execution = jobLauncher.run(processOrderJob(), param);

        System.out.println("Job finished with status :" + execution.getStatus());
    }

    @Bean
    public Job processOrderJob() {
        return jobBuilderFactory.get("processOrderJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener())
                .flow(orderStep()).end().build();
    }

    @Bean
    public Step orderStep() {
        return stepBuilderFactory.get("orderStep").<Order, SvcReq> chunk(3)
                .reader(reader()).processor(processor()).writer(writer())
                .build();
    }

    @Bean
    public FlatFileItemReader<Order> reader() {
        FlatFileItemReader<Order> reader = new FlatFileItemReader<Order>();
        reader.setResource(new ClassPathResource("PhoneData.csv"));
        reader.setLineMapper(new DefaultLineMapper<Order>() {
            {
                setLineTokenizer(new DelimitedLineTokenizer() {
                    {
                        setNames(new String[] { "orderID", "orderName" });
                    }
                });
                setFieldSetMapper(new BeanWrapperFieldSetMapper<Order>() {
                    {
                        setTargetType(Order.class);
                    }
                });
            }
        });
        return reader;
    }

    @Bean
    public OrderItemProcessor processor() {
        return new OrderItemProcessor();
    }

    @Bean
    public ItemWriter<SvcReq> writer() {
        return new OrderSvcInvoker();
    }

    @Bean
    public JobExecutionListener listener() {
        return new JobCompletionNotificationListener();
    }

}

In this example, I configured the job to start at '5 PM 53 minutes 1 second' and run for every 3 minutes till 6 PM with cron expression.

Output console logs

Job Started at :Tue Mar 22 17:53:01 IST 2016
2016-03-22 17:53:01.052  INFO 10932 --- [pool-2-thread-1] o.s.b.c.l.support.SimpleJobLauncher: Job: [FlowJob: [name=processOrderJob]] launched with the following parameters: [{JobID=1458649381004}]
2016-03-22 17:53:01.078  INFO 10932 --- [pool-2-thread-1] o.s.batch.core.job.SimpleStepHandler: Executing step: [orderStep]
Converting (Order [orderID=101, orderName=Apple IPhone]) into (SvcReq [id=101, name=APPLE IPHONE])
Converting (Order [orderID=102, orderName=Samsung Galaxy Y]) into (SvcReq [id=102, name=SAMSUNG GALAXY Y])
Converting (Order [orderID=103, orderName=Moto E]) into (SvcReq [id=103, name=MOTO E])
calling web service:SvcResp [id=101, message=APPLE IPHONE Processed successfully]
calling web service:SvcResp [id=102, message=SAMSUNG GALAXY Y Processed successfully]
calling web service:SvcResp [id=103, message=MOTO E Processed successfully]
Processed items:3
Converting (Order [orderID=104, orderName=Moto X]) into (SvcReq [id=104, name=MOTO X])
Converting (Order [orderID=105, orderName=Yuphoria]) into (SvcReq [id=105, name=YUPHORIA])
calling web service:SvcResp [id=104, message=MOTO X Processed successfully]
calling web service:SvcResp [id=105, message=YUPHORIA Processed successfully]
Processed items:2
BATCH JOB FINISHED SUCCESSFULLY
2016-03-22 17:53:01.350  INFO 10932 --- [pool-2-thread-1] o.s.b.c.l.support.SimpleJobLauncher: Job: [FlowJob: [name=processOrderJob]] completed with the following parameters: [{JobID=1458649381004}] and the following status: [COMPLETED]
Job finished with status :COMPLETED

Job Started at :Tue Mar 22 17:56:00 IST 2016
2016-03-22 17:56:01.006  INFO 10932 --- [pool-2-thread-1] o.s.b.c.l.support.SimpleJobLauncher: Job: [FlowJob: [name=processOrderJob]] launched with the following parameters: [{JobID=1458649560996}]
2016-03-22 17:56:01.031  INFO 10932 --- [pool-2-thread-1] o.s.batch.core.job.SimpleStepHandler: Executing step: [orderStep]
Converting (Order [orderID=101, orderName=Apple IPhone]) into (SvcReq [id=101, name=APPLE IPHONE])
Converting (Order [orderID=102, orderName=Samsung Galaxy Y]) into (SvcReq [id=102, name=SAMSUNG GALAXY Y])
Converting (Order [orderID=103, orderName=Moto E]) into (SvcReq [id=103, name=MOTO E])
calling web service:SvcResp [id=101, message=APPLE IPHONE Processed successfully]
calling web service:SvcResp [id=102, message=SAMSUNG GALAXY Y Processed successfully]
calling web service:SvcResp [id=103, message=MOTO E Processed successfully]
Processed items:3
Converting (Order [orderID=104, orderName=Moto X]) into (SvcReq [id=104, name=MOTO X])
Converting (Order [orderID=105, orderName=Yuphoria]) into (SvcReq [id=105, name=YUPHORIA])
calling web service:SvcResp [id=104, message=MOTO X Processed successfully]
calling web service:SvcResp [id=105, message=YUPHORIA Processed successfully]
Processed items:2
BATCH JOB FINISHED SUCCESSFULLY
2016-03-22 17:56:01.154  INFO 10932 --- [pool-2-thread-1] o.s.b.c.l.support.SimpleJobLauncher: Job: [FlowJob: [name=processOrderJob]] completed with the following parameters: [{JobID=1458649560996}] and the following status: [COMPLETED]
Job finished with status :COMPLETED

Recommend this on