Building Web Applications with Spring MVC, JSP, and JSTL Tags
Spring MVC remains a dependable choice for Java teams that prefer server-rendered pages over heavy client frameworks. In Australia, where banks like CBA and NAB still maintain extensive Java portfolios and consultancies in Melbourne port older Struts applications to Spring MVC, JSTL-driven JSPs continue to ship in production. Keeping the view layer unchanged during such migrations lowers the skill ramp and preserves years of accumulated template logic across local teams.
This walkthrough covers Maven setup, controller wiring, view resolution, and building reusable JSP templates with the JSTL tag library. Along the way, the article highlights common pitfalls and packaging choices. If you need help scaling the application or adding custom integrations, Rewans services offers practical advice drawn from real consulting engagements.
Project setup and Maven dependencies
Before writing a single controller, lay down a clean Maven structure. A typical web module under Spring MVC 5 or 6 requires spring-webmvc, jakarta.servlet-api (or javax.servlet-api for legacy setups), and jstl-api plus jstl-impl for tag library support. Adding the maven-war-plugin ensures the resulting WAR can be dropped straight into a Tomcat 10 instance running on a developer's laptop in Brisbane or a staging server hosted in a Sydney data centre.
The directory layout follows the standard servlet convention. Java classes live under src/main/java, while JSP templates and static assets sit inside src/main/webapp. A WEB-INF folder hides configuration files and the dispatcher servlet mapping from public access. Keeping CSS and JavaScript under WEB-INF/resources prevents direct browsing and lets the Spring Security filter chain govern every request that reaches the application.
Include the JSTL coordinates early so the IDE recognises the tag namespaces when templates are written. Adopting a parent pom for dependency versions keeps multiple modules aligned and avoids the classic drift problem when one microservice pulls in Servlet 5 while another uses Servlet 4.
Configuring DispatcherServlet and view resolvers
The DispatcherServlet is the front controller for every request, so its mapping in web.xml or through Spring Boot auto-configuration sets the tone for the whole application. Registering it against the URL pattern /* or a more selective /app/* ensures static assets still resolve through the default servlet. In the accompanying configuration, declare an InternalResourceViewResolver that prefixes view names with /WEB-INF/views/ and suffixes them with .jsp.
When JSTL tags are used inside JSPs, the view resolver works hand in hand with the container's expression language implementation. Tomcat 9 still requires an explicit el-api dependency, while Tomcat 10 ships EL with the container. Verifying which EL library is on the classpath saves hours of debugging when ${user.name} renders as plain text instead of an evaluated value.
Enabling annotation-driven MVC via @EnableWebMvc activates the default HandlerMapping, HandlerAdapter, and message converters. Registering a custom Converter or Formatter bean lets the binder parse Australian inputs such as DD/MM/YYYY dates and AUD currency correctly before the value reaches the controller method.
Writing controllers and mapping requests
Controllers in Spring MVC are plain Java classes annotated with @Controller. Each handler method carries an @RequestMapping, @GetMapping, or @PostMapping annotation that ties a URL pattern to the method and its HTTP verb. Returning a String view name, a ModelAndView, or a POJO that the framework serialises keeps the controller layer thin and readable.
Model attributes flow into the view through the ModelMap. A controller method that prepares a list of products for an e-commerce page can add them via model.addAttribute("products", productService.findAll()). The JSP template then iterates over the list using JSTL's core tags. Throwing a custom exception or returning a ResponseEntity lets the same controller handle both browser and API consumers, which is handy when the storefront shares an inventory endpoint with a mobile app.
Data binding converts request parameters to Java objects when forms are submitted. Registering an InitBinder method with a WebDataBinder argument lets you trim inputs, whitelist fields, and attach editors for java.time.LocalDate or BigDecimal. Configuring NumberFormat explicitly avoids floating-point drift in checkout flows that involve GST calculations.
Building JSP views with JSTL tag library
JSTL divides its functionality into several namespaces, each addressing a different concern. The core library, declared with the @taglib prefix c, offers iteration with c:forEach, branching with c:choose and c:when, and URL rewriting with c:url. The formatting library, prefix fmt, handles locale-aware date and number rendering, which is essential for any Australian-facing storefront that needs to show AEST timestamps or AUD pricing correctly.
A typical product listing page combines these tags into a clean template. The page imports the core and fmt libraries, formats each price using fmt:formatNumber with the currency code AUD, and iterates over the products list with c:forEach var="product" items="${products}". For empty lists, the c:if tag with a test against fn:length renders a polite "No products available" message instead of a blank table. Function tags such as fn:contains and fn:toUpperCase help sanitise user input before it is echoed back to the page.
Internationalisation works seamlessly with fmt:setBundle and fmt:message. Bundles stored under WEB-INF/i18n can ship English and Chinese variants for a Brisbane tourism portal, and the framework tag chooses the active language without any controller intervention. Reusable fragments belong in a tags folder, included via the JSP include action or the more modern Spring tag, which keeps shared header and footer markup consistent across hundreds of pages.
Form handling, validation, and deployment
Spring MVC's form tags bind input fields to model attributes, reducing boilerplate when rendering and re-populating forms after validation errors. The spring:form tag combined with spring:input, spring:select, and spring:errors creates accessible, server-rendered forms that survive JavaScript failures. Bean Validation through @NotBlank, @Size, and @Pattern on the form-backing object lets the framework reject bad input before the controller method runs.
Packaging the finished application as a WAR file prepares it for deployment to Tomcat, Jetty, or WildFly. Running mvn package produces an artefact that can be copied to a server in Ultimo or uploaded through the manager app. Configuring the maven-war-plugin to skip web.xml generation is sometimes necessary for older servlet containers. Once deployed, the application runs under the servlet container's class loader and serves JSPs through the InternalResourceViewResolver defined earlier.
Monitoring production requires more than just logs. Hooking the application into a centralised logging stack gives operations teams visibility from Sydney to regional offices. Combined with health endpoints exposed through Spring Boot Actuator, the application stays observable and easy to troubleshoot during peak retail periods.