(Editor’s note: At ~7,500 words, you probably don’t want to try reading this on a mobile device. Bookmark it and come back later.)
Introduction
What is Spring MVC?
Spring MVC is Spring’s web framework. It lets you build web sites or RESTful services (think: JSON/XML) and is nicely integrated into the Spring ecosystem, e.g. it powers the @Controllers and @RestControllers of your Spring Boot applications.
That doesn’t really help, does it?
Luckily, there’s also a long answer: The remainder of this document.
(If you are unsure of what Spring or Spring Boot is, you might want to read What Is Spring Framework?, first.)
Java Web Basics: HttpServlets
When writing web applications in Java, with or without Spring (MVC/Boot), you are mostly talking about writing applications that return two different data formats:
HTML → Your web app creates HTML pages that can be viewed in a browser.
JSON/XML → Your web app provides RESTful services, that produce JSON or XML. Javascript-heavy websites or even other web services can then consume the data that these services provide.
(Yes, there’s other data formats and use cases as well, but we’ll ignore them for now.)
How would you write such applications without any framework? Just with plain Java?
Answering this question is essential to really understanding Spring MVC, so do not just skip ahead because you think it has nothing to do with Spring MVC.
Answer
At the lowest level, every Java web application consists of one or more HttpServlets
. They generate your HTML, JSON, or XML.
In fact, (almost) every single framework of the 1 million available Java web frameworks (Spring MVC, Wicket, Struts) is built on top of HttpServlets.
How to write HTML pages with HttpServlets
Let’s have a look at a super simple HttpServlet that returns a very simple, static, HTML page.
package com.marcobehler.springmvcarticle;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyServletV1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (req.getRequestURI().equals("/")) {
resp.setContentType("text/html");
resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");
}
else {
throw new IllegalStateException("Help, I don't know what to do with this url");
}
}
}
Let’s break this down.
public class MyServletV1 extends HttpServlet {
Your servlet extends Java’s HttpServlet class.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
To handle (any) GET request, you need to override the doGet()
method from the superclass. For POST requests you would override doPost()
. Similarly, for all other HTTP methods.
if (req.getRequestURI().equals("/")) {
Your servlet needs to make sure that the URL that comes in is a request that it knows how to handle. For now, the servlet only handles "/", i.e.: it handles www.marcobehler.com
, but NOT www.marcobehler.com/hello
.
resp.setContentType("text/html");
You need to set the proper Content-Type on the ServletResponse to let the browser know what content you are sending. In this case, it’s HTML.
resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");
Remember: web sites are just HTML strings! So you need to generate an HTML string, any way you want, and send that back with the ServletResponse. One way of doing that is with the response’s writer.
After writing your servlet, you would register it with a servlet container, like Tomcat or Jetty. If you are using an embedded version of either servlet container, all the code needed to run your servlet would look like this:
package com.marcobehler.springmvcarticle;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Wrapper;
import org.apache.catalina.startup.Tomcat;
public class TomcatApplicationLauncher {
public static void main(String[] args) throws LifecycleException {
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector();
Context ctx = tomcat.addContext("", null);
Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());
servlet.setLoadOnStartup(1);
servlet.addMapping("/*");
tomcat.start();
}
}
Let’s break this down.
Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);
tomcat.getConnector();
You configure a new Tomcat server which will start on port 8080.
Context ctx = tomcat.addContext("", null);
Wrapper servlet = Tomcat.addServlet(ctx, "myServlet", new MyServletV2());
This is how you register your Servlet with Tomcat. This is the first part, where you simply tell Tomcat about your servlet.
servlet.addMapping("/*");
The second part is letting Tomcat know for what requests the servlet is responsible, i.e. the mapping. A mapping of /*
means it’s responsible for any incoming request (/users
, /register
, /checkout
).
tomcat.start();
That’s it. You run your main()
method now, go to port 8080 in your favorite web browser (http://localhost:8080/), and you’ll see a nice HTML page.
So, essentially, as long as you keep extending your doGet()
and doPost()
methods, your whole web application could consist of just one servlet. Let’s try that out.
How to write JSON endpoints with HttpServlets
Imagine that apart from your (pretty empty) HTML index page, you now also want to offer a REST API for your soon-to-be-developed front end. So your React or AngularJS front end would call a URL like this:
`/api/users/{userId}`
That endpoint should return data in JSON format for the user with the given userId. How could we enhance our MyServlet
to do this, again, no frameworks allowed?
package com.marcobehler.springmvcarticle;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyServletV2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (req.getRequestURI().equals("/")) {
resp.setContentType("text/html");
resp.getWriter().print("<html><head></head><body><h1>Welcome!</h1><p>This is a very cool page!</p></body></html>");
} else if (req.getRequestURI().startsWith("/api/users/")) {
Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1);
resp.setContentType("application/json");
// User user = dao.findUser(prettyFragileUserId)
// actually: jsonLibrary.toString(user)
resp.getWriter().print("{\n" +
" \"id\":" + prettyFragileUserId + ",\n" +
" \"age\": 55,\n" +
" \"name\" : \"John Doe\"\n" +
"}");
} else {
throw new IllegalStateException("Help, I don't know what to do with this url");
}
}
}
Let’s break this down.
} else if (req.getRequestURI().startsWith("/api/users/")) {
We add another if
to our doGet method, to handle the /api/users/
calls.
Integer prettyFragileUserId = Integer.valueOf(req.getRequestURI().lastIndexOf("/") + 1);
We do some extremely
fragile URL parsing. The last part of the URL is the userID, e.g. 5
for /api/users/5
. We just assume here that the user always passes in a valid int, which you would actually need to validate!
resp.setContentType("application/json");
Writing JSON to the browser means setting the correct content-type.
// User user = dao.findUser(prettyFragileUserId)
// actually: jsonLibrary.toString(user)
resp.getWriter().print("{\n" +
" \"id\":" + prettyFragileUserId + ",\n" +
" \"age\": 55,\n" +
" \"name\" : \"John Doe\"\n" +
"}");
Again, JSON is just text, so we can write that directly to the HTTPServletResponse. you would probably use a JSON library to convert our User Java object to this string, but for the sake of simplicity, I won’t show that here.
The problem with our One-Servlet-To-Rule-Them-All Approach
While our servlet above works, there are quite a few problems on the horizon:
Your Servlet needs to do a lot of manual HTTP-specific plumbing, checking request URIs, fumbling with strings, etc. In other words: it needs to know
WHAT
the users want to do.It then also needs to find the data for whatever you want to display. In other words: it needs to know the
HOW
. In our example above, that would be finding the user in a database, which we conveniently commented-out.It then also needs to convert that data to JSON or to HTML and set the appropriate response types.
Quite a lot of different responsibilities, eh? Wouldn’t it be nicer if you didn’t have to care about all that plumbing? No more request URI and parameter parsing, no more JSON conversions, no more servlet responses?
That’s exactly
where Spring MVC comes in.
What is Spring MVC’s DispatcherServlet?
What if I told you that Spring MVC is really just one servlet, like our uber-servlet above? (And yes, that’s of course a bit of a lie)
Meet the DispatcherServlet
.
Spring MVC’s DispatcherServlet handles every
incoming HTTP request (that’s it is also called front controller). Now, what does handle mean, exactly?
A sample HTTP request flow
Imagine a "register user workflow", where a user fills out a form and submits it to the server to get a nice little success HTML page back.
In that case, your DispatcherServlet needs to do the following things:
It needs to have a look at the incoming HTTP method request URI and any request parameters. E.g.:
POST /register?name=john&age33
.It needs to potentially convert the incoming data (request parameters/body) to nice little Java objects and forward them to a
@Controller
or@RestController
class that you wrote.Your
@Controller
method saves a new user to the database, maybe sends out an email, etc. It would highly likely delegate that to another service class, but let’s assume for now this happens inside the controller.It needs to take whatever the output from your @Controller was and convert it back to
HTML/JSON/XML
.
DispatcherServlet Overview
The whole process looks like this, with a fair number of intermediary classes neglected because DispatcherServlet doesn’t do all the work itself.
What’s a ModelAndView in the above graphic? How exactly does the DispatcherServlet convert the data?
It is easiest to understand by looking at a real-life example. For example: How do you write HTML websites with Spring MVC? Let’s find out in the next section.
How to write HTML with @Controllers
Whenever you want to write HTML to a client like a browser with Spring MVC (and that includes Spring Boot), you’ll want to write a @Controller class. Let’s do that now.
How to write a @Controller in Spring
For our user registration workflow from above (POST /register?name=john&age33
), you would write the following class.
package com.marcobehler.springmvcarticle;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class RegistrationController {
@PostMapping("/register")
public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {
User user = new User(name, age);
// TODO save user to database
// userDao.save(user);
// TODO send out registration email
// mailService.sendRegistrationEmail(user);
model.addAttribute("user", user);
return "registration-success";
}
}
Let’s break this down.
@Controller
public class RegistrationController {
A controller class in Spring is simply annotated with the @Controller
annotation, it does not need to implement a specific interface or extend from another class.
@PostMapping("/register")
This line tells our DispatcherServlet that whenever a POST request comes in for the path /register
, including any request parameters (e.g. ?username=), it should dispatch the request to this very controller method.
public String registerUser(@RequestParam(required = false) Integer age, @RequestParam String name, Model model) {
Note The naming of our method (registerUser
) does not really matter, it could be called anything.
We do however specify that each request should include two request parameters, which could either be part of the URL (?age=10&name=Joe
) or be in the POST request body. Furthermore, only the name
parameter is required (the age
parameter is optional)
And the age
parameter, if the user supplies it, is automatically converted to an Integer (an exception is thrown if the supplied value is not a valid Integer)
Last, but not least, Spring MVC automatically injects a model
parameter into our controller method. That model is a simple map where you need to put all the data that you want to show in your final HTML page, but more on that in a second.
User user = new User(name, age);
// TODO save user to database
// userDao.save(user);
// TODO send out registration email
// mailService.sendRegistrationEmail(user);
You do whatever you need to do with the incoming request data. Create a user, save it to a database, send out an email. This is your business logic.
model.addAttribute("user", user);
You add your user to the model, under key "user". Which means, you’ll be able to reference it in your HTML template later on, like "${user.name}". More on that in a second.
return "registration-success";
Your method returns a simple string, with the value registration-success
. This is not just any string, it is a reference to your view, i.e. the HTML template that you want Spring to render.
What do views look like?
Let’s ignore for now how (or rather where) Spring MVC will try and find that view, i.e. your template; instead, let’s see what your registration-success.html
template should look like.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="'Hello ' + ${user.name} + '!'"></p>
</body>
</html>
It’s just a simple HTML page, which contains one template-y line. It prints out the name of the user that has just registered.
<p th:text="'Hello ' + ${user.name} + '!'"></p>
The question is, what is this th:text=
syntax? Is that Spring-specific? Is it something else?
And the answer is that Spring MVC doesn’t really know anything about HTML templates. It needs a 3rd-party templating library to do all HTML templating work and doesn’t necessarily care what library you choose.
In the above case, you are looking at a Thymeleaf template, which is a very popular choice when working on Spring MVC projects.
Spring MVC & Templating Libraries
There are several different templating libraries that integrate well with Spring MVC that you can choose from: Thymeleaf, Velocity, Freemarker, Mustache and even JSP (even though that is not a templating library).
In fact, you must explicitly choose a templating library, because if you do not have such a templating library added to your project and configured correctly, then your @Controller method would not render your HTML page - because it wouldn’t know how to do it.
It also means that you have to learn and understand the syntax of the particular templating library depending on the project you’re, in because they’re all slightly different from each other. Fun, right?
What is a ViewResolver?
For a second, let’s think about where Spring will actually try and find your HTML templates that your @Controller returns.
The class that tries to find your template is called a ViewResolver
. So whenever a request comes into your Controller, Spring will have a look at the configured ViewResolvers and ask them, in order, to find a template with the given name. If you don’t have any ViewResolvers configured, this won’t work.
Imagine you want to integrate with Thymeleaf. Hence you would need a ThymeleafViewResolver.
package com.marcobehler.springmvcarticle;
import org.springframework.context.annotation.Bean;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
public class ThymeleafConfig {
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");
// some other lines neglected...
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
// some other lines neglected...
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
}
Let’s break this down.
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
In the end, a ThymeleafViewResolver simply implements Spring’s ViewResolver
interface. Given a template name (remember: registration-success
), ViewResolvers can find the actual template.
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
The ThymeleafViewResolver needs a couple of other Thymeleaf-specific classes to work properly. One of these classes is the SpringResourceTemplateResolver
. It does the actual work of finding your template.
Note
SpringResourceTemplateResolver is a Thymeleaf class
templateResolver.setPrefix("classpath:/templates");
templateResolver.setSuffix(".html");
You are basically saying (with help of the Spring Resources syntax): "All my templates are on the classpath, in the /templates
folder". And, by default, they all end with .html
. This means:
Whenever our @Controller returns a String like registration-success
, the ThymeleafViewResolver will look for a template: classpath:/templates/registration-success.html
.
Side Note: Spring MVC, Spring Boot & Controllers
You might be thinking: Marco, I’ve never had to configure such a ViewResolver in my entire life working on Spring Boot projects. And that is correct. Because Spring Boot automatically configures one for you, whenever you add a dependency such as spring-boot-starter-thymeleaf
to your project.
It also configures the ViewResolver to have a look at your src/main/resources/template
directory, by default.
So, Spring Boot really just pre-configures Spring MVC for you. Keep that in mind.
Summary: Model-View-Controller
Having seen a complete @Controller & ViewResolver example makes it much easier to talk about Spring’s Model-View-Controller concept.
With a couple of annotations (@Controller, @PostMapping, @RequestParam) you can write a
controller
that takes care of receiving request data and processes it accordingly.Your
model
contains all the data (and just the data) that you want to render in your view. It is your job to fill that model map.Your
view
is just an HTML template. It does not care about where you got the (model) data from. Or what the current HTTP request is. Or even whether you have an active HTTP Session or not.
It is all about separation of concerns.
While a bit annotation-heavy at first sight, our Spring @Controller class reads a lot better, with a lot less HTTP plumbing involved than our uber-servlet from the beginning.
More on @Controllers
We already saw a bit of the convenience that Spring MVC gives us when handling HTTP inputs.
You do not have to fumble with the requestURI, you can use an annotation instead.
You do not have to fumble with request parameter type conversions or if a parameter is optional or required, you can use an annotation instead.
Let’s have a look at the most common annotations that help you process incoming HTTP requests.
@GetMapping & @RequestMappping
You already saw the @GetMapping
annotation above. It is equal to the `@RequestMapping` annotation. Let’s see how:
@GetMapping("/books")
public void book() {
//
}
/* these two mappings are identical */
@RequestMapping(value = "/books", method = RequestMethod.GET)
public void book2() {
}
@GetMapping
, @[Post|Put|Delete|Patch]Mapping
is equivalent to @RequestMapping(method=XXX)
. It is simply a newer way (Spring 4.3+) of specifying a mapping, so you’ll find the @RequestMapping annotation used a lot in older, legacy Spring projects.
@RequestParam
For HTTP request parameters, be that in your URL (?key=value
) or in a submitted form request body, can be read in via the @RequestParam
annotation.
You already saw that it does basic type conversion (e.g. from HTTP String parameter to an int) as well as checking for required or optional parameters.
@PostMapping("/users") /* First Param is optional */
public User createUser(@RequestParam(required = false) Integer age, @RequestParam String name) {
// does not matter
}
If you forget to provide a required parameter with your request, you’ll get a 400 Bad Request
response code and, if using Spring Boot, a default error object that looks like this:
{"timestamp":"2020-04-26T08:34:34.441+0000","status":400,"error":"Bad Request","message":"Required Integer parameter 'age' is not present","path":"/users"}
If you want even more convenience, you can let Spring directly convert all @RequestParams to an object, without any needed annotations. Simply specify your object
as `method parameter`.
You just need to make sure your class has corresponding getters/setters.
@PostMapping("/users") /* Spring will convert this automatically if you have getters and setters */
public User createUser(UserDto userDto) {
//
}
@PathVariable
Next to request parameters, another popular way of specifying variables would be directly in the request URI, as a @PathVariable
. So for getting a user profile with userId=123
, you would call the following URL:
GET /users/123
@GetMapping("/users/{userId}")
public User getUser(@PathVariable String userId) {
// ...
return user;
}
- You just need to make sure that your parameter value matches the one between the
{}
in your request mapping annotation.
In addition, PathVariables
can also be required or optional.
@GetMapping("/users/{userId}")
public User getUser(@PathVariable(required = false) String userId) {
// ...
return user;
}
And PathVariables could, of course, be directly translated to a Java object (provided, the object has matching getters/setters).
@GetMapping("/users/{userId}")
public User getUser(UserDto userDto) {
// ...
return user;
}
Summary: @Controllers
In short, when writing HTML pages with Spring MVC you’ll have to do just a few things:
Write your @Controllers, sprinkled with a couple of annotations. Spring will take care to present you the request input (request params, path variables) in a convenient manner.
Execute whatever logic you need to fill your model. You can conveniently inject the model into any controller method.
Let your @Controller know which HTML template you want rendered and return the template’s name as a string.
Whenever a request comes in, Spring will make sure to call your controller method, and take the resulting model and view, render that to an HTML string and return it back to the browser.
(Provided of course, you set up an appropriate templating library, which Spring Boot will automatically do for you, as long as you add the needed dependencies to your project.)
That’s it.
How to write XML/JSON with @RestControllers
Things are a bit different when you are writing RESTFul services. Your client, be that a browser or another web service, will (usually) create JSON or XML requests. The client sends, say, a JSON request, you process it, and then the sender expects JSON back.
So, a sender might send you this piece of JSON as part of the HTTP request body.
POST http://localhost:8080/users
###
{"email": "angela@merkel.de"}
But on the Java side (in your Spring MVC program) you do not want to mess with raw JSON strings. Neither when receiving requests like above, nor when sending responses back to the client.
Instead, you’d like to just have Java objects that get converted automatically, by Spring.
public class UserDto {
private String email;
//...
}
This also means that you do not need all that model and view processing that you had to do when rendering HTML in your @Controllers. For RESTful services you don’t have a templating library reading in an HTML template and filling it with model data to generate a JSON response for you.
Instead, you want to go directly from HTTP Request → Java object and from Java Object → HTTP response.
As you might have guessed, that’s exactly what Spring MVC allows you to do, by writing @RestControllers
.
How to write @RestControllers
The first thing you need to do to output XML/JSON is to write a @RestController
instead of a @Controller. (Although @RestController IS a @Controller, see FAQ for what the exact difference is).
If we were to write a REST Controller for a bank, that returns a user’s transaction list, it could look something like this:
package com.marcobehler.springmvcarticle;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
@RestController
public class BankController {
@GetMapping("/transactions/{userId}")
public List<Transaction> transactions(String userId) {
// find transaction by user
// List<Transaction> = dao.findByUserId(userId);
List<Transaction> transactions = Collections.emptyList();
return transactions;
}
}
Let’s break it down.
@RestController
public class BankController {
You annotated the BankController class with the @RestController
annotation, which signals Spring that you do not want to write HTML pages through the usual ModelAndView process.
Instead, you want to write XML/JSON (or some other format) directly into the HTTP response body.
public List<Transaction> transactions(String userId) {
Your controller does not return a String (view) anymore. Instead, it returns a List<Transaction>
, that you want Spring to convert to an appropriate JSON or XML structure. You basically want your Transaction Java objects to become this (someone was hungry for fast food very early in the morning):
[
{
"occurred": "28.04.2020 03:18",
"description": "McDonalds - Binging",
"id": 1,
"amount": 10
},
{
"occurred": "28.04.2020 06:18",
"description": "Burger King - Never enough",
"id": 2,
"amount": 15
}
]
But how would Spring MVC know that your transaction list should get converted to JSON? Why not XML? Or YAML? How does your @RestController method know what the supposed response format should be?
For that, Spring has the concept of Content Negotiation
.
How (Response) Content Negotiation works: Accept Header
In short, content negotiation means that the client needs to tell your server what response format it wants to get back from your @RestController.
How? By specifying the Accept
header with the HTTP request.
GET http://localhost:8080/transactions/{userid}
Accept: application/json
Spring MVC will have a look at that Accept
header and know: The client wants JSON (application/json) back, so I need to convert my List<Transaction>
to JSON. (Quick note: there are other ways to do content negotiation, but the Accept header is the default way.)
Let’s call this response content negotiation, because it is about the data format of the HTTP response that you are sending back to your client.
But content negotiation also works for incoming requests. Let’s see how.
How Request Content Negotiation works: Content-Type Header
When building RESTful APIs, there’s an extremely high chance that you also want your clients to be able to send in JSON, or XML. Let’s pick up the example from the beginning of the chapter again, where you offer a REST endpoint to register new users:
POST http://localhost:8080/users
###
{"email": "angela@merkel.de"}
How would Spring know that the request body above contains JSON and not XML or YAML?
You might have guessed right, you’ll need to add another header, this time it’s the Content-Type
header.
POST ...
Content-Type: application/json; charset=UTF-8
###
...
What would the corresponding @RestController method for that request look like?
package com.marcobehler.springmvcarticle;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BookingController {
@PostMapping("/transactions")
public Transaction transaction(@RequestBody TransactionDto dto) {
// do something with the dto..create the booking..convert it to a transaction
Transaction transaction = null;
return transaction;
}
}
Let’s break it down.
public Transaction transaction(@RequestBody TransactionDto dto) {
Similar to @RequestParam or @Pathvariable, you’ll need another annotation, called @RequestBody
.
@RequestBody in combination with the correct Content-Type
will signal Spring that it needs to have a look at the HTTP request body and convert it to whatever Content-Type the user specified: JSON in our case.
// do something with the dto..create the booking..convert it to a transaction
Transaction transaction = null;
return transaction;
}
Your method then doesn’t have to care about the raw JSON string anymore, it can simply work with the TransactionDTO, save it to the database, convert it to a Transaction object, anything you want.
That is the power of Spring MVC.
How does Spring convert data formats?
There’s only one small problem: Spring knows about the Accept and Content-Type headers, but it does not know how to convert between Java Objects and JSON. Or XML. Or YAML.
It needs an appropriate 3rd-party library to do the dirty work (also called marshalling/unmarshalling
or serialization/deserialization
.)
And the classes that integrate between Spring MVC and these 3rd-party libaries are called HttpMessageConverters
.
What is a HttpMessageConverter?
An HttpMessageConverter is an interface with four methods (note, I simplified the interface a bit for an easier explanation, as it looks a bit more advanced in real life).
canRead(MediaType) → Can this converter read (JSON|XML|YAML|etc)? The MediaType passed in here is typically the value from the
Content-Type
request header.canWrite(MediaType) → Can this converter write (JSON|XML|YAML|etc)? The MediaType passed in here is typically the value from the
Accept
request header.read(Object, InputStream, MediaType) → Read my Java object from the (JSON|XML|YAML|etc.) InputStream
write(Object, OutputStream, MediaType) → Write my Java object to the OutputStream as (JSON|XML|YAML|etc.)
In short, a MessageConverter needs to know what MediaTypes it supports (think: application/json) and then needs to implement two methods to do the actual reading/writing in that data format.
Which HttpMessageConverters are there?
Luckily, you don’t need to write these message converters yourself. Spring MVC comes with a class that automatically registers a couple of default HTTPMessageConverters for you - if you have the appropriate 3rd-party libraries on the classpath.
If you don’t know about this, it’ll sound like magic. In any case, have a look at Spring’s AllEncompassingFormHttpMessageConverter
(I love the name).
static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}
Let’s break this down.
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
Spring MVC checks if the class javax.xml.bind.Binder
is present and if so, assumes you have added a needed library to your project to do JAXB conversions.
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
Spring MVC checks if two classes ..jackson..ObjectMapper
and ..jackson..JsonGenerator
are present and if so assumes that you have added Jackson to your project in order to do the JSON conversions.
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
Spring MVC checks if the class ..jackson..XmlMapper
is present and if so assumes that you have added Jackson’s XML support to your project in order to do the XML conversions.
And so on. And a couple of lines later, Spring simply adds an HttpMessageConverter for each library it 'detected'.
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
Side-Note: Spring MVC, Spring Boot & RestControllers
When building Spring Boot projects, you’ll automatically use Spring MVC under the hood. But Spring Boot also pulls in Jackson by default.
That is the reason why you can immediately write JSON endpoints with Spring Boot, because the correct HttpMessageConverts will be added automatically for you.
Summary: @RestControllers
Compared with the HTML flow, the JSON/XML flow is a bit simpler, as you bypass all that Model and View rendering.
Instead, your @Controllers directly return Java objects, which Spring MVC will conveniently serialize to JSON/XML or any other format that the user requested with the help of HttpMessageConverters.
You need to make sure of two things, however:
Have the appropriate 3rd party libraries on the classpath.
Make sure to send in the correct
Accept
orContent-Type
headers with every request.
FAQ
Did you publish the source code for this article somewhere?
You can find the working source code for most of this article in the following GitHub repository:
https://github.com/marcobehler/spring-mvc-article
Simply clone the project and run the SpringMvcArticleApplication
class to start up the web application.
What is the difference between Spring MVC and Spring Boot?
In short: There is no difference, Spring Boot uses and builds on top of Spring MVC.
For a more thorough explanation, you’ll need to read my What is Spring Framework? article first.
What is the fastest way to create a new Spring MVC application?
Unless you want to use Spring MVC without it (quite rare, nowadays), the quickest way will be to create a new Spring Boot project.
Go to https://start.spring.io/.
Make sure to select
Spring Web
as a dependency for your new project.
This will let you build web/RESTful applications with Spring MVC.
What kind of HTTP request input does Spring MVC understand?
Spring MVC understands basically everything that HTTP offers - with the help of third-party libraries.
That means you can throw JSON, XML, or HTTP (Multipart) Fileuploads request bodies at it, and Spring will conveniently convert that input to Java objects.
What kind of HTTP responses can Spring MVC write?
Spring MVC can write anything that you want into an HttpServletResponse - with the help of 3rd-party libraries.
Be that HTML, JSON, XML or even WebSocket response bodies. Even better, it takes your Java objects and generates those response bodies for you.
What is the difference between a @Controller and @RestController
@Controllers
, by default, return HTML to your users with the help of a templating library, unless you add the @ResponseBody annotation to specific methods, which let you return XML/JSON as well.@RestController’s
source code shows that it actually is a @Controller, with the added @ResponseBody annotation. Which is equivalent to writing @Controllers which have @ResponseBody annotated on every single method.
@Controller
@ResponseBody
public @interface RestController {
- Therefore, @RestControllers, by default, return XML/JSON, instead of HTML.
Note
XML and JSON are simply the most popular data formats you’ll use in a Spring MVC application. Your @Controllers/@RestControllers could, however, return anything else, like YAML for example. You’ll only need to make sure to have the right
HttpMessageConverter
registered in your ApplicationContext.
Which templating library should I choose?
Over the years I have personally worked with almost all templating libraries and while there is a certain push to use Thymeleaf in Spring projects, I have no strong preference. So, either go with Thymeleaf (if you don’t have experience with any other framework) or choose the one you are most comfortable with.
Why does my @Controller output 404? All mappings are correct.
A relatively common mistake is to have a @Controller
returning objects that you want to get converted to JSON or XML, but you are missing the @ResponseBody annotation.
Spring will return a rather meaningless 404 Not Found
exception in that case.
package com.marcobehler.springmvcarticle;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class _404WithMissingResponseBodyController {
@GetMapping("/users/{id}") /* This won't work and lead to a 404 */
public User getUser_404(@PathVariable String id) {
return new User("Everyone's name is John", id);
}
@GetMapping("/users2/{id}")
@ResponseBody /* This will work */
public User getUser_200(@PathVariable String id) {
return new User("Everyone's name is John", id);
}
}
Fix: Add @ResponseBody
or turn your @Controller
into a @RestController
.
What happens if you define the same request mapping for two different methods?
If the two methods have different HTTP methods, it won’t be a problem.
/* this works */
@PostMapping("/users")
public void method1() {
}
@GetMapping("/users")
publi void method(2) {
}
If, however, you map the same HTTP methods to the same path, you will get a problem.
/* this won't work */
@PostMapping("/users")
public void method1() {
}
@PostMapping("/users")
public void method(2) {
}
On application startup, this will lead to an IllegalStateException
, hinting to your ambiguous mapping.
Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'howToPassAndRetrieveRequestParametersController' method
com.marcobehler.springmvcarticle.HowToPassAndRetrieveRequestParametersController#createUser(User)
to {POST /users3}: There is already 'howToPassAndRetrieveRequestParametersController' bean method
Do I need to URL encode @RequestParams?
Yes, because Spring automatically URL decodes them. A common error:
Imagine your application sends out confirmation emails whenever a new user signs up and a user signs up with a "+" sign in his email address, like marco+wantsnospam@marcobehler.com.
@GetMapping("/confirm")
public void confirm(@RequestParam String email, @RequestParam String token){
// confirm user...
}
If you forgot to properly URL encode the + sign in your confirmation mail and send the string as is to your @Controller, which value will the email @RequestParam contain?
It will be "marco[space]wantsnospam@marcobehler.com", as Spring will replace the + with a space, which is proper RFC3986 handling.
Fix: Make sure that the URLs you feed to your application are properly encoded: , as Spring will decode them automatically.
How to access the user’s current HttpSession?
In your Spring MVC @Controller or @RestController, you can simply specify the HttpSession as a method argument, and Spring will automatically inject it (creating one if it doesn’t yet exist)
@RestController
public class HttpSessionController {
@GetMapping("/session")
public String getSession(HttpSession httpSession) {
System.out.println("httpSession = " + httpSession);
return httpSession.getId();
}
}
You cannot do that with random @Components or @Services, but you are still able to @Autowire the HttpSession into them.
@Service
class SomeOtherService {
@Autowired
private HttpSession httpSession;
public HttpSession getHttpSession() {
return httpSession;
}
}
How to access the HttpServletRequest?
In your Spring MVC @Controller or @RestController, you can simply specify the HttpServletRequest as a method argument and Spring will automatically inject it (creating one if it doesn’t yet exist)
@RestController
public class HttpServletRequestController {
@Autowired
private SomeRequestService someRequestService;
@GetMapping("/request")
public String getRequest(HttpServletRequest request) {
System.out.println("request = " + request);
return request.toString();
}
}
You cannot do that with random @Components or @Services, but you are still able to @Autowire the HttpServletRequest into them.
@Service
class SomeRequestService {
@Autowired
private HttpServletRequest httpServletRequest;
public HttpServletRequest getRequest() {
return httpServletRequest;
}
}
How to read HTTP Headers?
There’s a variety of ways to access the request headers, depending on whether you want just a single one or a map with all of them. In either case you need to annotate them with @RequestHeader.
Whatever version you pick, try to be consistent with your choice.
package com.marcobehler.springmvcarticle;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import java.util.Map;
@Controller
public class HttpHeaderController {
@GetMapping("/headers1")
public void singleHeader(@RequestHeader("x-forwarded-for") String xForwardedFor) {
// ...
}
@GetMapping("/headers2")
public void headersAsMap(@RequestHeader Map<String,String> headers) { // or MultiValueMap<String,String>
// ...
}
@GetMapping("/headers3")
public void headersAsObject(HttpHeaders headers) {
// ...
}
}
How to read and write cookies?
For reading cookies you can use the @CookieValue
annotation in your controllers. You’ll have to write cookies directly to the HttpServletResponse.
package com.marcobehler.springmvcarticle;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
@Controller
public class CookieController {
@GetMapping("/cookie")
public void handle(@CookieValue("JSESSIONID") String cookie, HttpServletResponse response) {
response.addCookie(new Cookie("SOME_COOKIE_NAME", "This is a crazy new cookie!"));
//...
}
}
How to get the user’s IP address?
This is a trick question. There is a method, called httpServletRequest.getRemoteAddr()
, which, however, only returns you the IP of the user or
the last proxy that sent the request, which 99,99% of the case is your Nginx or Apache.
Hence, you’ll need to parse the X-Forwarded-For
header for the correct IP address. But what happens if your application, in addition, runs behind a CDN, like CloudFront? Then your X-Forwarded-For would look like this:
X-Forwarded-For: maybeSomeSpoofedIp, realIp, cloudFrontIp
The problem is, you cannot read the header from left to right, as users could provide and therefore spoof their own X-Forwarded-For header. You always have to go from right
to left, and exclude
all known IP addresses. In the case of CloudFront, this means you’ll need to know the CloudFront IP ranges and remove them from the header. Yup!
This leads to quite elaborate IP-resolving code. Guess how many projects get that wrong!
package com.marcobehler.springmvcarticle;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class IpController {
private static final String[] HEADERS_TO_TRY = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR"};
@GetMapping("/ip")
public String getClientIpAddress(HttpServletRequest request) {
for (String header : HEADERS_TO_TRY) {
String ip = request.getHeader(header);
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
return getRealClientIpAddress(ip);
}
}
return request.getRemoteAddr();
}
/**
* Goes through the supplied ip string (could be one or multiple). Traverses it through the right side...
* and removes any known ip address ranges
*
* @param ipString
* @return
*/
public String getRealClientIpAddress(String ipString) {
String[] manyPossibleIps = ipString.split(",");
for (int i = manyPossibleIps.length - 1; i >= 0; i--) {
String rightMostIp = manyPossibleIps[i].trim();
if (isKnownAddress(rightMostIp)) {
continue; // skip this ip as it is trusted
} else {
return rightMostIp;
}
}
return ipString;
}
private boolean isKnownAddress(String rightMostIp) {
// do your check here..for cloudfront you'd need to download their ip address ranges
// from e.g. http://d7uri8nf7uskq.cloudfront.net/tools/list-cloudfront-ips
// and compare the current ip against them
return false;
}
}
How can you handle file uploads in a Spring MVC application?
Given that you have the proper HTML file upload form, which reads something like this:
<form method="POST" enctype="multipart/form-data" action="/upload">
File to upload:<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
You simply need a @Controller with a corresponding @PostMapping and a MultiPartFile parameter, which contains your upload data and convenient methods to store the file on your disk.
package com.marcobehler.springmvcarticle;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@Controller
public class FileUploadController {
@PostMapping("/upload")
public String handleFileUpload(@RequestParam MultipartFile file) throws IOException {
// don't generate upload files like this in a real project.
// give them random names and save their uploaded name as metadata in a database or similar
final Path uploadDestination = Paths.get("C:\\uploads").resolve(file.getName());
file.transferTo(uploadDestination);
return "redirect:/";
}
}
How to handle byte (xls, pdf, csv, jpg, zip file) downloads with Spring Controllers?
There’s a variety of ways to get this working, from writing directly to the HttpServletResponse or returning a byte[] as a result.
However, the most Spring-y and flexible version is by returning *ResponseEntity<Resource>*\
s. Depending on where you stored the file, you would use a different resource.
On a disk → FileSystemResource
On your project’s classpath → ClassPathResource
Stream it from "somewhere" → InputStreamResource
Have it available as byte[] in memory → ByteArrayResource
All that’s left to do then, is set the appropriate response HTTP headers (filename, content-type, etc).
package com.marcobehler.springmvcarticle;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
@Controller
public class FileDownloadController {
@RequestMapping(value = "/download/{jpgName}", method = RequestMethod.GET)
public ResponseEntity<Resource> downloadJpg(
@PathVariable String jpgName) throws IOException {
// Resource downloadResource = new InputStreamResource(soimeinputStream)
// Resource downloadResource = new ByteArrayResource(someByteArray)
// Resource downloadResource = new FileSystemResource(someFile)
final ClassPathResource downloadResource = new ClassPathResource(jpgName);
if (!downloadResource.exists()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
HttpHeaders headers = new HttpHeaders();
// 1. set the correct content type
headers.setContentType(MediaType.IMAGE_JPEG);
// 2. set the correct content length, maybe stored in a db table
headers.setContentLength(downloadResource.contentLength());
// 3. if you want to force downloads, otherwise attachments might be displayed directly in the brwoser
headers.setContentDispositionFormData("attachment", jpgName);
return new ResponseEntity<>(downloadResource, headers, HttpStatus.OK);
}
}
How can I handle exceptions from my @Controllers globally?
There’s literally a gazillion ways of handling exceptions with Spring MVC, if you don’t want to handle them directly in your @Controller methods, but rather in one central place.
Create a @ControllerAdvice
or @RestControllerAdvice
class, in combination with the @ResponseStatus and @ExceptionHandler annotations. A couple of notes:
You might guess the difference between these two by understanding the difference between @Controllers and @RestControllers.
@ResponseStatus lets you define the HTTP Status code that should be returned to the client after handling your exception.
@ExceptionHandler specifies the exception that should trigger your handler method.
Other than that, it’s like writing a normal @Controller or @RestController.
package com.marcobehler.springmvcarticle;
import org.springframework.http.HttpStatus;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class GlobalControllerExceptionHandler {
@ResponseStatus(HttpStatus.CONFLICT) // 409
@ExceptionHandler(SomeConflictException.class)
public String handleConflict(SomeConflictException e, Model model) {
// do something
model.addAttribute("message", e.getMessage());
return "new-template";
}
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED) // 409
@ExceptionHandler(NotYetImplementedExceptoin.class)
public void handleBandwithLimitExceeded(NotYetImplementedExceptoin e) {
// do nothing;
}
}
How to return any status code (400, 404, etc) from your @Controllers?
Throw a ResponseStatusException
, with the appropriate status code and possibly a reason.
An alternative would be returning a ResponseEntity object, but the exception is nicer in most cases.
package com.marcobehler.springmvcarticle;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.server.ResponseStatusException;
@Controller
public class HttpStatusCodeController {
@GetMapping("/somePath")
public void alwaysThrowsException() {
//throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Meeepp, not found.");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Well, that just wasn't right!");
}
}
What about concept XYZ of Spring MVC?
The official Spring MVC’s documentation literally contains hundreds of pages describing how the web framework works.
So, in case you want to know more about Models, Views, ViewHandlers, InitBinders, RootContexts, Filters, Caching, etc., I invite you to check it out. It is simply not in the scope of this guide to cover everything.
Fin
That was quite a ride. In the end, I hope that you have taken a couple of things away from this article:
Spring MVC is a good old MVC framework that lets you, rather easily, write HTML web sites or JSON/XML web services.
It integrates nicely with a lot of templating libraries and data conversion libraries, as well as with the rest of the Spring ecosystem, like Spring Boot.
It mainly allows you to focus on writing your business logic, without having to worry too much about servlet plumbing code, HTTP request/response parsing, and data conversion.
That’s it for today. Thanks for reading.
Acknowledgments
A big "thank you" goes out to Patricio "Pato" Moschcovich, who not only did the proofreading for this article but also provided invaluable feedback!
Top comments (0)