Employee CRUD Operations
Created on: Jan 19, 2025
We will learn basic CRUD operations along with crucial concepts in Spring Boot. This blog provides practical examples that you can run and test. Some of the technologies used are:
- Java
- Spring boot
- Mysql
- Docker (utility to run mysql locally )
- postman
- swagger/open docs
- Rest api
We will be learning below concept using crud operation for employee.
- Central error handling
- Input validation
- Pagination
- Logging
- Documentation
- Actuator
- open documentation
- create docker of application (pending)
- create kubernates (pending)
- Unit/junit testing
- Integration testing (pending)
Let's start by creating an initial project from start.spring.io. Add Spring Web, Lombok and MySQL Driver as dependency. Select Maven as the project type and Java as the language. Provide appropriate metadata for the project and generate it.
Import project in your preferred editor. We will be using mysql for this project. So you can install in your local system or use a docker command to quickly start mysql service.
docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=secret@123 -p 3300:3306 -d mysql:latest
Connect to mysql db and create employee db
mysql -h 127.0.0.1 -P 3300 -u root -psecret@123 create database employee; use employee;
Below is the detailed code in each package. Please create package and class in your editor.
pom.xml
file
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>employee</artifactId> <version>0.0.1-SNAPSHOT</version> <name>employee</name> <description>Demo project for employee management</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.6.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
- model package: We will be creating
Employee
class.
package com.example.employee.model; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @Builder @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "employee") public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int empId; private String name; private String role; private String department; private String address; private double salary; }
dto
package
package com.example.employee.dto; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor @Data @Builder public class EmployeeReqDto { private int empId; @NotNull(message="enter a valid first name") @NotBlank(message="enter a valid first name") private String name; private String role; @NotNull(message="enter a valid department name") @NotBlank(message="enter a valid department name") private String department; private String address; @Min(value = 1, message = "Salary must be greater than 0") private double salary; }
package com.example.employee.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor @Data @Builder public class EmployeeResDto { private int empId; private String name; private String role; private String department; private String address; private double salary; }
Notes:
- There is input validation for corresponding field
import com.example.employee.exception.BaseException; public class ValidationError extends BaseException { public ValidationError(String message, String code) { super(message, code); } }
exception
package:
package com.example.employee.exception; public class BaseException extends RuntimeException{ private String code; public BaseException(String message, String code) { super(message); this.code = code; } public String getCode() { return code; } }
package com.example.employee.exception; public class DbException extends BaseException { public DbException(String message, String code) { super(message, code); } }
package com.example.employee.exception; public class EmployeeNotFound extends BaseException{ public EmployeeNotFound(String message, String code) { super(message, code); } }
package com.example.employee.exception; import com.example.employee.constants.ErrorCode; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class ErrorResponse { private String errorCode; private String message; public ErrorResponse(ErrorCode errorCode){ this.errorCode = errorCode.getCode(); this.message = errorCode.getMessage(); } }
package com.example.employee.exception; import com.example.employee.constants.ErrorCode; import com.example.employee.dto.ValidationError; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler({ValidationError.class}) public ResponseEntity<ErrorResponse> handleConflictException(ValidationError error, WebRequest request) { return new ResponseEntity<>(new ErrorResponse(error.getCode(), error.getMessage()), HttpStatus.BAD_REQUEST); } @ExceptionHandler({EmployeeNotFound.class}) public ResponseEntity<ErrorResponse> notFound(EmployeeNotFound error, WebRequest request) { return new ResponseEntity<>(new ErrorResponse(error.getCode(), error.getMessage()), HttpStatus.NOT_FOUND); } @ExceptionHandler({Exception.class}) public ResponseEntity<ErrorResponse> handleConflictException(Exception error, WebRequest request) { error.printStackTrace(); log.error(error.getMessage()); return new ResponseEntity<>(new ErrorResponse(ErrorCode.TEMP_ERR.getCode(), ErrorCode.TEMP_ERR.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } }
- class annotated with
@ControllerAdvice
is used to handle exception globally. - We have configured every exception with a custom response code, message, http status code.
constants
package
package com.example.employee.constants; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ErrorCode { SAVE_EMPLOYEE_ERR("ERROR01", "Db error saving employee information"), EMPLOYEE_INPUT_VALIDATION("ERROR02", "Bad request, check input"), TEMP_ERR("ERROR03", "Some temporary error"), EMPLOYEE_NOTFOUND("ERROR04", "Employee not found"), DELETE_EMPLOYEE_ERR("ERROR05", "Error deleting employee"), FETCH_EMPLOYEE_ERR("ERROR06", "Error fetching employee"); private final String code; private final String message; }
- These constant are used in response to give unique error code and message.
repo
package
package com.example.employee.repo; import com.example.employee.model.Employee; import org.springframework.data.jpa.repository.JpaRepository; public interface EmployeeRepo extends JpaRepository<Employee, Integer> { }
service
package
package com.example.employee.service; import com.example.employee.dto.EmployeeReqDto; import com.example.employee.dto.EmployeeResDto; import java.util.List; public interface EmployeeService { public void saveEmployee(EmployeeReqDto employeeResDto); public EmployeeResDto fetchEmployee(int empId); public List<EmployeeResDto> fetchAllEmployee(int page, int size); public EmployeeResDto updateEmployee(EmployeeReqDto employeeResDto); public void deleteEmployee(int empId); }
package com.example.employee.service.impl; import com.example.employee.constants.ErrorCode; import com.example.employee.dto.EmployeeReqDto; import com.example.employee.dto.EmployeeResDto; import com.example.employee.exception.DbException; import com.example.employee.exception.EmployeeNotFound; import com.example.employee.model.Employee; import com.example.employee.repo.EmployeeRepo; import com.example.employee.service.EmployeeService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import java.awt.print.Pageable; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @Service @Slf4j public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeRepo employeeRepo; @Override public void saveEmployee(EmployeeReqDto employeeResDto) { Employee employee = Employee.builder() .name(employeeResDto.getName()) .role(employeeResDto.getRole()) .department(employeeResDto.getDepartment()) .address(employeeResDto.getAddress()) .salary(employeeResDto.getSalary()) .build(); try { employeeRepo.save(employee); } catch (Exception exception) { log.error("error saving employee information"); throw new DbException(ErrorCode.SAVE_EMPLOYEE_ERR.getMessage(), ErrorCode.SAVE_EMPLOYEE_ERR.getCode()); } } @Override public EmployeeResDto fetchEmployee(int empId) { try { Optional<Employee> opTemployee = employeeRepo.findById(empId); if (opTemployee.isPresent()) { Employee employee = opTemployee.get(); return EmployeeResDto.builder() .empId(employee.getEmpId()) .name(employee.getName()) .role(employee.getRole()) .address(employee.getAddress()) .department(employee.getDepartment()) .salary(employee.getSalary()) .build(); } else { throw new EmployeeNotFound(ErrorCode.EMPLOYEE_NOTFOUND.getMessage(), ErrorCode.EMPLOYEE_NOTFOUND.getCode()); } } catch (Exception e) { if (e instanceof EmployeeNotFound) throw e; log.error("error fetching employee information with empId {}", empId); e.printStackTrace(); throw new DbException(ErrorCode.SAVE_EMPLOYEE_ERR.getMessage(), ErrorCode.SAVE_EMPLOYEE_ERR.getCode()); } } @Override public List<EmployeeResDto> fetchAllEmployee(int page, int size) { try { // since page number is 0 based, so decreasing it PageRequest pageRequest = PageRequest.of(page-1, size); return employeeRepo.findAll(pageRequest).stream() .map(employee -> EmployeeResDto .builder().name(employee.getName()) .role(employee.getRole()) .department(employee.getDepartment()) .salary(employee.getSalary()) .address(employee.getAddress()) .empId(employee.getEmpId()).build() ).collect(Collectors.toList()); } catch (Exception e) { log.error("error find all employee {}", e.getMessage()); throw new DbException(ErrorCode.FETCH_EMPLOYEE_ERR.getMessage(), ErrorCode.FETCH_EMPLOYEE_ERR.getCode()); } } @Override public EmployeeResDto updateEmployee(EmployeeReqDto employeeResDto) { Employee employee = Employee.builder() .empId(employeeResDto.getEmpId()) .name(employeeResDto.getName()) .role(employeeResDto.getRole()) .department(employeeResDto.getDepartment()) .address(employeeResDto.getAddress()) .salary(employeeResDto.getSalary()) .build(); try { Employee employeeUpdated = employeeRepo.save(employee); return EmployeeResDto.builder() .empId(employeeUpdated.getEmpId()) .name(employeeUpdated.getName()) .role(employeeUpdated.getRole()) .department(employeeUpdated.getDepartment()) .address(employeeUpdated.getAddress()) .salary(employee.getSalary()) .build(); } catch (Exception exception) { log.error("error saving employee information"); exception.printStackTrace(); throw new DbException(ErrorCode.SAVE_EMPLOYEE_ERR.getMessage(), ErrorCode.SAVE_EMPLOYEE_ERR.getCode()); } } @Override public void deleteEmployee(int empId) { try { Optional<Employee> optionalEmployee = employeeRepo.findById(empId); if (!optionalEmployee.isPresent()) { throw new EmployeeNotFound(ErrorCode.EMPLOYEE_NOTFOUND.getMessage(), ErrorCode.EMPLOYEE_NOTFOUND.getCode()); } else { employeeRepo.deleteById(empId); } } catch (Exception e) { if (e instanceof EmployeeNotFound) throw e; log.error("error deleting employee"); e.printStackTrace(); throw new DbException(ErrorCode.DELETE_EMPLOYEE_ERR.getMessage(), ErrorCode.DELETE_EMPLOYEE_ERR.getCode()); } } }
PageRequest
handles the pagination.
controller
package
package com.example.employee.controller; import com.example.employee.constants.ErrorCode; import com.example.employee.dto.EmployeeReqDto; import com.example.employee.dto.EmployeeResDto; import com.example.employee.dto.ValidationError; import com.example.employee.service.EmployeeService; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/employee") @Slf4j public class EmployeeController { @Autowired private EmployeeService employeeService; @PostMapping public ResponseEntity<?> saveEmployee(@RequestBody @Valid EmployeeReqDto employeeReqDto, BindingResult bindingResult) { if (bindingResult.hasErrors()) { ObjectError objectError = bindingResult.getAllErrors().get(0); throw new ValidationError(objectError.getDefaultMessage(), ErrorCode.EMPLOYEE_INPUT_VALIDATION.getCode()); } employeeService.saveEmployee(employeeReqDto); return ResponseEntity.status(HttpStatus.CREATED).build(); } @GetMapping public ResponseEntity<?> fetchEmployee(@RequestParam(value = "empId", required = false) String empId, @RequestParam(value = "page" , defaultValue = "0", required = false) int page, @RequestParam(value = "size", defaultValue = "10", required = false ) int size ) throws Exception { if(StringUtils.hasLength(empId)){ EmployeeResDto employee = employeeService.fetchEmployee(Integer.parseInt(empId)); return ResponseEntity.ok(employee); }else{ List<EmployeeResDto> employees = employeeService.fetchAllEmployee(page, size); return ResponseEntity.ok(employees); } } @PutMapping public ResponseEntity<EmployeeResDto> updateEmployee(@RequestBody @Valid EmployeeReqDto employeeReqDto, BindingResult bindingResult) { if (bindingResult.hasErrors()) { ObjectError objectError = bindingResult.getAllErrors().get(0); throw new ValidationError(objectError.getDefaultMessage(), ErrorCode.EMPLOYEE_INPUT_VALIDATION.getCode()); } try { EmployeeResDto employeeResDto = employeeService.updateEmployee(employeeReqDto); return ResponseEntity.ok(employeeResDto); } catch (Exception e) { throw e; } } @DeleteMapping public ResponseEntity<Void> deleteEmployee(@RequestParam("empId") String empId) { try { employeeService.deleteEmployee(Integer.parseInt(empId)); return ResponseEntity.accepted().build(); } catch (Exception e) { throw e; } } }
- Main application code
package com.example.employee; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class EmployeeApplication { public static void main(String[] args) { SpringApplication.run(EmployeeApplication.class, args); } }
- Add following properties in properties file for mysql configuration.
spring.application.name=employee spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3300/employee spring.datasource.username=root spring.datasource.password=secret@123 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.show-sql=true spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl springdoc.swagger-ui.path=/swagger.html ### actuator endpoint config management.info.java.enabled=true management.info.env.enabled=true management.endpoints.web.exposure.include=* management.endpoint.metrics.enabled=true management.endpoint.metrics.jvm.enabled=true management.endpoint.metrics.cache.enabled=true management.metrics.enable.process.files=true management.metrics.web.server.requests=true management.endpoint.metrics.web.server.requests=true logging.pattern.console= %d [%level] %c{1.} [%t] %m%n logging.file.name=access.log info.app.website=employee-crud info.app.builddate=2025-01-18 info.app.version=1.00
springdoc.swagger-ui.path
is used to give open docs pathmanagement.endpoints.web.exposure.include
property exposes all the actuator endpointlogging.pattern.console
gives logging pattern
And start the application by starting main method in EmployeeApplication
class. You will see similar logs if you application works properly.
2024-09-20T12:11:29.305+05:30 INFO 1671 --- [employee] [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer 2024-09-20T12:11:29.319+05:30 INFO 1671 --- [employee] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2024-09-20T12:11:29.586+05:30 INFO 1671 --- [employee] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@44d43cc9 2024-09-20T12:11:29.587+05:30 INFO 1671 --- [employee] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 2024-09-20T12:11:29.838+05:30 INFO 1671 --- [employee] [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) 2024-09-20T12:11:29.840+05:30 INFO 1671 --- [employee] [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' 2024-09-20T12:11:29.875+05:30 WARN 1671 --- [employee] [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning 2024-09-20T12:11:30.152+05:30 INFO 1671 --- [employee] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2024-09-20T12:11:30.157+05:30 INFO 1671 --- [employee] [ main] c.example.employee.EmployeeApplication : Started EmployeeApplication in 2.222 seconds (process running for 2.677)
-
Testing REST API: Postman api testing
- Create employee
curl --location 'http://localhost:8080/api/employee' \ --header 'Content-Type: application/json' \ --data '{ "name": "John Doe", "role": "Developer", "department": "IT", "address": "123 Main Street, Cityville", "salary": 50000.0 }'
status code: 201 Created No response body
You can check by logging in mysql terminal and fire select query
mysql -h 127.0.0.1 -P 3300 -u root -psecret@123 use employee; mysql> select * from employee \G; *************************** 1. row *************************** empId: 1 address: 123 Main Street, Cityville department: IT name: John Doe role: Developer salary: 50000 1 row in set (0.00 sec)
- GET employee
curl --location 'http://localhost:8080/api/employee?empId=1'
Status code: 200
{ "empId": 1, "name": "John Doe", "role": "Developer", "department": "IT", "address": "123 Main Street, Cityville", "salary": 50000.0 }
- Update employee
curl --location --request PUT 'http://localhost:8080/api/employee' \ --header 'Content-Type: application/json' \ --data '{ "empId": 1, "name": "John Doe", "role": "Developer", "department": "IT", "address": "123 Main Street, Cityville update", "salary": 50000.0 }'
Status code: 200
{ "empId": 1, "name": "John Doe", "role": "Developer", "department": "IT", "address": "123 Main Street, Cityville update", "salary": 50000.0 }
- Delete employee
curl --location --request DELETE 'http://localhost:8080/api/employee?empId=1'
- status code: 202
- No response body
- check by firing in sql terminal
mysql> select * from employee \G; Empty set (0.02 sec) ERROR: No query specified
-
Swagger url:
-
Actuator
/actuator
: Provides a list of all available Actuator endpoints.
curl http://localhost:8080/actuator
{ "_links": { "self": { "href": "http://localhost:8080/actuator", "templated": false }, "beans": { "href": "http://localhost:8080/actuator/beans", "templated": false }, "caches-cache": { "href": "http://localhost:8080/actuator/caches/{cache}", "templated": true }, "caches": { "href": "http://localhost:8080/actuator/caches", "templated": false }, "health": { "href": "http://localhost:8080/actuator/health", "templated": false }, "health-path": { "href": "http://localhost:8080/actuator/health/{*path}", "templated": true }, "info": { "href": "http://localhost:8080/actuator/info", "templated": false }, "conditions": { "href": "http://localhost:8080/actuator/conditions", "templated": false }, "configprops": { "href": "http://localhost:8080/actuator/configprops", "templated": false }, "configprops-prefix": { "href": "http://localhost:8080/actuator/configprops/{prefix}", "templated": true }, "env": { "href": "http://localhost:8080/actuator/env", "templated": false }, "env-toMatch": { "href": "http://localhost:8080/actuator/env/{toMatch}", "templated": true }, "logfile": { "href": "http://localhost:8080/actuator/logfile", "templated": false }, "loggers": { "href": "http://localhost:8080/actuator/loggers", "templated": false }, "loggers-name": { "href": "http://localhost:8080/actuator/loggers/{name}", "templated": true }, "heapdump": { "href": "http://localhost:8080/actuator/heapdump", "templated": false }, "threaddump": { "href": "http://localhost:8080/actuator/threaddump", "templated": false }, "metrics-requiredMetricName": { "href": "http://localhost:8080/actuator/metrics/{requiredMetricName}", "templated": true }, "metrics": { "href": "http://localhost:8080/actuator/metrics", "templated": false }, "sbom": { "href": "http://localhost:8080/actuator/sbom", "templated": false }, "sbom-id": { "href": "http://localhost:8080/actuator/sbom/{id}", "templated": true }, "scheduledtasks": { "href": "http://localhost:8080/actuator/scheduledtasks", "templated": false }, "mappings": { "href": "http://localhost:8080/actuator/mappings", "templated": false } } }
/actuator/health
: Displays the health status of the application.
curl http://localhost:8080/actuator/health
{ "status": "UP" }
/actuator/info
: Displays custom application information (like version, description, etc.)
{ "app": { "website": "employee-crud", "builddate": "2025-01-18", "version": "1.00" }, "java": { "version": "17.0.5", "vendor": { "name": "Azul Systems, Inc.", "version": "Zulu17.38+21-CA" }, "runtime": { "name": "OpenJDK Runtime Environment", "version": "17.0.5+8-LTS" }, "jvm": { "name": "OpenJDK 64-Bit Server VM", "vendor": "Azul Systems, Inc.", "version": "17.0.5+8-LTS" } } }
metrics
: Provides application metrics like memory usage, garbage collection, threads, etc
curl --location 'http://localhost:8080/actuator/metrics'
{ "names": [ "application.ready.time", "application.started.time", "disk.free", "disk.total", "executor.active", "executor.completed", "executor.pool.core", "executor.pool.max", "executor.pool.size", "executor.queue.remaining", "executor.queued", "hikaricp.connections", "hikaricp.connections.acquire", "hikaricp.connections.active", "hikaricp.connections.creation", "hikaricp.connections.idle", "hikaricp.connections.max", "hikaricp.connections.min", "hikaricp.connections.pending", "hikaricp.connections.timeout", "hikaricp.connections.usage", "http.server.requests", "http.server.requests.active", "jdbc.connections.active", "jdbc.connections.idle", "jdbc.connections.max", "jdbc.connections.min", "jvm.buffer.count", "jvm.buffer.memory.used", "jvm.buffer.total.capacity", "jvm.classes.loaded", "jvm.classes.unloaded", "jvm.compilation.time", "jvm.gc.live.data.size", "jvm.gc.max.data.size", "jvm.gc.memory.allocated", "jvm.gc.memory.promoted", "jvm.gc.overhead", "jvm.info", "jvm.memory.committed", "jvm.memory.max", "jvm.memory.usage.after.gc", "jvm.memory.used", "jvm.threads.daemon", "jvm.threads.live", "jvm.threads.peak", "jvm.threads.started", "jvm.threads.states", "logback.events", "process.cpu.time", "process.cpu.usage", "process.files.max", "process.files.open", "process.start.time", "process.uptime", "system.cpu.count", "system.cpu.usage", "system.load.average.1m", "tomcat.sessions.active.current", "tomcat.sessions.active.max", "tomcat.sessions.alive.max", "tomcat.sessions.created", "tomcat.sessions.expired", "tomcat.sessions.rejected" ] }
/actuator/metrics/{name}
: Displays detailed metrics for a specific metric (e.g.,jvm.memory.used
)
curl --location 'http://localhost:8080/actuator/metrics/executor.pool.core'
{ "name": "executor.pool.core", "description": "The core number of threads for the pool", "baseUnit": "threads", "measurements": [ { "statistic": "VALUE", "value": 8.0 } ], "availableTags": [ { "tag": "name", "values": ["applicationTaskExecutor"] } ] }
/actuator/env
: Exposes the environment properties and their sources
curl http://localhost:8080/actuator/env
/actuator/loggers
: Displays and manages the logging levels for various packages/actuator/beans
: Displays all the Spring beans in the application context
{ "contexts": { "employee": { "beans": { "spring.jpa-org.springframework.boot.autoconfigure.orm.jpa.JpaProperties": { "aliases": [], "scope": "singleton", "type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties", "dependencies": [] }, "applicationTaskExecutor": { "aliases": [ "taskExecutor" ], "scope": "singleton", "type": "org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor", "resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations$TaskExecutorConfiguration.class]", "dependencies": [ "org.springframework.boot.autoconfigure.task.TaskExecutorConfigurations$TaskExecutorConfiguration", "taskExecutorBuilder" ] }, .. ...
/actuator/mappings
: Displays all request mappings in the application.
{ "handler": "com.example.employee.controller.EmployeeController#fetchEmployee(String, int, int)", "predicate": "{GET [/api/employee]}", "details": { "handlerMethod": { "className": "com.example.employee.controller.EmployeeController", "name": "fetchEmployee", "descriptor": "(Ljava/lang/String;II)Lorg/springframework/http/ResponseEntity;" }, "requestMappingConditions": { "consumes": [], "headers": [], "methods": [ "GET" ], "params": [], "patterns": [ "/api/employee" ], "produces": [] } } }, .... ...
/actuator/threaddump
: Provides a thread dump from the application/actuator/heapdump
: Generates a heap dump file for diagnosing memory issues./actuator/scheduledtasks
Displays details of scheduled tasks in the application.
{ "cron": [], "fixedDelay": [], "fixedRate": [], "custom": [] }
/actuator/shutdown
Shuts down the application gracefully.- Enable in
application.properties
:
management.endpoint.shutdown.enabled=true
- Enable in
-
Below is the unit testing code.
package com.example.employee.controller; import com.example.employee.dto.EmployeeReqDto; import com.example.employee.dto.EmployeeResDto; import com.example.employee.exception.EmployeeNotFound; import com.example.employee.service.EmployeeService; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class EmployeeControllerTest { @LocalServerPort private int port; @Autowired private TestRestTemplate restTemplate; @MockBean private EmployeeService employeeService; ObjectMapper objectMapper; public EmployeeControllerTest() { objectMapper = new ObjectMapper(); } private String createURLWithPort(String uri) { return "http://localhost:" + port + uri; } @Test public void saveEmployee_created() { EmployeeReqDto validEmployee = EmployeeReqDto.builder() .name("John Doe").role("Software Engineer").department("IT").salary(2000) .address("123 Main Street, City, Country").build(); HttpHeaders headers = new HttpHeaders(); HttpEntity<EmployeeReqDto> entity = new HttpEntity<>(validEmployee, headers); ResponseEntity<String> response = restTemplate.postForEntity( createURLWithPort("/api/employee"), entity, String.class); assertEquals(HttpStatus.CREATED, response.getStatusCode()); } @Test public void saveEmployee_inputValidation() throws JsonProcessingException { // make name empty for validation error EmployeeReqDto validEmployee = EmployeeReqDto.builder() .name("").role("Software Engineer").department("IT").salary(2000) .address("123 Main Street, City, Country").build(); HttpHeaders headers = new HttpHeaders(); HttpEntity<EmployeeReqDto> entity = new HttpEntity<>(validEmployee, headers); ResponseEntity<String> response = restTemplate.postForEntity( createURLWithPort("/api/employee"), entity, String.class); assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); JsonNode jsonNode = objectMapper.readTree(response.getBody()); assertEquals("ERROR02", jsonNode.get("errorCode").asText()); assertEquals("enter a valid first name", jsonNode.get("message").asText()); } @Test public void fetchEmployee_success() throws JsonProcessingException { EmployeeResDto employeeResDto = EmployeeResDto.builder().name("name").build(); when(employeeService.fetchEmployee(any(Integer.class))).thenReturn(employeeResDto); ResponseEntity<String> response = restTemplate.getForEntity(createURLWithPort("/api/employee") + "?empId=20", String.class); assertEquals(HttpStatus.OK, response.getStatusCode()); JsonNode jsonNode = objectMapper.readTree(response.getBody()); assertEquals("name", jsonNode.get("name").asText()); } @Test public void fetchEmployee_not_found() throws JsonProcessingException { when(employeeService.fetchEmployee(any(Integer.class))).thenThrow(new EmployeeNotFound("", "ERR01")); ResponseEntity<String> response = restTemplate.getForEntity(createURLWithPort("/api/employee") + "?empId=20", String.class); assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); JsonNode jsonNode = objectMapper.readTree(response.getBody()); assertEquals("ERR01", jsonNode.get("errorCode").asText()); } @Test public void fetchEmployee_any_other_exception() throws JsonProcessingException { when(employeeService.fetchEmployee(any(Integer.class))).thenThrow(new RuntimeException()); ResponseEntity<String> response = restTemplate.getForEntity(createURLWithPort("/api/employee") + "?empId=20", String.class); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); JsonNode jsonNode = objectMapper.readTree(response.getBody()); assertEquals("ERROR03", jsonNode.get("errorCode").asText()); } @Test public void deleteEmployee_success() { doNothing().when(employeeService).deleteEmployee(any(Integer.class)); ResponseEntity<Void> response = restTemplate.exchange(createURLWithPort("/api/employee") + "?empId=20", HttpMethod.DELETE, null, Void.class ); assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); } @Test public void deleteEmployee_employee_not_found() { doThrow(new EmployeeNotFound("", "")).when(employeeService).deleteEmployee(any(Integer.class)); ResponseEntity<Void> response = restTemplate.exchange(createURLWithPort("/api/employee") + "?empId=20", HttpMethod.DELETE, null, Void.class ); assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode()); } @Test public void deleteEmployee_any_other_error() { doThrow(new RuntimeException()).when(employeeService).deleteEmployee(any(Integer.class)); ResponseEntity<Void> response = restTemplate.exchange(createURLWithPort("/api/employee") + "?empId=20", HttpMethod.DELETE, null, Void.class ); assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode()); } }
package com.example.employee.service; import com.example.employee.dto.EmployeeReqDto; import com.example.employee.dto.EmployeeResDto; import com.example.employee.exception.DbException; import com.example.employee.model.Employee; import com.example.employee.repo.EmployeeRepo; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Page; import java.util.Optional; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @SpringBootTest public class EmployeeServiceTest { @Autowired private EmployeeService employeeService; @MockBean private EmployeeRepo employeeRepo; private EmployeeReqDto validEmployee; @BeforeEach public void setup() { validEmployee = EmployeeReqDto.builder() .name("John Doe").role("Software Engineer").department("IT").salary(2000) .address("123 Main Street, City, Country").build(); } @Test public void saveEmployee_success() { when(employeeRepo.save(any(Employee.class))).thenReturn(new Employee()); employeeService.saveEmployee(validEmployee); } @Test public void saveEmployee_dbError() { when(employeeRepo.save(any(Employee.class))).thenThrow(new DbException("", "")); assertThrows(DbException.class, () -> employeeService.saveEmployee(validEmployee)); } @Test public void fetchEmployee_success() { Employee employee = Employee.builder().name("some name").build(); when(employeeRepo.findById(any(Integer.class))).thenReturn(Optional.of(employee)); EmployeeResDto employeeResDto = employeeService.fetchEmployee(1); assertEquals(employee.getName(), employeeResDto.getName()); } @Test public void fetchEmployee_any_error() { when(employeeRepo.findById(any(Integer.class))).thenThrow(new RuntimeException()); assertThrows(DbException.class, () -> employeeService.fetchEmployee(1)); } @Test public void deleteEmployee_success() { Employee employee = Employee.builder().name("some name").build(); when(employeeRepo.findById(any(Integer.class))).thenReturn(Optional.of(employee)); assertDoesNotThrow(() -> employeeService.deleteEmployee(1)); } @Test public void fetchAllEmployee_test() { when(employeeRepo.findAll(any(PageRequest.class))).thenReturn(new PageImpl<>(List.of( Employee.builder().name("name1").build(), Employee.builder().name("name2").build() ))); List<EmployeeResDto> employeeResDtos = employeeService.fetchAllEmployee(1, 2); assertEquals(2, employeeResDtos.size()); } }
package com.example.employee; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class EmployeeApplicationTests { @Test void contextLoads() { } }
spring.jpa.hibernate.ddl-auto=update spring.datasource.url= jdbc:h2:mem:test spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver spring.jpa.show-sql: true spring.jpa.database-platform=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
mockito
is used to do mock.- For integration testing we can use h2 database
Next part of this blog will come soon where I will integrate spring boot backend application with react application.
Checkout whole code in my github