13. [Course]: Exposing a Database on the Web with Spring MVC
Keywords: multi-tier architecture, Spring, dependency injection, web service / JSON, client / server
13.1. Support
![]() | ![]() |
The projects for this chapter can be found in the [support / chap-13] folder. The SQL script [dbintrospringdata.sql] creates the MySQL database required for testing.
13.2. The Role of Spring MVC in a Web Application
Let’s situate Spring MVC within the development of a web application. Most often, it will be built on a multi-layer architecture such as the following:
![]() |
- the [Web] layer is the layer in contact with the web application user. The user interacts with the web application through web pages viewed in a browser. Spring MVC is located in this layer and only in this layer;
- the [business] layer implements the application’s business logic, such as calculating a salary or an invoice. This layer uses data from the user via the [Web] layer and from the DBMS via the [DAO] layer;
- the [DAO] (Data Access Objects) layer, the [ORM] (Object Relational Mapper) layer, and the JDBC driver manage access to data in the DBMS. The [ORM] layer acts as a bridge between the objects handled by the [DAO] layer and the rows and columns of tables in a relational database. The JPA (Java Persistence API) specification allows for abstraction from the ORM used, provided it implements these specifications. This will be the case here, and we will henceforth refer to the ORM layer as the JPA layer;
- The integration of the layers is handled by the Spring framework;
13.3. The Spring MVC Development Model
Spring MVC implements the MVC (Model–View–Controller) architectural pattern as follows:
![]() |
The processing of a client request proceeds as follows:
- request - the requested URLs are of the form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... The [Front Controller] uses a configuration file or Java annotations to "route" the request to the correct controller and the correct action within that controller. To do this, it uses the [Action] field of the URL. The rest of the URL [/param1/param2/...] consists of optional parameters that will be passed to the action. The C in MVC here refers to the chain [Front Controller, Controller, Action]. If no controller can handle the requested action, the web server will respond that the requested URL was not found.
- processing
- The selected action can use the parameters that the [Front Controller] has passed to it. These can come from several sources:
- the path [/param1/param2/...] of the URL,
- the [p1=v1&p2=v2] parameters of the URL,
- from parameters posted by the browser with its request;
- when processing the user's request, the action may need the [business] layer [2b]. Once the client's request has been processed, it may trigger various responses. A classic example is:
- an error page if the request could not be processed correctly
- a confirmation page otherwise
- the action instructs a specific view to be displayed [3]. This view will display data known as the view model. This is the M in MVC. The action will create this M model [2c] and instruct a V view to be displayed [3];
- response - the selected view V uses the model M constructed by the action to initialize the dynamic parts of the HTML response it must send to the client, then sends this response.
For a web service / JSON, the previous architecture is slightly modified:
![]() |
- in [4a], the model, which is a Java class, is converted into a JSON string by a JSON library;
- in [4b], this JSON string is sent to the browser;
Now, let’s clarify the relationship between MVC web architecture and layered architecture. Depending on how the model is defined, these two concepts may or may not be related. Consider a single-layer Spring MVC web application:
![]() |
If we implement the [Web] layer with Spring MVC, we will indeed have an MVC web architecture but not a multi-layer architecture. Here, the [Web] layer will handle everything: presentation, business logic, and data access. It is the actions that will perform this work.
Now, let’s consider a multi-layer web architecture:
![]() |
The [Web] layer can be implemented without a framework and without following the MVC pattern. In this case, we still have a multi-layer architecture, but the Web layer does not implement the MVC pattern.
For example, in the .NET world, the [Web] layer described above can be implemented using ASP.NET MVC, resulting in a layered architecture with an MVC-style [Web] layer. That said, this ASP.NET MVC layer can be replaced with a classic ASP.NET layer (WebForms) while keeping the rest (business logic, DAO, ORM) unchanged. We then have a layered architecture with a [Web] layer that is no longer MVC-based.
In MVC, we said that the M model was that of the V view, i.e., the set of data displayed by the V view. Another definition of the M model in MVC is given:
![]() |
Many authors consider that what lies to the right of the [Web] layer forms the M model of MVC. To avoid ambiguity, we can refer to:
- the domain model when referring to everything to the right of the [Web] layer
- the view model when referring to the data displayed by a view V
Hereinafter, the term "M model" will refer exclusively to the model of a V view.
13.4. A Web/JSON project with Spring MVC
The site [http://spring.io/guides] offers getting-started tutorials to explore the Spring ecosystem. We will follow one of them to discover the Maven configuration required for a Spring MVC project.
13.4.1. The demo project
![]() |
- In [1], we import one of the Spring guides;
![]() |
- in [2], we select the [Rest Service] example;
- in [3], we select the Maven project;
- in [4], we select the final version of the guide;
- in [5], we confirm;
- in [6], the imported project;
Web services accessible via standard URLs that return JSON data are often called REST (REpresentational State Transfer) services. A service is said to be RESTful if it follows certain rules.
Let’s now examine the imported project, starting with its Maven configuration.
13.4.2. Maven Configuration
The [pom.xml] file is as follows:
<?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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- lines 6–8: the Maven project properties. A [<packaging>] tag specifying the type of file produced by the Maven build is missing. In its absence, the [jar] type is used. The application is therefore a console-based executable application, not a web application, in which case the packaging would be [war];
- lines 10–14: The Maven project has a parent project [spring-boot-starter-parent]. This defines most of the project’s dependencies. They may be sufficient, in which case no additional dependencies are added, or they may not be, in which case the missing dependencies are added;
- Lines 17–20: The [spring-boot-starter-web] artifact includes the libraries necessary for a Spring MVC web service project where no views are generated. This artifact includes a very large number of libraries, including those for an embedded Tomcat server. The application will run on this server;
The libraries included in this configuration are very numerous:
![]() | ![]() |
Above, we see the three Tomcat server archives.
13.4.3. The architecture of a Spring [web / JSON] service
For a web/JSON service, Spring MVC implements the MVC model as follows:
![]() |
- In [4a], the model—which is a Java class—is converted into a JSON string by a JSON library;
- in [4b], this JSON string is sent to the browser;
13.4.4. The C controller
![]() |
The imported application has the following controller:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- Line 9: The [@RestController] annotation makes the [GreetingController] class a Spring controller, meaning its methods are registered to handle URLs. We have seen the similar [@Controller] annotation. The return type of that controller’s methods was [String], which was the name of the view to display. Here, it is different. The methods of a [@RestController] return objects that are serialized to be sent to the browser. The type of serialization performed depends on the Spring MVC configuration. Here, they will be serialized to JSON. It is the presence of a JSON library in the project dependencies that causes Spring Boot to automatically configure the project in this way;
- line 14: the [@RequestMapping] annotation specifies the URL handled by the method, in this case the URL [/greeting];
- line 15: we have already explained the [@RequestParam] annotation. The result returned by the method is an object of type [Greeting].
- line 12: a long integer of atomic type. This means it supports concurrent access. Multiple threads may want to increment the [counter] variable at the same time. This will be handled properly. A thread can only read the counter’s value once the thread currently modifying it has finished its modification.
13.4.5. The M model
The M model produced by the previous method is the following [Greeting] object:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
The JSON transformation of this object will create the string {"id":n,"content":"text"}. Ultimately, the JSON string produced by the controller method will be in the form:
or
13.4.6. Execution
![]() |
The [Application.java] class is the project’s executable class. Its code is as follows:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
We have already encountered and explained this code in the previous example.
13.4.7. Running the project
![]() |
We get the following console logs:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.9.RELEASE)
2014-11-28 15:22:55.005 INFO 3152 --- [ main] hello.Application : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046 INFO 3152 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762 INFO 3152 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567 INFO 3152 --- [ main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738 INFO 3152 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2014-11-28 15:22:56.740 INFO 3152 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869 INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870 INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478 INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481 INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping: Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231 INFO 3152 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318 INFO 3152 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319 INFO 3152 --- [ main] hello.Application : Started Application in 3.788 seconds (JVM running for 4.424)
- line 13: the Tomcat server starts on port 8080 (line 12);
- line 17: the [DispatcherServlet] servlet is present;
- line 20: the method [GreetingController.greeting] has been discovered;
To test the web application, we request the URL [http://localhost:8080/greeting]:
![]() | ![]() |
We receive the expected JSON string. It may be interesting to view the HTTP headers sent by the server. To do this, we will use the Chrome extension called [Advanced Rest Client] (Chrome / Ctrl-T / [Applications] menu / [Advanced Rest Client] - see Appendices, paragraph 22.5):
![]() |
- in [1], the requested URL;
- in [2], the GET method is used;
- in [3], the JSON response;
- in [4], the server indicated that it was sending a response in JSON format;
- in [5], we request the same URL but this time using a POST request;
- in [7], the information is sent to the server in [urlencoded] format;
- in [6], the "name" parameter and its value;
- in [8], the browser tells the server that it is sending it [urlencoded] information;
- in [9], the server's JSON response;
13.4.8. Creating an executable archive
We will now create an executable archive:
![]() |
![]() |
- in [1]: we run a Maven target;
- in [2]: there are two goals: [clean] to delete the [target] folder from the Maven project, [package] to regenerate it;
- in [3]: the generated [target] folder will be located in this folder;
- in [4]: we generate the target;
In the logs that appear in the console, it is important to see the [spring-boot-maven-plugin] plugin. This is the plugin that generates the executable archive.
Using a console, navigate to the generated folder:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
06/11/2014 3:30 PM <DIR> classes
06/11/2014 3:30 PM <DIR> generated-sources
06/11/2014 3:30 PM 11,073,572 gs-rest-service-0.1.0.jar
06/11/2014 3:30 PM 3,690 gs-rest-service-0.1.0.jar.original
06/11/2014 3:30 PM <DIR> maven-archiver
06/11/2014 3:30 PM <DIR> maven-status
...
- line 5: the generated archive;
This archive is executed as follows:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
Now that the web application is running, you can access it using a browser:
![]() |
13.4.9. Deploying the application on a Tomcat server
As we did for the previous project, we modify the [pom.xml] file as follows:
<?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>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
...
</project>
- Line 9: You must specify that you are going to generate a WAR (Web Archive) file;
You must also configure the web application. In the absence of a [web.xml] file, this is done using a class that extends [SpringBootServletInitializer]:
![]() |
The [ApplicationInitializer] class is as follows:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- line 6: the [ApplicationInitializer] class extends the [SpringBootServletInitializer] class;
- line 9: the [configure] method is overridden (line 8);
- line 10: the class that configures the project is provided;
To run the project, proceed as follows:
![]() |
- In [1-2], run the project on one of the servers registered in the Eclipse IDE;
Once this is done, you can request the URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in a browser:
![]() |
13.4.10. Conclusion
We have introduced a type of Spring MVC project where the web application sends a JSON stream to the browser. We will now develop a web/JSON application to expose the [dbintrospringdata] database studied in the [Introduction to Spring Data] tutorial on the web.
13.5. Exposing the [dbintrospringdata] database on the web
13.5.1. Web/JSON Service Architecture
We will implement the following architecture:
![]() |
The [DAO] and [JPA] layers are implemented by the application written in the [Introduction to Spring Data] tutorial.
13.5.2. Installing the database
![]() |
The SQL script [dbintrospringdata.sql] creates the MySQL database required for testing.
13.5.3. The Eclipse project for the web service / JSON
The Eclipse project for the web service / JSON is as follows:
![]() |
This is a Maven project whose [pom.xml] file is as follows:
<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>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>Spring MVC demo</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- lines 11–15: the parent Maven project already used for the [DAO] layer;
- lines 18–22: the dependency on the [DAO] layer;
- lines 23–26: the dependency on the [spring-boot-starter-web] artifact. This artifact includes all the dependencies needed to create a web/JSON service. It also includes unnecessary libraries. A more precise configuration would therefore be necessary. However, this configuration is useful for getting started;
- lines 28–30: the dependency on the [spring-boot-starter] artifact allows you to manage Spring Boot annotations;
The dependencies introduced by this configuration are as follows:
![]() |
- In [1], we can see that Eclipse has detected the dependency on the project archive [intro-spring-data-01];
The dependencies above are those of both the [DAO] layer and the [web] layer.
13.5.3.1. Configuration of the [web] layer
The [web] layer is configured by an [AppConfig] file:
![]() |
The [WebConfig] class configures the [web] layer:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- [web] layer configuration
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// JSON filters
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategoryWithProducts")
public ObjectMapper jsonMapperCategoryWithProducts() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategory", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduct", SimpleBeanPropertyFilter.serializeAllExcept("category")));
// result
return mapper;
}
@Bean(name = "jsonMapperProductWithCategory")
public ObjectMapper jsonMapperProductWithCategory() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduct", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategory", SimpleBeanPropertyFilter.serializeAllExcept("products")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategoryWithoutProducts")
public ObjectMapper jsonMapperCategoryWithoutProducts() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategory",
SimpleBeanPropertyFilter.serializeAllExcept("products")));
// result
return mapper;
}
@Bean(name = "jsonMapperProductWithoutCategory")
public ObjectMapper jsonMapperProductWithoutCategory() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduct",
SimpleBeanPropertyFilter.serializeAllExcept("category")));
// result
return mapper;
}
}
- line 18: the [@EnableWebMvc] annotation triggers automatic configurations for the Spring MVC framework;
- line 19: the [WebConfig] class extends the Spring [WebMvcConfigurerAdapter] class to redefine certain beans (lines 26–40);
- lines 22–23: injection of the Spring context;
- lines 25–29: definition of the Spring MVC framework’s servlet, which routes HTTP requests to the correct controller and method. [DispatcherServlet] is a Spring class;
- lines 31–34: we specify that this servlet handles all URLs;
- lines 36–39: the presence of this bean will activate the Tomcat server included in the project archives. It will listen for requests on port 8080;
- lines 42–91: beans that will be used to manage JSON filters;
- lines 42–45: a JSON mapper without filters;
- lines 47–57: the JSON mapper that allows you to retrieve a category along with its products. Note that when requesting a category with its products, you must configure both the JSON filter for the [Category] class and the one for the [Product] class. This is always the case. When serializing/deserializing a class to JSON, you must configure the JSON filter for the class and those for all dependencies to be included in it;
- lines 59–69: the JSON mapper that allows a product to be displayed with its category;
- lines 71–80: the JSON mapper that allows you to have a category without its products;
- lines 82–91: the JSON mapper that allows you to retrieve a product without its category;
The [AppConfig] class configures the entire application, i.e., the [web] and [DAO] layers:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- Line 9: imports the beans from the [DAO] layer and those from the [web] layer;
- line 8: specifies the packages where other Spring beans can be found;
Note that we have not used the [@EnableAutoConfiguration] annotation anywhere. We preferred to control the configuration ourselves.
13.5.4. The application model
![]() |
The [ApplicationModel] class is as follows:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Category;
import spring.data.entities.Product;
@Component
public class ApplicationModel implements IDao {
// the [DAO] layer
@Autowired
private IDao dao;
@Override
public void addProducts(List<Product> products) {
dao.addProducts(products);
}
@Override
public void deleteAllProducts() {
dao.deleteAllProducts();
}
@Override
public void updateProducts(List<Product> products) {
dao.updateProducts(products);
}
@Override
public List<Product> getAllProducts() {
return dao.getAllProducts();
}
@Override
public void addCategories(List<Category> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Category> categories) {
dao.updateCategories(categories);
}
@Override
public List<Category> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Product getProductByIdWithCategory(Long productId) {
return dao.getProductByIdWithCategory(productId);
}
@Override
public Product getProductByNameWithCategory(String name) {
return dao.getProductByNameWithCategory(name);
}
@Override
public Category getCategoryByIdWithProducts(Long categoryId) {
return dao.getCategoryByIdWithProducts(categoryId);
}
@Override
public Category getCategoryByNameWithProducts(String name) {
return dao.getCategoryByNameWithProducts(name);
}
@Override
public Product getProductByIdWithoutCategory(Long productId) {
return dao.getProductByIdWithoutCategory(productId);
}
@Override
public Category getCategoryByIdWithoutProducts(Long categoryId) {
return dao.getCategoryByIdWithoutProducts(categoryId);
}
@Override
public Product getProductByNameWithoutCategory(String name) {
return dao.getProductByNameWithoutCategory(name);
}
@Override
public Category getCategoryByNameWithoutProducts(String name) {
return dao.getCategoryByNameWithoutProducts(name);
}
}
- line 12: the class is a Spring singleton;
- line 13: which implements the [IDao] interface of the [DAO] layer;
- lines 16–17: injection of a reference into the [DAO] layer;
- lines 19–99: implementation of the [IDao] interface;
The architecture of the web layer evolves as follows:
![]() |
- in [2b], the methods of the controller(s) communicate with the [ApplicationModel] singleton;
This strategy provides flexibility regarding the management of a potential cache. The [ApplicationModel] class can be used to store information obtained from the [DAO] layer or configuration data. This can be useful when you do not have control over the [DAO] layer. This caching strategy may evolve over time. Changes will have no impact on the code of the controller(s).
13.5.5. The controller
![]() |
![]() |
Here we have only one controller, the [MyController] class.
13.5.5.1. Exposed URLs
The URLs exposed by this controller are as follows:
| Adds products to the database. These are posted. The response is a JSON string containing the list of added products with their primary keys. |
| Deletes all products from the database. |
| Updates products in the database. These are posted. The response is a JSON string containing the list of updated products. |
| Retrieves the JSON string for all products. |
| Adds categories to the database. These are posted. The response is a JSON string containing the list of added categories along with their primary keys. If the categories contain products, those are also added to the database. |
| Deletes all categories from the database along with all products in them. Afterward, the database is empty. |
| Updates categories in the database. These are posted. The response is the list of updated categories. If the categories contain products, those are also updated in the database. Returns the JSON string of the modified categories; |
| Retrieves the JSON string for all categories. |
| Retrieves the JSON string for a product identified by its ID, along with its category. |
| Retrieves the JSON string for a product identified by its ID, without its category. |
| Retrieves the JSON string for a product identified by its name, along with its category. |
| Retrieves the JSON string for a product identified by its name, without its category. |
| Retrieves the JSON string for a category specified by its ID, along with its products. |
| Retrieves the JSON string for a category specified by its name, along with its products. |
| Retrieves the JSON string for a category specified by its name, without its products. |
| Retrieves the JSON string for a category identified by its ID, excluding its products. |
The exposed URLs correspond to the methods of the [IDao] interface in the [DAO] layer. The methods of the web service / JSON are all built on the same model. We will examine a few of them.
13.5.5.2. The controller skeleton
The controller skeleton is as follows:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Category;
import spring.data.entities.Product;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// Spring dependencies
@Autowired
private ApplicationModel application;
// JSON filters
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategoryWithProducts")
private ObjectMapper jsonMapperCategoryWithProducts;
@Autowired
@Qualifier("jsonMapperProductWithCategory")
private ObjectMapper jsonMapperProductWithCategory;
@Autowired
@Qualifier("jsonMapperCategoryWithoutProducts")
private ObjectMapper jsonMapperCategoryWithoutProducts;
@Autowired
@Qualifier("jsonMapperProductWithoutCategory")
private ObjectMapper jsonMapperProductWithoutCategory;
// The [MyController] class is a singleton and is instantiated only once
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProducts", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProducts(HttpServletRequest request) throws JsonProcessingException {
...
}
- line 28: the [@Controller] annotation makes the class a Spring component;
- lines 32–33: injection of a reference to the [ApplicationModel] class;
- lines 36-50: injection of references to the JSON mappers;
- line 58: the exposed URL is [/addProducts]. The client must use a [POST] method to make its request (method = RequestMethod.POST). It must send the posted value as a JSON string (content-type = "application/json; charset=UTF-8"). The method itself returns the response to the client (line 59). This will be a string (line 60). The HTTP header [Content-type: application/json; charset=UTF-8] will be sent to the client to indicate that it will receive a JSON string (line 58);
- line 60: the [addProduits] method returns the JSON string containing the list of products added to the database;
13.5.5.3. Controller method responses
All controller methods return the following [Response] type:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// response body
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- line 5: the response encapsulates a type T;
- line 13: the response of type T;
- lines 9–11: a method may encounter an exception. In this case, it will return a response with:
- line 9: status!=0;
- line 11: the list of errors encountered;
13.5.5.4. The URL [/addProducts]
The URL [/addProducts] is handled by the following method:
@RequestMapping(value = "/addProducts", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProducts(HttpServletRequest request) throws JsonProcessingException {
// response
Response<List<Product>> response;
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
List<Product> products = jsonMapperProductWithoutCategory.readValue(body, new TypeReference<List<Product>>() {
});
// re-establish the link between products and categories
for (Product product : products) {
product.setCategory(application.getCategoryByIdWithoutProducts(product.getCategoryId()));
}
// persist the products
application.addProducts(products);
response = new Respon se<List<Product>>(0, null, products);
} catch (DaoException e1) {
response = new Response<List<Product>>(1000, e1.getErrors(), null);
} catch (Exception e2) {
response = new Response<List<Product>>(1000, getErrorsForException(e2), null);
}
// JSON response
return jsonMapperProductWithoutCategory.writeValueAsString(response);
}
- line 3: the method takes [HttpServletRequest request] as a parameter, which encapsulates all the information about the client's request;
- line 5: the response that will be sent to the client: a list of products;
- line 8: we retrieve the posted value. The [CharStreams] class belongs to the [Google Guava] library, whose reference we added to the [pom.xml] file. We obtain the JSON string posted by the client. We need to deserialize it to do something with it;
- lines 8–10: deserialization is performed. We obtain a list of products where each product has a [category=null] field;
- lines 12–14: We reset the [category] field for all products in the list. To do this, we use the product’s [categoryId] field, which is initialized;
- line 16: the products are inserted into the database;
- line 17: the [response] object is initialized with the list of products;
- lines 18-19: case where the method encounters an exception from the [DAO] layer. We initialize the response with [status=1000] (error code) [messages=e1.getMessages()], i.e., we send the client the list of errors encountered on the server side;
- lines 20–21: case where the method encounters another type of exception. We initialize the response with [status=1000] (error code) [messages=getErrorsForException(e)] where [getErrorsForException] is a private method of the class that returns the list of errors associated with the exceptions in e's exception stack, and [body=null];
- line 24: the JSON string of the response is returned;
13.5.5.5. The URL [/getAllProducts]
The URL [/getAllProducts] is handled by the following method:
@RequestMapping(value = "/getAllProducts", method = RequestMethod.GET, content-type = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProducts() throws JsonProcessingException {
// response
Response<List<Product>> response;
try {
response = new Response<List<Product>>(0, null, application.getAllProducts());
} catch (DaoException e1) {
response = new Response<List<Product>>(1003, e1.getErrors(), null);
} catch (Exception e2) {
response = new Response<List<Product>>(1003, getErrorsForException(e2), null);
}
// JSON response
return jsonMapperProductWithoutCategory.writeValueAsString(response);
}
- Line 1: The URL [/getAllProduits] is requested using a [GET] operation. It returns JSON;
- line 2: the method itself sends the JSON response to the client;
- line 5: the method returns a JSON string of type [Response<List<Product>>];
- line 7: products are requested without their category;
- lines 8–12: in case of an error, the response is initialized with an error code and error messages;
- line 14: the JSON response is sent to the client;
13.5.5.6. Conclusion
We will not cover the other methods of the controller. They are similar to one or the other of the two methods we just presented.
13.5.6. The Web Service / JSON Execution Class
![]() |
The [Boot] class is the project’s executable class:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
- Line 10: The static method [SpringApplication.run] is executed. The [SpringApplication] class is a class from the [Spring Boot] project (line 3). Two parameters are passed to it:
- [AppConfig.class]: the class that configures the entire application;
- [args]: any arguments passed to the [main] method on line 9. This parameter is not used here;
When this class is executed, the following logs are generated:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.2.2.RELEASE)
2015-03-24 16:22:46.608 INFO 9492 --- [ main] spring.webjson.server.boot.Boot : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654 INFO 9492 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521 INFO 9492 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569 INFO 9492 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for processing by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker: Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for processing by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker: Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for processing by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker: Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for processing by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586 INFO 9492 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850 INFO 9492 --- [ main] o.apache.catalina.core.StandardService : Starting Tomcat service
2015-03-24 16:22:48.852 INFO 9492 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992 INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992 INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645 INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650 INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651 INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380 INFO 9492 --- [ main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392 INFO 9492 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
2015-03-24 16:22:50.478 INFO 9492 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480 INFO 9492 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483 INFO 9492 --- [ main] org.hibernate.cfg.Environment : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697 INFO 9492 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806 INFO 9492 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058 INFO 9492 --- [ main] o.h.h.i.ast.ASTQueryTranslatorFactory : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Product>> spring.webjson.server.service.Controller.addProducts(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Product>> spring.webjson.server.service.Controller.updateProducts(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProducts],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Product>> spring.webjson.server.service.Controller.getAllProducts()
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProductByNameWithoutCategory/{name}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Product> spring.webjson.server.service.Controller.getProductByNameWithoutCategory(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProductByNameWithCategory/{name}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Product> spring.webjson.server.service.Controller.getProductByNameWithCategory(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProductByIdWithCategory/{productId}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Product> spring.webjson.server.service.Controller.getProductByIdWithCategory(java.lang.Long)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategoryByIdWithoutProducts/{categoryId}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProducts],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProductByIdWithoutCategory/{productId}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Product> spring.webjson.server.service.Controller.getProductByIdWithoutCategory(java.lang.Long)
2015-03-24 16:22:52.659 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] to a handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001 INFO 9492 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106 INFO 9492 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108 INFO 9492 --- [ main] spring.webjson.server.boot.Boot : Boot started in 6.752 seconds (JVM running for 7.433)
- lines 17-19: Tomcat server startup to run the web/JSON service;
- lines 25-33: construction of the [DAO] layer;
- lines 32-51: the exposed URLs are discovered;
13.5.7. Web service / JSON testing
To perform the tests, we generate the MySQL database [dbintrospringdata] from the SQL script [dbintrospringdata.sql]:
![]() |
Once this is done, we use the [Advanced Rest Client] (see section 22.5) to query the URLs exposed by the web service / JSON (the web service / JSON must be running).
![]() |
- In [1-3], we request the URL [/getAllCategories] via an HTTP GET request;
We receive the following response:
![]() |
- In [1], the client’s HTTP request;
- in [2], the server’s HTTP response;
- in [3], the status [200 OK] indicates that the server successfully processed the request;
- in [4], the server's JSON response;
The complete JSON response is as follows:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"name":"category0","products":[{"id":1849,"version":0,"name":"product00","categoryId":415,"price":100.0,"description":"desc00"},{"id":1850,"version":0,"name":"product01","categoryId":415,"price":101.0,"description":"desc01"},{"id":1851,"version":0,"name":"product02","categoryId":415,"price":102.0,"description":"desc02"},{"id":1852,"version":0,"name":"product03","categoryId":415,"price":103.0,"description":"desc03"},{"id":1853,"version":0,"name":"product04","categoryId":415,"price":104.0,"description":"desc04"}]},{"id":416,"version":0,"name":"category1","products":[{"id":1856,"version":0,"name":"product12","categoryId":416,"price":112.0,"description":"desc12"},{"id":1857,"version":0,"name":"product13","categoryId":416,"price":113.0,"description":"desc13"},{"id":1858,"version":0,"name":"product14","categoryId":416,"price":114.0,"description":"desc14"},{"id":1854,"version":0,"name":"product10","categoryId":416,"price":110.0,"description":"desc10"},{"id":1855,"version":0,"name":"product11","categoryId":416,"price":111.0,"description":"desc11"}]}]}
- status:0 means there were no server-side errors;
- messages: null means there are no error messages;
- body: is the body of the response, in this case the list of categories with their products. There are two categories, each with 5 products;
We will add the product [product15] to the category [category1]. To do this, we will use the URL [/addCategories], which has the following code:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Category>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// persist the categories
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategoryWithProducts);
List<Category> categories = mapper.readValue(body, new TypeReference<List<Category>>() {
});
// Restore the link between products and categories
for (Category category : categories) {
Set<Product> products = category.getProducts();
if (products != null) {
for (Product product : category.getProducts()) {
product.setCategory(category);
}
}
}
// Save the categories
application.addCategories(categories);
response = new Response<List<Category>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Category>>(1004, getErrorsForException(e), null);
}
// JSON response
return mapper.writeValueAsString(response);
}
- line 1: the client must send a POST request, and the posted value must be a JSON string;
- lines 9–12: the posted value must be a list of categories with their associated products;
We will create a category [category2] with a product [product21]. The JSON string to be sent is then as follows:
[{"id":null,"version":0,"name":"category2","products":[{"id":null,"version":0,"name":"product21","categoryId":null,"price":111.0,"description":"desc21"}]}]
The request to the web service / JSON is made as follows:
![]() |
- in [1], the requested URL;
- in [2], it is requested via a POST operation;
- in [3], the posted JSON string;
- in [4], the server is told that JSON data will be sent;
The server's response is as follows:
![]() |
- in [1], we see that both the category and its product now have a primary key, indicating that they have likely been inserted into the database. We will verify this by using the URL [/getCategorieByNameWithProduits/categorie2]:
![]() |
We get the following result:
![]() |
We have indeed retrieved the category [categorie2] with its single product [produit21]. We can also request only the product. To do this, let’s use the URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
We get the following result:
![]() |
All [GET] operations can be performed in a standard web browser:
![]() |
Readers are invited to test the other URLs of the web service / json.
13.6. A client programmed for the /json web service
Now that the [dbintrospringdata] database is available on the web, we will write an application that uses it. We will then have the following client/server architecture:
![]() |
The client application will have two layers:
- a [DAO] [2] layer to communicate with the /json web application that exposes the database;
- a JUnit [1] testing layer to verify that the client and server are functioning correctly;
13.6.1. The Eclipse Project
The client’s Eclipse project is as follows:
![]() |
- the [src/main/java] folder implements the [DAO] layer;
- the [src/test/java] folder implements the JUnit tests;
13.6.2. Maven Project Configuration
The project is a Maven project configured by the following [pom.xml] file:
<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>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Web server / JSON console client</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- JSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- logging library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- lines 14–18: the parent Maven project [spring-boot-starter-parent], which allows us to define a number of dependencies without specifying their versions, as these are defined in the parent project;
- lines 22–25: although we are not writing a web application, we need the [spring-web] dependency, which includes the [RestTemplate] class that allows for easy interfacing with a web/JSON application;
- lines 27–34: a JSON library;
- lines 36–39: a dependency that will allow us to set a timeout for the client’s HTTP requests. A timeout is the maximum wait time for the server’s response. After this time, the client signals a timeout error by throwing an exception;
- lines 41–46: the Google Guava library used in the JUnit test. For this reason, we have set its scope to [test] (line 45). This means that this dependency is included only when executing code from the [src/test/java] branch;
- lines 48–51: the logging library;
- lines 52–63: the dependency for JUnit tests. In particular, it includes the JUnit 4 library required for testing. These dependencies have the [<scope>test</scope>] attribute, indicating that they are only required for the testing phase. They are not included in the final project archive;
13.6.3. Implementation of the [DAO] layer
![]() |
![]() |
- The [spring.client.config] package contains the Spring configuration for the [DAO] layer;
- The [spring.client.dao] package contains the implementation of the [DAO] layer;
- The [spring.client.entities] package contains the objects exchanged with the web service / JSON;
13.6.3.1. Configuration
![]() |
The [DaoConfig] class handles the Spring configuration of the [DAO] layer. Its code is as follows:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// Create the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// Set the request timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// JSON filters
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategoryWithProducts")
public ObjectMapper jsonMapperCategoryWithProducts() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategory", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduct", SimpleBeanPropertyFilter.serializeAllExcept("category")));
// result
return mapper;
}
@Bean(name = "jsonMapperProductWithCategory")
public ObjectMapper jsonMapperProductWithCategory() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduct", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategory", SimpleBeanPropertyFilter.serializeAllExcept("products")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategoryWithoutProducts")
public ObjectMapper jsonMapperCategoryWithoutProducts() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("products")));
// result
return mapper;
}
@Bean(name = "jsonMapperProductWithoutCategory")
public ObjectMapper jsonMapperProductWithoutCategory() {
// JSON mapper
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduct",
SimpleBeanPropertyFilter.serializeAllExcept("category")));
// result
return mapper;
}
}
- line 13: the class is a Spring configuration class—Spring components can be found in the [spring.client.dao] package;
- line 17: a timeout of one second (1000 ms) is set;
- lines 32–35: the bean that returns this value;
- line 18: the URL of the web service / JSON;
- lines 37–40: the bean that returns this value;
- lines 20–30: the configuration of the [RestTemplate] class that handles communication with the web service / JSON. When no configuration is required, it can be instantiated in the code with a simple [new RestTemplate()]. Here, we want to set the timeout for communication with the web service / JSON. The [timeout] bean on line 36 is passed as a parameter to the [RestTemplate] method on line 24;
- line 23: the [HttpComponentsClientHttpRequestFactory] component is the one that allows us to set the timeout for communications (lines 29–30);
- line 24: the [RestTemplate] class is constructed using this component. Since it relies on this component to communicate with the web service / JSON, the exchanges will indeed be subject to the timeout;
- The client and server will exchange lines of text. A converter handles serializing an object into text and, conversely, deserializing text into an object. There may be several converters associated with the [RestTemplate] class, and the one chosen at any given time depends on the HTTP headers sent by the server. Here, we will have no converter. Therefore, the [RestTemplate] component will not attempt to convert the following two elements in any way:
- the posted text;
- the text received in response;
These texts will be JSON strings, which will therefore be left as-is by the [RestTemplate] component. It is we, the developers, who will perform the necessary JSON serialization and deserialization. This is because the filters to be applied to the posted value and the received response may differ, and experience shows that it is easier to handle them yourself than to try to configure the [RestTemplate] component to use the correct JSON converter;
- lines 42–92: define JSON filters. These are the same as the server-side filters presented and explained in Section 13.5.3.1;
- lines 43–46: a JSON mapper without filters;
- lines 64–68: a JSON mapper to retrieve a category without its products;
- lines 48–58: a JSON mapper to retrieve a category with its products;
- lines 83–92: a JSON mapper to retrieve a product without its category;
- lines 60-70: a JSON mapper to retrieve a product with its category;
All these beans will be available to the [DAO] layer code as well as to the JUnit test.
13.6.3.2. The Entities
![]() |
The entities handled by the [DAO] layer are those it exchanges with the web service / JSON. These are the items and products. On the server side, these entities had JPA persistence annotations. Here, those annotations have been removed. We’re including the entity code again for reference:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// properties
protected Long id;
protected Long version;
// constructors
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// override [equals] and [hashCode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// JSON signature
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters and setters
...
}
[Category]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategory")
public class Category extends AbstractEntity {
// properties
private String name;
// associated products
public Set<Product> products = new HashSet<Product>();
// constructors
public Category() {
}
public Category(String name) {
this.name = name;
}
// methods
public void addProduct(Product product) {
// Add the product
products.add(product);
// Set its category
product.setCategory(this);
}
// getters and setters
...
}
[Product]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduct")
public class Product extends AbstractEntity {
// name
private String name;
// category number
private Long categoryId;
// price
private double price;
// the description
private String description;
// the category
private Categorie category;
// constructors
public Product() {
}
public Product(String name, double price, String description) {
this.name = name;
this.price = price;
this.description = description;
}
// getters and setters
...
}
13.6.3.3. The [DaoException] class
![]() |
When the [DAO] layer encounters an error, it will throw a [DaoException]. This class is used on the server side and is described in section 11.3.7.
13.6.3.4. The [DAO] layer interface
![]() |
The [DAO] layer implements the [IDao] interface described in Section 11.3.7.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Category;
import spring.client.entities.Product;
public interface IDao {
// Insert a list of products
public List<Product> addProducts(List<Product> products);
// Delete all products
public void deleteAllProducts();
// Update a list of products
public List<Product> updateProducts(List<Product> products);
// Get all products
public List<Product> getAllProducts();
// insert a list of categories
public List<Category> addCategories(List<Category> categories);
// Delete all categories
public void deleteAllCategories();
// Update a list of categories
public List<Category> updateCategories(List<Category> categories);
// Get all categories
public List<Category> getAllCategories();
// a specific product
public Product getProductByIdWithCategory(Long productId);
public Product getProductByIdWithoutCategory(Long productId);
public Product getProductByNameWithCategory(String name);
public Product getProductByNameWithoutCategory(String name);
// a specific category
public Category getCategoryByIdWithProducts(Long categoryId);
public Category getCategoryByIdWithoutProducts(Long categoryId);
public Category getCategoryByNameWithProducts(String name);
public Category getCategoryByNameWithoutProducts(String name);
}
13.6.3.5. The web service / JSON response
![]() |
We have seen that all URLs of the web service / JSON return a [Response] type defined in section 13.5.5.3. We reproduce this class here:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// response body
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
13.6.3.6. Implementation of communication with the web service / JSON
![]() |
The [ AbstractDao] class implements communication with the web service / JSON:
package spring.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
public abstract class AbstractDao {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String webServiceJsonUrl;
// generic request
protected String getResponse(String url, String jsonPost) {
// url: URL to contact
// jsonPost: the JSON value to post
try {
// execute request
RequestEntity<?> request;
if (jsonPost != null) {
// POST request
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// GET request
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// execute the request
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- lines 15-16: injection of the [RestTemplate] component, which handles communication with the server;
- lines 17-18: injection of the web service URL / JSON;
The implementation of the methods for communicating with the server is factored into the [getResponse] method:
- line 21: the method receives 2 parameters:
- [url]: the requested URL;
- [jsonPost]: the JSON string to post, or null otherwise. If [jsonPost == null], the URL request is made using a GET; otherwise, using a POST;
- line 38: the statement that sends the request to the server and receives its response. The [RestTemplate] component offers a wide range of methods for interacting with the server. We have chosen the [exchange] method here, but others are available;
- lines 27–36: we need to construct the [RequestEntity] request. It differs depending on whether we use a GET or a POST request;
- lines 30–31: the request for a GET. The [RequestEntity] class provides static methods to create GET, POST, HEAD, and other requests. The [RequestEntity.get] method allows you to create a GET request by chaining the various methods that build it:
- the [RequestEntity.get] method takes the target URL as a parameter in the form of a URI instance,
- the [accept] method allows you to define the elements of the [Accept] HTTP header. Here, we specify that we accept the [application/json] type that the server will send;
- the [build] method uses this information to construct the [RequestEntity] type of the request;
- lines 34–35: the POST request. The [RequestEntity.post] method creates a POST request by chaining the various methods that build it:
- the [RequestEntity.post] method takes the target URL as a parameter in the form of a URI instance,
- the [header] method defines an HTTP header. Here, we send the [Content-Type: application/json] header to the server to indicate that the posted data will arrive in the form of a JSON string;
- the [accept] method allows us to indicate that we accept the [application/json] type that the server will send;
- the [body] method sets the posted value. This is the fourth parameter of the generic [getResponse] method (line 1);
- line 38: the [RestTemplate].exchange method returns a [ResponseEntity<String>] type that encapsulates the entire server response: HTTP headers and document body. The [ResponseEntity].getBody() method retrieves this body, which represents the server’s response—in this case, a string;
13.6.3.7. Implementation of the [IDao] interface
![]() |
The [Dao] class implements the [IDao] interface:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Category;
import spring.client.entities.Product;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// JSON filters
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategoryWithProducts")
private ObjectMapper jsonMapperCategoryWithProducts;
@Autowired
@Qualifier("jsonMapperProductWithCategory")
private ObjectMapper jsonMapperProductWithCategory;
@Autowired
@Qualifier("jsonMapperCategoryWithoutProducts")
private ObjectMapper jsonMapperCategoryWithoutProducts;
@Autowired
@Qualifier("jsonMapperProductWithoutCategory")
private ObjectMapper jsonMapperProductWithoutCategory;
@Override
public List<Product> addProducts(List<Product> products) {
// ----------- add products (without their category)
...
}
- line 17: the [Dao] class is a Spring component into which other Spring components can be injected;
- line 18: the [Dao] class extends the [AbstractDao] class we just saw and implements the [IDao] interface;
- lines 20–21: we inject the Spring context to access its beans;
- lines 24–38: injection of the JSON mappers defined in the [AppConfig] class presented in section 13.6.2;
The implementations of the various methods of the [IDao] interface all follow the same pattern. We will present two methods, one based on a [POST] operation, the other on a [GET] operation.
An example of [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String name) {
// ----------- retrieve a category by name, along with its products
try {
// request
Response<Category> response = jsonMapperCategoryWithProducts.readValue(
getResponse(String.format("/getCategoryByNameWithProducts/%s", name), null),
new TypeReference<Response<Category>>() {
});
// error?
if (response.getStatus() != 0) {
// Throw an exception
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// return the body of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- Line 7: The [getResponse] method of the parent class is called. This method handles communication with the web service/JSON. Its parameters are as follows:
getResponse(String.format("/getCategorieByNameWithProduits/%s", name), null)
- (continued)
- the URL of the service being queried [/getCategoryByNameWithProducts/name];
- the posted value. Here, there is none;
The [getResponse] method returns a String representing the JSON response sent by the server. We deserialize this JSON response as follows:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Category>>() {
});
because the JSON string is the serialization of a [Response<Category>] type;
- lines 11–17: we check the response status. If the status is not 0, then there was a server-side error. We then throw an exception (line 13), using the information contained in the response (status and list of error messages);
- line 16: if there was no server-side error, we return the body of type [Response<Category>], i.e., the requested category;
- lines 18–19: handle the exception thrown on line 16;
- lines 20–22: handle all other exceptions;
An example of [POST]: [addCategories]
@Override
public List<Category> addCategories(List<Category> categories) {
// ----------- add categories (with their products)
try {
// request
Response<List<Category>> response = jsonMapperCategoryWithProducts.readValue(
getResponse("/addCategories", jsonMapperCategoryWithProducts.writeValueAsString(categories)),
new TypeReference<Response<List<Category>>>() {
});
// error?
if (response.getStatus() != 0) {
// throw an exception
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// return the body of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- line 2: the [addCategories] method is used to persist the categories passed as parameters in the database. It returns these same categories enriched with their primary keys. If the categories are passed along with products, those are also persisted;
- line 7: the parent’s [getResponse] method is called to handle communication with the web service / JSON;
- the first parameter is the URL [/addCategories];
- the second parameter is the posted value, in this case the list of categories to be persisted;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
The resulting JSON string is then deserialized to obtain the expected [Response<List<Category>] type:
Response<List<Category>> response = jsonMapperCategoryWithProducts.readValue(
jsonResponse,
new TypeReference<Response<List<Category>>>() {
});
- lines 11–17: handling the server response (error or not);
- lines 20–22: exception handling;
All other methods follow the pattern of the two methods presented.
13.6.4. The JUnit test
Let’s return to the client/server architecture currently under development:
![]() |
We have built a [DAO] layer [2] with the same interface as the [DAO] layer [4]. To test the [DAO] layer [2], we can therefore use the JUnit test that was used to test the [DAO] layer [4]. As a reminder, it is as follows:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Category;
import spring.client.entities.Product;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// [DAO] layer
@Autowired
private IDao dao;
// JSON filters
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategoryWithProducts")
private ObjectMapper jsonMapperCategoryWithProducts;
@Autowired
@Qualifier("jsonMapperProductWithCategory")
private ObjectMapper jsonMapperProductWithCategory;
@Autowired
@Qualifier("jsonMapperCategoryWithoutProducts")
private ObjectMapper jsonMapperCategoryWithoutProducts;
@Autowired
@Qualifier("jsonMapperProductWithoutCategory")
private ObjectMapper jsonMapperProductWithoutCategory;
@Before
public void cleanAndFill() {
// We clear the database before each test
log("Clearing the database", 1);
// clear the [CATEGORIES] table—this will cascade and clear the [PRODUCTS] table
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Filling the database", 1);
// populating the tables
List<Category> categories = new ArrayList<Category>();
for (int i = 0; i < 2; i++) {
Category category = new Category(String.format("category%d", i));
for (int j = 0; j < 5; j++) {
category.addProduct(new Product(String.format("product%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j));
}
categories.add(category);
}
// Add the category—the products will also be added automatically
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// list of categories
log("List of categories", 2);
List<Category> categories = dao.getAllCategories();
display(categories, jsonMapperCategoryWithoutProducts);
// list of products
log("List of products", 2);
List<Product> products = dao.getAllProducts();
display(products, jsonMapperProductWithoutCategory);
// some checks
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, products.size());
Category category = findCategoryByName("category0", categories);
Assert.assertNotNull(category);
Product product = findProductByName("product03", products);
Assert.assertNotNull(product);
Long categoryId = product.getCategoryId();
Assert.assertEquals(category.getId(), categoryId);
}
@Test
public void getCategoryByNameWithProducts() {
log("getCategoryByNameWithProducts", 1);
Category category1 = dao.getCategoryByNameWithProducts("category1");
Assert.assertNotNull(category1);
Assert.assertEquals(5, category1.getProducts().size());
}
@Test
public void getCategoryByNameWithoutProducts() {
log("getCategoryByNameWithoutProducts", 1);
Category category1 = dao.getCategoryByNameWithoutProducts("category1");
Assert.assertNotNull(category1);
Assert.assertEquals("category1", category1.getName());
}
@Test
public void getCategoryByIdWithProducts() {
log("getCategoryByIdWithProducts", 1);
Category category1 = dao.getCategoryByNameWithProducts("category1");
Category category2 = dao.getCategoryByIdWithProducts(category1.getId());
Assert.assertNotNull(category2);
Assert.assertEquals(category1.getId(), category2.getId());
Assert.assertEquals(category1.getName(), category2.getName());
}
@Test
public void getCategoryByIdWithoutProducts() {
log("getCategoryByIdWithoutProducts", 1);
Category category1 = dao.getCategoryByNameWithProducts("category1");
Category category2 = dao.getCategoryByIdWithoutProducts(category1.getId());
Assert.assertNotNull(category2);
Assert.assertEquals(category1.getName(), category2.getName());
}
@Test
public void getProductByNameWithCategory() {
log("getProductByNameWithCategory", 1);
Product product = dao.getProductByNameWithCategory("product03");
Assert.assertNotNull(product);
Assert.assertNotNull(product.getCategory());
}
@Test
public void getProductByNameWithoutCategory() {
log("getProductByNameWithoutCategory", 1);
Product product = dao.getProductByNameWithoutCategory("product03");
Assert.assertNotNull(product);
assert.assertEquals("product03", product.getName());
}
@Test
public void getProductByIdWithCategory() {
log("getProductByNameWithCategory", 1);
Product product = dao.getProductByNameWithCategory("product03");
Product product2 = dao.getProductByIdWithCategory(product.getId());
Assert.assertNotNull(product2);
Assert.assertEquals(product2.getName(), product.getName());
Assert.assertEquals(product2.getId(), product.getId());
Assert.assertEquals(product.getCategory().getId(), product2.getCategory().getId());
}
@Test
public void getProductByIdWithoutCategory() {
log("getProductByIdWithoutCategory", 1);
Product product = dao.getProductByNameWithCategory("product03");
Product product2 = dao.getProductByIdWithoutCategory(product.getId());
Assert.assertNotNull(product2);
Assert.assertEquals(product2.getName(), product.getName());
Assert.assertEquals(product2.getId(), product.getId());
}
@Test
public void doInsertsInTransaction() {
log("Adding a category [cat1] with two products of the same name", 1);
// perform the insertion
Category category = new Category("cat1");
category.addProduct(new Product("x", 1.0, ""));
category.addProduct(new Product("x", 1.0, ""));
// adding the category - the products will also be inserted automatically
try {
category = dao.addCategories(Lists.newArrayList(category)).get(0);
} catch (DaoException e) {
show("The following errors occurred:", e.getErrors());
}
// checks
List<Category> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Product> products = dao.getAllProducts();
Assert.assertEquals(10, products.size());
}
@Test
public void updateDatabase() {
log("Updating product prices for [category1]", 1);
Category category1 = dao.getCategoryByNameWithProducts("category1");
Category category1Saved = dao.getCategoryByNameWithProducts("category1");
Set<Product> products = category1.getProducts();
for (Product product : products) {
product.setPrice(1.1 * product.getPrice());
}
List<Product> products2 = Lists.newArrayList(products);
products2 = dao.updateProducts(products2);
// checks
List<Product> savedProducts = Lists.newArrayList(category1Saved.getProducts());
for (Product product2 : products2) {
Product product = findProductByName(product2.getName(), productsSaved);
Assert.assertEquals(product2.getPrice(), product.getPrice() * 1.1, 1e-6);
}
}
@Test
public void addProducts() throws BeansException, JsonProcessingException {
log("Adding two products from category [categorie0]", 1);
Category category0 = dao.getCategoryByNameWithoutProducts("category0");
Long categoryId = category0.getId();
Product p1 = new Product("x", 1, "");
p1.setCategoryId(categoryId);
p1.setCategory(category0);
Product p2 = new Product("y", 1, "");
p2.setCategoryId(categoryId);
p2.setCategory(category0);
List<Product> products = new ArrayList<Product>();
products.add(p1);
products.add(p2);
products = dao.addProducts(products);
// validation
display(products, jsonMapperProductWithoutCategory);
}
// -------------- private methods
private Product findProductByName(String name, List<Product> products) {
for (Product product : products) {
if (product.getName().equals(name)) {
return product;
}
}
return null;
}
private Category findCategoryByName(String name, List<Category> categories) {
for (Category category : categories) {
if (category.getName().equals(name)) {
return category;
}
}
return null;
}
// Display an element of type T
static private <T> void display(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// Display a list of elements of type T
static private <T> void display(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
display(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// display message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// title
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
It executes successfully and produces the following results on the console:
Dumping the database --------------------------------
Filling the database --------------------------------
Adding two products from category [categorie0] --------------------------------
{"id":6285,"version":0,"name":"x","categoryId":1319,"price":1.0,"description":""}
{"id":6286,"version":0,"name":"y","categoryId":1319,"price":1.0,"description":""}
Clearing the database --------------------------------
Populating the database --------------------------------
Updating product prices for [category1] --------------------------------
Clearing the database --------------------------------
Populating the database --------------------------------
getCategoryByIdWithoutProducts --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getProductByNameWithoutCategory --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getCategoryByNameWithProducts --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getCategoryByNameWithoutProducts --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getProductByNameWithCategory --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getProductByNameWithCategory --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
getProductByIdWithoutCategory --------------------------------
Emptying the database --------------------------------
Populating the database --------------------------------
-- List of categories
{"id":1337,"version":0,"name":"category0"}
{"id":1338,"version":0,"name":"category1"}
-- List of products
{"id":6367,"version":0,"name":"product00","categoryId":1337,"price":100.0,"description":"desc00"}
{"id":6368,"version":0,"name":"product01","categoryId":1337,"price":101.0,"description":"desc01"}
{"id":6369,"version":0,"name":"product02","categoryId":1337,"price":102.0,"description":"desc02"}
{"id":6370,"version":0,"name":"product03","categoryId":1337,"price":103.0,"description":"desc03"}
{"id":6371,"version":0,"name":"product04","categoryId":1337,"price":104.0,"description":"desc04"}
{"id":6372,"version":0,"name":"product10","categoryId":1338,"price":110.0,"description":"desc10"}
{"id":6373,"version":0,"name":"product11","categoryId":1338,"price":111.0,"description":"desc11"}
{"id":6374,"version":0,"name":"product12","categoryId":1338,"price":112.0,"description":"desc12"}
{"id":6375,"version":0,"name":"product13","categoryId":1338,"price":113.0,"description":"desc13"}
{"id":6376,"version":0,"name":"product14","categoryId":1338,"price":114.0,"description":"desc14"}
Clearing the database --------------------------------
Populating the database --------------------------------
getCategoryByIdWithProducts --------------------------------
Emptying the database --------------------------------
Filling the database --------------------------------
Adding a category [cat1] with two products of the same name --------------------------------
The following errors occurred:
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NAME'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































