Spring 3.1 MVC skeleton project upgrade
Over the weekend I upgraded my Spring project skeleton to include Spring 3.1 MVC support.
The skeleton app uses Scaml, a HAML-variant, as its view layer. The index page merely lists all the examples we have available.
It also contains the standard dependencies I usually recommend: Spring Framework 3.1, Joda-Time 2.0, Guava 10, LogBack/SLF4j, JUnit, Mockito and PostgreSQL.
The skeleton application is available as a Maven project at my BitBucket page at http://dev.gueck.com/project-skeletons/src/tip/spring3-web. It can be executed with the command mvn -Dspring.profiles.active=dev jetty:run.
#!haml
-@ var examples:java.util.List[com.gueck.spring3.Example]
!!! 1.1
%html
%head
%title Example application with #{examples.size} examples.
%body
%form(method='POST' action='/submitExample')
%input(type='text' name='name')
%input(type='submit' value='OK')
%p Our Examples:
- for(x <- examples)
%p.example<
%a(href='/edit/#{x.id}') #{x.name}!The embedded development database gets re-initialized with a schema and test data at every application restart.
src/main/resources/com/gueck/spring3/dev-schema.sql:
#!sql
CREATE TABLE examples (
id IDENTITY,
name VARCHAR(65536)
);src/main/resources/com/gueck/spring3/dev-test-data.sql:
#!sql
INSERT INTO examples (name)
VALUES ('The Wire'), ('The West Wing'), ('The Daily Show');The Spring MVC controller, service and DAO classes don't need much introduction.
src/main/java/com/gueck/spring3/ExampleController.java:
#!java
package com.gueck.spring3;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class ExampleController {
@Autowired
private ExampleService exampleService;
@Autowired
private ExampleDao dao;
@RequestMapping("/")
public String welcome(final ModelMap model) {
model.put("examples", dao.listExamples());
return "index";
}
@RequestMapping(value="/submitExample", method=RequestMethod.POST)
public String submitExample(@RequestParam final String name) {
exampleService.submitNewExample(new Example(name));
return "redirect:/";
}
}The service makes the business decisions, such as whom to exclude, and also demonstrates Guava's usefulness in collection handling.
src/main/java/com/gueck/spring3/ExampleService.java:
#!java
package com.gueck.spring3;
import static com.google.common.collect.Iterables.any;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
@Autowired
private ExampleDao dao;
private static Predicate<String> contains(final String match) {
return new Predicate<String>() {
@Override public boolean apply(final String input) {
return match.toLowerCase().contains(input.toLowerCase());
}
};
}
private ImmutableList<String> badWords = ImmutableList.of("republican", "religion", "true finns");
public boolean submitNewExample(final Example example) {
if (any(badWords, contains(example.name))) {
return false;
} else {
return dao.createExample(example);
}
}
}The easiest way to do a simple CRUD DAO is with Spring JDBC, but it gets progressively more complex as your domain tables/classes get more complex, and pretty soon you're better off with just going with JPA.
src/main/java/com/gueck/spring3/ExampleDao.java:
#!java
package com.gueck.spring3;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ExampleDao {
private static final String INSERT_EXAMPLE = "INSERT INTO examples (name) VALUES (?)";
private static final String SELECT_EXAMPLE = "SELECT * FROM examples WHERE id = ?";
private static final String UPDATE_EXAMPLE = "UPDATE examples SET name = ? WHERE id = ?";
private static final String DELETE_EXAMPLE = "DELETE FROM examples WHERE id = ?";
private static final String LIST_EXAMPLES = "SELECT * FROM examples";
@Autowired
private JdbcTemplate jdbcTemplate;
public boolean createExample(final Example example) {
return 1 == jdbcTemplate.update(INSERT_EXAMPLE, example.name);
}
public Example readExample(final long id) {
return jdbcTemplate.queryForObject(SELECT_EXAMPLE, Example.MAPPER, id);
}
public boolean updateExample(final Example example) {
return 1 == jdbcTemplate.update(UPDATE_EXAMPLE, example.name, example.id);
}
public boolean deleteExample(final long id) {
return 1 == jdbcTemplate.update(DELETE_EXAMPLE, id);
}
public List<Example> listExamples() {
return jdbcTemplate.query(LIST_EXAMPLES, Example.MAPPER);
}
}The skeleton domain class uses Guava's ToStringHelper, and contains a static instance of Spring JDBC's RowMapper.
I like to situate some or many of the Spring JDBC helpers in the actual domain class, but the location varies depending on the various architectural visions you might embrace.
src/main/java/com/gueck/spring3/Example.java:
#!java
package com.gueck.spring3;
import com.google.common.base.Objects;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
/**
* Example domain class.
*/
public class Example {
public Long id;
/** A name to show in a user interface. */
public String name;
public Example() {
}
public Example(final String name) {
this.name = name;
}
public static final RowMapper<Example> MAPPER = new RowMapper<Example>() {
@Override
public Example mapRow(ResultSet rs, int rowNum) throws SQLException {
final Example ret = new Example();
ret.id = rs.getLong("id");
ret.name = rs.getString("name");
return ret;
}
};
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("id", id)
.add("name", name)
.toString();
}
@Override
public int hashCode() {
return Objects.hashCode(id, name);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Example other = (Example) obj;
if (!java.util.Objects.equals(this.id, other.id)) {
return false;
}
if (!java.util.Objects.equals(this.name, other.name)) {
return false;
}
return true;
}
}The Spring XML configuration file now demonstrates the new mvc:resources-tag, as well as profiles, and how to create a embedded development database with them.
src/main/webapp/WEB-INF/springmvc-servlet.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:mvc="http://www.springframework.org/schema/mvc"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Scans within the base package of the application for @Components to configure as beans -->
<!-- @Controller, @Service, @Configuration, etc. -->
<context:component-scan base-package="com.gueck.spring3" />
<!-- Enables the Spring MVC @Controller programming model -->
<mvc:annotation-driven />
<mvc:resources mapping="/resources/**" location="/WEB-INF/resources/" />
<bean id="viewNameTranslator" class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>
<bean id="scalateViewResolver" class="org.fusesource.scalate.spring.view.ScalateViewResolver"
p:order="1" p:prefix="/WEB-INF/view/" p:suffix=".scaml" />
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:order="2" p:viewClass="org.springframework.web.servlet.view.JstlView" p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
<context:property-placeholder />
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"
p:dataSource-ref="dataSource" />
<beans profile="production">
<bean id="dataSource" class="org.postgresql.ds.PGPoolingDataSource" destroy-method="close">
<property name="serverName" value="${db.serverName}"/>
<property name="databaseName" value="${db.databaseName}"/>
<property name="user" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</bean>
</beans>
<beans profile="dev">
<jdbc:embedded-database id="dataSource" type="HSQL">
<jdbc:script location="classpath:com/gueck/spring3/dev-schema.sql"/>
<jdbc:script location="classpath:com/gueck/spring3/dev-test-data.sql"/>
</jdbc:embedded-database>
</beans>
</beans>