产品概述
AgileBoot 是一个基于 Spring Boot 的快速开发框架,专为中小型项目设计 1 。该项目采用分层架构模式,具有清晰的关注点分离,旨在创建可维护和可扩展的代码库 2 。
系统架构
AgileBoot 采用五层模块化架构:
graph TD
subgraph "客户端访问层"
AdminModule["agileboot-admin<br>(管理后台入口)"]
ApiModule["agileboot-api<br>(外部API入口)"]
end
subgraph "业务逻辑层"
DomainModule["agileboot-domain<br>(核心业务逻辑)"]
end
subgraph "基础设施层"
InfraModule["agileboot-infrastructure<br>(配置和集成)"]
end
subgraph "通用工具"
CommonModule["agileboot-common<br>(基础工具)"]
end
AdminModule --> DomainModule
ApiModule --> DomainModule
DomainModule --> InfraModule
InfraModule --> CommonModule
| 模块 | 描述 | 依赖关系 |
| agileboot-admin | 管理后台接口模块,包含后台管理的控制器和API端点 3 | Domain |
| agileboot-api | 开放接口模块,为客户端应用提供RESTful端点 4 | Domain |
| agileboot-domain | 核心业务逻辑,包含领域模型、应用服务和数据库操作 5 | Infrastructure |
| agileboot-infrastructure | 基础设施配置,外部系统集成和横切关注点 6 | Common |
| agileboot-common | 共享工具、基础类和通用组件 7 | 无 |
技术栈
| 类别 | 技术 | 版本 | 用途 |
| 核心框架 | Spring Boot | 2.7.x | 应用框架 8 |
| 安全 | Spring Security | – | 认证和授权 |
| JWT | 0.9.1 | 基于令牌的认证 9 | |
| 数据库 | MySQL | 8.x | 主数据库 |
| MyBatis Plus | 3.5.2 | ORM框架和代码生成 10 | |
| Druid | 1.2.8 | 数据库连接池 | |
| 缓存 | Redis | – | 分布式缓存 |
| Guava | 31.0.1 | 本地缓存 11 | |
| 工具 | Hutool | 5.7.22 | Java工具库 |
| Lombok | 1.18.24 | 减少样板代码 | |
| 文档 | SpringDoc | 1.6.14 | API文档生成 12 |
核心功能
1. 用户管理
● 系统用户配置和属性管理
● 用户个人资料管理 13
● 头像上传功能 14
2. 角色管理
● 基于角色的权限分配
● 数据范围控制 15
3. 菜单管理
● 动态菜单配置
● 权限控制 16
4. 岗位管理
● 岗位信息的增删查改 17
5. 系统监控
● 操作日志记录 18
● 登录日志记录
● 系统资源监控
安全特性
JWT 认证系统
系统使用 JWT 进行无状态认证,支持多终端认证系统 19 。
Spring Security 集成
完整的 Spring Security 配置,包括:
● 登录流程处理 20
● 权限验证
● 登出处理
注解式权限控制
● @PreAuthorize 注解进行方法级权限控制
● 数据权限拦截
● 菜单权限拦截 21
缓存系统
AgileBoot 实现了多级缓存策略:
三级缓存架构
1. Map 缓存: 简单内存缓存,用于轻量级数据
2. Guava 缓存: 本地内存缓存,具有过期策略
3. Redis 缓存: 分布式缓存,用于用户会话和共享数据 11
缓存应用
● 权限判断使用多级缓存
● 用户登录信息缓存
● 字典数据缓存
项目结构设计
领域驱动设计
项目采用 CQRS(命令查询责任分离)理念,将查询和操作分开处理 22 :
查询流程: Controller → Query → ApplicationService → Service → Mapper
操作流程: Controller → Command → ApplicationService → Model → save/update
标准模块结构
agileboot-domain
├── module-name
│ ├── command (命令参数接收模型)
│ ├── dto (返回数据类)
│ ├── db
│ │ ├── entity (实体类)
│ │ ├── service (DB Service)
│ │ ├── mapper (DB Dao)
│ ├── model (领域模型类)
│ ├── query (查询参数模型)
│ └── ModuleApplicationService (应用服务)
特色功能
注解式功能
● 主从数据库切换
● 请求限流
● 重复请求拦截 23
动态权限
● 支持加载动态权限菜单
● 实时权限控制,无需重启 24
测试覆盖
项目包含大量的单元测试和集成测试,确保业务逻辑正确 25 。
快速开始
环境要求
● JDK
● MySQL
● Redis
● Node.js (前端)
启动步骤
1. 克隆项目代码
2. 导入数据库脚本
3. 配置数据库和Redis连接
4. 执行 mvn install
5. 启动 AgileBootAdminApplication 26
嵌入式模式
支持无需外部 MySQL 和 Redis 的启动方式,便于开发测试 27 。
在线体验
● 演示地址:www.agileboot.vip 或 www.agileboot.cc
● 默认账号:admin/admin123 28
Notes
AgileBoot 是一个完全重构的项目,基于 Ruoyi 项目进行了大量改进,包括代码规范、架构优化、性能提升等方面。项目采用现代化的开发理念和实践,提供了完整的后台管理解决方案。
Wiki pages you might want to explore:
● Overview (valarchie/AgileBoot-Back-End)
● Controller Layer (valarchie/AgileBoot-Back-End)
Citations
File: README.md (L31-36)
* 后端采用Spring Boot、Spring Security & Jwt、Redis & MySql、Mybatis Plus、Hutool工具包。 * 权限认证使用Jwt,支持多终端认证系统。 * 支持注解式主从数据库切换,注解式请求限流,注解式重复请求拦截。 * 支持注解式菜单权限拦截,注解式数据权限拦截。 * 支持加载动态权限菜单,实时权限控制。 * ***有大量的单元测试,集成测试覆盖确保业务逻辑正确***。 File: README.md (L48-52)
## 💥 在线体验 💥 演示地址: – www.agileboot.vip – www.agileboot.cc > 账号密码:admin/admin123 File:README.md (L99-102)
– 优化Redis缓存类,封装各个业务缓存,提供多级缓存实现(Redis+Guava) – 提供三个层级的缓存供使用者调用(Map,Guava,Redis使用者可依情况选择使用哪个缓存类) – 权限判断使用多级缓存 – IP地址查询引入离线包 File:README.md (L123-129)
| 技术 | 说明 | 版本 | |—————-|—————–|——————-| | `springboot` | Java项目必备框架 | 2.7 | | `druid` | alibaba数据库连接池 | 1.2.8 | | `springdoc` | 文档生成 | 3.0.0 | | `mybatis-plus` | 数据库框架 | 3.5.2 | | `hutool` | 国产工具包(简单易用) | 3.5.2 | File: README.md (L141-171)
#### 前置准备: 下载前后端代码 git clone https://github.com/valarchie/AgileBoot-Back-End
git clone https://github.com/valarchie/AgileBoot-Front-End
#### 安装好Mysql和Redis
#### 后端启动
1. 生成所需的数据库表
2. 找到后端项目根目录下的sql目录中的agileboot_xxxxx.sql脚本文件(取最新的sql文件)。 导入到你新建的数据库中。在admin模块底下,找到resource目录下的application-dev.yml文件
3. 配置数据库以及Redis的 地址、端口、账号密码在根目录执行mvn install
4. 找到agileboot-admin模块中的AgileBootAdminApplication启动类,直接启动即可
5. 当出现以下字样即为启动成功
/ | | | __ _ _ __ | | _ _ _ __ ___ _ _ ___ ___ ___ ___ ___ / | _ _ | || |
_ \ | |/ _` || ‘|| | | | | || ’ \ / || | | | / |/ |/ _ / |/ || | | | | || || |
) || || (| || | | | | || || |) | _ | |_| || (| (| /_ \ | || || || |||
|/ _|_,||| _| _,|| ./ |/ _,| _|_|_||/|/|| _,|||()
|_|File: README.md (L194-210)
1. > 对于想要尝试全栈项目的前端人员,这边提供更简便的后端启动方式,无需配置Mysql和Redis直接启动 #### 无Mysql/Redis 后端启动 找到agilboot-admin模块下的resource文件中的application.yml文件
2. 配置以下两个值
spring.profiles.active: basic,dev
改为
spring.profiles.active: basic,testagileboot.embedded.mysql: false
agileboot.embedded.redis: false
改为
agileboot.embedded.mysql: true
agileboot.embedded.redis: true请注意:高版本的MacOS系统,无法启动内置的Redis
**File:** README.md (L241-274)
```markdown
agileboot
├── agileboot-admin – 管理后台接口模块(供后台调用)
│
├── agileboot-api – 开放接口模块(供客户端调用)
│
├── agileboot-common – 精简基础工具模块
│
├── agileboot-infrastructure – 基础设施模块(主要是配置和集成,不包含业务逻辑)
│
├── agileboot-domain – 业务模块
├ ├── user – 用户模块(举例)
├ ├── command – 命令参数接收模型(命令)
├ ├── dto – 返回数据类
├ ├── db – DB操作类
├ ├── entity – 实体类
├ ├── service – DB Service
├ ├── mapper – DB Dao
├ ├── model – 领域模型类
├ ├── query – 查询参数模型(查询)
│ ├────── UserApplicationService – 应用服务(事务层,操作领域模型类完成业务逻辑)
### 代码流转
请求分为两类:一类是查询,一类是操作(即对数据有进行更新)。
**查询**:Controller > xxxQuery > xxxApplicationService > xxxService(Db) > xxxMapper
**操作**:Controller > xxxCommand > xxxApplicationService > xxxModel(处理逻辑) > save 或者 update (本项目直接采用JPA的方式进行插入已经更新数据)
这是借鉴CQRS的开发理念,将查询和操作分开处理。操作类的业务实现借鉴了DDD战术设计的理念,使用领域类,工厂类更面向对象的实现逻辑。
如果你不太适应这样的开发模式的话。可以在domain模块中按照你之前从Controller->Service->DAO的模式进行开发。it is up to you.
File: agileboot-infrastructure/src/main/java/com/agileboot/infrastructure/config/SpringDocConfig.java (L18-27)
public OpenAPI agileBootApi() {
return new OpenAPI()
.info(new Info().title("Agileboot后台管理系统")
.description("Agileboot API 演示")
.version("v1.8.0")
.license(new License().name("MIT 3.0").url("https://github.com/valarchie/AgileBoot-Back-End")))
.externalDocs(new ExternalDocumentation()
.description("Agileboot后台管理系统接口文档")
.url("https://juejin.cn/column/7159946528827080734"));
}
File: agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java (L44-66)
/**
* 个人信息
*/
@Operation(summary = "获取个人信息")
@GetMapping
public ResponseDTO<UserProfileDTO> profile() {
SystemLoginUser user = AuthenticationUtils.getSystemLoginUser();
UserProfileDTO userProfile = userApplicationService.getUserProfile(user.getUserId());
return ResponseDTO.ok(userProfile);
}
/**
* 修改用户
*/
@Operation(summary = "修改个人信息")
@AccessLog(title = "个人信息", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> updateProfile(@RequestBody UpdateProfileCommand command) {
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
command.setUserId(loginUser.getUserId());
userApplicationService.updateUserProfile(command);
return ResponseDTO.ok();
}
File: agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysProfileController.java (L81-96)
/**
* 头像上传
*/
@Operation(summary = "修改个人头像")
@AccessLog(title = "用户头像", businessType = BusinessTypeEnum.MODIFY)
@PostMapping("/avatar")
public ResponseDTO<UploadFileDTO> avatar(@RequestParam("avatarfile") MultipartFile file) {
if (file.isEmpty()) {
throw new ApiException(ErrorCode.Business.USER_UPLOAD_FILE_FAILED);
}
SystemLoginUser loginUser = AuthenticationUtils.getSystemLoginUser();
String avatarUrl = FileUploadUtils.upload(UploadSubDir.AVATAR_PATH, file);
userApplicationService.updateUserAvatar(new UpdateUserAvatarCommand(loginUser.getUserId(), avatarUrl));
return ResponseDTO.ok(new UploadFileDTO(avatarUrl));
}
File: agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysRoleController.java (L46-112)
public class SysRoleController extends BaseController {
private final RoleApplicationService roleApplicationService;
@Operation(summary = "角色列表")
@PreAuthorize("@permission.has('system:role:list')")
@GetMapping("/list")
public ResponseDTO<PageDTO<RoleDTO>> list(RoleQuery query) {
PageDTO<RoleDTO> pageDTO = roleApplicationService.getRoleList(query);
return ResponseDTO.ok(pageDTO);
}
@Operation(summary = "角色列表导出")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('system:role:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, RoleQuery query) {
PageDTO<RoleDTO> pageDTO = roleApplicationService.getRoleList(query);
CustomExcelUtil.writeToResponse(pageDTO.getRows(), RoleDTO.class, response);
}
/**
* 根据角色编号获取详细信息
*/
@Operation(summary = "角色详情")
@PreAuthorize("@permission.has('system:role:query')")
@GetMapping(value = "/{roleId}")
public ResponseDTO<RoleDTO> getInfo(@PathVariable @NotNull Long roleId) {
RoleDTO roleInfo = roleApplicationService.getRoleInfo(roleId);
return ResponseDTO.ok(roleInfo);
}
/**
* 新增角色
*/
@Operation(summary = "添加角色")
@PreAuthorize("@permission.has('system:role:add')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody AddRoleCommand addCommand) {
roleApplicationService.addRole(addCommand);
return ResponseDTO.ok();
}
/**
* 移除角色
*/
@Operation(summary = "删除角色")
@PreAuthorize("@permission.has('system:role:remove')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping(value = "/{roleId}")
public ResponseDTO<Void> remove(@PathVariable("roleId") List<Long> roleIds) {
roleApplicationService.deleteRoleByBulk(roleIds);
return ResponseDTO.ok();
}
/**
* 修改保存角色
*/
@Operation(summary = "修改角色")
@PreAuthorize("@permission.has('system:role:edit')")
@AccessLog(title = "角色管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> edit(@Validated @RequestBody UpdateRoleCommand updateCommand) {
roleApplicationService.updateRole(updateCommand);
return ResponseDTO.ok();
}
File: agileboot-domain/src/main/java/com/agileboot/domain/system/menu/db/SysMenuMapper.java (L25-33)
@Select("SELECT DISTINCT m.* "
+ "FROM sys_menu m "
+ " LEFT JOIN sys_role_menu rm ON m.menu_id = rm.menu_id "
+ " LEFT JOIN sys_user u ON rm.role_id = u.role_id "
+ "WHERE u.user_id = #{userId} "
+ " AND m.status = 1 "
+ " AND m.deleted = 0 "
+ "ORDER BY m.parent_id")
List<SysMenuEntity> selectMenuListByUserId(@Param("userId")Long userId);
File: agileboot-admin/src/main/java/com/agileboot/admin/controller/system/SysPostController.java (L34-122)
/**
* 岗位信息操作处理
*
* @author ruoyi
*/
@Tag(name = "职位API", description = "职位相关的增删查改")
@RestController
@RequestMapping("/system/post")
@Validated
@RequiredArgsConstructor
public class SysPostController extends BaseController {
private final PostApplicationService postApplicationService;
/**
* 获取岗位列表
*/
@Operation(summary = "职位列表")
@PreAuthorize("@permission.has('system:post:list')")
@GetMapping("/list")
public ResponseDTO<PageDTO<PostDTO>> list(PostQuery query) {
PageDTO<PostDTO> pageDTO = postApplicationService.getPostList(query);
return ResponseDTO.ok(pageDTO);
}
/**
* 导出查询到的所有岗位信息到excel文件
* @param response http响应
* @param query 查询参数
* @author Kevin Zhang
* @date 2023-10-02
*/
@Operation(summary = "职位列表导出")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.EXPORT)
@PreAuthorize("@permission.has('system:post:export')")
@GetMapping("/excel")
public void export(HttpServletResponse response, PostQuery query) {
List<PostDTO> all = postApplicationService.getPostListAll(query);
CustomExcelUtil.writeToResponse(all, PostDTO.class, response);
}
/**
* 根据岗位编号获取详细信息
*/
@Operation(summary = "职位详情")
@PreAuthorize("@permission.has('system:post:query')")
@GetMapping(value = "/{postId}")
public ResponseDTO<PostDTO> getInfo(@PathVariable Long postId) {
PostDTO post = postApplicationService.getPostInfo(postId);
return ResponseDTO.ok(post);
}
/**
* 新增岗位
*/
@Operation(summary = "添加职位")
@PreAuthorize("@permission.has('system:post:add')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.ADD)
@PostMapping
public ResponseDTO<Void> add(@RequestBody AddPostCommand addCommand) {
postApplicationService.addPost(addCommand);
return ResponseDTO.ok();
}
/**
* 修改岗位
*/
@Operation(summary = "修改职位")
@PreAuthorize("@permission.has('system:post:edit')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.MODIFY)
@PutMapping
public ResponseDTO<Void> edit(@RequestBody UpdatePostCommand updateCommand) {
postApplicationService.updatePost(updateCommand);
return ResponseDTO.ok();
}
/**
* 删除岗位
*/
@Operation(summary = "删除职位")
@PreAuthorize("@permission.has('system:post:remove')")
@AccessLog(title = "岗位管理", businessType = BusinessTypeEnum.DELETE)
@DeleteMapping
public ResponseDTO<Void> remove(@RequestParam @NotNull @NotEmpty List<Long> ids) {
postApplicationService.deletePost(new BulkOperationCommand<>(ids));
return ResponseDTO.ok();
}
}
File: agileboot-domain/src/main/java/com/agileboot/domain/system/log/LogApplicationService.java (L32-52)
public PageDTO<LoginLogDTO> getLoginInfoList(LoginLogQuery query) {
Page<SysLoginInfoEntity> page = loginInfoService.page(query.toPage(), query.toQueryWrapper());
List<LoginLogDTO> records = page.getRecords().stream().map(LoginLogDTO::new).collect(Collectors.toList());
return new PageDTO<>(records, page.getTotal());
}
public void deleteLoginInfo(BulkOperationCommand<Long> deleteCommand) {
QueryWrapper<SysLoginInfoEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.in("info_id", deleteCommand.getIds());
loginInfoService.remove(queryWrapper);
}
public PageDTO<OperationLogDTO> getOperationLogList(OperationLogQuery query) {
Page<SysOperationLogEntity> page = operationLogService.page(query.toPage(), query.toQueryWrapper());
List<OperationLogDTO> records = page.getRecords().stream().map(OperationLogDTO::new).collect(Collectors.toList());
return new PageDTO<>(records, page.getTotal());
}
public void deleteOperationLog(BulkOperationCommand<Long> deleteCommand) {
operationLogService.removeBatchByIds(deleteCommand.getIds());
}
File: agileboot-admin/src/main/java/com/agileboot/admin/customize/service/login/TokenService.java (L132-161)
private Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
private String getUsernameFromToken(String token) {
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @return token
*/
private String getTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(header);
if (StrUtil.isNotEmpty(token) && token.startsWith(Token.PREFIX)) {
token = StrUtil.stripIgnoreCase(token, Token.PREFIX, null);
}
return token;
}
File: agileboot-admin/src/main/java/com/agileboot/admin/customize/config/SecurityConfig.java (L34-114)
/**
* 主要配置登录流程逻辑涉及以下几个类
* @see UserDetailsServiceImpl#loadUserByUsername 用于登录流程通过用户名加载用户
* @see this#unauthorizedHandler() 用于用户未授权或登录失败处理
* @see this#logOutSuccessHandler 用于退出登录成功后的逻辑
* @see JwtAuthenticationTokenFilter#doFilter token的校验和刷新
* @see LoginService#login 登录逻辑
* @author valarchie
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenService tokenService;
private final RedisCacheService redisCache;
/**
* token认证过滤器
*/
private final JwtAuthenticationTokenFilter jwtTokenFilter;
private final UserDetailsService userDetailsService;
/**
* 跨域过滤器
*/
private final CorsFilter corsFilter;
/**
* 登录异常处理类
* 用户未登陆的话 在这个Bean中处理
*/
@Bean
public AuthenticationEntryPoint unauthorizedHandler() {
return (request, response, exception) -> {
ResponseDTO<Object> responseDTO = ResponseDTO.fail(
new ApiException(Client.COMMON_NO_AUTHORIZATION, request.getRequestURI())
);
ServletHolderUtil.renderString(response, JSONUtil.toJsonStr(responseDTO));
};
}
/**
* 退出成功处理类 返回成功
* 在SecurityConfig类当中 定义了/logout 路径对应处理逻辑
*/
@Bean
public LogoutSuccessHandler logOutSuccessHandler() {
return (request, response, authentication) -> {
SystemLoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser != null) {
String userName = loginUser.getUsername();
// 删除用户缓存记录
redisCache.loginUserCache.delete(loginUser.getCachedKey());
// 记录用户退出日志
ThreadPoolManager.execute(AsyncTaskFactory.loginInfoTask(
userName, LoginStatusEnum.LOGOUT, LoginStatusEnum.LOGOUT.description()));
}
ServletHolderUtil.renderString(response, JSONUtil.toJsonStr(ResponseDTO.ok()));
};
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 鉴权管理类
* @see UserDetailsServiceImpl#loadUserByUsername
*/
@Bean