背景

本文是《JavaEE 后端从小白到大神》修仙系列第十二篇,也是 JavaEE 体系的收官之作。若想详细学习请点击首篇博文,我们开始吧。

第十二篇:JavaEE 架构实战与项目落地

  • 项目概述:企业任务管理系统;
  • 模块划分与架构设计;
  • 实体层设计与 JPA 实现;
  • EJB 业务层实现;
  • REST API 层实现(JAX-RS);
  • 异步消息与事件驱动(JMS + CDI Event);
  • 安全与权限控制;
  • 单元测试与集成测试;
  • 部署与运维;
  • 架构总结与演进思考。

一、项目概述:企业任务管理系统

1. 项目背景

在现代化企业管理中,任务管理是核心需求之一。本实战项目“企业任务管理系统(Enterprise Task Platform)”旨在构建一个完整的企业级任务管理平台,涵盖用户管理、任务全生命周期管理、消息通知、权限控制、数据统计等核心模块。

2. 功能需求

模块 功能 说明
用户管理 注册、登录、权限管理 支持普通用户和管理员两种角色
任务管理 创建、分配、查询、更新状态、删除 任务全生命周期管理
任务状态 待处理、进行中、已完成、已取消 标准任务状态流转
消息通知 任务创建、状态变更时发送通知 支持邮件/站内信(异步消息)
数据统计 任务完成率、人员负载统计 管理员看板
审计日志 所有操作记录 安全审计

3. 技术栈

层次 技术选型 说明
运行环境 WildFly 32+ Jakarta EE 10 应用服务器
Web 层 JSP + JSTL + EL 管理后台视图
REST API JAX-RS(Jakarta REST) 前后端分离接口
业务层 EJB 3.2(Stateless + Singleton) 无状态会话 Bean + 单例 Bean
依赖注入 CDI 4.0 类型安全注入
持久层 JPA 3.1(Hibernate) ORM 映射
消息服务 JMS 3.1(ActiveMQ Artemis) 异步通知
安全框架 Jakarta Security 3.0 认证 + 授权
事务管理 EJB CMT 声明式事务
构建工具 Maven 3.9 项目构建
测试框架 JUnit 5 + Arquillian 容器内集成测试

二、模块划分与架构设计

1. 项目结构

enterprise-task-platform/
├── pom.xml                           # 父 POM
├── task-common/                      # 公共模块(JAR)
│   ├── pom.xml
│   └── src/main/java/
│       └── com/example/task/common/
│           ├── model/                # 数据传输对象(DTO)
│           │   ├── UserDTO.java
│           │   └── TaskDTO.java
│           ├── exception/            # 自定义异常
│           │   ├── BusinessException.java
│           │   └── ResourceNotFoundException.java
│           └── util/                 # 工具类
│               └── ValidationUtils.java
├── task-ejb/                         # EJB 模块(JAR)
│   ├── pom.xml
│   └── src/main/java/
│       └── com/example/task/ejb/
│           ├── entity/               # JPA 实体
│           │   ├── User.java
│           │   ├── Task.java
│           │   └── AuditLog.java
│           ├── repository/           # 数据访问层
│           │   ├── UserRepository.java
│           │   └── TaskRepository.java
│           ├── service/              # 业务逻辑层
│           │   ├── UserService.java
│           │   ├── TaskService.java
│           │   ├── SystemConfigService.java
│           │   └── AuditService.java
│           ├── mdb/                  # 消息驱动 Bean
│           │   └── NotificationMDB.java
│           ├── event/                # CDI 事件
│           │   ├── TaskCreatedEvent.java
│           │   └── TaskStatusChangedEvent.java
│           └── interceptor/          # 拦截器
│               ├── Logged.java
│               └── LoggingInterceptor.java
├── task-web/                         # Web 模块(WAR)
│   ├── pom.xml
│   └── src/main/
│       ├── java/
│       │   └── com/example/task/web/
│       │       ├── rest/             # JAX-RS 资源
│       │       │   ├── UserResource.java
│       │       │   └── TaskResource.java
│       │       ├── filter/           # Filter
│       │       │   ├── AuthenticationFilter.java
│       │       │   └── CorsFilter.java
│       │       ├── provider/         # ExceptionMapper
│       │       │   └── GlobalExceptionMapper.java
│       │       └── config/
│       │           └── ApplicationConfig.java
│       ├── webapp/
│       │   ├── WEB-INF/
│       │   │   ├── web.xml
│       │   │   ├── views/            # JSP 页面
│       │   │   │   ├── login.jsp
│       │   │   │   ├── dashboard.jsp
│       │   │   │   ├── task/
│       │   │   │   │   ├── list.jsp
│       │   │   │   │   └── detail.jsp
│       │   │   │   └── admin/
│       │   │   │       └── users.jsp
│       │   │   └── beans.xml
│       │   ├── index.html
│       │   ├── css/
│       │   └── js/
│       └── resources/
│           └── META-INF/
│               └── persistence.xml
└── task-ear/                         # EAR 模块
    ├── pom.xml
    └── src/main/application/
        └── META-INF/
            └── application.xml

2. 架构分层图

graph TB
    subgraph 客户端层
        A[浏览器 / 移动端 / API 调用方]
    end

    subgraph Web 层
        B[JSP + JSTL 管理后台]
        C[JAX-RS REST API]
        F[Filter 认证拦截]
        G[GlobalExceptionMapper]
    end

    subgraph EJB 业务层
        D[UserService]
        E[TaskService]
        H[AuditService]
        I[SystemConfigService]
    end

    subgraph 异步层
        J[CDI Event]
        K[NotificationMDB]
    end

    subgraph 数据层
        L[UserRepository]
        M[TaskRepository]
        N[EntityManager + JPA]
    end

    subgraph 基础设施
        O[(MySQL)]
        P[(ActiveMQ Artemis)]
    end

    A --> B
    A --> C
    B --> D
    C --> D
    D --> L
    E --> M
    D --> J
    E --> J
    J --> K
    K --> P
    L --> N
    M --> N
    N --> O
    F --> C
    G --> C
    I --> D
    I --> E
    H --> N

三、实体层设计与 JPA 实现

1. User 实体(用户)

 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.example.task.ejb.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "t_user")
@NamedQueries({
    @NamedQuery(name = "User.findByUsername", 
                query = "SELECT u FROM User u WHERE u.username = :username"),
    @NamedQuery(name = "User.findByEmail", 
                query = "SELECT u FROM User u WHERE u.email = :email"),
    @NamedQuery(name = "User.findAll", 
                query = "SELECT u FROM User u ORDER BY u.id")
})
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank
    @Size(min = 3, max = 50)
    @Column(nullable = false, unique = true, length = 50)
    private String username;
    
    @NotBlank
    @Size(min = 6, max = 100)
    @Column(nullable = false)
    private String password;  // 实际应存储加密后的密码
    
    @Email
    @NotBlank
    @Column(nullable = false, unique = true, length = 100)
    private String email;
    
    @Column(length = 50)
    private String fullName;
    
    @Column(nullable = false)
    private String role = "USER";  // USER, ADMIN
    
    @Column(nullable = false)
    private boolean active = true;
    
    @Column(name = "create_time", nullable = false, updatable = false)
    private LocalDateTime createTime;
    
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    
    @OneToMany(mappedBy = "assignee", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Task> assignedTasks = new ArrayList<>();
    
    @OneToMany(mappedBy = "creator", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Task> createdTasks = new ArrayList<>();
    
    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
        updateTime = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updateTime = LocalDateTime.now();
    }
    
    // getter/setter 省略
}

2. Task 实体(任务)

  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
package com.example.task.ejb.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;

@Entity
@Table(name = "t_task")
@NamedQueries({
    @NamedQuery(name = "Task.findByAssignee", 
                query = "SELECT t FROM Task t WHERE t.assignee.id = :userId ORDER BY t.createTime DESC"),
    @NamedQuery(name = "Task.findByCreator", 
                query = "SELECT t FROM Task t WHERE t.creator.id = :userId ORDER BY t.createTime DESC"),
    @NamedQuery(name = "Task.findByStatus", 
                query = "SELECT t FROM Task t WHERE t.status = :status ORDER BY t.createTime DESC"),
    @NamedQuery(name = "Task.countByStatus", 
                query = "SELECT COUNT(t) FROM Task t WHERE t.status = :status"),
    @NamedQuery(name = "Task.findOverdue", 
                query = "SELECT t FROM Task t WHERE t.dueDate < :now AND t.status != 'COMPLETED'")
})
public class Task {
    
    public enum Status {
        PENDING("待处理"),
        IN_PROGRESS("进行中"),
        COMPLETED("已完成"),
        CANCELLED("已取消");
        
        private final String label;
        Status(String label) { this.label = label; }
        public String getLabel() { return label; }
    }
    
    public enum Priority {
        LOW("低"),
        MEDIUM("中"),
        HIGH("高"),
        URGENT("紧急");
        
        private final String label;
        Priority(String label) { this.label = label; }
        public String getLabel() { return label; }
    }
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank
    @Size(min = 1, max = 200)
    @Column(nullable = false, length = 200)
    private String title;
    
    @Size(max = 2000)
    @Column(length = 2000)
    private String description;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Status status = Status.PENDING;
    
    @Enumerated(EnumType.STRING)
    private Priority priority = Priority.MEDIUM;
    
    @Column(name = "due_date")
    private LocalDateTime dueDate;
    
    @Column(name = "create_time", nullable = false, updatable = false)
    private LocalDateTime createTime;
    
    @Column(name = "update_time")
    private LocalDateTime updateTime;
    
    @Column(name = "complete_time")
    private LocalDateTime completeTime;
    
    @ManyToOne
    @JoinColumn(name = "assignee_id")
    private User assignee;  // 被分配人
    
    @ManyToOne
    @JoinColumn(name = "creator_id", nullable = false)
    private User creator;  // 创建人
    
    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
        updateTime = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updateTime = LocalDateTime.now();
        if (status == Status.COMPLETED && completeTime == null) {
            completeTime = LocalDateTime.now();
        }
    }
    
    // getter/setter 省略
}

3. AuditLog 实体(审计日志)

 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.example.task.ejb.entity;

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

@Entity
@Table(name = "t_audit_log")
@NamedQuery(name = "AuditLog.findByUser", 
            query = "SELECT a FROM AuditLog a WHERE a.userId = :userId ORDER BY a.createTime DESC")
public class AuditLog {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "user_id")
    private Long userId;
    
    @Column(name = "username")
    private String username;
    
    @Column(nullable = false)
    private String action;  // 操作类型
    
    @Column(length = 2000)
    private String detail;  // 操作详情
    
    @Column(name = "ip_address", length = 50)
    private String ipAddress;
    
    @Column(name = "create_time", nullable = false)
    private LocalDateTime createTime;
    
    @PrePersist
    protected void onCreate() {
        createTime = LocalDateTime.now();
    }
    
    // getter/setter 省略
}

4. persistence.xml 配置

 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
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
             https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"
             version="3.0">
    
    <persistence-unit name="taskPU" transaction-type="JTA">
        <jta-data-source>java:jboss/datasources/TaskDS</jta-data-source>
        
        <properties>
            <!-- 自动建表/更新表结构 -->
            <property name="jakarta.persistence.schema-generation.database.action" 
                      value="update"/>
            
            <!-- 显示 SQL -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            
            <!-- 二级缓存 -->
            <property name="hibernate.cache.use_second_level_cache" value="true"/>
            <property name="hibernate.cache.region.factory_class" 
                      value="org.hibernate.cache.jcache.JCacheRegionFactory"/>
            
            <!-- 事务隔离级别 -->
            <property name="hibernate.connection.isolation" value="2"/>  <!-- READ_COMMITTED -->
        </properties>
    </persistence-unit>
</persistence>

四、EJB 业务层实现

1. UserService(用户服务)

  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
package com.example.task.ejb.service;

import com.example.task.ejb.entity.User;
import com.example.task.ejb.entity.AuditLog;
import com.example.task.common.exception.ResourceNotFoundException;
import com.example.task.common.exception.BusinessException;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import java.util.List;

@Stateless
public class UserService {
    
    @PersistenceContext
    private EntityManager em;
    
    @Inject
    private AuditService auditService;
    
    // 注册用户(不需要认证)
    @PermitAll
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public User registerUser(String username, String email, String password, String fullName) {
        // 检查用户名是否已存在
        List<User> existing = em.createNamedQuery("User.findByUsername", User.class)
                .setParameter("username", username)
                .getResultList();
        if (!existing.isEmpty()) {
            throw new BusinessException("用户名已被占用");
        }
        
        User user = new User();
        user.setUsername(username);
        user.setEmail(email);
        user.setPassword(password);  // 生产环境需加密
        user.setFullName(fullName);
        user.setRole("USER");
        user.setActive(true);
        
        em.persist(user);
        auditService.log(user.getId(), username, "REGISTER", "用户注册成功");
        return user;
    }
    
    // 查询所有用户(仅管理员)
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<User> findAllUsers() {
        return em.createNamedQuery("User.findAll", User.class)
                 .getResultList();
    }
    
    // 根据 ID 查询用户
    @PermitAll
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public User findUserById(Long id) {
        User user = em.find(User.class, id);
        if (user == null) {
            throw new ResourceNotFoundException("用户不存在,ID:" + id);
        }
        return user;
    }
    
    // 根据用户名查询用户
    @PermitAll
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public User findByUsername(String username) {
        return em.createNamedQuery("User.findByUsername", User.class)
                .setParameter("username", username)
                .getSingleResult();
    }
    
    // 更新用户(仅管理员)
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public User updateUser(Long id, String email, String fullName, String role, Boolean active) {
        User user = findUserById(id);
        if (email != null) user.setEmail(email);
        if (fullName != null) user.setFullName(fullName);
        if (role != null) user.setRole(role);
        if (active != null) user.setActive(active);
        
        auditService.log(id, user.getUsername(), "UPDATE_USER", "用户信息已更新");
        return user;
    }
    
    // 删除用户(仅管理员)
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteUser(Long id) {
        User user = findUserById(id);
        String username = user.getUsername();
        em.remove(user);
        auditService.log(id, username, "DELETE_USER", "用户已被删除");
    }
}

2. TaskService(任务服务)

  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
package com.example.task.ejb.service;

import com.example.task.ejb.entity.Task;
import com.example.task.ejb.entity.User;
import com.example.task.common.exception.ResourceNotFoundException;
import com.example.task.common.exception.BusinessException;
import com.example.task.ejb.event.TaskCreatedEvent;
import com.example.task.ejb.event.TaskStatusChangedEvent;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import jakarta.enterprise.event.Event;
import java.time.LocalDateTime;
import java.util.List;

@Stateless
public class TaskService {
    
    @PersistenceContext
    private EntityManager em;
    
    @Inject
    private UserService userService;
    
    @Inject
    private AuditService auditService;
    
    @Inject
    private Event<TaskCreatedEvent> taskCreatedEvent;      // CDI 事件
    
    @Inject
    private Event<TaskStatusChangedEvent> taskStatusEvent; // CDI 事件
    
    // 创建任务
    @RolesAllowed({"USER", "ADMIN"})
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Task createTask(String title, String description, 
                           Long assigneeId, String priority, LocalDateTime dueDate,
                           Long creatorId) {
        User creator = userService.findUserById(creatorId);
        User assignee = userService.findUserById(assigneeId);
        
        Task task = new Task();
        task.setTitle(title);
        task.setDescription(description);
        task.setAssignee(assignee);
        task.setCreator(creator);
        task.setPriority(Task.Priority.valueOf(priority));
        task.setDueDate(dueDate);
        task.setStatus(Task.Status.PENDING);
        
        em.persist(task);
        
        auditService.log(creatorId, creator.getUsername(), "CREATE_TASK", 
                        "创建任务:" + title + ",分配给:" + assignee.getUsername());
        
        // 发布 CDI 事件(异步通知)
        taskCreatedEvent.fire(new TaskCreatedEvent(task.getId(), title, 
                                                   assignee.getId(), creator.getId()));
        
        return task;
    }
    
    // 查询分配给当前用户的任务
    @RolesAllowed({"USER", "ADMIN"})
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Task> findTasksByAssignee(Long userId) {
        return em.createNamedQuery("Task.findByAssignee", Task.class)
                .setParameter("userId", userId)
                .getResultList();
    }
    
    // 查询由当前用户创建的任务
    @RolesAllowed({"USER", "ADMIN"})
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Task> findTasksByCreator(Long userId) {
        return em.createNamedQuery("Task.findByCreator", Task.class)
                .setParameter("userId", userId)
                .getResultList();
    }
    
    // 查询所有任务(仅管理员)
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Task> findAllTasks(int page, int size) {
        return em.createQuery("SELECT t FROM Task t ORDER BY t.createTime DESC", Task.class)
                .setFirstResult((page - 1) * size)
                .setMaxResults(size)
                .getResultList();
    }
    
    // 更新任务状态
    @RolesAllowed({"USER", "ADMIN"})
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Task updateTaskStatus(Long taskId, String newStatus, Long operatorId) {
        Task task = findTaskById(taskId);
        User operator = userService.findUserById(operatorId);
        Task.Status oldStatus = task.getStatus();
        Task.Status status = Task.Status.valueOf(newStatus);
        
        // 验证状态流转合法性
        if (task.getStatus() == Task.Status.COMPLETED || 
            task.getStatus() == Task.Status.CANCELLED) {
            throw new BusinessException("任务已完成或已取消,无法更改状态");
        }
        
        task.setStatus(status);
        
        auditService.log(operatorId, operator.getUsername(), "UPDATE_TASK_STATUS",
                        "任务 " + task.getTitle() + " 状态从 " + oldStatus + " 变更为 " + status);
        
        // 发布状态变更事件
        taskStatusEvent.fire(new TaskStatusChangedEvent(taskId, task.getTitle(), 
                                                        oldStatus.toString(), newStatus,
                                                        task.getAssignee().getId()));
        
        return task;
    }
    
    // 删除任务(仅管理员)
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteTask(Long taskId, Long operatorId) {
        Task task = findTaskById(taskId);
        User operator = userService.findUserById(operatorId);
        
        em.remove(task);
        auditService.log(operatorId, operator.getUsername(), "DELETE_TASK",
                        "删除任务:" + task.getTitle());
    }
    
    // 按状态统计任务数量
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public long countTasksByStatus(String status) {
        return em.createNamedQuery("Task.countByStatus", Long.class)
                .setParameter("status", Task.Status.valueOf(status))
                .getSingleResult();
    }
    
    // 查询超时未完成的任务
    @RolesAllowed("ADMIN")
    @TransactionAttribute(TransactionAttributeType.SUPPORTS)
    public List<Task> findOverdueTasks() {
        return em.createNamedQuery("Task.findOverdue", Task.class)
                .setParameter("now", LocalDateTime.now())
                .getResultList();
    }
    
    private Task findTaskById(Long id) {
        Task task = em.find(Task.class, id);
        if (task == null) {
            throw new ResourceNotFoundException("任务不存在,ID:" + id);
        }
        return task;
    }
}

3. SystemConfigService(系统配置——单例)

 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
package com.example.task.ejb.service;

import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap;

@Singleton
@Startup
public class SystemConfigService {
    
    private ConcurrentHashMap<String, String> configs;
    
    @PostConstruct
    public void init() {
        configs = new ConcurrentHashMap<>();
        configs.put("system.name", "企业任务管理平台");
        configs.put("system.version", "1.0.0");
        configs.put("task.max.per.user", "20");
        configs.put("notification.enabled", "true");
        System.out.println("系统配置服务初始化完成");
    }
    
    @Lock(LockType.READ)
    public String getConfig(String key) {
        return configs.getOrDefault(key, "");
    }
    
    @Lock(LockType.READ)
    public String getConfig(String key, String defaultValue) {
        return configs.getOrDefault(key, defaultValue);
    }
    
    @Lock(LockType.WRITE)
    public void setConfig(String key, String value) {
        configs.put(key, value);
    }
    
    @Lock(LockType.READ)
    public int getMaxTaskPerUser() {
        return Integer.parseInt(configs.getOrDefault("task.max.per.user", "20"));
    }
}

4. AuditService(审计服务——独立事务)

 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.example.task.ejb.service;

import com.example.task.ejb.entity.AuditLog;
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@Stateless
public class AuditService {
    
    @PersistenceContext
    private EntityManager em;
    
    // 独立事务:外层回滚不影响审计日志
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void log(Long userId, String username, String action, String detail) {
        AuditLog log = new AuditLog();
        log.setUserId(userId);
        log.setUsername(username);
        log.setAction(action);
        log.setDetail(detail);
        em.persist(log);
    }
}

五、REST API 层实现(JAX-RS)

1. ApplicationConfig(JAX-RS 配置)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.task.web.config;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;

@ApplicationPath("/api")
public class ApplicationConfig extends Application {
    
    @Override
    public Set<Class<?>> getClasses() {
        Set<Class<?>> classes = new HashSet<>();
        // 资源类
        classes.add(UserResource.class);
        classes.add(TaskResource.class);
        // Provider
        classes.add(GlobalExceptionMapper.class);
        classes.add(AuthenticationFilter.class);
        classes.add(CorsFilter.class);
        return classes;
    }
}

2. UserResource(用户 REST API)

 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
package com.example.task.web.rest;

import com.example.task.ejb.entity.User;
import com.example.task.ejb.service.UserService;
import com.example.task.common.dto.UserDTO;
import jakarta.ejb.EJB;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import java.util.List;

@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "用户管理")
public class UserResource {
    
    @EJB
    private UserService userService;
    
    // 注册(公开接口)
    @POST
    @PermitAll
    @Path("/register")
    @Operation(summary = "用户注册")
    public Response register(UserDTO.RegisterRequest request) {
        User user = userService.registerUser(
            request.getUsername(),
            request.getEmail(),
            request.getPassword(),
            request.getFullName()
        );
        return Response.status(Response.Status.CREATED)
                .entity(UserDTO.fromEntity(user))
                .build();
    }
    
    // 查询所有用户(仅管理员)
    @GET
    @RolesAllowed("ADMIN")
    @Operation(summary = "查询所有用户")
    public Response listUsers() {
        List<User> users = userService.findAllUsers();
        return Response.ok(users.stream().map(UserDTO::fromEntity).toList())
                .build();
    }
    
    // 查询单个用户
    @GET
    @Path("/{id}")
    @RolesAllowed({"USER", "ADMIN"})
    @Operation(summary = "根据ID查询用户")
    public Response getUser(@PathParam("id") Long id) {
        User user = userService.findUserById(id);
        return Response.ok(UserDTO.fromEntity(user)).build();
    }
}

3. TaskResource(任务 REST API)

  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
package com.example.task.web.rest;

import com.example.task.ejb.entity.Task;
import com.example.task.ejb.service.TaskService;
import com.example.task.common.dto.TaskDTO;
import jakarta.ejb.EJB;
import jakarta.security.enterprise.SecurityContext;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import java.time.LocalDateTime;
import java.util.List;

@Path("/tasks")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "任务管理")
public class TaskResource {
    
    @EJB
    private TaskService taskService;
    
    @Inject
    private SecurityContext securityContext;
    
    // 创建任务
    @POST
    @RolesAllowed({"USER", "ADMIN"})
    @Operation(summary = "创建任务")
    public Response createTask(TaskDTO.CreateRequest request) {
        Long currentUserId = getCurrentUserId();
        Task task = taskService.createTask(
            request.getTitle(),
            request.getDescription(),
            request.getAssigneeId(),
            request.getPriority(),
            request.getDueDate(),
            currentUserId
        );
        return Response.status(Response.Status.CREATED)
                .entity(TaskDTO.fromEntity(task))
                .build();
    }
    
    // 查询当前用户的任务
    @GET
    @RolesAllowed({"USER", "ADMIN"})
    @Path("/my-tasks")
    @Operation(summary = "查询当前用户的任务")
    public Response getMyTasks() {
        Long currentUserId = getCurrentUserId();
        List<Task> tasks = taskService.findTasksByAssignee(currentUserId);
        return Response.ok(tasks.stream().map(TaskDTO::fromEntity).toList())
                .build();
    }
    
    // 查询所有任务(仅管理员)
    @GET
    @RolesAllowed("ADMIN")
    @Operation(summary = "查询所有任务")
    public Response listTasks(@QueryParam("page") @DefaultValue("1") int page,
                              @QueryParam("size") @DefaultValue("20") int size) {
        List<Task> tasks = taskService.findAllTasks(page, size);
        return Response.ok(tasks.stream().map(TaskDTO::fromEntity).toList())
                .build();
    }
    
    // 更新任务状态
    @PUT
    @Path("/{id}/status")
    @RolesAllowed({"USER", "ADMIN"})
    @Operation(summary = "更新任务状态")
    public Response updateStatus(@PathParam("id") Long id, 
                                  TaskDTO.StatusUpdateRequest request) {
        Long currentUserId = getCurrentUserId();
        Task task = taskService.updateTaskStatus(id, request.getStatus(), currentUserId);
        return Response.ok(TaskDTO.fromEntity(task)).build();
    }
    
    // 删除任务(仅管理员)
    @DELETE
    @Path("/{id}")
    @RolesAllowed("ADMIN")
    @Operation(summary = "删除任务")
    public Response deleteTask(@PathParam("id") Long id) {
        Long currentUserId = getCurrentUserId();
        taskService.deleteTask(id, currentUserId);
        return Response.noContent().build();
    }
    
    // 统计任务(仅管理员)
    @GET
    @RolesAllowed("ADMIN")
    @Path("/stats")
    @Operation(summary = "任务统计")
    public Response getStats() {
        long pending = taskService.countTasksByStatus("PENDING");
        long inProgress = taskService.countTasksByStatus("IN_PROGRESS");
        long completed = taskService.countTasksByStatus("COMPLETED");
        long cancelled = taskService.countTasksByStatus("CANCELLED");
        return Response.ok(new TaskDTO.Stats(pending, inProgress, completed, cancelled))
                .build();
    }
    
    // 获取当前用户 ID
    private Long getCurrentUserId() {
        String username = securityContext.getCallerPrincipal().getName();
        // 实际应查询 UserService 根据用户名获取 ID
        // 此处简化,实际通过 SecurityContext 获取
        return 1L;
    }
}

六、异步消息与事件驱动

1. CDI 事件定义

 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
package com.example.task.ejb.event;

public class TaskCreatedEvent {
    private Long taskId;
    private String taskTitle;
    private Long assigneeId;
    private Long creatorId;
    private LocalDateTime createTime;
    
    public TaskCreatedEvent(Long taskId, String taskTitle, 
                            Long assigneeId, Long creatorId) {
        this.taskId = taskId;
        this.taskTitle = taskTitle;
        this.assigneeId = assigneeId;
        this.creatorId = creatorId;
        this.createTime = LocalDateTime.now();
    }
    // getter 省略
}

public class TaskStatusChangedEvent {
    private Long taskId;
    private String taskTitle;
    private String oldStatus;
    private String newStatus;
    private Long assigneeId;
    private LocalDateTime changeTime;
    
    public TaskStatusChangedEvent(Long taskId, String taskTitle, 
                                   String oldStatus, String newStatus, Long assigneeId) {
        this.taskId = taskId;
        this.taskTitle = taskTitle;
        this.oldStatus = oldStatus;
        this.newStatus = newStatus;
        this.assigneeId = assigneeId;
        this.changeTime = LocalDateTime.now();
    }
    // getter 省略
}

2. CDI 事件监听器(同步 + 异步)

 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
package com.example.task.ejb.listener;

import com.example.task.ejb.event.TaskCreatedEvent;
import com.example.task.ejb.event.TaskStatusChangedEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import jakarta.inject.Inject;
import jakarta.jms.*;

@ApplicationScoped
public class TaskNotificationListener {
    
    @Inject
    private JMSContext jmsContext;
    
    @Resource(lookup = "java:jboss/queue/taskNotification")
    private Queue notificationQueue;
    
    // 同步监听:记录日志(快速执行)
    public void onTaskCreated(@Observes TaskCreatedEvent event) {
        System.out.println("[通知] 任务创建:" + event.getTaskTitle() + 
                           ",分配给:" + event.getAssigneeId());
    }
    
    // 异步监听:发送 JMS 消息(耗时操作,不阻塞主流程)
    public void sendNotificationAsync(@ObservesAsync TaskCreatedEvent event) {
        try {
            String message = String.format(
                "新任务通知:任务【%s】已分配给用户 %d,请及时处理。",
                event.getTaskTitle(), event.getAssigneeId()
            );
            TextMessage msg = jmsContext.createTextMessage(message);
            msg.setStringProperty("eventType", "TASK_CREATED");
            msg.setLongProperty("taskId", event.getTaskId());
            jmsContext.createProducer().send(notificationQueue, msg);
            System.out.println("[JMS] 任务创建通知已发送到消息队列");
        } catch (Exception e) {
            System.err.println("发送 JMS 消息失败:" + e.getMessage());
        }
    }
    
    // 同步监听状态变更
    public void onTaskStatusChanged(@Observes TaskStatusChangedEvent event) {
        System.out.println("[通知] 任务状态变更:" + event.getTaskTitle() + 
                           " [" + event.getOldStatus() + " → " + event.getNewStatus() + "]");
    }
}

3. 消息驱动 Bean(MDB)

 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
package com.example.task.ejb.mdb;

import jakarta.ejb.MessageDriven;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import jakarta.jms.TextMessage;
import jakarta.jms.JMSException;
import jakarta.ejb.ActivationConfigProperty;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import jakarta.annotation.Resource;

@MessageDriven(
    activationConfig = {
        @ActivationConfigProperty(
            propertyName = "destinationType",
            propertyValue = "jakarta.jms.Queue"
        ),
        @ActivationConfigProperty(
            propertyName = "destination",
            propertyValue = "queue/taskNotification"
        ),
        @ActivationConfigProperty(
            propertyName = "acknowledgeMode",
            propertyValue = "Auto-acknowledge"
        )
    }
)
public class NotificationMDB implements MessageListener {
    
    @Resource(lookup = "java:jboss/mail/Default")
    private Session mailSession;
    
    @Override
    public void onMessage(Message message) {
        try {
            if (message instanceof TextMessage) {
                TextMessage textMsg = (TextMessage) message;
                String content = textMsg.getText();
                String eventType = textMsg.getStringProperty("eventType");
                Long taskId = textMsg.getLongProperty("taskId");
                
                System.out.println("[MDB] 收到消息:类型=" + eventType + 
                                   ",任务ID=" + taskId + ",内容=" + content);
                
                // 模拟发送邮件
                // sendEmail("[email protected]", "任务通知", content);
            }
        } catch (JMSException e) {
            System.err.println("处理 JMS 消息失败:" + e.getMessage());
            throw new RuntimeException("消息处理失败", e);
        }
    }
    
    // 邮件发送(示例)
    private void sendEmail(String to, String subject, String body) {
        try {
            MimeMessage msg = new MimeMessage(mailSession);
            msg.setFrom(new InternetAddress("[email protected]"));
            msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
            msg.setSubject(subject);
            msg.setText(body);
            Transport.send(msg);
            System.out.println("[邮件] 已发送邮件到:" + to);
        } catch (MessagingException e) {
            System.err.println("邮件发送失败:" + e.getMessage());
        }
    }
}

七、安全与权限控制

1. 认证过滤器

 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
package com.example.task.web.filter;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

import java.io.IOException;
import java.util.Base64;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
    
    private static final String AUTH_HEADER = "Authorization";
    private static final String BASIC_PREFIX = "Basic ";
    
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        // 跳过 OPTIONS 预检请求
        if ("OPTIONS".equalsIgnoreCase(requestContext.getMethod())) {
            return;
        }
        
        // 跳过公开接口
        String path = requestContext.getUriInfo().getPath();
        if (path.contains("/users/register") || path.contains("/health")) {
            return;
        }
        
        String authHeader = requestContext.getHeaderString(AUTH_HEADER);
        if (authHeader == null || !authHeader.startsWith(BASIC_PREFIX)) {
            requestContext.abortWith(
                Response.status(Response.Status.UNAUTHORIZED)
                    .entity("{\"error\":\"未提供认证信息\"}")
                    .build()
            );
            return;
        }
        
        // 验证用户名/密码(实际应使用 SecurityContext)
        String credentials = new String(
            Base64.getDecoder().decode(authHeader.substring(BASIC_PREFIX.length()))
        );
        String[] parts = credentials.split(":");
        if (parts.length != 2) {
            requestContext.abortWith(
                Response.status(Response.Status.UNAUTHORIZED)
                    .entity("{\"error\":\"认证格式错误\"}")
                    .build()
            );
        }
        // 实际验证逻辑...
    }
}

2. CORS 过滤器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.task.web.filter;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerResponseContext;
import jakarta.ws.rs.container.ContainerResponseFilter;
import jakarta.ws.rs.ext.Provider;

import java.io.IOException;

@Provider
public class CorsFilter implements ContainerResponseFilter {
    
    @Override
    public void filter(ContainerRequestContext request, 
                       ContainerResponseContext response) throws IOException {
        response.getHeaders().add("Access-Control-Allow-Origin", "*");
        response.getHeaders().add("Access-Control-Allow-Methods", 
                                   "GET, POST, PUT, DELETE, OPTIONS");
        response.getHeaders().add("Access-Control-Allow-Headers", 
                                   "Content-Type, Authorization");
        response.getHeaders().add("Access-Control-Max-Age", "3600");
    }
}

3. 全局异常处理

 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
package com.example.task.web.provider;

import com.example.task.common.exception.BusinessException;
import com.example.task.common.exception.ResourceNotFoundException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import jakarta.validation.ConstraintViolationException;

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

@Provider
public class GlobalExceptionMapper implements ExceptionMapper<Throwable> {
    
    @Override
    public Response toResponse(Throwable exception) {
        Map<String, Object> error = new HashMap<>();
        error.put("timestamp", System.currentTimeMillis());
        
        if (exception instanceof ResourceNotFoundException) {
            error.put("code", 404);
            error.put("error", "Not Found");
            error.put("message", exception.getMessage());
            return Response.status(Response.Status.NOT_FOUND)
                    .entity(error)
                    .build();
        }
        
        if (exception instanceof BusinessException) {
            error.put("code", 400);
            error.put("error", "Bad Request");
            error.put("message", exception.getMessage());
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(error)
                    .build();
        }
        
        if (exception instanceof ConstraintViolationException) {
            error.put("code", 400);
            error.put("error", "Validation Failed");
            error.put("message", "参数校验失败");
            return Response.status(Response.Status.BAD_REQUEST)
                    .entity(error)
                    .build();
        }
        
        // 未知异常
        error.put("code", 500);
        error.put("error", "Internal Server Error");
        error.put("message", "系统内部错误");
        exception.printStackTrace();  // 记录日志
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                .entity(error)
                .build();
    }
}

八、单元测试与集成测试

1. 单元测试(JUnit 5 + Mockito)

 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
package com.example.task.ejb.service;

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 jakarta.persistence.EntityManager;

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

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private EntityManager em;
    
    @InjectMocks
    private UserService userService;
    
    private User testUser;
    
    @BeforeEach
    void setUp() {
        testUser = new User();
        testUser.setId(1L);
        testUser.setUsername("testuser");
        testUser.setEmail("[email protected]");
    }
    
    @Test
    void testFindUserById_WhenUserExists_ShouldReturnUser() {
        when(em.find(User.class, 1L)).thenReturn(testUser);
        
        User found = userService.findUserById(1L);
        
        assertNotNull(found);
        assertEquals("testuser", found.getUsername());
        verify(em, times(1)).find(User.class, 1L);
    }
    
    @Test
    void testFindUserById_WhenUserNotExists_ShouldThrowException() {
        when(em.find(User.class, 999L)).thenReturn(null);
        
        assertThrows(ResourceNotFoundException.class, 
                    () -> userService.findUserById(999L));
    }
}

2. 集成测试(Arquillian)

 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
package com.example.task.ejb.service;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit5.ArquillianExtension;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import jakarta.inject.Inject;
import jakarta.ejb.EJB;

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

@ExtendWith(ArquillianExtension.class)
public class TaskServiceIntegrationTest {
    
    @Deployment
    public static JavaArchive createDeployment() {
        return ShrinkWrap.create(JavaArchive.class)
            .addPackages(true, "com.example.task.ejb")
            .addAsResource("META-INF/persistence.xml")
            .addAsManifestResource("META-INF/beans.xml");
    }
    
    @EJB
    private TaskService taskService;
    
    @EJB
    private UserService userService;
    
    @Test
    void testCreateAndFindTask() {
        // 创建用户
        User user = userService.registerUser("testuser", "[email protected]", "password", "Test User");
        assertNotNull(user.getId());
        
        // 创建任务
        Task task = taskService.createTask(
            "测试任务", "这是一个集成测试任务",
            user.getId(), "MEDIUM", null, user.getId()
        );
        assertNotNull(task.getId());
        assertEquals("测试任务", task.getTitle());
        assertEquals("PENDING", task.getStatus().toString());
        
        // 查询任务
        List<Task> tasks = taskService.findTasksByAssignee(user.getId());
        assertFalse(tasks.isEmpty());
        assertTrue(tasks.stream().anyMatch(t -> t.getId().equals(task.getId())));
    }
}

九、部署与运维

1. 数据源配置(WildFly)

在 WildFly 中配置数据源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- standalone.xml 或通过 CLI 配置 -->
<datasource jndi-name="java:jboss/datasources/TaskDS" 
            pool-name="TaskPool" enabled="true">
    <connection-url>jdbc:mysql://localhost:3306/taskdb</connection-url>
    <driver>mysql</driver>
    <security>
        <user-name>task_user</user-name>
        <password>task_password</password>
    </security>
    <validation>
        <valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.mysql.MySQLValidConnectionChecker"/>
    </validation>
</datasource>

2. JMS 队列配置

1
2
3
4
5
<!-- standalone.xml -->
<jms-queue name="taskNotification">
    <entry name="queue/taskNotification"/>
    <entry name="java:jboss/queue/taskNotification"/>
</jms-queue>

3. 构建与部署命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 1. 构建项目
mvn clean package

# 2. 启动 WildFly
./standalone.sh -b 0.0.0.0

# 3. 部署 EAR
./jboss-cli.sh --connect command="deploy task-ear/target/task-ear-1.0.0.ear --force"

# 4. 验证部署
curl http://localhost:8080/task/api/health/live
curl http://localhost:8080/task/api/tasks

# 5. 查看日志
tail -f standalone/log/server.log

# 6. 卸载应用
./jboss-cli.sh --connect command="undeploy task-ear-1.0.0.ear"

4. Docker 部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM quay.io/wildfly/wildfly:latest

# 复制数据源配置
COPY standalone.xml /opt/jboss/wildfly/standalone/configuration/

# 复制应用
COPY task-ear/target/task-ear-1.0.0.ear /opt/jboss/wildfly/standalone/deployments/

# 暴露端口
EXPOSE 8080 9990

CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0", "-bmanagement", "0.0.0.0"]
1
2
docker build -t task-platform:1.0.0 .
docker run -p 8080:8080 -p 9990:9990 task-platform:1.0.0

十、架构总结与演进思考

1. 架构设计总结

graph TD
    subgraph 设计原则
        A[分层架构]
        B[依赖倒置]
        C[单一职责]
        D[开闭原则]
    end

    subgraph 核心模式
        E[EJB 业务逻辑]
        F[JPA 数据持久化]
        G[JAX-RS REST API]
        H[CDI 事件驱动]
        I[JMS 异步消息]
        J[声明式安全]
    end

    subgraph 质量属性
        K[可扩展性]
        L[可维护性]
        M[性能]
        N[安全性]
        O[可测试性]
    end

2. 本项目的核心技术实践

技术 应用方式
EJB(Stateless) 核心业务逻辑,容器管理事务(CMT)
EJB(Singleton) 全局系统配置,@Startup 启动时加载
JPA 实体映射,@NamedQuery 查询,二级缓存
JAX-RS RESTful API,@Path 资源,异常映射
CDI @Inject 依赖注入,@Qualifier 区分实现,事件发布/监听
JMS + MDB 异步消息通知,解耦业务逻辑
Jakarta Security @RolesAllowed 声明式授权
JTA 分布式事务,CMT 自动管理

3. 技术演进建议

方向 演进方案 收益
轻量化运行时 从 WildFly 迁移到 Quarkus 启动速度提升 10 倍,内存降低 70%
前后端分离 用 Vue 3 + Element Plus 替换 JSP 提升前端开发效率和用户体验
服务拆分 将任务、用户、通知拆分为独立服务 提高可伸缩性和团队并行开发能力
容器化 完善 Docker 镜像和 Kubernetes 编排 标准化部署,弹性伸缩
可观测性 集成 MicroProfile Telemetry 分布式追踪、指标监控
CI/CD 配置 GitLab CI / GitHub Actions 自动化测试和部署

4. 关键设计决策说明

决策 理由
为什么使用 EAR 而非 WAR? 需要包含 EJB 模块(JAR)和 Web 模块(WAR),EAR 提供统一部署和事务上下文
为什么使用 CMT 而非 BMT? 声明式事务代码最简洁,容器自动管理事务边界,减少人为错误
为什么使用 JMS 而非直接 CDI 事件发送邮件? JMS 提供持久化和重试机制,即使消费者暂时不可用也能保证消息可靠传递
为什么使用 @RolesAllowed 而非编程式安全? 声明式安全更清晰,与业务逻辑分离,便于维护和审计
为什么审计日志使用 REQUIRES_NEW? 即使主业务事务回滚,审计日志也能保留,便于问题追踪和安全审计

5. 学习路径回顾

纵观整个 JavaEE 学习体系,已经完成了从入门到架构师的系统成长:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
第1篇   → JavaEE 概述与体系总览
第2篇   → Servlet 深入理解与实战
第3篇   → JSP 与 JSTL 表示层开发
第4篇   → JDBC 与 JPA(持久化层)
第5篇   → EJB(企业级组件)深入理解
第6篇   → JMS(消息服务)与异步通信
第7篇   → JAX-RS(RESTful Web Service)实战
第8篇   → CDI(上下文依赖注入)机制
第9篇   → 安全与事务管理
第10篇  → JavaEE 应用服务器与容器原理
第11篇  → Jakarta EE 9+ 现代化与云原生转型
第12篇  → JavaEE 架构实战与项目落地 ✅

总结

1. 核心知识脉络

本实战项目综合运用了前 11 篇的全部知识,构建了一个完整的企业级任务管理系统:

层次 技术 核心职责
Web 层 JSP + JSTL(后台)+ JAX-RS(API) 用户交互、数据展示
业务层 EJB(Stateless/Singleton)+ CDI 业务逻辑、事务管理、依赖注入
异步层 CDI Event + JMS + MDB 事件驱动、消息解耦
持久层 JPA + Hibernate ORM、数据访问
安全层 Jakarta Security + Filter 认证、授权
基础设施 WildFly + MySQL + ActiveMQ 运行环境、数据存储

2. 关键能力提升

  • 架构设计能力:理解分层架构、模块划分、组件协作;
  • 标准规范理解:掌握 Jakarta EE 全栈规范的实战应用;
  • 企业级开发能力:事务管理、安全控制、异步消息、分布式部署;
  • 运维部署能力:应用服务器配置、容器化部署、健康检查。

3. 成为 JavaEE 顶尖架构师的核心启示

  1. 标准是基石:深入理解 Jakarta EE 规范,而非仅停留在某个框架的使用层面;
  2. 体系是整体:JavaEE 各组件(Servlet、EJB、JPA、CDI、JMS 等)不是孤立的,它们共同构成完整的企业级解决方案;
  3. 云原生是方向:传统 JavaEE 正加速向云原生转型,Quarkus、MicroProfile 是未来趋势;
  4. 实践出真知:理论知识必须通过实战项目才能真正内化——本项目就是最好的证明;
  5. 持续演进:技术日新月异,保持学习热情和架构思维,才能始终站在技术前沿。