Springboot guide
Install springboot
- method 1:
brew install springboot
- 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 + '}';
}
}
create a service to link the database to the controller
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);
}
}