Notes for Spring
августа 17, 2026
-
BeanFactoryvsApplicationContext: BeanFactory is lazy (never builds objects unlessgetBean()); ApplicationContext eagerly builds all singletons at startup. -
Almost everything (
@Autowired,@Transactional, etc.) is implemented viaBeanPostProcessors auto-registered only throughApplicationContext, not a rawBeanFactory. -
Business code should never call
getBean(), because it is Service Locator anti-pattern it defeats DI, breaks unit-testability without a full container. -
getBean()might hand us a proxy, not a literal class - this is why self-invocation breaks@Transactional(calling throughthisskips the proxy entirely). -
Bean names can silently collide - last one registered wins by default (logged at INFO); disable via
spring.main.allow-bean-definition-overriding=false. -
Nested classes get compound default names (
outer.Inner), not just decapitalized simple names - this can be confirmed via @Qualifier("c") lookup. -
Constructor injection > setter injection for required deps - fails at construction, not later.
-
Circular dependencies via constructor injection is an exception and problem is the design of code not the framework.
-
@Beanmethods calling each other directly inside@Configurationget CGLIB-intercpeted (routed through the container for singleton consistency) - but that path does not catch cycles the way parameter-based resolution does. Direct call inside method body is StackOverflowError; Parameter injection of the same cycle can detect spring bean cycle and give clean exception. -
The CGLIB interception only happens in
@Configurationclasses. Inside plain@Componentclasses,@Bean-to-@beancalls are ordinary Java method calls - no container routing, no singleton guarantee at all. -
static @Beanmethods are never CGLIB-intercepted, even inside @Configuration - hard technical limit, CGLIB can only override non-static methods. -
@DependsOnforces construction order, no object reference needed. Deconstruction order is the exact reverse. -
@Lazyis a request, not a guarantee - overridden by anything eager needing the bean, via a real dependency or@DependsOnalone. -
@DependsOn!=SmartLifecycle. Construction order says nothing about start/stop order for runtime behavior.getPhase()controls that - lower starts first, stops last. -
@PostConstructruns while a singleton-creation lock is held. Real deadlock risk exists in concurrent scenarios. It is suggested that keep it simple only for configuration validations to let it finish fast. -
Full per-bean lifecycle order: constructor -> dependency injection -> aware callbacks ->
postProccessBeforeInitialization->@PostConstruct->postProcessAfterInitialization-> bean live. -
Destroy order mirrors init:
@PreDestroy->DisposableBean.destroy()-> custom destroy method. -
Ambiguous single-value injection throws
NoUniqueBeanDefinitionExceptionunless resolved. -
@Primary: Binary flag, exactly-one-wins. Two@Primarybeans still throws - no ranking exists between them. -
@Fallback: Excludes candidates; Resolves only if exactly one non-fallback bean remains. Multiple situations still throw, unless they are all not required. -
@Primary: checked before@Fallback; If@Primaryresolves it,@Fallbackelsewhere is never consulted. -
No numeric priority tier system exists for single-value autowiring. Beyond one level of preference:
List<T>injection +@Orderon bean + manual logic to resolve. -
@Qualifier("name"): explicit override, beats@Primary. Should be added on bean as well as on injection point. -
Custom qualifier annotations: nest
@Qualifierinside custom annotation. -
Generics as implicit qualifiers:
Store<String>vsStore<Integer>disambiguate automatically via type parameter - no annotation needed, as longa s the specific type is preserved at the source. -
Multi-attribute qualifiers: one annotation, multiple attributes, all must match (
@RoutingRule(region=..., urgent=...)). -
autowireCandidate = falseon Bean blocks a bean from ALL type/qualifier resolution.defaultCandidate = falseblocks only plain by-type resolution, still allows targeted@Qualifierinjection. Neither flag has a@Component-level annotation equivalent - only@Beanmethod attributes. Verified via search. -
Name fallback matching: with nothing else present, Spring matches injection point variable/parameter name against bean names.
-
@Resourcevs@Autowired:@Resource= name-first, type-as-fallback.@Autowired= type-first, name-as-last-resort. Opposite priority direction. -
Self-injection: real, supported, lowest-priority fallback. Rides on the flag that should be configured viaspring.main.allow-circular-references=true, but is not suggested. -
Constructor selection rules: single constructor -> always used. Multiple constructors -> exactly one may be
required=true(or unmarked by default); If more than one is@Autowired, ALL must berequired=false. Mixing required + optional across multiple annotated constructors is a hard startup error. -
@Order: only reorders items inside a collection injection point. -
Post-procesors can't use
@Autowired/@Value/@Resourceon themselves - those annotations are implemented BY post-processors, which instantiate too early in the pipeline for that machinery to apply to them. -
singleton/prototypeuniversal;request/session/application/webscoketweb-only. -
Injecting
prototypeinto asingletonfield freezes it at one instance forever. -
@Lookup: abstract method, CGLIB-generated subclass provides a real implementation returning a fresh, fully-Spring-wired intance every call. Different fromnew-newskips DI entirely. Reuqires non-final class/method; doesn't work with@Beanfactory methods (container doesn't control instantiation there). Use case: fresh instance needed but must also receive its own injected dependencies - new alone can't wire it, @Lookup can. -
Scoped proxies: Same, core problem, one level more serious - injecting
request/sessionintosingleton. Spring injects a stand-in proxy fetching "the real one for right now" on every call. -
@RequestScope=@Scope("request")+proxyMode = TARGET_CLASSpre-enabled. Plain@Scope("request")has no proxy by default - breaks at startup if injected into a singleton. -
proxyMode:NO(no stand-in),TARGET_CLASS(CGLIB subclass, works on almost any non-final class),INTERFACES(JDK dynamic proxy, needs an interface). -
@PostConstruct/@PreDestroyare the modern replacement forInitializingBean/DisposableBean. -
Destroy-method inference:
close()method (orAutoCloseable/Closeable) get called automatically on shutdown - only for@Bean-declared beans, not@Component-scanned ones. -
registerShutdownHook()needed for non-web apps onConfigrableApplicationContext. -
SmartLifecycle: separate from construction order entirely - controls actual start/stop after ALL beans are built, viagetPhase(). -
Thread safety: singleton fields set only during init get safe-publication guarantees automatically. Fields mutated after init need volatile/locking.
-
BeanPostProcessor: operates on finished bean instances, two hooks around init callbacks. This is how@Autowired/@Transactionalproxying work. -
BeanFactoryPostProcessor: operates on bean definitions before anything is built.@Value("${...}")resolution is powered by one (PropertySourcesPlaceholderConfigurer). -
Post-processor
@Beanmethods must be static - must exist before the normal pipeline, including their own enclosing@Configurationinstance, is ready. Deeper reason: CGLIB can't intercept static methods anyway, so static also sidesteps unwanted interception. -
FactoryBean: hands complex construction logic to Spring viagetObject().getBean("name")-> product.getBean("&name")-> the factory itself. -
Constructor, setter, arbitrary multi-arg method, and field injection all supported; can be mixed.
-
required = false: fields/setters silently skip if unsatisfiable. Constructor args are different - use
Optional<T>or@Nullableto express genuine optionality. -
BeanFactory,ApplicationContext,Environment,ResourceLoader,ApplicationEventPublisher,MessageSourceare auto-resolvable with zero setup, for both@Autowiredand@Resource. -
@Service/@Repository/@Controllerare@Componentvia meta-annotation. -
@Repositoryhas one real behavioral extra: automatic exception translation. -
Filters (
ANNOTATION/ASSIGNABLE/REGEX/ASPECTJ/CUSTOM) exist for scanning in/out classes beyond the default stereotype set - rare, good to know exists. -
@Lazycan be placed directly on an@Autowiredinjection point, not just the bean itself - gives a lazy-resolution proxy for that specific injection. -
There exists other annotations for dependency injection called
@Inject/@Namedthat other DI farmework implement. -
proxyBeanMethods=falseNo CGLIB subclass,@Beanmethods are just plain factory methods, no interception, no singleton guarantee on direct calls.@Configuration(proxyBeanMethods = false)can be done on config level. -
AnnotationConfigApplicationContext=register()(explicit classes) +scan()(package-wide@Componentdetection) +refresh()(actually build).SpringApplication.run()does all three automatically. -
@Configurationclasses are meta-annotated with@Component- this is why they get auto-detected by component scanning with zero explicit registration, same as any other@Component. -
Declaring a
@Beanmethod's return type as an interface instead of the concrete class limits the container's type knowledge until that bean is actually instantiated -@Autowired ConcreteTypeelsewhere can become declaration-order-dependent: works or fails depending on whether the interface-returning bean happened to be built first. Prefer the most specific return type your injection points actually need. -
Default methods on interfaces can carry
@Bean- lets you compose bean definitions via interface inheritance (@Configuration class implements BaseConfig), including from multiple interfaces at once (Java allows multiple interface implementation, unlike class inheritance). Rare, but real. -
@Bean(destroyMethod = "")explicitly disables destroy-method inference - needed for externally-managed resources (doc's own example: a JNDI-providedDataSource, where Spring auto-closing it would be wrong since its lifecycle isn't Spring's to manage). -
@Bean({"name1", "name2", "name3"})- aString[], not just a single name - gives one bean multiple aliases simultaneously. All names resolve to the same singleton instance. -
@Description("...")- pure metadata, no runtime behavior, useful mainly for monitoring/JMX exposure. -
Inter-bean dependencies expressed by one
@Beanmethod calling another only work inside@Configurationclasses - not supported in plain@Componentclasses, even though such a call may compile and appear to run. -
@Configurationclasses get CGLIB-subclassed at startup, which is why they must not be final. Constructors - including@Autowiredconstructor injection - remain fully supported on@Configurationclasses themselves; the restriction only concerns subclassing, not construction. -
The actual CGLIB mechanism, stated directly: the generated subclass overrides every
@Beanmethod to check the container's singleton cache first, and only executes the real method body if nothing's cached yet. This is why calling the same@Beanmethod multiple times in source code still produces exactly one singleton instance. -
CGLIB is repackaged directly inside spring-core (
org.springframework.cglib) - no separate dependency needed to use it. -
To avoid CGLIB entirely: move
@Beanmethods to a plain@Componentclass, or use@Configuration(proxyBeanMethods = false). Either way,@Bean-to-@Beancalls stop being intercepted, and all inter-bean dependencies must be expressed as constructor/method parameters instead. -
@Import(OtherConfig.class)pulls in another@Configurationclass's beans without needing to list it explicitly at context startup - lets you build a tree of configs from one entry point. -
Cross-config-class dependencies: prefer method-parameter injection (works across
@Configurationclasses automatically).@Autowiredfields directly on a@Configurationclass are also valid (the config class is a bean itself), but the doc recommends parameter injection as the default, warning that field-injected dependencies on@Configurationclasses risk unexpected early initialization. -
Accessing other locally-defined beans from inside a
@PostConstructmethod on the same@Configurationclass is a real, documented anti-pattern - it's a circular reference in disguise (@PostConstructrequires the class instance to be fully constructed; the@Beanmethod call requires the class instance too), and triggersBeanCurrentlyInCreationExceptionunder Boot 2.6+'s circular-reference rejection. -
Background initialization (6.2+):
@Bean(bootstrap = BACKGROUND)allows a bean's construction to run on a separate thread during startup - but only if a bean namedbootstrapExecutor(type Executor) is also registered; otherwise the marker is silently ignored. Non-lazy dependents still block and wait for it; only@Lazyconsumers actually benefit from the concurrency. -
@Conditionalis the general mechanism underlying@Profile-@Profileis just a pre-built Condition implementation checking active environment profiles. You can write your own Condition class for arbitrary custom logic. -
A
@Conditionalon an outer@Configurationclass does NOT automatically apply to a nested@Configurationclass if that nested class gets discovered independently (e.g. via component scanning finding it on its own) rather than through the outer class's own processing or@Import- must be redeclared on the nested class in that case. -
@Import's real-world purpose is the exact mechanism behind Spring's own@Enable*annotations (@EnableScheduling,@EnableCaching,@EnableAsync, etc.) - each is just@Importwearing a friendlier name, pulling in infrastructure beans from a package the app never scans. -
Isolating
@Import's actual effect requires a genuinely unscanned package - a nested class inside the main app's own package gets picked up by component scanning regardless of@Import (since@Configurationis@Component-scannable), so a same-package test proves nothing. Working correctly once ExternalConfig/InternalConfig were placed in truly separate packages. -
@PostConstruct: using an ALREADY-INJECTED field inside@PostConstructis completely safe and common (DI finishes before@PostConstructruns). The danger is narrower than "never touch beans" - specificallygetBean()for something possibly still mid-construction, or a local@Beanmethod call on the same@Configurationclass. -
Reconciled the earlier "deadlock risk" claim with Boot's clean cycle-rejection: same root cause (needing something to be fully done while not yet done), different symptom by context. Real concurrent/multi-threaded scenario = genuine lock contention, possible hang. Single-threaded startup = Boot's cycle detector catches it via static graph analysis before any blocking occurs, producing a clean rejection instead.
-
Three legitimate
@PostConstructuse cases, all sharing the test "only touches already-injected fields, never asks the container for anything new": (1) validating injected dependencies and failing fast, (2) setup work using only already-injected fields, (3) one-time registration/side-effect using only what's already on hand. -
When
@PostConstruct's safe-zone test fails: use@DependsOn(ordering) orApplicationListener<ContextRefreshedEvent>(guaranteed-safe post-startup hook, runs after all singletons are fully built) instead. -
@Bean(bootstrap = BACKGROUND)vs@Lazy-@Lazydelays WHEN construction starts (only begins on first use, maybe never). BACKGROUND does not delay start at all - construction begins immediately, just on a separate thread, off the main bootstrap thread. -
A non-lazy consumer of a BACKGROUND bean still blocks and waits for it, despite the target being backgrounded. The intended real pattern combines both: mark the slow bean BACKGROUND, and mark its consumers
@Lazytoo, so neither construction start nor consumer-waiting is forced. -
Custom
@Conditionalfully working end-to-end, including reading the checked property fromapplication.properties, not just -D JVM flags - becauseCondition.matches()reads through theEnvironmentabstraction, unifying properties files, JVM system properties, OS env vars, and command-line args. Same exact mechanism@Profileis built on, just a custom property check instead ofenv.matchesProfiles(...). -
BeanRegistrar: imperative alternative to annotation-based bean registration. ImplementBeanRegistrar, import it via@Importon a@Configurationclass, and use plain Java (if/for/loops) insideregister(BeanRegistry, Environment)to decide what gets registered -registry.registerBean(name, Type.class, spec -> ...)with a supplier lambda works like a@Beanmethod body, with scope/laziness/description configurable via the spec builder. -
BeanRegistrar's actual use case: when the NUMBER or SHAPE of beans to register genuinely depends on runtime logic a static annotation can't express (e.g. a variable number of beans driven by a configured list) - a targeted escape hatch, not a replacement for ordinary@Bean/@Componentusage. -
@Conditionaloperates uniformly at both class-level (@Configuration/@Component- entire class skipped, including its@Imports, if condition fails) and method-level (@Bean- only that method skipped, rest of the config still processes). Not tied to which stereotype is involved. -
Profile expressions support
!(NOT),&(AND),|(OR) - e.g.@Profile("production & us-east"). Mixing&and|without parentheses is invalid (production & us-east | eu-centralfails) - must disambiguate with parentheses:production & (us-east | eu-central). -
@Profilecan be used as a meta-annotation to build composed annotations (e.g. custom@Productionnested around@Profile("production")) - same trick as@Channel/@RoutingRuleearlier, just applied to profiles. -
Modern Spring (
@Configuration(enforceUniqueMethods = true), the default) now actively refuses to start if a@Configurationclass has overloaded@Beanmethods sharing a name - throwsBeanDefinitionParsingExceptionoutright, telling you to use unique method names or explicitly setenforceUniqueMethods = false. The doc's described "only the first overload's condition matters" behavior only happens if you force the old permissive mode back on. -
With
enforceUniqueMethods = falseforced on, tested directly: the first-declared overload's@Profilegoverns whether the bean exists at all (not which one is "correct" for the active profile), but which overload's body actually executes is resolved separately, via ordinary greediest-satisfiable-candidate selection. Result: a bean can exist because profile A was active, while the actual returned value came from profile B's method body - a genuinely confusing mismatch between why the bean exists and what it actually returns. -
Activating a profile requires an explicit action - tagging beans with
@Profilealone does nothing; with no profile ever activated,@Profile-tagged beans simply never register (NoSuchBeanDefinitionExceptionif something depends on one). -
@Profile("default")does NOT mean "always active" - it means "active only when no profile has been explicitly activated at all." The moment any real profile is activated,defaultstops applying entirely; it is not a baseline layered underneath other profiles. -
Multiple profiles can be active simultaneously - not mutually exclusive (
spring.profiles.active=profile1,profile2, orsetActiveProfiles("p1","p2")). -
Environment.getProperty(...)walks an ordered list ofPropertySources and returns the first match - not merged, fully overridden by whichever source is checked first. Framework-level default hierarchy (highest to lowest): ServletConfig params -> ServletContext params -> JNDI -> JVM system properties (-D) -> OS environment variables. -
-DJVM flag forspring.profiles.activeoverrides the same key set inapplication.properties- matches the precedence hierarchy prediction (JVM system properties outrank lower sources), tested directly by setting conflicting values in both places and observing only the-D-specified profile's bean execute. -
@PropertySource("classpath:...")inserts a properties file into the samePropertySourcesearch chain used byEnvironment/@Value- not a separate mechanism. The resource path itself can contain a${...}placeholder (with optional:defaultfallback), resolved against whatever property sources are already registered before this annotation processes; unresolvable with no default throwsIllegalArgumentException, consistent with@Value's strict-by-default behavior. -
Load-time weaving modifies actual class bytecode at JVM class-loading time - a lower-level mechanism than Spring's runtime proxies (CGLIB/JDK dynamic proxy). Enabled via
@EnableLoadTimeWeaving, requires a JVM agent to actually function. Mainly relevant to JPA entity class transformation (lazy-loading field interception); rarely configured directly in application code. -
MessageSourceprovides i18n message lookup. Requires a bean named exactlymessageSource- if no bean has that exact name,ApplicationContextsilently falls back to an empty no-op implementation instead of failing, so a misnamed bean produces no error, just silent non-functioning. -
MessageSourcedoes not merge resource bundles with the same base name across the classpath - uses only the first one found, ignoring any duplicates. Relevant risk in multi-module projects where two dependencies might ship a bundle with the same base name. -
Locale-to-file mapping is standard Java
ResourceBundleconvention:basename_language_COUNTRY.properties(e.g.messages_en_GB.properties). ALocaleobject's own.toString()output matches this suffix exactly (Locale.UK.toString()returns"en_GB"), so the mapping can be checked directly rather than memorized. -
Locale fallback is a lookup chain, not a full-file merge: for a given locale, the most specific matching file is checked first; only keys missing from that file fall through to progressively less specific files (down to the base file with no suffix). Keys present in the specific file are used from there; keys absent from it are found in the more general file.
-
getMessage(code, args, defaultMessage, locale)returns the default string if the code isn't found.getMessage(code, args, locale)(no default parameter) throwsNoSuchMessageExceptioninstead if the code isn't found. -
The Spring event system (
ApplicationEventPublisher/@EventListener) is a decoupled publish-subscribe mechanism: one bean publishes an event object, any number of independently-written listener beans can react, without the publisher knowing who is listening. Adding a new reaction requires only a new listener class, no changes to the publisher. -
Since Spring 4.2, any plain object can be published as an event - it does not need to extend
ApplicationEvent. Spring wraps arbitrary objects automatically. -
Event listeners are synchronous by default:
publishEvent(...)blocks until every listener finishes. This allows a listener to participate in the same transaction context as the publisher, when one is available - the safe default exists specifically to preserve that transactional consistency. -
@Asyncon a specific@EventListenermethod makes that listener run on a separate thread, so the publisher does not wait for it to finish. Tradeoff: an async listener loses transaction participation and cannot publish a follow-up event via a return value. -
@EventListener(condition = "...")accepts a SpEL expression evaluated against the event (accessible as#eventor the parameter's declared name) - the listener method body only runs if the expression evaluates to true. Equivalent in effect to anifcheck inside the method body, but expressed declaratively on the annotation instead. -
@Orderapplies to@EventListenermethods the same way it applies to collection injection - lower number runs first among multiple synchronous listeners for the same event. Ordering guarantees do not meaningfully apply to@Asynclisteners, since they run whenever their own thread gets scheduled, not in a guaranteed sequence relative to others. -
An
@EventListenermethod can return a new event object instead ofvoid; Spring automatically publishes the returned object as a new event, without needing to manually inject and callApplicationEventPublisher. Returningnullmeans nothing gets published. This return-based chaining is not supported for@Asynclistener methods. -
Resourceis a unified abstraction over loading a file regardless of where it actually lives (classpath, filesystem, URL) - one consistent API instead of different code paths per source type.ApplicationContextimplementsResourceLoader, so it can resolveResourceobjects directly, and@Valueon aResource-typed field/parameter with a string like"classpath:..."gets auto-converted into the actualResourceobject. -
@PropertySource("classpath:...")uses this sameResource/prefix mechanism under the hood - theclasspath:prefix seen there is the general resource-location convention, not something specific to property files. -
ApplicationStartuprecords timestamped internal steps during container startup (package scanning, bean instantiation, post-processing, event handling) for performance diagnostics. It is a no-op by default, collecting nothing unless an actual implementation is explicitly configured on the context before startup. Explicitly scoped to the container's own internal phases - not a replacement for general-purpose profilers or metrics libraries. -
ResourceextendsInputStreamSource, which just contributesgetInputStream(). Each call is expected to return a fresh stream that can be read again from the start - the one exception is whenisOpen()returns true (only forInputStreamResource), meaning the stream is already open, can only be read once, and must then be closed. -
Resource string prefixes decide the concrete implementation created:
classpath:->ClassPathResource,file:->FileSystemResource,https:/other URL schemes ->UrlResource. No prefix at all defers to whatever the surrounding context type considers its default. -
ClassPathResourcecannot always resolve to a realjava.io.File- only works if the resource happens to be on an actual filesystem (e.g. during local dev, unpacked). Once the app is packaged into a jar, a classpath resource lives inside the jar, not as a standalone file, sogetFile()fails in that case whilegetInputStream()still always works. PreferringgetInputStream()overgetFile()is the safer default for code that must work identically from an IDE and from a packaged jar. -
PathResourceis a NIOjava.nio.file.Path-based alternative toFileSystemResource, same purpose, different underlying API.ServletContextResourceis web-app-specific, relative to a deployed web app's root - less relevant for standalone Boot jars.InputStreamResource/ByteArrayResourcewrap data that didn't originate from a file at all (an already-open stream, or raw bytes already in memory), letting that data still be passed around using the uniformResourcetype. -
org.springframework.validation.Validatoris a two-method interface:supports(Class<?>)decides whether this validator applies to a given type;validate(Object, Errors)performs the checks. It has zero dependency on Spring MVC,HttpServletRequest, or any web type — it can be instantiated and invoked directly (new SomeValidator().validate(obj, errors)) with noApplicationContextrunning. -
Errors(typically aBeanPropertyBindingResultinstance in standalone use) is an accumulator, not an exception.rejectValue(field, errorCode)records a failure without halting execution — multiple independent checks in onevalidate()call can all fail and all get reported in a single pass, unlike a design that throws on the first bad field. -
The
errorCodestring passed torejectValue(e.g."name.empty") is a lookup key for aMessageSource, not a message to display. This is what decouples validation logic from language — the same validator produces the same codes regardless of locale; only the later message-resolution step is locale-aware. -
ValidationUtilsis a static helper that wraps commonrejectValuepatterns (rejectIfEmpty,rejectIfEmptyOrWhitespace) so you don't hand-write null/empty checks repeatedly. -
Casting inside
validate(Object obj, Errors e)(Person p = (Person) obj) is safe only if the caller actually checkedsupports()first — the interface itself doesn't enforce this. Callingvalidate()with a typesupports()would have rejected produces aClassCastExceptionat runtime, not a compile error. -
For validating nested objects (e.g. a
Customercontaining anAddress), delegate to a dedicatedValidatorfor the nested type rather than inlining every field check into one giant validator. Wrap the delegated call inerrors.pushNestedPath("address")/errors.popNestedPath()so field errors from the nested validator get correctly prefixed ("address.street"instead of just"street"). Always do this in atry/finally— an early return between push and pop leaves the path prefix stuck for every subsequentrejectValuecall in that validation run. -
As of Spring Framework 6.1,
Validatorgained a default methodvalidateObject(Object)for one-off, non-binding validation — it returns anErrorsyou can inspect (hasErrors()) or convert to an exception via.failOnError(SomeException::new). This is newer and less universally documented than the core two-method interface, so confirm the exact signature against your actual Spring version rather than assuming it's present. -
Validator.validateObject(Object)(6.1+) is backed bySimpleErrors, notBeanPropertyBindingResult— a lighterErrorsimplementation that does not support nested paths. Any validator that callspushNestedPath()/popNestedPath()(i.e., any validator composing a nested validator, like theCustomer/Addresspattern) throwsUnsupportedOperationException: SimpleErrors does not support nested pathsif invoked viavalidateObject(). For validators that do nested delegation, constructnew BeanPropertyBindingResult(target, "name")manually and callvalidate(target, errors)directly instead of using thevalidateObject()shortcut. -
This distinction is not mentioned in the reference docs' prose — it only surfaces as a runtime exception, and only when the validator under test actually exercises nested-path support. A validator with no nested delegation will never reveal this limitation, which makes it an easy trap to hit later on an object graph that grows nested validators after
validateObject()was already in use.