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.
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.
Seven of your 21 misses are this table. Read the last column as the answer to "what does Spring do with it?"
| Return type | Valid? | What Spring does |
|---|---|---|
String | yes | a logical view name → handed to the ViewResolver |
View (JstlView, RedirectView…) | yes | the view object itself — bypasses the resolver |
ModelAndView | yes | model and view (or view name) together |
Model / Map | yes | model attributes; view name inferred from the URL |
void | yes | view name inferred from the URL, or you wrote the response yourself |
a domain object + @ResponseBody | yes | serialised by an HttpMessageConverter |
ResponseEntity<T> | yes | body + status + headers — the REST workhorse |
HttpHeaders | yes | a header-only response with an empty body (rare, but legal) |
HttpEntity<T> | yes | body + headers, no status |
an absolute path "/WEB-INF/views/home.jsp" | NO | treated as a logical name, so the resolver prefixes/suffixes it and it breaks |
HttpServletRequest / HttpServletResponse / ServletResponse | NO | these are parameters, injected in — never returned |
Object (bare, no @ResponseBody) | NO | carries no view contract — Spring can't tell what it is |
@RequestBody on the return type | NO | @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.
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 pipeline | Message-conversion pipeline | |
|---|---|---|
| Triggered by | @Controller, no @ResponseBody | @ResponseBody (or @RestController) |
return "list"; means | a view named list | the literal text list, as text/plain |
| Who converts | ViewResolver → View | HttpMessageConverter (Jackson for JSON) |
| Output | rendered HTML from a template | JSON / 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).
The status codes are the part people don't learn, and they're the most testable part.
| Annotation | Reads | Example | If it's missing |
|---|---|---|---|
@PathVariable | a URI template segment | /products/42 | 404 — the URL never matched the mapping |
@RequestParam | query string or form field | /products?id=42 | 400 — MissingServletRequestParameterException |
@RequestBody | the body, via a message converter | POST with JSON | 400 — nothing to deserialise |
@RequestHeader | a header | Authorization: … | 400 |
@RequestMapping | nothing — 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}") | Result | Failed at |
|---|---|---|
GET /products/42 | 200 | — |
GET /products/ | 404 Not Found | routing — no handler matched |
GET /products/abc | 400 Bad Request | binding — can't convert to Long |
POST /products/42 | 405 Method Not Allowed | routing — 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.
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.
void, String, ModelAndView, Model — servlet objects are parametersHttpServletRequest (#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.
void are both fineA returned String is always treated as a logical name, so the resolver's
prefix and suffix are applied to whatever you give it:
| You return | Resolver 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.
ViewResolver can resolveString, View, ModelAndView (and void) — bare Object cannotObject 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.
@RequestBody on a return type@RequestBody is input only; @ResponseBody is the output annotationpublic @RequestBody MyDataDto getMyData() as valid.
You correctly accepted HttpHeaders as a return type — the genuinely surprising
one.A matched pair pointing in opposite directions:
| Annotation | Direction | Legal on |
|---|---|---|
@RequestBody | IN — deserialise the request body | a method parameter, only |
@ResponseBody | OUT — serialise the return value | a 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.
| Signature | Verdict | Why |
|---|---|---|
getEmployeeName(@RequestParam String employeeId) | valid | name defaults to the parameter name |
createAccount(@RequestParam String accountId) | valid | same; ResponseEntity is a fine return type |
getUser(@PathVariable id, Model m) | invalid | won't compile — id has no type |
@PutMapping("/employees/{employeeId}")createEmployee(@PathVariable("id") String employeeId, …) | invalid | compiles, 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.
@RestController & REST 5 misses@RestController is also #3 and #4@Controller + @ResponseBody — which makes an explicit @ResponseBody redundant@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:
| Class | return "list"; produces | Content-Type |
|---|---|---|
@Controller | renders the view list | text/html |
@RestController | the four characters list | text/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.
@ResponseStatus// 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() { … }
| Claim | Verdict |
|---|---|
| Can set the status for a custom exception | TRUE |
| "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.
The six constraints these questions are drawn from:
| Constraint | Means |
|---|---|
| Stateless | the server holds no client session — every request carries what it needs |
| Client–server | separated concerns, independently evolvable |
| Uniform interface | standard verbs + resource URIs + self-describing messages |
| Cacheable | responses declare whether they can be cached |
| Layered system | proxies 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:
| Verb | Purpose | Safe? | Idempotent? | Typical success status |
|---|---|---|---|---|
GET | retrieve | yes | yes | 200 |
POST | create | no | no | 201 Created + Location |
PUT | replace | no | yes | 200 / 204 |
PATCH | partial update | no | no | 200 / 204 |
DELETE | remove | no | yes | 204 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".
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
DispatcherServlet does twice| Job | Component |
|---|---|
| Receive every request | DispatcherServlet — it extends HttpServlet |
| Find the handler for the URL | HandlerMapping |
| Invoke the handler, resolve its arguments | HandlerAdapter |
Turn a view name into a View | ViewResolver |
| Render the model into the response | the View |
| Handle exceptions from any of the above | HandlerExceptionResolver |
| Send the finished response | DispatcherServlet |
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.
Model–View–Controller is a separation-of-concerns pattern that long predates Spring:
| Role | Responsibility | In Spring MVC |
|---|---|---|
| Model | the data and business state | Model, ModelMap, your domain objects |
| View | rendering the data | JSP, Thymeleaf, a View implementation |
| Controller | handling input, choosing model + view | your @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.
@RequestBody — @RequestMapping only maps the URL@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:
| Annotation | Reads | Example |
|---|---|---|
@RequestBody | the body, via an HttpMessageConverter | create(@RequestBody User u) |
@RequestParam | query string / form fields | ?page=2 |
@PathVariable | a URI template segment | /users/{id} |
@RequestHeader | a header | Authorization |
@ModelAttribute | form fields bound onto an object | HTML form POST |
@RequestMapping | nothing — 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.
@PathVariable naming and failure modesWhen 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 default — required = false only makes sense when a
second mapping omits the segment.
RestTemplate — the client side 2 missesHttpEntity holding HttpHeaders to exchange()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().
| Scope | Mechanism |
|---|---|
| One request | HttpEntity passed to exchange() |
| Every request on this template | a ClientHttpRequestInterceptor |
| Defaults at build time | RestTemplateBuilder.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.
@RequestMapping is incorrectmethod, it maps every HTTP verb@GetMapping exists.| Declaration | Handles |
|---|---|
@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.
MessageSource & internationalisation 1 missMessageSource// 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 !"
| Piece | Role |
|---|---|
MessageSource | the interface — getMessage(code, args, locale) |
ResourceBundleMessageSource | the usual implementation, backed by .properties bundles |
ReloadableResourceBundleMessageSource | same, but re-reads files without a restart |
LocaleResolver | decides which locale a request uses (header, cookie, session) |
LocaleChangeInterceptor | lets ?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.
text/plain — not JSON, not a view name-parameters); name them only when they differexchange() per request · ClientHttpRequestInterceptor for all · never on the shared instance