Stage 18: keep a demo delivery page out of component scanning

Reused: package scanning. New: exclude one component.

The practical requirement: keep a demonstration delivery page in our codebase, but do not register its controller in the normal application. The real product-details page must continue showing the Stage 17 delivery information.

DemoDeliveryController is a new Java class we write for the preview. It supplies sample values to a Thymeleaf page. DeliveryScanConfig is another class we write to configure discovery. Neither name is a Spring keyword.

We reuse the complete Stage 17 product application, including its real delivery service and the earlier report. This stage asks which eligible classes within a scanned package should become beans. It does not introduce a new injection style.

Keep the demo controller source while excluding its bean; verify that real product delivery remains available.

Open this stage in the online learning path

Before you start

Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.

Continue the Stage 17 delivery application. Start with the real and demo page comparison, then inspect the four changed files and try the exclusion experiment.

The practical requirement

The delivery team keeps a preview page with sample terms: Instant delivery (demo), charge INR 0. Customers should continue seeing the real terms from Stage 17: 3-5 working days and INR 49. Keep the preview source for demonstrations, but exclude its controller from the normal application configuration.

Red demo-preview box without an exclusion, 404 with the exclusion, real delivery still available, and the startup discovery flow
The visible result before and after filtering. The supplied project starts with the exclusion enabled. Open the full-size diagram.
Scan lesson package → DeliveryScanConfig → Scan delivery with exclusion → Real service bean; no demo controller → Real page 200; demo 404

When would we actually need this exclusion?

Use a scan exclusion when a package contains components this application needs, alongside a particular component it deliberately should not register. Exclusions are a targeted configuration tool, not a requirement for every Spring application.

In our example, com.edupoly.delivery contains DeliveryInfoService, which the product controller needs, and DemoDeliveryController, which the normal application should leave out. We keep scanning that package and filter out the demo type.

Practical situationWhy an exclusion can help
A shared module contains useful services and demonstration controllers.Use its services without registering the demonstration controllers and their routes.
Older code contains a component this application has replaced.Keep the old source during a migration while preventing this scan from registering its bean. Ensure consumers receive the intended replacement.
Several applications scan a shared package.Each application can select the components it needs and leave out particular types.

Choose the mechanism that matches the requirement

What do we mean?A suitable approach
This class should never be automatically discovered as a component.Remove its stereotype annotation, such as @Component or @Controller, if we own the source and that change suits all applications using it.
This application needs this package, except one particular component.Apply an excludeFilters rule to the relevant component scan, as in this stage.
The demo code does not belong in the normal application scan.Place it outside the configured scan boundary or in a separate demonstration module when we control the layout.
This component belongs only in development.A profile usually expresses that environment choice more clearly. See Stage 26: profiles.
Enable this feature through an application setting.A Spring Boot property condition such as @ConditionalOnProperty usually expresses the feature switch more clearly. This is a later configuration option, not extra code required here.

Why this example uses a filter: we deliberately keep the real service and demo controller in the same scanned package so we can observe how a filter selects candidates within that package. In an application where we control the source layout, organising demo code separately may be simpler.

Removing an annotation, restricting a package scan and applying a filter affect discovery. They do not delete the Java class or prevent every possible explicit registration. Profiles and property conditions express when a component should participate. Choose based on that requirement rather than adding a filter routinely.

What Stage 18 teaches: how to control discovery inside a package. It does not mean every application needs scan exclusions.

Where filtering and the preview are added: four files

The real product controller, delivery service, product details, data file, styles and report are reused. application.properties declares port 8118 for this stage.

File — jump to complete sourceWhat changes
Application.javaChange back to the default lesson-package scan; DeliveryScanConfig handles the delivery package.
DeliveryScanConfig.javaScan delivery components while excluding the demo controller.
DemoDeliveryController.javaNew demonstration controller with sample delivery terms.
demo-delivery.htmlNew preview template used only when the demo controller participates.
Blue = familiar controller and ModelYellow = scan configuration and preview

Application.java

Application.java · focus lines
@SpringBootApplication

DeliveryScanConfig.java

DeliveryScanConfig.java · focus lines
@Configuration
@ComponentScan(
    basePackages = "com.edupoly.delivery",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = DemoDeliveryController.class
    )
)
public class DeliveryScanConfig { }

DemoDeliveryController.java

DemoDeliveryController.java · focus lines
        model.addAttribute("deliveryTime", "Instant delivery (demo)");
        model.addAttribute("deliveryCharge", 0);
        return "demo-delivery";

Four complete files below have yellow SCAN FILTER headings. The preview controller and template use familiar MVC concepts. The new mechanism is excludeFilters with FilterType.ASSIGNABLE_TYPE.

Guided experiment: include, exclude, compare

A. Run the supplied filtered application

  1. Extract the ZIP, open its pom.xml folder and run mvn test, then mvn spring-boot:run.
  2. Open http://localhost:8118/products/1. The real delivery panel shows 3-5 working days and INR 49.
  3. Open http://localhost:8118/demo/delivery. Expect HTTP 404. A default error page is normal; its exact appearance may vary.

B. Temporarily include the demo controller

Stop with Ctrl+C. In DeliveryScanConfig.java replace only its entire @ComponentScan(...) annotation with the one below. Keep @Configuration, the class and the imports. Do not change Application.java.

DeliveryScanConfig.java · temporary annotation replacement
@ComponentScan(basePackages = "com.edupoly.delivery")

Run mvn spring-boot:run again. Now /demo/delivery displays DEMO delivery preview, Instant delivery (demo), and INR 0. The real /products/1 page still displays INR 49. We did not change any service implementation or request condition; startup discovery registered an additional controller.

The final-state tests expect the demo controller to be absent. During this temporary experiment, running mvn test is expected to fail those assertions. Spring Boot's run command compiles the tests but does not execute them.

C. Restore the exclusion

Stop the application, restore the supplied @ComponentScan annotation with excludeFilters, run mvn test and restart. The demo URL returns 404 again; the real page remains available. The demo Java source, its @Controller annotation and its template have stayed in the project throughout.

Why change Application.java too?

In Stage 17, Application scanned both lesson and delivery directly. Here it scans its own lesson package by default. That discovers DeliveryScanConfig, whose separate scan controls the delivery package. This prevents another unfiltered scan from registering the class we intended to exclude.

Package layout and discovery
com.edupoly.lesson
  Application                  // default scan starts here
  DeliveryScanConfig           // scans delivery with a filter
  ProductController            // real product requests

com.edupoly.delivery
  DeliveryInfoService          // discovered normally
  DemoDeliveryController       // excluded by the filter

Read the filter one part at a time

Default stereotype detection stays enabled. Spring still discovers DeliveryInfoService because it is annotated with @Service and does not match the exclusion.

What does exclusion actually change?

The demo class still compiles. This scan simply does not register its bean definition. With no demo controller bean, Spring MVC registers no handler for /demo/delivery, so the request returns 404. The template under templates/ is not a directly served static page.

An exclusion applies to this scan; it is not a global ban on the class. Another scan, @Import or an explicit @Bean registration could register it separately. Keep the two scan boundaries shown above when doing this experiment. This controls application composition, not user authentication.

What we change and why

  1. Give the new classes concrete jobs

    DemoDeliveryController handles /demo/delivery and supplies sample values to demo-delivery.html. DeliveryScanConfig configures which classes Spring discovers in the delivery package. These are our own class names.

  2. Reuse the existing product application

    ProductController still receives DeliveryInfoService through its constructor. The 30-product file, product details and report remain unchanged. The preview uses familiar Controller, Model and Thymeleaf concepts.

  3. Keep each scan responsible for one area

    Application uses the default lesson-package scan. It discovers DeliveryScanConfig, whose scan handles the sibling delivery package. Remove the old broad scan from Application so it cannot independently register the excluded controller.

  4. Exclude a type, not a source file

    The ASSIGNABLE_TYPE filter selects DemoDeliveryController for exclusion. Both Java classes compile; only the real delivery service becomes a bean through this scan. Spring MVC therefore has no demo request mapping.

  5. Compare the two running configurations

    Remove only the exclusion, restart and view the demo page. Restore it, restart and expect 404. Both configurations retain the real product page. This is a startup configuration choice, not a checkbox evaluated for each user.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · Windows, macOS and Linux
mvn test
mvn spring-boot:run

Open http://localhost:8118/products. Keep the terminal running. Stop with Ctrl+C. If the port is occupied, edit server.port in application.properties before restarting.

Expected result

Expected output · ignore framework startup messages
Open http://localhost:8118/products/1
Real delivery: 3-5 working days; INR 49
Open http://localhost:8118/demo/delivery
With the supplied exclusion: HTTP 404
Without the exclusion, after restart: DEMO delivery preview; Instant delivery (demo); INR 0
Restore the exclusion: demo returns HTTP 404; product page still works.

Understand each complete file

Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.

pom.xml
pom.xml · complete file
<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-stage-18-scan-filters</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version></properties>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
    <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>
src/main/java/com/edupoly/delivery/DeliveryInfoService.java
src/main/java/com/edupoly/delivery/DeliveryInfoService.java · complete file
package com.edupoly.delivery;

import org.springframework.stereotype.Service;

@Service
public class DeliveryInfoService {
    public String deliveryTime() { return "3-5 working days"; }
    public int deliveryCharge() { return 49; }
}
src/main/java/com/edupoly/lesson/Notifier.java
src/main/java/com/edupoly/lesson/Notifier.java · complete file
package com.edupoly.lesson;

public class Notifier {
    public String message(int productCount) {
        return "Catalog report prepared for " + productCount + " products.";
    }
}
src/main/java/com/edupoly/lesson/Product.java
src/main/java/com/edupoly/lesson/Product.java · complete file
package com.edupoly.lesson;

import java.math.BigDecimal;

public class Product {
    private int id;
    private String name;
    private String category;
    private BigDecimal price;
    private String brand;
    private int stock;
    private String description;
    private int warrantyMonths;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCategory() {
        return category;
    }

    public void setCategory(String category) {
        this.category = category;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getWarrantyMonths() {
        return warrantyMonths;
    }

    public void setWarrantyMonths(int warrantyMonths) {
        this.warrantyMonths = warrantyMonths;
    }
}
src/main/java/com/edupoly/lesson/ProductController.java
src/main/java/com/edupoly/lesson/ProductController.java · complete file
package com.edupoly.lesson;

import com.edupoly.delivery.DeliveryInfoService;
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 ProductService service;

    private final DeliveryInfoService delivery;

    public ProductController(ProductService service, DeliveryInfoService delivery) {
        this.service = service;
        this.delivery = delivery;
    }

    @GetMapping({"/", "/products"})
    public String products(Model model) {
        model.addAttribute("products", service.findAll());
        model.addAttribute("totalProducts", service.count());
        return "products";
    }

    @GetMapping("/products/{id}")
    public String details(@PathVariable("id") int id, Model model) {
        for (Product product : service.findAll()) {
            if (product.getId() == id) {
                model.addAttribute("product", product);
                model.addAttribute("deliveryTime", delivery.deliveryTime());
                model.addAttribute("deliveryCharge", delivery.deliveryCharge());
                return "product-details";
            }
        }
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
    }
}
src/main/java/com/edupoly/lesson/ProductRepository.java
src/main/java/com/edupoly/lesson/ProductRepository.java · complete file
package com.edupoly.lesson;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Repository;
import tools.jackson.databind.json.JsonMapper;

@Repository
public class ProductRepository {
    private final Product[] products;

    public ProductRepository() 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);
        }
    }

    public Product[] findAll() { return products; }

    public List<String> titles() {
        List<String> titles = new ArrayList<>();
        for (Product product : products) {
            titles.add(product.getName());
        }
        return titles;
    }
}
src/main/java/com/edupoly/lesson/ProductService.java
src/main/java/com/edupoly/lesson/ProductService.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Service;

@Service
public class ProductService {
    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Product[] findAll() { return repository.findAll(); }

    public int count() {
        return repository.titles().size();
    }
}
src/main/java/com/edupoly/lesson/ReportConfig.java
src/main/java/com/edupoly/lesson/ReportConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ReportConfig {
    @Bean
    public Notifier notifier() {
        return new Notifier();
    }
}
src/main/java/com/edupoly/lesson/ReportController.java
src/main/java/com/edupoly/lesson/ReportController.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ReportController {
    private final ReportService reports;

    public ReportController(ReportService reports) {
        this.reports = reports;
    }

    @GetMapping("/report")
    public String report(Model model) {
        model.addAttribute("totalProducts", reports.productCount());
        model.addAttribute("reportMessage", reports.message());
        return "report";
    }
}
src/main/java/com/edupoly/lesson/ReportService.java
src/main/java/com/edupoly/lesson/ReportService.java · complete file
package com.edupoly.lesson;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ReportService {
    private final ProductService products;

    public ReportService(ProductService products) {
        this.products = products;
    }

    private Notifier notifier;

    @Autowired(required = false)
    public void setNotifier(Notifier notifier) {
        this.notifier = notifier;
    }

    public int productCount() { return products.count(); }

    public String message() {
        if (notifier == null) return "";
        return notifier.message(productCount());
    }
}
SCAN FILTERsrc/main/java/com/edupoly/lesson/Application.java

Change back to the default lesson-package scan; DeliveryScanConfig handles the delivery package.

src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}
SCAN FILTERsrc/main/java/com/edupoly/lesson/DeliveryScanConfig.java

Scan delivery components while excluding the demo controller.

src/main/java/com/edupoly/lesson/DeliveryScanConfig.java · complete file
package com.edupoly.lesson;

import com.edupoly.delivery.DemoDeliveryController;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
    basePackages = "com.edupoly.delivery",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = DemoDeliveryController.class
    )
)
public class DeliveryScanConfig { }
SCAN FILTERsrc/main/java/com/edupoly/delivery/DemoDeliveryController.java

New demonstration controller with sample delivery terms.

src/main/java/com/edupoly/delivery/DemoDeliveryController.java · complete file
package com.edupoly.delivery;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class DemoDeliveryController {
    @GetMapping("/demo/delivery")
    public String preview(Model model) {
        model.addAttribute("deliveryTime", "Instant delivery (demo)");
        model.addAttribute("deliveryCharge", 0);
        return "demo-delivery";
    }
}
src/main/resources/data/products.txt
src/main/resources/data/products.txt · complete file
[
  {
    "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
  },
  {
    "id": 2,
    "name": "Mechanical Keyboard",
    "category": "Electronics",
    "price": 3499,
    "brand": "EduTech",
    "stock": 19,
    "description": "Mechanical Keyboard for everyday learning, study and work. Product 2 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 3,
    "name": "USB-C Hub",
    "category": "Electronics",
    "price": 1599,
    "brand": "EduTech",
    "stock": 26,
    "description": "USB-C Hub for everyday learning, study and work. Product 3 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 4,
    "name": "Laptop Stand",
    "category": "Electronics",
    "price": 1299,
    "brand": "EduTech",
    "stock": 33,
    "description": "Laptop Stand for everyday learning, study and work. Product 4 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 5,
    "name": "Webcam",
    "category": "Electronics",
    "price": 2199,
    "brand": "EduTech",
    "stock": 40,
    "description": "Webcam for everyday learning, study and work. Product 5 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 6,
    "name": "Bluetooth Speaker",
    "category": "Electronics",
    "price": 2499,
    "brand": "EduTech",
    "stock": 47,
    "description": "Bluetooth Speaker for everyday learning, study and work. Product 6 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 7,
    "name": "Noise-Cancelling Headphones",
    "category": "Electronics",
    "price": 4999,
    "brand": "EduTech",
    "stock": 54,
    "description": "Noise-Cancelling Headphones for everyday learning, study and work. Product 7 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 8,
    "name": "Portable SSD",
    "category": "Electronics",
    "price": 6499,
    "brand": "EduTech",
    "stock": 61,
    "description": "Portable SSD for everyday learning, study and work. Product 8 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 9,
    "name": "USB Flash Drive",
    "category": "Electronics",
    "price": 599,
    "brand": "EduTech",
    "stock": 68,
    "description": "USB Flash Drive for everyday learning, study and work. Product 9 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 10,
    "name": "Monitor",
    "category": "Electronics",
    "price": 12999,
    "brand": "EduTech",
    "stock": 15,
    "description": "Monitor for everyday learning, study and work. Product 10 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 11,
    "name": "Desk Lamp",
    "category": "Electronics",
    "price": 899,
    "brand": "EduTech",
    "stock": 22,
    "description": "Desk Lamp for everyday learning, study and work. Product 11 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 12,
    "name": "Power Bank",
    "category": "Electronics",
    "price": 1799,
    "brand": "EduTech",
    "stock": 29,
    "description": "Power Bank for everyday learning, study and work. Product 12 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 13,
    "name": "Smartphone Stand",
    "category": "Electronics",
    "price": 399,
    "brand": "EduTech",
    "stock": 36,
    "description": "Smartphone Stand for everyday learning, study and work. Product 13 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 14,
    "name": "HDMI Cable",
    "category": "Electronics",
    "price": 499,
    "brand": "EduTech",
    "stock": 43,
    "description": "HDMI Cable for everyday learning, study and work. Product 14 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 15,
    "name": "Ethernet Adapter",
    "category": "Electronics",
    "price": 999,
    "brand": "EduTech",
    "stock": 50,
    "description": "Ethernet Adapter for everyday learning, study and work. Product 15 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 16,
    "name": "Graphics Tablet",
    "category": "Electronics",
    "price": 3999,
    "brand": "EduTech",
    "stock": 57,
    "description": "Graphics Tablet for everyday learning, study and work. Product 16 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 17,
    "name": "Microphone",
    "category": "Electronics",
    "price": 2999,
    "brand": "EduTech",
    "stock": 64,
    "description": "Microphone for everyday learning, study and work. Product 17 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 18,
    "name": "Laptop Backpack",
    "category": "Electronics",
    "price": 1499,
    "brand": "EduTech",
    "stock": 71,
    "description": "Laptop Backpack for everyday learning, study and work. Product 18 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 19,
    "name": "Screen Cleaner",
    "category": "Electronics",
    "price": 249,
    "brand": "EduTech",
    "stock": 18,
    "description": "Screen Cleaner for everyday learning, study and work. Product 19 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 20,
    "name": "Mouse Pad",
    "category": "Electronics",
    "price": 299,
    "brand": "EduTech",
    "stock": 25,
    "description": "Mouse Pad for everyday learning, study and work. Product 20 includes a simple user guide and protective packaging.",
    "warrantyMonths": 12
  },
  {
    "id": 21,
    "name": "Notebook",
    "category": "Study Essentials",
    "price": 149,
    "brand": "StudyWorks",
    "stock": 32,
    "description": "Notebook for everyday learning, study and work. Product 21 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 22,
    "name": "Ballpoint Pen Set",
    "category": "Study Essentials",
    "price": 199,
    "brand": "StudyWorks",
    "stock": 39,
    "description": "Ballpoint Pen Set for everyday learning, study and work. Product 22 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 23,
    "name": "Water Bottle",
    "category": "Study Essentials",
    "price": 599,
    "brand": "StudyWorks",
    "stock": 46,
    "description": "Water Bottle for everyday learning, study and work. Product 23 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 24,
    "name": "Desk Organizer",
    "category": "Study Essentials",
    "price": 449,
    "brand": "StudyWorks",
    "stock": 53,
    "description": "Desk Organizer for everyday learning, study and work. Product 24 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 25,
    "name": "Reading Light",
    "category": "Study Essentials",
    "price": 699,
    "brand": "StudyWorks",
    "stock": 60,
    "description": "Reading Light for everyday learning, study and work. Product 25 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 26,
    "name": "Calculator",
    "category": "Study Essentials",
    "price": 499,
    "brand": "StudyWorks",
    "stock": 67,
    "description": "Calculator for everyday learning, study and work. Product 26 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 27,
    "name": "Whiteboard",
    "category": "Study Essentials",
    "price": 1299,
    "brand": "StudyWorks",
    "stock": 14,
    "description": "Whiteboard for everyday learning, study and work. Product 27 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 28,
    "name": "Marker Set",
    "category": "Study Essentials",
    "price": 299,
    "brand": "StudyWorks",
    "stock": 21,
    "description": "Marker Set for everyday learning, study and work. Product 28 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 29,
    "name": "Book Stand",
    "category": "Study Essentials",
    "price": 799,
    "brand": "StudyWorks",
    "stock": 28,
    "description": "Book Stand for everyday learning, study and work. Product 29 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  },
  {
    "id": 30,
    "name": "Travel Mug",
    "category": "Study Essentials",
    "price": 649,
    "brand": "StudyWorks",
    "stock": 35,
    "description": "Travel Mug for everyday learning, study and work. Product 30 includes a simple user guide and protective packaging.",
    "warrantyMonths": 6
  }
]
src/main/resources/static/style.css
src/main/resources/static/style.css · complete file
body{margin:0;padding:28px 16px;background:#f5f7fb;color:#17253d;font:16px/1.6 system-ui,sans-serif}main{max-width:1100px;margin:20px auto;padding:28px;background:#fff;border:1px solid #dbe2ec;border-radius:12px}h1{margin-top:0}a{color:#2457a6;text-underline-offset:3px}.table-scroll{overflow-x:auto}table{width:100%;border-collapse:collapse}th,td{text-align:left;padding:12px;border-bottom:1px solid #dbe2ec}th{background:#edf3fc}tbody tr:hover{background:#f8faff}.detail{max-width:760px}dl{display:grid;grid-template-columns:160px minmax(0,1fr);gap:12px 20px;margin:24px 0}dt{font-weight:650}dd{margin:0;overflow-wrap:anywhere}@media(max-width:500px){main{padding:16px}dl{grid-template-columns:1fr;gap:4px}dd{margin-bottom:12px}}

.brand{color:#164aa5;font-weight:700}.total-products{display:inline-block;border:3px solid #ef111f;border-radius:4px;padding:12px 18px;background:#fff7f7;color:#14213a;font-size:24px;font-weight:700;margin:4px 0 18px}

.report-action{display:inline-block;background:#164aa5;color:white;padding:10px 16px;border-radius:6px;text-decoration:none}.report-count{font-size:24px;font-weight:700;color:#164aa5}.report-message{background:#fff7f7;border:3px solid #ef111f;border-radius:4px;padding:16px 20px;color:#14213a}

.delivery-info{border:3px solid #ef111f;background:#fff7f7;padding:18px 22px;border-radius:6px;margin:24px 0}.delivery-info h2{margin-top:0}
src/main/resources/templates/product-details.html
src/main/resources/templates/product-details.html · complete file
<!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>
    <section class="delivery-info" id="delivery-info">
        <h2>Delivery information</h2>
        <p>Standard delivery: <span th:text="${deliveryTime}">3-5 working days</span></p>
        <p>Delivery charges: INR <span th:text="${deliveryCharge}">49</span></p>
    </section>
    <a th:href="@{/products}">Back to products</a>
</main>
</body>
</html>
src/main/resources/templates/products.html
src/main/resources/templates/products.html · complete file
<!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>
    <p class="brand">Edupoly · Product catalog</p>
    <h1>Products</h1>
    <p class="total-products" id="total-products">Total products: <strong th:text="${totalProducts}">30</strong></p>
    <p>Select More details to see a product's complete information. The red box shows the number of products in the catalog.</p>
    <p><a class="report-action" th:href="@{/report}">Generate product report</a></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>
src/main/resources/templates/report.html
src/main/resources/templates/report.html · complete file
<!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>Product report</title><link rel="stylesheet" th:href="@{/style.css}"></head>
<body><main>
    <p class="brand">Edupoly · Product catalog</p>
    <h1>Product report</h1>
    <p class="report-count">Total products: <strong th:text="${totalProducts}">30</strong></p>
    <p class="report-message" id="report-message" th:if="${!reportMessage.isEmpty()}" th:text="${reportMessage}">Catalog report prepared.</p>
    <p><a th:href="@{/products}">Back to products</a></p>
</main></body></html>
SCAN FILTERsrc/main/resources/templates/demo-delivery.html

New preview template used only when the demo controller participates.

src/main/resources/templates/demo-delivery.html · complete file
<!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>DEMO delivery preview</title><link rel="stylesheet" th:href="@{/style.css}"></head>
<body><main class="detail">
    <h1>DEMO delivery preview</h1>
    <p>Sample values for demonstrations.</p>
    <section class="delivery-info">
        <h2>Sample delivery terms</h2>
        <p>Standard delivery: <span th:text="${deliveryTime}">Instant delivery (demo)</span></p>
        <p>Delivery charges: INR <span th:text="${deliveryCharge}">0</span></p>
    </section>
    <p><a th:href="@{/products/1}">View real product delivery</a></p>
</main></body></html>
src/main/resources/application.properties
src/main/resources/application.properties · complete file
server.port=8118
spring.application.name=spring-stage-18-scan-filters
src/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
package com.edupoly.lesson;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class LessonTest {
    @Test void filteredScanKeepsRealServiceAndExcludesDemo() {
        try (var context = new AnnotationConfigApplicationContext(DeliveryScanConfig.class)) {
            assertNotNull(context.getBean(com.edupoly.delivery.DeliveryInfoService.class));
            assertTrue(context.getBeansOfType(com.edupoly.delivery.DemoDeliveryController.class).isEmpty());
        }
    }

    @Test void unfilteredScanDiscoversTheCompiledDemoClass() {
        try (var context = new AnnotationConfigApplicationContext()) {
            context.scan("com.edupoly.delivery");
            context.refresh();
            assertNotNull(context.getBean(com.edupoly.delivery.DeliveryInfoService.class));
            assertNotNull(context.getBean(com.edupoly.delivery.DemoDeliveryController.class));
        }
    }

    @Test void filteredBootApplicationKeepsRealPagesAndReturns404ForDemo() throws Exception {
        try (var context = org.springframework.boot.SpringApplication.run(Application.class, "--server.port=0")) {
            assertTrue(context.getBeansOfType(com.edupoly.delivery.DemoDeliveryController.class).isEmpty());
            int port = context.getEnvironment().getProperty("local.server.port", Integer.class);
            var client = java.net.http.HttpClient.newHttpClient();
            String base = "http://127.0.0.1:" + port;
            var product = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/products/1")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(200, product.statusCode());
            assertTrue(product.body().contains("3-5 working days"));
            assertTrue(product.body().contains("<span>49</span>"));
            var demo = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/demo/delivery")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(404, demo.statusCode());
            var report = client.send(java.net.http.HttpRequest.newBuilder(java.net.URI.create(base + "/report")).build(), java.net.http.HttpResponse.BodyHandlers.ofString());
            assertEquals(200, report.statusCode());
            assertTrue(report.body().contains("Catalog report prepared for 30 products."));
        }
    }
}

Try a failure, then correct it

Temporarily remove excludeFilters using the replacement annotation in the guided experiment. The demo page becomes available. Restore the filter to remove its controller bean and route again. Running the supplied tests with the filter removed deliberately fails the exclusion assertions.

Words you can now explain

WordMeaning in this example
excludeFilters — newFilters out selected component candidates within a particular scan.
ASSIGNABLE_TYPE — newMatches the specified class or interface and assignable subtypes.
@ComponentScan — reused and extendedSearches delivery components, now with an exclusion.
@Configuration — reusedRegisters the configuration class that declares the delivery scan.
Controller and Model — reusedPrepare the demo view only when the demo controller is registered.
HTTP 404 — observed resultThe excluded controller contributes no handler for the demo route.

Practice assignment

Change the demo delivery text to Tomorrow (demo), verify it with the filter removed, then restore the filter and confirm 404. Check that the real product still shows 3-5 working days and INR 49. Explain why the source can compile without becoming a bean.

Read the official documentation →