AgileBoot 为中小型项目设计的 Spring Boot 快速开发框架

产品概述

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端点 3Domain
agileboot-api开放接口模块,为客户端应用提供RESTful端点 4Domain
agileboot-domain核心业务逻辑,包含领域模型、应用服务和数据库操作 5Infrastructure
agileboot-infrastructure基础设施配置,外部系统集成和横切关注点 6Common
agileboot-common共享工具、基础类和通用组件 7

技术栈

类别技术版本用途
核心框架Spring Boot2.7.x应用框架 8
安全Spring Security认证和授权
 JWT0.9.1基于令牌的认证 9
数据库MySQL8.x主数据库
 MyBatis Plus3.5.2ORM框架和代码生成 10
 Druid1.2.8数据库连接池
缓存Redis分布式缓存
 Guava31.0.1本地缓存 11
工具Hutool5.7.22Java工具库
 Lombok1.18.24减少样板代码
文档SpringDoc1.6.14API文档生成 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