STAGE 8 OF 37

Stage 8: Let Spring supply a product-file reader

Learning path / Stage 8 of 37

Before you start: use JDK 21+ and Maven 3.9+. Extract this stage’s project and open a terminal in its pom.xml folder. Complete the setup below, including a database connection where required, then run mvn spring-boot:run. Use the port specified for this stage and stop with Ctrl+C.

Download spring-products-di.zip · Complete file-by-file guide

Browser → ProductController → ProductFileReader → Product[] → Model → Thymeleaf
  1. Start from Stage 6

    The earlier controller loaded products.txt itself. This stage moves that exact stream-based reading code into ProductFileReader. The file still contains the same 30 objects. The Product data objects remain ordinary Java objects.

  2. Mark the helper with @Component

    Spring discovers ProductFileReader because it is in the application package. Spring creates one reader object during startup and keeps its reference. The reader constructor loads the file once. @Component does not make every Product object a bean.

  3. Ask for the reader through the controller constructor

    ProductController(ProductFileReader reader) declares what the controller needs. Spring creates the reader first, then supplies that object when constructing the controller. this.reader = reader stores the supplied reference; it does not create another reader. A class with one constructor does not need @Autowired on that constructor.

  4. Observe creation and reuse

    Start the app and read the terminal. Created ProductFileReader appears once. Refresh /products three times: the reader identity printed for each request stays the same in this running application. The identity number is only a demonstration aid, not a permanent identifier. Restarting creates a new application context and new objects.

  5. Connect DI, IoC and singleton to those lines

    The reader is a dependency because the controller needs it. Supplying it through the constructor is dependency injection (DI). Spring taking responsibility for constructing and connecting these objects is inversion of control (IoC). By default each bean definition has one shared instance per application context: singleton scope. Do not store request-specific values in shared controller fields.

Run this stage

Use JDK 21+ and Maven 3.9+. Extract spring-products-di.zip and open a terminal in the spring-products-di folder containing pom.xml. Keep the source files in their supplied paths.

Start the application
mvn spring-boot:run

Open http://localhost:8086/products. Keep the terminal open. Stop with Ctrl+C. If the port is occupied, change server.port in src/main/resources/application.properties, restart and use the new port in the browser.

src/main/resources/application.properties
# Choose another available port if this port is busy.
server.port=8086

Expected output and checks

Open /products: 30 product rows. Open /products/1: full details. Repeated list requests print the same reader identity. /products/99999 returns 404.

Read the changed Java files

The downloadable project includes every file, the 30-product text file, templates, CSS, pom.xml and a standalone README.html with all source files. These are the key changes from the preceding example.

src/main/java/com/edupoly/products/ProductFileReader.java
src/main/java/com/edupoly/products/ProductFileReader.java
package com.edupoly.products;

import java.io.IOException;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import tools.jackson.databind.json.JsonMapper;

@Component
public class ProductFileReader {
    private final Product[] products;

    public ProductFileReader() 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);
        }
        System.out.println("Created ProductFileReader");
    }

    public Product[] getProducts() {
        return products;
    }
}
src/main/java/com/edupoly/products/ProductController.java
src/main/java/com/edupoly/products/ProductController.java
package com.edupoly.products;

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;

@Controller
public class ProductController {
    private final ProductFileReader reader;

    public ProductController(ProductFileReader reader) {
        this.reader = reader;
        System.out.println("Created ProductController with its supplied reader");
    }

    @GetMapping({"/", "/products"})
    public String listProducts(Model model) {
        System.out.println("Reader used by controller: " + System.identityHashCode(reader));
        model.addAttribute("products", reader.getProducts());
        return "products";
    }

    @GetMapping("/products/{id}")
    public String productDetails(@PathVariable("id") int id, Model model) {
        for (Product product : reader.getProducts()) {
            if (product.getId() == id) {
                model.addAttribute("product", product);
                return "product-details";
            }
        }
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
    }
}

Practice

Add a diagnostic line in the reader constructor and refresh the page. Explain why the constructor does not run for every request.

References: Spring constructor injection, Spring Data MongoDB repositories.

Point to the exact dependency

ProductController is the class we wrote. Spring creates a ProductController object. That object needs a ProductFileReader object, so the controller constructor asks for one. Spring supplies the reference and the controller stores it. We did not turn the whole application, each Product data object or the HTML file into a bean. Stage 15 later demonstrates why this constructor also makes ordinary Java testing straightforward.

After Stage 8: the Spring words to recognize

TermOur concrete exampleIts place in Spring
@ComponentProductFileReaderMarks this class for discovery as a Spring-managed object.
Bean and containerThe reader object; the application contextThe bean is the actual managed object. The container is Spring’s system that creates and keeps that reader and the controller.
Constructor injection / DI / IoCProductController(ProductFileReader reader)Spring supplies the required reader instead of the controller constructing it.
Singleton scopeThe shared reader instanceOne instance for this bean definition in this application context by default.

One small check: Point to the class, constructor or annotation that demonstrates each term before continuing.

Concepts used in this stage

Use the right column for a quick reminder, or open a concept above for its full explanation.

Java 21 · Spring Boot 4.1.1 · Edupoly