【Java项目脚手架系列】第六篇:Spring Boot + JPA项目脚手架
前言
在前面的文章中,我们介绍了 Spring Boot + MyBatis 项目脚手架。今天,我们将介绍 Spring Boot + JPA 项目脚手架,这是一个用于快速搭建企业级应用的框架。
什么是 Spring Boot + JPA?
Spring Boot + JPA 是一个强大的组合,它提供了:
- Spring Boot 的快速开发能力
- JPA 的对象关系映射
- 完整的数据库操作支持
- 事务管理能力
- 测试框架支持
技术栈
- Spring Boot 2.7.0:核心框架
- Spring Data JPA:持久层框架
- MySQL 8.0:关系型数据库
- H2 Database:内存数据库,用于测试
- JUnit 5:测试框架
- Mockito:测试框架
- Maven 3.9.6:项目构建工具
Spring Boot + JPA 项目脚手架
1. 项目结构
src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ ├── Application.java
│ │ ├── config
│ │ │ └── DataInitializer.java
│ │ ├── controller
│ │ │ └── UserController.java
│ │ ├── entity
│ │ │ └── User.java
│ │ ├── repository
│ │ │ └── UserRepository.java
│ │ └── service
│ │ ├── UserService.java
│ │ └── impl
│ │ └── UserServiceImpl.java
│ └── resources
│ └── application.yml
└── test
└── java
└── com
└── example
├── controller
│ └── UserControllerTest.java
├── repository
│ └── UserRepositoryTest.java
└── service
└── UserServiceTest.java
2. 核心文件内容
2.1 pom.xml
<?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>
<groupId>com.example</groupId>
<artifactId>springboot-jpa-scaffold</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</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>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2 application.yml
server:
port: 8080
spring:
profiles:
active: dev
datasource:
url: jdbc:mysql://localhost:3306/springboot_jpa?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
logging:
level:
com.example: debug
org.hibernate.SQL: debug
---
spring:
config:
activate:
on-profile: dev
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: true
h2:
console:
enabled: true
path: /h2-console
2.3 User.java
package com.example.entity;
import lombok.Data;
import javax.persistence.*;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column(name = "create_time", nullable = false)
private LocalDateTime createTime;
@Column(name = "update_time", nullable = false)
private LocalDateTime updateTime;
@PrePersist
protected void onCreate() {
createTime = LocalDateTime.now();
updateTime = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updateTime = LocalDateTime.now();
}
}
2.4 UserRepository.java
package com.example.repository;
import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
2.5 UserService.java
package com.example.service;
import com.example.entity.User;
import java.util.List;
import java.util.Optional;
public interface UserService {
User createUser(User user);
Optional<User> getUserById(Long id);
List<User> getAllUsers();
User updateUser(Long id, User user);
void deleteUser(Long id);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
2.6 UserServiceImpl.java
package com.example.service.impl;
import com.example.entity.User;
import com.example.repository.UserRepository;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Autowired
public UserServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public User createUser(User user) {
return userRepository.save(user);
}
@Override
@Transactional(readOnly = true)
public Optional<User> getUserById(Long id) {
return userRepository.findById(id);
}
@Override
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Override
public User updateUser(Long id, User user) {
return userRepository.findById(id)
.map(existingUser -> {
existingUser.setUsername(user.getUsername());
existingUser.setPassword(user.getPassword());
existingUser.setEmail(user.getEmail());
return userRepository.save(existingUser);
})
.orElseThrow(() -> new RuntimeException("User not found with id: " + id));
}
@Override
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@Override
@Transactional(readOnly = true)
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(username);
}
@Override
@Transactional(readOnly = true)
public boolean existsByEmail(String email) {
return userRepository.existsByEmail(email);
}
}
2.7 UserController.java
package com.example.controller;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable Long id, @RequestBody User user) {
return ResponseEntity.ok(userService.updateUser(id, user));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok().build();
}
}
2.8 DataInitializer.java
package com.example.config;
import com.example.entity.User;
import com.example.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private final UserService userService;
public DataInitializer(UserService userService) {
this.userService = userService;
}
@Override
public void run(String... args) {
// 创建管理员用户
User admin = new User();
admin.setUsername("admin");
admin.setPassword("admin123");
admin.setEmail("admin@example.com");
userService.createUser(admin);
// 创建普通用户
User user = new User();
user.setUsername("user");
user.setPassword("user123");
user.setEmail("user@example.com");
userService.createUser(user);
// 创建测试用户
User test = new User();
test.setUsername("test");
test.setPassword("test123");
test.setEmail("test@example.com");
userService.createUser(test);
}
}
2.9 Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 使用说明
-
克隆项目
git clone git@gitee.com:zengqiang_wang/leecode-inteview-questions-journal.git
-
导入IDE
- 推荐使用 IntelliJ IDEA
- 选择 “Open as Maven Project”
- 等待 Maven 依赖下载完成
-
运行项目
mvn spring-boot:run
-
访问接口
- 创建用户:POST http://localhost:8080/api/users
- 查询用户:GET http://localhost:8080/api/users/
- 更新用户:PUT http://localhost:8080/api/users/
- 删除用户:DELETE http://localhost:8080/api/users/
- 用户列表:GET http://localhost:8080/api/users
-
运行测试
# 运行所有测试 mvn test # 运行特定测试类 mvn test -Dtest=UserControllerTest # 运行特定测试方法 mvn test -Dtest=UserControllerTest#testGetUserById
4. 单元测试
项目包含了完整的单元测试示例,展示了如何测试 Spring Boot + JPA 应用的不同组件:
-
控制器层测试
- 使用 MockMvc 测试 HTTP 接口
- 模拟服务层依赖
- 验证请求和响应
- 示例:
UserControllerTest.java
-
服务层测试
- 使用 Mockito 模拟依赖
- 测试业务逻辑
- 验证方法调用
- 示例:
UserServiceTest.java
-
数据访问层测试
- 使用 H2 内存数据库
- 测试数据库操作
- 事务自动回滚
- 示例:
UserRepositoryTest.java
5. 最佳实践
-
实体设计
- 使用 JPA 注解
- 合理使用关系映射
- 添加审计字段
- 使用 Lombok 简化代码
-
数据访问层设计
- 使用 Spring Data JPA
- 自定义查询方法
- 分页和排序支持
- 事务管理
-
服务层设计
- 业务逻辑封装
- 事务管理
- 异常处理
- 数据转换
-
控制器设计
- RESTful API 设计
- 参数验证
- 统一响应格式
- 异常处理
6. 常见问题
-
编译问题
- 问题:Java 版本不兼容
- 原因:Maven 编译时使用的 Java 版本设置有问题
- 解决方案:
- 在
pom.xml
中明确指定 Java 编译版本:<properties> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
- 添加 Maven 编译器插件配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin>
- 在
- 问题:Java 版本不兼容
-
数据库连接问题
-
问题:MySQL 连接失败
- 原因:MySQL 服务未启动或配置错误
- 解决方案:
- 确保 MySQL 服务正在运行
- 检查数据库连接配置
- 添加
allowPublicKeyRetrieval=true
参数:spring: datasource: url: jdbc:mysql://localhost:3306/springboot_jpa?allowPublicKeyRetrieval=true
-
问题:本地未安装 MySQL
- 原因:开发环境缺少 MySQL 数据库
- 解决方案:
- 使用 H2 数据库进行开发
- 配置多环境支持:
spring: profiles: active: dev
- 添加 H2 数据库配置:
spring: config: activate: on-profile: dev datasource: url: jdbc:h2:mem:testdb username: sa password: h2: console: enabled: true
-
-
测试问题
-
问题:测试数据未持久化
- 原因:H2 内存数据库在应用重启后数据会丢失
- 解决方案:
- 使用
@DataJpaTest
注解 - 在测试类中初始化测试数据
- 使用
TestEntityManager
管理测试数据
- 使用
-
问题:测试环境配置
- 原因:测试环境配置不正确
- 解决方案:
- 使用
@DataJpaTest
进行数据访问层测试 - 使用
@WebMvcTest
进行 Web 层测试 - 使用
@MockBean
模拟依赖
- 使用
-
问题:缺少测试数据
- 原因:开发环境需要测试数据,但手动创建数据繁琐
- 解决方案:
- 创建
DataInitializer
类实现CommandLineRunner
接口 - 在应用启动时自动创建测试数据
- 示例代码:
@Component public class DataInitializer implements CommandLineRunner { private final UserService userService; public DataInitializer(UserService userService) { this.userService = userService; } @Override public void run(String... args) { // 创建管理员用户 User admin = new User(); admin.setUsername("admin"); admin.setPassword("admin123"); admin.setEmail("admin@example.com"); userService.createUser(admin); // 创建普通用户 User user = new User(); user.setUsername("user"); user.setPassword("user123"); user.setEmail("user@example.com"); userService.createUser(user); // 创建测试用户 User test = new User(); test.setUsername("test"); test.setPassword("test123"); test.setEmail("test@example.com"); userService.createUser(test); } }
- 优点:
- 自动创建测试数据,无需手动操作
- 数据创建过程可追踪和版本控制
- 便于团队协作和测试环境一致性
- 可以根据不同环境配置不同的初始化数据
- 创建
-
参考资源
-
官方文档
-
推荐书籍
- 《Spring Boot 实战》
- 《Spring Data JPA 实战》
- 《Java 持久化技术详解》
-
在线教程
-
工具资源
总结
Spring Boot + JPA 脚手架提供了一个完整的企业级应用开发基础,包含了必要的配置和示例代码。通过这个项目,你可以:
- 快速搭建 Web 应用
- 实现数据库操作
- 进行单元测试
- 使用开发工具
下期预告
下期我们将介绍 Spring Boot + Redis 项目脚手架,主要内容包括:
- Redis 与 Spring Boot 的集成
- 缓存配置
- 分布式锁实现
- 消息队列
- 会话管理
- 单元测试示例
敬请期待!