Spring Boot + AngularJS + Spring Data + JPA CRUD App Example

In this post we will be developing a full-blown CRUD application using Spring Boot, AngularJS, Spring Data, JPA/Hibernate and MySQL, learning the concepts in details along the way. This application can as well serve as a base/starting point for your own application. In addition, we will also use the notion of profiles to deploy the application into two different databases [H2 & MySQL] to emulate the local and production environment, to be more realistic. Let’s get going.

springbootcrudapp_img5


Following technologies stack being used:

  • Spring Boot 1.4.3.RELEASE
  • Spring 4.3.5.RELEASE [transitively]
  • Spring data JPA 1.10.6.RELEASE [transitively]
  • Hibernate 5.0.11.Final [transitively]
  • MySQL 5.1.40 [transitively]
  • H2 1.4.187
  • Hikari CP 2.4.7 [transitively]
  • AngularJS 1.5.8
  • Maven 3.1
  • JDK 1.8
  • Eclipse MARS.1

Let’s Begin.

1. Project Structure

springbootcrudapp_img0_1springbootcrudapp_img0_2

2. Dependency Management [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.websystique.springboot</groupId>
	<artifactId>SpringBootCRUDApplicationExample</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>

	<name>SpringBootCRUDApplicationExample</name>

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

	<properties>
		<java.version>1.8</java.version>
		<h2.version>1.4.187</h2.version>
	</properties>

	<dependencies>
		<!-- Add typical dependencies for a web application -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Add freemarker template support -->		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-freemarker</artifactId>
		</dependency>
		<!-- Add JPA support -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<!-- Add Hikari Connection Pooling support -->
		<dependency>
			<groupId>com.zaxxer</groupId>
			<artifactId>HikariCP</artifactId>
		</dependency>
		<!-- Add H2 database support [for running with local profile] -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<version>${h2.version}</version>
		</dependency>
		<!-- Add MySQL database support [for running with PRODUCTION profile] -->		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
    		<groupId>org.apache.commons</groupId>
    		<artifactId>commons-lang3</artifactId>
    		<version>3.5</version>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin><!-- Include if you want to make an executable jar[FAT JAR which 
				includes all dependencies along with sprinboot loader] that you can run on 
					commandline using java -jar NAME -->
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

spring-boot-starter-parent : In most of the cases[unless imported], your maven project POM will simply inherit from the spring-boot-starter-parent project. The spring-boot-starter-parent provides useful Maven defaults, provides dependency-management section so that you can omit version tags for dependencies you would need for your own project. Once inherited from spring-boot-starter-parent, you would declare dependencies to one or more “Starters” jars.

spring-boot-starter-web : Provides typical WEB MVC + Embedded container support.

spring-boot-starter-freemarker : Provides freemarker template support. We will be using freemarker in this example.

spring-boot-starter-data-jpa : Provides spring-data setup using JPA abstraction. Since we are talking about fast-development using spring-boot, spring-data would certainly save time compare to traditional DAO/Creteria/Query manual setup.

HikariCP : Provides Hikari connection pooling support. We could have as well used Tomcat datapooling. Common DBCP is usually not recommended for performance reasons.

h2: Provides H2 database support. Please note that it is used here just to demonstrate the real-life scenarios where your local setup uses one database while the one on production might be altogether a different database.Additionally, we are deliberately using a different version of h2, just to demonstrate that you CAN change the dependencies if needed.

mysql-connector-java: Provides MySQL database support. Again, just because we are simulating a local[H2]-Production[MySQL] scenario.

2. Spring Boot Application [Main class]

You read it right. Good old main is what all we need to start our newly created spring boot app. Spring Boot provides SpringApplication class to bootstrap a Spring application that will be started from a main() method using static SpringApplication.run method.

package com.websystique.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

import com.websystique.springboot.configuration.JpaConfiguration;


@Import(JpaConfiguration.class)
@SpringBootApplication(scanBasePackages={"com.websystique.springboot"})
public class SpringBootCRUDApp {

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

This class is annotated with @SpringBootApplication which is actually the combination of [shortcut] @EnableAutoConfiguration, @Configuration & @ComponentScan. You can choose either of them.

Spring Boot @EnableAutoConfiguration attempts to automatically configure your Spring application based on the jar dependencies that you have added. Since we have added spring-boot-starter-web, Spring boot will setup the Spring configuration for a web-application.

3. JPA configuation

In this configuration class, we are doing a lot: Creating datasource [using Hikari connection pooling], creating EntityManagerFactory, setting up transaction manager, referring to Spring-data repositories etc.

  • Spring Data @EnableJpaRepositories: @EnableJpaRepositories Annotation enables JPA repositories. It will scan the specified packages for Spring Data repositories. by default, it will look into current package for Spring-data repositories.
  • Spring Boot DataSourceProperties : DataSourceProperties is the helper class for configuration of a data source. Interesting point is that we can map the properties right from .yml files, thanks to hierarchical data. Matching-name properties from .yml will be mapped directly to properties of DataSourceProperties object.
  • Spring Boot DataSourceBuilder : DataSourceBuilder is a builder that can help creating a datasource using the mapped properties.
  • Additionally,if a datasource property is missing in DataSourceProperties [maxPoolSize e.g.], we can still take the advantage of good old @Value annotation to map it form property file to actual object property.
package com.websystique.springboot.configuration;

import java.util.Properties;

import javax.naming.NamingException;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableJpaRepositories(basePackages = "com.websystique.springboot.repositories",
		entityManagerFactoryRef = "entityManagerFactory",
		transactionManagerRef = "transactionManager")
@EnableTransactionManagement
public class JpaConfiguration {

	@Autowired
	private Environment environment;

	@Value("${datasource.sampleapp.maxPoolSize:10}")
	private int maxPoolSize;

	/*
	 * Populate SpringBoot DataSourceProperties object directly from application.yml 
	 * based on prefix.Thanks to .yml, Hierachical data is mapped out of the box with matching-name
	 * properties of DataSourceProperties object].
	 */
	@Bean
	@Primary
	@ConfigurationProperties(prefix = "datasource.sampleapp")
	public DataSourceProperties dataSourceProperties(){
		return new DataSourceProperties();
	}

	/*
	 * Configure HikariCP pooled DataSource.
	 */
	@Bean
	public DataSource dataSource() {
		DataSourceProperties dataSourceProperties = dataSourceProperties();
			HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder
					.create(dataSourceProperties.getClassLoader())
					.driverClassName(dataSourceProperties.getDriverClassName())
					.url(dataSourceProperties.getUrl())
					.username(dataSourceProperties.getUsername())
					.password(dataSourceProperties.getPassword())
					.type(HikariDataSource.class)
					.build();
			dataSource.setMaximumPoolSize(maxPoolSize);
			return dataSource;
	}

	/*
	 * Entity Manager Factory setup.
	 */
	@Bean
	public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
		LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
		factoryBean.setDataSource(dataSource());
		factoryBean.setPackagesToScan(new String[] { "com.websystique.springboot.model" });
		factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
		factoryBean.setJpaProperties(jpaProperties());
		return factoryBean;
	}

	/*
	 * Provider specific adapter.
	 */
	@Bean
	public JpaVendorAdapter jpaVendorAdapter() {
		HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
		return hibernateJpaVendorAdapter;
	}

	/*
	 * Here you can specify any provider specific properties.
	 */
	private Properties jpaProperties() {
		Properties properties = new Properties();
		properties.put("hibernate.dialect", environment.getRequiredProperty("datasource.sampleapp.hibernate.dialect"));
		properties.put("hibernate.hbm2ddl.auto", environment.getRequiredProperty("datasource.sampleapp.hibernate.hbm2ddl.method"));
		properties.put("hibernate.show_sql", environment.getRequiredProperty("datasource.sampleapp.hibernate.show_sql"));
		properties.put("hibernate.format_sql", environment.getRequiredProperty("datasource.sampleapp.hibernate.format_sql"));
		if(StringUtils.isNotEmpty(environment.getRequiredProperty("datasource.sampleapp.defaultSchema"))){
			properties.put("hibernate.default_schema", environment.getRequiredProperty("datasource.sampleapp.defaultSchema"));
		}
		return properties;
	}

	@Bean
	@Autowired
	public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
		JpaTransactionManager txManager = new JpaTransactionManager();
		txManager.setEntityManagerFactory(emf);
		return txManager;
	}

}

4. Property file [application.yml]

Although traditional .properties would just do fine, Spring Boot’s SpringApplication class also supports YAML out of the box provided SnakeYAML library is on class-path which usually would be due to starters. YAML is a superset of JSON, and as such is a very convenient format for specifying hierarchical configuration data. YAML file is also known as streams, containing several documents, each separated by three dashes (—). A line beginning with “—” may be used to explicitly denote the beginning of a new YAML document.YAML specification is a good read to know more about them.

src/main/resources/application.yml

server:
  port: 8080
  contextPath: /SpringBootCRUDApp
---
spring:
  profiles: local,default
datasource:
  sampleapp:
    url: jdbc:h2:~/test
    username: SA
    password:
    driverClassName: org.h2.Driver
    defaultSchema:
    maxPoolSize: 10
    hibernate:
      hbm2ddl.method: create-drop
      show_sql: true
      format_sql: true
      dialect: org.hibernate.dialect.H2Dialect
---
spring:
  profiles: prod
datasource:
  sampleapp:
    url: jdbc:mysql://localhost:3306/websystique
    username: myuser
    password: mypassword
    driverClassName: com.mysql.jdbc.Driver
    defaultSchema:
    maxPoolSize: 20
    hibernate:
      hbm2ddl.method: update
      show_sql: true
      format_sql: true
      dialect: org.hibernate.dialect.MySQLDialect
  • Since our app will be running on an Embedded container, we would need a way to configure the port and context-path for our app. By-default, Spring-Boot will use no context-path, and the default port would be 8080, means your application would be available at localhost:8080. But you can overwrite these properties by declaring them in application.yml [or application.yaml/application.properties] file. In our case, the first document [top level part, above ‘—‘ line] is the one configuring port and context path.
  • Since we will be using profiles, we have created two separate documents each with it’s own profile.
  • By default if no profile is specified, ‘default’ profile is used, this is standard spring behavior. You can additionally create different profiles based on your environments and use them on run.
  • In our case, we are pointing both default and local to same profile, hence letting user to run the app directly, without specifying any profile, in that case the default profile will be used. But you are free to specify a profile. While running our example [via IDE or command-line], we can provide the profile information using -Dspring.profiles.active=local or -Dspring.profiles.active=prod in VM arguments[for IDE] or on command-line java -jar JARPATH --spring.profiles.active=local.
  • Notice the datasource part in yml file: here we are specifying all stuff related to database. Similarly if you have other aspects/concerns [security e.g.], you could create separate levels for that. We will be using H2 database while running under profile ‘local’ and MySQL while running with profile ‘prod’.
In case you face trouble with YAML, Online YAML editor comes handy to validate your YAML.

5. Model

User.java

package com.websystique.springboot.model;

import org.hibernate.validator.constraints.NotEmpty;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name="APP_USER")
public class User implements Serializable{

	@Id
	@GeneratedValue(strategy= GenerationType.IDENTITY)
	private Long id;

	@NotEmpty
	@Column(name="NAME", nullable=false)
	private String name;

	@Column(name="AGE", nullable=false)
	private Integer age;

	@Column(name="SALARY", nullable=false)
	private double salary;

--- getter/setter omitted to save space
}

6. Spring-Data repositories

This one is rather simple.

package com.websystique.springboot.repositories;

import com.websystique.springboot.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    User findByName(String name);

}

That’s it for spring-data part. Interface JpaRepository packs a punch. It provides all the CRUD operations by-default using id as the key. In case you need to lookup on a property other than id, you could just create a ‘camelCase’ signature with that property, spring-data will itself generate the implementation and execute the appropriate SQL to get the data out from database. spring-data @Query annotation goes a step further by allowing you to write the JPQL or even native SQL yourself instead of relying on spring-data to do that. One could as well extend from CrudRepository instead of JpaRepository but JpaRepository provides some goodies like paging and sorting which most of the time is needed in a FE application.

7. Service

Our controller will be using this service for all user-related operations. Service in turn uses our spring-data repository to access and update the user.

package com.websystique.springboot.service;

import com.websystique.springboot.model.User;
import java.util.List;

public interface UserService {
	
	User findById(Long id);

	User findByName(String name);

	void saveUser(User user);

	void updateUser(User user);

	void deleteUserById(Long id);

	void deleteAllUsers();

	List<User> findAllUsers();

	boolean isUserExist(User user);
}
package com.websystique.springboot.service;

import java.util.List;

import com.websystique.springboot.model.User;
import com.websystique.springboot.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;



@Service("userService")
@Transactional
public class UserServiceImpl implements UserService{

	@Autowired
	private UserRepository userRepository;

	public User findById(Long id) {
		return userRepository.findOne(id);
	}

	public User findByName(String name) {
		return userRepository.findByName(name);
	}

	public void saveUser(User user) {
		userRepository.save(user);
	}

	public void updateUser(User user){
		saveUser(user);
	}

	public void deleteUserById(Long id){
		userRepository.delete(id);
	}

	public void deleteAllUsers(){
		userRepository.deleteAll();
	}

	public List<User> findAllUsers(){
		return userRepository.findAll();
	}

	public boolean isUserExist(User user) {
		return findByName(user.getName()) != null;
	}

}

8. Controllers

We have two controllers in our application. One for handling the view and other for handling the REST API calls, coming from Our AngularJS based Front-end.

package com.websystique.springboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class AppController {

	@RequestMapping("/")
	String home(ModelMap modal) {
		modal.addAttribute("title","CRUD Example");
		return "index";
	}

	@RequestMapping("/partials/{page}")
	String partialHandler(@PathVariable("page") final String page) {
		return page;
	}

}

package com.websystique.springboot.controller;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import com.websystique.springboot.model.User;
import com.websystique.springboot.service.UserService;
import com.websystique.springboot.util.CustomErrorType;

@RestController
@RequestMapping("/api")
public class RestApiController {

	public static final Logger logger = LoggerFactory.getLogger(RestApiController.class);

	@Autowired
	UserService userService; //Service which will do all data retrieval/manipulation work

	// -------------------Retrieve All Users---------------------------------------------

	@RequestMapping(value = "/user/", method = RequestMethod.GET)
	public ResponseEntity<List<User>> listAllUsers() {
		List<User> users = userService.findAllUsers();
		if (users.isEmpty()) {
			return new ResponseEntity(HttpStatus.NO_CONTENT);
			// You many decide to return HttpStatus.NOT_FOUND
		}
		return new ResponseEntity<List<User>>(users, HttpStatus.OK);
	}

	// -------------------Retrieve Single User------------------------------------------

	@RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
	public ResponseEntity<?> getUser(@PathVariable("id") long id) {
		logger.info("Fetching User with id {}", id);
		User user = userService.findById(id);
		if (user == null) {
			logger.error("User with id {} not found.", id);
			return new ResponseEntity(new CustomErrorType("User with id " + id 
					+ " not found"), HttpStatus.NOT_FOUND);
		}
		return new ResponseEntity<User>(user, HttpStatus.OK);
	}

	// -------------------Create a User-------------------------------------------

	@RequestMapping(value = "/user/", method = RequestMethod.POST)
	public ResponseEntity<?> createUser(@RequestBody User user, UriComponentsBuilder ucBuilder) {
		logger.info("Creating User : {}", user);

		if (userService.isUserExist(user)) {
			logger.error("Unable to create. A User with name {} already exist", user.getName());
			return new ResponseEntity(new CustomErrorType("Unable to create. A User with name " + 
			user.getName() + " already exist."),HttpStatus.CONFLICT);
		}
		userService.saveUser(user);

		HttpHeaders headers = new HttpHeaders();
		headers.setLocation(ucBuilder.path("/api/user/{id}").buildAndExpand(user.getId()).toUri());
		return new ResponseEntity<String>(headers, HttpStatus.CREATED);
	}

	// ------------------- Update a User ------------------------------------------------

	@RequestMapping(value = "/user/{id}", method = RequestMethod.PUT)
	public ResponseEntity<?> updateUser(@PathVariable("id") long id, @RequestBody User user) {
		logger.info("Updating User with id {}", id);

		User currentUser = userService.findById(id);

		if (currentUser == null) {
			logger.error("Unable to update. User with id {} not found.", id);
			return new ResponseEntity(new CustomErrorType("Unable to upate. User with id " + id + " not found."),
					HttpStatus.NOT_FOUND);
		}

		currentUser.setName(user.getName());
		currentUser.setAge(user.getAge());
		currentUser.setSalary(user.getSalary());

		userService.updateUser(currentUser);
		return new ResponseEntity<User>(currentUser, HttpStatus.OK);
	}

	// ------------------- Delete a User-----------------------------------------

	@RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
	public ResponseEntity<?> deleteUser(@PathVariable("id") long id) {
		logger.info("Fetching & Deleting User with id {}", id);

		User user = userService.findById(id);
		if (user == null) {
			logger.error("Unable to delete. User with id {} not found.", id);
			return new ResponseEntity(new CustomErrorType("Unable to delete. User with id " + id + " not found."),
					HttpStatus.NOT_FOUND);
		}
		userService.deleteUserById(id);
		return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
	}

	// ------------------- Delete All Users-----------------------------

	@RequestMapping(value = "/user/", method = RequestMethod.DELETE)
	public ResponseEntity<User> deleteAllUsers() {
		logger.info("Deleting All Users");

		userService.deleteAllUsers();
		return new ResponseEntity<User>(HttpStatus.NO_CONTENT);
	}

}

Additionally, a helper class to send errors [in-case any] from API in JSON format iso string.

package com.websystique.springboot.util;


public class CustomErrorType {

    private String errorMessage;

    public CustomErrorType(String errorMessage){
        this.errorMessage = errorMessage;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

}

Populate MySQL database

If you look back at application.yml, we have set the hibernate hbm2ddl as ‘create-drop‘ under ‘local’ profile, where as ‘update‘ under ‘prod’ profile, just for demonstration purpose. That mean in ‘local’ [H2], table will be dropped and recreated at application startup so we don’t need to create it manually. But in case of ‘prod’ [MySQL], we need to manually create the table if it does not exist. For MySQL, You can run following SQL to create table and populate dummy data.

create table APP_USER (
   id BIGINT NOT NULL AUTO_INCREMENT,
   name VARCHAR(30) NOT NULL,
   age  INTEGER NOT NULL,
   salary REAL NOT NULL,
   PRIMARY KEY (id)
);
  
/* Populate USER Table */
INSERT INTO APP_USER(name,age,salary)
VALUES ('Sam',30,70000);
  
INSERT INTO APP_USER(name,age,salary)
VALUES ('Tom',40,50000);

commit;

Front-end

Let’s add a view to our MVC app. We would be using Freemarker templates in our app. Spring Boot WebMvcAutoConfiguration adds FreeMarkerViewResolver with id ‘freeMarkerViewResolver’ if freemarker jar is in classpath, which is the case since we are using spring-boot-starter-freemarker. It looks for resources in a loader path (externalized to spring.freemarker.templateLoaderPath, default ‘classpath:/templates/’) by surrounding the view name with a prefix and suffix (externalized to spring.freemarker.prefix and spring.freemarker.suffix, with empty and ‘.ftl’ defaults respectively). It can be overridden by providing a bean of the same name.

Although one can develop a complete FE using freemarker itself with tons of scripts and cryptic expressions with ‘#’ lurking around all over the page, question is should we, knowing that we are not in 1990 anymore? I decided to use AngularJS [with ui-router] instead, using freemarker just as a container, nothing else.

Freemarker Templates

src/main/resources/templates/index.ftl

<!DOCTYPE html>

<html lang="en" ng-app="crudApp">
    <head>
        <title>${title}</title>
        <link href="css/bootstrap.css" rel="stylesheet"/>
        <link href="css/app.css" rel="stylesheet"/>
    </head>
    <body>

        <div ui-view></div>
        <script src="js/lib/angular.min.js" ></script>
        <script src="js/lib/angular-ui-router.min.js" ></script>
        <script src="js/lib/localforage.min.js" ></script>
        <script src="js/lib/ngStorage.min.js"></script>
        <script src="js/app/app.js"></script>
        <script src="js/app/UserService.js"></script>
        <script src="js/app/UserController.js"></script>
    </body>
</html>

src/main/resources/templates/list.ftl

<div class="generic-container">
    <div class="panel panel-default">
        <!-- Default panel contents -->
        <div class="panel-heading"><span class="lead">Specific User </span></div>
		<div class="panel-body">
	        <div class="formcontainer">
	            <div class="alert alert-success" role="alert" ng-if="ctrl.successMessage">{{ctrl.successMessage}}</div>
	            <div class="alert alert-danger" role="alert" ng-if="ctrl.errorMessage">{{ctrl.errorMessage}}</div>
	            <form ng-submit="ctrl.submit()" name="myForm" class="form-horizontal">
	                <input type="hidden" ng-model="ctrl.user.id" />
	                <div class="row">
	                    <div class="form-group col-md-12">
	                        <label class="col-md-2 control-lable" for="uname">Name</label>
	                        <div class="col-md-7">
	                            <input type="text" ng-model="ctrl.user.name" id="uname" class="username form-control input-sm" placeholder="Enter your name" required ng-minlength="3"/>
	                        </div>
	                    </div>
	                </div>

	                <div class="row">
	                    <div class="form-group col-md-12">
	                        <label class="col-md-2 control-lable" for="age">Age</label>
	                        <div class="col-md-7">
	                            <input type="text" ng-model="ctrl.user.age" id="age" class="form-control input-sm" placeholder="Enter your Age." required ng-pattern="ctrl.onlyIntegers"/>
	                        </div>
	                    </div>
	                </div>
	
	                <div class="row">
	                    <div class="form-group col-md-12">
	                        <label class="col-md-2 control-lable" for="salary">Salary</label>
	                        <div class="col-md-7">
	                            <input type="text" ng-model="ctrl.user.salary" id="salary" class="form-control input-sm" placeholder="Enter your Salary." required ng-pattern="ctrl.onlyNumbers"/>
	                        </div>
	                    </div>
	                </div>

	                <div class="row">
	                    <div class="form-actions floatRight">
	                        <input type="submit"  value="{{!ctrl.user.id ? 'Add' : 'Update'}}" class="btn btn-primary btn-sm" ng-disabled="myForm.$invalid || myForm.$pristine">
	                        <button type="button" ng-click="ctrl.reset()" class="btn btn-warning btn-sm" ng-disabled="myForm.$pristine">Reset Form</button>
	                    </div>
	                </div>
	            </form>
    	    </div>
		</div>	
    </div>
    <div class="panel panel-default">
        <!-- Default panel contents -->
        <div class="panel-heading"><span class="lead">List of Users </span></div>
		<div class="panel-body">
			<div class="table-responsive">
		        <table class="table table-hover">
		            <thead>
		            <tr>
		                <th>ID</th>
		                <th>NAME</th>
		                <th>AGE</th>
		                <th>SALARY</th>
		                <th width="100"></th>
		                <th width="100"></th>
		            </tr>
		            </thead>
		            <tbody>
		            <tr ng-repeat="u in ctrl.getAllUsers()">
		                <td>{{u.id}}</td>
		                <td>{{u.name}}</td>
		                <td>{{u.age}}</td>
		                <td>{{u.salary}}</td>
		                <td><button type="button" ng-click="ctrl.editUser(u.id)" class="btn btn-success custom-width">Edit</button></td>
		                <td><button type="button" ng-click="ctrl.removeUser(u.id)" class="btn btn-danger custom-width">Remove</button></td>
		            </tr>
		            </tbody>
		        </table>		
			</div>
		</div>
    </div>
</div>

Static resources

Static resources like images/css/JS in a Spring boot application are commonly located in a directory called /static (or /public or /resources or /META-INF/resources) in the classpath or from the root of the ServletContext. In this example, we are using bootstrap.css which is located in src/main/resources/static/css.

Error Page

By default, Spring Boot installs a ‘whitelabel’ error page that is shown in browser client if you encounter a server error. You can override that page, based upon the templating technology you are using. For freemarker, you can create a page with name ‘error.ftl’ which would be shown in case an error occurred.

src/main/resources/templates/error.ftl

<!DOCTYPE html>

<html lang="en">
<head>
    <link rel="stylesheet" type="text/css" href="css/bootstrap.css" />
</head>
<body>
	<div class="container">
	    <div class="jumbotron alert-danger">
			<h1>Oops. Something went wrong</h1>
			<h2>${status} ${error}</h2>
	    </div>
	</div>
</body>

</html>

AngularJs [ui-router based app]

src/main/resources/static/js/app.js

var app = angular.module('crudApp',['ui.router','ngStorage']);

app.constant('urls', {
    BASE: 'http://localhost:8080/SpringBootCRUDApp',
    USER_SERVICE_API : 'http://localhost:8080/SpringBootCRUDApp/api/user/'
});

app.config(['$stateProvider', '$urlRouterProvider',
    function($stateProvider, $urlRouterProvider) {

        $stateProvider
            .state('home', {
                url: '/',
                templateUrl: 'partials/list',
                controller:'UserController',
                controllerAs:'ctrl',
                resolve: {
                    users: function ($q, UserService) {
                        console.log('Load all users');
                        var deferred = $q.defer();
                        UserService.loadAllUsers().then(deferred.resolve, deferred.resolve);
                        return deferred.promise;
                    }
                }
            });
        $urlRouterProvider.otherwise('/');
    }]);

src/main/resources/static/js/UserService.js

'use strict';

angular.module('crudApp').factory('UserService',
    ['$localStorage', '$http', '$q', 'urls',
        function ($localStorage, $http, $q, urls) {

            var factory = {
                loadAllUsers: loadAllUsers,
                getAllUsers: getAllUsers,
                getUser: getUser,
                createUser: createUser,
                updateUser: updateUser,
                removeUser: removeUser
            };

            return factory;

            function loadAllUsers() {
                console.log('Fetching all users');
                var deferred = $q.defer();
                $http.get(urls.USER_SERVICE_API)
                    .then(
                        function (response) {
                            console.log('Fetched successfully all users');
                            $localStorage.users = response.data;
                            deferred.resolve(response);
                        },
                        function (errResponse) {
                            console.error('Error while loading users');
                            deferred.reject(errResponse);
                        }
                    );
                return deferred.promise;
            }

            function getAllUsers(){
                return $localStorage.users;
            }

            function getUser(id) {
                console.log('Fetching User with id :'+id);
                var deferred = $q.defer();
                $http.get(urls.USER_SERVICE_API + id)
                    .then(
                        function (response) {
                            console.log('Fetched successfully User with id :'+id);
                            deferred.resolve(response.data);
                        },
                        function (errResponse) {
                            console.error('Error while loading user with id :'+id);
                            deferred.reject(errResponse);
                        }
                    );
                return deferred.promise;
            }

            function createUser(user) {
                console.log('Creating User');
                var deferred = $q.defer();
                $http.post(urls.USER_SERVICE_API, user)
                    .then(
                        function (response) {
                            loadAllUsers();
                            deferred.resolve(response.data);
                        },
                        function (errResponse) {
                           console.error('Error while creating User : '+errResponse.data.errorMessage);
                           deferred.reject(errResponse);
                        }
                    );
                return deferred.promise;
            }

            function updateUser(user, id) {
                console.log('Updating User with id '+id);
                var deferred = $q.defer();
                $http.put(urls.USER_SERVICE_API + id, user)
                    .then(
                        function (response) {
                            loadAllUsers();
                            deferred.resolve(response.data);
                        },
                        function (errResponse) {
                            console.error('Error while updating User with id :'+id);
                            deferred.reject(errResponse);
                        }
                    );
                return deferred.promise;
            }

            function removeUser(id) {
                console.log('Removing User with id '+id);
                var deferred = $q.defer();
                $http.delete(urls.USER_SERVICE_API + id)
                    .then(
                        function (response) {
                            loadAllUsers();
                            deferred.resolve(response.data);
                        },
                        function (errResponse) {
                            console.error('Error while removing User with id :'+id);
                            deferred.reject(errResponse);
                        }
                    );
                return deferred.promise;
            }

        }
    ]);

src/main/resources/static/js/UserController.js

'use strict';

angular.module('crudApp').controller('UserController',
    ['UserService', '$scope',  function( UserService, $scope) {

        var self = this;
        self.user = {};
        self.users=[];

        self.submit = submit;
        self.getAllUsers = getAllUsers;
        self.createUser = createUser;
        self.updateUser = updateUser;
        self.removeUser = removeUser;
        self.editUser = editUser;
        self.reset = reset;

        self.successMessage = '';
        self.errorMessage = '';
        self.done = false;

        self.onlyIntegers = /^\d+$/;
        self.onlyNumbers = /^\d+([,.]\d+)?$/;

        function submit() {
            console.log('Submitting');
            if (self.user.id === undefined || self.user.id === null) {
                console.log('Saving New User', self.user);
                createUser(self.user);
            } else {
                updateUser(self.user, self.user.id);
                console.log('User updated with id ', self.user.id);
            }
        }

        function createUser(user) {
            console.log('About to create user');
            UserService.createUser(user)
                .then(
                    function (response) {
                        console.log('User created successfully');
                        self.successMessage = 'User created successfully';
                        self.errorMessage='';
                        self.done = true;
                        self.user={};
                        $scope.myForm.$setPristine();
                    },
                    function (errResponse) {
                        console.error('Error while creating User');
                        self.errorMessage = 'Error while creating User: ' + errResponse.data.errorMessage;
                        self.successMessage='';
                    }
                );
        }


        function updateUser(user, id){
            console.log('About to update user');
            UserService.updateUser(user, id)
                .then(
                    function (response){
                        console.log('User updated successfully');
                        self.successMessage='User updated successfully';
                        self.errorMessage='';
                        self.done = true;
                        $scope.myForm.$setPristine();
                    },
                    function(errResponse){
                        console.error('Error while updating User');
                        self.errorMessage='Error while updating User '+errResponse.data;
                        self.successMessage='';
                    }
                );
        }


        function removeUser(id){
            console.log('About to remove User with id '+id);
            UserService.removeUser(id)
                .then(
                    function(){
                        console.log('User '+id + ' removed successfully');
                    },
                    function(errResponse){
                        console.error('Error while removing user '+id +', Error :'+errResponse.data);
                    }
                );
        }


        function getAllUsers(){
            return UserService.getAllUsers();
        }

        function editUser(id) {
            self.successMessage='';
            self.errorMessage='';
            UserService.getUser(id).then(
                function (user) {
                    self.user = user;
                },
                function (errResponse) {
                    console.error('Error while removing user ' + id + ', Error :' + errResponse.data);
                }
            );
        }
        function reset(){
            self.successMessage='';
            self.errorMessage='';
            self.user={};
            $scope.myForm.$setPristine(); //reset Form
        }
    }
    ]);

Run the application

Finally, Let’s run the application, firstly with ‘local’ profile [H2]. Next shot will be with ‘prod’ profile [MySQL].

Via Eclipse:: Run it directly, in that case default profile will be used. In case you want a different profile to be used, create a Run configuration for you main class, specifying the profile. To do that from toolbar, select Run->Run Configurations->Arguments->VM Arguments. Add -Dspring.profiles.active=local or -Dspring.profiles.active=prod]

Via Command line::
On project root
$> java -jar target/SpringBootCRUDApplicationExample-1.0.0.jar –spring.profiles.active=local

Please take special note of two ‘-‘ in front of spring.profiles.active. In the blog it might be appearing as single ‘-‘ but there are in fact two ‘-‘ of them.


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.3.RELEASE)

2016-12-26 17:15:35.114  INFO 4496 --- [           main] c.w.springboot.SpringBootCRUDApp         : Starting SpringBootCRUDApp on dragon with PID 4496 (D:\Development\workspaces\workspace_websystique\SpringBootCRUDApplicationExample\target\classes started by ADMIN in D:\Development\workspaces\workspace_websystique\SpringBootCRUDApplicationExample)
2016-12-26 17:15:35.121  INFO 4496 --- [           main] c.w.springboot.SpringBootCRUDApp         : The following profiles are active: local
2016-12-26 17:15:35.225  INFO 4496 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45b4c3a9: startup date [Mon Dec 26 17:15:35 CET 2016]; root of context hierarchy
2016-12-26 17:15:38.422  INFO 4496 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$e0f3dd92] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2016-12-26 17:15:39.458  INFO 4496 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-12-26 17:15:39.484  INFO 4496 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-12-26 17:15:39.486  INFO 4496 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.6
2016-12-26 17:15:39.692  INFO 4496 --- [ost-startStop-1] o.a.c.c.C.[.[.[/SpringBootCRUDApp]       : Initializing Spring embedded WebApplicationContext
2016-12-26 17:15:39.693  INFO 4496 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 4474 ms
2016-12-26 17:15:40.031  INFO 4496 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2016-12-26 17:15:40.037  INFO 4496 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-12-26 17:15:40.038  INFO 4496 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-12-26 17:15:40.038  INFO 4496 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-12-26 17:15:40.038  INFO 4496 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2016-12-26 17:15:40.334  INFO 4496 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2016-12-26 17:15:40.360  INFO 4496 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
	name: default
	...]
2016-12-26 17:15:40.488  INFO 4496 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.0.11.Final}
2016-12-26 17:15:40.490  INFO 4496 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2016-12-26 17:15:40.492  INFO 4496 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2016-12-26 17:15:40.569  INFO 4496 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.1.Final}
2016-12-26 17:15:40.786  INFO 4496 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Started.
2016-12-26 17:15:41.228  INFO 4496 --- [           main] com.zaxxer.hikari.pool.PoolBase          : HikariPool-1 - Driver does not support get/set network timeout for connections. (org.h2.jdbc.JdbcConnection.getNetworkTimeout()I)
2016-12-26 17:15:41.311  INFO 4496 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2016-12-26 17:15:41.908  INFO 4496 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
Hibernate: 
    drop table APP_USER if exists
Hibernate: 
    create table APP_USER (
        id bigint generated by default as identity,
        AGE integer not null,
        NAME varchar(255) not null,
        SALARY double not null,
        primary key (id)
    )
2016-12-26 17:15:41.927  INFO 4496 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2016-12-26 17:15:41.987  INFO 4496 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2016-12-26 17:15:42.983  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45b4c3a9: startup date [Mon Dec 26 17:15:35 CET 2016]; root of context hierarchy
2016-12-26 17:15:43.109  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto java.lang.String com.websystique.springboot.controller.AppController.home(org.springframework.ui.ModelMap)
2016-12-26 17:15:43.110  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/partials/{page}]}" onto java.lang.String com.websystique.springboot.controller.AppController.partialHandler(java.lang.String)
2016-12-26 17:15:43.116  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[GET]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.getUser(long)
2016-12-26 17:15:43.117  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[GET]}" onto public org.springframework.http.ResponseEntity<java.util.List<com.websystique.springboot.model.User>> com.websystique.springboot.controller.RestApiController.listAllUsers()
2016-12-26 17:15:43.117  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[POST]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.createUser(com.websystique.springboot.model.User,org.springframework.web.util.UriComponentsBuilder)
2016-12-26 17:15:43.117  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[PUT]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.updateUser(long,com.websystique.springboot.model.User)
2016-12-26 17:15:43.118  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[DELETE]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.deleteUser(long)
2016-12-26 17:15:43.118  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[DELETE]}" onto public org.springframework.http.ResponseEntity<com.websystique.springboot.model.User> com.websystique.springboot.controller.RestApiController.deleteAllUsers()
2016-12-26 17:15:43.122  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : 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)
2016-12-26 17:15:43.123  INFO 4496 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2016-12-26 17:15:43.185  INFO 4496 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 17:15:43.185  INFO 4496 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 17:15:43.261  INFO 4496 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 17:15:43.708  INFO 4496 --- [           main] o.s.w.s.v.f.FreeMarkerConfigurer         : ClassTemplateLoader for Spring macros added to FreeMarker configuration
2016-12-26 17:15:44.002  INFO 4496 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-12-26 17:15:44.004  INFO 4496 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'dataSource' has been autodetected for JMX exposure
2016-12-26 17:15:44.014  INFO 4496 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2016-12-26 17:15:44.102  INFO 4496 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-12-26 17:15:44.110  INFO 4496 --- [           main] c.w.springboot.SpringBootCRUDApp         : Started SpringBootCRUDApp in 9.731 seconds (JVM running for 10.325)

Open your browser and navigate to http://localhost:8080/SpringBootCRUDApp/

springbootcrudapp_img1

Add few users.

springbootcrudapp_img2

Try to add a user with same name as an existing user, should get an error [this is backend throwing the error, you can change the logic on backend based on your business rules].

springbootcrudapp_img3

Reset the form.Remove a user, edit the other one.

springbootcrudapp_img4

Now shutdown your app and restart it using ‘prod’ profile this time.


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.3.RELEASE)

2016-12-26 18:58:18.947  INFO 6828 --- [           main] c.w.springboot.SpringBootCRUDApp         : Starting SpringBootCRUDApp on dragon with PID 6828 (D:\Development\workspaces\workspace_websystique\SpringBootCRUDApplicationExample\target\classes started by ADMIN in D:\Development\workspaces\workspace_websystique\SpringBootCRUDApplicationExample)
2016-12-26 18:58:18.952  INFO 6828 --- [           main] c.w.springboot.SpringBootCRUDApp         : The following profiles are active: prod
2016-12-26 18:58:19.073  INFO 6828 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45b4c3a9: startup date [Mon Dec 26 18:58:19 CET 2016]; root of context hierarchy
2016-12-26 18:58:21.827  INFO 6828 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$e0f3dd92] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2016-12-26 18:58:22.808  INFO 6828 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2016-12-26 18:58:22.833  INFO 6828 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2016-12-26 18:58:22.835  INFO 6828 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.6
2016-12-26 18:58:23.052  INFO 6828 --- [ost-startStop-1] o.a.c.c.C.[.[.[/SpringBootCRUDApp]       : Initializing Spring embedded WebApplicationContext
2016-12-26 18:58:23.053  INFO 6828 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 3986 ms
2016-12-26 18:58:23.386  INFO 6828 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2016-12-26 18:58:23.395  INFO 6828 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2016-12-26 18:58:23.396  INFO 6828 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2016-12-26 18:58:23.397  INFO 6828 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2016-12-26 18:58:23.397  INFO 6828 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2016-12-26 18:58:23.640  INFO 6828 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2016-12-26 18:58:23.675  INFO 6828 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
	name: default
	...]
2016-12-26 18:58:23.823  INFO 6828 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.0.11.Final}
2016-12-26 18:58:23.826  INFO 6828 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2016-12-26 18:58:23.830  INFO 6828 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2016-12-26 18:58:23.912  INFO 6828 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.1.Final}
2016-12-26 18:58:24.110  INFO 6828 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Started.
2016-12-26 18:58:24.823  INFO 6828 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2016-12-26 18:58:25.607  INFO 6828 --- [           main] org.hibernate.tool.hbm2ddl.SchemaUpdate  : HHH000228: Running hbm2ddl schema update
2016-12-26 18:58:25.774  INFO 6828 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2016-12-26 18:58:26.707  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45b4c3a9: startup date [Mon Dec 26 18:58:19 CET 2016]; root of context hierarchy
2016-12-26 18:58:26.833  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto java.lang.String com.websystique.springboot.controller.AppController.home(org.springframework.ui.ModelMap)
2016-12-26 18:58:26.835  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/partials/{page}]}" onto java.lang.String com.websystique.springboot.controller.AppController.partialHandler(java.lang.String)
2016-12-26 18:58:26.840  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[GET]}" onto public org.springframework.http.ResponseEntity<java.util.List<com.websystique.springboot.model.User>> com.websystique.springboot.controller.RestApiController.listAllUsers()
2016-12-26 18:58:26.841  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[GET]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.getUser(long)
2016-12-26 18:58:26.841  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[POST]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.createUser(com.websystique.springboot.model.User,org.springframework.web.util.UriComponentsBuilder)
2016-12-26 18:58:26.842  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[PUT]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.updateUser(long,com.websystique.springboot.model.User)
2016-12-26 18:58:26.842  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/{id}],methods=[DELETE]}" onto public org.springframework.http.ResponseEntity<?> com.websystique.springboot.controller.RestApiController.deleteUser(long)
2016-12-26 18:58:26.842  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/api/user/],methods=[DELETE]}" onto public org.springframework.http.ResponseEntity<com.websystique.springboot.model.User> com.websystique.springboot.controller.RestApiController.deleteAllUsers()
2016-12-26 18:58:26.846  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : 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)
2016-12-26 18:58:26.847  INFO 6828 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2016-12-26 18:58:26.903  INFO 6828 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 18:58:26.904  INFO 6828 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 18:58:27.008  INFO 6828 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2016-12-26 18:58:27.363  INFO 6828 --- [           main] o.s.w.s.v.f.FreeMarkerConfigurer         : ClassTemplateLoader for Spring macros added to FreeMarker configuration
2016-12-26 18:58:27.604  INFO 6828 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2016-12-26 18:58:27.606  INFO 6828 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'dataSource' has been autodetected for JMX exposure
2016-12-26 18:58:27.614  INFO 6828 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2016-12-26 18:58:27.711  INFO 6828 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2016-12-26 18:58:27.720  INFO 6828 --- [           main] c.w.springboot.SpringBootCRUDApp         : Started SpringBootCRUDApp in 9.478 seconds (JVM running for 10.023)

Provided your mysql is up and running, you should get following, right from MySQL database this time :

springbootcrudapp_img5

Conclusion

Although the post was bit long, Spring Boot and associated concepts are fairly trivial. Spring Boot reduces the development time by many-fold, worth giving a try. The application we developed here is fully loaded and can be used in a live environment or as the base application for your own projects. Feel free to write your thoughts in comment section.

Download Source Code


References