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.
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
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-linejava -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’ andMySQL
while running with profile ‘prod’.
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/
Add few users.
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].
Reset the form.Remove a user, edit the other one.
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 :
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
If you like tutorials on this site, why not take a step further and connect me on Facebook , Google Plus & Twitter as well? I would love to hear your thoughts on these articles, it will help improve further our learning process.