Appearance
Why Spring Boot: From Servlets to Spring MVC to Spring Boot
To understand why Spring Boot exists, you need to know what came before it. Java web development went through three stages, and each one fixed the problems of the one before:
This chapter walks through the three stages in that order. At the end you run a Spring Boot application yourself.
Stage 1: Servlets
Servlets were the foundation of Java web applications, and Spring still runs on top of them today.
- A Servlet is a Java class that handles a client request, processes it and returns the response.
- A Servlet container (for example Tomcat) manages the servlets: it receives each request and decides which servlet handles it.
Writing a servlet
A servlet extends HttpServlet and overrides one method per HTTP verb: doGet, doPost, doPut, doDelete.
java
@WebServlet("/demoservlet1/*")
public class DemoServlet1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getPathInfo();
if ("/firstendpoint".equals(path)) {
// handle GET /demoservlet1/firstendpoint
} else if ("/secondendpoint".equals(path)) {
// handle GET /demoservlet1/secondendpoint
}
// ...one more else-if for every GET endpoint this servlet handles
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
// every POST endpoint of this servlet, again routed with if/else
}
}A servlet has only one doGet. If it handles ten GET endpoints, all ten are branches of one if/else chain.
An application has many servlets (DemoServlet1, DemoServlet2, ...). Something must say which servlet handles which URL.
web.xml: mapping URLs to servlets
That something is web.xml, the deployment descriptor:
xml
<web-app>
<servlet>
<servlet-name>DemoServlet1</servlet-name>
<servlet-class>com.example.DemoServlet1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DemoServlet1</servlet-name>
<url-pattern>/demoservlet1/*</url-pattern>
</servlet-mapping>
<!-- the same two blocks again for DemoServlet2, DemoServlet3, ... -->
<!-- plus filters, their order, error pages, ... -->
</web-app>(Servlet 3.0 added the @WebServlet annotation used above as an alternative, but large applications of that era kept their mappings and filters in web.xml.)
How a request flows
You package the application as a WAR file and deploy it into Tomcat. Then, for each request:
- The request reaches Tomcat.
- Tomcat looks up
web.xmlto find the servlet mapped to this URL. - It calls that servlet's method for the HTTP verb (
doGetfor a GET). - Inside, the
if/elsechain picks the code for the exact path, which builds the response.
What was painful
web.xmlgrows huge. A production application has hundreds of servlets, andweb.xmlalso holds filters and their order. It becomes hard to read and maintain.- Unit testing is hard (tight coupling). Classes create their dependencies with
new, so a test cannot replace them with mocks (shown below). - REST APIs are hard to organise. One
doGetper servlet means routing by hand withif/elseon the path. - WAR packaging and deployment into a separately installed server for every change.
Stage 2: Spring MVC
The Spring Framework is a family of projects; Spring MVC is its web part. It fixed the servlet problems one by one.
Fix 1: annotations instead of web.xml
Mappings are written as annotations right on the class and method that handle them:
java
@Controller
@RequestMapping("/payment")
public class PaymentController {
@GetMapping("/getpayment")
public String getPayment() {
return "payment details";
}
@GetMapping("/getrefund")
public String getRefund() {
return "refund details";
}
}No web.xml mapping, and no if/else routing: every endpoint gets its own method and its own annotation. This is also fix 3, organised REST APIs. (The annotations themselves are chapter 5.)
Fix 2: Inversion of Control and dependency injection
Inversion of Control (IoC) is a flexible way to manage objects, their dependencies and their lifecycle: the framework creates them, not your code. Dependency injection (DI) is how IoC is implemented, so the two terms are often used interchangeably.
Here is the problem it solves:
java
public class Payment {
User sender = new User(); // Payment creates its own dependency
String getSenderDetails(String id) {
return sender.getUserDetail(id);
}
}To unit test getSenderDetails, you want to test only Payment and replace User with a mock that returns fixed data. But Payment always creates a real User with new, so there is no way to slip a mock in. The two classes are tightly coupled.
With Spring:
java
@Component
public class User {
public String getUserDetail(String id) { ... }
}
@Component
public class Payment {
@Autowired
User sender; // Spring supplies this object
String getSenderDetails(String id) {
return sender.getUserDetail(id);
}
}@Componenttells Spring: create and manage an object of this class.@Autowiredtells Spring: when you create this object, fill this field with the right object.
Payment no longer decides which User it gets; Spring does (that is the "inversion"). In a unit test you can therefore hand Payment a mock User. Chapters 6 and 7 cover beans and dependency injection in depth.
Integrations
Spring also integrates with the tools a real application needs, and you choose the combination: JUnit and Mockito for testing, JDBC, JPA and Hibernate for data, plus asynchronous processing, caching, messaging and security.
How a request flows in Spring MVC
- Tomcat passes every request to one servlet that Spring provides: the
DispatcherServlet, also called the front controller. - The
DispatcherServletuses handler mapping, built from your@RequestMapping/@GetMappingannotations, to find which controller and which method handle this URL. - It calls that method on the controller object. That object is a bean: the IoC container created it once, at startup, and injected its
@Autowireddependencies then. (It is not created again for each request.) - The method's return value becomes the response.
What was still painful
Even a tiny Spring MVC application needs three kinds of setup besides the controller.
1. pom.xml with a version for every dependency:
xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>A real application needs 15 to 20 of these. Upgrading one (say JUnit) raises the question: does the new version still work with all the others? You have to check compatibility yourself.
2. A configuration class:
java
@Configuration
@EnableWebMvc // load Spring MVC's infrastructure
@ComponentScan("com.example") // where to look for controllers and beans
public class AppConfig implements WebMvcConfigurer {
}3. A class that registers the DispatcherServlet:
java
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() { return null; }
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { AppConfig.class }; // which configuration to load
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" }; // send every request to the DispatcherServlet
}
}4. And still a WAR, deployed into an installed Tomcat.
Stage 3: Spring Boot
Spring Boot is a quick way to create production-ready applications. It is built on the Spring Framework (Spring MVC for the web), so everything Spring MVC gives you is still there. It adds three things on top.
Advantage 1: dependency management
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version> <!-- the only version you write -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <!-- Spring MVC, JSON, Tomcat -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <!-- JUnit 5, Mockito, ... -->
<scope>test</scope>
</dependency>
</dependencies>- A starter is one dependency that brings in everything for a purpose:
spring-boot-starter-webbrings Spring MVC, the JSON library and Tomcat. - No versions on the dependencies. The parent fixes a set of versions that Spring Boot has tested together. Upgrading means changing one number, the parent's.
Advantage 2: auto-configuration
Spring Boot looks at what is on the classpath and configures it for you. With spring-boot-starter-web present, it registers the DispatcherServlet, sets up Spring MVC, and scans for components from your main class's package downwards. AppConfig and AppInitializer disappear; all that remains is:
java
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}@SpringBootApplication combines three annotations:
| Annotation | Does |
|---|---|
@SpringBootConfiguration | marks the class as a configuration class (it may define beans) |
@EnableAutoConfiguration | switches on auto-configuration |
@ComponentScan | scans this class's package and its sub-packages for components |
Spring Boot is opinionated: it picks a default for everything (scan from the main class's package, port 8080, and so on). This is convention over configuration: if you accept the defaults you write no configuration; if you disagree, you override only that one thing, for example with your own @ComponentScan("...") or a property in application.properties.
Note that @SpringBootApplication does not contain @EnableWebMvc. Spring Boot configures MVC itself; adding @EnableWebMvc to a Boot application tells Boot that you take over MVC configuration completely, and it switches its own MVC defaults off.
Advantage 3: embedded server
Tomcat is inside the application. There is no WAR to build and no server to install: you run the application like any Java program, and Tomcat starts inside it.
| Servlets / Spring MVC | Spring Boot | |
|---|---|---|
| Package as | WAR | executable JAR |
| Server | install Tomcat separately, deploy the WAR into it | embedded; starts with the application |
| Run | start Tomcat | run main, or java -jar app.jar |
Try it: the smallest Spring Boot application
Step 1. Next to the main class above, add a controller:
java
// File: MyController.java
@RestController
@RequestMapping("/myapi")
public class MyController {
@GetMapping("/firstapi")
public String getData() {
return "Hello from Spring Boot";
}
}Step 2. Run DemoApplication (or build and run java -jar target/demo-0.0.1-SNAPSHOT.jar). The log shows Tomcat starting inside the application:
text
Tomcat initialized with port 8080 (http)
Tomcat started on port 8080 (http) with context path '/'
Started DemoApplication in 1.608 seconds (process running for 1.947)Step 3. Open http://localhost:8080/myapi/firstapi:
text
Hello from Spring BootOn this first request the log adds Initializing Servlet 'dispatcherServlet': the same DispatcherServlet as in Spring MVC, which Spring Boot registered for you. Behind the scenes the request took exactly the Spring MVC path: Tomcat, DispatcherServlet, handler mapping, MyController.getData().
How to create this project is chapter 3.
Summary
| Servlets | Spring MVC | Spring Boot | |
|---|---|---|---|
| URL mapping | web.xml + if/else in doGet | annotations per method | annotations per method |
| Object creation | your code, with new | IoC container (DI) | IoC container (DI) |
| Unit testing | hard (tight coupling) | easy (inject mocks) | easy |
| Dependency versions | manual | manual, check compatibility | managed by the parent |
| Configuration | web.xml | config + initializer classes | auto-configured, override what you need |
| Server | external, deploy a WAR | external, deploy a WAR | embedded, run a JAR |
Next: Chapter 3 creates a Spring Boot project from scratch, runs it, and organises it into the controller, service and repository layers that every later chapter uses.
Check yourself
- What is a Servlet, and what is a Servlet container?
- Why does a servlet that serves ten GET endpoints end up with an
if/elsechain? - What does
web.xmlmap, and why does it become a problem? - Why is
User sender = new User();insidePaymentbad for unit testing, and how does Spring fix it? - What is the
DispatcherServlet, and how does it find the method to call? - When is a controller object created, and by whom?
- Name the setup a Spring MVC application needs that a Spring Boot application does not.
- What are Spring Boot's three advantages over Spring MVC?
- Which three annotations make up
@SpringBootApplication? What happens if you add@EnableWebMvc? - What does "convention over configuration" mean?
Answers
- A Servlet is a Java class that handles a request, processes it and returns a response. A Servlet container (such as Tomcat) manages servlets and routes each request to one.
- A servlet has only one
doGet, so every GET path it serves must be told apart inside that method. - It maps URLs to servlet classes (and holds filters). With hundreds of servlets it becomes huge and hard to maintain.
Paymentalways builds a realUser, so a test cannot replace it with a mock (tight coupling). With@Componentand@Autowired, Spring supplies theUser, and a test can supply a mock instead.- Spring's front controller: one servlet that receives every request. It uses handler mapping, built from the mapping annotations, to find the controller and method.
- Once, at startup, by the IoC container, which also injects its dependencies. It is reused for every request.
- A version for every dependency in
pom.xml, a configuration class (@EnableWebMvc,@ComponentScan), aDispatcherServletinitializer, and an external Tomcat to deploy a WAR into. - Dependency management (starters and one parent version), auto-configuration, and an embedded server.
@SpringBootConfiguration,@EnableAutoConfigurationand@ComponentScan.@EnableWebMvctells Boot you take over MVC configuration, so Boot's MVC auto-configuration backs off.- Spring Boot uses sensible defaults (conventions); you write configuration only where you want something different.