Spring Foundation
A guided bridge from Core Java, Servlets, and JSP to Spring Framework and Spring Boot.
Start here: seven small Spring stages
Build one small idea at a time. Start with a fixed web page, then let Java handle a request, read numbers and finally render a calculated result. Each stage has a complete project ZIP; stages 1–5 use no template engine. Stage 6 introduces Model and Thymeleaf with a product catalog. Stage 7 applies them to the calculator.
mvn spring-boot:run and keep the terminal open. Stop the previous stage with Ctrl+C before starting another on the same port.server.port=8081If 8081 is occupied, use another available value such as 8082. Save and restart. The browser address must use your chosen port. Stage 5 defaults to 8084, Stage 6 to 8085, and Stage 7 to 8082.
- Stage 1: serve Home and About Us as static HTML
- Stage 2: let a controller handle a request
- Stage 3: read two query parameters and return their sum
- Stage 4: read numbers from the URL path
- Stage 5: submit a form and return the answer directly
- Stage 6: product table and details
- Stage 7: apply Model and Thymeleaf to the calculator
Download all seven beginner stages
Stage 1: serve Home and About Us as static HTML
Start with two fixed pages. The browser requests a file and Spring Boot serves it.
Download hello-spring-simple.zip
GET / -> static/index.html -> Home page
GET /aboutus.html -> static/aboutus.html -> About Us pageThe static folder contains files served directly. index.html is the default Home page. No controller or template engine is needed for this stage.
Try and verify: Open / and /aboutus.html. Change the Home heading, stop and restart the app, then refresh.
Open the files to study in this stage
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Home</title></head>
<body>
<h1>Hello World</h1>
<a href="/aboutus.html">About Us</a>
</body>
</html>
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>About Us</title></head>
<body>
<h1>About Us</h1>
<p>This is our first Spring website.</p>
<a href="/">Home</a>
</body>
</html>
The ZIP contains every setup file and a README.html with instructions. You can run the supplied project directly.
After Stage 1: the Spring words to recognize
Focus: Start the server and serve a fixed HTML file.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| Spring Boot | HelloApplication starts our website. | Boot sets up and starts the Spring application and embedded server. |
| Static resource | static/index.html and static/aboutus.html are served as files. | Spring Boot provides a standard location for these files. |
| Configuration | server.port in application.properties chooses our port. | Spring Boot reads settings that control application startup. |
| Maven dependency | pom.xml includes the web starter. | Maven brings the Spring web libraries into this project. |
You should now be able to say: Spring Boot starts our server; Spring serves our HTML files from static.
One small check: Which file would you edit to change the port, and which file would you edit to change the Home heading?
Stage 2: let a controller handle a request
Keep the same HTML files and add one Java class. Visiting /aboutus now runs our Java method before serving the page.
Download spring-controller.zip
GET /aboutus -> aboutUs() -> forward:/aboutus.html -> same HTML page@Controller lets Spring discover this class as a web controller. @GetMapping connects GET /aboutus to aboutUs(). The println message appears in the server terminal, not in the browser. forward:/aboutus.html is an internal handoff to the static file; the browser address stays /aboutus. No template engine is needed. /aboutus.html remains directly accessible and bypasses this method.
Try and verify: Open /aboutus and watch the server terminal. Then open /aboutus.html directly and observe that our message is not printed for that direct file request.
Open the files to study in this stage
package com.edupoly.hello;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
@GetMapping("/aboutus")
public String aboutUs() {
System.out.println("Someone requested the About Us page");
return "forward:/aboutus.html";
}
}
The ZIP contains every setup file and a README.html with instructions. You can run the supplied project directly.
After Stage 2: the Spring words to recognize
Focus: Connect the controller class we wrote to the object Spring manages.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| Controller | PageController.aboutUs() handles GET /aboutus through @GetMapping. | A Spring MVC controller handles a mapped web request. |
| Component | @Controller marks PageController as a specialized component. | Spring can discover eligible annotated classes in scanned packages. |
| Bean | Spring manages an instance of PageController. | A bean is the actual object Spring manages; PageController is the class definition. |
| Container | Spring creates and manages the PageController instance. | The Spring container is the system that manages these bean objects. |
The PageController instance is the specific object being managed. The About Us HTML file is a resource, not a bean. We will demonstrate dependency injection and shared bean instances in Stage 8.
You should now be able to say: I wrote the PageController class. Spring manages a PageController object as a bean and calls its mapped method.
One small check: In this example, point to the class, explain what its object is, and say why that object is called a bean.
Stage 3: read two query parameters and return their sum
Move from serving a file to calculating an answer in Java. The browser sends two numbers after the question mark in the URL.
Download spring-request-params.zip
GET /add?a=10&b=20 -> add(10, 20) -> return 30 -> browser displays 30@RequestParam("a") reads the query parameter named a and converts it to int. The Java variable first receives that number. The same happens for b and second. @ResponseBody sends the returned number directly as response data. No HTML result page or template engine is used in this stage.
Try and verify: Open /add?a=10&b=20 and expect 30. Try /add?a=7&b=5 and expect 12. A missing or nonnumeric required value produces HTTP 400.
Open the files to study in this stage
package com.edupoly.hello;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class CalculatorController {
@GetMapping("/add")
@ResponseBody
public int add(@RequestParam("a") int first,
@RequestParam("b") int second) {
return first + second;
}
}
The ZIP contains every setup file and a README.html with instructions. You can run the supplied project directly.
After Stage 3: the Spring words to recognize
Focus: Read numbers from a request and send an answer directly.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| @RequestParam | a=10 and b=20 become the method arguments first and second. | Spring MVC binds named request parameters to Java method arguments. |
| Type conversion | The URL text "10" becomes a Java int value. | Spring MVC converts request text to the declared argument type. |
| @ResponseBody | The returned sum is sent directly to the browser. | Spring MVC writes the method result into the HTTP response body. |
You should now be able to say: Spring reads the query parameters, calls my Java method and sends its returned answer.
One small check: What changes if a is text such as abc instead of an integer?
Stage 4: read numbers from the URL path
Keep the query-parameter example and add a second method. This time the numbers appear as path sections.
Download spring-path-variables.zip
GET /add/10/20 -> addUsingPath(10, 20) -> return 30 -> browser displays 30In /add/{a}/{b}, the braces mark values captured from the path. @PathVariable("a") supplies the captured value to first. @PathVariable("b") supplies second. Java performs the addition. Both methods can exist together because /add and /add/{a}/{b} are different mappings.
Try and verify: Open /add/10/20 and expect 30. Open /add/7/5 and expect 12. Compare with /add?a=7&b=5: the input location changes, but the arithmetic is the same.
Open the files to study in this stage
package com.edupoly.hello;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class CalculatorController {
@GetMapping("/add")
@ResponseBody
public int add(@RequestParam("a") int first,
@RequestParam("b") int second) {
return first + second;
}
@GetMapping("/add/{a}/{b}")
@ResponseBody
public int addUsingPath(@PathVariable("a") int first,
@PathVariable("b") int second) {
return first + second;
}
}
The ZIP contains every setup file and a README.html with instructions. You can run the supplied project directly.
After Stage 4: the Spring words to recognize
Focus: Use an existing controller with a different location for its input values.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| @PathVariable | /add/10/20 supplies 10 and 20 through {a} and {b}. | Spring MVC binds captured URL path values to method arguments. |
| Request mapping — reused | /add and /add/{a}/{b} select different methods. | Spring MVC chooses a handler using the configured path and HTTP method. |
You should now be able to say: The arithmetic is unchanged; @PathVariable reads numbers from the URL path.
One small check: In /add/7/5, which value goes into first and which goes into second?
Stage 5: submit a form and return the answer directly
Use the same two number fields and five buttons, then send the calculated answer directly to the browser. This stage needs no Model, result.html or template engine.
Download spring-calculator-basic.zip
Form: enter 10 and 5, click Add
-> GET /calculate?a=10&b=5&operation=add
-> @RequestParam reads the three values
-> Java calculates 15.0
-> @ResponseBody sends "10.0 + 5.0 = 15.0"
-> browser displays the answer as text on the next pageThe HTML form uses action="/calculate" and method="get". Inputs named a and b supply the numbers. Only the clicked submit button supplies its operation value. The controller uses @RequestParam to read the values, calculates the result, and returns a String. @ResponseBody tells Spring to send that String directly as response text instead of interpreting it as a template name.
Try and verify: run the project and open http://localhost:8084/. Enter 10 and 5, then click Add. The next page displays 10.0 + 5.0 = 15.0. Use the browser Back button to return to the form. Try every button and division by zero. Remainder (%) is the amount left after division: 20 % 6 = 2.
Port: this project defaults to 8084. Edit src/main/resources/application.properties if needed, restart and use the matching browser address.
Open the files to study in this stage
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arithmetic Calculator — Stage 5</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>The answer opens as text on the next page. Use the browser Back button to calculate again.</p>
<p class="note">Remainder means what is left after division: 20 % 6 = 2.</p>
</main>
</body>
</html>
package com.edupoly.calculator;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class CalculatorController {
@GetMapping("/calculate")
@ResponseBody
public String calculate(
@RequestParam("a") double first,
@RequestParam("b") double second,
@RequestParam("operation") String operation) {
if (!Double.isFinite(first) || !Double.isFinite(second)) {
return "Please enter two finite numbers.";
}
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) {
return "Cannot divide by zero.";
}
answer = first / second;
symbol = "÷";
break;
case "remainder":
if (second == 0) {
return "Cannot calculate a remainder with zero as the divisor.";
}
answer = first % second;
symbol = "%";
break;
default:
return "Please choose one of the five operations.";
}
if (!Double.isFinite(answer)) {
return "The result is too large. Please use smaller numbers.";
}
return first + " " + symbol + " " + second + " = " + answer;
}
}
The ZIP includes the complete setup and a README.html. Stage 6 introduces templates through a product table and details page. Stage 7 then adds a template to this calculator.
After Stage 5: the Spring words to recognize
Focus: Let the HTML form build the request we previously typed ourselves.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| Form parameters | The fields named a and b supply the numbers to /calculate. | The browser creates a GET query string; Spring MVC reads it using @RequestParam. |
| Clicked button value | Clicking Add submits operation=add. | The controller receives the button value as a request parameter and chooses the calculation. |
| @ResponseBody — reused | The browser displays the controller's returned text on the next page. | Spring MVC sends the response directly; no result template is selected. |
You should now be able to say: The form sends numbers and an operation; Java calculates and returns a text response.
One small check: Which part of the request tells the controller whether to add or multiply?
Stage 6: introduce Model and Thymeleaf with a product catalog
We have a text file containing an array of 30 product objects. We want one table row per product and a More details link in each row. A second template displays the selected product's complete details.
Download spring-products-thymeleaf.zip
Run: extract the ZIP, open a terminal beside pom.xml and run mvn spring-boot:run. Requires JDK 21+ and Maven 3.9+. Choose a free port in src/main/resources/application.properties; this project defaults to 8085. Open http://localhost:8085/products after startup. Stop with Ctrl+C.
GET /products
-> listProducts(model)
-> model.addAttribute("products", products)
-> return "products"
-> Thymeleaf repeats a table row for each product
-> browser receives HTML containing 30 product rows
Click More details for product 1
-> GET /products/1
-> productDetails(1, model)
-> find the matching Product object
-> model.addAttribute("product", product)
-> return "product-details"
-> Thymeleaf renders product-details.html with all 8 fields1. Start with the data file
src/main/resources/data/products.txt is an ordinary UTF-8 text file. Its contents use JSON format: square brackets contain an array and each pair of braces contains one product. There are exactly 30 objects. The file stays on the server; the browser receives rendered HTML.
{
"id": 1,
"name": "Wireless Mouse",
"category": "Electronics",
"price": 699,
"brand": "EduTech",
"stock": 12,
"description": "Wireless Mouse for everyday learning, study and work. Product 1 includes a simple user guide and protective packaging.",
"warrantyMonths": 12
}Java needs Product objects to work with this JSON text. Jackson reads the file and converts its array into Product[]. The Product class describes each object's fields. These data objects are not automatically Spring beans.
2. Pass the array to the table template
@GetMapping({"/", "/products"})
public String listProducts(Model model) {
model.addAttribute("products", products);
return "products";
}Read this slowly: Spring supplies a Model for this request. Our code places the Java array in it under the name products. Returning "products" selects templates/products.html. The attribute name and template name happen to match here, but they serve different jobs.
| Code | Meaning |
|---|---|
| Model model | Spring provides a place to carry named data to this request's HTML view. |
| "products" in addAttribute | The name the template uses to find the array. |
| products after the comma | The actual Product[] Java array containing the 30 objects. |
| return "products" | Select the products.html template, rather than returning text directly. |
A model attribute means a named value placed in Model. Here we use model.addAttribute(). Model does not read the file or calculate values; our Java code prepares the data.
3. Repeat one HTML row for each product
<tr th:each="product : ${products}">
<td th:text="${product.id}">1</td>
<td th:text="${product.name}">Wireless Mouse</td>
<td th:text="${product.category}">Electronics</td>
<td th:text="${product.price}">699</td>
<td><a th:href="@{/products/{id}(id=${product.id})}">More details</a></td>
</tr>th:each reads products from Model. For each object it creates a row and calls that current object product. th:text writes a property as escaped text. For example, product.name reads getName(). One template row becomes 30 rows in the generated response. The placeholder Wireless Mouse is replaced by the actual name in each iteration.
4. Build a link for the current row
th:href builds /products/1 for product 1, /products/2 for product 2, and so on. Clicking the link sends a new request. The ID travels in the URL; the whole product object is not sent by the link.
5. Select one product and a different template
@GetMapping("/products/{id}")
public String productDetails(@PathVariable("id") int id, Model model) {
for (Product product : products) {
if (product.getId() == id) {
model.addAttribute("product", product);
return "product-details";
}
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}@PathVariable reads the selected ID. The loop finds the matching object in our server-side array. This request gets its own Model: we explicitly add the chosen object under product. We do not rely on the previous table request's Model. Returning "product-details" selects a different file, templates/product-details.html.
6. Display the selected object's details
<h2 th:text="${product.name}">Product name</h2>
<p th:text="${product.description}">Description</p>
<p th:text="${product.brand}">Brand</p>The complete details template shows all eight fields: ID, name, category, price, brand, stock, description and warranty months. The same template works for every product because the controller supplies a different product object.
7. Understand the file-reading step
The controller constructor reads products.txt once during application startup. ClassPathResource can read the bundled resource even from the packaged JAR. try-with-resources closes the input stream. JsonMapper converts JSON text into a Product array. This Spring Boot 4 project uses Jackson 3's tools.jackson package. The read-only array is reused; it does not hold per-user request state. Restart after editing the text file; rebuild first if running a packaged JAR.
8. Verify the result yourself
- Open /products and confirm 30 rows and 30 More details links.
- Click product 1: the address becomes /products/1 and all its fields appear.
- Return to the table and open product 30. The same detail template now shows Travel Mug.
- Try /products/999: the controller returns HTTP 404 because there is no matching product.
- Edit a product name in products.txt, restart and verify the table and detail page both use the new name.
Open the complete Java and template files
package com.edupoly.products;
import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;
import tools.jackson.databind.json.JsonMapper;
@Controller
public class ProductController {
private final Product[] products;
// Read the text file once when Spring creates this controller.
public ProductController() throws IOException {
var resource = new ClassPathResource("data/products.txt");
try (var input = resource.getInputStream()) {
JsonMapper mapper = JsonMapper.builder().build();
products = mapper.readValue(input, Product[].class);
}
}
@GetMapping({"/", "/products"})
public String listProducts(Model model) {
model.addAttribute("products", products);
return "products";
}
@GetMapping("/products/{id}")
public String productDetails(@PathVariable("id") int id, Model model) {
for (Product product : products) {
if (product.getId() == id) {
model.addAttribute("product", product);
return "product-details";
}
}
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
}
}
<!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>Products</title>
<link rel="stylesheet" th:href="@{/style.css}">
</head>
<body>
<main>
<h1>Products</h1>
<p>Basic details for 30 products. Select More details to see a product's complete information.</p>
<div class="table-scroll">
<table>
<thead>
<tr><th>ID</th><th>Name</th><th>Category</th><th>Price (INR)</th><th>Action</th></tr>
</thead>
<tbody>
<tr th:each="product : ${products}">
<td th:text="${product.id}">1</td>
<td th:text="${product.name}">Wireless Mouse</td>
<td th:text="${product.category}">Electronics</td>
<td th:text="${product.price}">699</td>
<td><a th:href="@{/products/{id}(id=${product.id})}">More details</a></td>
</tr>
</tbody>
</table>
</div>
</main>
</body>
</html>
<!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 th:text="${product.name}">Product details</title>
<link rel="stylesheet" th:href="@{/style.css}">
</head>
<body>
<main class="detail">
<h1>Product details</h1>
<h2 th:text="${product.name}">Wireless Mouse</h2>
<dl>
<dt>ID</dt><dd th:text="${product.id}">1</dd>
<dt>Name</dt><dd th:text="${product.name}">Wireless Mouse</dd>
<dt>Category</dt><dd th:text="${product.category}">Electronics</dd>
<dt>Price (INR)</dt><dd th:text="${product.price}">699</dd>
<dt>Brand</dt><dd th:text="${product.brand}">EduTech</dd>
<dt>Stock</dt><dd th:text="${product.stock}">12</dd>
<dt>Description</dt><dd th:text="${product.description}">Product description</dd>
<dt>Warranty (months)</dt><dd th:text="${product.warrantyMonths}">12</dd>
</dl>
<a th:href="@{/products}">Back to products</a>
</main>
</body>
</html>
The ZIP also contains Product.java, all 30 objects, the startup class, pom.xml, port configuration, CSS and README.html. No database is needed.
After Stage 6: the Spring words to recognize
Focus: Give Java data to HTML without building HTML strings in the controller.
| Term | Our concrete example | Its place in Spring |
|---|---|---|
| Model | listProducts(Model model) receives the view-data holder for the table request. | Spring MVC supplies a Model to carry named data to the selected view. |
| Model attribute | addAttribute("products", products) puts our Product array under the name products. | A model attribute is a named value available to the view for this request. |
| View name / template | return "products" selects products.html; return "product-details" selects product-details.html. | Spring MVC uses the view name to select the HTML template to render. |
| Thymeleaf | th:each repeats rows, th:text displays values, and th:href builds each details link. | Thymeleaf renders HTML on the server using the values supplied through Model. |
Product.java describes ordinary data objects. The 30 Product objects are not registered as Spring beans in this example. The product-details request receives a new Model; the controller adds its selected product again.
You should now be able to say: The controller supplies products through Model. Thymeleaf turns those products into HTML rows.
One small check: When /products/1 is requested, which product is added to Model and which template is selected?
Stage 7: apply Model and Thymeleaf to the calculator
Use two input fields and five buttons. A static form sends numbers to Java; the controller calculates the answer and selects a result template.
Download spring-calculator.zip
Form -> GET /calculate?a=20&b=6&operation=remainder
-> controller calculates 2.0
-> Model carries values
-> Thymeleaf renders result.html
-> browser displays a separate result pageThe two inputs submit a and b. Only the clicked button submits its operation value. @RequestParam reads these three values. Java chooses an operation using switch. Model carries named values from the controller to the template. return "result" selects templates/result.html; it does not send the literal word result. Thymeleaf replaces th:text expressions with the supplied values. There is no @ResponseBody on this method. Remainder (%) means what is left after division, not percentage. The result page includes messages for zero divisors.
Try and verify: Enter 20 and 6, then click each button. Expect 26.0, 14.0, 120.0, about 3.3333333333333335 and 2.0. Try division by zero, then Calculate again. The project uses double, so decimal rounding artifacts can occur.
Open the files to study in this stage
<!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>
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";
}
}
<!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>
The ZIP contains every setup file and a README.html with instructions. You can run the supplied project directly.
After Stage 7: the Spring words to recognize
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?
A useful continuation: planned next stages
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.
Compare the three ways we passed numbers
| Stage | Request | How Java reads it |
|---|---|---|
| Query parameters | /add?a=10&b=20 | @RequestParam |
| Path variables | /add/10/20 | @PathVariable |
| GET form | /calculate?a=10&b=20&operation=add | @RequestParam; browser builds the query string |
Practice: explain what the browser sends, which Java method runs, and what the browser receives for each stage. Continue below when these examples feel clear.
1. Connect the new concepts to familiar ones
| What you already know | Spring equivalent | What changes |
|---|---|---|
| new Service() inside a Servlet | Constructor injection of a Spring bean | The container constructs and supplies the service. |
| doGet / doPost | @GetMapping / @PostMapping | Methods declare the HTTP routes they handle. |
| request.getParameter | @ModelAttribute / @RequestParam | Spring binds request inputs into Java values. |
| request.setAttribute + RequestDispatcher.forward | Model attributes + view name | The view resolver finds the template. |
| JSP + JSTL | Thymeleaf templates + th:* attributes | HTML templates are rendered on the server. |
| Servlet filters | Spring Security filter chain | Security runs before controller methods. |
| Manually configuring Tomcat | Boot embedded Tomcat | The web server starts with main(). |
| JDBC DAO | MongoRepository | The Spring Data proxy supplies document CRUD. |
What is DAO? DAO — Data Access Object: interface versus implementation class (click here). Read the explanation, Java examples, and MongoRepository comparison in the third column.
2. What Spring Framework does — step by step
Start with a small Java program: a TodoPrinter displays task titles, a TodoService decides what to return, and a TodoStore supplies the tasks. These are classes you write. Their methods need actual objects to work with. Spring can create and keep track of selected objects and supply them to other objects that need them.
Open the Spring Framework explanation in the third column. Follow the steps below before learning the annotation names.
2.1 A class defines a type; an object is one instance
A class describes fields, constructors and methods. Declaring a class does not automatically run its constructor. An expression such as new TodoStore() creates an instance and calls its constructor. The variable store holds a reference to that instance, rather than containing the whole object.
TodoStore store = new TodoStore();
TodoService service = new TodoService(store);
TodoPrinter printer = new TodoPrinter(service);Read the second line as: create a service object and pass the existing store reference to its constructor. The service saves that reference in a field so its methods can call the store later. That required store is called a dependency.
2.2 Is the entire application one object? Does every class need an object?
| Question | Answer |
|---|---|
| Is the application a single object? | No. It is a running program with many collaborating objects, classes, resources and threads. A Spring ApplicationContext manages a selected group of those objects; it is not the entire application. |
| Is every class instantiated? | No. A class can exist without any instances. A static method can run without an instance of its declaring class. An abstract class cannot be directly instantiated, and an interface has no constructor for creating an instance. |
| How many objects can one class have? | Zero, one, or many. Two executions of new TodoStore() create two distinct instances. Several variables can also refer to the same instance. |
| Does Spring create every object? | No. It manages objects registered through configuration, component scanning, or framework integration. A Todo, DTO, String or local List is not automatically a Spring bean simply because the application uses Spring. |
| What is usually managed? | Controllers, services, repositories and shared configured helpers. These objects perform work. Individual Todo objects carry task data and are usually constructed by application code or data-mapping libraries. |
2.3 First connect the objects yourself
This is normal Java and already uses constructor dependency injection: the caller supplies the dependency through a constructor. Spring is not required for this Java pattern. In a small program, writing these three construction statements is straightforward. In a larger application, many services need shared collaborators, configuration and lifecycle handling.
2.4 Tell Spring how to build the selected objects
The runnable example uses explicit @Bean factory methods so you can see the construction code. @Configuration marks the configuration class. Each @Bean method tells Spring how to obtain one managed object. Spring supplies a method's parameters from the objects in its context; our method then calls the Java constructor using those references.
@Bean
public TodoService todoService(TodoStore store) {
return new TodoService(store);
}The parameter store is supplied by Spring. The new expression still creates the Java object. Spring records the returned service as a bean and makes it available when the printer needs a service. In this example there is exactly one matching bean for each dependency type.
Factory methods: from ordinary Java to Spring
A factory method is a method whose job is to create or obtain an object and return its reference. In this example, it contains the instructions Spring uses to obtain a service object. Explore factory methods in the third column.
Step 1: create the service directly
TodoStore store = new TodoStore();
TodoService service = new TodoService(store);new TodoService(store) constructs a service and calls its constructor. The existing store reference is passed as an argument. The service variable receives the reference to the resulting service object.
Step 2: put construction inside a method
public TodoService createTodoService(TodoStore store) {
return new TodoService(store);
}Read it as: “Give me a store reference, and I will construct and return a service that uses it.” This is an ordinary Java method. The word factory describes its purpose; Java has no special factory-method keyword.
TodoStore store = new TodoStore();
TodoService service = createTodoService(store);The caller supplies the store and receives the service. These excerpts explain the idea; the complete files and run command are in section 2.6 below.
Step 3: tell Spring to use the factory
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public TodoStore todoStore() {
return new TodoStore();
}
@Bean
public TodoService todoService(TodoStore store) {
return new TodoService(store);
}
}When AppConfig is registered with the context, Spring reads the @Bean methods as bean definitions. Spring supplies each method's required dependency arguments and manages its returned object. You write the factory body, including the constructor call.
| Part of the service factory | Meaning |
|---|---|
| TodoService before the name | The return type: what kind of object reference the method returns. |
| todoService | The method name; also the bean name with the default naming configuration. |
| TodoStore store | A required dependency that Spring supplies from the context. |
| new TodoService(store) | Calls the constructor to construct the service. |
| return | Passes the service reference back to Spring. |
Step 4: follow who calls what
Spring calls todoStore()
-> receives and registers a TodoStore object
-> supplies that store to todoService(store)
-> the method constructs and returns a TodoService
-> Spring manages the returned servicevar context = new AnnotationConfigApplicationContext(AppConfig.class);
TodoService service = context.getBean(TodoService.class);
// Close the context when finished; the complete demo uses try-with-resources.The context invokes the factories as needed to create the configured beans. In this default singleton example, subsequent lookups reuse the registered service. Getting a bean is different from directly calling a Java factory method to construct another object.
Step 5: distinguish the constructor from the factory
// Constructor declared inside TodoService
public TodoService(TodoStore store) {
this.store = store;
}The constructor has the class name and no return type. It initializes the service during construction. The factory method has a return type, an ordinary method name and a return statement. Here the factory calls the constructor internally and returns its result.
Factories are useful when creation needs choices or setup: selecting an implementation, supplying configuration, or creating an object from a library. A factory need not be static and can return a new or existing object. Spring's bean scope separately determines how the managed object is reused.
For this configuration: proxyBeanMethods = false means direct calls to its methods are ordinary Java calls. Supply dependencies through factory parameters and use the context to retrieve managed beans.
Read “Spring calls the factory methods” as: Spring calls the configured @Bean methods, supplies their required arguments, and manages the objects those methods return.
Practice: find all three @Bean methods in Main.java below. Identify the return type, the dependency parameters and the constructor call in each one.
Reference: Spring: using @Bean.
2.5 What happens at startup, and what happens later?
- Our main method starts an
AnnotationConfigApplicationContextwithAppConfig.class. - Spring reads the configuration and its bean definitions.
- To create the printer, Spring needs the service; to create the service, it needs the store. Spring obtains the dependencies before completing each dependent object.
- Spring calls the factory methods with the required references, registers their returned objects and completes their initialization. In this example the default singleton beans are created during context startup.
- Our code asks the context for the printer and calls its print method. The printer calls the service, which calls the store. These are ordinary Java method calls.
The two diagrams show different things: construction supplies references; later method calls use those references. Spring does not invent the business logic inside these methods.
2.6 Run a complete small example
Download the Spring objects demo ZIP. Extract it and open a terminal in spring-objects-demo, where pom.xml is located. You need JDK 21+ and Maven 3.9+ on PATH, plus internet for dependencies on the first run. This console example needs no database or Tomcat installation.
spring-objects-demo/
pom.xml
src/main/java/com/edupoly/demo/Main.javaTo create it manually, save these two complete files at those paths. All helper classes are nested inside Main to keep this first exercise small; real projects normally use separate files.
<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>
<groupId>com.edupoly</groupId>
<artifactId>spring-objects-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>7.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<mainClass>com.edupoly.demo.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.edupoly.demo;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class Main {
// A data class: individual Todo instances are not registered as beans here.
public record Todo(String title) {}
public static class TodoStore {
public List<Todo> findAll() {
return List.of(new Todo("Learn Java"), new Todo("Learn Spring"));
}
}
public static class TodoService {
private final TodoStore store;
public TodoService(TodoStore store) {
this.store = store;
}
public List<String> titles() {
return store.findAll().stream().map(Todo::title).toList();
}
}
public static class TodoPrinter {
private final TodoService service;
public TodoPrinter(TodoService service) {
this.service = service;
}
public void print(String label) {
System.out.println(label + ": " + service.titles());
}
}
@Configuration(proxyBeanMethods = false)
public static class AppConfig {
@Bean
public TodoStore todoStore() {
return new TodoStore();
}
@Bean
public TodoService todoService(TodoStore store) {
return new TodoService(store);
}
@Bean
public TodoPrinter todoPrinter(TodoService service) {
return new TodoPrinter(service);
}
}
public static void main(String[] args) {
// First: our code creates and connects all three objects.
TodoStore store = new TodoStore();
TodoService service = new TodoService(store);
TodoPrinter printer = new TodoPrinter(service);
printer.print("Manual Java");
// Second: Spring calls the configured factory methods and supplies
// each method's arguments from objects registered in the context.
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
TodoPrinter first = context.getBean(TodoPrinter.class);
TodoPrinter second = context.getBean(TodoPrinter.class);
first.print("Spring");
System.out.println("Same printer bean: " + (first == second));
System.out.println("Store passed to service: "
+ (context.getBean(TodoService.class).store
== context.getBean(TodoStore.class)));
}
}
}
mvn -q compile exec:javaManual Java: [Learn Java, Learn Spring]
Spring: [Learn Java, Learn Spring]
Same printer bean: true
Store passed to service: trueMaven or the JDK may also print diagnostic messages. Both approaches produce the same titles because they call the same classes. The first true shows that two lookups return the same singleton printer. The second true confirms that Spring supplied the same store reference to the service. The manual objects and Spring-managed objects are separate sets.
2.7 Connect this example to the todo web application
TodoPrinter is a console helper, not a web controller. In the web project, a TodoController takes the entry-point role: Spring MVC receives an HTTP request and invokes a mapped method on the controller bean. The controller uses its injected service, which uses its repository. Your code still implements the task operations.
Later, @Service and @Controller, together with enabled component scanning, let Spring find classes without an explicit factory method for each one. With one constructor, Spring supplies its required arguments without needing @Autowired. Spring Data separately registers implementations for configured repository interfaces. A plain interface cannot be instantiated using new.
The default singleton scope means one bean instance per definition per application context, not one object for the entire program and not one new controller for each request. Do not store a current user's request data in shared controller or service fields. DTOs and Todo records are normally separate data objects.
2.8 What else does Spring Framework help with?
| Need | Spring's support | You still write |
|---|---|---|
| Connect collaborating classes | A container that manages configured objects and supplies dependencies. | Classes, constructors, configuration and operations. |
| Handle a web request | Spring MVC maps routes, binds inputs and renders views or JSON. | Controller methods and their intended responses. |
| Validate input | Integration that invokes configured validation rules. | Constraints and useful error responses. |
| Coordinate database transactions | Transaction support when configured with a suitable data source or database setup. | Transaction boundaries and business behavior. |
Spring Boot builds on these facilities and reduces startup and configuration work. Start with the objects and their relationships; the next section explains Boot.
2.9 Practice and check your understanding
- Add a third Todo in findAll(), rerun the command and predict which output lines change.
- Explain why both bean lookups return the same printer, while two new TodoPrinter(service) expressions would create separate printers.
- Point to the exact parameter where Spring supplies the store. Then point to the line where Java constructs the service.
- Explain why new Todo("Learn Java") creates a data object but does not register a Spring bean.
Further reading: Spring's container and beans.
3. What Spring Boot adds
Spring Boot builds on Spring Framework and simplifies application setup. It provides automatic configuration, starter dependencies, and tools for packaging your application as an executable JAR. For web applications, it can also configure an embedded server.
Automatic configuration examines the libraries included in your project, your application settings, and the Spring beans you have already configured. It uses this information to decide which components to set up.
For example:
- Adding the Spring MVC starter allows Spring Boot to configure components for handling web requests.
- Adding the Thymeleaf starter allows it to configure a template engine that generates HTML pages.
- Adding the MongoDB starter and providing a valid connection URI allows it to configure the components your application needs to access MongoDB.
A library is “present” when it is included in your project and available to the application when it runs. For example, adding a starter dependency to pom.xml lets Maven download the required library JARs and include them in the application. The classpath is where Java looks for application classes and libraries. “Thymeleaf is present on the classpath” means Java can find the Thymeleaf library.
For MongoDB, this refers to the Java libraries used to connect to the database. The database itself runs on a server or Atlas cluster, and the connection URI tells your application how to reach it. Spring Boot also considers configuration conditions and existing beans; adding a library does not guarantee that every possible component will be configured.
A starter is a convenient dependency group. The parent POM manages compatible versions, so individual starter versions are omitted. The Maven plugin packages dependencies into an executable JAR. You can run it with java -jar rather than deploying a WAR manually.
@SpringBootApplication combines @Configuration, @EnableAutoConfiguration, and @ComponentScan. Put it above your application packages. It is not a substitute for understanding beans; it is the entry point that connects their discovery and configuration.
Boot is also useful for applications without web pages. A REST application still uses Spring MVC and Tomcat, but JSON serialization replaces template resolution. Spring Boot and Spring MVC are related parts of the stack, not two competing frameworks.
4. Trace one web request
For GET /todos, Tomcat accepts the HTTP request. Security filters apply their rules. DispatcherServlet locates the mapped controller method. The controller requests a list from the service, which uses the repository to read MongoDB documents. The controller places that list in Model and returns "list". Thymeleaf resolves templates/list.html and renders HTML. The response goes back to the browser.
For a JSON endpoint, the same route ends with the controller returning an object. A message converter serializes it to JSON. @RequestBody performs the reverse operation: JSON becomes the request DTO before your method executes.
Keep responsibilities clear: controllers handle HTTP and view/API contracts, services implement operations and authorization rules, repositories access documents, DTOs define accepted inputs, and templates/components present results.
5. HTTP and validation before coding
GET retrieves state and should not mutate it. POST creates a resource or submits a form. PUT replaces the editable state at a known resource URL. DELETE removes it. HTML forms natively use GET and POST; the Thymeleaf lab uses POST routes for update and delete. JSON clients can send PUT and DELETE directly.
Client validation improves usability, while server validation protects the contract. @NotBlank rejects empty titles, @Size caps length, @Email checks email shape, and @NotNull rejects missing required values. @Valid triggers validation. A validation annotation on a field alone does not make every method call validate automatically.
400 means invalid request, 401 means authentication is missing or invalid, 403 means the authenticated request lacks access, 404 means the resource is not available, and 409 means a conflict such as a duplicate email. The personal application deliberately uses 404 for foreign-owned todo ids.
6. MongoDB essentials
MongoDB stores BSON documents in collections. A database groups collections. A todo might contain _id, title, and completed. BSON supports more types than JSON; APIs expose JSON views of the stored data. A Java document is mapped by Spring Data rather than manually converted in each controller.
@Document names the collection. @Id selects the id field. MongoRepository<Todo,String> identifies the document and id types. Derived query names such as findByIdAndOwnerId instruct Spring Data to build an equality query. MongoDB is not accessed using @Entity, JpaRepository, or SQL joins in these projects.
Atlas is a managed MongoDB hosting service. Its website account manages projects and clusters; a separate database user authenticates application connections. Your app also needs a matching network IP rule. Each application uses its own database name to keep the labs independent.
A single document write is atomic. The read-then-save update shown in these labs can overwrite concurrent changes. That tradeoff keeps the first implementation teachable. A later exercise can add optimistic locking with @Version and return a conflict for a stale update.
7. Configuration and project layout
src/main/java contains compiled Java source. src/main/resources contains templates, static assets, and configuration that are copied into the JAR. src/test/java contains tests. target is generated output. pom.xml defines coordinates, dependencies, Java baseline, and plugins.
application.properties separates deployment choices from source. server.port selects the HTTP port. spring.mongodb.uri reads the driver URI; spring.mongodb.database selects the application database. ${MONGODB_URI} is a property placeholder resolved from the environment. Never substitute an actual password into the checked-in file.
When starting from an IDE, configure MONGODB_URI in its run configuration. Variables set in one terminal do not automatically appear inside an already-running IDE. Restarting or changing the IDE launch environment may be necessary.
8. Prepare your development tools
Install a JDK 21, Apache Maven 3.9+, and an IDE such as IntelliJ IDEA, Eclipse/STS, or VS Code with Java extensions. Maven must be on PATH and JAVA_HOME must point to the JDK directory, not its bin subdirectory. Use a terminal in the project folder. Internet access is needed on the first dependency download. Spring Boot projects include an embedded Tomcat server. The Servlets and JSPs tutorial explains how to install Tomcat separately.
For the React lab, also install Node.js 22.12+ and npm. For the REST labs, install Postman Desktop, or use the web client with its Desktop Agent for localhost requests.
java -version
javac -version
mvn -version
node --version
npm --versionExpect Java and javac to report version 21, Maven to report 3.9.x and its Java home, and Node to report a supported 22.x or newer version. If multiple JDKs are installed, Maven’s reported Java version determines compilation. The projects target Java 21; using an older JDK causes “release version 21 not supported.”
These examples deliberately pin Boot 4.1.1. Boot 4’s MVC starter is spring-boot-starter-webmvc; its MongoDB connection property is spring.mongodb.uri. When reading a Boot 3 tutorial, check its starter and configuration names before mixing it with this code.
Version references: Spring Boot requirements, Boot configuration properties, and Vite setup.
9. Check understanding
- Explain why a controller should not create a repository with new.
- Trace a GET request until Thymeleaf produces HTML.
- Explain the difference between Spring Framework, Spring MVC, and Spring Boot.
- Name the folder for templates and the folder for static CSS.
- Explain why a validation rule must run on the server even if the browser uses required.