Implementing the backend of the application (9/9)

At the end of this part, we will have a Spring Boot application relying on AngularJS (seen in previous parts) and Jersey/Jackson/Hibernate that implements the backend part. 

Here is the list of tasks we will perform :



The Maven modules design

The application is very simple but it is not a reason to not following good design principles.

We will follow a classic design by splitting the application into three logical layers :

  • Web application
  • Business services (used by the Web application)
  • Data access services (used by the Business services)

All these layers depend on a common module that is not particularly coupled to a layer. It could contain things as enumeration or utilities.

In the first part, we have declared only the contact-webapp module. Now, we will declare others.
We update the parent/multi-module contact-parent that now declares the following modules :  contact-webapp , contact-servicecontact-db and contact-common.

Here is the target layout :

app-angular-layout-modules

Setting up the parent pom

In the parent pom.xml, we define the configuration to enforce in the modules :

  • the compiler version (JDK 8)
  • the file encoding type (UTF-8)
  • the dependencies that modules may use if they specify them.
    The dependencies pulled by Spring Boot are of course not explicitly declared in our parent pom. For modules, these are managed dependencies and not directly inherited dependencies. So, they have to declare them (without the version) to use it.

Here is the parent/multi-modules pom :

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
 
	<groupId>davidhxxx.example.rest.springbootangular</groupId>
	<artifactId>contact-parent</artifactId>
	<packaging>pom</packaging>
	<version>1.0-SNAPSHOT</version>
	<name>${project.artifactId}</name>
 
	<properties>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<springboot.version>1.4.0.RELEASE</springboot.version>
	</properties>
 
	<modules>
		<module>contact-common</module>
		<module>contact-db</module>
		<module>contact-service</module>
		<module>contact-webapp</module>
	</modules>
 
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>${springboot.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
 
</project>

You can notice that the parent pom doesn’t inherit from the spring-boot-starter-parent artifactId :

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

The way I use to  configure Spring Boot in the Maven project is designed for projects that need to declare a parent pom specific to their application (therefore different from spring-boot-starter-parent).
As a part of the contact application we are going to build, the parent pom doesn’t need to declare a specific parent pom but I will not use spring-boot-starter-parent as  parent pom either. 
Personally, I avoid to use spring-boot-starter-parent as it has multiple drawbacks :
For example : Java 1.6 is used as the default compiler level (why 1.6 ?), the Maven filtering is changed to use @..@ placeholders (Is the benefit deserves to break the Maven standard ?), we loose the ability to choose the hierarchy of the parent poms, etc…

Setting up the common module part

Here is the pom :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<artifactId>contact-common-task9</artifactId>
	<name>${project.artifactId}</name>
 
	<parent>
		<groupId>davidhxxx.example.rest.springbootangular</groupId>
		<artifactId>contact-parent-task9</artifactId>
		<version>1.0-SNAPSHOT</version>
	</parent>
</project>

The module doesn’t need any dependency as it will only contain classes.
It doesn’t contain a Spring configuration either.
This is the Sex enum, the single class we declare in this module :

package davidhxxx.example.angularsboot.common;
 
public enum Sex {
    MALE, FEMALE, UNKNOWN;
}

Setting up the database access module part

We include the spring-boot-starter-data-jpa to provide libraries needed to use Spring JPA.
We also need to include the h2 library in order that Spring Boot starts an H2 database instance and use the h2 JDBC drivers to configure the Hibernate configuration.
Here is the pom :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
 
	<artifactId>contact-db-task9</artifactId>
	<name>${project.artifactId}</name>
 
	<parent>
		<groupId>davidhxxx.example.rest.springbootangular</groupId>
		<artifactId>contact-parent-task9</artifactId>
		<version>1.0-SNAPSHOT</version>
	</parent>
 
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>
		<!-- my dependencies -->
		<dependency>
			<groupId>davidhxxx.example.rest.springbootangular</groupId>
			<artifactId>contact-common-task9</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
	</dependencies>
 
</project>

It is a good practice to have one Spring configuration by logical layer in order to keep an isolation between. It eases the reading and the maintainability of them. 
Here is the Spring configuration used for the db acess module :

package davidhxxx.example.angularsboot.db;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
import davidhxxx.example.angularsboot.db.model.Contact;
 
@EnableAutoConfiguration
@ComponentScan
@EntityScan(basePackageClasses = Contact.class)
@EnableJpaRepositories
public class AppDbConfig {
 
}

We add the Contact class that is also the Contact entity.

package davidhxxx.example.angularsboot.db.model;
 
import java.io.Serializable;
import java.time.LocalDate;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
 
import davidhxxx.example.angularsboot.common.Sex;
 
@Entity
@Table(name = "CONTACT")
public class Contact implements Serializable {
 
	private static final long serialVersionUID = 1L;
 
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;
 
	@Column
	private String firstName;
 
	@Column
	private String lastName;
 
	@Column
	private String email;
 
	@Column
	private Double salary;
 
	@OneToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE })
	@JoinColumn(name = "ADDRESS_FK", unique = true, nullable = true, updatable = true)
	private Address address;
 
	@Column
	private String phone;
 
	@Enumerated
	private Sex sex;
 
        @Column
	private LocalDate birthday;
 
	public Long getId() {
		return id;
	}
 
	public void setId(Long id) {
		this.id = id;
	}
 
	public String getFirstName() {
		return firstName;
	}
 
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
 
	public String getLastName() {
		return lastName;
	}
 
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
 
	public Double getSalary() {
		return salary;
	}
 
	public void setSalary(Double salary) {
		this.salary = salary;
	}
 
	public String getEmail() {
		return email;
	}
 
	public void setEmail(String email) {
		this.email = email;
	}
 
	public Address getAddress() {
		return address;
	}
 
	public void setAddress(Address address) {
		this.address = address;
	}
 
	public Sex getSex() {
		return sex;
	}
 
	public void setSex(Sex sex) {
		this.sex = sex;
	}
 
	public LocalDate getBirthday() {
		return birthday;
	}
 
	public void setBirthday(LocalDate birthday) {
		this.birthday = birthday;
	}
 
	public String getPhone() {
		return phone;
	}
 
	public void setPhone(String phone) {
		this.phone = phone;
	}
 
}

Here is the Spring JpaRepository interface that we will use to perform reading, writing and deleting on Contact entities :

package davidhxxx.example.angularsboot.db.repository;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
import davidhxxx.example.angularsboot.db.model.Contact;
 
public interface ContactRepository extends JpaRepository<Contact, Long> {
}

This interface provides Default implementation of the org.springframework.data.repository.CrudRepository interface. This offer us a more sophisticated interface (with more CRUD methods) than the plain EntityManager . The implementation of this interface is SimpleJpaRepository and is created by Spring when the Spring Context is loaded.

Setting up the service module part

We declare the db module dependency but also third party dependencies.
We declare the jackson-databind library to specify JSON annotations on the DTO classes and the jackson-datatype-jsr310 library to be able to use the Java 8 LocalDate in the JSON mapping.
At last, we use the modelmapper library to map from DTOs to JPA entities and reversely.

Here is the pom :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
 
	<artifactId>contact-service-task9</artifactId>
	<name>${project.artifactId}</name>
 
	<parent>
		<groupId>davidhxxx.example.rest.springbootangular</groupId>
		<artifactId>contact-parent-task9</artifactId>
		<version>1.0-SNAPSHOT</version>
	</parent>
 
	<dependencies>
		<!-- my dependencies -->
		<dependency>
			<groupId>davidhxxx.example.rest.springbootangular</groupId>
			<artifactId>contact-db-task9</artifactId>
			<version>1.0-SNAPSHOT</version>
		</dependency>
		<!-- other dependencies -->
		<dependency>
			<groupId>org.modelmapper</groupId>
			<artifactId>modelmapper</artifactId>
			<version>0.7.4</version>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>
	</dependencies>
 
</project>

Here is the Spring configuration used for this module :

package davidhxxx.example.angularsboot.service;
 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
 
import davidhxxx.example.angularsboot.db.AppDbConfig;
 
@Import(AppDbConfig.class)
@EnableAutoConfiguration
@ComponentScan
public class AppServiceConfig {
}

We can see that it imports the AppDbConfig configuration.

We add the ContactDTO class that represents the Contact counterpart in the service layer.

package davidhxxx.example.angularsboot.service.dto;
 
import java.io.Serializable;
import java.time.LocalDate;
 
import com.fasterxml.jackson.annotation.JsonFormat;
 
import davidhxxx.example.angularsboot.common.Sex;
 
public class ContactDTO implements Serializable {
 
	private static final long serialVersionUID = 1L;
 
	private Long id;
 
	private Sex sex;
 
	private String firstName;
 
	private String lastName;
 
	private String email;
 
	private Double salary;
 
	private LocalDate birthday;
 
	private AddressDTO address;
 
	private String phone;
 
	public Long getId() {
		return id;
	}
 
	public void setId(Long id) {
		this.id = id;
	}
 
	public String getFirstName() {
		return firstName;
	}
 
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
 
	public String getLastName() {
		return lastName;
	}
 
	public void setLastName(String lastName) {
		this.lastName = lastName;
	}
 
	public Double getSalary() {
		return salary;
	}
 
	public void setSalary(Double salary) {
		this.salary = salary;
	}
 
	public AddressDTO getAddress() {
		return address;
	}
 
	public void setAddress(AddressDTO address) {
		this.address = address;
	}
 
	public String getEmail() {
		return email;
	}
 
	public void setEmail(String email) {
		this.email = email;
	}
 
	public Sex getSex() {
		return sex;
	}
 
	public void setSex(Sex sex) {
		this.sex = sex;
	}
 
	public String getPhone() {
		return phone;
	}
 
	public void setPhone(String phone) {
		this.phone = phone;
	}
 
	public LocalDate getBirthday() {
		return birthday;
	}
 
	public void setBirthday(LocalDate birthday) {
		this.birthday = birthday;
	}
 
}

Here is ModelMapperService, a custom service to map entities to DTOs and reversely.
It is a simple wrapper of the org.modelmapper.ModelMapper class that provides two methods :

package davidhxxx.example.angularsboot.service.util;
 
import java.util.ArrayList;
import java.util.List;
 
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
 
@Service
public class ModelMapperService {
 
	private ModelMapper modelMapper = new ModelMapper();
 
	public <D> D map(Object source, Class<D> destinationType) {
		return modelMapper.map(source, destinationType);
	}
 
	public <D> List<D> mapList(List<?> inputList, Class<D> class1) {
		List<D> targetList = new ArrayList<>();
		for (Object o : inputList) {
			targetList.add(map(o, class1));
		}
		return targetList;
	}
 
}

At last, here is ContactService, the service that implements the use case logic (when it is required), delegate to ContactRepository and do the model conversion between the layers.

package davidhxxx.example.angularsboot.service.impl;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import davidhxxx.example.angularsboot.db.model.Contact;
import davidhxxx.example.angularsboot.db.repository.ContactRepository;
import davidhxxx.example.angularsboot.service.dto.ContactDTO;
import davidhxxx.example.angularsboot.service.util.ModelMapperService;
 
@Service
public class ContactService {
 
	private ContactRepository contactRepository;
	private ModelMapperService modelMapperService;
 
	@Autowired
	public ContactService(ContactRepository contactRepository, ModelMapperService modelMapperService) {
		this.contactRepository = contactRepository;
		this.modelMapperService = modelMapperService;
	}
 
	public List<ContactDTO> findAllContacts() {
		List<Contact> contacts = contactRepository.findAll();
		List<ContactDTO> contactsDTO = modelMapperService.mapList(contacts, ContactDTO.class);
		return contactsDTO;
	}
 
	public Long insertContact(ContactDTO contactDTO) {
		Contact contact = modelMapperService.map(contactDTO, Contact.class);
		contactRepository.save(contact);
		return contact.getId();
	}
 
	public void updateContact(ContactDTO contactDTO) {
		Contact contact = modelMapperService.map(contactDTO, Contact.class);
		contactRepository.save(contact);
	}
 
	public void deleteContact(Long contactId) {
		contactRepository.delete(contactId);
	}
 
}

Setting up the web application module part

In the previous parts relying on a mocked backend, we used a minimalist version of the webapp module.
Here we have a much more complete version of the webapp module :

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
 
	<artifactId>contact-webapp-task9</artifactId>
	<name>${project.artifactId}</name>
	<packaging>jar</packaging>
 
	<parent>
		<groupId>davidhxxx.example.rest.springbootangular</groupId>
		<artifactId>contact-parent-task9</artifactId>
		<version>1.0-SNAPSHOT</version>
	</parent>
 
	<dependencies>
 
		<!-- my dependencies -->
		<dependency>
			<groupId>davidhxxx.example.rest.springbootangular</groupId>
			<artifactId>contact-service-task9</artifactId>
			<version>1.0-SNAPSHOT</version>
			<exclusions>
				<exclusion>
					<groupId>davidhxxx.example.rest.springbootangular</groupId>
					<artifactId>contact-db-task9</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
 
		<dependency>
			<groupId>davidhxxx.example.rest.springbootangular</groupId>
			<artifactId>contact-db-task9</artifactId>
			<version>1.0-SNAPSHOT</version>
			<scope>runtime</scope>
		</dependency>
 
		<!-- Spring dependencies -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-access</artifactId>
		</dependency>
		<dependency>
			<groupId>org.codehaus.janino</groupId>
			<artifactId>janino</artifactId>
		</dependency>
	</dependencies>
 
	<properties>
		<mockServices></mockServices>
	</properties>
 
	<build>
		<resources>
			<resource>
				<directory>src/main/resources</directory>
				<filtering>true</filtering>
				<excludes>
					<exclude>static/mockServices.js</exclude>
				</excludes>
			</resource>
		</resources>
		<plugins>
			<!-- ignore web.xml missing with maven-war-plugin version >=3 that specifies 
				: failOnMissingWebXml = false -->
			<plugin>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.0.0</version>
			</plugin>
			<!-- Required if we want to run without profile -->
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<version>${springboot.version}</version>
			</plugin>
		</plugins>
	</build>
 
	<profiles>
		<profile>
			<id>dev</id>
			<properties>
				<mockServices>&lt;script src="mockServices.js"&gt;&lt;/script&gt;</mockServices>
			</properties>
			<dependencies>
				<dependency>
					<groupId>org.springframework.boot</groupId>
					<artifactId>spring-boot-devtools</artifactId>
				</dependency>
			</dependencies>
			<build>
				<resources>
					<resource>
						<directory>src/main/resources</directory>
						<filtering>true</filtering>
					</resource>
				</resources>
				<defaultGoal>clean spring-boot:run</defaultGoal>
				<plugins>
					<plugin>
						<groupId>org.springframework.boot</groupId>
						<artifactId>spring-boot-maven-plugin</artifactId>
						<version>${springboot.version}</version>
						<configuration>
							<profiles>
								<profile>dev</profile>
							</profiles>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
		<profile>
			<id>create-db</id>
			<build>
				<defaultGoal>spring-boot:run</defaultGoal>
				<plugins>
					<plugin>
						<groupId>org.springframework.boot</groupId>
						<artifactId>spring-boot-maven-plugin</artifactId>
						<version>${springboot.version}</version>
						<configuration>
							<profiles>
								<profile>dev-drop-create-db</profile>
							</profiles>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>
</project>

Some points deserve some explanations.

Mocking the backend

We have introduced multiple things in the pom.xml to provide a mocked backend feature on demand (by enabling the dev Maven profile). All these were explained in a previous part. Please, follow this link to understand how it works.

Third party libraries added 

– org.springframework.boot:spring-boot-starter-web : Spring provides multiples starters which are  sets of convenient dependency descriptors that we can include in your application. The starter-web is useful for building web, including RESTful, applications using Spring MVC. Besides, it provides a  Tomcat as the default embedded container. For more information, you can read the official documentation : Spring Boot Starter.

– org.springframework.boot:spring-boot-actuator : The Spring Boot actuator includes a number of features to help you monitor and manage your application when it’s pushed to production. You can choose to manage and monitor your application using HTTP endpoints or with JMX. Auditing, health and metrics gathering can be automatically applied to your application. Actuator HTTP endpoints are only available with a Spring MVC-based application. For more information, you can read the official documentation : Spring Boot Actuator Beyond the production environment, the Spring Boot actuator is very helpful. In development for example it may used to diagnosis some problems or errors. So, we add it.

– ch.qos.logback:logback-access : To log http requests, we need to configure the embedded Tomcat server to use the Logback implementation of tomcat’s Valve interface. It requires that the ch.qos.logback.access.tomcat.LogbackValve class be included at compile and runtime.
Beware, the ch.qos.logback:logback-access dependency and the ch.qos.logback:logback-classic dependency are two distinct dependencies. The logback-classic module provides classic logging features. You can assimilate it to a significantly improved version of log4j. The logback-access module provides very specific logging features. It integrates with Servlet containers such as Tomcat and Jetty to provide HTTP-access log functionality.
The Spring Boot starter-web includes logback-classic in its dependencies but doesn’t include the logback-access dependency. That’s why, we have to declare it in our pom.xml to use it but we don’t need to declare the logback-classic dependency as it is already included.

You can check it by displaying the dependency tree of the org.springframework.boot:spring-boot-starter-web:jar dependency :

[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:1.4.0.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:1.4.0.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:1.4.0.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.1.7:compile
[INFO] | | | | +- ch.qos.logback:logback-core:jar:1.1.7:compile
[INFO] | | | | \- org.slf4j:slf4j-api:jar:1.7.21:compile
[INFO] | | | +- org.slf4j:jcl-over-slf4j:jar:1.7.21:compile
[INFO] | | | +- org.slf4j:jul-to-slf4j:jar:1.7.21:compile
[INFO] | | | \- org.slf4j:log4j-over-slf4j:jar:1.7.21:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.17:runtime

– org.codehaus.janino:janino :  Conditional processing in Logback configuration files requires the Janino library. So, we add it. We will explain in the Logback configuration part why wee will use it.


The create-db Maven profile 

When we are in the development environment and we are working with an in-memory database we want to have a fast way to recreate the database structure according to the modifications done on JPA entities. Hibernate, the JPA implementation we are using provides features to create the database from the discovered entities during the Hibernate initialization but we don’t want that this task be performed at each time that the Spring Boot application is started and we want also that this be executed only in a local environment.
To address the requirement, we add the create-db Maven profile that starts the Spring Boot application with the create-db Spring Boot profile that enables the drop-create of the database from the discovered entities.

The dev Maven profile

In the previous part, we have explained it. Please refer to the mocked back end part to have more information about it.

Isolating the logical layers 

In the chosen design, we don’t want the web application layer manipulates the entities from the database access layer. Keeping this principle in mind is good but ensuring that the web application classes cannot compile if they use entities classes is better.

With Maven ,when you declare a dependency in a module, the transitive dependencies of it are also pulled. The service module depends on the db module and the web module depends on the service module. So the web module depends also on the db module.

We don’t want it at compile time. So we exclude the db module dependency from the service module dependency declaration but as we need the db module dependency (and its transitive dependencies) at runtime (and also during integration tests that we will write at the end of the part), we declare also this dependency by specifying the runtime scope :

<!-- my dependencies -->
<dependency>
	<groupId>davidhxxx.example.rest.springbootangular</groupId>
	<artifactId>contact-service-task9</artifactId>
	<version>1.0-SNAPSHOT</version>
	<exclusions>
		<exclusion>
			<groupId>davidhxxx.example.rest.springbootangular</groupId>
			<artifactId>contact-db-task9</artifactId>
		</exclusion>
	</exclusions>
</dependency>
 
<dependency>
	<groupId>davidhxxx.example.rest.springbootangular</groupId>
	<artifactId>contact-db-task9</artifactId>
	<version>1.0-SNAPSHOT</version>
	<scope>runtime</scope>
</dependency>

Here is the Spring configuration used for this module :

package davidhxxx.example.angularsboot.webapp;
 
import java.io.IOException;
 
import javax.annotation.PostConstruct;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
 
import ch.qos.logback.access.tomcat.LogbackValve;
import davidhxxx.example.angularsboot.service.AppServiceConfig;
 
@Import(AppServiceConfig.class)
@SpringBootApplication
public class AppConfig extends SpringBootServletInitializer {
 
	private final Logger log = LoggerFactory.getLogger(AppConfig.class);
 
	@Value("${my.application.properties.file}")
	String currentApplicationPropertiesFileName;
 
	public static void main(String[] args) throws IOException {
		SpringApplication.run(AppConfig.class, args);
	}
 
	@PostConstruct
	private void postConstruct() {
		log.info("Current application properties file is " + currentApplicationPropertiesFileName);
	}
 
	@Bean
	public EmbeddedServletContainerCustomizer containerCustomizer(
			final @Value("${my.logbackaccess.path:}") String fileName) {
		return new EmbeddedServletContainerCustomizer() {
			public void customize(ConfigurableEmbeddedServletContainer container) {
				if ((container instanceof TomcatEmbeddedServletContainerFactory)) {
					((TomcatEmbeddedServletContainerFactory) container)
							.addContextCustomizers(new TomcatContextCustomizer[] { new TomcatContextCustomizer() {
 
								@Override
								public void customize(org.apache.catalina.Context context) {
									LogbackValve logbackValve = new LogbackValve();
									logbackValve.setFilename(fileName);
									context.getPipeline().addValve(logbackValve);
								}
							} });
				}
			}
		};
	}
 
}

In this configuration we do mainly two things : we configure the bootstrap of the application in a Tomcat embedded server.
This is specified by extending SpringBootServletInitializer  and with the code in the main() method.
The @SpringBootApplication annotation is not mandatory to make a class, the bootstrap class of the Spring Boot Application. This is « only » a shorthand annotation that is equivalent to declaring @Configuration, @EnableAutoConfiguration and @ComponentScan.

You can also see that we enable the Logback Valve for logback-access logging.  It is a little cumbersome but for now we have not really a better way to do it with Spring Boot.

We finish this part with the Rest Controller that exposes Rest services for Contact resources.

package davidhxxx.example.angularsboot.webapp.controller;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
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 davidhxxx.example.angularsboot.service.dto.ContactDTO;
import davidhxxx.example.angularsboot.service.impl.ContactService;
 
@RestController
@RequestMapping("/api/contacts")
public class ContactController {
 
 
	private ContactService contactService;
 
	@Autowired
	public ContactController(ContactService contactService){
		this.contactService = contactService;
	}
 
	/**
	 * GET /contacts -> get all
	 */
	@RequestMapping(method = RequestMethod.GET)
	public ResponseEntity<?> getAll(HttpServletRequest request, HttpServletResponse response) {
 
		List<ContactDTO> contactsDTOs = contactService.findAllContacts();
		return new ResponseEntity<List<ContactDTO>>(contactsDTOs, HttpStatus.OK);
	}
 
	/**
	 * POST /contacts -> create one
	 */
	@RequestMapping(method = RequestMethod.POST)
	public ResponseEntity<?> create(@RequestBody ContactDTO contactDTO, HttpServletRequest request,
			HttpServletResponse response) throws Exception {
 
		Long contactId = contactService.insertContact(contactDTO);
		final ResponseEntity<Void> responseEntity = ResponseEntity
				.created(new URI("/api/contacts/" + contactId.toString())).build();
		return responseEntity;
	}
 
	/**
	 * PUT /contacts/{id} -> update
	 */
	@RequestMapping(value = "/{id}", method = RequestMethod.PUT)
	public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ContactDTO contactDTO,
			HttpServletRequest request, HttpServletResponse response) throws URISyntaxException {
 
		if (id == null || contactDTO.getId() == null || !id.equals(contactDTO.getId())) {
			return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
		}
		contactService.updateContact(contactDTO);
		return new ResponseEntity<>(HttpStatus.OK);
	}
 
	/**
	 * DELETE /contacts/{id} -> delete one
	 */
	@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
	public ResponseEntity<?> delete(@PathVariable Long id, HttpServletRequest request, HttpServletResponse response) {
		contactService.deleteContact(id);
		return new ResponseEntity<>(HttpStatus.OK);
	}
 
}

Creating Spring Boot profiles to have for a same property, different values according to the running environment

We define 4 Spring Boot profiles :

application.properties to set the default values for any profile
application-dev.properties to start the application in a dev environment 
application-create-db.properties to drop and re-create the database 
–  application-test.properties to set the configuration for testing

We could of course define other profiles. 

application.properties 

As previously said, it defines default properties.

my.application.properties.file=application.properties
my.logbackaccess.path=logback-access.xml
 
# built in
server.port=8095
server.contextPath=/contact-webapp
logging.level.root=INFO
spring.jpa.hibernate.ddl-auto=none

With Spring Boot, we can use predefined properties used by Spring Boot but we can also create our own properties if we deem that some values should be different according to the environment.
To distinguish and avoid collisions between our own properties and properties provided by Spring Boot, we will prefix ours with my

my.application.properties.file is a debugging-purpose property. It has as value the name of the current properties file. When the application starts, we log this information. 

my.logbackaccess.path indicates the path of the configuration file for the logback-access module. We use it to configure the Tomcat valve for Logback.

server.port specifies the port used by the embedded server. You have not necessarily to fill it. By default, server.port has as value 8080. Personally I specify my own value because I have sometimes multiple Spring Boot application deployed and they cannot use all the same port. 

server.contextPath  specifies the context path of the web application. By default, it is empty. So, the application is reachable from this url : http//localhost:8095. 
By habit, I find more meaningful to have a explicit context path like that : http//localhost:8095/contact-webapp
Nothing prevents us to remove the context path in a Spring Boot production profile if we want that our application to be reachable from the root of the web server.

logging.level.root specifies  the root level of the logging. By default, we want to give a  average level of verbosity : INFO. But for some environments (in production for example) and for debugging purposes, we know that the INFO root level may be too verbose or not enough. We set a acceptable default value and we leave each other environment overrides it like it needs.

spring.jpa.hibernate.ddl-auto specifies  the hibernate behavior on DDL when Hibernate is initialized (these are valid values : none, validate, update, create, create-drop). We set it to none as Spring uses as default the create-drop value if the database used is embedded.  In our case, we don’t want indeed that the database be dropped and recreated at each startup of the application.

Go on with the properties used in the development environment.

application-dev.properties 

my.application.properties.file=application-dev.properties
 
# built in
logging.file=D:/logs/contact-server.log
spring.datasource.url=jdbc:h2:file:~/db/contact-hsql
spring.datasource.username=sa
spring.datasource.password=sa
spring.jpa.hibernate.ddl-auto=valid
spring.jpa.show-sql=true

logging.file indicates the file (exact location or relative to the current directory) where the log file must be created/located. Beware there is something a little misleading with logging path/file with Spring Boot. logging.file and logging.path are two  predefined properties to configure log location with Spring Boot but you cannot mix both. Either, you use one, either you use the other one.
You should specify a 
value for logging.path only if you want to use the default value for  logging.file (which is spring.log) but you want to define in which directory spring.log is located.
You should specify a value for logging.file if you want to define only the filename or both the filename and its location.

Next it the the properties file used to recreate the database.

application-create-db.properties 

my.application.properties.file=application-create-db.properties
 
# built in
logging.file=D:/logs/contact-server.log
spring.datasource.url = jdbc:h2:file:~/db/contact-hsql
spring.datasource.username = sa
spring.datasource.password = sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto:create

This uses almost the same values than the application-dev.properties. The main difference is spring.jpa.hibernate.ddl-auto:none that is replaced by spring.jpa.hibernate.ddl-auto:create.
You can also note that we use the same database (the file path specified in url).

At last, below is the properties file used for integration testing.

Test must be isolated. Having a specific and a fresh database when the tests starts is so a requirement.  
We do two important things :
– we override : spring.jpa.hibernate.ddl-auto to enable the database drop/creation
– we specify for the spring.datasource.url property an in-memory database storage (that is so distinct from the file database storage used for the no testing environments)

application-test.properties 

my.application.properties.file=application-test.properties
 
# built in
logging.file=D:/logs/contact-server-test.log
spring.datasource.url=jdbc:h2:~/test;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create

Configuring suitably Logback configurations files : logback-classic and logback-access

With Spring Boot, no Spring Boot properties have to be necessarily filled explicitly by the application since Spring Boot defines default values for almost any things that have to be configured.
If for some concerns Spring Boot default values are very often suitable, for other concerns, it is less the case.  And in a some way, it is normal. Spring Boot cannot guess everything.
I have realized that for the first time when I tried to configure Logback by using only Spring Boot properties. 

For setting up a very simple logging configuration, no problem but when you must  configure logging in a finer way, you have to provide your own logging configuration file.
For example, the rolling policy is not configurable from the Spring Boot properties. Personally, I think that mixing properties in Spring Boot property files and in specific configuration files may decrease the readability if it is not done according to some rules.
I use this very simple rule : when for a concern (here is the logging), I mix the configuration from the Spring Boot properties with a specific configuration file, I use Spring Boot properties to define properties environment-dependent and I use specific configuration files to define properties independent of the environment.
In this way, I know where searching properties when I have to read them (with my eyes) or modify them.
For example, when I search a Logback configuration information which in my application is not dependent of the environment (for example : the rollingPolicy or the log pattern), I look into logback-spring.xml and logback-access.xml. But when I search a Logback configuration information which in my application is dependent of the environment (for example : the logging level root or the logging file), I look at the Spring Boot profile properties .

Another specificity should be explained.
Traditionally, the configuration file for the logback-classic module is  logback.xml. At runtime, this is one of the files that logback  searches in the classpath during its initialization phase.

So, why do we name our configuration file logback-spring.xml ?

Because the standard Logback initialization is performed before that the Spring Boot application is started.
If we use the standard Logback initialization, the ${LOG_FILE} property is not available when Logback starts. For information, ${LOG_FILE} is a Spring Boot property used if logging.file was set in Boot’s external configuration. Which is the case in the application-dev.properties file we have defined.

Concretely, instead of creating or using the value bound to ${LOG_FILE} property, Logback creates/uses the value « LOG_FILE_IS_UNDEFINED » as log file.
Then, when Spring boot starts, it initializes Logback again but this time with expected bound properties but the original log file is not removed.
We  finish with two files  : the LOG_FILE_IS_UNDEFINED log file with no or almost no logging information  and the log file with the expected name and in the expected path.
It is not a very clean approach. That’s why Spring Boot promotes the use of  logback-spring.xml over logback.xmllogback-spring.xml is not recognized by Logback, so Logback does nothing and Spring waits that properties be bound to start the Logback initialization.

Here are the Logback configuration files.

logback-spring.xml

<configuration scan="true" scanPeriod="120 seconds">
 
	<!-- <include resource="org/springframework/boot/logging/logback/base.xml"/>  include all loger--> 
 
	<include resource="org/springframework/boot/logging/logback/defaults.xml" /> <!-- include some default valuse--> -->
 
	<!-- include only the console appender -->
	<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
 
	<jmxConfigurator />
 
	<appender name="FILE"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${LOG_FILE}</file>
		<append>true</append>
		<encoder>
			<pattern>%date{dd MMM yyyy;HH:mm:ss.SSS}[%thread] %-5level
				%logger{50} - %msg%n
			</pattern>
		</encoder>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
			<maxHistory>30</maxHistory>
		</rollingPolicy>
	</appender>
 
 
	<root>
		<appender-ref ref="CONSOLE" />
		<appender-ref ref="FILE" />
	</root>
 
</configuration>

 logback-access.xml :

<configuration>
	<statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener" />
 
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
			<evaluator name="exclude-resources">
				<expression>
					return
					event.getRequestURI().contains(".css")
					||
					event.getRequestURI().contains(".html")
					||
					event.getRequestURI().contains(".js");
				</expression>
			</evaluator>
			<OnMatch>DENY</OnMatch>
			<onMismatch>NEUTRAL</onMismatch>
		</filter>
		<encoder>
			<pattern>HTTP Request:%n%fullRequest%n HTTP
				Response:%n%fullResponse%n Processing time: %elapsedTime%n</pattern>
		</encoder>
	</appender>
 
	<appender name="FILE"
		class="ch.qos.logback.core.rolling.RollingFileAppender">
		<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
			<evaluator name="exclude-resources">
				<expression>
					return
					event.getRequestURI().contains(".css")
					||
					event.getRequestURI().contains(".html")
					||
					event.getRequestURI().contains(".js");
				</expression>
			</evaluator>
			<OnMatch>DENY</OnMatch>
			<onMismatch>NEUTRAL</onMismatch>
		</filter>
		<file>${LOG_FILE}</file>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
			<maxHistory>10</maxHistory>
		</rollingPolicy>
		<append>true</append>
		<encoder>
			<pattern>HTTP Request:%n%fullRequest%n HTTP
				Response:%n%fullResponse%n Processing time: %elapsedTime%n</pattern>
		</encoder>
	</appender>
 
	<appender-ref ref="STDOUT" />
	<appender-ref ref="FILE" />
</configuration>

You can notice that we use the ch.qos.logback.core.filter.EvaluatorFilter class to not log HTTP request that contains  .css, .html or .js in their URI. It prevents log pollution by excluding resources that we don’t need to trace.
This is the reason we have to declare the janino dependency.

Writing a integration test to validate our implementation

In the web module, we write integration tests to validate that :
– we can  create a resource contact with the rest API 
– we can  find all resource contacts with the rest API  
– we can delete a resource contact with the rest API 
– we can update a resource contact with the rest API 

package davidhxxx.example.angularsboot.webapp.controller;
 
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
 
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import davidhxxx.example.angularsboot.db.repository.ContactRepository;
import davidhxxx.example.angularsboot.service.dto.ContactDTO;
 
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles({ "test" })
public class ContactControllerIntegrationTest {
 
	@Autowired
	private TestRestTemplate template;
 
	@Value("${server.contextPath}")
	private String contextPath;
 
	@Autowired
	private ContactRepository contactRepository;
 
	@Before
	public void setup() {
		contactRepository.deleteAll();
	}
 
	@Test
	public void getEntitiesReturnAllExistingEntities() throws Exception {
 
		ContactDTO contact = new ContactDTO();
		contact.setFirstName("Martin");
		contact.setLastName("Sugar");
		LocalDate date = LocalDate.of(2017, 1, 1);
		contact.setBirthday(date);
		// action
		ResponseEntity<ContactDTO> responseEntity = template.postForEntity(getBaseUrl(), contact, ContactDTO.class);
 
		// assertion
		Assert.assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
		Long idRetrieved = retrieveIdFrom(responseEntity);
		ResponseEntity<ContactDTO[]> responseEntityGetEntities = template.getForEntity(getBaseUrl(),
				ContactDTO[].class);
		List<ContactDTO> contacts = Arrays.asList(responseEntityGetEntities.getBody());
		Assert.assertEquals(1, contacts.size());
		ContactDTO actualContact = contacts.get(0);
		Assert.assertEquals(idRetrieved, actualContact.getId());
		Assert.assertEquals(contact.getFirstName(), actualContact.getFirstName());
		Assert.assertEquals(contact.getLastName(), actualContact.getLastName());
		Assert.assertEquals(contact.getBirthday(), actualContact.getBirthday());
	}
 
	@Test
	public void updateEntity() throws Exception {
 
		ContactDTO contact = new ContactDTO();
		contact.setFirstName("Martin");
		contact.setLastName("Sugar");
		LocalDate date = LocalDate.of(2017, 1, 1);
		contact.setBirthday(date);
		ResponseEntity<ContactDTO> responseEntity = template.postForEntity(getBaseUrl(), contact, ContactDTO.class);
		Assert.assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
 
		// action
		Long idLastCreatedContact = retrieveIdFrom(responseEntity);
		contact = new ContactDTO();
		contact.setId(idLastCreatedContact);
		contact.setFirstName("Martino");
		contact.setLastName("Sugars");
		template.put(getBaseUrl() + "/" + idLastCreatedContact, contact);
 
		// assertion
		ResponseEntity<ContactDTO[]> responseEntityGetEntities = template.getForEntity(getBaseUrl(),
				ContactDTO[].class);
		List<ContactDTO> contacts = Arrays.asList(responseEntityGetEntities.getBody());
		Assert.assertEquals(1, contacts.size());
		ContactDTO actualContact = contacts.get(0);
		Assert.assertEquals(idLastCreatedContact, actualContact.getId());
		Assert.assertEquals(contact.getFirstName(), actualContact.getFirstName());
		Assert.assertEquals(contact.getLastName(), actualContact.getLastName());
 
	}
 
	@Test
	public void deleteEntity() throws Exception {
 
		// contact creation One that will remain
		ContactDTO firstContact = new ContactDTO();
		firstContact.setFirstName("john");
		firstContact.setLastName("doe");
		ResponseEntity<ContactDTO> responseEntity = template.postForEntity(getBaseUrl(), firstContact,
				ContactDTO.class);
		Assert.assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
		Long idContactThatWillRemain = retrieveIdFrom(responseEntity);
 
		// contact creation Two that we will delete
		ContactDTO secondContact = new ContactDTO();
		secondContact.setFirstName("jane");
		secondContact.setLastName("calimity");
		responseEntity = template.postForEntity(getBaseUrl(), secondContact, ContactDTO.class);
		Assert.assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
 
		Long idLastCreatedContact = retrieveIdFrom(responseEntity);
		template.delete(getBaseUrl() + "/" + idLastCreatedContact);
 
		// assertion
		ResponseEntity<ContactDTO[]> responseEntityGetEntities = template.getForEntity(getBaseUrl(),
				ContactDTO[].class);
		List<ContactDTO> contacts = Arrays.asList(responseEntityGetEntities.getBody());
		Assert.assertEquals(1, contacts.size());
		ContactDTO actualContact = contacts.get(0);
		Assert.assertEquals(idContactThatWillRemain, actualContact.getId());
		Assert.assertEquals(firstContact.getFirstName(), actualContact.getFirstName());
		Assert.assertEquals(firstContact.getLastName(), actualContact.getLastName());
	}
 
	private long retrieveIdFrom(ResponseEntity<ContactDTO> responseEntity) {
		String path = responseEntity.getHeaders().getLocation().getPath();
		int indexOf = path.lastIndexOf("/");
		long idLastCreatedContact = Long.valueOf(path.substring(indexOf + 1));
		return idLastCreatedContact;
	}
 
	private String getBaseUrl() {
		return contextPath + "/api/contacts";
	}
 
}

Some explanations about this code :

These tests are integration tests.

Nothing is mocked. We test the code from the REST controller to the access to the database.
The single difference with the no testing environment is here we use a H2 in memory database as we don’t need to use a persistence database storage for unit tests (contrary to the application that works with a H2 file database). 

The test class is annotated with three important annotations

@RunWith(SpringJUnit4ClassRunner.class) is the classic Spring JUnit4 Runner.

@ActiveProfiles(« test »)  enables the test Spring Boot profile during the test execution (that configures among other things the in memory H2 storage).

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT).
This annotation « catch all » provides multiple things :
– use SpringBootContextLoader as the default ContextLoader when no specific @ContextConfiguration(loader=…) is defined.
– automatically searches for a @SpringBootConfiguration when nested @Configuration is not used, and no explicit classes are specified.
– allow custom Environment properties to be defined using the properties attribute.
– provide support for different webEnvironment modes, including the ability to start a fully running container listening on a defined or random port.
– registers a TestRestTemplate bean for use in web tests.

In our case, this annotation will allow us to have :
– The Spring Boot application started before the tests execution
– starting it on a random port (we don’t want that test may use a already used port in the application and we don’t care about the port used for test)
– having a TestRestTemplate bean that we will use to request your RestController.

We don’t want side effects between tests

We delete all contacts from the DB before each executed method.

@Before
public void setup() {
	contactRepository.deleteAll();
}

We inject beans and properties required for the test execution and the test data cleaning :

@Autowired
private TestRestTemplate template;
 
@Value("${server.contextPath}")
private String contextPath;
 
@Autowired
private ContactRepository contactRepository;

Running the application

The first time : from the contact-webapp module, execute mvn -Pcreate-db to create the persistent database.
Then, from the parent module, execute the mvn clean install to build all modules and check that tests pass.

Now, the application can be started.
From the contact-webapp module, execute mvn -PDev. It is a shortcut provided by the Maven dev profile that we have added.
It avoids entering the verbose command :
 mvn clean spring-boot:run -Pdev -Dspring.profiles.active=dev

Downloading the source code

It contains the final code of the application (both front-end and back-end part) [sdm_download id= »2656″ fancy= »1″]

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *