Building a stand-alone Spring Boot Web Application from scratch

In this tutorial I am going to show how to build a Spring Boot Web Application from Scratch, using Thymeleaf as template engine and h2database as persistence storage.

Our application is going to be a “blog” style application where we can post something.

This application will have :

  • A list of posts
  • Add new post
  • Delete post
  • Edit post

Our project structure will look like the below image :

Screen Shot 2015-10-31 at 5.55.53 pm

Why ? because Spring Boot takes an opinionated view about how to build a Spring based application and sets its expectations about where and how things should look like in order to magically work ūüôā

All resources need to be kept inside the resources folder, which means templates, css, js and so on.

Going further, let’s run through the following steps :

1) Install JDK 8 –¬†http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

2) Install Maven –¬†https://maven.apache.org/install.html

3) Create our application structure using maven “archetype:generate” :

mvn -B archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=au.com.riosoftware.firstapp -DartifactId=firstapp

4) Generate a IntelliJ/Eclipse project files with :

mvn idea:idea 
- OR -
mvn eclipse:eclipse

5) Configure our pom.xml file to have the proper dependencies

<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/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>au.com.riosoftware.firstapp</groupId>
    <artifactId>firstapp</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>firstapp</name>
    <url>http://maven.apache.org</url>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.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-thymeleaf</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>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-milestone</id>
            <url>https://repo.spring.io/libs-release</url>
        </repository>

        <repository>
            <id>sonatype-snapshots</id>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestone</id>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
        <pluginRepository>
            <id>jetty</id>
            <url>http://mvnrepository.com/artifact/org.eclipse.jetty</url>
        </pluginRepository>
    </pluginRepositories>

</project>

6) Create a Post domain model to represent our posts :

package au.com.riosoftware.firstapp.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String message;

    public Post(){
    }

    public Post(String message){
        this.message = message;
    }

    public long getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

7) Create a PostRepository to persist and retrieve our Post domain class :

package au.com.riosoftware.firstapp.domain;

import org.springframework.data.repository.CrudRepository;

public interface PostRepository extends CrudRepository<Post, Long> {
}

8) Create a PostController to handle our requests :

package au.com.riosoftware.firstapp.controllers;

import au.com.riosoftware.firstapp.domain.Post;
import au.com.riosoftware.firstapp.domain.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/posts")
public class PostController {

    @Autowired
    private PostRepository repository;

    @RequestMapping(value="", method=RequestMethod.GET)
    public String listPosts(Model model) {
        model.addAttribute("posts", repository.findAll());
        return "posts/list";
    }

    @RequestMapping(value = "/{id}/delete", method = RequestMethod.GET)
    public ModelAndView delete(@PathVariable long id) {
        repository.delete(id);
        return new ModelAndView("redirect:/posts");
    }

    @RequestMapping(value="/new", method = RequestMethod.GET)
    public String newProject() {
        return "posts/new";
    }

    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public ModelAndView create(@RequestParam("message") String comment) {
        repository.save(new Post(comment));
        return new ModelAndView("redirect:/posts");
    }

    @RequestMapping(value = "/update", method = RequestMethod.POST)
    public ModelAndView update(@RequestParam("post_id") long id,
                               @RequestParam("message") String message) {
        Post post = repository.findOne(id);
        post.setMessage(message);
        repository.save(post);
        return new ModelAndView("redirect:/posts");
    }

    @RequestMapping(value = "/{id}/edit", method = RequestMethod.GET)
    public String edit(@PathVariable long id,
                       Model model) {
        Post post = repository.findOne(id);
        model.addAttribute("post", post);
        return "posts/edit";
    }
}

9) The mains template ( layout.html ) is a thymeleaf template, a basic html style template which has special tags to integrate dynamic content. It is similar to other template engines, such as Freemarker or Velocity, but more browser/designer friendly :

<!DOCTYPE html>
<html lang="en" ng-app="manager">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
    <meta name="description" content=""/>
    <meta name="author" content=""/>
    <!--<link rel="icon" href="../../favicon.ico"/>-->

    <title>Spring Boot Web Application</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous"/>
    <link href="/css/sticky-footer-navbar.css" rel="stylesheet"/>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha512-K1qjQ+NcF2TYO/eI3M6v8EiNYZfA95pQumfvcVrTHtwQVDG+aHRqLi/ETn2uB+1JqwYqVG3LIvdm9lj6imS/pQ==" crossorigin="anonymous"></script>
</head>

<body>

<!-- Fixed navbar -->
<!--<nav class="navbar navbar-default navbar-fixed-top">-->
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">FirstApp</a>
        </div>
    </div>
</nav>

<div class="container">

    <div layout:fragment="content">
        <!-- ============================================================================ -->
        <!-- This content is only used for static prototyping purposes (natural templates)-->
        <!-- and is therefore entirely optional.                                          -->
        <!-- ============================================================================ -->
        <h1>Static content for prototyping purposes only</h1>

        <p>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            Praesent scelerisque neque neque, ac elementum quam dignissim interdum.
            Phasellus et placerat elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
            Praesent scelerisque neque neque, ac elementum quam dignissim interdum.
            Phasellus et placerat elit.
        </p>

    </div>

</div>
<!-- /.container -->

<footer class="footer">
    <div class="container">
        <p class="text-muted">Marcio Marinho.</p>
    </div>
</footer>

</body>

</html>

10) The posts list template :

<html layout:decorator="layout">

<div layout:fragment="content">

    <a class="btn btn-primary" href="/posts/new">Add new post</a>

    <table class="table table-striped" id="posts_list">
        <thead>
        <tr>
            <th>Comments</th>
            <th>Action</th>
            <th></th>
        </tr>
        </thead>
        <tbody>
        <tr data-th-each="post : ${posts}">
            <td class="message" data-th-text="${post.message}"></td>

            <td>
                <a th:href="@{'/posts/{id}/delete'(id=${post.id})}" class="delete" th:attr="name=${post.message}">
                        <span class="fa-stack">
                            <i class="glyphicon glyphicon-trash"></i>
                        </span>
                </a>
            </td>

            <td>
                <a th:href="@{'/posts/{id}/edit'(id=${post.id})}" class="edit" th:attr="name=${post.message}">
                    <span class="fa-stack">
                        <i class="glyphicon glyphicon-edit"></i>
                    </span>

                </a>
            </td>
        </tr>
        </tbody>
    </table>
</div>
</html>

11) Add new post template :

<html layout:decorator="layout">
    <div layout:fragment="content">
        <form method="post" name="comment_form" id="comment_form" action="/posts/create" role="form">
            <div class="form-group">
                <label for="message">Post</label>
                <textarea rows="5" class="form-control" id="message" name="message"/>
            </div>
            <button type="submit" id="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</html>

12) Edit posts template :

<html layout:decorator="layout">
    <div layout:fragment="content">
        <form method="post" name="comment_form" id="comment_form" action="/posts/update" role="form">
            <div class="form-group">
                <label for="message">Post</label>
                <textarea rows="5" class="form-control" id="message" name="message" th:text="${post.message}"/>
            </div>
            <input type="hidden" id="post_id" rows="5" class="form-control" name="post_id" th:value="${post.id}"/>
            <button type="submit" id="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</html>

13) Two CSS files to decorate the templates :

/* Sticky footer styles
-------------------------------------------------- */
html {
    position: relative;
    min-height: 100%;
}
body {
    /* Margin bottom by footer height */
    margin-bottom: 60px;
}
.footer {
    position: absolute;
    bottom: 0;
    width: 100%;
    /* Set the fixed height of the footer here */
    height: 60px;
    background-color: #f5f5f5;
}


/* Custom page CSS
-------------------------------------------------- */
/* Not required for template or sticky footer method. */

body > .container {
    padding: 60px 15px 0;
}
.container .text-muted {
    margin: 20px 0;
}

.footer > .container {
    padding-right: 15px;
    padding-left: 15px;
}

code {
    font-size: 80%;
}
body {
    padding-top: 50px;
}
.starter-template {
    padding: 40px 15px;
    text-align: center;
}

14) Create a bootstrap main class

package au.com.riosoftware.firstapp;

import au.com.riosoftware.firstapp.domain.Post;
import au.com.riosoftware.firstapp.domain.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    private PostRepository repository;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... strings) throws Exception {
        for (int i = 0; i < 5; i++) {
            repository.save(new Post("My post number #" + (i+1)));
        }
    }
}

We’re done !

Now just build and run the application :

  • mvn package
  • java -jar target/firstapp-1.0-SNAPSHOT.jar
  • Open a browser and type http://localhost:8080/posts

You can also watch my videos building the application :

Building a stand-alone Spring Boot Application  : https://www.youtube.com/watch?v=VS8W-tEqIiw

Building a CRUD Web Application with Spring Boot : https://www.youtube.com/watch?v=TcP5kFPq354

Source code on GitHub : https://github.com/marciomarinho/firstapp

One thought on “Building a stand-alone Spring Boot Web Application from scratch

Leave a Reply

Your email address will not be published. Required fields are marked *