Topic revision clinic · Spring MVC · 21 misses

Spring MVC — the 21 you've got wrong

Built from the Spring MVC topic bank and every MVC question you've missed across nine exam papers, organised by concept rather than by question number. Spring MVC is your weakest section — 57% on the latest paper, and it has never once been above 75% in nine attempts. But the good news is in the shape of the data: this is not a broad topic where you're weak everywhere. It's four ideas, and two of them account for more than half the damage.

7
Return types & pipelines
5
Controllers & REST
3
The request lifecycle
57%
Latest MVC score
Where the 21 fall. Half the section is fine. The damage is concentrated in two clusters.
Return types & the two pipelines 7missed in #4, #6, #8 ×2, #9, and twice in the bank Controllers, @RestController & REST 5#4, #9 ×2, bank ×2 The request lifecycle 3#1 ×2, #8 Binding request data 2#5, bank RestTemplate (client side) 2#7, bank Mapping annotations 1bank MessageSource / i18n 1bank

The headline: one question has now cost you seven marks. "What can a controller method return, and what does Spring do with it?" has appeared as which return types are valid, which can a ViewResolver resolve, which cannot be returned, which method signatures are valid and is @RequestBody legal on a return type — in attempts #4, #6, #8 (twice), #9, and twice more in the topic bank. It is one table. It is printed below. Learn it and the section stops being your weakest.

Master table 1 — what a handler method can return

Seven of your 21 misses are this table. Read the last column as the answer to "what does Spring do with it?"

Return typeValid?What Spring does
Stringyesa logical view name → handed to the ViewResolver
View (JstlView, RedirectView…)yesthe view object itself — bypasses the resolver
ModelAndViewyesmodel and view (or view name) together
Model / Mapyesmodel attributes; view name inferred from the URL
voidyesview name inferred from the URL, or you wrote the response yourself
a domain object + @ResponseBodyyesserialised by an HttpMessageConverter
ResponseEntity<T>yesbody + status + headers — the REST workhorse
HttpHeadersyesa header-only response with an empty body (rare, but legal)
HttpEntity<T>yesbody + headers, no status
an absolute path "/WEB-INF/views/home.jsp"NOtreated as a logical name, so the resolver prefixes/suffixes it and it breaks
HttpServletRequest / HttpServletResponse / ServletResponseNOthese are parameters, injected in — never returned
Object (bare, no @ResponseBody)NOcarries no view contract — Spring can't tell what it is
@RequestBody on the return typeNO@RequestBody is input only; the output annotation is @ResponseBody

One sentence that carries the whole table: a controller returns what to render (a name, a view, a model) or the response body itself (with @ResponseBody). It never returns the machinery — not the request, not the response, not a filesystem path.

Master table 2 — the two pipelines

Everything above resolves to one of two paths, and a single annotation decides which. This is the fork behind most of your MVC misses.

View pipelineMessage-conversion pipeline
Triggered by@Controller, no @ResponseBody@ResponseBody (or @RestController)
return "list"; meansa view named listthe literal text list, as text/plain
Who convertsViewResolverViewHttpMessageConverter (Jackson for JSON)
Outputrendered HTML from a templateJSON / XML / plain text written straight to the body

Same return type, opposite meaning. This one fork explains "a String from a @RestController is text/plain, not JSON and not a view name" (missed in #3 and #4), "@RestController = @Controller + @ResponseBody" (missed in #9), and "which return types can a ViewResolver resolve" (missed in #6).

Master table 3 — binding request data, and the status code when it fails

The status codes are the part people don't learn, and they're the most testable part.

AnnotationReadsExampleIf it's missing
@PathVariablea URI template segment/products/42404 — the URL never matched the mapping
@RequestParamquery string or form field/products?id=42400MissingServletRequestParameterException
@RequestBodythe body, via a message converterPOST with JSON400 — nothing to deserialise
@RequestHeadera headerAuthorization: …400
@RequestMappingnothing — it maps the handler@RequestMapping("/api")

404 vs 400 vs 405 — the rule is "how far did the request get?" Routing happens first, binding second:

Request against @GetMapping("/products/{id}")ResultFailed at
GET /products/42200
GET /products/404 Not Foundrouting — no handler matched
GET /products/abc400 Bad Requestbinding — can't convert to Long
POST /products/42405 Method Not Allowedrouting — path matched, verb didn't

Both @PathVariable and @RequestParam default to the parameter name — name them explicitly only when the two differ. That relies on the -parameters compiler flag, which Boot's build plugins enable for you.

Jump to a section

① Return types & the two pipelines — 7 ② Controllers, @RestController & REST — 5 ③ The request lifecycle — 3 ④ Binding request data — 2 ⑤ RestTemplate (client side) — 2 ⑥ Mapping annotations — 1 ⑦ MessageSource / i18n — 1

① Return types & the two pipelines 7 misses

This is the section. Seven questions, one table, five different papers. If you only revise one thing on this page, revise master tables 1 and 2.

Missed in #4, #8 and the bank · Valid controller return types three times

void, String, ModelAndView, Model — servlet objects are parameters

Your answers over time: HttpServletRequest (#4 and #8), ServletResponse (bank). Every time, a servlet object ticked as a return type.

Spring injects the servlet objects so you can read from them. Returning one tells the framework nothing about what to render:

@GetMapping("/hello")
public String hello(HttpServletRequest request,      // ← injected IN
                    HttpServletResponse response,    // ← injected IN
                    HttpSession session) {           // ← injected IN
    log.info(request.getRemoteAddr());
    return "helloView";                              // ← a view NAME comes OUT
}

The parameter side is worth knowing as its own fact, because it's the mirror question. Spring can inject: HttpServletRequest/Response, HttpSession, Model, Locale, Principal, HttpMethod, InputStream/Reader, BindingResult, and anything annotated @PathVariable/@RequestParam/@RequestBody/@RequestHeader/@ModelAttribute.

If you take the response as a parameter and write to it yourself, return void. That's the supported way to handle the response by hand — not returning the response object.

Memory hook: Servlet objects go IN as parameters. What comes OUT is a view name, a view, a model, or a body.
Missed in #8 · What a controller cannot return polarity + concept

An absolute path to the view — a logical name and void are both fine

Your answer: you ticked "a logical view name" and "void". The two most ordinary returns there are — a polarity slip on top of the concept.

A returned String is always treated as a logical name, so the resolver's prefix and suffix are applied to whatever you give it:

You returnResolver produces
"home"/WEB-INF/views/home.jsp
"/WEB-INF/views/home.jsp"/WEB-INF/views/WEB-INF/views/home.jsp.jsp
new JstlView("/WEB-INF/views/home.jsp")used as-is — a View object skips resolution ✅

That third row is the key, and it's what makes the question fair: if you genuinely need a physical path, wrap it in a View. The rule is "a String is a name; a View is a location."

Two special String prefixes are the exception that proves it — "redirect:/home" and "forward:/other" are recognised by the resolver and handled specially rather than resolved as names.

Memory hook: String ⇒ logical NAME. Need a real path? Return a View object — that skips the resolver.
Missed in #6 · Which return types a ViewResolver can resolve

String, View, ModelAndView (and void) — bare Object cannot

Your answer: you ticked Object alongside the correct three. A bare Object tells the resolver nothing about what it's holding.

The question is narrower than "what's a valid return type" — it asks specifically what the view pipeline can consume. ResponseEntity and domain objects are valid returns but never reach a ViewResolver; they go down the message-conversion path instead.

Watch for that narrowing in the stem: "so that ViewResolver can resolve a View" restricts the answer to master table 1's first five rows.

Memory hook: Resolvable by a ViewResolver: String · View · ModelAndView · void · Model/Map. Everything else goes to a message converter.
Missed in #9 and the bank · @RequestBody on a return type

@RequestBody is input only; @ResponseBody is the output annotation

Your answer: you ticked public @RequestBody MyDataDto getMyData() as valid. You correctly accepted HttpHeaders as a return type — the genuinely surprising one.

A matched pair pointing in opposite directions:

AnnotationDirectionLegal on
@RequestBodyIN — deserialise the request bodya method parameter, only
@ResponseBodyOUT — serialise the return valuea method, a return type, or a class
@PostMapping("/products")
public @ResponseBody Product create(@RequestBody Product incoming) { … }
//     ^^^^^^^^^^^^^ OUT              ^^^^^^^^^^^^ IN

Both use the same machinery — HttpMessageConverters — just in opposite directions. Jackson deserialises the incoming JSON and serialises the outgoing object.

Memory hook: Request ⇒ in ⇒ parameter. Response ⇒ out ⇒ method or return type. The words say which way they point.
Missed in the bank · Valid handler method signatures

Two different failures: one won't compile, the other fails at runtime

Your answer: the bank marks both broken signatures — worth separating them, because the material's own explanation blurs them.
SignatureVerdictWhy
getEmployeeName(@RequestParam String employeeId)validname defaults to the parameter name
createAccount(@RequestParam String accountId)validsame; ResponseEntity is a fine return type
getUser(@PathVariable id, Model m)invalidwon't compileid has no type
@PutMapping("/employees/{employeeId}")
createEmployee(@PathVariable("id") String employeeId, …)
invalidcompiles, fails at runtime — no template variable named id

A correction to the source material: its explanation says the second one "should be @PathVariable("id") or @PathVariable String id". Only the latter fixes it — @PathVariable("id") alone still leaves the parameter with no type, so it still won't compile. Worth knowing, because the two failure modes are miles apart.

And the rule the fourth signature tests: the name in @PathVariable("…") must match a variable in the URI template, not the Java parameter name. Template {employeeId} plus @PathVariable("id") is a mismatch.

Memory hook: Names match ⇒ no value needed. Names differ ⇒ name the TEMPLATE variable, not the parameter.

② Controllers, @RestController & REST 5 misses

Missed in #9 and the bank · What @RestController is also #3 and #4

@Controller + @ResponseBody — which makes an explicit @ResponseBody redundant

In #9 you ticked the two options containing @SpringBootApplication; in the bank the point was that @ResponseBody on a method inside a @RestController can be removed.

The source is two lines:

@Target(ElementType.TYPE)
@Controller          // it's a controller bean
@ResponseBody        // every method writes to the response body
public @interface RestController { … }

So inside a @RestController, a method-level @ResponseBody is harmless but redundant. And @RequestMapping is not part of it — you still declare mappings yourself.

The consequence worth memorising, because it was missed in both #3 and #4:

Classreturn "list"; producesContent-Type
@Controllerrenders the view listtext/html
@RestControllerthe four characters listtext/plain — not JSON

A String is handled by StringHttpMessageConverter, which writes it verbatim. Only non-String objects reach Jackson and become JSON. Returning a String from a REST controller does not produce a JSON string.

Mixed controllers: if some methods return views and others JSON, use @Controller and put @ResponseBody on the individual JSON methods. @RestController is all-or-nothing.

Memory hook: @RestController = @Controller + @ResponseBody. A String from it is text/plain, not JSON and not a view.
Bank · @ResponseStatus

It sets the HTTP status — on a handler method or on an exception class

Two placements, and the exception one is the more useful:
// on an exception — Spring returns 404 whenever it propagates
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException { }

// on a handler method — this endpoint always answers 201
@PostMapping("/resources")
@ResponseStatus(HttpStatus.CREATED)
public void create() { … }
ClaimVerdict
Can set the status for a custom exceptionTRUE
"Can only be applied to controller methods"false — also exception classes (and @ExceptionHandler methods)
"Automatically maps exceptions to views"false — it sets a status code, nothing more. Views come from @ExceptionHandler
"Ignored if a RedirectView is used"false — it overrides status set by other means, including a redirect's default 302

The related family, since exception handling questions travel together: @ExceptionHandler handles exceptions for one controller; @ControllerAdvice / @RestControllerAdvice apply handlers globally; ResponseStatusException (Spring 5+) does the same job as @ResponseStatus but thrown programmatically, so you don't need a class per status.

Memory hook: @ResponseStatus = a status code, on a method or an exception class. It overrides other status sources. It never picks a view.
Missed in #9 · What REST is

An architectural style — stateless, uniform, cacheable, layered, interoperable

Your answer: "REST is Stateful" and "REST is Relative". "Stateful" is the exact inverse of a core constraint; "Relative" isn't a REST concept.

The six constraints these questions are drawn from:

ConstraintMeans
Statelessthe server holds no client session — every request carries what it needs
Client–serverseparated concerns, independently evolvable
Uniform interfacestandard verbs + resource URIs + self-describing messages
Cacheableresponses declare whether they can be cached
Layered systemproxies and gateways can sit in between invisibly
Code on demand (optional)the server may ship executable code

"Style, not protocol" is the other stock answer: REST is a set of constraints, HTTP is the protocol it rides on. SOAP by contrast is a protocol — envelope format, WSDL contract, specification. That comparison is what these questions are usually building toward.

And the verb semantics, which pair with it constantly:

VerbPurposeSafe?Idempotent?Typical success status
GETretrieveyesyes200
POSTcreatenono201 Created + Location
PUTreplacenoyes200 / 204
PATCHpartial updatenono200 / 204
DELETEremovenoyes204 No Content

Safe = doesn't change state. Idempotent = doing it twice has the same effect as once. That's why the bank's Q1 says GET is right for a find and 201 is wrong for a retrieval — 201 means "created".

Memory hook: REST = style, not protocol. Stateless, uniform, cacheable, layered, interoperable. GET is safe + idempotent; POST is neither; PUT and DELETE are idempotent but not safe.

③ The request lifecycle 3 misses

Who does what. Three questions across attempts #1 and #8 turned on attributing a job to the wrong component.

  browser
     │  GET /persons/list
     ▼
┌────────────────────┐
│ DispatcherServlet  │  the FRONT CONTROLLER — extends HttpServlet
└────────────────────┘
     │ 1. which handler?           ──►  HandlerMapping        (finds the @Controller method)
     │ 2. invoke it                ──►  HandlerAdapter        (calls it, resolves arguments)
     │                                    └─► your controller returns "personList"
     │ 3. which View for that name? ──►  ViewResolver         (name → View object)
     │ 4. render                    ──►  View.render(model)   (JSP/Thymeleaf produces HTML)
     ▼
  response  ◄── written by the DispatcherServlet
Missed in #1 and #8 · What the DispatcherServlet does twice

It coordinates and produces the response — it never resolves views or calls a View directly

#1: you had it delegating straight to Views. #8: you under-selected, taking one of the two correct statements. Both times the issue was which job belongs to which component.
JobComponent
Receive every requestDispatcherServlet — it extends HttpServlet
Find the handler for the URLHandlerMapping
Invoke the handler, resolve its argumentsHandlerAdapter
Turn a view name into a ViewViewResolver
Render the model into the responsethe View
Handle exceptions from any of the aboveHandlerExceptionResolver
Send the finished responseDispatcherServlet

The registration fact, which was #8's fourth option: @EnableWebMvc does not register a DispatcherServlet. It only switches on the MVC infrastructure beans. The servlet is registered by Boot's auto-configuration (mapped to /) or by hand in web.xml / a WebApplicationInitializer.

And from #1: the DispatcherServlet never goes straight to a View. It always goes through a handler first — that indirection is what makes it a front controller rather than a router.

Memory hook: DispatcherServlet coordinates and responds. HandlerMapping finds · HandlerAdapter invokes · ViewResolver resolves · View renders.
Missed in #1 · MVC itself

MVC is a design pattern; Spring MVC is a framework that implements it

Model–View–Controller is a separation-of-concerns pattern that long predates Spring:

RoleResponsibilityIn Spring MVC
Modelthe data and business stateModel, ModelMap, your domain objects
Viewrendering the dataJSP, Thymeleaf, a View implementation
Controllerhandling input, choosing model + viewyour @Controller methods

Spring MVC adds the front controller pattern on top — a single DispatcherServlet that all requests pass through, which is what enables centralised handler mapping, argument resolution, exception handling and view resolution.

Memory hook: MVC = the pattern. Spring MVC = a framework implementing it, with a front controller on top.

④ Binding request data 2 misses

Missed in #5 · Deserialising the request body taught in #3

@RequestBody@RequestMapping only maps the URL

Your answer: @RequestMapping. That routes the request; it never touches the body.

Keep the four apart by which part of the request each one reads — that's master table 3, repeated because it's worth over-learning:

AnnotationReadsExample
@RequestBodythe body, via an HttpMessageConvertercreate(@RequestBody User u)
@RequestParamquery string / form fields?page=2
@PathVariablea URI template segment/users/{id}
@RequestHeadera headerAuthorization
@ModelAttributeform fields bound onto an objectHTML form POST
@RequestMappingnothing — it maps the handler to a URL@RequestMapping("/api")

The distractors in that question were a Jackson annotation (@JsonDeserialize, which customises how a type deserialises, not where the data comes from) and an invented one (@RequestDeserialize). Combine @RequestBody with @Valid for bean validation, and note the body stream can only be read once.

Memory hook: Body ⇒ @RequestBody. Mapping ⇒ @RequestMapping. Jackson annotations shape types; they don't bind parameters.
Bank · @PathVariable naming and failure modes

The name defaults to the parameter name — and missing ⇒ 404, unconvertible ⇒ 400

When template variable and parameter share a name, the value is optional:

@GetMapping("/products/{id}")
public Product get(@PathVariable Long id) { … }                      // implicit

@GetMapping("/categories/{catId}/products/{prodId}")
public Product get(@PathVariable("catId") Long categoryId,           // names differ → explicit
                   @PathVariable Long prodId) { … }                  // names match → implicit

The catch: implicit matching needs parameter names in the class file, which requires the -parameters compiler flag. Boot's build plugins enable it; without it you get "Name for argument of type [java.lang.Long] not specified, and parameter name information not found in class file either."

The status codes are the testable part, and the logic is routing first, binding second — see master table 3. A missing segment never reaches your handler (404); a present-but-unconvertible one does (400); a wrong verb matches the path but not the mapping (405).

Two extras: @PathVariable Map<String,String> captures every template variable at once, and path variables are required by defaultrequired = false only makes sense when a second mapping omits the segment.

Memory hook: Names match ⇒ implicit. Missing segment ⇒ 404. Unconvertible ⇒ 400. Wrong verb ⇒ 405.

RestTemplate — the client side 2 misses

Missed in #7 and the bank · Custom request headers

Pass an HttpEntity holding HttpHeaders to exchange()

Your answer: "application.properties" and "set them on the RestTemplate instance". The second is the interesting error.

Why headers can't live on the template: RestTemplate is thread-safe once configured and normally a shared singleton bean. A per-request value stored on it would leak across threads — one user's token on another user's call. Per-request state must travel with the call.

HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);

HttpEntity<Void> entity = new HttpEntity<>(headers);      // headers only, no body

ResponseEntity<User> res = restTemplate.exchange(
        "/users/{id}", HttpMethod.GET, entity, User.class, 1);

Why exchange() specifically: the convenience methods have no header slot — getForObject(url, Class) and postForObject(url, body, Class) take no HttpEntity. If a question involves custom headers, the answer involves exchange().

ScopeMechanism
One requestHttpEntity passed to exchange()
Every request on this templatea ClientHttpRequestInterceptor
Defaults at build timeRestTemplateBuilder.defaultHeader(…)

The direction trap: @RequestHeader is server-side — it reads an incoming header in a controller. HttpEntity is client-side — it writes an outgoing one. The exam pairs them because both contain "header" and they point opposite ways.

The entity family: HttpEntity (headers + body) → RequestEntity adds method + URL; ResponseEntity adds the status code.

Memory hook: Shared singleton ⇒ per-request state goes in HttpEntity, via exchange(). Sending ⇒ HttpEntity. Reading ⇒ @RequestHeader.

⑥ Mapping annotations 1 miss

Bank · Which statement about @RequestMapping is incorrect

It does not default to GET — with no method, it maps every HTTP verb

This is the single most common misconception about the annotation, and the reason @GetMapping exists.
DeclarationHandles
@RequestMapping("/items")GET, POST, PUT, DELETE, PATCH — all of them
@RequestMapping(value="/items", method=RequestMethod.GET)GET only
@GetMapping("/items")GET only — identical to the line above

The five composed shortcuts — @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping — are each just @RequestMapping(method = …):

@Target(ElementType.METHOD)          // ← METHOD only
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping { … }

Three facts that follow from that @Target: (1) there is no @HeadMapping, @OptionsMapping or @TraceMapping — five shortcuts only; (2) the shortcuts are method-level only, so a class-level mapping must stay @RequestMapping, which is the only one targeting both TYPE and METHOD; (3) paths concatenate — class "/persons" + method "/list" = GET /persons/list.

Also true, and the other three options in that question: @RequestMapping accepts multiple paths (@RequestMapping({"/a","/b"})), and can be narrowed further by params, headers, consumes and produces.

Memory hook: Bare @RequestMapping = every verb. Five shortcuts (GET/POST/PUT/DELETE/PATCH), method-level only. Class level must be @RequestMapping. Paths concatenate.

MessageSource & internationalisation 1 miss

Bank · The purpose of MessageSource

Internationalisation — resolving messages by code, for a locale

"A store of common messages" is the near-miss: it's not a general message store, it's specifically locale-aware resolution.
// messages_en.properties → welcome.message=Hello, {0}!
// messages_fr.properties → welcome.message=Bonjour, {0} !

@Autowired MessageSource messageSource;

String msg = messageSource.getMessage(
        "welcome.message",              // code
        new Object[]{ "World" },        // {0} substitution
        Locale.FRENCH);                 // → "Bonjour, World !"
PieceRole
MessageSourcethe interface — getMessage(code, args, locale)
ResourceBundleMessageSourcethe usual implementation, backed by .properties bundles
ReloadableResourceBundleMessageSourcesame, but re-reads files without a restart
LocaleResolverdecides which locale a request uses (header, cookie, session)
LocaleChangeInterceptorlets ?lang=fr switch it

Two connections worth having. ApplicationContext itself extends MessageSource, which is why you can call getMessage() straight on the context — one of the standard "what does ApplicationContext add over BeanFactory?" answers. And Boot auto-configures one from messages.properties on the classpath, tunable via spring.messages.basename.

The distractor about "mapping error codes to messages" points at MessageCodesResolver, which generates codes like NotNull.user.email from validation failures — those codes are then looked up in a MessageSource. Related, but a different job.

Memory hook: MessageSource = i18n: code + args + locale → text. ApplicationContext extends it. LocaleResolver picks the locale.

The whole section on one page

21 facts, one line each — your pre-exam pass
How to use this page (1) Master tables 1 and 2 first. Return types and the two pipelines are 7 of these 21 and have cost you marks in five separate papers. Cover the right-hand column and reconstruct it — if you can do that, a third of the section is automatic. (2) Then the status-code rule. 404 = routing failed, 400 = binding failed, 405 = right path wrong verb. One sentence, and it answers a whole family of questions. (3) Do the drills, not the prose. There are 29 questions on this page. Reading builds recognition; answering builds recall, and only recall is tested. (4) Watch the answer count. Across these 21 you repeatedly ticked several options on a one-answer question. Decide how many the stem is asking for before you look at the options. (5) Come back on a three-day cycle. Spring MVC has never been above 75% in nine attempts, and the sections that stick are the ones you revisit — Testing has held 100% twice running for exactly that reason.
I'm your teacher — ask me anything. Say "drill MVC return types" for a rapid round on master tables 1 and 2, "drill the status codes" for the 404/400/405 family, or "quiz me on Spring MVC" for all 21 interleaved in exam wording. If you want the same treatment for Actuator — which has cost you marks in four consecutive papers — just ask.
← Dashboard Data Management clinic Lesson 11 · Spring MVC Lesson 12 · REST