“What is your favourite design pattern?” Question asked in a job interview.
WTF question is that?
“Hmmm… my favourite design pattern is Salami pattern – approaching to the finish line slice by slice.”
— From Antidisestablishmentarianism I.T.
In my previous blog – Spring 3 shacks up JSF 2, the maverick way, demonstrating how JSF 2 a component, event driven framework as the presentation layer, and Spring 3, Spring MVC 3 work in middleware. Well, technology keeps evolving. Hopefully even pigs could programe one day. This time, Hibernate 4 is added as the persistence layer. Now, we have end-to-end, a “traditional” three tiers web application. Moreover, an unit test to test business logic layer, the most important, runs outside of container.
Firstly, files and directories structure:
The backend database is Oracle’s example of Human Resources (HR) application for a fictitious company (check the reference at the end of the blog).
Let’s start with configuration in the project.
pom.xml
[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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>hellospring</groupId>
<artifactId>hellospring</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>hellospring</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-api</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>2.1.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.webflow</groupId>
<artifactId>spring-faces</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
<!– Unit Test –>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>test</scope>
</dependency>
<!– ORACLE database driver –>
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>4.1.1.Final</version>
</dependency>
<!– Hibernate c3p0 connection pool –>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-c3p0</artifactId>
<version>4.1.1.Final</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
<!– logback –>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.1</version>
</dependency>
<!– not include log4j –>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>HelloSpring</finalName>
<plugins>
<!–
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.5</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src-generated</directory>
<excludes>
<exclude>**/*.java </exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
–>
<!– Plugin to add source path to build cycle –>
<!–
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.7</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src-generated</source>
</sources>
</configuration>
</execution>
<execution>
<id>attach-artifacts</id>
<phase>package</phase>
<goals>
<goal>attach-artifact</goal>
</goals>
<configuration>
<artifacts>
<artifact>
<file>${basedir}/src-generated/hibernate.cfg.xml</file>
<type>xml</type>
<classifier>hibernate</classifier>
</artifact>
</artifacts>
</configuration>
</execution>
</executions>
</plugin>
–>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.0-beta-1</version>
<configuration>
<url>http://localhost:8080/manager/html</url>
<server>TomcatServer</server>
<path>/HelloSpring</path>
</configuration>
</plugin>
</plugins>
</build>
</project>
[/xml]
applicationContext.xml
[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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
">
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:properties/jdbc.properties"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"
p:driverClassName="${jdbc.driverClass}"
p:url="${jdbc.url}"
p:password="${jdbc.password}"
p:username="${jdbc.username}"
>
</bean>
<!– Hibernate session factory –>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
p:dataSource-ref="dataSource">
<property name="annotatedClasses">
<list>
<value>org.paradise.sandbox.entity.OehrCustomers</value>
<value>org.paradise.sandbox.entity.OehrJobHistory</value>
<value>org.paradise.sandbox.entity.OehrDepartments</value>
<value>org.paradise.sandbox.entity.OehrLocations</value>
<value>org.paradise.sandbox.entity.OehrEmployees</value>
<value>org.paradise.sandbox.entity.OehrAccountManagers</value>
<value>org.paradise.sandbox.entity.OehrProductInformation</value>
<value>org.paradise.sandbox.entity.OehrCountries</value>
<value>org.paradise.sandbox.entity.OehrJobs</value>
<value>org.paradise.sandbox.entity.OehrSydneyInventory</value>
<value>org.paradise.sandbox.entity.OehrOcOrders</value>
<value>org.paradise.sandbox.entity.OehrBombayInventory</value>
<value>org.paradise.sandbox.entity.OehrProducts</value>
<value>org.paradise.sandbox.entity.OehrOcCustomers</value>
<value>org.paradise.sandbox.entity.OehrProductPrices</value>
<value>org.paradise.sandbox.entity.OehrWarehouses</value>
<value>org.paradise.sandbox.entity.OehrProductDescriptions</value>
<value>org.paradise.sandbox.entity.OehrTorontoInventory</value>
<value>org.paradise.sandbox.entity.OehrCustomers</value>
<value>org.paradise.sandbox.entity.OehrRegions</value>
<value>org.paradise.sandbox.entity.OehrOrderItems</value>
<value>org.paradise.sandbox.entity.OehrOcInventories</value>
<value>org.paradise.sandbox.entity.OehrOcProductInformation</value>
<value>org.paradise.sandbox.entity.OehrOrders</value>
<value>org.paradise.sandbox.entity.OehrOcCorporateCustomers</value>
<value>org.paradise.sandbox.entity.OehrPromotions</value>
<value>org.paradise.sandbox.entity.OehrInventories</value>
</list>
</property>
<property name="hibernateProperties">
<value> hibernate.dialect=org.hibernate.dialect.Oracle10gDialect
hibernate.show_sql=${jdbc.showSql}
hibernate.format_sql=${jdbc.formatSql}
</value>
</property>
</bean>
<!– Activates annotation-based bean configuration –>
<context:annotation-config/>
<context:component-scan base-package="org.paradise.sandbox" />
<!– map all requests to /resources/** to the container default servlet (ie, don’t let Spring handle them) –>
<bean id="defaultServletHttpRequestHandler"
class="org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler" />
<bean id="simpleUrlHandlerMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="urlMap">
<map>
<entry key="/resources/**" value-ref="defaultServletHttpRequestHandler" />
</map>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter" />
<mvc:annotation-driven />
<!– JSF for representation layer. All JSF files under /WEB-INF/views directory –>
<bean id="viewResolver"
class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="cache" value="false" />
<property name="viewClass" value="org.springframework.faces.mvc.JsfView" />
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".xhtml" />
</bean>
</beans>
[/xml]
logging with logback.xml
[xml]
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d{yyyy-MM-dd_HH:mm:ss.SSS} %-5level %logger{36} – %msg%n</Pattern>
</encoder>
</appender>
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/usr/local/tomcat/logs/hibernate.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<Pattern>%d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n</Pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<FileNamePattern>/usr/local/tomcat/logs/hibernate.%i.log.zip</FileNamePattern>
<MinIndex>1</MinIndex>
<MaxIndex>10</MaxIndex>
</rollingPolicy>
<triggeringPolicy
class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>2MB</MaxFileSize>
</triggeringPolicy>
</appender>
<appender name="lilith" class="ch.qos.logback.classic.net.SocketAppender">
<RemoteHost>localhost</RemoteHost>
<Port>4560</Port>
<ReconnectionDelay>170</ReconnectionDelay>
<IncludeCallerData>true</IncludeCallerData>
</appender>
<logger name="org.hibernate.type" level="ALL" />
<logger name="org.hibernate" level="INFO" />
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
<appender-ref ref="lilith" />
</root>
</configuration>
[/xml]
Now, let’s have a look the presentation tier codes.
HelloSpring.java – a simple data transfer bean
[java]
package org.paradise.sandbox;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;
@Component
@Scope(value="request", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class HelloSpring {
private String message;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
[/java]
- @Component annotation indicates this is an auto scan component
HelloSpringController.java – a controller in MVC architecture
[java]
package org.paradise.sandbox;
import org.hibernate.Session;
import org.paradise.sandbox.entity.OehrCustomers;
import org.paradise.sandbox.util.HibernateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.paradise.sandbox.business.CustomerBo;
@Controller
public class HelloSpringController {
@Autowired
private HelloSpring helloSpring;
@Autowired
private CustomerBo customerBo;
@RequestMapping(value="/helloSpring", method=RequestMethod.GET)
public void helloWorld() {
helloSpring.setMessage("Hello Spring from Spring MVC to JSF");
}
@RequestMapping(value = "/helloSpring", method = RequestMethod.POST)
public void helloWorld(@RequestParam String msg) {
OehrCustomers oherCustomer = customerBo.findByCustomerId(101);
if (oherCustomer != null) {
helloSpring.setMessage(oherCustomer.getCustFirstName() + " "
+ oherCustomer.getCustLastName() + " says " + msg);
} else {
helloSpring.setMessage(msg);
}
}
}
[/java]
- @Controller annotation indicates this is a controller component in the presentation layer
Good practice is NOT include any kind of business logic in the controller. All the business logic should be delegated to business layer. Controller should be the as thin as possible layer between the web world and business world. You can seen object CustomerBo as the delegator is injected into the controller.
Now, in business layer.
CustomerBo.java
[java]
package org.paradise.sandbox.business;
import org.paradise.sandbox.entity.OehrCustomers;
public interface CustomerBo {
void save(OehrCustomers customer);
void update(OehrCustomers customer);
void delete(OehrCustomers customer);
OehrCustomers findByCustomerId(int customerId);
}
[/java]
CustomerBoImpl.java
[java]
package org.paradise.sandbox.business;
import org.paradise.sandbox.dao.CustomerDao;
import org.paradise.sandbox.entity.OehrCustomers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("customerBo")
public class CustomerBoImpl implements CustomerBo {
@Autowired
CustomerDao customerDao;
@Override
public void save(OehrCustomers customer) {
}
@Override
public void update(OehrCustomers customer) {
}
@Override
public void delete(OehrCustomers customer) {
}
@Override
public OehrCustomers findByCustomerId(int customerId) {
return customerDao.findByCustomerId(customerId);
}
}
[/java]
- @Service annotation indicates this is service component in the business layer
Last but not least persistence layer.
CustomerDao.java
[java]
package org.paradise.sandbox.dao;
import org.paradise.sandbox.entity.OehrCustomers;
public interface CustomerDao {
void save(OehrCustomers customer);
void update(OehrCustomers customer);
void delete(OehrCustomers customer);
OehrCustomers findByCustomerId(int customerId);
}
[/java]
CustomerDaoImpl.java
[java]
package org.paradise.sandbox.dao;
import org.paradise.sandbox.entity.OehrCustomers;
import org.paradise.sandbox.util.DaoSupport;
import org.springframework.stereotype.Repository;
@Repository("customerDao")
public class CustomerDaoImpl extends DaoSupport implements CustomerDao {
@Override
public void save(OehrCustomers customer) {
}
@Override
public void update(OehrCustomers customer) {
}
@Override
public void delete(OehrCustomers customer) {
}
@Override
public OehrCustomers findByCustomerId(int customerId) {
return (OehrCustomers) getSessionFactory().openSession().get(
OehrCustomers.class, customerId);
}
}
[/java]
- @Repository annotation indicates this is DAO component in the persistence layer
DaoSupport.java
[java]
package org.paradise.sandbox.util;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
public abstract class DaoSupport {
@Autowired
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return this.sessionFactory;
}
}
[/java]
A high quality application must come with high quality test. The following unit test tests DAO component in persistence layer domain only, and outside any container.
CustomerDaoTest.java
[java]
package org.paradise.sandbox.dao;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.paradise.sandbox.entity.OehrCustomers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( { "/applicationContext.xml" })
public class CustomerDaoTest {
@Autowired
private CustomerDao customerDao;
private int customerId = 101;
private String expectedCustomerFirstName = "Constantin";
private String expectedCustomerLastName = "Welles";
@Test
public void testFindByCustomerId() {
OehrCustomers customer = customerDao.findByCustomerId(customerId);
Assert.assertEquals(expectedCustomerFirstName, customer.getCustFirstName());
Assert.assertEquals(expectedCustomerLastName, customer.getCustLastName());
}
}
[/java]
The dependency to above unit test in Eclipse is just amazing. See how many jars files need to set up:
This is output of unit test run by Maven:
tmiao@chmbp02-2 ~/Projects/Sandbox/src/HelloSpring
13:57:22 362 $ mvn test
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building hellospring 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:resources (default-resources) @ hellospring ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 10 resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ hellospring ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:2.4.3:testResources (default-testResources) @ hellospring ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/tmiao/Projects/Sandbox/src/HelloSpring/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ hellospring ---
[INFO] Compiling 1 source file to /Users/tmiao/Projects/Sandbox/src/HelloSpring/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ hellospring ---
[INFO] Surefire report directory: /Users/tmiao/Projects/Sandbox/src/HelloSpring/target/surefire-reports
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running org.paradise.sandbox.dao.CustomerDaoTest
Hibernate:
select
oehrcustom0_.CUSTOMER_ID as CUSTOMER1_0_0_,
oehrcustom0_.CREDIT_LIMIT as CREDIT2_0_0_,
oehrcustom0_.CUST_ADDRESS as CUST3_0_0_,
oehrcustom0_.CUST_EMAIL as CUST4_0_0_,
oehrcustom0_.CUST_FIRST_NAME as CUST5_0_0_,
oehrcustom0_.CUST_GEO_LOCATION as CUST6_0_0_,
oehrcustom0_.CUST_LAST_NAME as CUST7_0_0_,
oehrcustom0_.NLS_LANGUAGE as NLS8_0_0_,
oehrcustom0_.NLS_TERRITORY as NLS9_0_0_,
oehrcustom0_.ACCOUNT_MGR_ID as ACCOUNT11_0_0_,
oehrcustom0_.PHONE_NUMBERS as PHONE10_0_0_
from
OEHR_CUSTOMERS oehrcustom0_
where
oehrcustom0_.CUSTOMER_ID=?
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.97 sec
Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 11.143s
[INFO] Finished at: Sun Apr 22 16:57:11 EST 2012
[INFO] Final Memory: 12M/81M
[INFO] ------------------------------------------------------------------------
In this demo, I try to use Annotation as many as possible. The reason for that the difference between Annotation and XML configuration. XML is good if some settings need to be kept updating. On the contrary, Annotation which is for the typesafe and injection, is good for some settings just need to write once.
Another thing in persistence layer of this demo, sessionFactory is Autowired and injected into the DAO bean. The reason why not using HibernateDaoSupport / HibernateTemplate as parent class for DAO bean is because they are unnecessarily ties your codes to Spring classes. This is important that Hibernate should not know Spring and what Spring is doing at all.
Well, that’s all for this blog. What do you reckon? Is it a simple, straight-forward, test friendly and a very light way to do the web application development?
Related Content: