The previous lesson ended by pointing out BiblioTech's biggest debt: a hundred-and-fifty-line SimpleContainer that resolves dependencies by reflection and is missing absolutely everything else. No scopes, no lifecycle, no profiles, no externalised properties, no ambiguity resolution and no transactions.
This lesson replaces it with the container that has been polished for twenty years.
Spring is the dominant back-end framework in Java, and its core is exactly what you wrote: a container that instantiates objects, resolves their dependencies and manages their lifecycle. Everything else —web, persistence, security, messaging, observability— is built on top of that base.
The advantage you arrive with is enormous. You are not going to learn Spring as a list of annotations to memorise: you are going to recognise, one by one, the pieces you already built. When you see @Component, you will see your @Component. When you see how the container resolves the constructor, you will see your getDeclaredConstructors(). And when you reach @Transactional, you will see your AuditProxy — with the very same internal-call trap included.
By the end you will know what the ApplicationContext is and how beans are declared; why constructor injection is the only advisable form and what concrete problems field injection has; what each stereotype adds; how a bean's lifecycle works and which scopes exist; how ambiguities are resolved when there are two implementations; how configuration is externalised with typed properties and profiles; what Spring Boot is and what auto-configuration really is —including how to debug it—; and how AOP works underneath, with the proxies you already know.
And BiblioTech will move from its home-made container to Spring, with dev and prod profiles and typed configuration.
Contents
- What Spring is and what Spring Boot is
- The Spring Framework modules
- The IoC container:
ApplicationContext - What a bean is and how it is declared
- The three forms of injection
- Why constructor injection
- A comparison with the home-made
SimpleContainer - Stereotypes:
@Component,@Service,@Repository,@Controller @Configuration+@Beanversus component scanning- A bean's lifecycle
- Bean scopes
- Ambiguity resolution:
@Qualifier,@Primaryand collections @Valueand externalised properties@ConfigurationProperties: typed, validated configuration- Profiles:
@Profileandspring.profiles.active - BiblioTech: from
BusinessRulesto Spring configuration - Spring Boot: the
pom.xmland the starters @SpringBootApplicationdissected- Auto-configuration, properly explained
- Debugging auto-configuration
CommandLineRunnerandApplicationRunner- The executable jar and
spring-boot-maven-plugin - AOP in Spring: aspects and pointcuts
@Transactional: the canonical aspect- How it works underneath: the proxies you already wrote
- The internal-call trap
- BiblioTech on Spring: the result
- What is NOT covered in this lesson
- Common Mistakes and Tips
- Exercises
- What Spring is and what Spring Boot is
This is everybody's first confusion, and it is worth clearing up before writing a single line.
Spring Framework (version 6.x in this course) is the framework: the IoC container, the data abstraction, aspect-oriented programming, the MVC web model, transaction management. It is the functionality.
Spring Boot (version 3.x) does not replace Spring Framework: it wraps it. It is a layer that solves three problems bare Spring had, problems that made starting a project take a whole day:
| Problem | Spring Boot's solution |
|---|---|
| Choosing compatible versions of 30 dependencies | Starters: one dependency brings a coherent, already-tested set |
Writing 300 lines of XML or @Bean for the same old thing |
Auto-configuration: if it detects H2 on the classpath, it configures the DataSource by itself |
Deploying a .war to an application server |
Embedded server and executable jar: java -jar bibliotech.jar |
A useful analogy: Spring Framework is the engine; Spring Boot is the assembled car, with the engine inside, the wheels on and the key in the ignition. You can use the engine on its own —and some people do— but practically nobody starts a new project that way.
An important detail for reading documentation: everything you learn about Spring Framework applies in Spring Boot. @Component, @Autowired, @Bean and @Transactional belong to Spring Framework. @SpringBootApplication, the starters and auto-configuration belong to Spring Boot. In this lesson you learn the core first (sections 3-16) and then the Boot layer (17-22), because that is the order in which they make sense.
- The Spring Framework modules
Spring is not a monolithic block. These are the modules that matter:
| Module | What it brings | Where it is seen |
|---|---|---|
| spring-core / spring-beans | The IoC container, BeanFactory, injection |
This lesson |
| spring-context | ApplicationContext, events, @Configuration, scheduling |
This lesson |
| spring-aop | Aspect-oriented programming with proxies | This lesson (§23-26) |
| spring-tx | Transaction management, @Transactional |
This lesson and 11-03 |
| spring-jdbc / spring-orm | JdbcTemplate, integration with JPA/Hibernate |
11-03 |
| spring-web / spring-webmvc | HTTP client and server, REST controllers | 12-04 |
| spring-test | @SpringBootTest, MockMvc, the test context |
11-04, 11-06, 12-05 |
The first three are the core, and they are the subject of this lesson.
- The IoC container:
ApplicationContext
ApplicationContextThe heart of Spring is the container. Its job is exactly your SimpleContainer's:
- Work out which objects have to be created.
- Work out what each one needs.
- Create them in the right order, injecting the dependencies.
- Store them and hand them over when they are asked for.
- Destroy them in an orderly way on shutdown.
In Spring, that container is an ApplicationContext. You can create it by hand to watch it work:
package com.nexussoftware.bibliotech;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.nexussoftware.bibliotech.service.LoanManager;
public class ManualStartup {
public static void main(String[] args) {
// 1. The container is created, telling it where to look for components.
var context = new AnnotationConfigApplicationContext(
"com.nexussoftware.bibliotech");
// 2. A bean is requested by type. Spring has already built it, with
// its dependencies resolved recursively.
LoanManager manager = context.getBean(LoanManager.class);
manager.lend("978-0000000001", "Marta Ruiz");
// 3. On close, the destruction methods of every bean are executed.
context.close();
}
}Three lines, and there is the whole mechanism. Compare it with your container:
// Your SimpleContainer from 10-03
var container = new SimpleContainer("com.nexussoftware.bibliotech");
LoanManager manager = container.get(LoanManager.class);It is the same API. What changes is everything underneath.
A vocabulary nuance that shows up in the documentation: BeanFactory is the container's base interface (create beans, inject). ApplicationContext extends BeanFactory and adds internationalisation, events, resource loading and AOP integration. In practice you always use ApplicationContext.
- What a bean is and how it is declared
A bean is, quite simply, an object managed by the Spring container. Nothing more. It is not a special class, it implements no interface, it inherits from nothing. It is a normal object that Spring creates instead of you creating it with new.
There are two ways of declaring them, and both are used:
Form 1: annotate the class (for your own classes)
package com.nexussoftware.bibliotech.service;
import org.springframework.stereotype.Service;
@Service // "Spring, this is one of your beans"
public class FineCalculator {
// ...
}Form 2: a @Bean method in a @Configuration class (for third-party classes you cannot annotate)
package com.nexussoftware.bibliotech.config;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClockConfiguration {
@Bean
public Clock clock() {
// Clock belongs to the JDK: you cannot put @Component on it.
return Clock.systemDefaultZone();
}
}That Clock is exactly the one from 10-05, the one you made injectable so it could be tested. Now Spring manages it, and in 11-04 you will replace it with a Clock.fixed in the tests without touching a line of the logic.
The practical rule: @Component (and its derivatives) for your classes; @Bean for what you cannot annotate (JDK classes, external library classes, or when construction requires logic).
- The three forms of injection
Spring can inject dependencies in three ways. All three work; only one is advisable.
Constructor injection (the recommended one)
package com.nexussoftware.bibliotech.service;
import org.springframework.stereotype.Service;
import com.nexussoftware.bibliotech.persistence.LoanRepository;
@Service
public class LoanManager {
private final LoanRepository repository;
private final FineCalculator calculator;
private final NoticeService notices;
// Since Spring 4.3: if there is only ONE constructor, @Autowired is optional.
public LoanManager(LoanRepository repository,
FineCalculator calculator,
NoticeService notices) {
this.repository = repository;
this.calculator = calculator;
this.notices = notices;
}
}Notice that there is not a single Spring annotation on the constructor. That class is plain Java: it can be instantiated with new in a test without starting anything.
Setter injection
@Service
public class LoanManager {
private LoanRepository repository; // cannot be final
@Autowired
public void setRepository(LoanRepository repository) {
this.repository = repository;
}
}Its only legitimate use is for genuinely optional dependencies, which are rare.
Field injection (the one to avoid)
@Service
public class LoanManager {
@Autowired
private LoanRepository repository; // tempting, and a bad idea!
}It is the shortest to write, and that is why it is everywhere in old tutorials. Its problems are concrete, not aesthetic:
- It cannot be tested without Spring or without reflection. The field is private and there is no constructor or setter. For a unit test you have to start the whole context or use
ReflectionTestUtils. With a constructor, it isnew LoanManager(mock1, mock2, mock3). - You cannot use
final. You lose immutability and the guarantee that the dependency does not change. - It hides an excess of dependencies. A constructor with nine parameters shouts "this class does too much". Nine annotated fields catch nobody's eye. The ugly constructor is useful information.
- It allows inconsistent states. With field injection, the object exists for an instant constructed but without dependencies. If anything runs there, you get a
NullPointerException. - It couples the class to Spring.
@Autowiredon the field makes the class work only inside a container.
Compared:
| Aspect | Constructor | Setter | Field |
|---|---|---|---|
Allows final |
Yes | No | No |
| Object always valid after construction | Yes | No | No |
Can be instantiated in a test with new |
Yes | Yes | No |
| Detects circular dependencies | At start-up, with a clear error | Hides them | Hides them |
| Makes an excess of dependencies visible | Yes | No | No |
| Class independent of Spring | Yes | No | No |
| Spring's official recommendation | Yes | Optional ones | Discouraged |
Circular dependencies
A relevant detail: with constructor injection, if A needs B and B needs A, start-up fails with an explicit message. That is good: a cycle is a design problem and you want to know about it. With field injection the cycle is resolved silently and the problem stays buried.
Your SimpleContainer also detected cycles and threw an exception — for the same reason and with the same logic.
- Why constructor injection
Summed up in one sentence: because it produces classes that are plain Java.
// Unit test (you will see this in 11-04 and 11-06). No Spring, no context, in 3 ms.
var manager = new LoanManager(
mockRepository,
new FineCalculator(Clock.fixed(...)),
mockNotices);If the class can be built like that, it can be tested. If it cannot, it cannot. That is the whole argument, and it is enough.
- A comparison with the home-made
SimpleContainer
SimpleContainerIt is worth seeing the leap explicitly, with your code alongside.
| Aspect | Your SimpleContainer |
Spring |
|---|---|---|
| Marking a component | @Component |
@Component, @Service, @Repository, @Controller |
| Scanning | You walked the package with reflection | @ComponentScan (implicit in @SpringBootApplication) |
| Injection | A single mandatory constructor | Constructor, setter or field |
| Resolution | Recursive, by type | By type, with tie-breaking by name, @Qualifier and @Primary |
| Cycles | Your own exception | BeanCurrentlyInCreationException with the full chain |
| Cache | A HashMap<Class<?>, Object> |
A singleton registry with three cache levels |
| Scopes | Singleton only | 6 scopes |
| Lifecycle | None | @PostConstruct, @PreDestroy, InitializingBean, DisposableBean, BeanPostProcessor |
| External configuration | None | @Value, @ConfigurationProperties, profiles, a source hierarchy |
| Aspects | Three proxies applied by hand | AOP built in and transparent |
| Errors | NullPointerException or a generic exception |
Messages that say which bean is missing, where it was requested and which candidates exist |
That last point is always underrated and it is the one that saves the most time in practice. A typical Spring error:
Parameter 0 of constructor in com.nexussoftware.bibliotech.service.LoanManager
required a bean of type 'com.nexussoftware.bibliotech.persistence.LoanRepository'
that could not be found.
Action:
Consider defining a bean of type 'LoanRepository' in your configuration.It tells you the bean, the constructor, the parameter and what to do.
- Stereotypes:
@Component, @Service, @Repository, @Controller
@Component, @Service, @Repository, @ControllerSpring offers four annotations for marking components. Everybody's first question is: how do they differ?
Technically, very little. @Service, @Repository and @Controller are themselves annotated with @Component. They are meta-annotations, exactly the mechanism you studied in 10-02:
// Real Spring code, simplified
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component // <-- @Service IS a @Component
public @interface Service {
@AliasFor(annotation = Component.class)
String value() default "";
}The real differences:
| Annotation | Layer | What it adds beyond being a bean |
|---|---|---|
@Component |
Generic | Nothing. It is the base |
@Service |
Business logic | Nothing technical. Semantics: it marks the service layer |
@Repository |
Data access | Exception translation: it converts technology-specific exceptions (SQLException, Hibernate exceptions) into Spring's DataAccessException hierarchy |
@Controller |
Web presentation | It makes the class eligible for Spring MVC request mapping |
@Repository's exception translation is real, highly relevant functionality: thanks to it, your service layer does not depend on whether JDBC, Hibernate or JPA sits underneath. It is a textbook case of isolating the technology, and it connects with the layered strategies from module 6.
Applied to BiblioTech:
@Service // business layer
public class LoanManager { ... }
@Service
public class FineCalculator { ... }
@Repository // data layer
public class JpaLoanRepository implements LoanRepository { ... }
@Component // generic infrastructure: it fits no layer
public class OperationLog { ... }A tip: use the right stereotype even though technically they make no difference. It is free documentation, and some tools and aspects rely on them.
@Configuration + @Bean versus component scanning
@Configuration + @Bean versus component scanningThere are two styles of telling Spring which beans exist, and in a real project they coexist.
Component scanning
Spring walks the classpath under that package, looks for classes with @Component or its derivatives and registers them. It is what @SpringBootApplication does implicitly over its own package.
- In favour: zero configuration. You add a class with
@Serviceand it is already a bean. - Against: the dependencies are implicit. To know which beans exist, you have to hunt for annotations all over the code.
Explicit configuration with @Bean
package com.nexussoftware.bibliotech.config;
import java.time.Clock;
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.context.annotation.*;
@Configuration
public class BiblioTechConfiguration {
@Bean
public Clock clock() {
return Clock.systemDefaultZone();
}
@Bean
public HttpClient httpClient() {
// The HttpClient from 09-06: expensive to create, it must be reused.
// As a Spring singleton, it is created ONCE.
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
@Bean
public MetadataClient metadataClient(HttpClient http,
@Value("${bibliotech.metadata.url}") String url) {
// The parameters of a @Bean method are OTHER beans: Spring injects them.
return new MetadataClient(http, url);
}
}Three things to learn from this block:
- The method name is the bean name (
clock,httpClient,metadataClient). - The parameters of a
@Beanmethod are injected just like a constructor's. - Here a real problem from 09-06 is finally solved: the
HttpClientis expensive to create and had to be reused. As a container singleton, it is created just once and injected wherever it is needed.
A surprising detail: @Configuration's full mode
@Configuration
public class Config {
@Bean
public Clock clock() { return Clock.systemDefaultZone(); }
@Bean
public FineCalculator calculator() {
return new FineCalculator(clock()); // direct call to another @Bean
}
@Bean
public NoticeService notices() {
return new NoticeService(clock()); // a different Clock?
}
}Intuitively, clock() is called twice and there are two clocks. That is not what happens. Spring generates a CGLIB subclass of the @Configuration class that intercepts calls to @Bean methods and returns the singleton already created. There is one single Clock.
And there, once again, is your dynamic proxy from 10-03 — this time by subclass generation instead of by interface. This is also why a @Configuration class cannot be final.
| Style | When to use it |
|---|---|
@Component + scanning |
Your domain classes, services and repositories |
@Configuration + @Bean |
Third-party or JDK classes, or construction with conditional logic |
- A bean's lifecycle
A bean does not merely "exist". It goes through a sequence of phases, and you can hook into several of them.
graph TD
A["Container start-up"] --> B["Reading of bean definitions<br/>(scanning + @Bean)"]
B --> C["Instantiation<br/>(the constructor is called)"]
C --> D["Dependency injection<br/>(setters and fields)"]
D --> E["Aware: BeanNameAware,<br/>ApplicationContextAware"]
E --> F["BeanPostProcessor<br/>before initialisation"]
F --> G["@PostConstruct"]
G --> H["afterPropertiesSet<br/>(InitializingBean)"]
H --> I["BeanPostProcessor<br/>after initialisation<br/>THIS IS WHERE AOP PROXIES ARE CREATED"]
I --> J["BEAN READY AND IN USE"]
J --> K["context.close()"]
K --> L["@PreDestroy"]
L --> M["destroy (DisposableBean)"]
M --> N["Bean destroyed"]
Two observations worth their weight in gold:
- Constructor injection happens at instantiation; setter and field injection happen afterwards. That is why with field injection there is an instant when the object exists incomplete.
- AOP proxies are created in the last
BeanPostProcessor. That is the exact point at which@Transactionalstops being a label and becomes a wrapper. Remember it for section 26.
In practice you will only use two hooks, and both belong to Jakarta, not to Spring:
package com.nexussoftware.bibliotech.persistence;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Repository;
@Repository
public class InMemoryCatalog {
private final Map<String, Material> byIsbn = new ConcurrentHashMap<>();
@PostConstruct
public void loadInitialCatalog() {
// Runs AFTER injection: here every dependency is available.
// In the constructor they might not be.
byIsbn.putAll(importer.importFromResource("initial-catalog.csv"));
log.info("Catalogue loaded with {} materials", byIsbn.size());
}
@PreDestroy
public void saveState() {
// Runs when the context is closed in an orderly way.
// It replaces the GracefulShutdown with shutdown hooks from 08-05.
writer.dump(byIsbn.values());
log.info("Catalogue persisted before shutdown");
}
}And there another piece is settled: the GracefulShutdown you wrote with shutdown hooks in module 8 is no longer needed. The container closes beans in the reverse order of creation, which is exactly what you want: a service closes before the repository it depends on.
An important rule for the constructor: do no heavy work in it. No connecting to databases, reading files or calling the network. That is what @PostConstruct is for.
- Bean scopes
A scope defines how many instances the container creates and how long they live.
| Scope | Instances | Lifetime | When to use it |
|---|---|---|---|
singleton (default) |
One per container | The whole application lifecycle | Almost always: services, repositories, configuration |
prototype |
A new one on every request for the bean | Managed by whoever uses it | Objects with unshareable mutable state |
request |
One per HTTP request | The request | Data for the current request (web only) |
session |
One per HTTP session | The user's session | Basket, preferences (web only) |
application |
One per ServletContext |
The web application | Rare |
websocket |
One per WebSocket session | The session | Rare |
@Service
@Scope("prototype")
public class MonthlyReport {
// Every time it is requested, a new object is obtained.
}And here is the most important consequence of all, which links straight back to module 8:
Singleton beans are shared by every thread. They must be stateless or thread-safe.
// BAD: mutable state in a singleton
@Service
public class LoanCounter {
private int total = 0; // shared by ALL threads
public void record() { total++; } // race condition (08-04)
}
// GOOD: stateless
@Service
public class FineCalculator {
private final Clock clock; // final, immutable
public BigDecimal calculate(Loan l) { ... } // local variables only
}
// GOOD: state, but thread-safe (08-06)
@Service
public class LoanCounter {
private final AtomicLong total = new AtomicLong();
public void record() { total.incrementAndGet(); }
}This is one of the most frequent and hardest-to-diagnose errors in Spring applications: it works perfectly in development with one user and gives inconsistent results in production with fifty. Everything you learned in module 8 applies directly here.
A second classic trap: injecting a prototype inside a singleton. Since the singleton is built only once, it receives one single instance of the prototype and uses it forever. The usual solution is to inject an ObjectProvider<T> and ask for a new instance when needed:
@Service
public class ReportGenerator {
private final ObjectProvider<MonthlyReport> provider;
public ReportGenerator(ObjectProvider<MonthlyReport> provider) {
this.provider = provider;
}
public void generate() {
MonthlyReport report = provider.getObject(); // a NEW instance
// ...
}
}
- Ambiguity resolution:
@Qualifier, @Primary and collections
@Qualifier, @Primary and collectionsSpring injects by type. What happens if there are two implementations?
public interface NoticeService {
void notify(Employee recipient, String message);
}
@Service
public class EmailNoticeService implements NoticeService { ... }
@Service
public class SmsNoticeService implements NoticeService { ... }And somebody asks for a NoticeService. Start-up fails:
Parameter 2 of constructor in ...LoanManager required a single bean,
but 2 were found:
- emailNoticeService
- smsNoticeServiceYour SimpleContainer failed the same way, but without telling you the candidates. There are four solutions, and each has its case.
@Primary: the default candidate
Anyone asking for a plain NoticeService will get the email one. Useful when there is a clearly principal implementation.
@Qualifier: choosing explicitly
@Service("smsNotices")
public class SmsNoticeService implements NoticeService { ... }
@Service
public class LoanManager {
public LoanManager(@Qualifier("smsNotices") NoticeService notices) { ... }
}A qualifier of your own (cleaner and verifiable)
This picks up the meta-annotations from 10-02 directly:
@Qualifier("sms")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
public @interface BySms { }
@Service @BySms
public class SmsNoticeService implements NoticeService { ... }
// Usage: no magic strings, with compiler checking
public LoanManager(@BySms NoticeService notices) { ... }Better than @Qualifier("sms") because a misspelled string fails at runtime; a misspelled annotation name does not compile.
Injecting every implementation
Sometimes you do not want to choose: you want them all. Spring injects a List with every bean of the type.
@Service
public class MultiChannelNotifier {
private final List<NoticeService> channels;
public MultiChannelNotifier(List<NoticeService> channels) {
this.channels = channels; // email, SMS and whatever is added tomorrow
}
public void notifyThroughEveryChannel(Employee recipient, String message) {
channels.forEach(channel -> channel.notify(recipient, message));
}
}This is genuinely powerful: adding a new channel means creating a class with @Service. Not a line of the notifier changes. It is the open/closed principle made real, and it is a pattern you will see a lot (strategies, validators, event handlers).
It also works with Map<String, NoticeService>, where the key is the bean name, and it can be ordered with @Order or by implementing Ordered.
| Mechanism | When |
|---|---|
@Primary |
There is an obvious principal implementation |
@Qualifier("name") |
You choose explicitly, in a one-off case |
| Your own qualifier | You choose explicitly and want compiler checking |
List<T> / Map<String,T> |
You want every implementation |
@Profile |
The choice depends on the environment (§15) |
@Value and externalised properties
@Value and externalised propertiesNo application should carry hard-coded configuration. In 07-07 you solved this with Properties and your Configuration and BusinessRules classes. Spring takes it much further.
An application.properties in src/main/resources:
bibliotech.name=BiblioTech - Nexus Software
bibliotech.loan.default-days=15
bibliotech.loan.max-per-employee=3
bibliotech.fine.euros-per-day=0.50
bibliotech.fine.max=20.00
bibliotech.metadata.url=https://api.example.com/books
bibliotech.notices.enabled=trueOr the same content in application.yml, more readable when there is a hierarchy:
bibliotech:
name: BiblioTech - Nexus Software
loan:
default-days: 15
max-per-employee: 3
fine:
euros-per-day: 0.50
max: 20.00
metadata:
url: https://api.example.com/books
notices:
enabled: trueThey are read with @Value:
@Service
public class FineCalculator {
private final BigDecimal eurosPerDay;
private final BigDecimal maxFine;
private final Clock clock;
public FineCalculator(
@Value("${bibliotech.fine.euros-per-day}") BigDecimal eurosPerDay,
@Value("${bibliotech.fine.max:20.00}") BigDecimal maxFine,
Clock clock) {
this.eurosPerDay = eurosPerDay;
this.maxFine = maxFine;
this.clock = clock;
}
}Important details:
- Automatic type conversion: the string
"0.50"arrives as aBigDecimal. Spring converts toint,boolean,Duration,List<String>, enums and many more. - A default value with
:—${property:defaultValue}. Without it, if the property is missing, start-up fails. That is usually the right thing: better not to start than to start misconfigured.
Where the properties come from
Spring Boot looks in several sources with a defined precedence. From highest to lowest priority, simplified:
| Priority | Source | Example |
|---|---|---|
| 1 | Command-line arguments | --bibliotech.fine.euros-per-day=0.75 |
| 2 | Environment variables | BIBLIOTECH_FINE_EUROSPERDAY=0.75 |
| 3 | System properties | -Dbibliotech.fine.euros-per-day=0.75 |
| 4 | application-{profile}.properties |
application-prod.properties |
| 5 | application.properties |
The general one |
| 6 | Default values in the code | ${prop:0.50} |
This hierarchy is what makes modern deployment possible: the same container image in development, staging and production, changing only environment variables. It is developed in 12-06.
Security note: passwords and API keys never go in
application.propertiesinside the repository. They go in environment variables or in a secrets manager. This is covered in 12-07.
@ConfigurationProperties: typed, validated configuration
@ConfigurationProperties: typed, validated configuration@Value works, but with fifteen properties it becomes noisy and it validates nothing. The modern alternative groups the configuration into an object.
package com.nexussoftware.bibliotech.config;
import java.math.BigDecimal;
import jakarta.validation.constraints.*;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record BiblioTechProperties(
@NotBlank String name,
@NotNull Loan loan,
@NotNull Fine fine) {
public record Loan(
@Min(1) @Max(90) int defaultDays,
@Min(1) @Max(10) int maxPerEmployee) { }
public record Fine(
@DecimalMin("0.0") BigDecimal eurosPerDay,
@DecimalMin("0.0") BigDecimal max) { }
}It is enabled on the main class:
@SpringBootApplication
@ConfigurationPropertiesScan // detects the @ConfigurationProperties
public class BiblioTechApplication { ... }And it is injected like any other bean:
@Service
public class FineCalculator {
private final BiblioTechProperties properties;
private final Clock clock;
public FineCalculator(BiblioTechProperties properties, Clock clock) {
this.properties = properties;
this.clock = clock;
}
public BigDecimal calculate(Loan loan) {
LocalDate today = LocalDate.now(clock);
long daysLate = ChronoUnit.DAYS.between(loan.dueDate(), today);
if (daysLate <= 0) return BigDecimal.ZERO;
BigDecimal fine = properties.fine().eurosPerDay()
.multiply(BigDecimal.valueOf(daysLate));
return fine.min(properties.fine().max());
}
}Advantages over @Value:
| Aspect | @Value |
@ConfigurationProperties |
|---|---|---|
| Grouping | Property by property | A whole object with hierarchy |
| Typing | Yes | Yes, and structured |
| Validation | No | Yes, Jakarta Bean Validation |
| Relaxed binding | No | Yes (default-days = defaultDays = DEFAULT_DAYS) |
| IDE auto-completion | No | Yes (with the metadata processor) |
| Failure when the configuration is invalid | At the point of use | At start-up |
That last row is the decisive one. If somebody deploys with max-per-employee=0, you want the application not to start, with a clear message, rather than employees being unable to borrow books and nobody knowing why:
Binding to target ...BiblioTechProperties failed:
Property: bibliotech.loan.max-per-employee
Value: "0"
Reason: must be greater than or equal to 1Notice as well that the validation block is exactly what your AnnotatedValidator from 10-03 did: annotations read by reflection that check a field's value. The difference is that this version is standardised, covers thirty cases and produces translated messages.
- Profiles:
@Profile and spring.profiles.active
@Profile and spring.profiles.activeA profile is a named environment. It allows different beans and properties to exist depending on where the application runs.
Per-profile files, all in src/main/resources:
# application.properties (common to all)
bibliotech.name=BiblioTech - Nexus Software
bibliotech.loan.default-days=15# application-dev.properties
bibliotech.fine.euros-per-day=0.10
bibliotech.notices.enabled=false
logging.level.com.nexussoftware=DEBUG
spring.datasource.url=jdbc:h2:mem:bibliotech# application-prod.properties
bibliotech.fine.euros-per-day=0.50
bibliotech.notices.enabled=true
logging.level.com.nexussoftware=INFO
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}Look at the last part: in production the credentials are not in the file, they are read from environment variables.
A profile is activated in three ways:
# 1. Command-line argument
java -jar bibliotech.jar --spring.profiles.active=prod
# 2. Environment variable (the usual one in containers)
export SPRING_PROFILES_ACTIVE=prod
# 3. In application.properties (only for the development default)
# spring.profiles.active=devAnd different beans depending on the profile:
@Service
@Profile("dev")
public class ConsoleNoticeService implements NoticeService {
// In development no emails are sent: they are printed.
public void notify(Employee recipient, String message) {
System.out.printf("[SIMULATED NOTICE] %s -> %s%n", recipient.email(), message);
}
}
@Service
@Profile("prod")
public class EmailNoticeService implements NoticeService {
// In production they are really sent.
}This solves a very real problem: during development, nobody accidentally sends fine notices to colleagues. And there is no if (isDevelopment) anywhere in the code.
A warning: do not overuse profiles. If you have eight cross-cutting profiles (dev, prod, test, noEmail, withCache...) the combinatorics become unmanageable and nobody knows which beans are active. Two or three, and everything else through properties.
- BiblioTech: from
BusinessRules to Spring configuration
BusinessRules to Spring configurationThe complete before and after. This is what you had in 07-07:
// BEFORE (module 7): hand-written Properties
public final class BusinessRules {
private static final Properties PROPERTIES = new Properties();
static {
try (var input = BusinessRules.class.getResourceAsStream("/rules.properties")) {
PROPERTIES.load(input);
} catch (IOException e) {
throw new InitialisationException("Could not load the rules", e);
}
}
public static int loanDays() {
return Integer.parseInt(PROPERTIES.getProperty("loan.days", "15"));
}
public static BigDecimal finePerDay() {
return new BigDecimal(PROPERTIES.getProperty("fine.euros.day", "0.50"));
}
}The problems, all of them real:
static: it cannot be replaced in a test. It is exactly the "untestable code" 11-04 is going to analyse.- No validation:
loan.days=abcthrows aNumberFormatExceptionthe day somebody requests a loan, not at start-up. - No environments: one single file for development and production.
- Manual conversion on every access:
Integer.parseIntandnew BigDecimalon every call. - A
staticblock that can fail: if the file is missing, the error shows up as an incomprehensibleExceptionInInitializerError.
And this is what you have now:
// AFTER: an immutable record, validated, injectable and per environment
@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record BiblioTechProperties(
@NotBlank String name,
@NotNull Loan loan,
@NotNull Fine fine) { ... }| Aspect | BusinessRules (module 7) |
BiblioTechProperties (Spring) |
|---|---|---|
| Access | static, global |
Injected, replaceable |
| Types | Converted manually every time | Converted once at start-up |
| Validation | None | Jakarta Validation, at start-up |
| Environments | One | As many profiles as needed |
| Overriding at deployment | Impossible without recompiling | Environment variables |
| Testable | No | Yes, it is built with new |
| Failure with invalid configuration | At runtime, late | At start-up, with a clear message |
- Spring Boot: the
pom.xml and the starters
pom.xml and the startersNow for the Boot layer. This is BiblioTech's minimal pom.xml (Maven is explained in depth in 11-05; here only what is needed to run it):
<?xml version="1.0" encoding="UTF-8"?>
<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>
<!-- The "parent" provides the BOM: it pins compatible versions of EVERYTHING -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>
<groupId>com.nexussoftware</groupId>
<artifactId>bibliotech</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>BiblioTech</name>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Core: IoC container, auto-configuration, logging -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Validation: @NotNull, @Min... for @ConfigurationProperties -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- AOP: needed for @Transactional and your own aspects -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Testing: JUnit 5, AssertJ, Mockito. Used in 11-04 and 11-06 -->
<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>Notice that no dependency carries a <version>. They are managed by spring-boot-starter-parent, which guarantees that the versions of Spring, Jackson, Logback, Tomcat and everything else are compatible with each other. Pinning a version by hand there takes that job away from it and risks a conflict (explained in 11-05).
What a starter is
A starter is a dependency that contains no code: it merely declares a coherent set of dependencies. spring-boot-starter brings:
| Artefact it drags in | What for |
|---|---|
spring-boot |
The start-up |
spring-boot-autoconfigure |
All the auto-configuration |
spring-core, spring-context, spring-beans |
The IoC container |
spring-boot-starter-logging |
SLF4J + Logback (you already have it! — 11-07) |
snakeyaml |
Reading application.yml |
The most common starters:
| Starter | What it brings |
|---|---|
spring-boot-starter |
Core, auto-configuration, logging |
spring-boot-starter-web |
MVC, REST, embedded Tomcat, Jackson (12-04) |
spring-boot-starter-data-jpa |
Hibernate, JPA, Spring Data, transactions (11-03) |
spring-boot-starter-test |
JUnit 5, Mockito, AssertJ, spring-test (11-04, 11-06) |
spring-boot-starter-validation |
Jakarta Bean Validation |
spring-boot-starter-aop |
AspectJ and proxies |
spring-boot-starter-actuator |
Metrics and health probes (12-07) |
spring-boot-starter-security |
Authentication and authorisation (12-07) |
This is an excellent moment to run what section 15 of the previous lesson showed:
You will see that a thirty-line pom.xml has brought in some forty jars. And you will see with your own eyes that Jackson, SLF4J and Logback are already there, before 11-07 explains them.
@SpringBootApplication dissected
@SpringBootApplication dissectedBiblioTech's main class:
package com.nexussoftware.bibliotech;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
@ConfigurationPropertiesScan
public class BiblioTechApplication {
public static void main(String[] args) {
SpringApplication.run(BiblioTechApplication.class, args);
}
}Those two lines of main start everything. @SpringBootApplication is a meta-annotation exactly equivalent to three others:
@Configuration // this class can declare @Bean methods
@EnableAutoConfiguration // switches on auto-configuration
@ComponentScan // scans THIS package and its subpackages
public class BiblioTechApplication { }| Annotation | What it does |
|---|---|
@SpringBootConfiguration |
Marks the class as a source of bean definitions (a specialised @Configuration) |
@EnableAutoConfiguration |
Switches on the mechanism from section 19 |
@ComponentScan |
Scans the class's package and all its subpackages |
From that last row comes a practical rule that saves hours of bewilderment:
The main class must live in the root package.
com.nexussoftware.bibliotech.BiblioTechApplicationscans.domain,.service,.persistence,.network,.presentation. If you put it in.presentation, it would find no service and start-up would fail with "bean not found".
What SpringApplication.run does internally, in order:
- It works out the application type (console, servlet web, reactive web) from what is on the classpath.
- It creates the appropriate
ApplicationContext. - It loads the property sources (
application.properties, profiles, environment, arguments). - It registers the bean definitions: scanning +
@Bean+ auto-configuration. - It instantiates every singleton, injecting dependencies.
- It runs
@PostConstructand creates the AOP proxies. - It starts the embedded server if there is one.
- It runs the
CommandLineRunnerandApplicationRunnerbeans. - It registers a shutdown hook to close the context in an orderly way.
- Auto-configuration, properly explained
This is the point where Spring Boot looks like magic, and where it stops looking like it.
The problem it solves. Configuring a DataSource by hand in "classic" Spring is about forty lines: creating the connection pool, configuring the EntityManagerFactory, the transaction manager, the dialect... And in 95% of projects, those forty lines are identical.
The idea. If Spring Boot can detect what you have on the classpath and what you have configured, it can supply the obvious configuration for you — and step aside as soon as you define your own.
The mechanism, in three pieces.
Piece 1: a catalogue of candidate configurations. The spring-boot-autoconfigure artefact contains a text file:
with about 150 class names, one per line:
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
...@EnableAutoConfiguration reads that file. It is a catalogue of "things that could be configured".
Piece 2: conditions. Every candidate is full of @Conditional annotations, which decide whether it applies or not. A simplified real example:
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // <-- THE KEY
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}The most important conditions:
| Condition | Holds when |
|---|---|
@ConditionalOnClass |
The class is on the classpath (that is: you have the dependency) |
@ConditionalOnMissingClass |
The class is not there |
@ConditionalOnBean |
A bean of that type already exists |
@ConditionalOnMissingBean |
NO bean of that type exists: "only if you have not done it" |
@ConditionalOnProperty |
A property has a certain value |
@ConditionalOnWebApplication |
It is a web application |
@ConditionalOnMissingFilterBean |
No filter of that type is registered |
Piece 3: the ordering. Auto-configurations are evaluated after your beans. That is why @ConditionalOnMissingBean works: when it is evaluated, your definitions are already registered.
And from that comes Spring Boot's golden rule:
Auto-configuration never overrides you. If you define a bean, its own is not created.
A concrete example with H2, the one you will use in 11-03:
graph TD
A["Spring Boot starts"] --> B{"Is the DataSource class<br/>on the classpath?"}
B -->|No| Z["It does nothing"]
B -->|Yes| C{"Have you defined<br/>a DataSource bean?"}
C -->|Yes| Y["YOURS is used.<br/>Auto-configuration steps aside"]
C -->|No| D{"Is there an embedded DB<br/>(H2, HSQL, Derby)?"}
D -->|Yes| E["It creates an in-memory H2<br/>DataSource, with no configuration"]
D -->|No| F["It uses spring.datasource.url<br/>and the HikariCP pool"]
That diagram explains why, in 11-03, simply adding the H2 dependency will be enough to have a working database without writing a line of configuration. It is not magic: it is two conditions.
- Debugging auto-configuration
When something configures itself and it was not what you wanted, or when it does not get configured and you do not know why, Spring Boot gives you the full report:
or in application.properties:
Output (trimmed):
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
DataSourceAutoConfiguration matched:
- @ConditionalOnClass found required classes 'javax.sql.DataSource',
'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType'
DataSourceAutoConfiguration#dataSource matched:
- @ConditionalOnMissingBean (types: javax.sql.DataSource;
SearchStrategy: all) did not find any beans
Negative matches:
-----------------
MongoAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'com.mongodb.client.MongoClient'
Exclusions:
-----------
None
Unconditional classes:
----------------------
...ConfigurationPropertiesAutoConfigurationHow to read it:
- Positive matches: what has been auto-configured and why.
- Negative matches: what has not, and which condition failed. Here lies the answer to most "why doesn't it work?" questions.
And to see the final result, the container itself:
@Bean
CommandLineRunner listBeans(ApplicationContext context) {
return args -> Arrays.stream(context.getBeanDefinitionNames())
.sorted()
.filter(name -> name.startsWith("bibliotech") || name.contains("nexus"))
.forEach(System.out::println);
}If you need to switch off a specific auto-configuration:
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class BiblioTechApplication { }
CommandLineRunner and ApplicationRunner
CommandLineRunner and ApplicationRunnerAnd where does the code you want to run at start-up go? Not in main: when SpringApplication.run returns the context is already assembled, but the idiomatic place is a runner.
package com.nexussoftware.bibliotech.presentation;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class BiblioTechStartup implements CommandLineRunner {
private final LoanManager manager;
private final BiblioTechProperties properties;
public BiblioTechStartup(LoanManager manager, BiblioTechProperties properties) {
this.manager = manager;
this.properties = properties;
}
@Override
public void run(String... args) {
System.out.println("=== " + properties.name() + " ===");
System.out.println("Loan days: " + properties.loan().defaultDays());
manager.lend("978-0000000001", "Marta Ruiz");
manager.lend("978-0000000002", "Diego Alonso");
manager.activeLoans()
.forEach(l -> System.out.printf(" %s -> %s (due %s)%n",
l.isbn(), l.employee(), l.dueDate()));
}
}It runs after the context is completely ready. The difference from ApplicationRunner:
| Interface | Signature | When |
|---|---|---|
CommandLineRunner |
run(String... args) |
Raw arguments |
ApplicationRunner |
run(ApplicationArguments args) |
Already-parsed arguments (--key=value, options) |
If there are several, they are ordered with @Order. And a note: if a runner throws an exception, the application does not start. That is usually what you want.
- The executable jar and
spring-boot-maven-plugin
spring-boot-maven-pluginThe plugin declared in the pom.xml does two things.
Running in development without packaging:
mvn spring-boot:run
mvn spring-boot:run -Dspring-boot.run.profiles=dev
mvn spring-boot:run -Dspring-boot.run.arguments=--bibliotech.fine.euros-per-day=0.75Packaging an executable jar:
mvn clean package
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar
java -jar target/bibliotech-1.0.0-SNAPSHOT.jar --spring.profiles.active=prodThat jar is a fat jar with a special structure:
bibliotech-1.0.0-SNAPSHOT.jar
├── META-INF/MANIFEST.MF <- Main-Class: JarLauncher
├── org/springframework/boot/loader/ <- Spring Boot's loader
└── BOOT-INF/
├── classes/ <- YOUR classes and resources
└── lib/ <- ALL the dependencies, as jarsThe trick is that Java cannot run nested jars, so Spring Boot includes its own class loader. The result is a single, self-contained artefact: you copy it to any machine with Java 17 and it works. It is the basis of the container deployment in 12-06.
- AOP in Spring: aspects and pointcuts
The last piece of the core, and the one that connects most directly with what you wrote.
Aspect-oriented programming (AOP) solves a concrete problem: there are concerns that cut across the whole application —transactions, security, tracing, metrics, retries, caching— and which, written by hand, litter every method with code that is not business logic.
It is exactly what motivated your three proxies from 10-03. The vocabulary:
| Term | What it is | Your equivalent |
|---|---|---|
| Aspect | The class that groups the cross-cutting functionality | Your AuditProxy |
| Join point | A point where you can intervene. In Spring, always a method call | The InvocationHandler's invoke |
| Pointcut | The expression that selects which join points | Your if (method.isAnnotationPresent(...)) |
| Advice | The code that runs | The body of invoke |
| Weaving | How it is applied. In Spring, runtime proxies | Proxy.newProxyInstance |
An aspect for BiblioTech, replacing your TimingProxy from 10-03:
package com.nexussoftware.bibliotech.infrastructure;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
// Pointcut: EVERY public method in the .service package
@Around("execution(public * com.nexussoftware.bibliotech.service..*(..))")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
return joinPoint.proceed(); // <- the real call to the method
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
if (ms > 100) {
log.warn("Slow method: {} took {} ms",
joinPoint.getSignature().toShortString(), ms);
}
}
}
}Compare it with what you wrote:
// Your TimingProxy from 10-03
(proxy, method, args) -> {
long start = System.nanoTime();
try {
return method.invoke(target, args);
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
// ...
}
};It is the same code. What Spring adds is the @Around("execution(...)"): instead of applying the proxy by hand object by object, you declare an expression and the container applies it to everything that matches.
The advice types:
| Annotation | When it runs | Typical use |
|---|---|---|
@Before |
Before the method | Security checks, tracing |
@AfterReturning |
After finishing normally | Success auditing |
@AfterThrowing |
After an exception is thrown | Error logging, metrics |
@After |
Always (like finally) |
Cleanup |
@Around |
Wraps the call | The most powerful: transactions, caching, retries, timing |
And pointcuts by annotation, which is what is used most and what connects with your @Auditable:
@Around("@annotation(com.nexussoftware.bibliotech.annotations.Auditable)")
public Object audit(ProceedingJoinPoint joinPoint) throws Throwable {
// Applies ONLY to methods annotated with @Auditable.
// Your annotation from 10-02, now consumed by Spring instead of by your proxy.
operationLog.recordStart(joinPoint.getSignature().getName());
Object result = joinPoint.proceed();
operationLog.recordEnd(joinPoint.getSignature().getName());
return result;
}Your @Auditable is still alive. What has changed is who reads it.
@Transactional: the canonical aspect
@Transactional: the canonical aspectYou do not write the most-used aspect in the Java world: it comes with Spring.
@Service
public class LoanManager {
@Transactional
public Loan lend(String isbn, String employee) {
Material material = materialRepository.findByIsbn(isbn)
.orElseThrow(() -> new MaterialNotFoundException(isbn));
material.decrementAvailable(); // UPDATE 1
Loan loan = new Loan(material, employee, LocalDate.now(clock));
loanRepository.save(loan); // INSERT
auditLog.record(loan); // INSERT
return loan;
}
}Without @Transactional, if the third INSERT fails, the first two are already committed: there is one fewer copy available and an unaudited loan. Inconsistent data.
With @Transactional, the three operations are atomic: either all three or none.
Exactly what the proxy does:
// Pseudocode of Spring's transactional aspect
public Object invoke(Method method, Object[] args) throws Throwable {
Transaction tx = transactionManager.begin(); // BEGIN
try {
Object result = method.invoke(target, args);
transactionManager.commit(tx); // COMMIT
return result;
} catch (RuntimeException | Error e) {
transactionManager.rollback(tx); // ROLLBACK
throw e;
} catch (Exception checkedException) {
transactionManager.commit(tx); // IT COMMITS! (by default)
throw checkedException;
}
}Look at that last catch, because it is the default behaviour that surprises people most:
By default, Spring rolls back on unchecked exceptions (
RuntimeExceptionandError) and commits on checked exceptions.
If your BiblioTechException from module 6 is checked (it extends Exception), throwing it does not roll the transaction back. There are two fixes: make it unchecked (advisable, and consistent with what was decided in 06-04), or declare it:
@Transactional(rollbackFor = BiblioTechException.class)
public Loan lend(String isbn, String employee) { ... }The attributes that really get used:
| Attribute | What for | Usual value |
|---|---|---|
readOnly |
Optimises queries; Hibernate switches off dirty checking | true on read-only methods |
rollbackFor |
Forces a rollback on checked exceptions | Your base exception |
propagation |
What to do if a transaction already exists | REQUIRED (the default) |
isolation |
The database isolation level | The manager's (default) |
timeout |
Maximum seconds | Case by case |
Propagation and isolation are developed in 11-03, where they make sense with a real database in front of you. Here it is enough to know that @Transactional is an aspect and that the aspect is a proxy.
- How it works underneath: the proxies you already wrote
Go back to the lifecycle diagram (section 10) and look at the second-to-last step: "BeanPostProcessor after initialisation: THIS IS WHERE AOP PROXIES ARE CREATED".
Exactly what happens:
- Spring instantiates your
LoanManagerand injects its dependencies. - A
BeanPostProcessorchecks whether any of its methods matches a pointcut (for example, it has@Transactional). - If it matches, it creates a proxy that wraps your object and registers the proxy in the container, not your object.
- Everyone who injects a
LoanManagerreceives the proxy.
graph LR
A["Caller<br/>(another bean)"] -->|lend| B["Proxy<br/>(generated by Spring)"]
B -->|1. BEGIN| D[("Database")]
B -->|2. proceed| C["LoanManager<br/>(YOUR real object)"]
C -->|3. SQL| D
B -->|4. COMMIT or ROLLBACK| D
There are two ways of creating that proxy, and this has practical consequences:
| Mechanism | When Spring uses it | Limitation |
|---|---|---|
| JDK dynamic proxy | The class implements some interface | It only intercepts the interface's methods |
| CGLIB (generated subclass) | The class implements no interface | The class and its methods cannot be final |
Spring Boot uses CGLIB by default (spring.aop.proxy-target-class=true), which avoids the first limitation but introduces the second. Hence a silent and very real failure:
@Service
public class LoanManager {
@Transactional
public final Loan lend(...) { // <- final: CGLIB cannot override it
// The transaction is NOT applied. And there is no error.
}
}The behaviour is identical to the Proxy.newProxyInstance you wrote, with the difference that yours only worked over interfaces.
A curious consequence you will see in stack traces: classes like LoanManager$$SpringCGLIB$$0 will appear. Do not panic: it is your class, wrapped.
- The internal-call trap
This section probably explains problem number one in the Spring world. And you have already seen it: it is exactly the hole in your proxy from 10-03.
@Service
public class LoanManager {
public void lendMany(List<String> isbns, String employee) {
for (String isbn : isbns) {
lend(isbn, employee); // <- INTERNAL CALL: this.lend(...)
}
}
@Transactional
public Loan lend(String isbn, String employee) {
// Called from lendMany: THERE IS NO TRANSACTION.
}
}Why it fails, step by step:
- The external caller invokes
lendManyon the proxy. - The proxy checks:
lendManyhas no@Transactional. It does nothing special. - The proxy invokes
lendManyon your real object. - Inside,
lend(isbn, employee)is reallythis.lend(...), andthisis your real object, not the proxy. - The call does not go through the proxy. There is no transaction. And there is no error.
graph TD
A["External caller"] -->|lendMany| P["PROXY"]
P -->|no transaction, correct| G["Real LoanManager"]
G -->|"this.lend(...)<br/>DOES NOT GO THROUGH THE PROXY"| G
G -.->|"@Transactional IGNORED"| X["No transaction"]
You wrote this very error, with this very diagnosis, when you tested your AuditProxy: internal methods were not audited. It is the same mechanism, and it is inherent to proxies. It is not a Spring bug: it is a consequence of a proxy only being able to intercept what passes through it.
Solutions, from best to worst:
1. Put the annotation on the outer method (almost always the right answer):
@Transactional // the whole operation in one transaction
public void lendMany(List<String> isbns, String employee) {
for (String isbn : isbns) { lend(isbn, employee); }
}2. Split into two beans (right when the transactional semantics must differ):
@Service
public class BulkLoanManager {
private final LoanManager manager; // ANOTHER bean: the call DOES go through its proxy
public BulkLoanManager(LoanManager manager) { this.manager = manager; }
public void lendMany(List<String> isbns, String employee) {
isbns.forEach(isbn -> manager.lend(isbn, employee)); // one transaction per loan
}
}3. Self-injection (it works, but it is a symptom of a design that could be better):
@Service
public class LoanManager {
@Autowired @Lazy
private LoanManager self; // the PROXY of itself
public void lendMany(List<String> isbns, String employee) {
isbns.forEach(isbn -> self.lend(isbn, employee));
}
}The same trap affects @Cacheable, @Async, @Retryable, @PreAuthorize and any aspect of your own. Now that you know why, you will recognise it in all of them.
- BiblioTech on Spring: the result
The project structure after the migration:
bibliotech/
├── pom.xml
└── src/main/
├── java/com/nexussoftware/bibliotech/
│ ├── BiblioTechApplication.java <- @SpringBootApplication
│ ├── config/
│ │ ├── BiblioTechProperties.java <- @ConfigurationProperties + @Validated
│ │ └── BiblioTechConfiguration.java <- @Bean Clock, @Bean HttpClient
│ ├── domain/ <- NO Spring annotations
│ │ ├── Material.java (sealed), Book, Magazine, Dvd
│ │ ├── Employee.java, Loan.java, Reservation.java
│ │ └── Severity, MaterialType, LoanStatus, Card, SessionSummary
│ ├── service/
│ │ ├── LoanManager.java <- @Service, @Transactional
│ │ ├── FineCalculator.java <- @Service, injected Clock
│ │ ├── NoticeService.java <- interface
│ │ ├── ConsoleNoticeService.java <- @Service @Profile("dev")
│ │ ├── EmailNoticeService.java <- @Service @Profile("prod")
│ │ └── BiblioTechStatistics.java <- @Service
│ ├── persistence/ <- @Repository (JPA in 11-03)
│ ├── network/ <- MetadataClient, CatalogServer
│ ├── infrastructure/
│ │ └── TimingAspect.java <- @Aspect @Component
│ └── presentation/
│ └── BiblioTechStartup.java <- CommandLineRunner
└── resources/
├── application.yml
├── application-dev.yml
└── application-prod.ymlNotice one deliberate detail: the domain package has not a single Spring annotation. Material, Loan and Employee are pure Java. They can be instantiated with new, tested without a context and reused in another framework. That separation is a design principle explored further in 12-02, and it is one of the decisions you will be most grateful for in two years' time.
And what has disappeared:
| Removed | Replaced by |
|---|---|
SimpleContainer (150 lines) |
ApplicationContext |
Your own @Component and @Inject |
@Service, @Repository, @Component |
AuditProxy, TimingProxy (applied by hand) |
@Aspect with declarative pointcuts |
Static Configuration / BusinessRules |
Validated @ConfigurationProperties |
GracefulShutdown with shutdown hooks |
The container lifecycle and @PreDestroy |
A 60-line main assembling objects by hand |
SpringApplication.run and a CommandLineRunner |
To run it:
- What is NOT covered in this lesson
So that the map is clear:
| Topic | Where |
|---|---|
REST controllers, @RestController, @GetMapping |
12-04 |
| Persistence with JPA and Spring Data | 11-03 (full introduction) |
@SpringBootTest, MockMvc, integration tests |
11-04, 11-06 and 12-05 |
Actuator: health probes, metrics, /actuator |
12-07 |
| Spring Security | 12-07 |
| The design patterns behind it (Dependency Injection, Factory, Proxy, Singleton) | 12-02 |
| Packaging into a container and deploying | 12-06 |
Spring Actuator deserves a mention here because it is switched on by a single starter: it exposes /actuator/health, /actuator/metrics and /actuator/env without writing any code. It is the direct answer to "how do I know my application is alive in production". It is developed in 12-07.
- Common Mistakes and Tips
Mistake: field injection with @Autowired. It is the shortest form and the worst. It prevents final, prevents instantiating the class in a test, hides an excess of dependencies and couples the class to Spring. Always use the constructor.
Mistake: the main class outside the root package. @ComponentScan only looks at the annotated class's package and its subpackages. If BiblioTechApplication is in com.nexussoftware.bibliotech.presentation, it will not see com.nexussoftware.bibliotech.service and start-up will fail with "bean not found".
Mistake: mutable state in a singleton bean. The singleton is shared by every thread. A private int counter is a textbook race condition (08-04). Either stateless, or Atomic* and concurrent collections (08-06).
Mistake: an internal call to a @Transactional method. It does not go through the proxy and the annotation is ignored with no error at all. Section 26.
Mistake: a final class or method with AOP annotations. CGLIB cannot override it and the aspect is not applied, also silently.
Mistake: expecting a rollback on a checked exception. By default Spring commits. Either make the exception unchecked, or use rollbackFor.
Mistake: heavy work in the constructor. Connecting to the database, reading files or calling the network in the constructor breaks the container's ordering and makes the class impossible to instantiate in a test. Use @PostConstruct.
Mistake: putting <version> on dependencies the starter parent manages. You take away its job and cause version conflicts that are hard to diagnose.
Mistake: javax.* instead of jakarta.*. Spring Boot 3 migrated every package. An import javax.persistence.Entity copied from a 2020 tutorial does not compile.
Tip: keep the domain free of framework annotations. Material and Loan must be able to exist without Spring. Annotate the services and the repositories, not the business entities.
Tip: prefer @ConfigurationProperties to @Value. It groups, validates, auto-completes and fails at start-up instead of at runtime.
Tip: use --debug when something is not configured as you expect. The conditions report answers almost every "why".
Tip: @Profile("dev") instead of if (isDevelopment). Environment-conditional logic inside business code ages very badly.
Tip: when you are unsure what is going on, print the beans. A CommandLineRunner that walks getBeanDefinitionNames() is an excellent diagnostic tool.
- Exercises
Exercise 1: migrating a service into the container
You start from this BiblioTech class, written in the style of module 7:
public class ReservationProcessor {
private final ReservationRepository repository = new CsvReservationRepository(
Path.of("data/reservations.csv"));
private final LoanManager manager = new LoanManager();
private final int reservationExpiryDays =
Integer.parseInt(BusinessRules.get("reservation.expiry.days", "3"));
public void processPending() {
for (Reservation reservation : repository.pending()) {
if (reservation.date().plusDays(reservationExpiryDays).isBefore(LocalDate.now())) {
repository.expire(reservation);
} else if (manager.isAvailable(reservation.isbn())) {
manager.lend(reservation.isbn(), reservation.employee());
repository.complete(reservation);
}
}
}
}Convert it to Spring. It must satisfy:
- Constructor injection with
finalfields. - The right stereotype.
- The number of days from external configuration and validated (
@Min(1) @Max(30)). LocalDate.now()replaced by an injectedClock(10-05), so it can be tested.processPendingmust be atomic.- Add the corresponding
application.ymlfragment.
Exercise 2: two implementations and one environment
BiblioTech needs to send due-date notices. There are two implementations of NoticeService: EmailNoticeService (real SMTP) and ConsoleNoticeService (prints to the screen).
Requirements:
- In the
devprofile the console one must be used; inprod, the email one. No class may ask which environment it is in. LoanManagerreceives aNoticeServicewithout knowing which.- Add a third implementation,
SmsNoticeService, and aMultiChannelNotifierthat uses every active implementation, without needing changes when a fourth is added. - Explain what happens if in the
prodprofile there are two active implementations andLoanManagerasks for a single one, and give two different solutions.
Exercise 3: diagnosing three proxy failures
These three snippets compile, start with no errors and do not do what they look like they do. For each one: explain what happens, why, and fix it.
// CASE A
@Service
public class CatalogService {
public void importBatch(List<Material> materials) {
materials.forEach(this::importOne);
}
@Transactional
public void importOne(Material material) {
repository.save(material);
index.update(material);
}
}// CASE B
@Service
public class FineManager {
@Transactional
public final void waive(Long loanId) {
repository.deleteFine(loanId);
audit.record("Fine waived: " + loanId);
}
}// CASE C
@Service
public class CatalogImporter {
@Transactional
public void importFrom(Path file) throws CorruptMaterialException {
for (String line : Files.readAllLines(file)) {
repository.save(parse(line)); // throws CorruptMaterialException
}
}
}
// where: public class CorruptMaterialException extends Exception { ... }Solutions
Solution 1
package com.nexussoftware.bibliotech.service;
import java.time.Clock;
import java.time.LocalDate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service // (2) business layer: @Service, not @Component or @Repository
public class ReservationProcessor {
// (1) everything final, injected through the constructor
private final ReservationRepository repository;
private final LoanManager manager;
private final BiblioTechProperties properties;
private final Clock clock; // (4) injected clock
public ReservationProcessor(ReservationRepository repository,
LoanManager manager,
BiblioTechProperties properties,
Clock clock) {
this.repository = repository;
this.manager = manager;
this.properties = properties;
this.clock = clock;
}
// (5) atomic: either every reservation is processed or none is
@Transactional
public void processPending() {
LocalDate today = LocalDate.now(clock); // (4) deterministic in tests
int days = properties.reservation().expiryDays(); // (3) configured and validated
for (Reservation reservation : repository.pending()) {
if (reservation.date().plusDays(days).isBefore(today)) {
repository.expire(reservation);
} else if (manager.isAvailable(reservation.isbn())) {
manager.lend(reservation.isbn(), reservation.employee());
repository.complete(reservation);
}
}
}
}Extending the properties (point 3):
@Validated
@ConfigurationProperties(prefix = "bibliotech")
public record BiblioTechProperties(
@NotBlank String name,
@NotNull Loan loan,
@NotNull Fine fine,
@NotNull Reservation reservation) {
public record Reservation(@Min(1) @Max(30) int expiryDays) { }
// ... Loan and Fine as before
}# (6) application.yml
bibliotech:
name: BiblioTech - Nexus Software
loan:
default-days: 15
max-per-employee: 3
fine:
euros-per-day: 0.50
max: 20.00
reservation:
expiry-days: 3What has been gained, point by point:
| Before | After | Benefit |
|---|---|---|
new CsvReservationRepository(...) |
Injected | Replaceable by JPA in 11-03 without touching this class |
new LoanManager() |
Injected | Replaceable by a mock in 11-06 |
Static BusinessRules.get(...) |
BiblioTechProperties |
Validated at start-up, different per environment |
LocalDate.now() |
LocalDate.now(clock) |
Testable: Clock.fixed in the test |
| No transaction | @Transactional |
No half-finished states if something fails |
Note: processPending is a public method of the bean itself called from outside, so it does go through the proxy and the transaction is applied. If another method of this same class called it, it would not (section 26).
Solution 2
Points 1 and 2: profiles.
public interface NoticeService {
void notify(Employee recipient, String message);
}
@Service
@Profile("dev")
public class ConsoleNoticeService implements NoticeService {
private static final Logger log = LoggerFactory.getLogger(ConsoleNoticeService.class);
@Override public void notify(Employee recipient, String message) {
log.info("[SIMULATED NOTICE] {} -> {}", recipient.email(), message);
}
}
@Service
@Profile("prod")
public class EmailNoticeService implements NoticeService {
@Override public void notify(Employee recipient, String message) { /* SMTP */ }
}LoanManager does not change at all:
@Service
public class LoanManager {
private final NoticeService notices; // it does not know which one it gets
public LoanManager(NoticeService notices, ...) { this.notices = notices; }
}No class asks about the environment: the decision lives in the annotations and in spring.profiles.active.
Point 3: every implementation.
@Service
@Profile("prod")
public class SmsNoticeService implements NoticeService { ... }
@Service
public class MultiChannelNotifier {
private static final Logger log = LoggerFactory.getLogger(MultiChannelNotifier.class);
private final List<NoticeService> channels;
public MultiChannelNotifier(List<NoticeService> channels) {
this.channels = channels; // Spring injects EVERY ACTIVE bean of the type
log.info("Notifier with {} active channels", channels.size());
}
public void notifyThroughAll(Employee recipient, String message) {
channels.forEach(channel -> {
try {
channel.notify(recipient, message);
} catch (Exception e) {
// One channel being down must not stop the rest (layered strategy, 06-07)
log.warn("Channel {} failed", channel.getClass().getSimpleName(), e);
}
});
}
}Adding a fourth channel means creating a class with @Service. MultiChannelNotifier is untouched: that is the open/closed principle, and Spring gives it to you for free.
Point 4: the ambiguity. In prod there are two beans of type NoticeService (EmailNoticeService and SmsNoticeService). When LoanManager asks for one, start-up fails:
Parameter 0 of constructor in ...LoanManager required a single bean,
but 2 were found: emailNoticeService, smsNoticeServiceTwo solutions:
// Solution A: mark the principal one
@Service @Profile("prod") @Primary
public class EmailNoticeService implements NoticeService { ... }// Solution B: choose explicitly with a qualifier of your own (10-02)
@Qualifier("email")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER})
public @interface ByEmail { }
@Service @Profile("prod") @ByEmail
public class EmailNoticeService implements NoticeService { ... }
@Service
public class LoanManager {
public LoanManager(@ByEmail NoticeService notices, ...) { ... }
}A is more convenient; B is explicit and checked by the compiler. And MultiChannelNotifier, which asks for a List<NoticeService>, is not affected by any ambiguity: it wants them all.
Solution 3
CASE A — internal call.
- What happens:
importOnedoes not run in a transaction. Eachsavecommits on its own; if material number 40 fails, 39 are left half-imported, with the index possibly out of step. - Why:
materials.forEach(this::importOne)is a method reference onthis, which is the real object, not the proxy. The call does not cross the proxy and@Transactionalis ignored. - Fix (depending on the semantics you want):
// Option 1: the whole batch in ONE transaction (the usual choice for an import)
@Service
public class CatalogService {
@Transactional
public void importBatch(List<Material> materials) {
materials.forEach(this::importOne); // now a transaction is already open
}
// no @Transactional: it inherits the outer method's
void importOne(Material material) {
repository.save(material);
index.update(material);
}
}// Option 2: one transaction PER material (if one failure must not bring the batch down)
@Service
public class BatchImporter {
private final CatalogService catalog; // another bean -> it goes through ITS proxy
public BatchImporter(CatalogService catalog) { this.catalog = catalog; }
public void importBatch(List<Material> materials) {
materials.forEach(catalog::importOne);
}
}CASE B — final method.
- What happens: there is no transaction. If
audit.recordfails, the fine is already deleted and there is no record of it. - Why: Spring Boot uses CGLIB, which generates a subclass. A
finalmethod cannot be overridden, so the proxy does not intercept it. No error is emitted; the failure is completely silent. - Fix: remove
final.
@Service
public class FineManager {
@Transactional
public void waive(Long loanId) { // no final
repository.deleteFine(loanId);
audit.record("Fine waived: " + loanId);
}
}A general rule: a class or method meant to carry aspects cannot be final. The same goes for @Cacheable, @Async and @PreAuthorize.
CASE C — checked exception.
- What happens: if line 300 is corrupt,
CorruptMaterialExceptionpropagates... and Spring commits the transaction. 299 materials are left imported and the file is treated as failed. The next retry duplicates those 299. - Why: by default it only rolls back on
RuntimeExceptionandError.CorruptMaterialExceptionextendsException: it is checked, and on a checked exception Spring commits. - Fix, two options:
// Option 1 (recommended): declare it explicitly
@Transactional(rollbackFor = CorruptMaterialException.class)
public void importFrom(Path file) throws CorruptMaterialException { ... }// Option 2 (better in the long run): make the module 6 hierarchy UNCHECKED
public class BiblioTechException extends RuntimeException { ... }
public class CorruptMaterialException extends BiblioTechException { ... }
@Transactional // now it rolls back by default
public void importFrom(Path file) { ... }Option 2 is consistent with what was decided in 06-04: exceptions the caller cannot reasonably recover from should be unchecked, and that way they do not pollute every signature in the code with throws either.
All three cases share one root: AOP works through proxies, and a proxy only intercepts what passes through it. With that clear, all three are diagnosed in seconds.
Conclusion
SimpleContainer is gone, and with it half of BiblioTech's technical debt.
You can tell Spring Framework from Spring Boot: the first is the functionality —container, AOP, transactions, web—; the second is the layer that solves versions with starters, repetitive configuration with auto-configuration and deployment with an executable jar and an embedded server. And you know that everything you learn about the core applies unchanged inside Boot.
You have mastered the IoC container: what an ApplicationContext is, what a bean is —simply an object Spring manages— and the two ways of declaring them, @Component for your classes and @Bean for those you cannot annotate, such as that Clock from 10-05 and that HttpClient from 09-06 that is finally created just once. You know the three forms of injection and why only one is worth using: the constructor, because it allows final, leaves the object always valid, makes an excess of dependencies visible, detects cycles at start-up and —decisively— produces classes you instantiate with new in a test. Field injection is short to write and expensive to maintain.
You know what each stereotype adds —and that @Repository does contribute something real, exception translation into DataAccessException—, when to use @Configuration with @Bean, and why a @Configuration class cannot be final: because Spring generates a CGLIB subclass so that calling clock() twice returns the same clock.
You know the full bean lifecycle, with its two points that matter: that constructor injection happens at instantiation and field injection afterwards, and that AOP proxies are created in the last BeanPostProcessor — the fact that explains the whole AOP section. You use @PostConstruct for the heavy work that must not go in the constructor and @PreDestroy for the orderly shutdown you handled with shutdown hooks in module 8. And you know the scopes, with the consequence that causes the most production incidents: a singleton is shared by every thread, so it either has no state or it uses what you learned in 08-04 and 08-06.
You know how to resolve ambiguities with @Primary, @Qualifier and your own compiler-checked qualifiers; and you know the pattern of injecting a List<T> with every implementation, which turns "adding a notice channel" into "creating a class".
You have externalised the configuration properly. @Value with type conversion and default values, the source hierarchy that lets you deploy the same image in three environments by changing environment variables, and above all @ConfigurationProperties with validation, which turns that static, unvalidated, environment-less BusinessRules from 07-07 into an immutable, typed, injectable record that prevents start-up if the configuration is invalid — with a message saying which property, which value and why. And you have profiles: dev printing notices to the console and prod sending them, without a single if (isDevelopment) in the code.
On the Boot side, you have the pom.xml with spring-boot-starter-parent pinning compatible versions —and that is why you do not put <version>—, you know what a starter is and what each one drags in, and you have dissected @SpringBootApplication into its three annotations, with the practical rule that follows: the main class goes in the root package. And above all, you understand auto-configuration for what it is: a catalogue of candidates in an .imports file, a set of conditions —@ConditionalOnClass, @ConditionalOnProperty and, crucially, @ConditionalOnMissingBean— and an evaluation order that guarantees the golden rule: if you define a bean, its own is not created. With --debug you can read the conditions report and see, for every piece, whether it applied and why.
And you have closed the circle with AOP. You know what an aspect, a pointcut and an advice are, and you have seen that your TimingProxy from 10-03 and a Spring @Around("execution(...)") are the same code, with the difference that Spring applies it by expression instead of object by object. You know @Transactional as the canonical aspect, with its default behaviour that surprises everybody —it rolls back on unchecked exceptions and commits on checked ones—, and you know that underneath there is a JDK proxy over interfaces or a CGLIB subclass, with its two silent consequences: final things cannot be proxied and an internal call does not go through the proxy. That second trap, which causes more Spring incidents than any other, you recognised instantly because the proxy you wrote yourself had exactly the same hole.
BiblioTech is now a Spring Boot application: it starts with mvn spring-boot:run, its domain is free of framework annotations, its services are injected through the constructor, its configuration is typed and validated, it has dev and prod profiles, declarative aspects and a managed lifecycle.
And its persistence is still CSV files.
That @Transactional you have just put on lend is doing nothing, because there is no database underneath that could commit or roll back. JpaLoanRepository is still a name with no content. The spring.datasource.url=jdbc:h2:mem:bibliotech in the dev profile points to a database that does not exist. And that DataSourceAutoConfiguration that appeared in the conditions report is waiting for somebody to add a dependency.
In the next lesson it gets added. You will see what the object-relational mismatch is and what an ORM automates exactly; you will see JDBC in twenty lines as a baseline and grasp how much work disappears; you will map Material, Employee and Loan to tables with @Entity, @Id and @Column —annotations read by reflection, like the @CsvField you wrote—; you will model the relationships between them and run into LazyInitializationException and the N+1 problem, the two classics everybody suffers before understanding them; you will discover automatic dirty checking, which saves your modifications without you calling save; and @Transactional will finally make sense, with its propagation, its isolation and its optimistic locking for the case you already know from module 8: two librarians lending the same copy at the same time.
BiblioTech's CSVs are living on borrowed time.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
