skip to content

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.

level: principalimportance: should knowfreq 25%

answer

  1. List<ViewResolver>, sorted by order
  2. first NON-NULL View wins
  3. null = pass to next resolver
  4. InternalResourceViewResolver never returns null -> put LAST
  5. CNVR is the exception -> put FIRST

basics

~20 s

DispatcherServlet 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 s

DispatcherServlet 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
java
@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

for a junior

Not expected to reason about multi-resolver chains.

for a middle

Knows there can be several resolvers and order matters.

for a senior

Explains the first-non-null contract and the JSP-last rule.

for a principal

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)

context