This is the story of how I went from zero to Spring Boot in seven days, and how you can too. This entire article is based on my experience alone. Expect plenty of Node and Ruby references.
- Zero to Spring Boot in Seven Days
For this project, we'll be creating a Spring app that lets us sign in, and get advice.
It is assumed that you:
- Can use Git
- Are comfortable with the command line
- Have prior programming experience
The Spring Framework is Platform that provides you the building blocks to a great Java app. This lets you focus on business logic without being bogged down by the other stuff.
While the Java Platform is robust, as a developer your time is better spent implementing your own application, as opposed to creating an ORM for your database layer, or a web framework. This is where Spring makes your job easier.
Spring provides Java objects which abstract away these low level tasks. These objects encapsulate best practices, and allow you to build a whole web application from "Plain Old Java Objects" (POJOs).
The Spring Framework injects its modules automagically when required, during the build process. Focus on building your own application and not the
Spring organizes its features in a set of about 20 modules, grouped into:
- Core
fundamental spring modules. - AOP and Instrumentation
an AOP implementation to help you decouple code that doesn't belong in business logic itself.
e.g. runningLog( User + " made a " + transaction.type);
every time a user makes a transaction from their bank account.
This way we see"John made a deposit"
when John makes a deposit.
AOP would allow us to write this rule in an aspect, keeping the transaction related code clean. - Messaging
Support for integrating with messaging systems like AQMP and STOMP. - Data Access/Integration
This contains theJDBC
,ORM
,OXM
,JMS
, andTransaction
modules. - Web
Modules related to web, including HTTP, MVC and Web Sockets. - Test
Unit and Integration testing frameworks
Getting a Spring application running can be tedious and daunting. That's why Spring Boot "takes and opinionated view of building Spring applications and gets you up and running as fast as possible."
The idea is that Spring Boot lets you bootstrap your project, and then gets out of your way as the project starts becoming more complex.
Spring boot also uses a large amount of convention over configuration, so if you place your pieces correctly, everything just works.
Let's break this down into subtasks:
-
Bootstrapping with Spring Initializer
Precursors to the project -
Making A Web App
A Spring MVC app that serves static content -
Adding Sign Up functionality
Creating Models for our User and saving them to a database on registration. -
Securing The Web App
Using Spring Security to enable authentication for the with an in-memory user -
Switching Spring Security to Use The Database
Making Spring Security use our User Model for Authentication.
We'll be using Maven for our build.
Maven is a build automation tool for Java projects. It defines project details, manages dependencies, and can execute builds. Loosely translated, it's like package.json
and grunt
for Java, all packaged into one, with build conventions baked in.
To get started with a Spring Boot project, the quickest way is to use Spring Initializer and select all the modules that you would require. For this project we'll need:
- Web
- Thymeleaf
- JPA
- H2
- Security
We'll be using Maven for our build.
TLDR: run: curl start.spring.io/starter.zip -d dependencies=web,data-jpa,thymeleaf,h2,security -d javaVersion=1.8 -d applicationName=myapp -d artifactId=myapp -d packageName=myapp -d type=maven-project -d packaging=jar -o initial.zip
Go ahead and explore the pom.xml
file, this is the manifest for your project
We're going to make a web application with no security.
First, go ahead and temporarily comment the spring-boot-starter-security
dependency, until we configure the security
package later on. It defaults to restricting everything behind a 401 Unauthorized
error, which isn't needed at the moment.
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> -->
The first and page we'll build is the simples, a page that displays advice to the visitor, this will later on be our password protected page
<!-- main/resources/templates/advice.html -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Boot Tutorial | Login</title>
<link href='http://fonts.googleapis.com/css?family=Titillium+Web:400,300,600' rel='stylesheet' type='text/css' />
<link rel="stylesheet" type="text/css" href="/assets/css/normalize.css" />
<link rel="stylesheet" type="text/css" href="/assets/css/style.css" />
</head>
<body>
<h2 id="advice">Wait for it...</h2>
<button class="advice">more</button>
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<script src="/assets/js/advice.js"></script>
</body>
</html>
The HTML above should be simple to understand. It displays a line of text that says 'Wait for it...' and a 'more button'
var getAdvice = function() {
$.getJSON('http://api.adviceslip.com/advice', function(data) {
$("h2#advice").replaceWith('<h2 id="advice">' + data.slip.advice + '</h2>');
});
};
getAdvice();
$('button.advice').on('click', function() {
getAdvice();
});
This uses jQuery to fetch a JSON object from the api at adviceslips.com, and replaces the "Wait for it..." on the html. It also adds functionality to our button.
// main/java/myapp/controller/AdviceController.java
@Controller
public class AdviceController {
@RequestMapping(value = "/advice", method = RequestMethod.GET)
public String showAdvice() {
return "advice";
}
}
This controller maps on the Thymeleaf template onto GET /advice
We'll now be:
- Creating a User Entity
- Creating a User Repository
- Creating a Signup page
// main/java/myapp/entity/User.java
@Entity
public class User {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private long id;
private String userName;
private String password;
protected User() {}
public User(String userName, String password) {
this.userName = userName;
this.password = password;
}
// standard getters and setters
@Override
public String toString() {
return String.format(
"User[id=%d, userName='%s', password='%s']",
id, userName, password);
}
}
This is the entity that will be saved to our database, a User only has a username
and password
public interface UserRepository extends CrudRepository<User, Long> {
User findByUserName(String UserName);
}
This repository inherits from the CrudRepository
in the data
dependency, and loads users from their usernames
First add normalize.css to main/resources/static/css/normalize.css
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Boot Tutorial | Login</title>
<link href='http://fonts.googleapis.com/css?family=Titillium+Web:400,300,600' rel='stylesheet' type='text/css' />
<link rel="stylesheet" type="text/css" href="/assets/css/normalize.css" />
<link rel="stylesheet" type="text/css" href="/assets/css/style.css" />
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<div class="form">
<ul class="tab-group">
<li class="tab active"><a href="#login">Log In</a></li>
<li class="tab"><a href="#signup">Sign Up</a></li>
</ul>
<div class="tab-content">
<div id="login">
<h1>Get your advice inside!</h1>
<form th:action="@{/}" method="post">
<div class="field-wrap">
<label>
Username<span class="req">*</span>
</label>
<input name="username" type="text" required="true" autocomplete="off" />
</div>
<div class="field-wrap">
<label>
Password<span class="req">*</span>
</label>
<input name="password" type="password" required="true" autocomplete="off" />
</div>
<button class="button button-block">Log In</button>
</form>
</div>
<div id="signup">
<h1>Sign Up for some advice.</h1>
<form th:action="@{/register}" th:object="${user}" method="post">
<div class="field-wrap">
<label>
Username<span class="req">*</span>
</label>
<input name="userName" type="text" required="true" autocomplete="off" />
</div>
<div class="field-wrap">
<label>
Set A Password<span class="req">*</span>
</label>
<input name="password" type="password" required="true" autocomplete="off" />
</div>
<button type="submit" class="button button-block">Get Started</button>
</form>
</div>
</div>
<!-- tab-content -->
</div>
<!-- /form -->
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<script src="/assets/js/login.js"></script>
</body>
</html>
This adds a sign up and sign in page in one fell swoop.
*, *:before, *:after {
box-sizing: border-box;
}
html {
overflow-y: scroll;
}
body {
background: #c1bdba;
font-family: 'Titillium Web', sans-serif;
}
a {
text-decoration: none;
color: #1ab188;
-webkit-transition: .5s ease;
transition: .5s ease;
}
a:hover {
color: #179b77;
}
.form {
background: rgba(19, 35, 47, 0.9);
padding: 40px;
max-width: 600px;
margin: 40px auto;
border-radius: 4px;
box-shadow: 0 4px 10px 4px rgba(19, 35, 47, 0.3);
}
.tab-group {
list-style: none;
padding: 0;
margin: 0 0 40px 0;
}
.tab-group:after {
content: "";
display: table;
clear: both;
}
.tab-group li a {
display: block;
text-decoration: none;
padding: 15px;
background: rgba(160, 179, 176, 0.25);
color: #a0b3b0;
font-size: 20px;
float: left;
width: 50%;
text-align: center;
cursor: pointer;
-webkit-transition: .5s ease;
transition: .5s ease;
}
.tab-group li a:hover {
background: #179b77;
color: #ffffff;
}
.tab-group .active a {
background: #1ab188;
color: #ffffff;
}
.tab-content > div:last-child {
display: none;
}
h1 {
text-align: center;
color: #ffffff;
font-weight: 300;
margin: 0 0 40px;
}
label {
position: absolute;
-webkit-transform: translateY(6px);
transform: translateY(6px);
left: 13px;
color: rgba(255, 255, 255, 0.5);
-webkit-transition: all 0.25s ease;
transition: all 0.25s ease;
-webkit-backface-visibility: hidden;
pointer-events: none;
font-size: 22px;
}
label .req {
margin: 2px;
color: #1ab188;
}
label.active {
-webkit-transform: translateY(50px);
transform: translateY(50px);
left: 2px;
font-size: 14px;
}
label.active .req {
opacity: 0;
}
label.highlight {
color: #ffffff;
}
input, textarea {
font-size: 22px;
display: block;
width: 100%;
height: 100%;
padding: 5px 10px;
background: none;
background-image: none;
border: 1px solid #a0b3b0;
color: #ffffff;
border-radius: 0;
-webkit-transition: border-color .25s ease, box-shadow .25s ease;
transition: border-color .25s ease, box-shadow .25s ease;
}
input:focus, textarea:focus {
outline: 0;
border-color: #1ab188;
}
textarea {
border: 2px solid #a0b3b0;
resize: vertical;
}
.field-wrap {
position: relative;
margin-bottom: 40px;
}
.top-row:after {
content: "";
display: table;
clear: both;
}
.top-row > div {
float: left;
width: 48%;
margin-right: 4%;
}
.top-row > div:last-child {
margin: 0;
}
.button {
border: 0;
outline: none;
border-radius: 0;
padding: 15px 0;
font-size: 2rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .1em;
background: #1ab188;
color: #ffffff;
-webkit-transition: all 0.5s ease;
transition: all 0.5s ease;
-webkit-appearance: none;
}
.button:hover, .button:focus {
background: #179b77;
}
.button-block {
display: block;
width: 100%;
}
.forgot {
margin-top: -20px;
text-align: right;
}
Standard styling.
$('.form').find('input, textarea').on('keyup blur focus', function(e) {
var $this = $(this),
label = $this.prev('label');
if (e.type === 'keyup') {
if ($this.val() === '') {
label.removeClass('active highlight');
} else {
label.addClass('active highlight');
}
} else if (e.type === 'blur') {
if ($this.val() === '') {
label.removeClass('active highlight');
} else {
label.removeClass('highlight');
}
} else if (e.type === 'focus') {
if ($this.val() === '') {
label.removeClass('highlight');
} else if ($this.val() !== '') {
label.addClass('highlight');
}
}
});
$('.tab a').on('click', function(e) {
e.preventDefault();
$(this).parent().addClass('active');
$(this).parent().siblings().removeClass('active');
target = $(this).attr('href');
$('.tab-content > div').not(target).hide();
$(target).fadeIn(600);
});
Standard Javascript.
@Controller
public class UserController {
@Autowired
private UserRepository repository;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String showLoginForm() {
return "login";
}
@RequestMapping(value = "/register", method = RequestMethod.POST)
public String registerUserAccount(WebRequest request,
@ModelAttribute("user") @Valid User user,
BindingResult result) {
if (!result.hasErrors()) {
repository.save(user);
System.out.println("saved the user!");
System.out.println(
repository.findByUserName(user.getUserName() ));
}
return "redirect:/advice";
}
}
This will map our login and registration form to the index.
To let our registered users use Spring Security
Remember when we first commented out Spring Security in our POM file? Well, it's time to uncomment it.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
// main/java/myapp/service/MyUserDetailsService.java
@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
public MyUserDetailsService() {
super();
}
@Override
public UserDetails loadUserByUsername(String userName)
throws UsernameNotFoundException {
User user = userRepository.findByUserName(userName);
if (user == null) {
return null;
}
List<GrantedAuthority> auth = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
String password = user.getPassword();
return new org.springframework.security.core.userdetails.User(userName, password, auth);
}
}
This returns looks up and returns a version of User compatible with Spring Security
// main/java/myapp/configuration/WebSecurityConfig.java
Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/register", "/assets/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/")
.defaultSuccessUrl("/advice")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
private UserDetailsService userDetailsService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
Finally we configure with pages are public and which pages require a login.
We now have a basic application with authentication, but this is still not secure, things you should try yourself are:
- Using Bcrypt to salt and hash passwords
- Add email and other information to Users
- Send an activation email
- Password Reset options
Or you could skip all this and try out Auth0
Spring boot feels to be extremely modular with components for everything. While this tutorial focused on building a monolithic MVC application, the pieces available allow opportunities ranging from a simple website, to complex micro services for a large system, aided by robust modules for your choice of messaging protocols.
- Learn Java
- Learn about Maven
- Spring:
- Spring Framework Quickstart
- Spring Framework Reference
- Spring Boot Quickstart
- Spring Boot Reference Documentation
- Eric for the sign up and login form