Stage 22: obtain a fresh draft for each operation

Keep the report service shared while obtaining a new draft whenever it starts an operation.

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.

This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.

Shared ReportService → Provider.getObject() → Fresh draft

What we change and why

  1. Reuse the Stage 20 report draft

    ReportDraft still has prototype scope. The new ReportService has default singleton scope.

  2. Resolve the dependency when needed

    Spring injects an ObjectProvider that can resolve ReportDraft later. Each call to getObject() asks the container for a prototype instance. ObjectProvider extends ObjectFactory and adds optional and stream-based lookup operations; only getObject is needed here.

  3. Compare two business operations

    The two calls return different references. A provider does not make every bean fresh: getObject on a singleton definition still returns its shared instance.

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

This is a console lesson. It does not open a web port. Application.main starts a Spring context, calls the demonstration and closes the context. Read the terminal, not a browser. Spring Boot’s Maven plugin is used as a convenient launcher; these examples show Spring Framework directly.

Expected result

Expected output · ignore framework startup messages
Two operations share a draft: false

Object identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.

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-22-fresh-prototype</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>
  </dependencies>
  <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
src/main/java/com/edupoly/lesson/ReportDraft.java
src/main/java/com/edupoly/lesson/ReportDraft.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Component;
import org.springframework.context.annotation.Scope;
@Component @Scope("prototype")
public class ReportDraft { }
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.stereotype.Service;
import org.springframework.beans.factory.ObjectProvider;
@Service public class ReportService {
    private final ObjectProvider<ReportDraft> drafts;
    public ReportService(ObjectProvider<ReportDraft> drafts) { this.drafts = drafts; }
    public ReportDraft draftForOperation() { return drafts.getObject(); }
}
src/main/java/com/edupoly/lesson/Lesson.java
src/main/java/com/edupoly/lesson/Lesson.java · complete file
package com.edupoly.lesson;

import org.springframework.stereotype.Component;
@Component public class Lesson {
    private final ReportService reports;
    public Lesson(ReportService reports) { this.reports = reports; }
    public void run() {
        ReportDraft first = reports.draftForOperation();
        ReportDraft second = reports.draftForOperation();
        System.out.println("Two operations share a draft: " + (first == second));
    }
}
src/main/java/com/edupoly/lesson/Application.java
src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
    public static void main(String[] args) throws Exception {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            context.getBean(Lesson.class).run();
        }
    }
}
src/main/java/com/edupoly/lesson/AppConfig.java
src/main/java/com/edupoly/lesson/AppConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.edupoly.lesson")
public class AppConfig { }
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 operationsHaveExpectedDraftIdentity() {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var service = context.getBean(ReportService.class);
            assertNotSame(service.draftForOperation(), service.draftForOperation());
        }
    }
}

Try a failure, then correct it

Remove prototype scope from ReportDraft. The provider now returns the same singleton. Restore prototype scope.

Words you can now explain

WordMeaning in this example
Prototype dependencyA fresh object is produced whenever this bean is resolved.
Singleton serviceOne service is reused by all calls.
ObjectProviderAn injected handle asks Spring for a bean on demand.

Practice assignment

Add a third operation and verify it gets a third reference. Explain why injecting the provider is different from injecting one draft.

Read the official documentation →