背景

本文是《Java 后端从小白到大神》修仙系列之框架学习,Java框架之Spring框架第六篇。本篇文章主要聊Java框架,那么必然从Spring框架开始,可以说Spring框架是Java企业级开发的基石,若想详细学习请点击首篇博文,我们现在开始吧。

文章概览

  1. Spring 项目练习

Spring框架

Spring 项目练习

1. 小项目练习:简单用户管理系统

根据Java框架-Spring框架 1-6 所学,现基于 Spring 框架实现一个“简单用户管理系统”小项目,涵盖依赖注入、Bean 生命周期、AOP、数据访问、事务、Web MVC、测试、事件、缓存、定时任务等核心模块。

2. 功能需求

  • 用户注册与查询:提供 REST 接口,新用户注册后可通过 ID 或分页查询所有用户。
  • 登录事件监听:当用户登录时,发布自定义事件并由监听器记录日志。
  • 定时汇总:每天定时统计当日注册用户数量并缓存结果。
  • 事务管理:注册过程包含写入用户表与写入审计表,需确保事务原子性。
  • 性能优化:对查询接口使用 Spring Cache 缓存。

3. 技术点映射

模块 学习清单 对应章节
DI/IoC Spring框架-1(6),Bean 定义与注入
Bean 生命周期 Spring框架-1(8),自定义 init/destroy 方法
AOP 日志 Spring框架-2,日志切面
数据访问 Spring框架-3,JdbcTemplate、JPA 集成
事务管理 Spring框架-3,@Transactional
Spring MVC Spring框架-4,REST 控制器
事件监听 Spring框架-4(2),应用事件
测试 Spring框架-5,集成测试、Mockito 模拟
缓存 Spring框架-5(4),缓存支持 (@Cacheable)
定时任务 Spring框架-5(4),调度 (@Scheduled)

4. 项目结构示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
usermgmt-project/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/yutao/usermgmt/
│   │   │   ├── UsermgmtApplication.java
│   │   │   ├── config/
│   │   │   │   └── AppConfig.java
│   │   │   │   └── AppProperties
│   │   │   ├── domain/
│   │   │   │   └── User.java
│   │   │   │   └── AuditLog
│   │   │   ├── repository/
│   │   │   │   ├── UserDao.java
│   │   │   │   └── UserRepository.java
│   │   │   │   └── AuditLogRepository
│   │   │   ├── service/
│   │   │   │   ├── UserService.java
│   │   │   │   └── AuditService.java
│   │   │   ├── web/
│   │   │   │   └── UserController.java
│   │   │   ├── aspect/
│   │   │   │   └── LoggingAspect.java
│   │   │   ├── event/
│   │   │   │   ├── UserLoginEvent.java
│   │   │   │   └── UserEventListener.java
│   │   │   └── scheduler/
│   │   │   │   └── DailyReportScheduler.java
│   │   │   ├── exception/
│   │   │   │   └── ResourceNotFoundException.java
│   │   │   │   └── GlobalExceptionHandler.java
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── application-dev.properties
│   │       └── application-prod.properties
│   └── test/
│       ├── java/com/yutao/usermgmt/
│       │   ├── service/
│       │   │   └── UserServiceTest.java // Mockito 单元测试
│       │   └── web/
│       │       └── UserControllerIntegrationTest.java // MockMvc 集成测试
│       └── resources/
│           └── application-test.properties

5. 项目代码

主程序 src/main/java/com/yutao/usermgmt/

用户管理系统

UsermgmtApplication.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package com.yutao.usermgmt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; // 启用缓存
import org.springframework.scheduling.annotation.EnableScheduling; // 启用定时任务
import org.springframework.context.annotation.EnableAspectJAutoProxy; // 启用AOP自动代理

@SpringBootApplication
@EnableCaching // 启用缓存支持
@EnableScheduling // 启用定时任务
@EnableAspectJAutoProxy // 启用Spring AOP自动代理
public class UsermgmtApplication {

    public static void main(String[] args) {
        SpringApplication.run(UsermgmtApplication.class, args);
    }

}

AppConfig.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package com.yutao.usermgmt.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;

import java.util.concurrent.TimeUnit;
import com.github.benmanes.caffeine.cache.Caffeine;

@Configuration
public class AppConfig {

    private final Environment env;

    public AppConfig(Environment env) {
        this.env = env;
    }

    // 配置 Caffeine CacheManager
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "audits"); // 定义缓存名称
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
                .maximumSize(1000)); // 最大缓存条目数
        return cacheManager;
    }

    // Profile 特定配置 - 开发/测试数据库信息
    @Bean
    @Profile({ "dev", "test" })
    public String devTestDatabaseInfo() {
        System.out.println("--- 激活开发/测试数据库配置 ---");
        System.out.println("数据库URL: " + env.getProperty("spring.datasource.url"));
        System.out.println("--- ");
        return "Dev/Test Database Active";
    }

    // Profile 特定配置 - 生产数据库信息
    @Bean
    @Profile("prod")
    public String prodDatabaseInfo() {
        System.out.println("--- 激活生产数据库配置 ---");
        System.out.println("数据库URL: " + env.getProperty("spring.datasource.url"));
        System.out.println("--- ");
        return "Production Database Active";
    }

    @Bean
    public String currentProfileMessage() {
        String[] activeProfiles = env.getActiveProfiles();
        StringBuilder message = new StringBuilder("当前激活的Profile: ");
        if (activeProfiles.length > 0) {
            for (String profile : activeProfiles) {
                message.append(profile).append(" ");
            }
        } else {
            message.append("没有特定Profile激活 (默认)");
        }
        message.append("| 应用消息: ").append(env.getProperty("app.message"));
        System.out.println(message.toString());
        return message.toString();
    }
}

AppProperties.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.yutao.usermgmt.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 用于绑定 application.properties 中的 app.message 属性。
 */
@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

User.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package com.yutao.usermgmt.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.io.Serializable; // 用于缓存和可能存在的Remoting

@Entity
@Table(name = "users")
public class User implements Serializable { // 实现Serializable以便进行缓存或远程传输

    private static final long serialVersionUID = 1L; // 序列化ID

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank(message = "Name is mandatory")
    @Size(max = 100, message = "Name cannot exceed 100 characters")
    private String name;

    @NotBlank(message = "Email is mandatory")
    @Email(message = "Email should be valid")
    @Column(unique = true, nullable = false) // Email应该唯一且非空
    private String email;

    // Default constructor for JPA
    public User() {
    }

    // Constructor for creating new users (without ID)
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Constructor for testing or loading existing users (with ID)
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    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;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o)
            return true;
        if (o == null || getClass() != o.getClass())
            return false;
        User user = (User) o;
        return id != null && id.equals(user.id);
    }

    @Override
    public int hashCode() {
        return id != null ? id.hashCode() : 0;
    }
}

AuditLog.java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
package com.yutao.usermgmt.domain;

import jakarta.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@Entity
@Table(name = "audit_logs")
public class AuditLog implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String action; // e.g., "USER_CREATED", "USER_UPDATED", "USER_DELETED"

    @Column(nullable = false)
    private String entityType; // e.g., "User"

    @Column
    private Long entityId; // ID of the entity that was acted upon

    @Column(nullable = false)
    private LocalDateTime timestamp;

    @Column
    private String performedBy; // User who performed the action (e.g., username, IP)

    public AuditLog() {
    }

    public AuditLog(String action, String entityType, Long entityId, String performedBy) {
        this.action = action;
        this.entityType = entityType;
        this.entityId = entityId;
        this.timestamp = LocalDateTime.now();
        this.performedBy = performedBy;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }

    public String getEntityType() {
        return entityType;
    }

    public void setEntityType(String entityType) {
        this.entityType = entityType;
    }

    public Long getEntityId() {
        return entityId;
    }

    public void setEntityId(Long entityId) {
        this.entityId = entityId;
    }

    public LocalDateTime getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(LocalDateTime timestamp) {
        this.timestamp = timestamp;
    }

    public String getPerformedBy() {
        return performedBy;
    }

    public void setPerformedBy(String performedBy) {
        this.performedBy = performedBy;
    }

    @Override
    public String toString() {
        return "AuditLog{" +
                "id=" + id +
                ", action='" + action + '\'' +
                ", entityType='" + entityType + '\'' +
                ", entityId=" + entityId +
                ", timestamp=" + timestamp +
                ", performedBy='" + performedBy + '\'' +
                '}';
    }
}

UserDao.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.yutao.usermgmt.repository;

import com.yutao.usermgmt.domain.User;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.lang.NonNull;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

@Repository // 标记为Repository,由Spring管理
public class UserDao { // 使用Dao而非Repository避免与Spring Data JPA的Repository混淆

    private final JdbcTemplate jdbcTemplate;

    public UserDao(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // RowMapper用于将ResultSet映射到User对象
    private RowMapper<User> userRowMapper = new RowMapper<User>() {
        @Override
        public User mapRow(@NonNull ResultSet rs, int rowNum) throws SQLException {
            return new User(
                    rs.getLong("id"),
                    rs.getString("name"),
                    rs.getString("email"));
        }
    };

    public List<User> findAll() {
        String sql = "SELECT id, name, email FROM users";
        return jdbcTemplate.query(sql, userRowMapper);
    }

    public Optional<User> findById(Long id) {
        String sql = "SELECT id, name, email FROM users WHERE id = ?";
        List<User> users = jdbcTemplate.query(sql, userRowMapper, id);
        return users.stream().findFirst();
    }

    public int save(User user) {
        String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
        return jdbcTemplate.update(sql, user.getName(), user.getEmail());
    }

    public int update(User user) {
        String sql = "UPDATE users SET name = ?, email = ? WHERE id = ?";
        return jdbcTemplate.update(sql, user.getName(), user.getEmail(), user.getId());
    }

    public int deleteById(Long id) {
        String sql = "DELETE FROM users WHERE id = ?";
        return jdbcTemplate.update(sql, id);
    }
}

UserRepository.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.yutao.usermgmt.repository;

import com.yutao.usermgmt.domain.User;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

// Spring Data JPA 会自动为这个接口生成实现
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 可以添加自定义的查询方法,例如:
    Optional<User> findByEmail(String email);

    List<User> findByNameContaining(String name);
}

AuditLogRepository.java

1
2
3
4
5
6
7
8
9
package com.yutao.usermgmt.repository;

import com.yutao.usermgmt.domain.AuditLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
}

UserService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
package com.yutao.usermgmt.service;

import com.yutao.usermgmt.domain.User;
import com.yutao.usermgmt.event.UserLoginEvent; // 导入事件
import com.yutao.usermgmt.repository.UserRepository;
import jakarta.transaction.Transactional; // Spring事务注解
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.context.ApplicationEventPublisher; // 导入事件发布器
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

import com.yutao.usermgmt.exception.ResourceNotFoundException;  

@Service
public class UserService {

    private final UserRepository userRepository;
    private final AuditService auditService; // 注入审计服务
    private final ApplicationEventPublisher eventPublisher; // 注入事件发布器

    public UserService(UserRepository userRepository, AuditService auditService,
            ApplicationEventPublisher eventPublisher) {
        this.userRepository = userRepository;
        this.auditService = auditService;
        this.eventPublisher = eventPublisher;
    }

    @Transactional // 开启事务管理
    @CacheEvict(value = "users", allEntries = true) // 创建新用户时,清空所有用户缓存,确保getAllUsers获取最新
    public User createUser(User user) {
        // 实际应用中需要检查email唯一性,这里简化
        User savedUser = userRepository.save(user);
        auditService.logAction("USER_CREATED", "User", savedUser.getId(), savedUser.getEmail()); // 记录审计日志
        // 发布用户登录事件(示例,创建后也可能触发登录事件)
        // 这里只是为了演示事件机制,实际用户创建后不一定直接触发登录
        eventPublisher.publishEvent(new UserLoginEvent(this, savedUser.getId(), savedUser.getEmail(), "localhost"));
        return savedUser;
    }

    @Cacheable(value = "users", key = "#id") // 缓存getUserById方法结果
    public Optional<User> getUserById(Long id) {
        System.out.println(">>> UserService: 从数据库获取用户ID: " + id); // 用于观察是否命中缓存
        return userRepository.findById(id);
    }

    @Cacheable(value = "users", key = "'allUsers'") // 缓存所有用户列表
    public List<User> getAllUsers() {
        System.out.println(">>> UserService: 从数据库获取所有用户"); // 用于观察是否命中缓存
        return userRepository.findAll();
    }

    @Transactional
    @Caching(evict = { // 组合缓存操作:清除特定用户和所有用户的缓存
            @CacheEvict(value = "users", key = "#id"),
            @CacheEvict(value = "users", key = "'allUsers'")
    })
    public User updateUser(Long id, User userDetails) {
        return userRepository.findById(id).map(user -> {
            user.setName(userDetails.getName());
            user.setEmail(userDetails.getEmail());
            User updatedUser = userRepository.save(user);
            auditService.logAction("USER_UPDATED", "User", updatedUser.getId(), updatedUser.getEmail()); // 记录审计日志
            return updatedUser;
        }).orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id)); // 抛出运行时异常
    }

    @Transactional
    @Caching(evict = { // 组合缓存操作:清除特定用户和所有用户的缓存
            @CacheEvict(value = "users", key = "#id"),
            @CacheEvict(value = "users", key = "'allUsers'")
    })
    public void deleteUser(Long id) {
        if (userRepository.existsById(id)) {
            userRepository.deleteById(id);
            auditService.logAction("USER_DELETED", "User", id, "System"); // 记录审计日志
        } else {
            throw new ResourceNotFoundException("User not found with id " + id); // 用户不存在时抛出异常
        }
    }
}

AuditService.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.yutao.usermgmt.service;

import com.yutao.usermgmt.domain.AuditLog;
import com.yutao.usermgmt.repository.AuditLogRepository;
import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional; // 注意:这里使用 jakarta.transaction.Transactional,为了演示独立事务

@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    public AuditService(AuditLogRepository auditLogRepository) {
        this.auditLogRepository = auditLogRepository;
    }

    // 独立事务:无论调用方事务是否成功,审计日志都会被保存
    // 适用于日志记录这种即使主业务失败也希望记录的场景
    @Transactional(Transactional.TxType.REQUIRES_NEW) // 使用REQUIRES_NEW,开启新事务
    public AuditLog logAction(String action, String entityType, Long entityId, String performedBy) {
        AuditLog auditLog = new AuditLog(action, entityType, entityId, performedBy);
        System.out.println(
                "审计日志写入: " + auditLog.getAction() + " for " + auditLog.getEntityType() + " " + auditLog.getEntityId());
        return auditLogRepository.save(auditLog);
    }
}

UserController.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.yutao.usermgmt.web;

import com.yutao.usermgmt.domain.User;
import com.yutao.usermgmt.exception.ResourceNotFoundException;
import com.yutao.usermgmt.service.UserService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
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;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        User createdUser = userService.createUser(user);
        return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userService.getUserById(id)
                .map(user -> new ResponseEntity<>(user, HttpStatus.OK))
                .orElseThrow(() -> new ResourceNotFoundException("User not found with id " + id));
    }

    @GetMapping
    public ResponseEntity<List<User>> getAllUsers() {
        List<User> users = userService.getAllUsers();
        return new ResponseEntity<>(users, HttpStatus.OK);
    }

    @PutMapping("/{id}")
    public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User userDetails) {
        User updatedUser = userService.updateUser(id, userDetails);
        return new ResponseEntity<>(updatedUser, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
}

LoggingAspect.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.yutao.usermgmt.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect // 标记为切面类
@Component // 标记为Spring组件
public class LoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    // 定义切入点:匹配com.yutao.usermgmt.service包下所有类的所有方法
    @Pointcut("execution(* com.yutao.usermgmt.service.*.*(..))")
    private void serviceMethods() {
    }

    // 定义切入点:匹配com.yutao.usermgmt.web包下所有类的所有方法
    @Pointcut("execution(* com.yutao.usermgmt.web.*.*(..))")
    private void controllerMethods() {
    }

    // @Before:在目标方法执行之前执行
    @Before("serviceMethods() || controllerMethods()")
    public void logBefore(JoinPoint joinPoint) {
        logger.info("Entering {}.{}() with arguments[s] = {}",
                joinPoint.getSignature().getDeclaringTypeName(), // 返回被拦截方法所属的类的全限定名
                joinPoint.getSignature().getName(), // 返回被拦截方法的方法名
                joinPoint.getArgs()); // 返回被拦截方法参数列表
    }

    // @AfterReturning:在目标方法成功执行并返回结果之后执行
    @AfterReturning(pointcut = "serviceMethods() || controllerMethods()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        logger.info("Exiting {}.{}() with result = {}",
                joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName(),
                result);
    }

    // @AfterThrowing:在目标方法抛出异常之后执行
    @AfterThrowing(pointcut = "serviceMethods() || controllerMethods()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        logger.error("Exception in {}.{}() with cause = {}",
                joinPoint.getSignature().getDeclaringTypeName(),
                joinPoint.getSignature().getName(),
                e.getMessage());
    }

    // @Around:环绕通知,可以完全控制方法的执行,包括是否执行、修改参数、修改返回值等
    // 它结合了@Before, @AfterReturning, @AfterThrowing的功能
    @Around("serviceMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = null;
        try {
            result = joinPoint.proceed(); // 执行目标方法
            long end = System.currentTimeMillis();
            logger.info("Around Advice: {}.{}() executed in {} ms",
                    joinPoint.getSignature().getDeclaringTypeName(),
                    joinPoint.getSignature().getName(),
                    (end - start));
            return result;
        } catch (IllegalArgumentException e) {
            logger.error("Around Advice: Illegal argument {} in {}.{}()",
                    joinPoint.getArgs()[0],
                    joinPoint.getSignature().getDeclaringTypeName(),
                    joinPoint.getSignature().getName());
            throw e; // 重新抛出异常
        }
    }
}

UserLoginEvent.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.yutao.usermgmt.event;

import org.springframework.context.ApplicationEvent;

// 用户登录事件
public class UserLoginEvent extends ApplicationEvent {

    private Long userId;
    private String email;
    private String ipAddress;

    public UserLoginEvent(Object source, Long userId, String email, String ipAddress) {
        super(source);
        this.userId = userId;
        this.email = email;
        this.ipAddress = ipAddress;
    }

    public Long getUserId() {
        return userId;
    }

    public String getEmail() {
        return email;
    }

    public String getIpAddress() {
        return ipAddress;
    }

    @Override
    public String toString() {
        return "UserLoginEvent{" +
                "userId=" + userId +
                ", email='" + email + '\'' +
                ", ipAddress='" + ipAddress + '\'' +
                ", timestamp=" + getTimestamp() +
                '}';
    }
}

UserEventListener.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.yutao.usermgmt.event;

import org.springframework.context.ApplicationListener;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;

// 事件监听器:监听UserLoginEvent
@Component
public class UserEventListener implements ApplicationListener<UserLoginEvent> {

    @Override
    public void onApplicationEvent(@NonNull UserLoginEvent event) {
        System.out.println("事件监听器捕获到用户登录事件: " + event.getEmail() + " (" + event.getUserId() + ") 从 IP: "
                + event.getIpAddress() + " 登录。");
        // 这里可以执行一些异步操作,例如发送通知邮件、更新用户活跃度等
    }
}

DailyReportScheduler.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.yutao.usermgmt.scheduler;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

@Component
public class DailyReportScheduler {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    // 每隔 10 秒执行一次,用于演示
    @Scheduled(fixedRate = 10000) // 从方法开始执行时计时
    public void demoScheduledTask() {
        System.out.println("定时任务:演示任务,当前时间是 " + dateFormat.format(new Date()));
    }

    // 每天凌晨 3 点 30 分执行一次(根据你的学习清单,选择一个cron表达式)
    @Scheduled(cron = "0 30 3 * * ?") // 秒 分 时 日 月 周
    public void generateDailyReport() {
        System.out.println("定时任务:每天凌晨 3:30 自动生成日报...");
        // 实际应用中:
        // 1. 调用Service层方法获取数据
        // 2. 处理数据,生成报告(例如CSV, PDF)
        // 3. 将报告存储到文件系统或发送邮件
    }
}

ResourceNotFoundException.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.yutao.usermgmt.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

// @ResponseStatus 注解可以在抛出此异常时直接设置HTTP状态码
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
    
}

GlobalExceptionHandler.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.yutao.usermgmt.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

// @RestControllerAdvice 结合了 @ControllerAdvice 和 @ResponseBody
// 它使得这个类可以处理整个应用程序的异常,并直接返回JSON/XML格式的响应
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理 @Valid 注解导致的验证失败异常
    @ResponseStatus(HttpStatus.BAD_REQUEST) // 设置响应状态码为 400 Bad Request
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((org.springframework.validation.FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return errors; // 返回一个包含字段名和错误信息的Map
    }

    // 处理自定义的 ResourceNotFoundException 异常
    @ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404 Not Found
    @ExceptionHandler(ResourceNotFoundException.class)
    public Map<String, String> handleResourceNotFoundException(ResourceNotFoundException ex) {
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", "Resource Not Found");
        errorResponse.put("message", ex.getMessage());
        return errorResponse;
    }

    // 通用的异常处理,捕获所有未被特定处理方法捕获的异常
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500 Internal Server Error
    @ExceptionHandler(Exception.class)
    public Map<String, String> handleAllUncaughtException(Exception ex) {
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", "Internal Server Error");
        errorResponse.put("message", "An unexpected error occurred: " + ex.getMessage());
        // 在生产环境中,可以只记录日志,不返回详细错误信息给客户端
        // Logger.error("Unhandled exception: ", ex);
        return errorResponse;
    }

    // 返回 ResponseEntity,提供更细粒度的控制,例如:
    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Map<String, String>> handleRuntimeException(RuntimeException ex) {
        Map<String, String> errorResponse = new HashMap<>();
        errorResponse.put("error", "Bad Request");
        errorResponse.put("message", ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
}

application-dev.properties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 激活开发环境profile
# spring.profiles.active=dev # 通常在命令行或IDE配置,而不是直接写在这里

# H2 内存数据库配置 (开发/测试)
spring.datasource.url=jdbc:h2:mem:usermgmt_db_dev;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 控制台,方便查看数据库内容 (仅开发环境开启)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA 针对H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

# 应用消息
app.message=This is the DEV environment message.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 激活生产环境profile
# spring.profiles.active=prod # 通常在命令行或部署时设置

# MySQL 数据库配置 (生产)
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

# JPA 针对MySQL
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect

# 应用消息
app.message=This is the PROD environment message.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Spring Boot 默认端口
server.port=8080

# 默认应用消息
app.message=This is the default application message.

# JPA/Hibernate 配置 (通用设置)
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# 日志级别
logging.level.com.yutao.usermgmt=INFO
logging.level.org.springframework=INFO
logging.level.org.hibernate=INFO

测试程序 src/test/java/com/yutao/usermgmt

测试用户管理系统

UserServiceTest.java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.yutao.usermgmt.service;

import com.yutao.usermgmt.domain.AuditLog;
import com.yutao.usermgmt.domain.User;
import com.yutao.usermgmt.event.UserLoginEvent;
import com.yutao.usermgmt.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;
    @Mock
    private AuditService auditService; // 模拟审计服务
    @Mock
    private ApplicationEventPublisher eventPublisher; // 模拟事件发布器

    @InjectMocks
    private UserService userService;

    private User testUserWithId; // 用于模拟返回的带ID的用户
    private User testUserNoId; // 用于作为传入参数的无ID用户

    @BeforeEach
    void setUp() {
        // 这个对象代表了:当 userRepository.save() 被调用后,模拟它会返回什么。
        // 它应该包含一个模拟的 ID,因为真实的保存操作会生成 ID。
        testUserWithId = new User(1L, "Alice", "[email protected]");

        // 这个对象代表了:作为输入参数传递给 createUser 方法的原始 User 对象,它没有 ID。
        testUserNoId = new User("Alice", "[email protected]");
    }

    @Test
    void testCreateUser_Success() {
        // 设置模拟行为:当 userRepository.save() 接收到任何 User 对象时,
        // 返回我们预先定义的带有 ID 的 testUserWithId 对象。
        when(userRepository.save(any(User.class))).thenReturn(testUserWithId);
        // 模拟审计服务
        when(auditService.logAction(anyString(), anyString(), anyLong(), anyString())).thenReturn(mock(AuditLog.class));

        // 调用被测试的 UserService 方法
        User createdUser = userService.createUser(testUserNoId);

        // 验证返回的用户对象
        assertNotNull(createdUser);
        assertEquals(testUserWithId.getId(), createdUser.getId());
        assertEquals(testUserWithId.getName(), createdUser.getName());
        assertEquals(testUserWithId.getEmail(), createdUser.getEmail());

        // 验证 userRepository.save() 方法被调用了一次,且传入的参数是 testUserNoId
        verify(userRepository, times(1)).save(testUserNoId);
        // 验证 auditService.logAction() 被调用了一次
        verify(auditService, times(1)).logAction("USER_CREATED", "User", testUserWithId.getId(),
                testUserWithId.getEmail());
        // 验证 UserLoginEvent 事件被发布了一次
        verify(eventPublisher, times(1)).publishEvent(any(UserLoginEvent.class));
    }

    @Test
    void testGetUserById_Found() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(testUserWithId));

        Optional<User> foundUser = userService.getUserById(1L);

        assertTrue(foundUser.isPresent());
        assertEquals(testUserWithId.getName(), foundUser.get().getName());
        verify(userRepository, times(1)).findById(1L);
    }

    @Test
    void testGetUserById_NotFound() {
        when(userRepository.findById(2L)).thenReturn(Optional.empty());

        Optional<User> foundUser = userService.getUserById(2L);

        assertFalse(foundUser.isPresent());
        verify(userRepository, times(1)).findById(2L);
    }

    @Test
    void testGetAllUsers() {
        User user2 = new User(2L, "Bob", "[email protected]");
        when(userRepository.findAll()).thenReturn(Arrays.asList(testUserWithId, user2));

        List<User> users = userService.getAllUsers();

        assertNotNull(users);
        assertEquals(2, users.size());
        verify(userRepository, times(1)).findAll();
    }

    @Test
    void testUpdateUser_Success() {
        User updatedDetails = new User("Alice Updated", "[email protected]");
        User existingUser = new User(1L, "Alice", "[email protected]");
        User finalUpdatedUser = new User(1L, "Alice Updated", "[email protected]");

        when(userRepository.findById(1L)).thenReturn(Optional.of(existingUser));
        when(userRepository.save(any(User.class))).thenReturn(finalUpdatedUser);
        when(auditService.logAction(anyString(), anyString(), anyLong(), anyString())).thenReturn(mock(AuditLog.class));

        User result = userService.updateUser(1L, updatedDetails);

        assertNotNull(result);
        assertEquals("Alice Updated", result.getName());
        assertEquals("[email protected]", result.getEmail());
        verify(userRepository, times(1)).findById(1L);
        verify(userRepository, times(1)).save(any(User.class)); // 验证save被调用
        verify(auditService, times(1)).logAction("USER_UPDATED", "User", finalUpdatedUser.getId(),
                finalUpdatedUser.getEmail());
    }

    @Test
    void testUpdateUser_NotFound() {
        User updatedDetails = new User("Alice Updated", "[email protected]");
        when(userRepository.findById(99L)).thenReturn(Optional.empty());

        assertThrows(RuntimeException.class, () -> userService.updateUser(99L, updatedDetails));

        verify(userRepository, times(1)).findById(99L);
        verify(userRepository, never()).save(any(User.class)); // 确保没有调用save
        verify(auditService, never()).logAction(anyString(), anyString(), anyLong(), anyString()); // 确保没有审计日志
    }

    @Test
    void testDeleteUser_Success() {
        when(userRepository.existsById(1L)).thenReturn(true);
        doNothing().when(userRepository).deleteById(1L);
        when(auditService.logAction(anyString(), anyString(), anyLong(), anyString())).thenReturn(mock(AuditLog.class));

        userService.deleteUser(1L);

        verify(userRepository, times(1)).existsById(1L);
        verify(userRepository, times(1)).deleteById(1L);
        verify(auditService, times(1)).logAction("USER_DELETED", "User", 1L, "System");
    }

    @Test
    void testDeleteUser_NotFound() {
        when(userRepository.existsById(2L)).thenReturn(false);

        assertThrows(RuntimeException.class, () -> userService.deleteUser(2L));

        verify(userRepository, times(1)).existsById(2L);
        verify(userRepository, never()).deleteById(anyLong());
        verify(auditService, never()).logAction(anyString(), anyString(), anyLong(), anyString());
    }
}

UserControllerIntegrationTest.java

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package com.yutao.usermgmt.web;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yutao.usermgmt.domain.User;
import com.yutao.usermgmt.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest // 启动完整的Spring Boot应用上下文
@AutoConfigureMockMvc // 自动配置MockMvc
@ActiveProfiles("test") // 激活测试profile,使用application-test.properties中的H2数据库
@Transactional // 每个测试方法执行完后回滚事务,保证测试隔离性
class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc; // 用于模拟HTTP请求

    @Autowired
    private ObjectMapper objectMapper; // 用于JSON序列化/反序列化

    @Autowired
    private UserRepository userRepository; // 用于在测试前/后准备数据

    @BeforeEach
    void setup() {
        // 清理数据库,确保每个测试方法都在干净的环境中运行
        userRepository.deleteAll();
    }

    @Test
    void testCreateUser_Success() throws Exception {
        User newUser = new User("John Doe", "[email protected]");

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isCreated()) // 期望HTTP 201 Created
                .andExpect(jsonPath("$.id").exists()) // 期望返回的JSON中包含id字段
                .andExpect(jsonPath("$.name").value("John Doe"))
                .andExpect(jsonPath("$.email").value("[email protected]"));
    }

    @Test
    void testCreateUser_ValidationFailure() throws Exception {
        // name为空,email格式不正确
        User invalidUser = new User("", "invalid-email");

        mockMvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(invalidUser)))
                .andExpect(status().isBadRequest()) // 期望HTTP 400 Bad Request
                .andExpect(jsonPath("$.name").value("Name is mandatory")) // 验证name字段的错误消息
                .andExpect(jsonPath("$.email").value("Email should be valid")); // 验证email字段的错误消息
    }

    @Test
    void testGetUserById_Found() throws Exception {
        User existingUser = userRepository.save(new User("Jane Doe", "[email protected]"));

        mockMvc.perform(get("/api/users/{id}", existingUser.getId()))
                .andExpect(status().isOk()) // 期望HTTP 200 OK
                .andExpect(jsonPath("$.id").value(existingUser.getId()))
                .andExpect(jsonPath("$.name").value("Jane Doe"))
                .andExpect(jsonPath("$.email").value("[email protected]"));
    }

    @Test
    void testGetUserById_NotFound() throws Exception {
        mockMvc.perform(get("/api/users/{id}", 999L)) // 查询一个不存在的ID
                .andExpect(status().isNotFound()); // 期望HTTP 404 Not Found
    }

    @Test
    void testUpdateUser_Success() throws Exception {
        User existingUser = userRepository.save(new User("Original Name", "[email protected]"));
        User updatedDetails = new User("Updated Name", "[email protected]");
        updatedDetails.setId(existingUser.getId()); // ID 必须一致

        mockMvc.perform(put("/api/users/{id}", existingUser.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updatedDetails)))
                .andExpect(status().isOk()) // 期望HTTP 200 OK
                .andExpect(jsonPath("$.id").value(existingUser.getId()))
                .andExpect(jsonPath("$.name").value("Updated Name"))
                .andExpect(jsonPath("$.email").value("[email protected]"));
    }

    @Test
    void testDeleteUser_Success() throws Exception {
        User existingUser = userRepository.save(new User("To Be Deleted", "[email protected]"));

        mockMvc.perform(delete("/api/users/{id}", existingUser.getId()))
                .andExpect(status().isNoContent()); // 期望HTTP 204 No Content

        // 验证用户是否真的被删除了
        mockMvc.perform(get("/api/users/{id}", existingUser.getId()))
                .andExpect(status().isNotFound());
    }

    @Test
    void testDeleteUser_NotFound() throws Exception {
        mockMvc.perform(delete("/api/users/{id}", 999L)) // 删除一个不存在的ID
                .andExpect(status().isNotFound()); // 期望HTTP 404 Not Found
    }
}

application-test.properties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 激活测试环境profile
# spring.profiles.active=test # 通常在@ActiveProfiles注解中设置

# H2 内存数据库配置 (用于测试)
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# JPA 针对H2
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop

# 日志级别
logging.level.com.yutao.usermgmt=DEBUG
logging.level.org.springframework=INFO
logging.level.org.hibernate=INFO

完成此项目,即可系统复用并加深对所有 Spring 模块的理解和掌握。

总结

Spring 框架是 Java 生态的基石,必知必会。