Create the following folder structure:

pom.xml: (if using Gradle, replace pom.xml with build.gradle)
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> </parent> <groupId>net.maxjava</groupId> <artifactId>SpringBootWebServiceHibernateJpa</artifactId> <version>1.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>6.1.6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>6.4.5.Final</version> </dependency> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-c3p0</artifactId> <version>6.4.5.Final</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.5</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
plugins { id 'java' id 'org.springframework.boot' version '3.2.5' id 'io.spring.dependency-management' version '1.1.4' } group 'net.maxjava' version '1.0' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation group: 'org.springframework', name: 'spring-orm', version: '6.1.6' implementation group: 'org.hibernate', name: 'hibernate-core', version: '6.4.5.Final' implementation group: 'org.hibernate', name: 'hibernate-c3p0', version: '6.4.5.Final' implementation group: 'com.mchange', name: 'c3p0', version: '0.9.5.5' implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.33' compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' }
Some notes about pom.xml:
- Use spring-boot-starter-parent as parent POM for Spring Boot application
- Spring Boot 3.2.5 requires Java 17
- spring-orm for Hibernate related classes like session factories or transactions
- hibernate-core for Hibernate classes
- hibernate-c3p0 and c3p0 are for datasource
- lombok is used to remove boilerplate code like get and set
- Use spring-boot-maven-plugin to re-package all dependencies into a single, executable jar
- By running mvn clean package, a jar artifact which is ready to run by using java -jar will be created
Some notes about gradle.build:
- Use org.springframework.boot plugin for Spring Boot application, to make the jar executable and re-package all dependencies into a single jar
- Spring Boot 3.2.5 requires Java 17
- spring-orm for Hibernate related classes like session factories or transactions
- hibernate-core for Hibernate classes
- hibernate-c3p0 and c3p0 are for datasource
- Use io.spring.dependency-management plugin for dependency management
- lombok is used to remove boilerplate code like get and set
- By running gradle clean build, a jar artifact which is ready to run by using java -jar will be created
MySpringBootWebApplication.java:
package maxjava; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MySpringBootWebApplication { public static void main(String[] args) { SpringApplication.run(MySpringBootWebApplication.class, args); } }
Some notes about MySpringBootWebApplication.java:
- Mark a class with @SpringBootApplication and pass the class in SpringApplication.run will be good enough to run a Spring Boot application with web server (default setting)
- We could just put SpringApplication.run inside the static main function and annotate the same class for simplicity
MyController.java:
package maxjava.controller; import maxjava.repository.CustomerRepository; import maxjava.model.Customer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/myapp") public class MyController { @Autowired private CustomerRepository customerRepository; @GetMapping("/customer") public Customer getCustomer(@RequestParam("name") String name) { Customer customer = customerRepository.getCustomerByName(name); return customer; } @PostMapping("/customer") public void postCustomer(@RequestBody Customer customer) { customerRepository.saveCustomer(customer); } @PutMapping("/customer") public void putCustomer(@RequestBody Customer customer) { customerRepository.updateCustomer(customer); } @DeleteMapping("/customer") public void deleteCustomer(@RequestParam("name") String name) { customerRepository.deleteCustomerByName(name); } }
Some notes about MyController.java:
- Specifying @RestController will make this a controller to handle web request. Using @RestController instead of @Controller to perform serialization automatically, without the need of using @ResponseBody
- This provides some basic CRUD APIs
- CustomerRepository is autowired for interacting with the database
Customer.java:
package maxjava.model; import lombok.Data; @Data public class Customer { private String name; private String email; }
Some notes about Customer.java:
- This is the object used in the request or response body on the Controller APIs
- Use Lombok @Data for get and set
JpaConfiguration.java:
package maxjava.configuration; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.hibernate.jpa.HibernatePersistenceProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; import java.util.Properties; @Configuration @EnableTransactionManagement public class JpaConfiguration { @Bean public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean() { LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); localContainerEntityManagerFactoryBean.setDataSource(getDataSource()); localContainerEntityManagerFactoryBean.setPersistenceProviderClass(HibernatePersistenceProvider.class); localContainerEntityManagerFactoryBean.setJpaProperties(getJpaProperties()); localContainerEntityManagerFactoryBean.setPackagesToScan("maxjava.repository"); return localContainerEntityManagerFactoryBean; } @Bean JpaTransactionManager transactionManager() { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(localContainerEntityManagerFactoryBean().getObject()); return transactionManager; } private DataSource getDataSource() { final ComboPooledDataSource dataSource = new ComboPooledDataSource(); dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); dataSource.setUser("myuser"); dataSource.setPassword("mypassword"); try { dataSource.setDriverClass("com.mysql.cj.jdbc.Driver"); } catch (java.beans.PropertyVetoException pve) { // Handle exception } return dataSource; } private Properties getJpaProperties() { Properties properties = new Properties(); properties.put("jakarta.persistence.sharedCache.mode","NONE"); return properties; } }
Some notes about JpaConfiguration.java:
- Register the bean LocalContainerEntityManagerFactoryBean, which providing Entity Manager factory thus Entity Manager
- Register the bean JpaTransactionManager as transaction is used
- Use ComboPooledDataSource to create the connection pool to MySQL
CustomerRepository.java:
package maxjava.repository; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import maxjava.model.Customer; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.util.Date; @Repository public class CustomerRepository { @PersistenceContext public EntityManager entityManager; public Customer getCustomerByName(String name) { CustomerEntity customerEntity = entityManager.find(CustomerEntity.class, name); Customer customer = new Customer(); customer.setName(customerEntity.getName()); customer.setEmail(customerEntity.getEmail()); return customer; } @Transactional public void saveCustomer(Customer customer) { CustomerEntity customerEntity = new CustomerEntity(); customerEntity.setName(customer.getName()); customerEntity.setEmail(customer.getEmail()); customerEntity.setCreatedDate(new Date()); entityManager.persist(customerEntity); } @Transactional public void updateCustomer(Customer customer) { CustomerEntity customerEntity = new CustomerEntity(); customerEntity.setName(customer.getName()); customerEntity.setEmail(customer.getEmail()); customerEntity.setCreatedDate(new Date()); entityManager.merge(customerEntity); } @Transactional public void deleteCustomerByName(String name) { CustomerEntity customerEntity = entityManager.find(CustomerEntity.class, name); if (customerEntity != null) { entityManager.remove(customerEntity); } } }
Some notes about CustomerRepository.java:
- Inject the EntityManager bean by using the @PersistenceContext annotation
- Put @Transactional around each function requires transaction (the ones with writes)
- Mark this with @Repository as a bean
CustomerEntity.java:
package maxjava.repository; import jakarta.persistence.*; import lombok.Data; import java.util.Date; @Data @Entity @Table(name="Customer") public class CustomerEntity { @Id @Column(name="Name", length=50, nullable=false) private String name; @Basic @Column(name="Email") private String email; @Temporal(TemporalType.TIMESTAMP) @Column(name="CreatedDate") private Date createdDate; }
Some notes about CustomerEntity.java:
- Use Lombok @Data for get and set
- This is the mapped class for the table "Customer" with following chema:
CREATE TABLE Customer ( Name VARCHAR(50) NOT NULL, Email VARCHAR(50) NULL DEFAULT NULL, CreatedDate TIMESTAMP NULL DEFAULT NULL, PRIMARY KEY (Name) )
Sample requests and responses:
- Add a new Customer record:
curl --request POST '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "name": "Ethan", "email": "ethan@maxjava.net" }'
- Retrieve the Customer record by name
Response:curl --request GET '<CONTEXT_PATH>/myapp/customer?name=Ethan'{ "name": "Ethan", "email": "ethan@maxjava.net" } - Updating the Customer record by name
curl --request PUT '<CONTEXT_PATH>/myapp/customer' --header 'Content-Type: application/json' --data-raw ' { "name": "Ethan", "email": "ethan.new@maxjava.net" }'
- Retrieve the record again by same name, the data is changed
Response:curl --request GET '<CONTEXT_PATH>myapp/customer?name=Ethan'{ "name": "Ethan", "email": "ethan.new@maxjava.net" } - deleting the Customer record by name
curl --request DELETE '<CONTEXT_PATH>/myapp/customer?name=Ethan'