You configure multiple ViewResolvers in one application. Explain how DispatcherServlet walks the resolver chain, the role of Ordered/order, and the classic pitfall with InternalResourceViewResolver in a mixed chain.
answer
- List<ViewResolver>, sorted by order
- first NON-NULL View wins
- null = pass to next resolver
- InternalResourceViewResolver never returns null -> put LAST
- CNVR is the exception -> put FIRST
basics
~20 sDispatcherServlet holds an ordered list of ViewResolvers and calls each in order until one returns a non-null View. Order comes from the Ordered interface / order property. InternalResourceViewResolver always returns a View (never null), so it must be last or it shadows the others.
solid answer
~40 sDispatcherServlet collects all ViewResolver beans, sorts them by the Ordered interface (or their order property; lower = higher priority), and on each request calls resolveViewName in that order, using the first non-null View returned. Most resolvers return null when they can't handle a name, letting the chain continue. The classic pitfall: InternalResourceViewResolver (JSP) resolves via a servlet forward and cannot check whether the target JSP exists, so it ALWAYS returns a View. If it isn't ordered last (lowest precedence), it short-circuits the chain and every logical name resolves to a (possibly missing) JSP, shadowing Thymeleaf/CNVR. Rule: catch-all resolvers go last; specific resolvers first. ContentNegotiatingViewResolver is the exception that must go first because it orchestrates the others. Prefix-based resolvers can also collide, so keep their name patterns disjoint.
code
java · 29 lines@Configuration
class ViewConfig {
// Special-case View beans (PDF/Excel), resolved by bean name. Returns null otherwise.
@Bean
BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver r = new BeanNameViewResolver();
r.setOrder(1); // high precedence, safe (nullable)
return r;
}
@Bean
ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine engine) {
ThymeleafViewResolver r = new ThymeleafViewResolver();
r.setTemplateEngine(engine);
r.setViewNames(new String[] {"pages/*", "home"}); // scoped -> null for others
r.setOrder(2);
return r;
}
@Bean
InternalResourceViewResolver jspResolver() {
InternalResourceViewResolver r = new InternalResourceViewResolver();
r.setPrefix("/WEB-INF/views/");
r.setSuffix(".jsp");
r.setOrder(Ordered.LOWEST_PRECEDENCE); // MUST be last: never returns null
return r;
}
}go deeper
Not expected to reason about multi-resolver chains.
Knows there can be several resolvers and order matters.
Explains the first-non-null contract and the JSP-last rule.
Designs the whole chain (CNVR first, scoped Thymeleaf, JSP last), reasons about null-contract, caching, and diagnoses ordering incidents.
## The resolver chain `DispatcherServlet` doesn't hold a single `ViewResolver` — it holds a **`List<ViewResolver>`**. During init it discovers all `ViewResolver` beans in the web application context (or falls back to a default). It **sorts** them using Spring's `OrderComparator`, which reads: - the **`Ordered`** interface (`getOrder()`), or the `@Order` annotation, or a settable **`order`** property (many resolvers extend `WebApplicationObjectSupport`/`AbstractCachingViewResolver` and expose `setOrder`). - **Lower `order` value = higher precedence** (runs earlier). `Ordered.HIGHEST_PRECEDENCE` = `Integer.MIN_VALUE`, `LOWEST_PRECEDENCE` = `Integer.MAX_VALUE`. ## Resolution algorithm For each request needing a view, `DispatcherServlet.resolveViewName(...)` iterates the sorted list and calls `resolveViewName(name, locale)` on each resolver **in order**, returning the **first non-null `View`**. A resolver that returns **null** means "not my responsibility — try the next one." This null-means-pass contract is what makes chaining work. ## The InternalResourceViewResolver pitfall `InternalResourceViewResolver` renders JSPs through a servlet **`RequestDispatcher` forward**. It has **no way to test whether the JSP file actually exists** without forwarding to it. Consequently it **never returns null** — it always hands back an `InternalResourceView`. Implications: - If it sits **anywhere but last**, it **short-circuits** the chain: every logical name matches it first, so Thymeleaf, a `BeanNameViewResolver`, or `ContentNegotiatingViewResolver` further down never run. You'll see blank pages, 404-on-forward, or "missing JSP" instead of your intended template. - Therefore it must be assigned the **lowest precedence** (highest `order` int, e.g. `Ordered.LOWEST_PRECEDENCE`). Spring's own docs call this out. ## Other chain members and their ordering intent - **`BeanNameViewResolver`** — maps a view name to a `View` bean of the same name; returns null if no such bean. Safe to place early. - **`ThymeleafViewResolver`** — can be scoped with `setViewNames("...")` / `setExcludedViewNames(...)` so it returns null for names it shouldn't own, coexisting with others. - **`ContentNegotiatingViewResolver`** — the deliberate exception: it must be **first** because it *wraps* and orchestrates the other resolvers; it queries them internally, then picks by media type. - **`ResourceBundleViewResolver` / `XmlViewResolver`** — declarative; return null for unknown names. ## Designing a clean chain 1. `ContentNegotiatingViewResolver` first (if used). 2. Specific/declarative resolvers (`BeanNameViewResolver`, Thymeleaf scoped by view-name patterns). 3. The catch-all `InternalResourceViewResolver` **last**. Keep prefix/suffix and `viewNames` patterns **disjoint** so two resolvers don't both claim a name; the first in order wins, which can silently mask the other. ## Caching note Most resolvers extend `AbstractCachingViewResolver` and **cache** resolved Views by name+locale (`setCache(false)` for dev). Caching is per-resolver and doesn't change ordering, but a cached wrong-resolver result can make an ordering bug look intermittent. ## Diagnosing ordering bugs - Symptom: correct template exists but a blank/404 forward renders → a catch-all resolver is ahead of the real one. - Fix: set explicit `order` on each resolver; push JSP last; scope Thymeleaf with `viewNames`. ## When this matters Monolithic apps that migrated JSP→Thymeleaf, or that mix `BeanNameViewResolver` special views (PDF/Excel `View` beans) with template resolvers. Pure single-resolver Boot apps rarely hit it, but understanding the contract explains many "my Thymeleaf page won't render" incidents.
- Why can't InternalResourceViewResolver just return null when the JSP is missing?JSP rendering happens by forwarding to the resource via the servlet RequestDispatcher; existence can only be discovered by attempting the forward, which is a terminal operation. Since it can't pre-check, it optimistically always returns a View, hence the never-null / must-be-last rule.
- How do you let ThymeleafViewResolver and another resolver coexist without one swallowing the other?Scope the Thymeleaf resolver with setViewNames(...)/setExcludedViewNames(...) so it returns null for names it shouldn't handle, and assign disjoint order values. Names it doesn't match fall through to the next resolver in the ordered chain.
- What does a lower order value mean, and what interface controls it?Lower order = higher precedence (runs earlier). It's driven by Spring's Ordered interface / @Order / a setOrder property, sorted by OrderComparator. HIGHEST_PRECEDENCE is Integer.MIN_VALUE, LOWEST_PRECEDENCE is Integer.MAX_VALUE.
saying these in an interview costs you the question
- Thinking DispatcherServlet uses only one ViewResolver
- Believing resolvers return an error (not null) when they can't handle a name
- Placing InternalResourceViewResolver first or in the middle of the chain
- Assuming higher order value = higher priority (it's the opposite)