产品概述
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