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.
Creating a Spring Boot Project
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.
Project Configurations
Dependencies
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.
Auto-configurations
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);
}
}
View Resolvers
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/
”.
IMPORTANTMany 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.
Embedded Servlet Container
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.
Spring Boot + JSP Login Page
Login View
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">
<head>
<link href="webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
<title>Login</title>
</head>
<body>
<div class="jumbotron">
<h1>Spring Boot 2 + JSP Demo</h1>
</div>
<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>
<div class="form-group">
<label for="password">Password</label>
<form:password path="password" cssClass="form-control"/>
<form:errors path="password" cssStyle="color: red"/>
</div>
<form:button class="btn btn-primary">Log in</form:button>
</form:form>
</div>
<!--- 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.
</div>
<% }
if ("FAILED".equals(status)) { %>
<div class="alert alert-danger" role="alert">
Login failed. Please try again!!!
</div>
<% } %>
</div>
</div>
<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 namelyloginForm
<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.
...
<link href="webjars/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet">
...
<script src="webjars/bootstrap/4.1.3/js/bootstrap.min.js"></script>
Login Controller
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.
Handling GET Requests
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
”).
Handling POST Requests
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.
Handling Log-Out
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;
}
Data Binding
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:
- Declare a new method that returns a
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. - For the GET method
showLoginView()
, create a parameter of typeLoginForm
and annotate it with@ModelAttribute("loginForm")
- Do the same as (2) but omit
@ModelAttribute
for the method parameter because according to Spring Web documentation, it will be resolved as@ModelAttribute
implicitly - Create a parameter of type
Model
for the GET methodshowLoginView()
and then invokeModel.addAttribute("loginForm", new LoginForm())
to add a model attributeloginForm
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()
.
Data Validation
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 attributeloginForm
, Spring Boot will throw aBindException
that will be resolved viaDefaultHandlerExceptionResolver
as an error HTTP 404 - Bad Request.
Building and Executing
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
}
Updates
- The original Git repository with Spring Boot v2 is here: https://github.com/htr3n/spring-boot-jsp-demo
- 2024-03-23: I created a demo with Spring Boot v3 and JSP/JSTL at https://github.com/htr3n/springboot3-web-war. There are few changes, especially with
jakarta.servlet
instead ofjavax.servlet
and upgrading WebJars Bootstrap v5.
References
- https://spring.io/guides/gs/serving-web-content/
- https://stackoverflow.com/questions/46391915/how-to-render-jsp-using-spring-boot-application/46399638#46399638
- https://stackoverflow.com/questions/45533908/unable-to-render-the-jsp-using-spring-boot/45543355#45543355
- https://stackoverflow.com/questions/24661289/spring-boot-not-serving-static-content