Making JSP work with Spring Boot 2

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,

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 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.

...
<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:

  1. 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.
  2. For the GET method showLoginView(), create a parameter of type LoginForm and annotate it with @ModelAttribute("loginForm")
  3. 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
  4. Create a parameter of type 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().

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 attribute loginForm, Spring Boot will throw a BindException that will be resolved via DefaultHandlerExceptionResolver 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

References

Related Articles

comments powered by Disqus