【Java项目脚手架系列】第六篇:Spring Boot + JPA项目脚手架

Scroll Down

【Java项目脚手架系列】第六篇:Spring Boot + JPA项目脚手架

前言

在前面的文章中,我们介绍了 Spring Boot + MyBatis 项目脚手架。今天,我们将介绍 Spring Boot + JPA 项目脚手架,这是一个用于快速搭建企业级应用的框架。

什么是 Spring Boot + JPA?

Spring Boot + JPA 是一个强大的组合,它提供了:

  1. Spring Boot 的快速开发能力
  2. JPA 的对象关系映射
  3. 完整的数据库操作支持
  4. 事务管理能力
  5. 测试框架支持

技术栈

  • 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. 使用说明

  1. 克隆项目

    git clone git@gitee.com:zengqiang_wang/leecode-inteview-questions-journal.git
    
  2. 导入IDE

    • 推荐使用 IntelliJ IDEA
    • 选择 “Open as Maven Project”
    • 等待 Maven 依赖下载完成
  3. 运行项目

    mvn spring-boot:run
    
  4. 访问接口

  5. 运行测试

    # 运行所有测试
    mvn test
    
    # 运行特定测试类
    mvn test -Dtest=UserControllerTest
    
    # 运行特定测试方法
    mvn test -Dtest=UserControllerTest#testGetUserById
    

4. 单元测试

项目包含了完整的单元测试示例,展示了如何测试 Spring Boot + JPA 应用的不同组件:

  1. 控制器层测试

    • 使用 MockMvc 测试 HTTP 接口
    • 模拟服务层依赖
    • 验证请求和响应
    • 示例:UserControllerTest.java
  2. 服务层测试

    • 使用 Mockito 模拟依赖
    • 测试业务逻辑
    • 验证方法调用
    • 示例:UserServiceTest.java
  3. 数据访问层测试

    • 使用 H2 内存数据库
    • 测试数据库操作
    • 事务自动回滚
    • 示例:UserRepositoryTest.java

5. 最佳实践

  1. 实体设计

    • 使用 JPA 注解
    • 合理使用关系映射
    • 添加审计字段
    • 使用 Lombok 简化代码
  2. 数据访问层设计

    • 使用 Spring Data JPA
    • 自定义查询方法
    • 分页和排序支持
    • 事务管理
  3. 服务层设计

    • 业务逻辑封装
    • 事务管理
    • 异常处理
    • 数据转换
  4. 控制器设计

    • RESTful API 设计
    • 参数验证
    • 统一响应格式
    • 异常处理

6. 常见问题

  1. 编译问题

    • 问题:Java 版本不兼容
      • 原因:Maven 编译时使用的 Java 版本设置有问题
      • 解决方案:
        1. 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>
          
        2. 添加 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>
          
  2. 数据库连接问题

    • 问题:MySQL 连接失败

      • 原因:MySQL 服务未启动或配置错误
      • 解决方案:
        1. 确保 MySQL 服务正在运行
        2. 检查数据库连接配置
        3. 添加 allowPublicKeyRetrieval=true 参数:
          spring:
            datasource:
              url: jdbc:mysql://localhost:3306/springboot_jpa?allowPublicKeyRetrieval=true
          
    • 问题:本地未安装 MySQL

      • 原因:开发环境缺少 MySQL 数据库
      • 解决方案:
        1. 使用 H2 数据库进行开发
        2. 配置多环境支持:
          spring:
            profiles:
              active: dev
          
        3. 添加 H2 数据库配置:
          spring:
            config:
              activate:
                on-profile: dev
            datasource:
              url: jdbc:h2:mem:testdb
              username: sa
              password:
            h2:
              console:
                enabled: true
          
  3. 测试问题

    • 问题:测试数据未持久化

      • 原因:H2 内存数据库在应用重启后数据会丢失
      • 解决方案:
        1. 使用 @DataJpaTest 注解
        2. 在测试类中初始化测试数据
        3. 使用 TestEntityManager 管理测试数据
    • 问题:测试环境配置

      • 原因:测试环境配置不正确
      • 解决方案:
        1. 使用 @DataJpaTest 进行数据访问层测试
        2. 使用 @WebMvcTest 进行 Web 层测试
        3. 使用 @MockBean 模拟依赖
    • 问题:缺少测试数据

      • 原因:开发环境需要测试数据,但手动创建数据繁琐
      • 解决方案:
        1. 创建 DataInitializer 类实现 CommandLineRunner 接口
        2. 在应用启动时自动创建测试数据
        3. 示例代码:
          @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);
              }
          }
          
        4. 优点:
          • 自动创建测试数据,无需手动操作
          • 数据创建过程可追踪和版本控制
          • 便于团队协作和测试环境一致性
          • 可以根据不同环境配置不同的初始化数据

参考资源

  1. 官方文档

  2. 推荐书籍

    • 《Spring Boot 实战》
    • 《Spring Data JPA 实战》
    • 《Java 持久化技术详解》
  3. 在线教程

  4. 工具资源

总结

Spring Boot + JPA 脚手架提供了一个完整的企业级应用开发基础,包含了必要的配置和示例代码。通过这个项目,你可以:

  1. 快速搭建 Web 应用
  2. 实现数据库操作
  3. 进行单元测试
  4. 使用开发工具

下期预告

下期我们将介绍 Spring Boot + Redis 项目脚手架,主要内容包括:

  • Redis 与 Spring Boot 的集成
  • 缓存配置
  • 分布式锁实现
  • 消息队列
  • 会话管理
  • 单元测试示例

敬请期待!