One of my past projects relied heavily on XML based configurations and JSP technologies dated back to Spring Framework 3.x. It is still working fine after upgrading to Spring 5 and corresponding dependent libraries such as Hibernate 5, HikariCP 3 and Apache CXF 3.3.
Exploring further into Spring ecosystem, I decided to give a shot on migrating to Spring Boot 2 whilst keeping most of the existing MVC (e.g. controllers, validations, JSP views) working. Most of the significant information are scattered throughout the Internet and most of the useful tutorials are sort of “ just-follow-these-steps-and-it-will-guarantee-to-work-but-nothing-will-be-explained ”.
It took me much unexpected effort and trial-and-error to get things right not to mention numerous do’s and don’ts got undocumented or lost among a bunch of documentations. This post merely focuses some on major aspects on bringing Spring Boot 2 and Spring Web with JSP together. The versions that I consider here are Spring Boot 2.1 and respectively Spring 5.1 .
We can quickly create a new Spring Boot project using Spring Initializr web site or via command line or IDE (e.g. Eclipse, IntelliJ IDEA). For less verbose build configuration, I will opt for Gradle build instead of Maven.
For instance, with the “ New Project ” wizard with Spring Initialzr in Intellij IDEA, I picked “ War ” for “ Packaging ”. The other non-default option is “ Gradle Config ”, as mentioned above.
Spring Boot 2 comes with
a number of starters
(conventionally named as
spring-boot-starter-*
) to help us integrating appropriate dependencies in our Spring projects. I pick the module “
spring-boot-starter-web
” that adds an embedded
Tomcat
servlet container and Spring MVC.
Note that the embedded Tomcat package does not include JSP by default, we must add the module “
org.apache.tomcat.embed:tomcat-embed-jasper
” as well. In case you need JavaServer Pages Standard Tag Library (JSTL), just add “
javax.servlet:jstl
”.
Apart from those, I also consider to use WebJars as it provides client-side web libraries in terms of JAR files. Thus, It’s truly convenient for Java based web projects to bundle popular front-end libraries such as jQuery, Bootstrap, Font-Awesome, to name but a few. I will illustrate the use of WebJars Bootstrap 4 module to our demo project.
Eventually, our main build configuration
build.gradle
should look like this.
buildscript {
ext {
springBootVersion = '2.1.1.RELEASE'
repositories {
mavenCentral()
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'war'
group = 'io.github.htr3n'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
mavenCentral()
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.webjars:bootstrap:4.1.3'
providedRuntime 'javax.servlet:jstl:1.2'
providedRuntime 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.14'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
You might notice that some Spring Boot modules such as
spring-boot-starter-*
do not have their version numbers. The plugin
io.spring.dependency-management
will automatically
imports the
spring-boot-dependencies
bom
with adequate versions for Spring Boot’s dependencies.
The aforementioned wizard created a
DemoApplication
class annotated with
@SpringBootApplication
to start our Spring Boot application. The
@SpringBootApplication
annotation includes three other annotations that perform several configuration tasks automatically, which are,
@SpringBootConfiguration
embraces
@Configuration
(i.e. the annotated application class also provides Spring configurations)
@EnableAutoConfiguration
will trigger Spring Boot
auto-configuration mechanism
@ComponentScan
will enable the scanning for Spring components annotated with
@Component
or its sub-types on the package of the main application.
It’s convenient but not mandatory to use
@SpringBootApplication
. For advanced configuring or fine-tuning, one can surely pick apart individual annotations and add/customise them to particular needs.
In order to create
a deployable WAR
file, Spring Boot team
recommends to subclass
SpringBootServletInitializer
and override its
configure()
method
.
package io.github.htr3n;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(DemoApplication.class);
In Spring Web MVC, the return value of a
@Controller
, if not a valid
View
object, should be resolved by a
ViewResolver
. Spring Boot 2, by default, will load
InternalResourceViewResolver
that combines with
InternalResourceView
for handling JSP based views. As such, a controller’s return values will be mapped to a JSP resource.
InternalResourceViewResolver
, which is based on
UrlBasedViewResolver
, will use the following rule “
prefix + view_name + suffix
” for view resolution. For instance, if a view’s logical name is “
index
”, prefix = “
/WEB-INF/jsp/
” and suffix = “
.jsp
” then the resulting view will be “
/WEB-INF/jsp/index.jsp
”.
Therefore, we need to configure the
prefix
and
suffix
. This can be done quite easily in Spring Boot , either using
application.properties
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
or an equivalent but longer Java based configuration
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver jspViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
Note that Spring Boot will map a logical view onto a JSP file inside the folder “
src/main/webapp/WEB-INF/jsp/
”.
IMPORTANT
Many developers, especially who have a lot of experience with Spring Web MVC, tend to annotate the
@Configuration
class with
@EnableWebMvc
. It’s crucial to notice that
@EnableWebMvc
will
switch off all default Spring Boot auto-configuration
for Spring Web MVC. That means, JSP files and other resources might not be served correctly without extra configurations.
By default, Spring Boot package
spring-boot-starter-web
includes Tomcat server via
spring-boot-starter-tomcat
which is rather sufficient. As we adopt Spring Boot’s convention for the embedded Tomcat, it will be later accessible at the default address “
http://localhost:8080
”. In case you want to fine tune Tomcat, please reference to the
full documentation
.
To illustrate how Spring Boot works with JSP technologies, I will use a simple login page that asks for an email and password, conducts some trivial validations, and then informs whether it’s successful or failed.
First, let’s create a simple JSP file “
webapp/WEB-INF/jsp/login.jsp
” as the login view.
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html lang="en">
<link href="webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
<title>Login</title>
</head>
<div class="jumbotron">
<h1>Spring Boot 2 + JSP Demo</h1>
<div class="container">
<div class="row">
<form:form method="POST" modelAttribute="loginForm">
<div class="form-group">
<label for="email">E-Mail</label>
<form:input path="email" cssClass="form-control"/>
<form:errors path="email" cssStyle="color: red"/>
<div class="form-group">
<label for="password">Password</label>
<form:password path="password" cssClass="form-control"/>
<form:errors path="password" cssStyle="color: red"/>
<form:button class="btn btn-primary">Log in</form:button>
</form:form>
<!--- check login status and display message -->
<div class="row">
<% Object status = session.getAttribute("loginStatus");
if ("OK".equals(status)) { %>
<div class="alert alert-success" role="alert">
Congratulations! Login successfully.
if ("FAILED".equals(status)) { %>
<div class="alert alert-danger" role="alert">
Login failed. Please try again!!!
<% } %>
<script src="webjars/bootstrap/4.1.3/js/bootstrap.min.js"></script>
</body>
</html>
Nothing is spectacular here. I will use some tags from Spring form library to conveniently define and bind HTML form elements with the back-end controllers and model. The major elements are
<form:form method="POST" modelAttribute="loginForm">
—indicates the HTTP method POST as well as a model attribute namely
loginForm
<form:input path="email">
— the input element for email
<form:input path="password">
—the input element for password
<form:errors .../>
are placeholders for Spring’s validation errors (if any)
The later part of
login.jsp
is to check whether the login process succeeded or failed and display a corresponding message via access to a session attribute
loginStatus
. The actual value of
loginStatus
will be updated by the business logic of the controller. Other than that, the JSP view includes Bootstrap 4’s stylesheet and JavaScript as provided by WebJars and use Bootstrap 4 CSS classes for styling.
First of all, we will need a data object to transfer the login input data in the JSP view (e.g. email and password) with the controllers and models. Here comes the
LoginForm
class.
public class LoginForm {
private String email;
private String password;
public LoginForm() {}
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
No worries about the annotations
@NotBlank
,
@Email
for now as they are part of
data validation
. Apart from those,
LoginForm
is just a normal bean with a no-arg constructor and the getters / setters.
Now, we create a
LoginHandler
class to coordinate and handle the whole login process.
@Controller
@RequestMapping("/login")
public class LoginHandler {
private static final String LOGIN_VIEW = "login";
private static final String LOGOUT_VIEW = "logout";
// controller body
The
LoginHandler
class is annotated with
@Controller
to indicate it is a web controller that will be automatically detected by Spring Boot. In the
LoginHandler
class, let’s define some handler methods.
We create a
showLoginView()
method annotated with
@GetMapping
for handling GET requests, i.e. when a client opens the corresponding URL “
http://localhost:8080/login
”.
@GetMapping("/login")
public String showLoginView(Model model) {
model.addAttribute("loginForm", new LoginForm());
return LOGIN_VIEW;
This method simply adds a new model attribute
loginForm
(explained later in
Data Binding
) and returns a logical login view name “
login
” (which will be resolved to “
login.jsp
”).
For processing POST requests (i.e. when the client enters some input data and submits the form), we define a
login()
method annotated with
@PostMapping
.
@PostMapping("/login")
public String login(@Valid @ModelAttribute("loginForm") LoginForm form,
BindingResult bindingResult,
HttpSession session) {
if (!bindingResult.hasErrors()) {
final boolean authenticated = "abc@test.com".equals(form.getEmail());
final String loginStatus = authenticated ? "OK" : "FAILED";
session.setAttribute("loginStatus", loginStatus);
return LOGIN_VIEW;
The input data of the submitted form will be explicitly mapped to the method parameter
form
via the annotation
@ModelAttribute
. The
@Valid
annotation and the parameter
bindingResult
are for
data validation
, and the
session
is for storing session data indicating a valid authentication.
The actual authentication business logic has been simplified so that an input email “
abc@test.com
” will pass. Of course, it should be revised with proper data and authentication process in real apps.
For the sake of completeness, we should also add a handler method for logging out in which we remove the session attribute
loginStatus
added in the POST method
login()
and redirect to a “
logout.jsp
” page.
@RequestMapping("/logout")
public String logout(HttpSession session) {
session.removeAttribute("loginStatus");
return LOGOUT_VIEW;
We need to connect the HTML login form to a data transfer object (also known as
command object
,
form object
,
form-backing object
) of type
LoginForm
. Otherwise, Spring MVC will raise the infamous error “
Caused by: java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name ’loginForm’ available as request attribute
”.
There are more than one way to do that:
LoginForm
object and is annotated with
@ModelAttribute("loginForm")
. Note that all
@ModelAttribute
annotated methods will be invoked before any handler methods. Thus,
LoginForm
objects can be fed with some initial data, for instance, from a database.
showLoginView()
, create a parameter of type
LoginForm
and annotate it with
@ModelAttribute("loginForm")
@ModelAttribute
for the method parameter because according to
Spring Web documentation
, it will be resolved as
@ModelAttribute
implicitly
Model
for the GET method
showLoginView()
and then invoke
Model.addAttribute("loginForm", new LoginForm())
to add a model attribute
loginForm
Even though I can just go with (3) for its simplicity, I always prefer the other three as they advocate explicit declaration. It might make the code a bit verbose but would be easy to understand and maintain later on. I exemplify (4) and implement in
showLoginView()
.
In my previous Spring 3 projects, I used to create validators that implement the
Validator
interface and utilise
ValidationUtils
helper methods to check the input data. These validator beans will be injected into Spring controllers to validate the input before handing over to the actual business logics.
Nevertheless, we can also leverage
Bean Validation 1.0
(JSR-303) and
1.1
(JSR-349) supported since Spring 4 and
Bean Validation 2.0
(JSR-380) supported since Spring 5 via
Hibernate Validator
. These modules are already included with
spring-boot-starter-web
.
First, we annotate
LoginForm
’s fields with some Bean Validation constraints and corresponding messages.
public class LoginForm {
@NotBlank(message = "E-Mail must not be empty.")
@Email(message = "Please enter a valid e-mail address.")
private String email;
@NotBlank(message = "Password must not be empty.")
private String password;
Then we need to update
LoginHandler
with respect to validation logics but mostly with the POST handler
login()
. The simplest way is to add a
@Valid
annotation to the parameter
form
. It indicates that Spring should pass
form
to a
Validator
beforehand. To get access to validation errors, we have to declare a parameter
bindingResult
of type
BindingResult
and place it immediately next to
form
. Inside
login()
, we can invoke the method
BindingResult#hasErrors()
to see whether there are some validation errors and act accordingly. In this case, the login process won’t proceed until no validation error occurs.
public String login(@Valid @ModelAttribute("loginForm") LoginForm form,
BindingResult bindingResult,
HttpSession session) {
if (!bindingResult.hasErrors()) {
Thanks to Spring tag “
<form:errors .../>
”, any validation errors will be displayed on the JSP login view. For instance, when I left both email and password empty, I got the following messages.
If the
BindingResult
parameter
is not placed immediately next to
the corresponding model attribute
loginForm
, Spring Boot will throw a
BindException
that will be resolved via
DefaultHandlerExceptionResolver
as an error HTTP 404 - Bad Request.
The
build.gradle
created previously is mainly based on the
Spring Boot Gradle plugin
for dependency and task management.
./gradlew bootRun
When visiting http://localhost:8080/login , you can see the login page.
To build a WAR package that is executable and deployable
./gradlew bootWar
The resulting WAR file is “
./build/libs/spring-boot-jsp-demo-0.0.1-SNAPSHOT.war
”. It’s interesting that you can deploy the WAR file to a standalone Tomcat server as well as directly execute it with “
java -jar
” .
java -jar ./build/libs/spring-boot-jsp-demo-0.0.1-SNAPSHOT.war
In case you want to change the WAR file name, Spring Boot Gradle plugin inherits from, and therefore, offers similar options as, the
official Gradle War plugin
. For instance, I can get rid of the version and use the build project’s name to form the WAR file name by customise the task
BootWar
in
build.gradle
.
bootWar {
archiveName "${project.name}.war"
Also note that, when applying together with Gradle war plugin, Spring Boot Gradle plugin
will disable the task
war
. Should you want to use
war
alongside
bootWar
, just simply enable it.
war {
enabled = true
jakarta.servlet
instead of
javax.servlet
and upgrading WebJars Bootstrap v5.