Spring MVC: REST application with CNVR vol. 1
Not so long time ago I have read an article written by Paul Chapman about Content Negotiating View Resolver (CNVR). That post on a Spring Framework Blog inspired me to investigate this framework’s area. As a result I have developed a sample REST application based on Spring MVC with CNVR. The application demonstrates a basic flow of a REST service – creation, deletion, reading and edition of an entity.
Spring Framework supports REST services for a long time, earlier you could develop some service using Message Converters. In Spring 3.2 all this stuff become more easier in configuration and development. So lets stop talking because I’m going to show a basic setup and exploitation of Spring REST service with CNVR.
A basic idea of CNVR is to define which kind of representation form for a resource give back to a client depending on information which CNVR gets from the client’s request. You can ask me: what is that information in the request which can impact on CNVR decision? The answer is simple:
- URL sufix (e.g. .xml, .json, .html etc
- URL parameter (format by default)
- HTTP Accept header property
Here is an illustration of high level CNVR workflow:
For the more information I recommend to read a full article of Paul Chapman.
Setup the Spring MVC REST project with CNVR
I will work with a maven project, as always I will provide a link to a project’s GitHub repository. Here is a screenshot of entire project:
I have explained numerous times how to setup Dynamic Web Project in Eclipse, so now I will provide just source files with some short notes. You can find required maven dependencies below:
<properties> <mysql.connector>5.1.25</mysql.connector> <hibernate.version>4.2.3.Final</hibernate.version> <spring.version>3.2.3.RELEASE</spring.version> <spring.data.version>1.3.2.RELEASE</spring.data.version> <jackson.version>1.9.12</jackson.version> </properties> <dependencies> <!-- DataBase libs --> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> <version>${mysql.connector}</version> </dependency> <dependency> <groupid>commons-dbcp</groupid> <artifactid>commons-dbcp</artifactid> <version>1.4</version> </dependency> <!-- Hibernate --> <dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-core</artifactid> <version>${hibernate.version}</version> </dependency> <dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-entitymanager</artifactid> <version>${hibernate.version}</version> </dependency> <!-- Spring --> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-webmvc</artifactid> <version>${spring.version}</version> </dependency> <dependency> <groupid>org.springframework.data</groupid> <artifactid>spring-data-jpa</artifactid> <version>${spring.data.version}</version> <exclusions> <exclusion> <artifactid>spring-aop</artifactid> <groupid>org.springframework</groupid> </exclusion> </exclusions> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-orm</artifactid> <version>${spring.version}</version> </dependency> <dependency> <groupid>org.springframework</groupid> <artifactid>spring-tx</artifactid> <version>${spring.version}</version> </dependency> <!-- CGLIB is required to process @Configuration classes --> <dependency> <groupid>cglib</groupid> <artifactid>cglib</artifactid> <version>3.0</version> </dependency> <!-- Other --> <dependency> <groupid>javax.servlet</groupid> <artifactid>javax.servlet-api</artifactid> <version>3.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupid>jstl</groupid> <artifactid>jstl</artifactid> <version>1.2</version> </dependency> <!-- CNVR resources --> <dependency> <groupid>org.codehaus.jackson</groupid> <artifactid>jackson-mapper-asl</artifactid> <version>${jackson.version}</version> </dependency> </dependencies>
A full version of the pom.xml file you can find on GitHub. So let’s go ahead with the preparation. I will use MySQL as a database. And I need to create a following table in it:
CREATE TABLE `smartphones` ( `id` int(6) NOT NULL AUTO_INCREMENT, `producer` varchar(20) NOT NULL, `model` varchar(20) NOT NULL, `price` double NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
Now we need appropriate java object which will represent the smartphones table:
@Entity @Table(name="smartphones") public class Smartphone { @Id @GeneratedValue private Integer id; private String producer; private String model; private double price; /** * Method updates already existed {@link Smartphone} object with values from the inputed argument. * @param sPhone - Object which contains new Smartphone values. * @return {@link Smartphone} object to which this method applied. */ public Smartphone update(Smartphone sPhone) { this.producer = sPhone.producer; this.model = sPhone.model; this.price = sPhone.price; return this; } @Override public String toString() { return producer+": "+model+" with price "+price; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getProducer() { return producer; } public void setProducer(String producer) { this.producer = producer; } public String getModel() { return model; } public void setModel(String model) { this.model = model; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } }
The most part of preparation is done. Service and DAO layer is the last thing which I need to do. I’m going to use Spring Data for the DAO layer, and in the one of my previous posts I have made a detail review of its setup.
public interface SmartphoneRepository extends JpaRepository< Smartphone, Integer >{ }
And here are corresponding service interface and its implementation:
public interface SmartphoneService { public Smartphone create(Smartphone sp); public Smartphone get(Integer id); public List< Smartphone > getAll(); public Smartphone update(Smartphone sp) throws SmartphoneNotFoundException; public Smartphone delete(Integer id) throws SmartphoneNotFoundException; }
Service implementation:
@Service @Transactional(rollbackFor=SmartphoneNotFoundException.class) public class SmartphoneServiceImpl implements SmartphoneService { @Autowired private SmartphoneRepository smartphoneRepository; @Override public Smartphone create(Smartphone sp) { return smartphoneRepository.save(sp); } @Override public Smartphone get(Integer id) { return smartphoneRepository.findOne(id); } @Override public List< Smartphone > getAll() { return smartphoneRepository.findAll(); } @Override public Smartphone update(Smartphone sp) throws SmartphoneNotFoundException { Smartphone sPhoneToUpdate = get(sp.getId()); if (sPhoneToUpdate == null) throw new SmartphoneNotFoundException(sp.getId().toString()); sPhoneToUpdate.update(sp); return sPhoneToUpdate; } @Override public Smartphone delete(Integer id) throws SmartphoneNotFoundException { Smartphone sPhone = get(id); if (sPhone == null) throw new SmartphoneNotFoundException(id.toString()); smartphoneRepository.delete(id); return sPhone; } }
In the end of setting up of the project let’s consider the “heart” of configurations: Initializer and WebAppConfig files.
@Configuration @EnableWebMvc @EnableTransactionManagement @ComponentScan("com.mobapp") @PropertySource("classpath:application.properties") @EnableJpaRepositories("com.mobapp.repository") public class WebAppConfig extends WebMvcConfigurerAdapter { private static final String PROPERTY_NAME_DATABASE_DRIVER = "db.driver"; private static final String PROPERTY_NAME_DATABASE_PASSWORD = "db.password"; private static final String PROPERTY_NAME_DATABASE_URL = "db.url"; private static final String PROPERTY_NAME_DATABASE_USERNAME = "db.username"; private static final String PROPERTY_NAME_HIBERNATE_DIALECT = "hibernate.dialect"; private static final String PROPERTY_NAME_HIBERNATE_SHOW_SQL = "hibernate.show_sql"; private static final String PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN = "entitymanager.packages.to.scan"; @Resource private Environment env; @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(env.getRequiredProperty(PROPERTY_NAME_DATABASE_DRIVER)); dataSource.setUrl(env.getRequiredProperty(PROPERTY_NAME_DATABASE_URL)); dataSource.setUsername(env.getRequiredProperty(PROPERTY_NAME_DATABASE_USERNAME)); dataSource.setPassword(env.getRequiredProperty(PROPERTY_NAME_DATABASE_PASSWORD)); return dataSource; } @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); entityManagerFactoryBean.setDataSource(dataSource()); entityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistence.class); entityManagerFactoryBean. setPackagesToScan(env.getRequiredProperty(PROPERTY_NAME_ENTITYMANAGER_PACKAGES_TO_SCAN)); entityManagerFactoryBean.setJpaProperties(hibProperties()); return entityManagerFactoryBean; } private Properties hibProperties() { Properties properties = new Properties(); properties.put(PROPERTY_NAME_HIBERNATE_DIALECT, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_DIALECT)); properties.put(PROPERTY_NAME_HIBERNATE_SHOW_SQL, env.getRequiredProperty(PROPERTY_NAME_HIBERNATE_SHOW_SQL)); return properties; } @Bean public JpaTransactionManager transactionManager() { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); return transactionManager; } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorPathExtension(true) .useJaf(false) .ignoreAcceptHeader(true) .mediaType("html", MediaType.TEXT_HTML) .mediaType("json", MediaType.APPLICATION_JSON) .defaultContentType(MediaType.TEXT_HTML); } @Bean public ViewResolver contentNegotiatingViewResolver( ContentNegotiationManager manager) { List< ViewResolver > resolvers = new ArrayList< ViewResolver >(); InternalResourceViewResolver r1 = new InternalResourceViewResolver(); r1.setPrefix("/WEB-INF/pages/"); r1.setSuffix(".jsp"); r1.setViewClass(JstlView.class); resolvers.add(r1); JsonViewResolver r2 = new JsonViewResolver(); resolvers.add(r2); ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); resolver.setViewResolvers(resolvers); resolver.setContentNegotiationManager(manager); return resolver; } /** * View resolver for returning JSON in a view-based system. Always returns a * {@link MappingJacksonJsonView}. */ public class JsonViewResolver implements ViewResolver { public View resolveViewName(String viewName, Locale locale) throws Exception { MappingJacksonJsonView view = new MappingJacksonJsonView(); view.setPrettyPrint(true); return view; } } }
Despite that the file is big enough I want to focus your attention just on several things. The first one is the JsonViewResolver inner class. It’s required for handling of JSON requests. Of course, it can be declared separatly of the WebAppConfig class and just be imported in it. But I decide to put it directly in the WebAppConfig to avoid a dispersal of attention. The second one is the configureContentNegotiation method. There I set options for a content negotiation view resolver. And finally in the contentNegotiatingViewResolver bean I have determined which view resolvers will be available in the my application.
public class Initializer implements WebApplicationInitializer { private static final String DISPATCHER_SERVLET_NAME = "dispatcher"; @Override public void onStartup(ServletContext servletContext) throws ServletException { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(WebAppConfig.class); ctx.setServletContext(servletContext); registerHiddenHttpMethodFilter(servletContext); Dynamic servlet = servletContext.addServlet(DISPATCHER_SERVLET_NAME, new DispatcherServlet(ctx)); servlet.addMapping("/"); servlet.setLoadOnStartup(1); } private void registerHiddenHttpMethodFilter(ServletContext servletContext) { FilterRegistration.Dynamic fr = servletContext .addFilter("hiddenHttpMethodFilter", HiddenHttpMethodFilter.class); fr.addMappingForServletNames( EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD), false, DISPATCHER_SERVLET_NAME); } }
In the Initializer class there is just one important thing in context of this tutorial. It is registerHiddenHttpMethodFilter method. This method will help to deal with such HTTP methods as PUT and DELETE.
I hope you didn’t tired, because the most interesting will wait for you in the following part of this tutorial.