Advanced · Architect
Recognize the Patterns Behind Spring Annotations
Connect proxies, strategies, templates, factories, and adapters to the behavior annotations activate.
Primary source: Spring Framework 6.2 Core Technologies: The IoC Container
Lesson 2 of 3
1d streakThe Architectural Question
Annotations read like compiler features; recognizing the Proxy, Strategy, Template Method, and Factory machinery underneath explains their limits before they bite.
What this guide proves
- Know when a call crosses a proxy boundary
- Prefer composition and explicit strategies in application code
- Debug generated behavior from the underlying pattern
The Spring IoC Core: BeanPostProcessor & BeanDefinitionRegistry
To master Spring Boot, one must understand how the container constructs, configures, and proxies beans. The Spring Bean lifecycle exposes two primary extension points used by framework developers and custom starters:
1. BeanFactoryPostProcessor: Operates on bean metadata (BeanDefinition) BEFORE any bean instances are created. Examples include ConfigurationClassPostProcessor (which parses @Configuration and @Bean methods).
2. BeanPostProcessor (BPP): Operates on actual bean instances immediately before and after their initialization callback (e.g. afterPropertiesSet or @PostConstruct). This is the exact mechanism Spring uses for @Autowired injection, @Transactional proxying, and @Cacheable wrapping.
package com.example.infra;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class AuditLoggingBeanPostProcessor implements BeanPostProcessor {
private static final Logger log = LoggerFactory.getLogger(AuditLoggingBeanPostProcessor.class);
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// Invoked BEFORE @PostConstruct or InitializingBean.afterPropertiesSet()
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// Invoked AFTER bean is fully initialized. Ideal place to return dynamic proxy!
if (bean.getClass().isAnnotationPresent(AuditMonitored.class)) {
log.info("Registered audit monitoring proxy for bean: {}", beanName);
// Return CGLIB or Dynamic Proxy wrapping bean
}
return bean;
}
}Never Inject High-Level Beans into a BeanPostProcessor
Custom AOP Interceptors: Building Enterprise Decorators
Spring AOP allows developers to extract cross-cutting concerns (rate limiting, distributed locking, audit trails) cleanly into custom annotations and aspects.
package com.example.infra.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ExecutionTimeAspect {
private static final Logger log = LoggerFactory.getLogger(ExecutionTimeAspect.class);
@Around("@annotation(logExecutionTime)")
public Object logTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long duration = System.currentTimeMillis() - start;
if (duration > logExecutionTime.slowThresholdMs()) {
log.warn("SLOW METHOD EXECUTION: {}.{}() took {}ms",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
duration);
}
}
}
}The Strategy Behind Custom Spring Boot Starters
Spring Boot auto-configuration is driven by condition evaluation. When authoring internal shared libraries or starters across microservices, use Spring condition annotations to allow consuming services to override library defaults gracefully:
| Condition Annotation | Evaluation Logic | Use Case |
|---|---|---|
| @ConditionalOnMissingBean | Triggers only if no matching bean definition exists in context | Provides default fallback bean that user can override |
| @ConditionalOnProperty | Checks presence or value of an application.yml property | Enables/disables starter features via config |
| @ConditionalOnClass | Checks if specific class is present on the JVM classpath | Configures integrations only when library is added to pom.xml |
Tracing Container Startup and Condition Evaluations
Inspect internal condition evaluations and startup timings using Spring Boot debug logging:
# 1. Run Spring Boot with condition evaluation report enabled
./mvnw spring-boot:run -Dspring-boot.run.arguments="--debug"
# 2. Inspect ConditionEvaluationReport in console output:
# ============================
# CONDITIONS EVALUATION REPORT
# ============================
# Positive matches:
# -----------------
# AuditLoggingAutoConfiguration matched:
# - @ConditionalOnProperty (app.audit.enabled) found property 'true'
#
# Negative matches:
# -----------------
# RedisAuditStoreAutoConfiguration:
# - Did not match: @ConditionalOnClass did not find org.springframework.data.redis.core.RedisTemplateActive Recall Checkpoint
Why does @ConditionalOnMissingBean allow applications to easily customize shared starter beans?
Hint: Consider how Spring Boot supplies default PasswordEncoder or ObjectMapper beans.
Show answer & explanation
If the application defines its own @Bean, Spring skips the auto-configuration method, letting the user bean take precedence without throwing DuplicateBeanDefinitionException.
Runtime Architecture & Flow
Dependency direction
Boundaries follow ownership
Architecture is the set of boundaries that keeps important decisions easy to test and expensive details replaceable.
Know when a call crosses a proxy boundary
B1Prefer composition and explicit strategies in application code
B2Debug generated behavior from the underlying pattern
B3Read these as design constraints. The arrows belong in the implementation only after the dependency rule is chosen.
Working Implementation
Code Walkthrough
Trace `@Transactional`, repository creation, and message conversion to their supporting patterns.
package com.example.internals;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
@Configuration(proxyBeanMethods = false)
public class InternalPatternsBppLab {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditedComponent {}
@Component
public static class AuditTrackingBeanPostProcessor implements BeanPostProcessor {
private final List<String> auditedBeans = new ArrayList<>();
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean.getClass().isAnnotationPresent(AuditedComponent.class)) {
auditedBeans.add(beanName);
}
return bean;
}
public List<String> getAuditedBeans() {
return List.copyOf(auditedBeans);
}
}
public record AuditNotifier(String channel) {}
// Allows downstream consumers to override default notifier without duplicate bean collisions
@Bean
@ConditionalOnMissingBean
public AuditNotifier defaultAuditNotifier() {
return new AuditNotifier("console");
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Identify the pattern behind @Transactional, repository proxies, and HttpMessageConverter selection
- Detect proxy crossings in stack traces while debugging
- Prefer explicit strategies over annotation tricks in application code
Key Takeaway
Spring IoC customization relies on BeanPostProcessor for instance modification/proxying and @Conditional annotations for extensible starter defaults.
Architectural & Technical Review
Core Interview Questions
Q01What is the difference between BeanFactoryPostProcessor and BeanPostProcessor in the Spring lifecycle?Show answer
BeanFactoryPostProcessor runs first and modifies bean metadata (BeanDefinition) before any instances exist. BeanPostProcessor runs later and operates on actual bean instances before and after initialization callbacks, enabling dynamic proxying.
Q02Why should BeanPostProcessor implementations never inject high-level business services?Show answer
BeanPostProcessors are created very early in container startup. Injecting business services causes those services to instantiate prematurely, bypassing other BeanPostProcessors and losing AOP proxy wrapping.
Q03How does @ConditionalOnMissingBean enable shared enterprise starters to provide extensible default beans?Show answer
It evaluates bean definitions in the context. If the consuming application provides its own @Bean definition, Spring skips the starter method, allowing seamless user overrides without duplicate bean definition errors.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Injecting managed transactional services into an early BeanPostProcessor — Forces premature bean initialization, bypassing proxy generation and breaking @Transactional advice.
- Pitfall: Implementing custom AOP aspects without considering proxy self-invocation limitations — Internal calls within the same bean bypass custom aspects unless routed through the proxy.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources