Spring Boot has a lot of magic going for it.
Developing applications with Spring Boot is both cool and fun.
It makes it easy to create stand-alone, production-grade Spring applications that you can just run.
Most Spring Boot applications require minimal configuration, letting you focus on business logic rather than
boilerplate setup.
You will explore the key concepts of Spring Boot and discover how Starter projects simplify application development.
What you will learn
You will build:
- A web application to manage your to-dos
- A basic REST service to manage survey questions
This course is a perfect first step into Spring Boot.
You will learn Spring Boot step by step โ in more than 90 hands-on steps.
You will work with:
- REST services
- Spring (dependency management)
- Spring MVC
- Spring Boot and Spring Boot Starter projects
- Spring Security (authentication & authorization)
- Bootstrap (UI styling)
- Maven (dependency management)
- Eclipse (IDE) and embedded Tomcat (web server)
Weโll help you set up each of these.
Topics covered
- Basics of Spring Boot
- Auto-configuration and Spring Boot โmagicโ
- Spring Boot Starter projects and Spring Initializr
- DispatcherServlet and request flow
- Todo management application with login/logout
- Models, Controllers, ViewResolver, and Filters
- Forms: data binding and validation
- Annotation-based handling:
@RequestParam,@ModelAttribute,@SessionAttributes, etc. - Using Bootstrap to style pages
- Building REST APIs with Spring Boot Starter Web
- Content negotiation (JSON and XML)
- Embedded servlet containers: Tomcat, Jetty, Undertow
- Unit and integration testing with Spring Boot Starter Test
- Profiles and dynamic configuration
- Spring Data JPA
- Spring Boot Actuator
- Spring Security essentials and configuration
- Developer tools and LiveReload
Getting Started
Installing Tools
- Installation Resources
- ๐ฅ Video Playlist - ๐ PDF Guide
- ๐ More Details
-
Troubleshooting
๐ ๏ธ Troubleshooting Guide (50 pages, 200+ errors and FAQs answered)
Running Examples
- Download the ZIP file or clone the Git repository.
- Unzip the ZIP file (if you downloaded one).
- Open Command Prompt/Terminal and navigate (
cd) to the folder containingpom.xml. - Open Eclipse:
- Go to File โ Import โ Existing Maven Project
- Navigate to the folder where you unzipped the project
- Select the correct project
- Locate the Spring Boot Application file (look for the class annotated with
@SpringBootApplication). - Right-click the file โ Run As โ Java Application.
- โ Youโre all set!
- For help: Installation Guide (Video Playlist)
Spring Level 1 to Level 6 โ Section Overview
Hereโs a quick overview of the different sections of the course:
- Level 1: Introduction to Spring in 10 Steps
- Level 2: Develop a Todo Management Web Application in 30 Steps
- Level 3: Introduction to Unit Testing with JUnit in 5 Steps
- Level 4: Introduction to Mocking with Mockito in 5 Steps
- Level 5: Advanced Features of Spring Boot in 25 Steps
- Build a simple API for managing a survey questionnaire
- Level 6: Introduction to JPA in 10 Steps
- Level 7: Connect the Todo Management Web Application to JPA
| Title | Category | Github |
|---|---|---|
| Spring Framework in 10 Steps | Spring - Level 1 | Project Folder on Github |
| Spring in Depth | Spring - Level 2 | Project Folder on Github |
| Unit Testing with Spring Framework | Spring - Level 3 | Project Folder on Github |
| Spring Boot in 10 Steps | Spring - Level 4 | Project Folder on Github |
| Spring AOP | Spring - Level 5 | Project Folder on Github |
| Spring JDBC and JPA | Spring - Level 6 | Project Folder on Github |
5 Bonus Sections - Introduction to Tools and Frameworks
| Title | Category | Github |
|---|---|---|
| Eclipse in 5 Steps | Introduction | Project Folder on Github |
| Maven in 5 Steps | Introduction | Project Folder on Github |
| JUnit in 5 Steps | Introduction | Project Folder on Github |
| Mockito in 5 Steps | Introduction | Project Folder on Github |
| Basic Web Application with Spring MVC | Introduction | Project Folder on Github |
Step-by-Step Details
Introductory Modules
Note: These introductory modules are organized across the course to enable JIT (Just-In-Time) learning. If you are new to Spring, donโt forget to check the Introduction to Spring module in the appendix.
- Introduction to JUnit โ 5 steps
- Introduction to Mockito โ 5 steps
- Introduction to JPA โ 10 steps
- Introduction to Spring โ 10 steps
Web Application with Spring Boot
- Step 01 โ Part 1: Basic Spring Boot web application setup
- Step 01 โ Part 2:
pom.xml, Spring Boot application, andapplication.properties - Step 02 โ Part 1: First Spring MVC controller โ
@ResponseBodyvs@Controller - Step 02 โ Part 2: Understanding HTTP request flow
- Step 03: Demystifying some of the Spring Boot โmagicโ
- Step 04: Redirect to Login JSP โ
@ResponseBodyand view resolver - Step 05: Show user id and password on welcome page โ
ModelMapand@RequestParam - Step 06:
DispatcherServletand Spring MVC flow - Step 07: Your first HTML form
- Step 08: Add hard-coded validation of user id and password
- Step 09: More Spring โmagicโ
- Step 10: Create
TodoControllerandlist-todosview. MakeTodoServicea@Service - Step 11: Architecture of web applications
- Step 12: Session vs Model vs Request โ
@SessionAttributes - Step 13: Add new to-do
- Step 14: Display to-dos in a table using JSTL tags
- Step 15: Bootstrap for page formatting using webjars
- Step 16: Delete a to-do
- Step 17: Format โAdd Todoโ page and add basic HTML5 form validation
- Step 18 โ Part 1: Validations with Hibernate Validator โ using command bean
- Step 18 โ Part 2: Using JSR 349 (Bean Validation)
- Step 19: Updating a to-do
- Step 20: Add a target date for to-do โ use
initBinderto handle date fields - Step 21: JSP fragments and navigation bar
- Step 22: Preparing for Spring Security
- Step 23: Initial Spring Security setup
- Step 24: Refactor and add logout functionality using Spring Security
- Step 25: Exception handling
Connecting Web Application with JPA
Note: Ensure you complete the Introduction to JPA module before starting this section.
- Step 26: Add dependencies for JPA and H2
- Step 27: Configure H2 console
- Step 28: Create
Todoentity and JPA repository - Step 29: Insert
Todousing JPA repository - Step 30: Update, delete, and retrieve to-dos using JPA repository
- Step 31: Data initialization with
data.sql - Step 32: Connecting JPA to other databases
- Step 33: Upgrading to Spring Boot 2 and Spring 5
Spring Boot Deep Dive โ Simple API
- Step 01: Setup and launch Spring Boot application with Maven and Eclipse
- Step 02: Create your first
@RestController - Step 03: Understanding Spring Boot magic:
spring-boot-starter-web - Step 04: Understanding Spring Boot magic:
spring-boot-starter-parent - Step 05: Spring Boot vs Spring framework
- Step 06: Create services for survey and questions
- Step 07: What is REST? Create REST service with
@GetMappingand@PathVariable - Step 08: Second REST service to retrieve a specific question
- Step 09: Spring Boot Developer Tools and LiveReload โ develop faster!
- Step 10: Create a REST service to add a new question:
@PostMapping, Postman - Step 11: Content negotiation โ deliver XML responses from REST services
- Step 12: Spring Initializr โ create Spring Boot projects on the fly
- Step 13: Spring Boot Actuator โ monitor your application
- Step 14: Embedded servlet containers โ switch to Jetty or Undertow
- Step 15: Add dynamic configuration: YAML & more
- Step 16: Basics of profiles
- Step 17: Advanced configuration with type-safe
@ConfigurationProperties - Step 18: Spring Data JPA with
CommandLineRunner - Step 19: H2 console and add a custom JPA repository method
- Step 20: Introduction to Spring Data REST
- Step 21: Spring Boot integration tests
- Step 22: Add integration test for POST requests
- Step 23: Small refactor to organize code
- Step 24: Unit tests with Spring Boot and Mockito
- Step 25: Unit test for
createTodo - Step 26: Secure services with Basic Authentication using Spring Security
- Step 27: Configure Spring Security roles for survey and other services
- Step 28: Deep dive into Spring Boot auto-configuration
Step-by-Step Notes and Code Examples
02. Spring-Boot-Web-Application
Step01.md
What you will learn in this step
- Letโs create a simple web application using Spring Boot.
- How to run the Spring Boot application.
- An overview of the โmagicโ that Spring Boot provides (deep dive in Step 03).
Files list
- (Add list of files for this step โ
e.g.,
pom.xml,src/main/java/...,src/main/resources/application.properties,src/main/resources/templates/*)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot.web</groupId>
<artifactId>spring-boot-first-web-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-first-web-application</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.0-M2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/in28minutes/springboot/web/SpringBootFirstWebApplication.java
package com.in28minutes.springboot.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootFirstWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootFirstWebApplication.class, args);
}
}
src/main/resources/application.properties
src/test/java/com/in28minutes/springboot/web/SpringBootFirstWebApplicationTests.java
package com.in28minutes.springboot.web;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class SpringBootFirstWebApplicationTests {
@Test
public void contextLoads() {
}
}
Step02.md
What You Will Learn during this Step:
- Using
@RequestMapping(value = "/login", method = RequestMethod.GET) - Accessing endpoint:
http://localhost:8080/login - Why use
@ResponseBody? - Importance of the
@RequestMappingmethod - How web applications work โ Request and Response cycle:
- Browser sends HTTP Request to the web server
- Code in the web server executes
- Input:
HttpRequest - Output:
HttpResponse
- Input:
- Web server responds with an HTTP Response
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java New
package com.in28minutes.springboot.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login")
public String loginMessage() {
return "Hello World";
}
}
src/main/resources/application.properties Modified New Lines
logging.level.org.springframework.web: DEBUG
Step03.md
What You Will Learn during this Step:
- Demystifying some of the Spring Boot magic:
- Spring Boot Starter Parent
- Spring Boot Starter Web
- Embedded Tomcat
- Spring Boot DevTools
Step04.md
What You Will Learn during this Step:
- Creating your first JSP
- (Note: A bit of setup is required before this step!)
- Introduction to ViewResolver
Useful Snippets and References
- First Snippet:
/src/main/webapp/WEB-INF/jsp/login.jsp
<html>
<head>
<title>Yahoo!!</title>
</head>
<body>
My First JSP!!!
</body>
</html>
Second Snippet - /src/main/resources/application.properties
spring.mvc.view.prefix: /WEB-INF/jsp/
spring.mvc.view.suffix: .jsp
logging.level.: DEBUG
Third Snippet : To enable jsp support in embedded tomcat server!
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>//default for IntelliJ IDEA
</dependency>
Exercises
- Create a new jsp and a new controller method to redirect to it!
- Play around!
Files List
pom.xml Modified New Lines
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
//default for IntelliJ IDEA
</dependency>
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified
package com.in28minutes.springboot.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class LoginController {
@RequestMapping("/login")
public String loginMessage() {
return "login";
}
}
src/main/resources/application.properties Modified New Lines
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
logging.level.org.springframework.web=DEBUG
src/main/webapp/WEB-INF/jsp/login.jsp New
<html>
<head>
<title>First Web Application</title>
</head>
<body>
My First JSP!!
</body>
</html>
Step05.md
What You Will Learn during this Step:
- You first GET Parameter
- Problem with using GET
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified
@Controller
public class LoginController {
@RequestMapping("/login")
public String loginMessage(@RequestParam String name, ModelMap model) {
model.put("name", name);
return "login";
}
}
src/main/webapp/WEB-INF/jsp/login.jsp Modified New Lines
My First JSP!! Welcome ${name}!
Step06.md
What You Will Learn during this Step:
- Importance of the DispatcherServlet in Spring MVC.
Spring MVC Request Flow
- DispatcherServlet receives the HTTP Request.
- DispatcherServlet identifies the correct Controller based on the URL.
- The Controller executes business logic.
- The Controller returns:
a) Model
b) View Name back to DispatcherServlet.
- DispatcherServlet uses a ViewResolver to identify the correct view.
- DispatcherServlet makes the Model available to the View and executes it.
- DispatcherServlet sends the final HTTP Response back to the client.
๐ Reference Flow Diagram:
Spring MVC Request Flow
Step07.md
What You Will Learn during this Step:
- Lets get the name from the user in a form
Useful Snippets and References
First Snippet
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String showLoginPage(ModelMap model, @RequestParam String name) {
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String handleLogin(ModelMap model, @RequestParam String name) {
model.put("name", name);
return "welcome";
}
Second Snippet
<form method="POST">
Name : <input name="name" type="text"/> <input type="submit"/>
</form>
Files List
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified
package com.in28minutes.springboot.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class LoginController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String showLoginPage(ModelMap model) {
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String showWelcomePage(ModelMap model, @RequestParam String name) {
model.put("name", name);
return "welcome";
}
}
src/main/webapp/WEB-INF/jsp/login.jsp Modified New Lines
<form method="post">
Name : <input type="text" name="name" />
Password : <input type="password" name="password" />
<input type="submit" />
</form>
src/main/webapp/WEB-INF/jsp/welcome.jsp New
<html>
<head>
<title>First Web Application</title>
</head>
<body>
Welcome ${name}!!
</body>
</html>
Step08.md
What You Will Learn during this Step:
- Add validation for userid and password.
- Understand the concept of hard-coded validation (checking credentials against fixed values).
- Show appropriate error messages when invalid credentials are entered.
- Learn the flow of form submission:
- User submits login form.
- Application checks credentials against hard-coded values.
- On success โ Redirect to welcome page.
- On failure โ Stay on login page and display error message.
โ This step introduces basic login validation, preparing the ground for more flexible and secure authentication approaches later in the course.
Files List
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified
@Controller
public class LoginController {
@Autowired
LoginService service;
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String showLoginPage(ModelMap model) {
return "login";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String showWelcomePage(ModelMap model, @RequestParam String name, @RequestParam String password) {
boolean isValidUser = service.validateUser(name, password);
if (!isValidUser) {
model.put("errorMessage", "Invalid Credentials");
return "login";
}
model.put("name", name);
model.put("password", password);
return "welcome";
}
}
src/main/java/com/in28minutes/springboot/web/service/LoginService.java New
package com.in28minutes.springboot.web.service;
import org.springframework.stereotype.Component;
@Component
public class LoginService {
public boolean validateUser(String userid, String password) {
// in28minutes, dummy
return userid.equalsIgnoreCase("in28minutes")
&& password.equalsIgnoreCase("dummy");
}
}
src/main/webapp/WEB-INF/jsp/login.jsp Modified New Lines
<font color="red">${errorMessage}</font>
Step09.md
What You Will Learn during this Step:
- Understand the magic of Spring.
- Learn about Spring Auto-wiring and Dependency Injection.
- Key concepts:
@Autowired,@Component. - How Spring manages dependencies automatically to reduce boilerplate code.
Step10.md
What You Will Do:
- Create
TodoControllerand thelist-todos.jsppage. - Implement
TodoServiceas a Spring@Serviceand inject it into the controller.
Pending for Next Step:
- The variable
${name}is not yet available inlist-todos.jsp. - The username โin28Minutesโ is still hardcoded in
TodoController.
Notes:
- In this step, you will see controller-service interaction in action.
- Prepare for dynamic data handling and model binding in the next step.
package com.in28minutes.springboot.web.model;
import java.util.Date;
public class Todo {
private int id;
private String user;
private String desc;
private Date targetDate;
private boolean isDone;
public Todo(int id, String user, String desc, Date targetDate,
boolean isDone) {
super();
this.id = id;
this.user = user;
this.desc = desc;
this.targetDate = targetDate;
this.isDone = isDone;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Date getTargetDate() {
return targetDate;
}
public void setTargetDate(Date targetDate) {
this.targetDate = targetDate;
}
public boolean isDone() {
return isDone;
}
public void setDone(boolean isDone) {
this.isDone = isDone;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Todo other = (Todo) obj;
if (id != other.id) {
return false;
}
return true;
}
@Override
public String toString() {
return String.format(
"Todo [id=%s, user=%s, desc=%s, targetDate=%s, isDone=%s]", id,
user, desc, targetDate, isDone);
}
}
Snippet - /src/main/java/com/in28minutes/springboot/web/service/TodoService.java
package com.in28minutes.springboot.web.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.springframework.stereotype.Service;
import com.in28minutes.springboot.web.model.Todo;
@Service
public class TodoService {
private static List<Todo> todos = new ArrayList<>();
private static int todoCount = 3;
static {
todos.add(new Todo(1, "in28Minutes", "Learn Spring MVC", new Date(),
false));
todos.add(new Todo(2, "in28Minutes", "Learn Struts", new Date(), false));
todos.add(new Todo(3, "in28Minutes", "Learn Hibernate", new Date(),
false));
}
public List<Todo> retrieveTodos(String user) {
List<Todo> filteredTodos = new ArrayList<>();
for (Todo todo : todos) {
if (todo.getUser().equals(user)) {
filteredTodos.add(todo);
}
}
return filteredTodos;
}
public void addTodo(String name, String desc, Date targetDate,
boolean isDone) {
todos.add(new Todo(++todoCount, name, desc, targetDate, isDone));
}
public void deleteTodo(int id) {
todos.removeIf(todo -> todo.getId() == id);
}
}
Step11.md
What You Will Do:
- Discuss the architecture of web applications.
- Understand how different layers (Controller, Service, View) interact.
- Learn about the responsibilities of each layer and how they contribute to separation of concerns.
Step12.md
What You Will Learn during this Step:
- Differences between Session, Model, and Request scopes.
- Best practices for using Session data โ avoid storing unnecessary information.
- How
@SessionAttributes("name")works to store model attributes in the session. - Why Model is preferred over adding elements directly to
HttpServletRequest:- Ensures view-agnostic design.
- Allows the use of different view technologies not tied to
HttpServletRequest.
- Understanding Spring documentation on
@SessionAttributes:- Lists model attribute names to be transparently stored in session or conversational storage.
Notes:
- This step is crucial for handling user-specific data across multiple requests.
- Helps prepare for form handling, login sessions, and dynamic content rendering.
First Snippet
@SessionAttributes("name")
Files List
src/main/java/com/in28minutes/springboot/web/SpringBootFirstWebApplication.java Modified New Lines
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("com.in28minutes.springboot.web")
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified
@Controller
@SessionAttributes("name")
public class LoginController {
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java New
package com.in28minutes.springboot.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import com.in28minutes.springboot.web.service.LoginService;
import com.in28minutes.springboot.web.service.TodoService;
@Controller
@SessionAttributes("name")
public class TodoController {
@Autowired
TodoService service;
@RequestMapping(value = "/list-todos", method = RequestMethod.GET)
public String showTodos(ModelMap model) {
String name = (String) model.get("name");
model.put("todos", service.retrieveTodos(name));
return "list-todos";
}
}
src/main/java/com/in28minutes/springboot/web/model/Todo.java New
package com.in28minutes.springboot.web.model;
import java.util.Date;
public class Todo {
private int id;
private String user;
private String desc;
private Date targetDate;
private boolean isDone;
public Todo(int id, String user, String desc, Date targetDate,
boolean isDone) {
super();
this.id = id;
this.user = user;
this.desc = desc;
this.targetDate = targetDate;
this.isDone = isDone;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Date getTargetDate() {
return targetDate;
}
public void setTargetDate(Date targetDate) {
this.targetDate = targetDate;
}
public boolean isDone() {
return isDone;
}
public void setDone(boolean isDone) {
this.isDone = isDone;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Todo other = (Todo) obj;
return id == other.id;
}
@Override
public String toString() {
return String.format(
"Todo [id=%s, user=%s, desc=%s, targetDate=%s, isDone=%s]", id,
user, desc, targetDate, isDone);
}
}
src/main/java/com/in28minutes/springboot/web/service/LoginService.java Modified
@Service
public class LoginService {
src/main/java/com/in28minutes/springboot/web/service/TodoService.java New
package com.in28minutes.springboot.web.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.springframework.stereotype.Service;
import com.in28minutes.springboot.web.model.Todo;
@Service
public class TodoService {
private static List<Todo> todos = new ArrayList<>();
private static int todoCount = 3;
static {
todos.add(new Todo(1, "in28Minutes", "Learn Spring MVC", new Date(),
false));
todos.add(new Todo(2, "in28Minutes", "Learn Struts", new Date(), false));
todos.add(new Todo(3, "in28Minutes", "Learn Hibernate", new Date(),
false));
}
public List<Todo> retrieveTodos(String user) {
List<Todo> filteredTodos = new ArrayList<>();
for (Todo todo : todos) {
if (todo.getUser().equals(user)) {
filteredTodos.add(todo);
}
}
return filteredTodos;
}
public void addTodo(String name, String desc, Date targetDate,
boolean isDone) {
todos.add(new Todo(++todoCount, name, desc, targetDate, isDone));
}
public void deleteTodo(int id) {
todos.removeIf(todo -> todo.getId() == id);
}
}
src/main/webapp/WEB-INF/jsp/list-todos.jsp New
<html>
<head>
<title>First Web Application</title>
</head>
<body>
Here are the list of your todos:
${todos}
<BR/>
Your Name is : ${name}
</body>
</html>
src/main/webapp/WEB-INF/jsp/welcome.jsp Modified New Lines
Welcome ${name}!! <a href="/list-todos">Click here</a> to manage your todo's.
Step13.md
What You Will Do:
- Add functionality to create a new Todo.
- Work with the todo.jsp page for adding todos.
- Learn the importance of using redirect:/list-todos after form submission:
- Prevents duplicate form submissions on page refresh.
- Updates the list view to show the newly added todo.
Notes:
- Controller handles the form submission and redirects to the list view.
- Model and view interaction ensures the new todo is displayed correctly.
Files Modified:
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java
@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
public String showAddTodoPage(ModelMap model) {
return "todo";
}
@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
public String addTodo(ModelMap model, @RequestParam String desc) {
service.addTodo((String) model.get("name"), desc, new Date(), false);
return "redirect:/list-todos";
}
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified New Lines
Here are the list of ${name}'s todos:
${todos}.
<a href="/add-todo">Add a Todo</a>
src/main/webapp/WEB-INF/jsp/todo.jsp New
<html>
<head>
<title>First Web Application</title>
</head>
<body>
ADD Todo Page for ${name}
<form method="post">
Description : <input name="desc" type="text"/>
<input type="submit"/>
</form>
</body>
</html>
Step14.md
What You Will Do:
- Display the list of Todos in a table using JSTL tags.
- Use the JSTL core tag library:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
- Add the necessary JSTL dependency to your project.
Notes:
- JSTL tags make it easier to iterate over collections, conditionally display content, and reduce scriptlets in JSP.
- This step focuses on dynamic content rendering using standard JSP techniques.
Files Modified:
list-todos.jsp(to use JSTL tags for displaying todos)
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
Step15.md
What You Will Do:
- Add Bootstrap to give basic styling and formatting to pages.
- Use Bootstrap classes like
container,table, andtable-striped.
- Use Bootstrap classes like
- Use WebJars to manage Bootstrap dependencies.
- Leverage Spring Boot auto-configuration for WebJars:
SimpleUrlHandlerMappingautomatically maps/webjars/**URLs to the appropriate resource handler.
Notes:
- WebJars allow you to include client-side libraries (like Bootstrap) via Maven.
- This step improves UI presentation without manual setup of static resources.
Useful Snippets
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.6</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>1.9.1</version>
</dependency>
<script src="webjars/jquery/1.9.1/jquery.min.js"></script>
<script src="webjars/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<link href="webjars/bootstrap/3.3.6/css/bootstrap.min.css"
rel="stylesheet">
Files List
pom.xml Modified New Lines
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope> <!--default for IntelliJ -->
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
src/main/resources/application.properties Modified New Lines
logging.level.org.springframework.web=INFO
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified New Lines
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<title>Todo's for ${name}</title>
<link href="webjars/bootstrap/3.3.6/css/bootstrap.min.css"
rel="stylesheet">
<div class="container">
<table class="table table-striped">
<caption>Your todos are</caption>
<thead>
<tr>
<th>Description</th>
<th>Target Date</th>
<th>Is it Done?</th>
</tr>
</thead>
<tbody>
<c:forEach items="${todos}" var="todo">
<tr>
<td>${todo.desc}</td>
<td>${todo.targetDate}</td>
<td>${todo.done}</td>
</tr>
</c:forEach>
</tbody>
</table>
<div><a class="button" href="/add-todo">Add a Todo</a></div>
<script src="webjars/jquery/1.9.1/jquery.min.js"></script>
<script src="webjars/bootstrap/3.3.6/js/bootstrap.min.js"></script>
</div>
Step16.md
What we will do:
- Add functionality to delete a todo
Useful Snippets
<a type="button" class="btn btn-warning" href="/delete-todo?id=${todo.id}">Delete</a>
Files List
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified New Lines
@RequestMapping(value = "/delete-todo", method = RequestMethod.GET)
public String deleteTodo(@RequestParam int id) {
service.deleteTodo(id);
}
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified New Lines
<td><a type="button" class="btn btn-warning" href="/delete-todo?id=${todo.id}">Delete</a></td>
Step17.md
Step17.md
What You Will Do:
- Format the Add Todo page for better presentation.
- Add HTML5 form validations to ensure proper input from the user.
- Examples: required fields, date format checks, and text length constraints.
Notes:
- HTML5 validations provide basic client-side checks before the form is submitted.
- Improves user experience by preventing invalid data entry.
Useful Snippets:
- Form fields with HTML5 validation attributes (
required,type="date", etc.).
<fieldset class="form-group">
<label>Description</label>
<input name="desc" type="text" class="form-control" required="required"/>
</fieldset>
Files List
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified Snippets
<td><a type="button" class="btn btn-warning"
href="/delete-todo?id=${todo.id}">Delete</a></td>
<div>
<a class="button" href="/add-todo">Add a Todo</a>
</div>
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
src/main/webapp/WEB-INF/jsp/todo.jsp Modified New Lines
<link href="webjars/bootstrap/5.1.3/css/bootstrap.min.css"
rel="stylesheet">
<div class="container">
<form method="post">
<fieldset class="form-group">
<label>Description</label>
<input name="desc" type="text"
class="form-control" required="required"/>
</fieldset>
<button type="submit" class="btn btn-success">Add</button>
</form>
</div>
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
Step18.md
What we will do:
- Lets use a command bean for Todo
- Add Validations
- The JSR 303 and JSR 349 defines specification for the Bean Validation API (version 1.0 and 1.1, respectively), and Hibernate Validator is the reference implementation.
- org.hibernate:hibernate-validator
Useful Snippets
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="post" commandName="todo">
<!-- use modelAttribute instead of commandName for Spring Boot Versions > 2.*
<form:form method="post" modelAttribute="todo"> -->
<fieldset class="form-group">
<form:label path="desc">Description</form:label>
<form:input path="desc" type="text" class="form-control" required="required"/>
</fieldset>
</form:form>
@Size(min = 10, message = "Enter atleast 10 Characters.")
@Valid Todo todo, BindingResult result
if (result.hasErrors())
return "todo";
<form:errors path="desc" cssClass="text-warning"/>
Files List
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
@Controller
@SessionAttributes("name")
public class TodoController {
@Autowired
TodoService service;
@RequestMapping(value = "/list-todos", method = RequestMethod.GET)
public String showTodos(ModelMap model) {
String name = (String) model.get("name");
model.put("todos", service.retrieveTodos(name));
return "list-todos";
}
@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
public String showAddTodoPage(ModelMap model) {
model.addAttribute("todo", new Todo(0, (String) model.get("name"), "Default Desc",
new Date(), false));
return "todo";
}
@RequestMapping(value = "/delete-todo", method = RequestMethod.GET)
public String deleteTodo(@RequestParam int id) {
service.deleteTodo(id);
return "redirect:/list-todos";
}
@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
public String addTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
service.addTodo((String) model.get("name"), todo.getDesc(), new Date(),
false);
return "redirect:/list-todos";
}
}
src/main/java/com/in28minutes/springboot/web/model/Todo.java Modified New Lines
public Todo() {
super();
}
@Size(min = 10, message = "Enter at least 10 Characters...")
private String desc;
src/main/webapp/WEB-INF/jsp/todo.jsp Modified New Lines
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="post" commandName="todo">
<!-- use modelAttribute instead of commandName for Spring Boot Versions > 2.*
<form:form method="post" modelAttribute="todo"> -->
<form:label path="desc">Description</form:label>
<form:input path="desc" type="text"
<form:errors path="desc" cssClass="text-warning"/>
</form:form>
Step19.md
What You Will Do:
- Add update functionality for existing todos.
- Reuse the same JSP page used for adding todos to handle updates.
Notes:
- Updating a todo involves pre-populating the form with existing data.
- This step demonstrates reusing views and keeping the UI consistent.
- Controller logic will differentiate between add and update operations based on the presence of an existing todo.
Useful Snippets
public Todo retrieveTodo(int id) {
for (Todo todo : todos) {
if (todo.getId() == id)
return todo;
}
return null;
}
public void updateTodo(Todo todo) {
todos.remove(todo);
todos.add(todo);
}
todo.
setUser("in28Minutes"); //TODO:Remove Hardcoding Later
service.
updateTodo(todo);
<form:
hidden path = "id" / >
Files List
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
@RequestMapping(value = "/update-todo", method = RequestMethod.GET)
public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
Todo todo = service.retrieveTodo(id);
model.put("todo", todo);
return "todo";
}
@RequestMapping(value = "/update-todo", method = RequestMethod.POST)
public String updateTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
todo.setUser((String) model.get("name"));
service.updateTodo(todo);
return "redirect:/list-todos";
}
src/main/java/com/in28minutes/springboot/web/service/TodoService.java Modified
public List<Todo> retrieveTodos(String user) {
List<Todo> filteredTodos = new ArrayList<>();
for (Todo todo : todos) {
if (todo.getUser().equalsIgnoreCase(user)) {
filteredTodos.add(todo);
}
}
return filteredTodos;
}
public Todo retrieveTodo(int id) {
for (Todo todo : todos) {
if (todo.getId() == id) {
return todo;
}
}
return null;
}
public void updateTodo(Todo todo) {
todos.remove(todo);
todos.add(todo);
}
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified New Lines
<td><a type="button" class="btn btn-success" href="/update-todo?id=${todo.id}">Update</a></td>
src/main/webapp/WEB-INF/jsp/todo.jsp Modified New Lines
<form:hidden path="id"/>
Step20.md
What You Will Do:
- Utilize the Target Date field in the Todo form.
- Learn about the
initBindermethod to handle date formatting and binding.
Notes:
initBinderallows customization of how form input is converted to Java objects.- Ensures that date fields are properly parsed and bound to the command bean.
- Prepares the application for handling other complex data types in forms.
Useful Snippets
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<fmt:formatDate pattern="dd/MM/yyyy" value="${todo.targetDate}"/>
@InitBinder
protected void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
binder.registerCustomEditor(Date.class, new CustomDateEditor(
dateFormat, false));
}
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap-datepicker</artifactId>
<version>1.9.0</version>
</dependency>
<script
src="webjars/bootstrap-datepicker/1.0.1/js/bootstrap-datepicker.js"></script>
<script>
$('#targetDate').datepicker({
format: 'dd/mm/yyyy'
});
</script>
Files List
pom.xml Modified New Lines
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap-datepicker</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
@RequestMapping(value = "update-todo", method = RequestMethod.POST)
public String updateTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
String username = getLoggedInUsername(model);
todo.setUsername(username);
todoService.updateTodo(todo);
return "redirect:list-todos";
}
@RequestMapping(value = "add-todo", method = RequestMethod.POST)
public String addNewTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
String username = getLoggedInUsername(model);
todoService.addTodo(username, todo.getDescription(),
LocalDate.now().plusYears(1), false);
return "redirect:list-todos";
}
src/main/webapp/WEB-INF/jsp/todo.jsp Modified New Snippets
<form:input type="hidden" path="id"/>
<form:input type="hidden" path="done"/>
<form:label path="desc">Description</form:label>
<form:input path="desc" type="text" class="form-control"
required="required" />
<form:errors path="desc" cssClass="text-warning" />
<form:label path="targetDate">Target Date</form:label>
<form:input path="targetDate" type="text" class="form-control"
required="required" />
<form:errors path="targetDate" cssClass="text-warning" />
<script src="webjars/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.js"></script>
<script type="text/javascript">
$('#targetDate').datepicker({
format: 'yyyy-mm-dd'
});
</script>
Step21.md
What we will do:
- Add a navigation bar
- Use JSP Fragments
- Exercise : Align the login & welcome pages.
- Exercise : Highlight the correct menu item.
Useful Snippets
<nav class="navbar navbar-expand-md navbar-light bg-light mb-3 p-1">
<a class="navbar-brand m-1" href="https://courses.in28minutes.com">in28minutes</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/list-todos">Todos</a></li>
</ul>
</div>
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
</ul>
</nav>
Files List
src/main/webapp/WEB-INF/jsp/common/footer.jspf New
<script src="webjars/bootstrap/5.1.3/js/bootstrap.min.js"></script>
<script src="webjars/jquery/3.6.0/jquery.min.js"></script>
<script src="webjars/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"></script>
</body>
</html>
src/main/webapp/WEB-INF/jsp/common/header.jspf New
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<link href="webjars/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet" >
<link href="webjars/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.standalone.min.css" rel="stylesheet" >
<title>Manage Your Todos</title>
</head>
<body>
src/main/webapp/WEB-INF/jsp/common/navigation.jspf New
<nav class="navbar navbar-expand-md navbar-light bg-light mb-3 p-1">
<a class="navbar-brand m-1" href="https://courses.in28minutes.com">in28minutes</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
<li class="nav-item"><a class="nav-link" href="/list-todos">Todos</a></li>
</ul>
</div>
<ul class="navbar-nav">
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
</ul>
</nav>
src/main/webapp/WEB-INF/jsp/list-todos.jsp Modified New Lines
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>
<%@ include file="common/footer.jspf" %>
src/main/webapp/WEB-INF/jsp/login.jsp Modified New Lines
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>
<div class="container">
<!--OLD CODE-->
</div>
<%@ include file="common/footer.jspf" %>
src/main/webapp/WEB-INF/jsp/todo.jsp Modified New Lines
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>
<div class="container">
<form:form method="post" commandName="todo">
<!-- use modelAttribute instead of commandName for Spring Boot Versions > 2.*
<form:form method="post" modelAttribute="todo"> -->
<form:hidden path="id" />
<fieldset class="form-group">
<form:label path="desc">Description</form:label>
<form:input path="desc" type="text" class="form-control"
required="required" />
<form:errors path="desc" cssClass="text-warning" />
</fieldset>
<fieldset class="form-group">
<form:label path="targetDate">Target Date</form:label>
<form:input path="targetDate" type="text" class="form-control"
required="required" />
<form:errors path="targetDate" cssClass="text-warning" />
</fieldset>
<button type="submit" class="btn btn-success">Add</button>
</form:form>
</div>
<%@ include file="common/footer.jspf" %>
src/main/webapp/WEB-INF/jsp/welcome.jsp Modified New Lines
<%@ include file="common/header.jspf"%>
<%@ include file="common/navigation.jspf"%>
<div class="container">
Welcome ${name}!! <a href="/list-todos">Click here</a> to manage your
todo's.
</div>
<%@ include file="common/footer.jspf"%>
Step22.md
What we will do:
- Set up the application for Spring Security
- Remove all custom login-related functionality
- Make the Welcome page the default (with initial hardcoding)
- Refactor the
getLoggedInUserNamemethod - Update the Home Page link in the navigation
Files List
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Modified New Lines
@RequestMapping(value = "/", method = RequestMethod.GET)
public String showLoginPage(ModelMap model) {
model.put("name", "in28Minutes");
}
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
@RequestMapping(value = "/list-todos", method = RequestMethod.GET)
public String showTodos(ModelMap model) {
String name = getLoggedInUserName(model);
model.put("todos", service.retrieveTodos(name));
return "list-todos";
}
private String getLoggedInUserName(ModelMap model) {
return (String) model.get("name");
}
src/main/webapp/WEB-INF/jsp/common/navigation.jspf Modified New Lines
<li class="active"><a href="/">Home</a></li>
src/main/webapp/WEB-INF/jsp/login.jsp Deleted
Step23.md
What we will do:
- Get Setup for Spring Security
Useful Snippets
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
package com.in28minutes.springboot.myfirstwebapp.security;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.function.Function;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SpringSecurityConfiguration {
//LDAP or Database
//In Memory
//InMemoryUserDetailsManager
//InMemoryUserDetailsManager(UserDetails... users)
@Bean
public InMemoryUserDetailsManager createUserDetailsManager() {
UserDetails userDetails1 = createNewUser("in28minutes", "dummy");
UserDetails userDetails2 = createNewUser("ranga", "dummydummy");
return new InMemoryUserDetailsManager(userDetails1, userDetails2);
}
private UserDetails createNewUser(String username, String password) {
Function<String, String> passwordEncoder
= input -> passwordEncoder().encode(input);
UserDetails userDetails = User.builder()
.passwordEncoder(passwordEncoder)
.username(username)
.password(password)
.roles("USER", "ADMIN")
.build();
return userDetails;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//All URLs are protected
//A login form is shown for unauthorized requests
//CSRF disable
//Frames
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
auth -> auth.anyRequest().authenticated());
http.formLogin(withDefaults());
http.csrf(AbstractHttpConfigurer::disable);
// OR
// http.csrf(AbstractHttpConfigurer::disable);
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); // Starting from SB 3.1.x
return http.build();
}
}
Not Needed anymore with Spring Boot Auto Configuration
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Files List
pom.xml Modified New Lines
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
src/main/java/com/in28minutes/springboot/web/security/SecurityConfiguration.java New
package com.in28minutes.springboot.myfirstwebapp.security;
import static org.springframework.security.config.Customizer.withDefaults;
import java.util.function.Function;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SpringSecurityConfiguration {
//LDAP or Database
//In Memory
//InMemoryUserDetailsManager
//InMemoryUserDetailsManager(UserDetails... users)
@Bean
public InMemoryUserDetailsManager createUserDetailsManager() {
UserDetails userDetails1 = createNewUser("in28minutes", "dummy");
UserDetails userDetails2 = createNewUser("ranga", "dummydummy");
return new InMemoryUserDetailsManager(userDetails1, userDetails2);
}
private UserDetails createNewUser(String username, String password) {
Function<String, String> passwordEncoder
= input -> passwordEncoder().encode(input);
UserDetails userDetails = User.builder()
.passwordEncoder(passwordEncoder)
.username(username)
.password(password)
.roles("USER", "ADMIN")
.build();
return userDetails;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//All URLs are protected
//A login form is shown for unauthorized requests
//CSRF disable
//Frames
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
auth -> auth.anyRequest().authenticated());
http.formLogin(withDefaults());
http.csrf(AbstractHttpConfigurer::disable);
// OR
// http.csrf(AbstractHttpConfigurer::disable);
http.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)); // Starting from SB 3.1.x
return http.build();
}
}
Step24.md
What we will do:
- Remove hardcoded user name
- Delete the
LoginService - Rename
LoginControllertoWelcomeController - Implement logout functionality
Useful Snippets
private String getLoggedInUserName(ModelMap model) {
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails)
return ((UserDetails) principal).getUsername();
return principal.toString();
}
<ul class="nav navbar-nav navbar-right">
<li><a href="/logout">Logout</a></li>
</ul>
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(HttpServletRequest request,
HttpServletResponse response) {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return "redirect:/";
}
Step25.md
What we will do:
- Introduce basic exception handling
- Understand that exception handling is a cross-cutting concern
- Avoid handling exceptions in Controllers or Services unless additional value is added
- Refactor controllers for cleaner error management
- Explore the default Spring Boot Whitelabel Error Page
- Review the details shown on error pages
- Learn how to customize error pages if needed
- Use
@ControllerAdvicefor global and controller-specific exception handling - Handle errors thrown from views
Useful Snippets
@Controller("error")
public class ExceptionController {
private Log logger = LogFactory.getLog(ExceptionController.class);
@ExceptionHandler(Exception.class)
public ModelAndView handleError(HttpServletRequest req, Exception ex) {
logger.error("Request: " + req.getRequestURL() + " raised " + ex);
ModelAndView mav = new ModelAndView();
mav.addObject("exception", ex);
mav.addObject("url", req.getRequestURL());
mav.setViewName("error");
return mav;
}
}
Files List
src/main/java/com/in28minutes/springboot/web/controller/ErrorController.java New
package com.in28minutes.springboot.web.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@Controller("error")
public class ErrorController {
@ExceptionHandler(Exception.class)
public ModelAndView handleException
(HttpServletRequest request, Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("exception", ex.getLocalizedMessage());
mv.addObject("url", request.getRequestURL());
mv.setViewName("error");
return mv;
}
}
src/main/java/com/in28minutes/springboot/web/controller/LoginController.java Deleted
src/main/java/com/in28minutes/springboot/web/controller/LogoutController.java New
package com.in28minutes.springboot.web.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LogoutController {
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(HttpServletRequest request,
HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response,
authentication);
}
return "redirect:/";
}
}
src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
private String getLoggedInUserName(ModelMap model) {
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal.toString();
}
src/main/java/com/in28minutes/springboot/web/controller/WelcomeController.java New
package com.in28minutes.springboot.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class WelcomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String showWelcomePage(ModelMap model) {
model.put("name", getLoggedinUserName());
return "welcome";
}
private String getLoggedinUserName() {
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal.toString();
}
}
src/main/java/com/in28minutes/springboot/web/service/LoginService.java Deleted
src/main/webapp/WEB-INF/jsp/common/navigation.jspf Modified New Lines
<nav role="navigation" class="navbar navbar-default">
<div class="">
<a href="http://www.in28minutes.com" class="navbar-brand">in28Minutes</a>
</div>
<div class="navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home</a></li>
<li><a href="/list-todos">Todos</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</nav>
src/main/webapp/WEB-INF/jsp/error.jsp New
<%@ include file="common/header.jspf"%>
<%@ include file="common/navigation.jspf"%>
<div class="container">
An exception occurred! Please contact Support!
</div>
<%@ include file="common/footer.jspf"%>
Connecting Web Application with JPA
Note: Complete the โIntroduction to JPAโ module before starting this section.
- Step 26: Add dependencies for JPA and H2
- Step 27: Configure the H2 Console
- Step 28: Create the
Todoentity and JPA repository - Step 29: Insert a
Todousing the JPA repository - Step 30: Update, delete, and retrieve
Todosusing the JPA repository - Step 31: Initialize data with
data.sql - Step 32: Connect JPA to other databases
- Step 33: Upgrade to Spring Boot 2 and Spring 5
Step 26 to 32
/pom.xml New
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot.web</groupId>
<artifactId>spring-boot-first-web-application-git</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-first-web-application</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope> <!--default for IntelliJ -->
</dependency>
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.1.3</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap-datepicker</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
/src/main/java/com/in28minutes/springboot/web/SpringBootFirstWebApplication.java New
package com.in28minutes.springboot.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.in28minutes.springboot.web")
public class SpringBootFirstWebApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootFirstWebApplication.class, args);
}
}
/src/main/java/com/in28minutes/springboot/web/controller/ErrorController.java New
package com.in28minutes.springboot.web.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@Controller("error")
public class ErrorController {
@ExceptionHandler(Exception.class)
public ModelAndView handleException
(HttpServletRequest request, Exception ex) {
ModelAndView mv = new ModelAndView();
mv.addObject("exception", ex.getLocalizedMessage());
mv.addObject("url", request.getRequestURL());
mv.setViewName("error");
return mv;
}
}
/src/main/java/com/in28minutes/springboot/web/controller/LogoutController.java New
package com.in28minutes.springboot.web.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LogoutController {
@RequestMapping(value = "/logout", method = RequestMethod.GET)
public String logout(HttpServletRequest request,
HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response,
authentication);
}
return "redirect:/";
}
}
/src/main/java/com/in28minutes/springboot/web/controller/TodoController.java New
package com.in28minutes.springboot.web.controller;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.in28minutes.springboot.web.model.Todo;
import com.in28minutes.springboot.web.service.TodoRepository;
@Controller
public class TodoController {
@Autowired
TodoRepository repository;
@InitBinder
public void initBinder(WebDataBinder binder) {
// Date - dd/MM/yyyy
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
binder.registerCustomEditor(Date.class, new CustomDateEditor(
dateFormat, false));
}
@RequestMapping(value = "/list-todos", method = RequestMethod.GET)
public String showTodos(ModelMap model) {
String name = getLoggedInUserName(model);
model.put("todos", repository.findByUser(name));
//model.put("todos", service.retrieveTodos(name));
return "list-todos";
}
private String getLoggedInUserName(ModelMap model) {
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal.toString();
}
@RequestMapping(value = "/add-todo", method = RequestMethod.GET)
public String showAddTodoPage(ModelMap model) {
model.addAttribute("todo", new Todo(0, getLoggedInUserName(model),
"Default Desc", new Date(), false));
return "todo";
}
@RequestMapping(value = "/delete-todo", method = RequestMethod.GET)
public String deleteTodo(@RequestParam int id) {
//if(id==1)
//throw new RuntimeException("Something went wrong");
repository.delete(id);
//service.deleteTodo(id);
return "redirect:/list-todos";
}
@RequestMapping(value = "/update-todo", method = RequestMethod.GET)
public String showUpdateTodoPage(@RequestParam int id, ModelMap model) {
Todo todo = repository.findOne(id);
//Todo todo = service.retrieveTodo(id);
model.put("todo", todo);
return "todo";
}
@RequestMapping(value = "/update-todo", method = RequestMethod.POST)
public String updateTodo(ModelMap model, @Valid Todo todo,
BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
todo.setUser(getLoggedInUserName(model));
repository.save(todo);
//service.updateTodo(todo);
return "redirect:/list-todos";
}
@RequestMapping(value = "/add-todo", method = RequestMethod.POST)
public String addTodo(ModelMap model, @Valid Todo todo, BindingResult result) {
if (result.hasErrors()) {
return "todo";
}
todo.setUser(getLoggedInUserName(model));
repository.save(todo);
/*service.addTodo(getLoggedInUserName(model), todo.getDesc(), todo.getTargetDate(),
false);*/
return "redirect:/list-todos";
}
}
/src/main/java/com/in28minutes/springboot/web/controller/WelcomeController.java New
package com.in28minutes.springboot.web.controller;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class WelcomeController {
@RequestMapping(value = "/", method = RequestMethod.GET)
public String showWelcomePage(ModelMap model) {
model.put("name", getLoggedinUserName());
return "welcome";
}
private String getLoggedinUserName() {
Object principal = SecurityContextHolder.getContext()
.getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal.toString();
}
}
/src/main/java/com/in28minutes/springboot/web/model/Todo.java New
package com.in28minutes.springboot.web.model;
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Size;
@Entity
public class Todo {
@Id
@GeneratedValue
private int id;
private String user;
@Size(min = 10, message = "Enter at least 10 Characters...")
private String desc;
private Date targetDate;
private boolean isDone;
public Todo() {
super();
}
public Todo(int id, String user, String desc, Date targetDate,
boolean isDone) {
super();
this.id = id;
this.user = user;
this.desc = desc;
this.targetDate = targetDate;
this.isDone = isDone;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Date getTargetDate() {
return targetDate;
}
public void setTargetDate(Date targetDate) {
this.targetDate = targetDate;
}
public boolean isDone() {
return isDone;
}
public void setDone(boolean isDone) {
this.isDone = isDone;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + id;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Todo other = (Todo) obj;
if (id != other.id) {
return false;
}
return true;
}
@Override
public String toString() {
return String.format(
"Todo [id=%s, user=%s, desc=%s, targetDate=%s, isDone=%s]", id,
user, desc, targetDate, isDone);
}
}
/src/main/java/com/in28minutes/springboot/web/security/SecurityConfiguration.java New
package com.in28minutes.springboot.web.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//Create User - in28Minutes/dummy
@Autowired
public void configureGlobalSecurity(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().passwordEncoder(org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()).withUser("in28Minutes").password("dummy")
.roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login", "/h2-console/**").permitAll()
.antMatchers("/", "/*todo*/**").access("hasRole('USER')").and()
.formLogin();
http.csrf().disable();
http.headers().frameOptions().disable();
}
}
/src/main/java/com/in28minutes/springboot/web/service/TodoRepository.java New
package com.in28minutes.springboot.web.service;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.in28minutes.springboot.web.model.Todo;
public interface TodoRepository extends JpaRepository<Todo, Integer> {
List<Todo> findByUser(String user);
//service.retrieveTodos(name)
//service.deleteTodo(id);
//service.retrieveTodo(id)
//service.updateTodo(todo)
//service.addTodo(getLoggedInUserName(model), todo.getDesc(), todo.getTargetDate(),false);
}
/src/main/java/com/in28minutes/springboot/web/service/TodoService.java New
package com.in28minutes.springboot.web.service;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.springframework.stereotype.Service;
import com.in28minutes.springboot.web.model.Todo;
@Service
public class TodoService {
private static List<Todo> todos = new ArrayList<Todo>();
private static int todoCount = 3;
static {
todos.add(new Todo(1, "in28Minutes", "Learn Spring MVC", new Date(),
false));
todos.add(new Todo(2, "in28Minutes", "Learn Struts", new Date(), false));
todos.add(new Todo(3, "in28Minutes", "Learn Hibernate", new Date(),
false));
}
public List<Todo> retrieveTodos(String user) {
List<Todo> filteredTodos = new ArrayList<Todo>();
for (Todo todo : todos) {
if (todo.getUser().equalsIgnoreCase(user)) {
filteredTodos.add(todo);
}
}
return filteredTodos;
}
public Todo retrieveTodo(int id) {
for (Todo todo : todos) {
if (todo.getId() == id) {
return todo;
}
}
return null;
}
public void updateTodo(Todo todo) {
todos.remove(todo);
todos.add(todo);
}
public void addTodo(String name, String desc, Date targetDate,
boolean isDone) {
todos.add(new Todo(++todoCount, name, desc, targetDate, isDone));
}
public void deleteTodo(int id) {
Iterator<Todo> iterator = todos.iterator();
while (iterator.hasNext()) {
Todo todo = iterator.next();
if (todo.getId() == id) {
iterator.remove();
}
}
}
}
/src/main/resources/application.properties New
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
logging.level.org.springframework.web=INFO
spring.jpa.show-sql=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.data.jpa.repositories.bootstrap-mode=default
spring.h2.console.enabled=true
/src/main/resources/data.sql New
insert into TODO
values(10001, 'Learn Spring Boot', false, sysdate(), 'in28Minutes');
insert into TODO
values(10002, 'Learn Angular JS', false, sysdate(), 'in28Minutes');
insert into TODO
values(10003, 'Learn to Dance', false, sysdate(), 'in28Minutes');
/src/main/webapp/WEB-INF/jsp/common/footer.jspf New
<script src="webjars/jquery/1.9.1/jquery.min.js"></script>
<script src="webjars/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script
src="webjars/bootstrap-datepicker/1.0.1/js/bootstrap-datepicker.js"></script>
<script>
$('#targetDate').datepicker({
format : 'dd/mm/yyyy'
});
</script>
</body>
</html>
/src/main/webapp/WEB-INF/jsp/common/header.jspf New
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<html>
<head>
<title>First Web Application</title>
<link href="webjars/bootstrap/3.3.6/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
/src/main/webapp/WEB-INF/jsp/common/navigation.jspf New
<nav role="navigation" class="navbar navbar-default">
<div class="">
<a href="http://www.in28minutes.com" class="navbar-brand">in28Minutes</a>
</div>
<div class="navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home</a></li>
<li><a href="/list-todos">Todos</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="/logout">Logout</a></li>
</ul>
</div>
</nav>
/src/main/webapp/WEB-INF/jsp/error.jsp New
<%@ include file="common/header.jspf"%>
<%@ include file="common/navigation.jspf"%>
<div class="container">
An exception occurred! Please contact Support!
</div>
<%@ include file="common/footer.jspf"%>
/src/main/webapp/WEB-INF/jsp/list-todos.jsp New
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>
<div class="container">
<table class="table table-striped">
<caption>Your todos are</caption>
<thead>
<tr>
<th>Description</th>
<th>Target Date</th>
<th>Is it Done?</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<c:forEach items="${todos}" var="todo">
<tr>
<td>${todo.desc}</td>
<td><fmt:formatDate value="${todo.targetDate}" pattern="dd/MM/yyyy"/></td>
<td>${todo.done}</td>
<td><a type="button" class="btn btn-success"
href="/update-todo?id=${todo.id}">Update</a></td>
<td><a type="button" class="btn btn-warning"
href="/delete-todo?id=${todo.id}">Delete</a></td>
</tr>
</c:forEach>
</tbody>
</table>
<div>
<a class="button" href="/add-todo">Add a Todo</a>
</div>
</div>
<%@ include file="common/footer.jspf" %>
/src/main/webapp/WEB-INF/jsp/todo.jsp New
<%@ include file="common/header.jspf" %>
<%@ include file="common/navigation.jspf" %>
<div class="container">
<form:form method="post" commandName="todo">
<!-- use modelAttribute instead of commandName for Spring Boot Versions > 2.*
<form:form method="post" modelAttribute="todo"> -->
<form:hidden path="id" />
<fieldset class="form-group">
<form:label path="desc">Description</form:label>
<form:input path="desc" type="text" class="form-control"
required="required" />
<form:errors path="desc" cssClass="text-warning" />
</fieldset>
<fieldset class="form-group">
<form:label path="targetDate">Target Date</form:label>
<form:input path="targetDate" type="text" class="form-control"
required="required" />
<form:errors path="targetDate" cssClass="text-warning" />
</fieldset>
<button type="submit" class="btn btn-success">Add</button>
</form:form>
</div>
<%@ include file="common/footer.jspf" %>
/src/main/webapp/WEB-INF/jsp/welcome.jsp New
<%@ include file="common/header.jspf"%>
<%@ include file="common/navigation.jspf"%>
<div class="container">
Welcome ${name}!! <a href="/list-todos">Click here</a> to manage your
todo's.
</div>
<%@ include file="common/footer.jspf"%>
/src/test/java/com/in28minutes/springboot/web/SpringBootFirstWebApplicationTests.java New
package com.in28minutes.springboot.web;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootFirstWebApplicationTests {
@Test
public void contextLoads() {
}
}
step33-Upgrade-To-Sprint-Boot-2-M3.md
/pom.xml Modified
New Lines
<version>2.3.1.RELEASE</version>
/src/main/java/com/in28minutes/springboot/web/controller/TodoController.java Modified
New Lines
repository.deleteById(id);
Todo todo = repository.findById(id).get();
/src/main/webapp/WEB-INF/jsp/todo.jsp Modified
New Lines
<form:form method="post" modelAttribute="todo">
Files List
02.Spring-Boot-Web-Application/notes.md
Schema
create table todo
(
id integer not null,
desc varchar(255),
is_done boolean not null,
target_date timestamp,
user varchar(255),
primary key (id)
)
Data
INSERT INTO TODO
VALUES (10001, 'Learn Spring Boot', false, sysdate(), 'in28Minutes')
INSERT
INTO TODO
VALUES (10002, 'Learn RESTful Web Services', false, sysdate(), 'in28Minutes')
INSERT INTO TODO
VALUES (10003, 'Learn SOAP Web Services', false, sysdate(), 'in28Minutes')
02.Spring-Boot-Web-Application/README.md
Developing your first Spring Boot Web Application is fun.
Installing and Setting Up MySQL
- Install MySQL https://dev.mysql.com/doc/en/installing.html
- More details : http://www.mysqltutorial.org/install-mysql/
- Trouble Shooting - https://dev.mysql.com/doc/refman/en/problems.html
- Startup the Server (as a service)
- Go to command prompt (or terminal)
- Execute following commands to create a database and a user
mysql --user=user_name --password db_name
create database todo_example;
create user 'todouser'@'localhost' identified by 'YOUR_PASSWORD';
grant all on todo_example.* to 'todouser'@'localhost';
- Execute following sql queries to create the table and insert the data
Table
create table todo
(
id integer not null,
desc varchar(255),
is_done boolean not null,
target_date timestamp,
user varchar(255),
primary key (id)
);
Data
INSERT INTO TODO
VALUES (10001, 'Learn Spring Boot', false, sysdate(), 'in28Minutes');
INSERT INTO TODO
VALUES (10002, 'Learn RESTful Web Services', false, sysdate(), 'in28Minutes');
INSERT INTO TODO
VALUES (10003, 'Learn SOAP Web Services', false, sysdate(), 'in28Minutes');
Code changes to connect to MySQL
- Add dependency to pom.xml (and remove H2 dependency)
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
- Configure application.properties
spring.jpa.hibernate.ddl-auto=none
spring.datasource.url=jdbc:mysql://localhost:3306/todo_example
spring.datasource.username=todouser
spring.datasource.password=YOUR_PASSWORD
- Restart and You are ready!
05.Spring-Boot-Advanced
You will be using Spring Boot and get introduced to REST Services.
You will learn about
- Basics of Auto Configuration and Spring Boot Magic
- Spring Boot Starter Projects
- Spring Initializr
- REST Service Content Negotiation with JSON and XML
- Embedded servlet containers : Tomcat, Jetty and Undertow
- Writing Unit and Integration tests using Spring Boot Starter Test
- Profiles and Dynamic Configuration with Spring Boot
- Spring Boot Data JPA
- Spring Boot Actuator
- Spring Boot Developer Tools and LiveReload
Step01.md
What You Will Learn during this Step:
- Set up an Maven Project with Eclipse.
- Intellij Link : https://www.jetbrains.com/help/idea/2016.2/getting-started-with-maven.html#create_maven_project
- Copy Two Files pom.xml and Application.java
- Launch Your First Spring Boot Application.
- You will be introduced to Maven
- Dependency Management
Cool thing to note!
- Without a lot of configuration, we are up and running with a web application
- Refer https://github.com/in28minutes/SpringMvcStepByStep/blob/master/Step15.md to understand the sort of stuff - web.xml, dispatcher servlet configuration, maven dependency management and plugins - that are need to launch a typical web application without Spring Boot!
What You Will NOT Learn during this Step:
- Spring Boot does a lot of magic. This magic is called Auto Configuration. We will discuss about different terms related to Spring Boot - Starter Parent, Starter projects, Auto configuration - in depth during our first 10 steps.
- As far as this step is concerned, we will focus on getting up and running with Spring Boot. We will understand all the magic a little later.
- We will copy a lot of code in this step - just to avoid typos
Exercises
- If you are comfortable with Spring, try to create a few dependencies and see if are automatically auto-wired!
Files List
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot</groupId>
<artifactId>first-springboot-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/in28minutes/springboot/Application.java
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
Step02.md
What You Will Learn during this Step:
- Lets add a RestController with a dependency and see Spring Boot Magic live
Theory Break : Quick Spring and Spring MVC Primer
- What is dependency?
- @Component
- @Autowired
- @RestController
Useful Snippets and References
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
}
@Component
class WelcomeService {
public String retrieveWelcomeMessage() {
//Complex Method
return "Good Morning updated";
}
}
Files List
src/main/java/com/in28minutes/service/WelcomeService.java New
package com.in28minutes.service;
import org.springframework.stereotype.Component;
@Component
public class WelcomeService {
public String retrieveWelcomeMessage() {
//Complex Method
return "Good Morning updated";
}
}
src/main/java/com/in28minutes/springboot/Application.java Modified New Lines
import org.springframework.context.annotation.ComponentScan;
@ComponentScan("com.in28minutes")
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.in28minutes")
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
src/main/java/com/in28minutes/springboot/WelcomeController.java New
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.service.WelcomeService;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
}
Step03.md
What You Will Learn during this Step:
- First installment of revealing how magic happens with Spring Boot. As a Spring Boot developer, you need to understand whatโs happening beneath the hood of Spring Boot!
- spring-boot-starter-web : starter for building applications with Spring MVC. Tomcat is default embedded container.
- We already added this starter in the first step! Now we will explore the features it provides
- We will enable logging in DEBUG mode to understand further
spring-boot-starter-web
- Spring Boot Starter Web brings all dependencies needed to build normal and RESTful web applications. Look at the dependency tree.
- All the dependencies are added in because of spring-boot-starter-web
- Also look at /META-INF/spring.provides inside the spring-boot-starter-web.jar
- Spring Boot Starter Web auto configures things needed to startup a web application. Look at the log
- Mapping servlet: โdispatcherServletโ to [/]
- Mapped โ{[/error]}โ onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Objectยป org.springframework.boot.autoconfigure.web.BasicErrorController.error( javax.servlet.http.HttpServletRequest)
- Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
- Look at package org.springframework.boot.autoconfigure.web in spring-boot-autoconfigure-*.jar
- Go to url http://localhost:8080/some-non-existing-url
Useful Snippets
/src/main/resources/application.properties
logging.level.org.springframework: DEBUG
Files List
src/main/java/com/in28minutes/service/WelcomeService.java Deleted src/main/java/com/in28minutes/springboot/Application.java Modified New Lines
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
src/main/java/com/in28minutes/springboot/WelcomeController.java Modified New Lines
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
}
src/main/java/com/in28minutes/springboot/WelcomeService.java New
package com.in28minutes.springboot;
import org.springframework.stereotype.Component;
@Component
public class WelcomeService {
public String retrieveWelcomeMessage() {
//Complex Method
return "Good Morning updated";
}
}
src/main/resources/application.properties New
logging.level.org.springframework: DEBUG
Step04.md
What You Will Learn during this Step:
- Understand Starter Parent
- How to override things defined in Starter Parent
- Other starter projects
Starter Parent
- Dependency Versions
- Java Versions
- Default Plugins
Other Starter Projects
- spring-boot-starter-web-services
- spring-boot-starter-test
- spring-boot-starter-jdbc
- spring-boot-starter-security
- spring-boot-starter-data-jpa
- spring-boot-starter-data-rest
- More at https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#using-boot-starter
Useful Snippets and References
First Snippet
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
Step05.md
What You Will Learn during this Step:
- Spring Boot vs Spring
- What Spring Boot is Not!
Spring Boot vs Spring
Applications with Spring Framework New
- Over the next few years, a number of applications were developed with Spring Framework
- Testable but
- Lot of configuration (XML and Java)
- Developing Spring Based application need configuration of a lot of beans!
- Integration with other frameworks need configuration as well!
- In the last few years, focus is moving from monolith applications to microservices. We need to be able to start project quickly. Minimum or Zero start up time
- Framework Setup
- Deployment - Configurability
- Logging, Transaction Management
- Monitoring
- Web Server Configuration
Spring New
- Spring is just a dependency injection framework. Spring focuses on the โplumbingโ of enterprise applications so that teams can focus on application-level business logic, without unnecessary ties to specific deployment environments.
- First half of the 2000 decade! EJBs
- EJBs were NOT easy to develop.
- Write a lot of xml and plumbing code to get EJBs running
- Impossible to Unit Test
- Alternative - Writing simple JDBC Code involved a lot of plumbing
- Spring framework started with aim of making Java EE development simpler.
- Goals
- Make applications testable. i.e. easier to write unit tests
- Reduce plumbing code of JDBC and JMS
- Simple architecture. Minus EJB.
- Integrates well with other popular frameworks.
Spring Boot New
- Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can โjust runโ.
- We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss.
- Example Problem Statements
- You want to add Hibernate to your project. You dont worry about configuring a data source and a session factory. I will do if for you!
- Goals
- Provide quick start for projects with Spring.
- Be opinionated but provide options.
- Provide a range of non-functional features that are common to large classes of projects (e.g. embedded servers, security, metrics, health checks, externalized configuration).
What Spring Boot is NOT?
- Itโs not an app or a web server
- Does not implement any specific framework - for example, JPA or JMS
- Does not generate code
pom.xml Deleted src/main/java/com/in28minutes/springboot/Application.java Deleted src/main/java/com/in28minutes/springboot/WelcomeController.java Deleted src/main/java/com/in28minutes/springboot/WelcomeService.java Deleted src/main/resources/application.properties Deleted
Step06.md
What You Will Learn during this Step:
- We want to prepare for creating a Rest Service
- Survey
- Question
- SurveyService
- We use hard-coded data to get started
Files List
Applications with Spring Framework Deleted
Spring Deleted
Spring Boot Deleted
pom.xml New
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot</groupId>
<artifactId>first-springboot-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/in28minutes/springboot/Application.java New
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
}
src/main/java/com/in28minutes/springboot/WelcomeController.java New
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
}
src/main/java/com/in28minutes/springboot/WelcomeService.java New
package com.in28minutes.springboot;
import org.springframework.stereotype.Component;
@Component
public class WelcomeService {
public String retrieveWelcomeMessage() {
//Complex Method
return "Good Morning updated";
}
}
src/main/java/com/in28minutes/springboot/model/Question.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Question {
private String id;
private String description;
private String correctAnswer;
private List<String> options;
// Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException:
// Can not construct instance of com.in28minutes.springboot.model.Question:
// no suitable constructor found, can not deserialize from Object value
// (missing default constructor or creator, or perhaps need to add/enable
// type information?)
public Question() {
}
public Question(String id, String description, String correctAnswer,
List<String> options) {
super();
this.id = id;
this.description = description;
this.correctAnswer = correctAnswer;
this.options = options;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public String getCorrectAnswer() {
return correctAnswer;
}
public List<String> getOptions() {
return options;
}
@Override
public String toString() {
return String
.format("Question [id=%s, description=%s, correctAnswer=%s, options=%s]",
id, description, correctAnswer, options);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Question other = (Question) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}
src/main/java/com/in28minutes/springboot/model/Survey.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Survey {
private String id;
private String title;
private String description;
private List<Question> questions;
public Survey(String id, String title, String description,
List<Question> questions) {
super();
this.id = id;
this.title = title;
this.description = description;
this.questions = questions;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Question> getQuestions() {
return questions;
}
public void setQuestions(List<Question> questions) {
this.questions = questions;
}
@Override
public String toString() {
return "Survey [id=" + id + ", title=" + title + ", description="
+ description + ", questions=" + questions + "]";
}
}
src/main/java/com/in28minutes/springboot/service/SurveyService.java New
package com.in28minutes.springboot.service;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.model.Survey;
@Component
public class SurveyService {
private static List<Survey> surveys = new ArrayList<>();
static {
Question question1 = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question2 = new Question("Question2",
"Most Populus Country in the World", "China", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question3 = new Question("Question3",
"Highest GDP in the World", "United States", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question4 = new Question("Question4",
"Second largest english speaking country", "India", Arrays
.asList("India", "Russia", "United States", "China"));
List<Question> questions = new ArrayList<>(Arrays.asList(question1,
question2, question3, question4));
Survey survey = new Survey("Survey1", "My Favorite Survey",
"Description of the Survey", questions);
surveys.add(survey);
}
public List<Survey> retrieveAllSurveys() {
return surveys;
}
public Survey retrieveSurvey(String surveyId) {
for (Survey survey : surveys) {
if (survey.getId().equals(surveyId)) {
return survey;
}
}
return null;
}
public List<Question> retrieveQuestions(String surveyId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
return survey.getQuestions();
}
public Question retrieveQuestion(String surveyId, String questionId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
for (Question question : survey.getQuestions()) {
if (question.getId().equals(questionId)) {
return question;
}
}
return null;
}
private SecureRandom random = new SecureRandom();
public Question addQuestion(String surveyId, Question question) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
String randomId = new BigInteger(130, random).toString(32);
question.setId(randomId);
survey.getQuestions().add(question);
return question;
}
}
src/main/resources/application.properties New
logging.level.org.springframework: DEBUG
Step07.md
What You Will Learn during this Step:
- Create a REST Service for Retrieving all questions for a survey
- Autowire SurveyService
- Create @GetMapping(โ/surveys/{surveyId}/questionsโ)
- Use @PathVariable String surveyId
- http://localhost:8080/surveys/Survey1/questions/
- How does the Bean get converted to a JSON?
- Auto Configuration : If Jackson jar is on the class path, message converters are auto created! (Search in log : Creating shared instance of singleton bean โmappingJackson2HttpMessageConverterโ)
Some Theory
- What is REST?
- Architectural style for the web. REST specifies a set of constraints.
- Client - Server : Server (service provider) should be different from a client (service consumer).
- Enables loose coupling and independent evolution of server and client as new technologies emerge.
- Each service should be stateless.
- Each Resource has a resource identifier.
- It should be possible to cache response.
- Consumer of the service may not have a direct connection to the Service Provider. Response might be sent from a middle layer cache.
- A resource can have multiple representations. Resource can modified through a message in any of the these representations.
- Client - Server : Server (service provider) should be different from a client (service consumer).
Useful Snippets and References
- JSON View : https://jsonview.com/ First Snippet
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
}
Exercise
- Try to think about how the URI for retrieving the details of a specific question should be!
Files List
pom.xml Modified New Lines
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
src/main/java/com/in28minutes/springboot/controller/SurveyController.java New
package com.in28minutes.springboot.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
}
Step08.md
What You Will Learn during this Step:
- Adding the second method to rest service to retrieve a specific question
- This will be a very short step
- http://localhost:8080/surveys/Survey1/questions/Question1
- Different Request Methods
- GET - Retrieve details of a resource
- POST - Create a new resource
- PUT - Update an existing resource
- PATCH - Update part of a resource
- DELETE - Delete a resource
Useful Snippets and References
First Snippet
@GetMapping(path = "/surveys/{surveyId}/questions/{questionId}")
public Question retrieveQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
}
Exercises
- Write the method to retrieve all surveys!
Files List
src/main/java/com/in28minutes/springboot/controller/SurveyController.java Modified New Lines
// GET "/surveys/{surveyId}/questions/{questionId}"
@GetMapping("/surveys/{surveyId}/questions/{questionId}")
public Question retrieveDetailsForQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
package com.in28minutes.springboot.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
// GET "/surveys/{surveyId}/questions/{questionId}"
@GetMapping("/surveys/{surveyId}/questions/{questionId}")
public Question retrieveDetailsForQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
}
}
Step09.md
What You Will Learn during this Step:
- I hate the fact that Iโve to stop and start the server each time. Can somebody save me?
- Yeah. Spring Boot Developer Tools
- By default, any entry on the classpath that points to a folder will be monitored for changes.
- These will not trigger restart - /META-INF/maven, /META-INF/resources ,/resources ,/static ,/public or /templates
- Folders can be configured : spring.devtools.restart.exclude=static/,public/
- Additional Paths : spring.devtools.restart.additional-paths
- LiveReload http://livereload.com/extensions/
- Technology in progress!! So, expect a few problems!
- Programming Tip
- Become an expert at your IDE - https://www.youtube.com/watch?v=dN9GYsG1v_c
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
Exercises
- Make changes and see if they reflect immediately
Step10.md
What You Will Learn during this Step:
- Create a REST Service to add a new question to survey
- @PostMapping(โ/surveys/{surveyId}/questionsโ)
- @RequestBody Question question
- What should be Response Status for create?
- ResponseEntity.created(location).build()
- ResponseEntity.noContent().build()
- Using Postman : https://www.getpostman.com
- URL to POST to - http://localhost:8080/surveys/Survey1/questions
Useful Snippets and References
Sample Body for POST Request
{"description":"Second Most Populous Country in the World","correctAnswer":"India","options":["India","Russia","United States","China"]}
First Snippet
@PostMapping("/surveys/{surveyId}/questions")
ResponseEntity<?> add(@PathVariable String surveyId,
@RequestBody Question question) {
Question createdTodo = surveyService.addQuestion(surveyId, question);
if (createdTodo == null) {
return ResponseEntity.noContent().build();
}
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(createdTodo.getId()).toUri();
return ResponseEntity.created(location).build();
}
Exercises
- Create more REST services of your choice
Files List
src/main/java/com/in28minutes/springboot/controller/SurveyController.java Modified New Lines
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
// /surveys/{surveyId}/questions
@PostMapping("/surveys/{surveyId}/questions")
public ResponseEntity<Void> addQuestionToSurvey(
@PathVariable String surveyId, @RequestBody Question newQuestion) {
Question question = surveyService.addQuestion(surveyId, newQuestion);
if (question == null)
return ResponseEntity.noContent().build();
// Success - URI of the new resource in Response Header
// Status - created
// URI -> /surveys/{surveyId}/questions/{questionId}
// question.getQuestionId()
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(
"/{id}").buildAndExpand(question.getId()).toUri();
// Status
return ResponseEntity.created(location).build();
package com.in28minutes.springboot.controller;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
// GET "/surveys/{surveyId}/questions/{questionId}"
@GetMapping("/surveys/{surveyId}/questions/{questionId}")
public Question retrieveDetailsForQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
}
// /surveys/{surveyId}/questions
@PostMapping("/surveys/{surveyId}/questions")
public ResponseEntity<Void> addQuestionToSurvey(
@PathVariable String surveyId, @RequestBody Question newQuestion) {
Question question = surveyService.addQuestion(surveyId, newQuestion);
if (question == null)
return ResponseEntity.noContent().build();
// Success - URI of the new resource in Response Header
// Status - created
// URI -> /surveys/{surveyId}/questions/{questionId}
// question.getQuestionId()
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(
"/{id}").buildAndExpand(question.getId()).toUri();
// Status
return ResponseEntity.created(location).build();
}
}
Step11.md
What You Will Learn during this Step:
- Understand Content Negotiation
- Accept:application/xml
- Deliver XML Responses from the REST Services
- http://localhost:8080/surveys/Survey1/questions/
Useful Snippets and References
First Snippet
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
Second Snippet
<List>
<item>
<id>Question1</id>
<description>Largest Country in the World</description>
<correctAnswer>Russia</correctAnswer>
<options>
<options>India</options>
<options>Russia</options>
<options>United States</options>
<options>China</options>
</options>
</item>
<item>
<id>Question2</id>
<description>Most Populus Country in the World</description>
<correctAnswer>China</correctAnswer>
<options>
<options>India</options>
<options>Russia</options>
<options>United States</options>
<options>China</options>
</options>
</item>
<item>
<id>Question3</id>
<description>Highest GDP in the World</description>
<correctAnswer>United States</correctAnswer>
<options>
<options>India</options>
<options>Russia</options>
<options>United States</options>
<options>China</options>
</options>
</item>
<item>
<id>Question4</id>
<description>Second largest english speaking country</description>
<correctAnswer>India</correctAnswer>
<options>
<options>India</options>
<options>Russia</options>
<options>United States</options>
<options>China</options>
</options>
</item>
</List>
Exercises
- Execute other services using xml and see what happens!
Step12.md
What You Will Learn during this Step:
- Spring Initializr
- https://start.spring.io
- Create a few projects!
Exercises
- Create more projects with Spring Initializr and play around with it!
Step13.md
What You Will Learn during this Step:
- Spring Boot Actuator
- /env, /metrics, /trace, /dump, /shutdown, /beans, / autoconfig, /configprops, /mappings
- HAL Browser
- http://localhost:8080/actuator/
- Execute individual REST Services for each of above
- Programming Tip
- Use static code analysis - https://www.youtube.com/watch?v=rB_BaftN3nE
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
Files List
pom.xml Modified New Lines
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<artifactId>spring-boot-starter-actuator</artifactId>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
Step14.md
What You Will Learn during this Step:
- Embedded servlet containers
- Default Tomcat
- We did not install Tomcat. Did we? Magic is done by Spring Boot!
- Switching to Jetty or Undertow
- Configuration
- server.port
- Programming Tip
- Always review code : https://www.youtube.com/watch?v=hVJGu0xdXII
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
Exercises
- Find out from documentation (https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-servlet-containers.html) how to switch to undertow!
Files List
pom.xml Modified New Lines
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
<artifactId>spring-boot-starter-jetty</artifactId>
Step15.md
What You Will Learn during this Step:
- Using Dynamic Configuration in your application
- Customize Welcome Message
- Different ways of configuration
- โwelcome.message=โSomethingElseโ in Program Arguments
- โspring.config.location=classpath:/default.properties
- We will learn about profiles in next step
- Using Placeholders
- YAML
Snippets
First Snippet
logging:
level:
org.springframework: DEBUG
app:
name: In28Minutes
description: ${app.name} is your first Spring Boot application
welcome:
message: Welcome to your first Spring Boot app!
Second Snippet
@Value("${welcome.message}")
Files List
src/main/java/com/in28minutes/springboot/WelcomeService.java Modified New Lines
import org.springframework.beans.factory.annotation.Value;
//Spring to manage this bean and create an instance of this
@Value("${welcome.message}")
private String welcomeMessage;
return welcomeMessage;
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
//Spring to manage this bean and create an instance of this
@Component
public class WelcomeService {
@Value("${welcome.message}")
private String welcomeMessage;
public String retrieveWelcomeMessage() {
//Complex Method
return welcomeMessage;
}
}
src/main/resources/application.properties Modified New Lines
logging.level.org.springframework.web.servlet: DEBUG
app.name=in28Minutes
welcome.message=Welcome message from property file! Welcome to ${app.name}
src/main/resources/application.yaml New
logging:
level:
org.springframework: INFO
org.springframework.web.servlet: DEBUG
Step16.md
What You Will Learn during this Step:
- Understand Basics of Profiles
- Setting a profile
- Using -Dspring.profiles.active=prod in VM Arguments
- spring.profiles.active=prod
- Using a profile
- application-{profile-name}.properties
- @Profile(โdevโ) on a bean
- Usage
- Configure Resources - Databases, Queues, External Services
Files List
src/main/java/com/in28minutes/springboot/Application.java Modified New Lines
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
@Profile("prod")
@Bean
public String dummy() {
return "something";
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
@Profile("prod")
@Bean
public String dummy() {
return "something";
}
}
src/main/resources/application-dev.properties New
logging.level.org.springframework: TRACE
src/main/resources/application-prod.properties New
logging.level.org.springframework: INFO
src/main/resources/application.properties Modified New Lines
spring.profiles.active=prod
src/main/resources/application.yaml Deleted
Step17.md
What You Will Learn during this Step:
- Even better configuration management than @Value
- Type-safe Configuration Properties
- http://localhost:8080/dynamic-configuration
- Also look at http://localhost:8080/actuator/#http://localhost:8080/configprops
Useful Snippets and References
First Snippet
package com.in28minutes.springboot.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("basic")
public class BasicConfiguration {
private boolean value;
private String message;
private int number;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
Second Snippet
@Autowired
private BasicConfiguration configuration;
@RequestMapping("/dynamic-configuration")
public Map dynamicConfiguration() {
// Not the best practice to use a map to store differnt types!
Map map = new HashMap();
map.put("message", configuration.getMessage());
map.put("number", configuration.getNumber());
map.put("key", configuration.isValue());
return map;
}
Third Snippet
basic.value: true
basic.message: Dynamic Message
basic.number: 100
Fourth Snippet
basic:
value: true
message: Dynamic Message YAML
number: 100
Exercises
Understand Type Safety
***************************
APPLICATION FAILED TO START
***************************
Description:
Binding to target com.in28minutes.springboot.configuration.BasicConfiguration@391b8545 failed:
Property: basic.number
Value: ABC
Reason: Failed to convert property value of type [java.lang.String] to required type [int] for property 'number'; nested exception is org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [int]
Action:
Update your application's configuration
Files List
src/main/java/com/in28minutes/springboot/WelcomeController.java Modified New Lines
import java.util.HashMap;
import java.util.Map;
import com.in28minutes.springboot.configuration.BasicConfiguration;
private BasicConfiguration configuration;
@RequestMapping("/dynamic-configuration")
public Map dynamicConfiguration() {
Map map = new HashMap();
map.put("message", configuration.getMessage());
map.put("number", configuration.getNumber());
map.put("value", configuration.isValue());
return map;
package com.in28minutes.springboot;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.springboot.configuration.BasicConfiguration;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@Autowired
private BasicConfiguration configuration;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
@RequestMapping("/dynamic-configuration")
public Map dynamicConfiguration() {
Map map = new HashMap();
map.put("message", configuration.getMessage());
map.put("number", configuration.getNumber());
map.put("value", configuration.isValue());
return map;
}
}
src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java New
package com.in28minutes.springboot.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("basic")
public class BasicConfiguration {
private boolean value;
private String message;
private int number;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
src/main/resources/application.properties Modified New Lines
basic.value=true
basic.message=Welcome to in28minutes
basic.number=200
Step18.md
What You Will Learn during this Step:
- Letโs switch back to tomcat first!
- Get introduced to Spring Data JPA
- Create a very simple example with Spring Data JPA
- Use CommandLineRunner!
Some Notes
- Useful Properties
- spring.datasource.driver-class-name=com.mysql.jdbc.Driver
- spring.datasource.url=jdbc:mysql://localhost:3306/test
- spring.datasource.username=root
- spring.datasource.password=admin
- spring.datasource.initialize=true
- spring.jpa.hibernate.ddl-auto=update
- spring.jpa.show-sql=true spring.datasource.url=jdbc:h2:mem:testdb spring.data.jpa.repositories.bootstrap-mode=default
Useful Snippets and References
First Snippet - Add H2 Later after showing the error
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
Second Snippet
package com.in28minutes.springboot.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;// Not perfect!! Should be a proper object!
private String role;// Not perfect!! An enum should be a better choice!
protected User() {
}
public User(String name, String role) {
super();
this.name = name;
this.role = role;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
@Override
public String toString() {
return String.format("User [id=%s, name=%s, role=%s]", id, name, role);
}
}
Third Snippet
package com.in28minutes.springboot.jpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class UserCommandLineRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory
.getLogger(UserCommandLineRunner.class);
@Autowired
private UserRepository repository;
@Override
public void run(String... args) {
// save a couple of customers
repository.save(new User("Ranga", "Admin"));
repository.save(new User("Ravi", "User"));
repository.save(new User("Satish", "Admin"));
repository.save(new User("Raghu", "User"));
log.info("-------------------------------");
log.info("Finding all users");
log.info("-------------------------------");
for (User user : repository.findAll()) {
log.info(user.toString());
}
}
}
Fourth Snippet
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
}
Exercises
- Look at other methods provided by the UserRepository
Files List
pom.xml Modified New Lines
<artifactId>spring-boot-starter-data-jpa</artifactId>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
src/main/java/com/in28minutes/springboot/jpa/User.java New
package com.in28minutes.springboot.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String role;
protected User() {
}
public User(String name, String role) {
super();
this.name = name;
this.role = role;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", role=" + role + "]";
}
}
src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java New
package com.in28minutes.springboot.jpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class UserCommandLineRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory
.getLogger(UserCommandLineRunner.class);
@Autowired
private UserRepository repository;
@Override
public void run(String... args) throws Exception {
repository.save(new User("Ranga", "Admin"));
repository.save(new User("Ravi", "User"));
repository.save(new User("Satish", "Admin"));
repository.save(new User("Raghu", "User"));
for (User user : repository.findAll()) {
log.info(user.toString());
}
log.info("Admin users are.....");
log.info("____________________");
for (User user : repository.findByRole("Admin")) {
log.info(user.toString());
}
}
}
src/main/java/com/in28minutes/springboot/jpa/UserRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
}
Step19.md
What You Will Learn during this Step:
- Look at H2 Console : http://localhost:8080/h2-console
- Use db url jdbc:h2:mem:testdb
- Add findByRole method
Some Notes
- Useful Properties
- spring.datasource.driver-class-name=com.mysql.jdbc.Driver
- spring.datasource.url=jdbc:mysql://localhost:3306/test
- spring.datasource.username=root
- spring.datasource.password=admin
- spring.datasource.initialize=true
- spring.jpa.hibernate.ddl-auto=update
- spring.jpa.show-sql=true spring.datasource.url=jdbc:h2:mem:testdb spring.data.jpa.repositories.bootstrap-mode=default
Useful Snippets and References
First Snippet
log.info("-------------------------------");
log.info("Finding user with id 1");
log.info("-------------------------------");
User user = repository.findOne(1L);
log.info(user.toString());
log.info("-------------------------------");
log.info("Finding all Admins");
log.info("-------------------------------");
for (User admin : repository.findByRole("Admin")) {
log.info(admin.toString());
// Do something...
}
Second Snippet
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByRole(String description);
}
Files List
src/main/java/com/in28minutes/springboot/jpa/UserRepository.java Modified New Lines
List<User> findByRole(String role);
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByRole(String role);
}
Step20.md
What You Will Learn during this Step:
- Introduction to Spring Data Rest
- Hit http://localhost:8080/users in POSTMAN
- http://localhost:8080/users/1
- http://localhost:8080/users/?size=4
- http://localhost:8080/users/?sort=name,desc
- @Param(โroleโ)
- http://localhost:8080/users/search/findByRole?role=Admin
- Good for quick prototype! Be cautious about using this in Big applications!
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
Second Snippet
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRestRepository extends
PagingAndSortingRepository<User, Long> {
List<User> findByRole(@Param("role") String role);
}
Third Snippet : POST to http://localhost:8080/users
HEADER
Content-Type:application/json
BODY
{
"name": "Raja",
"role": "Admin"
}
Fourth Snippet : POST to http://localhost:8080/users/5
HEADER
Content-Type:application/json
BODY
{
"name": "Raja-Updated",
"role": "Admin"
}
Files List
pom.xml Modified New Lines
<artifactId>spring-boot-starter-data-rest</artifactId>
src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "users", collectionResourceRel = "users")
public interface UserRestRepository extends
PagingAndSortingRepository<User, Long> {
List<User> findByRole(@Param("role") String role);
}
Step21.md
/pom.xml New
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes</groupId>
<artifactId>springboot-for-beginners-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Your First Spring Boot Example</name>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
/src/main/java/com/in28minutes/springboot/Application.java New
package com.in28minutes.springboot;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.springboot.configuration.BasicConfiguration;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
@RestController
class SomeBean {
@Autowired
private SomeDependency someDependency;
@Autowired
private BasicConfiguration configuration;
@RequestMapping("/")
public String index() {
return someDependency.getSomething();
}
@RequestMapping("/dynamic-configuration")
public Map dynamicConfiguration() {
// Not the best practice to use a map to store differnt types!
Map map = new HashMap();
map.put("message", configuration.getMessage());
map.put("number", configuration.getNumber());
map.put("key", configuration.isValue());
return map;
}
}
@Component
class SomeDependency {
@Value("${welcome.message}")
private String welcomeMessage;
public String getSomething() {
return welcomeMessage;
}
}
}
/src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java New
package com.in28minutes.springboot.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("basic")
public class BasicConfiguration {
private boolean value;
private String message;
private int number;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
/src/main/java/com/in28minutes/springboot/controller/SurveyController.java New
package com.in28minutes.springboot.controller;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
@GetMapping(path = "/surveys/{surveyId}/questions/{questionId}")
public Question retrieveQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
}
@PostMapping("/surveys/{surveyId}/questions")
ResponseEntity<?> add(@PathVariable String surveyId,
@RequestBody Question question) {
Question createdTodo = surveyService.addQuestion(surveyId, question);
if (createdTodo == null) {
return ResponseEntity.noContent().build();
}
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(createdTodo.getId()).toUri();
return ResponseEntity.created(location).build();
}
}
/src/main/java/com/in28minutes/springboot/jpa/User.java New
package com.in28minutes.springboot.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;// Not perfect!! Should be a proper object!
private String role;// Not perfect!! An enum should be a better choice!
protected User() {
}
public User(String name, String role) {
super();
this.name = name;
this.role = role;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
@Override
public String toString() {
return String.format("User [id=%s, name=%s, role=%s]", id, name, role);
}
}
/src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java New
package com.in28minutes.springboot.jpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class UserCommandLineRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory
.getLogger(UserCommandLineRunner.class);
@Autowired
private UserRepository repository;
@Override
public void run(String... args) {
// save a couple of customers
repository.save(new User("Ranga", "Admin"));
repository.save(new User("Ravi", "User"));
repository.save(new User("Satish", "Admin"));
repository.save(new User("Raghu", "User"));
log.info("-------------------------------");
log.info("Finding all users");
log.info("-------------------------------");
for (User user : repository.findAll()) {
log.info(user.toString());
}
log.info("-------------------------------");
log.info("Finding user with id 1");
log.info("-------------------------------");
User user = repository.findOne(1L);
log.info(user.toString());
log.info("-------------------------------");
log.info("Finding all Admins");
log.info("-------------------------------");
for (User admin : repository.findByRole("Admin")) {
log.info(admin.toString());
// Do something...
}
}
}
/src/main/java/com/in28minutes/springboot/jpa/UserRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByRole(String description);
}
/src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRestRepository extends
PagingAndSortingRepository<User, Long> {
List<User> findByRole(@Param("role") String role);
}
/src/main/java/com/in28minutes/springboot/model/Question.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Question {
private String id;
private String description;
private String correctAnswer;
private List<String> options;
// Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException:
// Can not construct instance of com.in28minutes.springboot.model.Question:
// no suitable constructor found, can not deserialize from Object value
// (missing default constructor or creator, or perhaps need to add/enable
// type information?)
public Question() {
}
public Question(String id, String description, String correctAnswer,
List<String> options) {
super();
this.id = id;
this.description = description;
this.correctAnswer = correctAnswer;
this.options = options;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public String getCorrectAnswer() {
return correctAnswer;
}
public List<String> getOptions() {
return options;
}
@Override
public String toString() {
return String
.format("Question [id=%s, description=%s, correctAnswer=%s, options=%s]",
id, description, correctAnswer, options);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Question other = (Question) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
}
/src/main/java/com/in28minutes/springboot/model/Survey.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Survey {
private String id;
private String title;
private String description;
private List<Question> questions;
public Survey(String id, String title, String description,
List<Question> questions) {
super();
this.id = id;
this.title = title;
this.description = description;
this.questions = questions;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getDescription() {
return description;
}
public List<Question> getQuestions() {
return questions;
}
@Override
public String toString() {
return String.format(
"Survey [id=%s, title=%s, description=%s, questions=%s]", id,
title, description, questions);
}
}
/src/main/java/com/in28minutes/springboot/service/SurveyService.java New
package com.in28minutes.springboot.service;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.model.Survey;
@Component
public class SurveyService {
private static List<Survey> surveys = new ArrayList<>();
static {
Question question1 = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question2 = new Question("Question2",
"Most Populus Country in the World", "China", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question3 = new Question("Question3",
"Highest GDP in the World", "United States", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question4 = new Question("Question4",
"Second largest english speaking country", "India",
Arrays.asList("India", "Russia", "United States", "China"));
List<Question> questions = new ArrayList<>(Arrays.asList(question1,
question2, question3, question4));
Survey survey = new Survey("Survey1", "My Favorite Survey",
"Description of the Survey", questions);
surveys.add(survey);
}
public List<Survey> retrieveAllSurveys() {
return surveys;
}
public Survey retrieveSurvey(String surveyId) {
for (Survey survey : surveys) {
if (survey.getId().equals(surveyId)) {
return survey;
}
}
return null;
}
public List<Question> retrieveQuestions(String surveyId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
return survey.getQuestions();
}
public Question retrieveQuestion(String surveyId, String questionId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
for (Question question : survey.getQuestions()) {
if (question.getId().equals(questionId)) {
return question;
}
}
return null;
}
private SecureRandom random = new SecureRandom();
public Question addQuestion(String surveyId, Question question) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
String randomId = new BigInteger(130, random).toString(32);
question.setId(randomId);
survey.getQuestions().add(question);
return question;
}
}
/src/main/resources/application.properties New
logging.level.org.springframework: INFO
app.name: In28Minutes
app.description: ${app.name} is your first Spring Boot application Properties
welcome.message: Welcome to your first Spring Boot application
basic.value: true
basic.message: Dynamic Message
basic.number: 123
/src/main/resources/application.yaml New
logging:
level:
org.springframework: DEBUG
app:
name: In28Minutes
description: ${app.name} is your first Spring Boot application
welcome:
message: Welcome to your first Spring Boot app!
basic:
value: true
message: Dynamic Message YAML
number: 100
/src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java New
package com.in28minutes.springboot.controller;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.in28minutes.springboot.Application;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
@LocalServerPort
private int port;
private TestRestTemplate template = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
@Before
public void setupJSONAcceptType() {
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
}
@Test
public void retrieveSurveyQuestion() throws Exception {
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia,options:[India,Russia,United States,China]}";
ResponseEntity<String> response = template.exchange(
createUrl("/surveys/Survey1/questions/Question1"),
HttpMethod.GET, new HttpEntity<String>("DUMMY_DOESNT_MATTER",
headers), String.class);
JSONAssert.assertEquals(expected, response.getBody(), false);
}
private String createUrl(String uri) {
return "http://localhost:" + port + uri;
}
}
What You Will Learn during this Step:
- First thing first : I hate the fact that we did not write tests until Step 20 of this course
- I love TDD and use it in all my projects. However, when learning something new, I think it is important to focus on one thing at a time!
- You can learn more about unit testing here - https://www.youtube.com/playlist?list=PL83C941BB0D27A6AF
- Letโs write a Integration Test for our service
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
UnRefactored Snippet
package com.in28minutes.springboot.controller;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.in28minutes.springboot.Application;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
@LocalServerPort
private int port;
@Test
public void testRetrieveSurveyQuestion() {
String url = "http://localhost:" + port
+ "/surveys/Survey1/questions/Question1";
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<String>(null, headers);
ResponseEntity<String> response = restTemplate.exchange(url,
HttpMethod.GET, entity, String.class);
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia}";
JSONAssert.assertEquals(expected, response.getBody(), false);
}
}
Refactored Snippet
package com.in28minutes.springboot.controller;
import java.util.Arrays;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.in28minutes.springboot.Application;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
@LocalServerPort
private int port;
private TestRestTemplate template = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
@Before
public void setupJSONAcceptType() {
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
}
@Test
public void retrieveSurveyQuestion() throws Exception {
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia,options:[India,Russia,United States,China]}";
ResponseEntity<String> response = template.exchange(
createUrl("/surveys/Survey1/questions/Question1"),
HttpMethod.GET, new HttpEntity<String>("DUMMY_DOESNT_MATTER",
headers), String.class);
JSONAssert.assertEquals(expected, response.getBody(), false);
}
private String createUrl(String uri) {
return "http://localhost:" + port + uri;
}
}
Exercises
- Try writing integration test for retrieveAllQuestions
Files List
pom.xml Deleted src/main/java/com/in28minutes/springboot/Application.java Deleted src/main/java/com/in28minutes/springboot/WelcomeController.java Deleted src/main/java/com/in28minutes/springboot/WelcomeService.java Deleted src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java Deleted src/main/java/com/in28minutes/springboot/controller/SurveyController.java Deleted src/main/java/com/in28minutes/springboot/jpa/User.java Deleted src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java Deleted src/main/java/com/in28minutes/springboot/jpa/UserRepository.java Deleted src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java Deleted src/main/java/com/in28minutes/springboot/model/Question.java Deleted src/main/java/com/in28minutes/springboot/model/Survey.java Deleted src/main/java/com/in28minutes/springboot/service/SurveyService.java Deleted src/main/resources/application-dev.properties Deleted src/main/resources/application-prod.properties Deleted src/main/resources/application.properties Deleted
Step22.md
/pom.xml Deleted
/src/main/java/com/in28minutes/springboot/Application.java Deleted
/src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java Deleted
/src/main/java/com/in28minutes/springboot/controller/SurveyController.java Deleted
/src/main/java/com/in28minutes/springboot/jpa/User.java Deleted
/src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java Deleted
/src/main/java/com/in28minutes/springboot/jpa/UserRepository.java Deleted
/src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java Deleted
/src/main/java/com/in28minutes/springboot/model/Question.java Deleted
/src/main/java/com/in28minutes/springboot/model/Survey.java Deleted
/src/main/java/com/in28minutes/springboot/service/SurveyService.java Deleted
/src/main/resources/application.properties Deleted
/src/main/resources/application.yaml Deleted
/src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java Deleted
What You Will Learn during this Step:
- Exercise from previous step
- Integration Test for POST Request - Add To do
Useful Snippets and References
First Snippet
@Test
public void retrieveSurveyQuestions() throws Exception {
ResponseEntity<List<Question>> response = template.exchange(
createUrl("/surveys/Survey1/questions/"), HttpMethod.GET,
new HttpEntity<String>("DUMMY_DOESNT_MATTER", headers),
new ParameterizedTypeReference<List<Question>>() {
});
Question sampleQuestion = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
assertTrue(response.getBody().contains(sampleQuestion));
}
Before Refactoring Snippet
//NEEDS REFACTORING
@Test
public void retrieveAllSurveyQuestions() throws Exception {
String url = "http://localhost:" + port + "/surveys/Survey1/questions";
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<List<Question>> response = restTemplate.exchange(url,
HttpMethod.GET, new HttpEntity<String>("DUMMY_DOESNT_MATTER",
headers),
new ParameterizedTypeReference<List<Question>>() {
});
Question sampleQuestion = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
assertTrue(response.getBody().contains(sampleQuestion));
}
After Refactoring Snippet - We will discuss this in Step 23
@Test
public void createSurveyQuestion() throws Exception {
Question question = new Question("DOESN'T MATTER", "Smallest Number",
"1", Arrays.asList("1", "2", "3", "4"));
ResponseEntity<String> response = template.exchange(
createUrl("/surveys/Survey1/questions/"), HttpMethod.POST,
new HttpEntity<Question>(question, headers), String.class);
assertThat(response.getHeaders().get(HttpHeaders.LOCATION).get(0),
containsString("/surveys/Survey1/questions/"));
}
Files List
pom.xml New
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot</groupId>
<artifactId>first-springboot-project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
src/main/java/com/in28minutes/springboot/Application.java New
package com.in28minutes.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(Application.class, args);
}
@Profile("prod")
@Bean
public String dummy() {
return "something";
}
}
src/main/java/com/in28minutes/springboot/WelcomeController.java New
package com.in28minutes.springboot;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.in28minutes.springboot.configuration.BasicConfiguration;
@RestController
public class WelcomeController {
//Auto wiring
@Autowired
private WelcomeService service;
@Autowired
private BasicConfiguration configuration;
@RequestMapping("/welcome")
public String welcome() {
return service.retrieveWelcomeMessage();
}
@RequestMapping("/dynamic-configuration")
public Map dynamicConfiguration() {
Map map = new HashMap();
map.put("message", configuration.getMessage());
map.put("number", configuration.getNumber());
map.put("value", configuration.isValue());
return map;
}
}
src/main/java/com/in28minutes/springboot/WelcomeService.java New
package com.in28minutes.springboot;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
//Spring to manage this bean and create an instance of this
@Component
public class WelcomeService {
@Value("${welcome.message}")
private String welcomeMessage;
public String retrieveWelcomeMessage() {
//Complex Method
return welcomeMessage;
}
}
src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java New
package com.in28minutes.springboot.configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("basic")
public class BasicConfiguration {
private boolean value;
private String message;
private int number;
public boolean isValue() {
return value;
}
public void setValue(boolean value) {
this.value = value;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
src/main/java/com/in28minutes/springboot/controller/SurveyController.java New
package com.in28minutes.springboot.controller;
import java.net.URI;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RestController
class SurveyController {
@Autowired
private SurveyService surveyService;
@GetMapping("/surveys/{surveyId}/questions")
public List<Question> retrieveQuestions(@PathVariable String surveyId) {
return surveyService.retrieveQuestions(surveyId);
}
// GET "/surveys/{surveyId}/questions/{questionId}"
@GetMapping("/surveys/{surveyId}/questions/{questionId}")
public Question retrieveDetailsForQuestion(@PathVariable String surveyId,
@PathVariable String questionId) {
return surveyService.retrieveQuestion(surveyId, questionId);
}
// /surveys/{surveyId}/questions
@PostMapping("/surveys/{surveyId}/questions")
public ResponseEntity<Void> addQuestionToSurvey(
@PathVariable String surveyId, @RequestBody Question newQuestion) {
Question question = surveyService.addQuestion(surveyId, newQuestion);
if (question == null)
return ResponseEntity.noContent().build();
// Success - URI of the new resource in Response Header
// Status - created
// URI -> /surveys/{surveyId}/questions/{questionId}
// question.getQuestionId()
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path(
"/{id}").buildAndExpand(question.getId()).toUri();
// Status
return ResponseEntity.created(location).build();
}
}
src/main/java/com/in28minutes/springboot/jpa/User.java New
package com.in28minutes.springboot.jpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private String role;
protected User() {
}
public User(String name, String role) {
super();
this.name = name;
this.role = role;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getRole() {
return role;
}
@Override
public String toString() {
return "User [id=" + id + ", name=" + name + ", role=" + role + "]";
}
}
src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java New
package com.in28minutes.springboot.jpa;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class UserCommandLineRunner implements CommandLineRunner {
private static final Logger log = LoggerFactory
.getLogger(UserCommandLineRunner.class);
@Autowired
private UserRepository repository;
@Override
public void run(String... args) throws Exception {
repository.save(new User("Ranga", "Admin"));
repository.save(new User("Ravi", "User"));
repository.save(new User("Satish", "Admin"));
repository.save(new User("Raghu", "User"));
for (User user : repository.findAll()) {
log.info(user.toString());
}
log.info("Admin users are.....");
log.info("____________________");
for (User user : repository.findByRole("Admin")) {
log.info(user.toString());
}
}
}
src/main/java/com/in28minutes/springboot/jpa/UserRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
List<User> findByRole(String role);
}
src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java New
package com.in28minutes.springboot.jpa;
import java.util.List;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "users", collectionResourceRel = "users")
public interface UserRestRepository extends
PagingAndSortingRepository<User, Long> {
List<User> findByRole(@Param("role") String role);
}
src/main/java/com/in28minutes/springboot/model/Question.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Question {
private String id;
private String description;
private String correctAnswer;
private List<String> options;
// Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException:
// Can not construct instance of com.in28minutes.springboot.model.Question:
// no suitable constructor found, can not deserialize from Object value
// (missing default constructor or creator, or perhaps need to add/enable
// type information?)
public Question() {
}
public Question(String id, String description, String correctAnswer,
List<String> options) {
super();
this.id = id;
this.description = description;
this.correctAnswer = correctAnswer;
this.options = options;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public String getCorrectAnswer() {
return correctAnswer;
}
public List<String> getOptions() {
return options;
}
@Override
public String toString() {
return String
.format("Question [id=%s, description=%s, correctAnswer=%s, options=%s]",
id, description, correctAnswer, options);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Question other = (Question) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
}
src/main/java/com/in28minutes/springboot/model/Survey.java New
package com.in28minutes.springboot.model;
import java.util.List;
public class Survey {
private String id;
private String title;
private String description;
private List<Question> questions;
public Survey(String id, String title, String description,
List<Question> questions) {
super();
this.id = id;
this.title = title;
this.description = description;
this.questions = questions;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public List<Question> getQuestions() {
return questions;
}
public void setQuestions(List<Question> questions) {
this.questions = questions;
}
@Override
public String toString() {
return "Survey [id=" + id + ", title=" + title + ", description="
+ description + ", questions=" + questions + "]";
}
}
src/main/java/com/in28minutes/springboot/service/SurveyService.java New
package com.in28minutes.springboot.service;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.model.Survey;
@Component
public class SurveyService {
private static List<Survey> surveys = new ArrayList<>();
static {
Question question1 = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question2 = new Question("Question2",
"Most Populus Country in the World", "China", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question3 = new Question("Question3",
"Highest GDP in the World", "United States", Arrays.asList(
"India", "Russia", "United States", "China"));
Question question4 = new Question("Question4",
"Second largest english speaking country", "India", Arrays
.asList("India", "Russia", "United States", "China"));
List<Question> questions = new ArrayList<>(Arrays.asList(question1,
question2, question3, question4));
Survey survey = new Survey("Survey1", "My Favorite Survey",
"Description of the Survey", questions);
surveys.add(survey);
}
public List<Survey> retrieveAllSurveys() {
return surveys;
}
public Survey retrieveSurvey(String surveyId) {
for (Survey survey : surveys) {
if (survey.getId().equals(surveyId)) {
return survey;
}
}
return null;
}
public List<Question> retrieveQuestions(String surveyId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
return survey.getQuestions();
}
public Question retrieveQuestion(String surveyId, String questionId) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
for (Question question : survey.getQuestions()) {
if (question.getId().equals(questionId)) {
return question;
}
}
return null;
}
private SecureRandom random = new SecureRandom();
public Question addQuestion(String surveyId, Question question) {
Survey survey = retrieveSurvey(surveyId);
if (survey == null) {
return null;
}
String randomId = new BigInteger(130, random).toString(32);
question.setId(randomId);
survey.getQuestions().add(question);
return question;
}
}
src/main/resources/application-dev.properties New
logging.level.org.springframework: TRACE
src/main/resources/application-prod.properties New
logging.level.org.springframework: INFO
src/main/resources/application.properties New
logging.level.org.springframework: DEBUG
app.name=in28Minutes
welcome.message=Welcome message from property file! Welcome to ${app.name}
basic.value=true
basic.message=Welcome to in28minutes
basic.number=200
src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java New
package com.in28minutes.springboot.controller;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.in28minutes.springboot.Application;
import com.in28minutes.springboot.model.Question;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
@LocalServerPort
private int port;
//NEEDS REFACTORING
@Test
public void testRetrieveSurveyQuestion() {
String url = "http://localhost:" + port
+ "/surveys/Survey1/questions/Question1";
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity<String> entity = new HttpEntity<String>(null, headers);
ResponseEntity<String> response = restTemplate.exchange(url,
HttpMethod.GET, entity, String.class);
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia}";
JSONAssert.assertEquals(expected, response.getBody(), false);
}
//NEEDS REFACTORING
@Test
public void retrieveAllSurveyQuestions() throws Exception {
String url = "http://localhost:" + port + "/surveys/Survey1/questions";
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
ResponseEntity<List<Question>> response = restTemplate.exchange(url,
HttpMethod.GET, new HttpEntity<String>("DUMMY_DOESNT_MATTER",
headers),
new ParameterizedTypeReference<List<Question>>() {
});
Question sampleQuestion = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
assertTrue(response.getBody().contains(sampleQuestion));
}
//NEEDS REFACTORING
@Test
public void addQuestion() {
String url = "http://localhost:" + port + "/surveys/Survey1/questions";
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
Question question = new Question("DOESNTMATTER", "Question1", "Russia",
Arrays.asList("India", "Russia", "United States", "China"));
HttpEntity entity = new HttpEntity<Question>(question, headers);
ResponseEntity<String> response = restTemplate.exchange(url,
HttpMethod.POST, entity, String.class);
String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);
assertTrue(actual.contains("/surveys/Survey1/questions/"));
}
}
Step23.md
What You Will Learn during this Step:
- Lets do some cleanup
- Lets Refactor the SurveyControllerIT.java
Exercises
- Test and make sure everything is working fine
Files List
src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java
package com.in28minutes.springboot.controller;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import com.in28minutes.springboot.Application;
import com.in28minutes.springboot.model.Question;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
@LocalServerPort
private int port;
TestRestTemplate restTemplate = new TestRestTemplate();
HttpHeaders headers = new HttpHeaders();
@Before
public void before() {
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
}
@Test
public void testRetrieveSurveyQuestion() {
HttpEntity<String> entity = new HttpEntity<String>(null, headers);
ResponseEntity<String> response = restTemplate.exchange(
createURLWithPort("/surveys/Survey1/questions/Question1"),
HttpMethod.GET, entity, String.class);
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia}";
JSONAssert.assertEquals(expected, response.getBody(), false);
}
@Test
public void retrieveAllSurveyQuestions() throws Exception {
ResponseEntity<List<Question>> response = restTemplate.exchange(
createURLWithPort("/surveys/Survey1/questions"),
HttpMethod.GET, new HttpEntity<String>("DUMMY_DOESNT_MATTER",
headers),
new ParameterizedTypeReference<List<Question>>() {
});
Question sampleQuestion = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
assertTrue(response.getBody().contains(sampleQuestion));
}
@Test
public void addQuestion() {
Question question = new Question("DOESNTMATTER", "Question1", "Russia",
Arrays.asList("India", "Russia", "United States", "China"));
HttpEntity entity = new HttpEntity<Question>(question, headers);
ResponseEntity<String> response = restTemplate.exchange(
createURLWithPort("/surveys/Survey1/questions"),
HttpMethod.POST, entity, String.class);
String actual = response.getHeaders().get(HttpHeaders.LOCATION).get(0);
assertTrue(actual.contains("/surveys/Survey1/questions/"));
}
private String createURLWithPort(final String uri) {
return "http://localhost:" + port + uri;
}
}
Step24.md
What You Will Learn during this Step:
- Write a Unit Test for retrieving a specific question from a survey.
- Different between Unit Test and Integration Test
- Basics of Mocking
- MockMvc framework
- @MockBean
Programming Tip - Become an expert at Mockito - https://courses.in28minutes.com/p/mockito-for-beginner-in-5-steps
Exercises
- Write unit test for retrieve all questions for a survey
Files List
src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java New
package com.in28minutes.springboot.controller;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import com.in28minutes.springboot.model.Question;
import com.in28minutes.springboot.service.SurveyService;
@RunWith(SpringRunner.class)
@WebMvcTest(value = SurveyController.class)
public class SurveyControllerTest {
@Autowired
private MockMvc mockMvc;
// Mock @Autowired
@MockBean
private SurveyService surveyService;
@Test
public void retrieveDetailsForQuestion() throws Exception {
Question mockQuestion = new Question("Question1",
"Largest Country in the World", "Russia", Arrays.asList(
"India", "Russia", "United States", "China"));
Mockito.when(
surveyService.retrieveQuestion(Mockito.anyString(), Mockito
.anyString())).thenReturn(mockQuestion);
RequestBuilder requestBuilder = MockMvcRequestBuilders.get(
"/surveys/Survey1/questions/Question1").accept(
MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(requestBuilder).andReturn();
String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia}";
JSONAssert.assertEquals(expected, result.getResponse()
.getContentAsString(), false);
// Assert
}
}
Step25.md
What You Will Learn during this Step:
- Exercise from previous step
- Unit test for createTodo
Files List
src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java Modified
@Test
public void createSurveyQuestion() throws Exception {
Question mockQuestion = new Question("1", "Smallest Number", "1",
Arrays.asList("1", "2", "3", "4"));
String questionJson = "{\"description\":\"Smallest Number\",\"correctAnswer\":\"1\",\"options\":[\"1\",\"2\",\"3\",\"4\"]}";
//surveyService.addQuestion to respond back with mockQuestion
Mockito.when(
surveyService.addQuestion(Mockito.anyString(), Mockito
.any(Question.class))).thenReturn(mockQuestion);
//Send question as body to /surveys/Survey1/questions
RequestBuilder requestBuilder = MockMvcRequestBuilders.post(
"/surveys/Survey1/questions")
.accept(MediaType.APPLICATION_JSON).content(questionJson)
.contentType(MediaType.APPLICATION_JSON);
MvcResult result = mockMvc.perform(requestBuilder).andReturn();
MockHttpServletResponse response = result.getResponse();
assertEquals(HttpStatus.CREATED.value(), response.getStatus());
assertEquals("http://localhost/surveys/Survey1/questions/1", response
.getHeader(HttpHeaders.LOCATION));
}
Step26.md
What You Will Learn during this Step:
- Securing our services with Basic Authentication using Spring Security
- Executing Requests using Basic Authentication with Postman
- default user name is user
- default security password is printed in console
Useful Snippets and References
First Snippet
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Second Snippet
Using default security password:
Third Snippet : Executing a GET to http://localhost:8080/surveys/Survey1/questions/
{
"timestamp": 1483514297025,
"status": 401,
"error": "Unauthorized",
"message": "Full authentication is required to access this resource",
"path": "/surveys/Survey1/questions/"
}
Step27.md
What You Will Learn during this Step:
- Configure different user roles for survey and other services
- Update integration tests
- Update unit tests
Files List
pom.xml Modified New Lines
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
src/main/java/com/in28minutes/springboot/security/SecurityConfig.java New
package com.in28minutes.springboot.security;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Authentication : User --> Roles
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication().passwordEncoder(org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()).withUser("user1").password("secret1")
.roles("USER").and().withUser("admin1").password("secret1")
.roles("USER", "ADMIN");
}
// Authorization : Role -> Access
// survey -> USER
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests().antMatchers("/surveys/**")
.hasRole("USER").antMatchers("/users/**").hasRole("USER")
.antMatchers("/**").hasRole("ADMIN").and().csrf().disable()
.headers().frameOptions().disable();
}
}
src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java Modified New Lines
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SurveyControllerIT {
//Other Code
HttpHeaders headers = new HttpHeaders();
@Before
public void before() {
headers.add("Authorization", createHttpAuthenticationHeaderValue(
"user1", "secret1"));
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
}
//Other Code
private String createHttpAuthenticationHeaderValue(String userId,
String password) {
String auth = userId + ":" + password;
byte[] encodedAuth = Base64.encode(auth.getBytes(Charset
.forName("US-ASCII")));
String headerValue = "Basic " + new String(encodedAuth);
return headerValue;
}
}
src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java Modified
@WebMvcTest(value = SurveyController.class, secure = false)
Step28.md
What You Will Learn during this Step:
- A Deep Dive into Autoconfiguration
- spring-boot-autoconfigure-1.4.0.RELEASE.jar
- /META-INF/spring.factories
- Package org.springframework.boot.autoconfigure
- Lets look at the log in debug mode!
- Examples
- JdbcTemplateAutoConfiguration
- HttpMessageConvertersAutoConfiguration
- Programming Tips
- Understand Design Patterns
- https://www.youtube.com/watch?v=Vp7q_pE7Fzg
- Understand Modern Development Practices
- https://www.youtube.com/watch?v=0Kqzfyp-w4s
Useful Snippets and References
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}

