In this chapter, you will learn about the key concepts of the Spring framework, and how we can use this framework to develop web applications. The following are some of the topics that will be dealt with:
- Introduction to the Spring IOC container
- Introduction to the Spring web MVC
- Building a
Hello World
web application with Spring Boot - Implementing controllers
- Handling request parameters
- Handler interceptors
- Handling responses
- Creating a RESTful Web Service
- Dockerizing Spring Boot Application
Starting with the section, Implementing controllers in this chapter, we will start building a sample healthcare web application to demonstrate the key concepts. The following are some of the key functionalities of the sample app which will be covered as part of building the app:
- Signup for doctors and patients
- Logging into the app
- Searching for doctors based on their specialties
- Fixing an appointment
- Interacting with the doctor
As a primer to learning the Spring Web MVC framework, it is recommended to learn some of the following object-oriented design principles, which act as a basis for the Spring Web framework. Note that these principles form a part of the famous Single responsibility, Open-closed, Liskov substitution, Interface segregation, and Dependency inversion (SOLID) principle by Robert Martin (Uncle Bob):
- Single Responsibility Principle (SIP): This principle states that a module, a class, or a method should have the responsibility of serving just one functionality of the underlying software. In other words, a module, a class or a method should have just one reason to change. Modules or classes following SIP have high cohesiveness, and thus, can be termed as reusable entities. Classes violating SIP are found to have high cyclomatic complexity, and thus, low testability.
- Open-Closed Principle (OCP): This principle states that the classes are open for extension, but closed for modification. Based on this principle, the core classes of the Spring Web MVC consist of some methods which are marked as final, which, essentially, means that these final methods can not be overridden with custom behavior.
- Liskov Substitution Principle (LSP): This principle states that if a class
A
(child class) is derived from classB
(parent class), then the object of classB
can be replaced by (or substituted with) an object of classA
without changing any of the properties of classB
. It can be inferred that the functions which use references of the base class must be able to use objects of the derived class without the need to know about the implementation of the base class. For example, let's consider the square and rectangle example. In the case where square derives from rectangle, then, as per LSP, an object of the classRectangle
can be substituted with an object of the classSquare
. However, in reality, this is not possible without doing appropriate implementation in theSquare
class setter methods, where setting either of length or breadth sets another side of equal length, and/or code using these classes do appropriate checks to find out whether the object is an instance of the classSquare
orRectangle
. - Interface Segregation Principle (ISP): This principle states that the fat interfaces having large number of API definitions should be split into smaller interfaces defining a set of cohesive APIs. Not following this principle leads to the client providing empty implementations for unwanted APIs.
- Dependency Inversion Principle (DIP): This principle is pretty much related to the IOC principle, which is discussed in the next section. It states that the dependency relationship between higher-level modules with low-level modules is reversed, thus making these modules independent of each other's implementation details.
Before we get into understanding what is Spring IOC Container, let us quickly learn what is IOC.
Many a times, the Hollywood principle of Don't call us, we will call you is used to explain the concept of Inversion of Control. In Hollywood, this is what one gets to hear after auditioning is done. If we apply this principle to the programming model, instead of the classes creating dependencies along with serving functionality, the dependencies are appropriately made available to the classes during runtime, and the classes just need to focus on delivering the functionality.
Simply speaking, Inversion of Control (IOC) is about inverting the flow of control that the traditional programming model used to have in terms of objects at the higher-level handling the creation and management of lower-level objects' (can also be termed as dependencies) life cycle. In the IOC programming model, higher-level objects rather receive one or more instances of these dependencies from the calling object or external framework. This is why IOC is also termed Dependency Injection, wherein the dependencies are injected appropriately, and, objects bother themselves solely with the program execution and not with the object creation. Inversion of Control is related to the object-oriented principle known as the Dependency Inversion Principle (DIP), coined by Robert Martin (Uncle Bob).
Let's take a look at the following code sample, which represents higher-level objects handling the creation of lower-level objects (dependencies):
/* * This class demonstrates the dependency of higher-level object * (DrawWithoutIOC) * onto lower level objects such as Rectangle, Square which are * created within * Shape class based on the value of shapeType which is passed as a * method * parameter to draw method. */ public class DrawWithoutIOC { Logger logger = Logger.getLogger(DrawWithoutIOC.class.getName()); public void draw(String shapeType) { Shape shape = new Shape(); try { shape.draw(shapeType); } catch (UndefinedShapeException e) { logger.log(Level.INFO, e.getMessage(), e); } } /* * Note that Shape class creates instances of Rectangle or Square class * based on the value of shapeType. Any new value that needs to be supported requires change in the draw method of Shape class. */ private class Shape { public void draw(String shapeType) throws UndefinedShapeException { if(shapeType.equals("rectangle")) { Rectangle rectangle = new Rectangle(); rectangle.draw(); } else if(shapeType.equals("square")) { Square square = new Square(); square.draw(); } else { String shapeNotSupportedMessage = "Shape " + shapeType + " not supported"; logger.log(Level.INFO, shapeNotSupportedMessage); throw new UndefinedShapeException (shapeNotSupportedMessage); } } } public static void main(String[] args) { DrawWithoutIOC drawWithoutIOC = new DrawWithoutIOC(); drawWithoutIOC.draw("circle"); } }
Let us take a look at the class DrawWithIOC
, which accepts the implementation of the Shape
object in its public constructor. Note that the dependencies, such as different implementations of the Shape
object, are rather injected, and code just does the execution of business logic without bothering about creation of objects related to the different implementations of Shape
. Other alternatives to injecting the dependencies are passing arguments to a factory method of the class, or through properties that are set on the object instance:
/** * In this class, the Shape is passed as parameter to DrawWithIOC * constructor. * draw method on a DrawWithIOC object just invokes the draw method * on Shape object. * It, no longer, manage the creation of Shape object based on * shapeType and there upon, invoke the draw method. **/ public class DrawWithIOC { Logger logger = Logger.getLogger(DrawWithIOC.class.getName()); private Shape shape; public DrawWithIOC(Shape shape) { this.shape = shape; } public void draw() { this.shape.draw(); } public static void main(String[] args) { Shape shape = new Rectangle(); DrawWithIOC drawWithIOC = new DrawWithIOC(shape); drawWithIOC.draw(); } }
A Spring IOC container is a framework which, basically, manages the life cycle of plain old Java objects (POJOs), and injects them into the application as required. Java objects define their dependencies using one of the following methods:
- Dependencies are passed as arguments to the
constructor
method of the object. See how the object is passed as an argument to theconstructor
method in the example cited in the previous section. - Dependencies are passed as arguments to the
setter
method of the object. - Dependencies are passed as arguments to a
factory
method of the object.
A Spring IOC container injects the dependencies after it creates the beans. Note the fact that dependencies are no longer managed by Java objects. They are rather managed and injected by the framework, and hence, Inversion of Control.
The following are the packages which are core to the IOC container:
org.springframework.beans
org.springframework.context
It is the interface, org.springframework.context.ApplicationContext
(sub-interface of the interface, BeanFactory
), which represents the Spring IOC container and is responsible for managing the life cycle of beans. The instructions related to creating, configuring, and assembling the objects is termed Configuration Metadata. The configuration metadata is often represented using Java annotations or XML. A Java application using Spring IOC Container is created by combining Business POJOs with the previously mentioned configuration metadata, and passing it on to the IOC Container (an instance of ApplicationContext
). The same is represented using the following diagram:
Figure 1: Java Application with Spring IOC Container
Let's illustrate the preceding diagram with an example. The following code represents how a service, namely, UserService
is instantiated using Spring IOC container in a Spring Boot web application. Notice how the annotation-based autowiring feature has been used to have ApplicationContext
autowired to the userService
field in this code:
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.services.UserService;
import com.services.domain.User;
@Controller
@SpringBootApplication (scanBasePackages={"com.services"})
public class DemoApplication {
@Autowired
private UserService userService;
@RequestMapping("/")
@ResponseBody
String home() {
User user = null;
return "Hello " + userService.getUsername(user) + ". How are you?";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
In this section, we will learn the key elements of the Spring Web model-view-controller (Spring Web MVC) framework, and how to quickly get started with a Spring web application using the Spring Web MVC components. The following will be covered in detail in this section:
- Key building blocks of a Spring Web MVC application
- Introduction to the Dispatcher servlet
In this section, we will learn about the key building blocks of a Spring web MVC application. The following diagram represents the key building blocks:
Figure 2: Key building blocks of Spring web MVC framework
The following are the details in relation to the preceding diagram:
- Dispatcher servlet: Dispatcher servlet, also termed the front controller, is at the core of the Spring Web MVC framework. Simply speaking, the Dispatcher servlet determines which
controller
class and method needs to be called when a page request or an API request arrives. In addition, it sends the response using the appropriate JSP page or JSON/XML objects. It dispatches the incoming requests to the appropriate handlers (custom controllers) with different handler mappings. This is integrated with the Spring IOC container, which allows it to use all the features that the Spring framework provides. - Handler Mappings: Handler mappings are used to map the request URL with the appropriate handlers such as controllers. The Dispatcher servlet uses the handler mappings to determine the controllers which will process the incoming requests. The handler mappings are specified in the XML file, or as annotations such as
@RequestMapping
,@GetMapping
, or@PostMapping
, and so on. The following diagram represents the@RequestMapping
annotation that is used for URL mapping. - Handler Interceptors: Handler interceptors are used to invoke preprocessing and post-processing logic before and after the invocation of the actual handler method respectively.
- Controllers: These are custom controllers created by the developers and used for processing the incoming requests. The controllers are tagged with annotations such as
@Controller
or@RestController
. Controllers are used to access the application behavior through one or more service interfaces. Controllers are used to interpret the user input, pass them to the services for implementation of business logic, and transform the service output into a model which is presented to the user as a view. The following diagram shows the@Controller
annotation which represents theDemoApplication
class to play the role of a controller:
Figure 3: Representing handler mappings (URL mapping) and Controller annotation
- Services: These are the components coded by the developers. These components contain the business logic. One or more methods of services are invoked from within the
Controller
methods. Spring provides annotations such as@Service
for identifying services. The following code represents the service classUserService
, which consists of a method,getUsername
. Pay attention to the@Service
annotation. Note how the instance ofUserService
is defined in@Controller
, as shown in the preceding code, and the methodgetUsername
is invoked on the instance ofUserService
.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.services.dao.UserDAO;
import com.services.domain.User;
@Service ("userService")
public class UserService {
@Autowired
private UserDAO userDAO;
public String getUsername(User user) {
return userDAO.getUsername(user);
}
}
- Data Access Objects (DAO): The classes which represent DOA, are used to do data processing with the underlying data sources. These classes are annotated with annotations such as
@Repository
. The preceding code ofUserService
consists, a DAO, namely,UserDAO
. Note the@Repository
annotation used by the class,UserDAO
, in the following code:
import org.springframework.stereotype.Repository;
import com.services.domain.User;
@Repository ("userDAO")
public class UserDAO {
public String getUsername(User user) {
return "Albert Einstein";
}
}
- View Resolvers: View resolvers are components which map view names to views. They help in rendering models in the browser based on different view technologies such as JSP, FreeMarker, JasperResports, Tiles, Velocity, XML, and so on. Spring comes with different view resolvers such as
InternalResourceViewResolver
,ResourceBundleViewResolver
,XMLViewResolver
, and others. View resolvers are used by the Dispatcher servlet to invoke the appropriate view components. - Views: Views are used to render the response data on the UI. They can be represented using different technologies such as JSP, Velocity, Tiles, and so on.
In this section, we will learn about the core component of a Spring web MVC application, the Dispatcher servlet.
As introduced in the previous section, Dispatcher servlet is the front controller which processes the user requests by invoking the appropriate components such as handler mappings, interceptors, adapters, custom controllers, services, DOA, view resolver, and finally, the views. The following diagram represents the web request/response workflow of the Dispatcher servlet:
Figure 4: Web request/response flow across the Dispatcher servlet
As per the preceding diagram, the journey of a request/response through a Spring web MVC application looks like the following:
- The user requests arrive at the server, and are intercepted by the Dispatcher servlet.
- The Dispatcher servlet gets a handler object (primarily, an instance of
HandlerExecutionChain
) from theHandlerMapping
object based on the URL mapping. URL mappings can be defined with theweb.xml
file or as annotations on the Controllers' methods.
- One or more instances of the
HandlerInterceptor
objects are retrieved from the handler object, and preprocessing logic is processed on the request object. - The instance of
HandlerAdapter
is retrieved from the handler object, and thehandle
method is invoked. This results in execution of logic within thecontroller
class. In the preceding diagram (Figure 3), the request withRequestMapping
as "/
" leads to the execution of code within thehome
method as part of this step. - The post-processing logic on the
HandlerInterceptor
instances is executed. This is the final step before therendering
method is invoked. - The
ViewResolver
instance is used to retrieve the appropriate view component. - The
render
method is invoked on the instance of view.
In this section, we will learn how to quickly build a web application using Spring Boot. The following will be covered in this section:
- The Spring Tool Suite (STS) setup in Eclipse IDE
- Introduction to Spring Boot
- Building the Hello World web app using Spring Boot
Spring Tool Suite (STS) provides the development environment for Spring-powered enterprise applications. This can be easily downloaded from the Eclipse marketplace in the following manner:
- Within the Eclipse IDE, click on
Help
|Eclipse Marketplace...
and search for Spring STS by submitting Spring STS in theFind
text field. The search result would show up different versions of STS for different Eclipse versions. - Choose the appropriate version and install. The most current version is
3.9.0.RELEASE
. - Restart Eclipse, and you are all set to create your first Spring Boot web application.
Spring Boot is a quick and easy way to get up and running with production-grade standalone applications in no time. If you hated all the XML configurations required to be set for creating a Spring web application, Spring Boot helps us to get away with all those troubles, and lets us focus on developing the application from the word go. The following are some of the key attributes of a Spring Boot application:
- Requires no XML configuration or code generation.
- Automatically configures Spring wherever appropriate and possible.
- Supports embedded web servers such as Tomcat, Jett, and so on. One of the key disadvantages while working with the Spring web framework prior to Spring Boot was deploying these apps explicitly on the web server either manually, or using some tools/scripts. This is no more required with Spring Boot, as it comes with support for embedded web servers.
- Helps to quickly and easily get started with microservices development. Spring Boot has seen great adoption in recent times thanks to the advent of micro-services architecture style apps. Spring Boot supports creation of micro-services in the form of a JAR file, which could easily be deployed within a server container.
- Supports features such as health checks, metrics, and so on.
- Provides useful annotations such as
@ConfigurationProperties
to accomplish tasks such as loading properties' details from theapplication.properties
file.
In this section, we will go through the steps required to build a Hello World Web App using Spring Boot. The following given steps assume that you have set up Spring STS within your Eclipse IDE by following the steps given earlier. Now, with the steps given next, one can set up the Hello World web app in an easy manner:
- Press Ctrl + N to open up the
Project creation Wizard
dialog box. - Write
Spring
in theWizards
text field. This will show various project options related to Spring projects. Select the optionSpring Starter Project
, and click onNext
.
- Name the project
HelloWorld
, or leave the default namedemo
, and click onNext
:
- Select
Web
in the list of dependencies as shown in the following screenshot, and click onFinish
:
- Clicking on
Finish
will create aHelloWorld
project whose file structure will look as seen in the following screenshot. Pay attention to the annotation@SpringBootApplication
in theHelloWorldApplication
class shown in the screenshot. Spring applications are commonly found to be annotated with annotations such as@Configuration
,@EnableAutoConfiguration
, and@ComponentScan
. Spring Boot came up with@SpringBootApplication
as an alternative to using three annotations together:
- Right-click on the project, and start
Spring Boot App,
as shown in the following screenshot. It will start the app on the embedded Tomcat server. Access the URLhttp://localhost:8080
on your browser. It will show up a page with the heading asWhitelabel Error Page
followed by some content.
- At times, the project may not run successfully due to one or more errors encountered during setting up the project. One can open the Problems View, and find out if there are any issues. As creating and running a Maven project requires the dependencies to be downloaded from the internet, it is to be ensured that the internet connection is active while setting up and running the project.
- It is time to write some custom code in the
HelloWorldApplication
Java file, and run the app. Place the following code (in bold) as shown and run the app. Access the URLhttp://localhost:8080
, and you will find the following getting printed:Hello World. How are you?
. Pay attention to the usage of the@Controller
annotation, which is required to annotate a class as a controller. In case one wants the class to serve REST APIs, one can use either@Controller
and@ResponseBody
together appropriately at the class and method level, or use@RestController
at the class level:
@Controller @SpringBootApplication public class HelloWorldApplication { @RequestMapping("/") String home() { return "Hello world. How are you?"; } public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }
- When wanting to use the JSP files for displaying views, the following steps needed to be taken:
- Include the following code in the
pom.xml
file. This is done to enable JSP support:
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency>
- The following needs to be put in
src/main/resources/application.properties
in order to define the templateprefix
andsuffix
for the JSP files:
spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp
- Create the JSP files in the folder
src/main/resources/META-INF/resources/WEB-INF/jsp/
. For the sake of this current example, let's call the file asindex.jsp
. - The code for
@Controller
would look like the following to render theindex.jsp
file. Note the absence of the@ResponseBody
annotation on thehome()
method. This is why the JSP file,index.jsp
, is rendered. Otherwise, a string object would have been returned as a response leading to errors while rendering.
@Controller @SpringBootApplication public class HelloWorldApplication { @RequestMapping("/") String home() { return "index"; } public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }
Before we go into the details of understanding the different aspects of implementing the controllers, let's quickly go over how controllers take part in the MVC workflow.
The following is the workflow representing the processing of a web request and the response by a controller:
- The requests are first intercepted by the Dispatcher servlet.
- The Dispatcher servlet does the initial stage of request processing by resolving locale/themes, performing activities such as taking snapshots of the request attributes, and making framework objects available to handlers and view objects.
- Once the initial processing is done as mentioned in the preceding step, an appropriate handler is determined and invoked for further processing the request. It is the
handleRequest
method that is invoked on the handler instance, which, in turn, invokes the appropriate method on the appropriate controller determined usingHandlerMappings
. - The controller then processes the request, and returns an instance of
ModelAndView
appropriately. - The instance of
ModelAndView
is further processed by the Dispatcher servlet to send out the response to the user.
Let us start building the sample healthcare app for doctors and patients. This app will be used to illustrate the controllers' implementations with multiple examples. In this section, we will see how to implement the controllers for handling users' requests for accessing the home page, signup form, login form, and so on. In the previous section, we have seen how to build a Hello World web app using Spring Boot. We will start implementing controllers using the Spring Boot app.
The following are the different controllers which are used to process requests for accessing the home page, signup form, and login page:
UserAccountController
RxController
DoctorSearchController
We will see the implementation of the preceding controllers in code examples listed as follows. In the most trivial form, controller classes can be implemented in the following manner:
- The
controller
class can be annotated with@Controller
or@RestController
annotations at the class level. The code samples given below represent the usage of both types of annotations.@RestController
is used as a convenience annotation to represent the annotations such as@Controller
and@ResponseBody
. When used at the class level, the controller can serve REST API requests. - When using the
@Controller
or@RestController
annotation, request mappings can be provided using one of the following techniques:- Using RequestMapping Annotation at Method Level: The following code represents the
RequestMapping
annotation at the method level,home()
. Spring framework 4.3 introduced annotations such as@GetMapping
,@PostMapping
,@PutMapping
, and so on to simplify mappings for common HTTP method types such asGET
,POST
,PUT
, and so on respectively. These new annotations enhance code readability. In the following code, the aforementioned annotations have been used interchangeably for providing greater clarity and ease of understanding. When the application is started, accessinghttp://localhost:8080/
would lead to a display of the view,index.jsp
file. Note thecontroller
classHealthApplication
.
- Using RequestMapping Annotation at Method Level: The following code represents the
@Controller @SpringBootApplication public class HealthApplication { @RequestMapping("/") String home() { return "index"; } public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }
In the code given next, the
@GetMapping
annotation is used in place of@RequestMapping
, as shown in the preceding code sample.GetMapping
is a composed annotation which acts as a shortcut for@RequestMapping
(method = RequestMethod.GET
). When the application is started, accessinghttp://localhost:8080/
will printHello world. This is a health application!
.
@RestController @SpringBootApplication public class HealthApplication { @GetMapping("/") String home() { return "Hello world. This is a health application!"; } public static void main(String[] args) { SpringApplication.run(HelloWorldApplication.class, args); } }
- Using RequestMapping Annotation at both, Method & Class Level: The following code represents
RequestMapping
at both the class and the method level. When the URL such ashttp://localhost:8080/account/
is accessed, one will be taken to the login page. Note that a URL such ashttp://localhost:8080/account
(without trailing "/") would result in an error. The point to note is that thelogin
method does not have URL mapping with theRequestMapping
annotation. This, primarily, acts as a catch--all method to handle all the requests with different paths represented by using "/*
" (defined at the class level) except/account/
. When a URL such ashttp://localhost:8080/account/signup
is accessed, the signup page is displayed. Similar is the case with the URLhttp://localhost:8080/account/forgotpassword
which would open up the forgot password page.
@Controller @RequestMapping("/account/*") public class UserAccountController { @RequestMapping public String login() { return "login"; } @GetMapping("/signup") public String signup() { return "signup"; } @GetMapping("/forgotpassword") public String forgotpassword() { return "forgotpassword"; } }
- Using RequestMapping Annotations with Http Requests Type: In the following example, the HTTP request type
Get
is mapped to the methodlogin
:
@Controller @RequestMapping("/account/login") public class LoginController { // // @GetMapping can as well be used // @RequestMapping(method = RequestMethod.GET) public String login() { return "login"; } }
In the next section, we will learn the concepts and examples of handling request parameters in relation to handling forms such as signup, login, and so on as discussed in this section.
There are different ways in which user request parameters can be handled. We shall be taking the example of the signup form to understand the following concepts related to the most common ways of handling request parameters:
- Using the
RequestParam
annotation: This is used to bind the method parameters to web request parameters - Using the
RequestBody
annotation: This is used to bind the method parameter to the body of the web request - Using the
PathVariable
annotation: This is used to bind the method parameter to the URI template variable
In this section, we will learn how to use the RequestParam
annotation for reading web request parameters in the controller
class. The following image shows what the signup form looks like with three input fields such as Nick Name
, Email address,
and Password
:
Figure: New User Signup form
On submission of the preceding form, the user request parameters will be handled using the @RequestParam
annotation. The RequestParam
annotation is used to bind a method parameter to a web request parameter. The following code displays the binding of method parameters such as nickname
, emailAddress
, and password
with web request parameters such as nickname
, emailaddress
, and password
respectively. In simple words, the frontend has to send parameters with keys as nickname, email address, and password for the code given next to work.
@Controller @RequestMapping("/account/*") public class UserAccountController { @GetMapping("/signup") public String signup() { return "signup"; } @PostMapping("/signup/process") public String processSignup(ModelMap model, @RequestParam("nickname") String nickname, @RequestParam("emailaddress") String emailAddress, @RequestParam("password") String password) { model.addAttribute("login", true); model.addAttribute("nickname", nickname); return "index"; } }
In this section, we will learn when and how to use the RequestBody
annotation (@RequestBody
) for handling web requests.
The RequestBody
annotation is used to bind the method parameter with the body of the incoming request. In the process of binding, HttpMessageConverters
converts the body of the request appropriately (most commonly into the parameter object) based on the content type of the request.
The RequestBody
annotation is most commonly used in scenarios dealing with REST APIs.
The following example demonstrates the usage of the @RequestBody
annotation using a domain object, User
, which is made a parameter to the controller
method:
@RestController public class RestDemoController { @PostMapping("/hello") public HelloMessage getHelloMessage(@RequestBodyUser user) { HelloMessage helloMessage = new HelloMessage(); String name = user.getName(); helloMessage.setMessage( "Hello " + name + "! How are you doing?"); helloMessage.setName(name); return helloMessage; } }
In the preceding example, note some of the following:
- The
@PostMapping
annotation maps the REST API endpoint,/hello
, with the handler method,getHelloMessage
. Recall that@PostMapping
is a composed annotation which acts as a shortcut for@RequestMapping (method = RequestMethod.POST)
. - The
@RequestBody
annotation is used with theUser
object. This binds (or maps) the method parameter, user of typeUser
, with the body of the web request. The body of the request arrives in the following JSON format:
{"name": "Calvin Hobbes"}
The HttpMessageConverter
method converts the preceding into the User
object, whose code looks like the following:
public class User { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
- The
@RestController
annotation, a convenient annotation, which is itself annotated with@Controller
and@ResponseBody
annotations, and is used for programming REST API integration endpoints. - The
HelloMessage
class is returned as a response. The following is the code forHelloMessage
:
public class HelloMessage { private String message; private String name; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
The HttpMessageConverter
method converts the preceding response object into the following response message:
{"message": "message text goes here...", "name": "name goes here..."}
In this section, we will learn how to use the PathVariable
annotation for handling request parameters.
The PathVariable
annotation is used to bind a method parameter to a URI template variable. A URI template is a URI-like string containing one or more variables. For example, in the following code, /{nickname}
is a URI template consisting of nickname
as a variable. A method can have multiple @Pathvariable
annotations to bind multiple variables present in the URI template. PathVariable
is seen to be mainly used in the REST web service. Later, while going through Angular chapters, you will see that PathVariable
is very similar to handling routing parameters using the ActivatedRoute
concept:
@Controller @SpringBootApplication public class HealthAppApplication { @RequestMapping("/") public String home() { return "index"; } @GetMapping("/{nickname}") public String home(ModelMap model, @PathVariable String nickname) { model.addAttribute("name", nickname); return "index"; } public static void main(String[] args) { SpringApplication.run(HealthAppApplication.class, args); } }
Both RequestParam
and PathVariable
can be used to read the request parameters. @RequestParam
is used to retrieve the query parameters from the request URL. On the other hand, @PathVariable
is used to retrieve one or more placeholders from the URI. URLs without query parameters, for example, paths, tend to be cached. The following code demonstrates the usage of both RequestParam
and PathVariable
. Different URLs will be handled using different methods represented in the code as follows:
- URL (
http://localhost:8080, localhost:8080/?name=calvin
): This URL consists of a parameter such as name. The value of this parameter is obtained usingRequestParam
. Note that asrequired = false
is declared with the@RequestParam
definition in the code example given next, thus, a request URL such ashttp://localhost:8080/
would also get mapped to theusingRequestParam
method. - URI (
http://localhost:8080/calvin
): This URL consists of a name which can be handled using a placeholder variable whose value can be obtained using thePathVariable
method.
The following code displays the usage of both RequestParam
and PathVariable
:
@Controller @SpringBootApplication public class HealthAppApplication { // // RequestParam is used to retrieve value of parameter, name. // @RequestMapping("/") public String usingRequestParam(Model model, @RequestParam(value="name", required=false) String nickname) { model.addAttribute("nickname", nickname); return "index"; } @RequestMapping("/{nickname}") public String usingPathVariable(Model model, @PathVariable String nickname) { model.addAttribute("nickname", nickname); return "index"; } }
In the next section, you will learn how to use interceptors to handle web requests-response, before and after the requests are handled by the controller respectively.
In this section, we will learn about some of the following:
- Why use interceptors?
- How to implement interceptor methods such as pre-handle and post-handle to process the web requests before and after the requests get processed by the controllers respectively.
Interceptors' implementation provides methods for intercepting incoming requests before it gets processed by the controller classes or, intercepting the outgoing response after being processed by the controller and before being fed to the client. Interceptor methods help to get away with boilerplate code which is required to be invoked on each request and response. For example, let's take the authentication scenario where every request need to be checked for an authenticated session before being processed by the code in the controller. If the session is not found to be authenticated, the request is forwarded to the login page, else the request is forwarded to the controller for further processing. Given that the controller logic could span across multiple controller classes, and the aforementioned authentication logic needs to be processed before one or more controllers processes the incoming requests, it is suitable to put the authentication processing in the interceptor method. Another example of using interceptor methods includes measuring the time spent in the method execution.
For implementing interceptors for processing web requests-response, custom interceptor classes need to be implemented. Custom interceptor classes need to implement one or all of the methods provided in the HandlerInterceptor
interface which are as follows:
preHandle
: The code within thepreHandle
method gets executed before the controller method is invokedpostHandle
: The code within thepostHandle
method is executed after the controller method is invokedafterCompletion
: The code withinafterCompletion
is executed after the view gets rendered
In the following example, we will use an interceptor to process web requests arriving from the signup form. The following steps are required to be taken:
- Create an Interceptor class: Create an
Interceptor
class extending theHandlerInterceptorAdapter
class. In the following example, only thepreHandle
method is implemented. Other methods such aspostHandle
andafterCompletion
are not provided with the implementation:
public class SignupInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String emailAddress = request.getParameter("emailaddress"); String password = request.getParameter("password"); if(StringUtils.isEmpty(emailAddress) || StringUtils.containsWhitespace(emailAddress) || StringUtils.isEmpty(password) || StringUtils.containsWhitespace(password)) { throw new Exception("Invalid Email Address or Password. Please try again."); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) throws Exception { ... } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { ... } }
- Implement the Configuration class: Implement a custom
@Configuration
class by extendingWebMvcConfigurerAdapter
, thereby adding the interceptor with appropriate path patterns within theaddInterceptors
method. If the path pattern is not specified, the interceptor is executed for all the requests.@Configuration public class AppConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SignupInterceptor()).addPathPatterns("/account/signup/process"); } }
- Make sure that the controller with the appropriate URI pattern is implemented as specified within the
addInterceptors
method in the aforementioned@Configuration
class.@Controller @RequestMapping("/account/*") public class UserAccountController { @RequestMapping("/signup") public String signup() { return "signup"; } @RequestMapping("/signup/process") public String processSignup(ModelMap model, @RequestParam("nickname") String nickname, @RequestParam("emailaddress") String emailAddress, @RequestParam("password") String password) { model.addAttribute("login", true); model.addAttribute("nickname", nickname); return "index"; } }
In the next section, you will learn about the different types of responses from controllers.
The following are some of the common types of responses returned from controllers:
- An instance of
ModelAndView
- Using
@ResponseBody
ModelAndView
is a container object to hold both Model and View. With ModelAndView
as a return object, the controller returns the both model and view as a single return value. The model is a map object which makes it possible to store key-value pairs. The following code sample represents the usage of ModelAndView
in a Controller:
@Controller @RequestMapping("/account/*") public class UserAccountController { @PostMapping("/signup/process") public ModelAndView processSignup(ModelMap model, @RequestParam("nickname") String nickname, @RequestParam("emailaddress") String emailAddress, @RequestParam("password") String password) { model.addAttribute("login", true); model.addAttribute("nickname", nickname); model.addAttribute("message", "Have a great day ahead."); return new ModelAndView("index", model); } }
The following code samples represent the different ways in which an instance of ModelAndView
is returned with different sets of information:
// Will result in display of index.jsp page return new ModelAndView("index"); // Will result in display of index.jsp page. //The JSP page could consist of code such as "Hello ${name}" //which will get displayed as "Hello Calvin Hobbes" return new ModelAndView("index", "name", "Calvin Hobbes"); // Will result in display of index.jsp page. // The JSP page could consist of code such as //"Hello ${model.firstName} ${model.lastName}" //which will get displayed as "Hello Calvin Hobbes" UserInfo userInfo = new UserInfo(); userInfo.setFirstName("Calvin"); userInfo.setLastName("Hobbes"); return new ModelAndView("index", "model", userInfo); // Will result in display of index.jsp page. // The JSP page could consist of code such as "Hello ${name}" // which will get displayed as "Hello Calvin Hobbes" Map<String, Object> map = new HashMap<String, Object>(); map.put("name", "Calvin Hobbes"); return new ModelAndView("index", map);
This section represents the concepts related to the usage of the @ResponseBody
annotation for returning a response to the client request.
The @ResponseBody
annotation can be applied both at the class level and the method level. When @ResponseBody
is applied at the class level along with the @Controller
annotation, another annotation such as @RestController
can be used instead.
The @ResonseBody
annotation represents the fact that the value returned by the method will form the body of the response. When the value returned is an object, the object is converted into an appropriate JSON or XML format by HttpMessageConverters
. The format is decided based on the value of the produce
attribute of the @RequestMapping
annotation, and also the type of content that the client accepts. Take a look at the following example:
@Controller public class RestDemoController { @RequestMapping(value="/hello", method=RequestMethod.POST, produces="application/json") @ResponseBody public HelloMessage getHelloMessage(@RequestBody User user) { HelloMessage helloMessage = new HelloMessage(); String name = user.getName(); helloMessage.setMessage( "Hello " + name + "! How are you doing?"); helloMessage.setName(name); return helloMessage; } }
In this section, you will learn how to create a RESTful web service. The concepts explained earlier in this chapter will be used.
The following are some of the key aspects of creating a RESTful web service. Let's take the example of retrieving the doctors' list based on the specialties, location, and so on.
- Create a controller representing the RESTful endpoint: In the following code, note the usage of the
@RestController
annotation, which is used to represent the annotation@Controller
and@ResponseBody
. The controller has a method,searchDoctor
, for handling the request represented using the URL such as/doctors?location=xxx&speciality=yyy
. Note the@RequestMapping
annotation and its attributes, especially, "produces", which signifies the fact that the output sent to the user will be in the JSON format.
@RestController public class DoctorSearchController { @Autowired DoctorService docService; @RequestMapping(value="/doctors", method=RequestMethod.GET, produces="application/json") public DoctorList searchDoctor( @RequestParam(value="location", required=false) String location, @RequestParam(value="speciality", required=false) String speciality) { DoctorList docList = docService.find(location, speciality); return docList; } }
The following is how the DoctorService
implementation may look like:
@Service public class DoctorServiceImpl implements DoctorService { @Autowired private DoctorDAO doctorDAO; @Override public List<Doctor> findByLocationAndSpeciality(String location, String speciality) { return doctorDAO.findByLocationAndSpeciality(location, specialityCode); } }
The following is how the DoctorDAO
implementation may look like:
@Repository @Transactional public class DoctorDAOImpl implements DoctorDAO { private SessionFactory sessionFactory; @Autowired public DoctorDAOImpl(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @Override public List<Doctor> findByLocationAndSpeciality(String location, String speciality) { Session session = this.sessionFactory.getCurrentSession(); TypedQuery<Doctor> query = session.getNamedQuery("findByLocationAndSpeciality"); query.setParameter("location", location); query.setParameter("speciality", speciality); List<Doctor> doctors = query.getResultList(); return doctors; } }
- Create a RESTful API: For retrieving the list of doctors based on location and speciality, the URL could look like
http://localhost:8080/doctors?location=xxx&speciality=yyy
. - Identify the method of processing incoming requests data:
@RequestParam
will be used to process the incoming requests data as mentioned in the preceding URL. In the previous code, note how@RequestParam
is used for processing the value of both thelocation
and thespecialty
parameter. The following code represents the same:
public DoctorList searchDoctor( @RequestParam(value="location", required=false) String location, @RequestParam(value="specialty", required=false) String speciality) { // Code goes here }
- Create a class representing ResponseBody: The return value is the
DoctorList
object, which consists of a list of Doctors. The following code represents theDoctorList
object which is a list of the Doctor object:
// The class representing the list of Doctor; Returned as a response public class DoctorList { private List<Doctor> doctors; public DoctorInfo(List<Doctor> doctors) { this.setDoctors(doctors); } public List<Doctor> getDoctors() { return doctors; } public void setDoctors(List<Doctor> doctors) { this.doctors = doctors; } }
The following represents the Doctor
class which is returned as part of the response object:
public class Doctor { private String id; private String firstName; private String lastName; private String specialityCode; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getSpecialityCode() { return specialityCode; } public void setSpecialityCode(String specialityCode) { this.specialityCode = specialityCode; } }
- Client response: The client receives the response body in the JSON format. The following is a sample response which is returned by the execution of the preceding code:
[{ "id": "doc1", "firstName": "Calvin", "lastName": "Hobbes", "specialityCode": "pediatrics" }, { "id": "doc2", "firstName": "Susan", "lastName": "Storm", "specialityCode": "cardiology" }]
In this section, you will learn how to Dockerize a Spring Boot application. The detailed introduction to Docker and the related aspects such as Docker images, containers, Dockerfile, and so on is provided in Chapter 2, Preparing Spring Web Development Environment. In case, you are new to Docker, it may be a good idea to get an understanding of Docker before going ahead with this section.
The following are some of the reasons why it may be a good idea to Dockerize a Spring Boot application:
- Containerized Springboot App-- a Microservice: It aligns well with the cloud-native architecture style, which recommends containerizing a microservice (using Docker or other runtimes such as rkt), and managing these containers using container orchestration tools such as Kubernetes, Docker Swarm, and so on. It should be noted that Spring Boot can be used to create a microservice. Thus, one may need to Dockerize a Spring Boot microservice when creating a cloud-native app.
- Quick Dev/QA environments: A containerized/Dockerized Spring Boot app is easy to commission or decommission.
- Continuous delivery made easy: It is easy to achieve a continuous delivery of containerized/Dockerized Spring Boot app in different environments such as QA, UAT, production.
It should be noted that Docker can be used while working with both Maven and Gradle, details of which will be presented in the next chapter while understanding the aspects of build. The following are some of the key aspects of Dockerizing a Spring Boot app. The same instructions can be used to wrap Spring boot micro-services within Docker containers.
- Dockerfile: The first step is to create the Dockerfile which will be used to build the Docker image. The following is the content of the Dockerfile. Save the Dockerfile as Dockerfile in the root folder.
FROM frolvlad/alpine-oraclejdk8:slim VOLUME /tmp ADD target/demo-0.0.1-SNAPSHOT.jar app.jar RUN sh -c 'touch /app.jar' ENV JAVA_OPTS="" ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS - Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
- Build and test the Spring Boot app:
- Go to the Spring Boot app root folder. Make sure that you saved Dockerfile in the root folder. Execute the following command to build the Docker image:
docker built -t springboot-app:latest . // In case you are executing the above command from another folder docker built -t springboot-app:latest -f path_to_dockerfile.
- Execute the following command to start and access the container:
// Start the container; Name of the container is sbapp docker run -tid -p 8080:8080 --name sbapp springboot-app:latest // Access the container docker exec -ti sbapp bash // Access the logs docker logs sbapp
Once started, open the REST client, and test the preceding RESTful API with URL as http://localhost:8080/doctors?location=xxx&speciality=yyy
.
In this chapter, you learnt the fundamentals of the Spring web framework with a focus on understanding the key concepts such as IOC container, Dispatcher servlet, implementing controllers, web requests and response handling, using custom interceptors, and so on. You also learnt how to create a web application using Spring Boot, how to create a RESTful web service, and finally, how to Dockerize a Spring Boot application.
In the next chapter, you will learn how to prepare a Spring web development environment.