Skip to main content

Springboot guide

Install springboot

  1. method 1: brew install springboot
  2. method 2: curl -s "https://get.sdkman.io" | bash
    • source "$HOME/.sdkman/bin/sdkman-init.sh"
    • Install springboot
      • sdk install springboot

Create new project

  • get list of avaliable dependencies
    • spring init --list
  • create a new web dependency project using maven
    • spring init --dependencies web --build maven --groupId groupName --artifactId projectName --name projectName saveAsThisFolderName
    • spring init -d web --build maven -g groupName -a projectName -n projectName saveAsThisFolderName
    • spring init -d web,data-jdbc,postgresql --build maven -g groupName -a projectName -n projectName saveAsThisFolderName

Inside the project

pom.xml

  • similar to the package.json file for JavaScript
  • contains the parent library which is spring boot and the list of dependencies installed

src/main/resources/application.properties

  • file to configure all the properties for the app and for environment specific properties
  • it will be used when connected to a real database

src/main/resources/static/ && src/main/resources/templates/

  • these are for web development related files such as html, css, javascript

default code in the src/main/java/.../appname/AppName.java

package com.example.demoapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

create a simple class model that creates tables in the database

e.g.: src/java/.../appname/classname/ModelName.java

  • this will enable database to auto create a table and sequence related to this file
package com.example.demoapi.student;

import java.time.LocalDate;
import java.time.Period;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient

@Entity // required for hibernate
@Table // required for table in the database
public class Student {
@Id
@SequenceGenerator(name = "student_sequence", sequenceName = "student_sequence", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "student_sequence")
private Long id;
private String name;
private String email;
private LocalDate dob;
@Transient // helps to auto calculate age
private Integer age;

public Student() {
}

public Student(Long id, String name, String email, LocalDate dob) {
this.id = id;
this.name = name;
this.email = email;
this.dob = dob;
}

// ids are auto generated by database
public Student(String name, String email, LocalDate dob) {
this.name = name;
this.email = email;
this.dob = dob;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public LocalDate getDob() {
return dob;
}

public void setDob(LocalDate dob) {
this.dob = dob;
}

public Integer getAge() {
return Period.between(dob, LocalDate.now()).getYears();
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" + "id=" + id + ", name='" + name + '\'' + ", email='" + email + '\'' + ", dob=" + dob + ", age="
+ age + '}';
}
}

e.g.: src/main/java/.../appname/classname/ClassNameService.java

  • gain data access by querying in the database
package com.example.demoapi.student;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javax.transaction.Transactional;

import com.example.demoapi.student.exception.BadRequestException;
import com.example.demoapi.student.exception.StudentNotFoundException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

// @Component also works, but better to be specific as this is a Service
@Service // this states that StudentService needs to be instantiated (must be a spring bean) linking it to @Autowired in the controller
public class StudentService {

private final StudentRepository studentRepository;

@Autowired // this states that StudentRepository should be autowired by being instantiated and then injected into this constructor
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}

// uses the built-in methods to query the database to return the JSON data
public List<Student> getStudents() {
return studentRepository.findAll();
}

public void addNewStudent(Student student) {
// Optional<Student> studentOptional = studentRepository.findStudentByEmail((student.getEmail()));
// if (studentOptional.isPresent()) {
Boolean existsEmail = studentRepository.selectExistsEmail(student.getEmail());
if (existsEmail) {
// method 1, using default
// throw new IllegalStateException("email taken");

// method 2, using custom exception handler
throw new BadRequestException("Email " + student.getEmail() + " taken");
}
studentRepository.save(student);
}

public void deleteStudent(Long studentId) {
boolean exists = studentRepository.existsById(studentId);
if (!exists) {
// method 1, using default
// throw new IllegalStateException("student with id " + studentId + " does not
// exists");

// method 2, using custom exception handler
throw new StudentNotFoundException("Student with id " + studentId + " does not exists");
}
studentRepository.deleteById(studentId);
}

// used for put request, allows us to not have to implement JPQL query
// thus can use the setters from the entity to check if update is possible
// and to use setters to auto update the entity in the database
@Transactional
public void updateStudent(Long studentId, String name, String email) {
Student student = studentRepository.findById(studentId)
.orElseThrow(() -> new IllegalStateException("student with id " + studentId + " does not exists"));

// Optional<Student> studentOptional = studentRepository.findStudentByEmail(email);
// if (studentOptional.isPresent()) {
Boolean existsEmail = studentRepository.selectExistsEmail(email));
if (existsEmail) {
throw new IllegalStateException("email taken");
}

if (name != null && name.length() > 0 && !Objects.equals(student.getName(), name)) {
student.setName(name);
}
if (email != null && email.length() > 0 && !Objects.equals(student.getEmail(), email)) {
student.setEmail(email);
}
}
}

create tests e.g.: src/test/java/.../appname/classname/ClassNameServiceTest.java

package com.example.demoapi.student;

import com.example.demoapi.student.exception.BadRequestException;
import com.example.demoapi.student.exception.StudentNotFoundException;

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; // use to parse in any string instead of the actual string value
import static org.mockito.BDDMockito.given;

import java.time.LocalDate;
import java.time.Month;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; // allow skipping of test
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; // method 2: for mocking

@ExtendWith(MockitoExtension.class) // method 2: for mocking
public class StudentServiceTest {

// mock should be used as we have already tested respository
@Mock
private StudentRepository studentRepository;
private StudentService underTest;
// private AutoCloseable autoCloseable; // method 1: for mocking

@BeforeEach
void setUp() {
// autoCloseable = MockitoAnnotations.openMocks(this); // method 1: for mocking
underTest = new StudentService(studentRepository);
}

// method 1: for mocking
// @AfterEach
// void tearDown() throws Exception {
// autoCloseable.close();
// }

@Test
void canGetStudents() {
// when
underTest.getStudents();
// then
verify(studentRepository).findAll();
}

@Test
void canAddNewStudent() {
// given
Student student = new Student("Name1", "name1@gmail.com", LocalDate.of(2000, Month.JANUARY, 5));
// when
underTest.addNewStudent(student);
// then
ArgumentCaptor<Student> studentArgumentCaptor = ArgumentCaptor.forClass(Student.class);
verify(studentRepository).save(studentArgumentCaptor.capture());
Student captureStudent = studentArgumentCaptor.getValue();
assertThat(captureStudent).isEqualTo(student);
}

@Test
void willThrowWhenEmailIsTaken() {
// given
Student student = new Student("Name1", "name1@gmail.com", LocalDate.of(2000, Month.JANUARY, 5));

// method 1: get actual email
// given(studentRepository.selectExistsEmail(student.getEmail())).willReturn(true);
// method 2: parse any string as email
given(studentRepository.selectExistsEmail(anyString())).willReturn(true);
// when
// then
assertThatThrownBy(() -> underTest.addNewStudent(student)).isInstanceOf(BadRequestException.class)
.hasMessageContaining("Email " + student.getEmail() + " taken");

// our mocked studendRepository never saves anything
verify(studentRepository, never()).save(any());
}

@Test
void canDeleteStudent() {
// given
long studentId = 10;
given(studentRepository.existsById(studentId)).willReturn(true);
// when
underTest.deleteStudent(studentId);

// then
verify(studentRepository).deleteById(studentId);
}

@Test
void willThrowWhenDeleteStudentNotFound() {
// given
long studentId = 10;
given(studentRepository.existsById(studentId)).willReturn(false);
// when
// then
assertThatThrownBy(() -> underTest.deleteStudent(studentId)).isInstanceOf(StudentNotFoundException.class)
.hasMessageContaining("Student with id " + studentId + " does not exists");

verify(studentRepository, never()).deleteById(any());
}

@Test
void canUpdateStudent() {
// given
Student student = new Student("Name1", "name1@gmail.com", LocalDate.of(2000, Month.JANUARY, 5));
String name = "name2";
String email = "email2@gmail.com";
// when
// then
student.setName(name);
student.setEmail(email);
assertThat(student.getName()).isEqualTo(name);
assertThat(student.getEmail()).isEqualTo(email);
}

@Test
void willThrowWhenStudentNotFound() {
// given
long studentId = 10;
String name = "name2";
String email = "email2@gmail.com";
// when
// then
assertThatThrownBy(() -> underTest.updateStudent(studentId, name, email)).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("student with id " + studentId + " does not exists");
}

@Test
void willThrowWhenNewEmailIsTaken() {
// given
Student student = new Student("Name1", "name1@gmail.com", LocalDate.of(2000, Month.JANUARY, 5));
long studentId = 10;
String name = "name2";
String email = "email2@gmail.com";
// when
when(studentRepository.findById(studentId)).thenReturn(Optional.of(student));
given(studentRepository.selectExistsEmail(email)).willReturn(true);
// then
assertThatThrownBy(() -> underTest.updateStudent(studentId, name, email)).isInstanceOf(IllegalStateException.class)
.hasMessageContaining("email taken");
}

}
  • might be required to add in pom.xml to support custom exception imports for tests
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
</dependency>

create and enable restful framework by creating a class controller and linking it with the service

e.g.: src/main/java/.../appname/classname/ClassNameController.java

package com.example.demoapi.student;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "api/v1/student")
public class StudentController {

private final StudentService studentService;

@Autowired // this states that StudentService should be autowired by being instantiated and then injected into this constructor
public StudentController(StudentService studentService) {
this.studentService = studentService; // use dependency injection instead of manually instantiating here
}

// enable get request http://localhost:8080/api/v1/student
@GetMapping
public List<Student> getStudents() {
return studentService.getStudents();
}

// enable post request http://localhost:8080/api/v1/student
@PostMapping
public void registerNewStudent(@RequestBody Student student) { // @RequestBody enable retrieve payload from body
studentService.addNewStudent(student);
}

// enable delete request http://localhost:8080/api/v1/student/delete/1
@DeleteMapping(path = "delete/{studentId}")
public void deleteStudent(@PathVariable("studentId") Long studentId) { // @PathVariable enable retrieve parameter from path
studentService.deleteStudent(studentId);
}

// enable put request http://localhost:8080/api/v1/student/update/1?name=Test&email=test1@gmail.com
@PutMapping(path = "update/{studentId}")
public void updateStudent(@PathVariable("studentId") Long studentId, @RequestParam(required = false) String name,
@RequestParam(required = false) String email) { // @RequestParm enable retrieve of key=value parameter from path
studentService.updateStudent(studentId, name, email);
}
}

setup environment settings to connect to postgresql in src/main/resources/application.properties

spring.jpa.hibernate.ddl-auto values

  • create
    • Hibernate first drops existing tables, then creates new tables
  • create-drop
    • use for test case scenarios
    • create schema, add some mock data, run tests
    • during the test case cleanup, the schema objects are dropped, leaving an empty database
  • validate
    • can use in production
    • but typically that should be a setting to use in quality/test environment to verify that the database scripts written or applied to database migration tool are accurate
    • Another reason not to use validate in production is that it could be a bottleneck during the startup process of the application, particularly if the object model is quite extensive in size or if other network related factors come into play
  • update
    • commonly used in development
    • to automatically modify the schema to add new additions upon restart
    • does not remove a column or constraint that may exist from previous executions that is no longer necessary
  • none
    • highly recommended to use in production
    • this value effectively turns off the DDL generation
    • it's common practice for DBAs to review migration scripts for database changes
      • particularly if your database is shared across multiple services and applications
spring.datasource.url=jdbc:postgresql://localhost:5432/databaseName
spring.datasource.username=
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialet=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true

// allow display of error message in the response during an error
server.error.include-message=always

setup environments for unit tests and use H2 database src/test/resources/application.properties

  • paste h2 database dependency into the pom.xml file
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
  • in the application.properties file
spring.datasource.url=jdbc:h2://mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialet=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.format_sql=true

create an interface that is responsible for data access src/main/java/.../appname/classname/ClassNameRepository.java

package com.example.demoapi.student;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

// method 1: SELECT * FROM student WHERE email = ?
// Optional<Student> findStudentByEmail(String email);

// method 2: be more specific, the following is JPQL query and not SQL
// Student refers to the Student class
// @Query("SELECT s FROM Student s WHERE s.email = ?1")
// Optional<Student> findStudentByEmail(String email);

@Query("" + "SELECT CASE WHEN COUNT(s) > 0 THEN " + "TRUE ELSE FALSE END " + "FROM Student s " + "WHERE s.email = ?1")
Boolean selectExistsEmail(String email);
}

unit test for interface src/test/java/.../appname/classname/ClassNameRepositoryTest.java

  • Not using the test configurations (not recommended)
package com.example.demoapi.student;

import java.time.LocalDate;
import static java.time.Month.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@DataJpaTest // required for test to pass
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // required for test to pass
public class StudentRepositoryTest {

@Autowired
private StudentRepository underTest;

@AfterEach
void tearDown() {
underTest.deleteAll();
}

@Test
void itShouldCheckIfStudentExistsEmail() {
// given
String email = "name1@gmail.com";
Student student = new Student("Name1", email, LocalDate.of(2000, JANUARY, 5));
underTest.save(student);

// when
Boolean expected = underTest.selectExistsEmail(email);

// then
assertThat(expected).isTrue();
}

@Test
void itShouldCheckWhenStudentEmailDoesNotExists() {
// given
String email = "jamila@gmail.com";

// when
Boolean expected = underTest.selectExistsEmail(email);

// then
assertThat(expected).isFalse();
}
}
  • using the test configuration (recommended)
package com.example.demoapi.student;

import java.time.LocalDate;
import static java.time.Month.*;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

// not required
// @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
public class StudentRepositoryTest {

@Autowired
private StudentRepository underTest;

@AfterEach
void tearDown() {
underTest.deleteAll();
}

@Test
void itShouldCheckIfStudentExistsEmail() {
// given
String email = "name1@gmail.com";
Student student = new Student("Name1", email, LocalDate.of(2000, JANUARY, 5));
underTest.save(student);

// when
Boolean expected = underTest.selectExistsEmail(email);

// then
assertThat(expected).isTrue();
}

@Test
void itShouldCheckWhenStudentEmailDoesNotExists() {
// given
String email = "jamila@gmail.com";

// when
Boolean expected = underTest.selectExistsEmail(email);

// then
assertThat(expected).isFalse();
}
}

create a config file that seeds the table contents src/main/java/.../appname/classname/ClassNameConfig.java

package com.example.demoapi.student;

import java.time.LocalDate;
import java.time.Month;
import java.util.List;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class StudentConfig {

@Bean
CommandLineRunner commandLineRunner(StudentRepository repository) {
return args -> {
Student studentA = new Student("Name1", "name1@gmail.com", LocalDate.of(2000, Month.JANUARY, 5));

Student studentB = new Student("Name2", "name2@gmail.com", LocalDate.of(2004, Month.FEBRUARY, 10));

repository.saveAll(List.of(studentA, studentB));
};
}
}

Create custom error exception handlers

src/main/java/.../appname/classname/exception/TypeNameException.java

package com.example.demoapi.student.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String msg) {
super(msg);
}
}

src/main/java/.../appname/classname/exception/TypeNameException.java

package com.example.demoapi.student.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String msg) {
super(msg);
}
}