First complete Stage 5: form and direct response without Model. First explore Stage 6: the product table and details page. This stage applies those template concepts to the calculator.
Enter two numbers. Click Add, Subtract, Multiply, Divide or Remainder. Spring calculates the result and sends back a separate HTML page.
Extract spring-calculator.zip. Open a terminal in the spring-calculator folder containing pom.xml. Requires JDK 21+ and Maven 3.9+, with internet for the first dependency download.
Open src/main/resources/application.properties to choose a free port:
server.port=8082
If 8082 is busy, choose another available port and use it in the browser address. Save the file before starting. Restart after changing the port.
mvn spring-boot:run
Wait for CalculatorApplication to start, then open http://localhost:8082/. Keep the terminal running. Stop with Ctrl+C.
Enter 10 and 5, then click Add. The next page displays 10.0 + 5.0 = 15.0. Click Calculate again to return to the form.
| Button | Result for 10 and 5 |
|---|---|
| Add | 15.0 |
| Subtract | 5.0 |
| Multiply | 50.0 |
| Divide | 2.0 |
| Remainder | 0.0 |
Remainder is the % operator, not percentage: 20 % 6 is 2. Division by zero and remainder with zero as the divisor show a message on the result page.
Form page: enter 10 and 5, click Add
↓
Browser sends GET /calculate?a=10&b=5&operation=add
↓
Spring calls the controller with those three values
↓
Java calculates 10 + 5
↓
Controller puts the answer in Model
↓
Thymeleaf fills result.html with that answer
↓
Browser displays the result page<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arithmetic Calculator</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main>
<h1>Arithmetic Calculator</h1>
<p>Enter two numbers and choose an operation.</p>
<form action="/calculate" method="get">
<label for="a">First number</label>
<input id="a" name="a" type="number" step="any" required>
<label for="b">Second number</label>
<input id="b" name="b" type="number" step="any" required>
<div class="buttons">
<button type="submit" name="operation" value="add">Add (+)</button>
<button type="submit" name="operation" value="subtract">Subtract (-)</button>
<button type="submit" name="operation" value="multiply">Multiply (×)</button>
<button type="submit" name="operation" value="divide">Divide (÷)</button>
<button type="submit" name="operation" value="remainder">Remainder (%)</button>
</div>
</form>
<p class="note">Remainder means what is left after division: 20 % 6 = 2.</p>
</main>
</body>
</html>
Read the form attributes:
| HTML | Meaning |
|---|---|
| action="/calculate" | Send the form values to /calculate. |
| method="get" | Put the values in the URL query string. A calculation reads input and returns an answer without changing stored data. |
| name="a" and name="b" | These are the parameter names sent to Spring. |
| type="number" step="any" required | Ask the browser for two numbers, allowing decimal values. |
| name="operation" value="add" | When Add is clicked, submit operation=add. The other buttons have different values. |
Only the clicked submit button supplies its operation value. No text box for the operation is needed. Click one of the five buttons to submit.
package com.edupoly.calculator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class CalculatorController {
@GetMapping("/calculate")
public String calculate(
@RequestParam("a") double first,
@RequestParam("b") double second,
@RequestParam("operation") String operation,
Model model) {
if (!Double.isFinite(first) || !Double.isFinite(second)) {
model.addAttribute("error", "Please enter two finite numbers.");
return "result";
}
double answer;
String symbol;
switch (operation) {
case "add":
answer = first + second;
symbol = "+";
break;
case "subtract":
answer = first - second;
symbol = "-";
break;
case "multiply":
answer = first * second;
symbol = "×";
break;
case "divide":
if (second == 0) {
model.addAttribute("error", "Cannot divide by zero.");
return "result";
}
answer = first / second;
symbol = "÷";
break;
case "remainder":
if (second == 0) {
model.addAttribute("error", "Cannot calculate a remainder with zero as the divisor.");
return "result";
}
answer = first % second;
symbol = "%";
break;
default:
model.addAttribute("error", "Please choose one of the five operations.");
return "result";
}
if (!Double.isFinite(answer)) {
model.addAttribute("error", "The result is too large. Please use smaller numbers.");
return "result";
}
model.addAttribute("first", first);
model.addAttribute("second", second);
model.addAttribute("symbol", symbol);
model.addAttribute("answer", answer);
return "result";
}
}
| Code | Meaning |
|---|---|
| @Controller | Let Spring discover this class as a web controller. |
| @GetMapping("/calculate") | Connect GET /calculate to this method. |
| @RequestParam("a") double first | Read a from the request and convert it to a decimal-capable Java number. |
| @RequestParam("b") double second | Read b into second. |
| @RequestParam("operation") String operation | Read the value of the clicked button. |
| switch (operation) | Choose the matching arithmetic operation. |
| model.addAttribute("answer", answer) | Make the calculated value available to the result template under the name answer. |
| return "result" | Select templates/result.html as the response page. |
There is no @ResponseBody here, because the return value selects an HTML template. The method keeps request values in local variables, so different requests do not overwrite shared controller fields.
The first checks and the checks inside divide/remainder give useful error messages. The finite-result check catches values too large for double. Missing or nonnumeric required parameters sent directly to /calculate are rejected by Spring with HTTP 400; normal browser form validation asks the user to complete the fields.
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Calculation Result</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<main>
<h1>Calculation Result</h1>
<div th:if="${error == null}">
<p class="equation">
<span th:text="${first}">10</span>
<span th:text="${symbol}">+</span>
<span th:text="${second}">5</span>
=
<strong id="answer" th:text="${answer}">15</strong>
</p>
</div>
<p id="error" class="error" th:if="${error != null}" th:text="${error}"></p>
<a href="/">Calculate again</a>
</main>
</body>
</html>
Spring supplies the Model data to Thymeleaf. th:text="${answer}" inserts the answer as escaped text. The other th:text attributes insert the input numbers and operator. th:if chooses between the normal answer and an error message. The numbers such as 10 and 15 inside the HTML are placeholders replaced during rendering.
Open /calculate with form parameters to get this page; result.html belongs in templates, not static. The browser receives the rendered HTML, not the Thymeleaf expressions.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.edupoly</groupId>
<artifactId>spring-calculator</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The webmvc starter handles HTTP requests. The Thymeleaf starter renders the result template. This project uses Spring Boot 4.1.1 and Java 21 as its compilation target.
package com.edupoly.calculator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CalculatorApplication {
public static void main(String[] args) {
SpringApplication.run(CalculatorApplication.class, args);
}
}
# Choose another available port if 8082 is busy.
server.port=8082
body { margin: 0; padding: 32px 16px; font-family: system-ui, sans-serif; color: #17253d; background: #f5f7fb; }
main { max-width: 620px; margin: 32px auto; padding: 28px; background: white; border: 1px solid #dbe2ec; border-radius: 12px; }
h1 { margin-top: 0; font-size: 28px; }
p { line-height: 1.6; }
label { display: block; margin: 18px 0 6px; font-weight: 600; }
input { box-sizing: border-box; width: 100%; padding: 12px; font-size: 18px; border: 1px solid #8796ac; border-radius: 5px; }
.buttons { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }
button { padding: 10px 14px; background: #2457a6; border: 0; border-radius: 5px; color: white; font-size: 15px; cursor: pointer; }
button:hover { background: #193f7b; }
a { color: #2457a6; }
.note { font-size: 14px; color: #536075; }
.equation { font-size: 28px; overflow-wrap: anywhere; }
.error { color: #a52323; }
This learning example uses double. Some decimal fractions have small binary rounding differences, such as 0.1 + 0.2. That is a numeric-type topic to explore later.
Official references: Reading request parameters and Handling form submission.
Focus: Reuse the Model and template concepts from the product catalog.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| Model attributes — reused | The controller supplies first, second, symbol and answer to result.html. | The view receives named calculation values instead of direct response text. |
| Conditional rendering | th:if selects the answer section or the error message. | Thymeleaf includes the relevant HTML according to a condition. |
| th:text — reused | The answer appears where result.html uses th:text="${answer}". | Thymeleaf writes a supplied value as escaped HTML text. |
You should now be able to say: Java calculates; Model carries the values; the result template presents the answer or error.
One small check: Which line calculates the answer, which line adds it to Model, and which HTML attribute displays it?
These are the next learning steps, not additional projects already implemented in this bundle. Each proposed change gives the new terminology a concrete example.
| Next stage | One useful change | Words to learn when we make that change |
|---|---|---|
| 8. A separate product-file reader | Move the existing file-reading code into ProductFileReader and have Spring supply that object to ProductController through its constructor. Print object creation and request messages to observe reuse. | @Component, dependency, constructor injection, DI, IoC and default singleton scope. |
| 9. Product operations | Add ProductService for rules such as choosing products currently in stock. | @Service, business rule and separation of responsibilities. |
| 10. Product data access | Place file-based reading and lookup behind a product repository. | Repository pattern, @Repository and data access. |
| 11. MongoDB storage | Replace the text-file storage with MongoDB while keeping the page flow familiar. | @Document, ID, MongoRepository, persistence and CRUD. |
| 12. Explicit bean configuration | Supply a helper through a configured factory method and inspect how it is registered. | @Configuration, @Bean, factory method, bean definition and registry. |
Until Stage 8 is implemented, our examples do not demonstrate Spring injecting a separate ProductFileReader into ProductController. The current product controller still reads the file itself. Keep the original getInputStream() and try-with-resources code for that step.