**摘要:**实习期间参与企业后台项目开发,熟悉企业开发流程与代码规范。


实习核心流程(结合实际经历)
由于自己进入的是一个小公司实习,当时项目刚好启动,参与了较多基础模块的开发。
一、基础准备与环境搭建阶段(入职 1-3 天)

- 公司基础配置:进入公司飞书、拥有个人邮箱等基础办公权限
- 代码拉取与环境搭建:
- 学习并使用 git/svn 等版本管理工具 clone 项目代码(公司使用的是阿里云云效)
- 配置项目所需配置文件,搭建后端 + 前端开发环境(后端需兼顾前端环境)
- 解决环境依赖问题,确保项目能正常跑起来(熟悉配置文件与环境)
- 熟悉开发工具的使用,避免因操作问题浪费时间(mentor 教了debug技巧,快捷键)
二、项目熟悉阶段(入职 1-2 周)
这个阶段任务主要是熟悉环境,熟练使用通用封装 / 工具类,自己在熟悉项目的时候,寻找少量项目bug,提交问题给 mentor 审核,并进行功能的测试,完成简单 的demo任务,熟练框架使用,代码风格,尤其是掌握 Git 与 MP 相关的使用,在代码中用的非常多。
1. 基础认知
- 系统学习公司核心业务范围、业务流程及新人岗位能力要求,明确学习方向与目标;
- 由同事 / 组长系统性讲解项目核心模块划分、整体技术架构、核心业务场景及上下游依赖关系,建立项目整体认知。
2. 深度熟悉
- 梳理项目完整目录结构、模块间交互逻辑,深入理解数据库表字段设计;
- 熟读 Common 通用包的代码,熟练掌握封装的通用工具类的调用方式;
- 重点学习公司主流技术框架的核心,结合框架特性理解业务逻辑的实现思路;
- 拆解业务三层架构(控制层、服务层、数据层),对照接口文档理解代码具体写法。
3. 代码实践
- 基于公司现有框架完成简单 Demo 开发,覆盖核心业务场景的基础流程,验证对框架及通用工具的掌握程度。
三、初步实践阶段(入职 2 周后)
本人当时已较为熟悉业务,快速投入开发工作,仿照其他模块的业务代码编写风格,负责管理模块相关开发,按照接口文档完成增删改查核心操作。
同时,我也参与了部分难度较复杂任务的讨论,提出了一些建议和优化思路(实际作用有限),涉及Redisson分布式锁、第三方平台Token无感续期等相关问题。
在项目迭代过程中,我重点观察技术负责人在实践redis缓存,Redission结合自定义注解 + AOP实现方法级别的分布式锁,处理异步日志等任务的提交与执行、自定义合理线程池、使用JUC并发编程工具类等业务场景时,解决实际问题的方法。
熟悉企业项目
权限管理模块
java
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Auth {
String[] value() default "";
}
Auth: 定义了一个自定义的 Java 注解,名为 Auth,通常用于标记需要权限校验的类或方法。
java
@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class AuthAop {
@Pointcut("@annotation(auth)")
public void controllerAspect(Auth auth) {
}
@Around(value = "controllerAspect(auth)", argNames = "proceedingJoinPoint,auth")
public Object aroundAuth(ProceedingJoinPoint proceedingJoinPoint, Auth auth) throws Throwable {
LoginUser loginUser = LoginUserHandler.getLoginUser();
if (loginUser == null) {
throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR);
}
String role = Optional.ofNullable(loginUser.getRole()).orElse("").toUpperCase();
if (!Arrays.asList(auth.value()).contains(role)) {
throw new BusinessException(ResponseCode.INSUFFICIENT_PERMISSIONS);
}
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
arg = loginUser;
}
}
return proceedingJoinPoint.proceed(args);
}
}
AuthAspect:
@Pointcut:Spring AOP 的注解,用于声明一个切点,参数是「切点表达式」,指定拦截规则。
切点表达式 @annotation(auth):
-
@annotation()是 AOP 内置的切点表达式,含义是拦截所有方法上标注了指定注解的方法。 -
这里的
auth是参数名,对应后面方法的Auth auth,表示拦截标注@Auth的方法,并把该注解实例传入切点方法。
public void controllerAspect(Auth auth):
-
这是一个「切点签名方法」,本身无业务逻辑,仅用于承载
@Pointcut注解和参数声明。 -
参数
Auth auth:表示将拦截到的方法上的@Auth注解实例注入到该参数中,后续通知方法可直接使用。
java
@Component
@WebFilter(urlPatterns = "/*")
@Slf4j
@AllArgsConstructor
public class LoginFilter implements Filter {
private static final List<String> OPEN_API = List.of();
private static final String OPEN_API_HEADER = "X-Open-Api";
private static final String OPEN_API_VERSION = "1.0.0";
private static final PathPatternParser PATH_PATTERN_PARSER = PathPatternParser.defaultInstance;
private final TokenService tokenService;
private final ObjectMapper objectMapper;
private final SecurityIgnoreUrls authPath;
private final com.daochengtech.lock.platform.openapi.auth.OpenApiAuthHandler openApiAuthHandler;
// 白名单路径缓存
private List<PathPattern> ignorePatterns;
//init 方法:过滤器初始化(仅启动时执行一次)
@Override
public void init(jakarta.servlet.FilterConfig filterConfig) {
ignorePatterns = authPath.getUrls().stream()
.map(PATH_PATTERN_PARSER::parse)
.toList();
}
/**
* 登录过滤器
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.setContentType("application/json;charset=utf-8");
String url = httpRequest.getRequestURI();
log.info("请求url:{}", url);
try {
// 判断是否白名单
PathContainer pathContainer = PathContainer.parsePath(url);
if (ignorePatterns.stream().anyMatch(p -> p.matches(pathContainer))) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if (handle(httpRequest, httpResponse)) {
filterChain.doFilter(servletRequest, servletResponse);
}
} finally {
// 确保清理所有上下文
try {
LoginUserHandler.removeLoginUser();
} catch (Exception e) {
log.debug("清理用户上下文异常", e);
}
try {
openApiAuthHandler.clearAuthContext();
} catch (Exception e) {
log.debug("清理OpenAPI上下文异常", e);
}
}
}
/**
* 处理认证
*
* @param httpRequest 请求
* @param httpResponse 响应
*/
private boolean handle(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
try {
// 检查是否为OpenAPI请求
if (openApiAuthHandler.isOpenApiRequest(httpRequest)) {
return handleOpenApiAuth(httpRequest, httpResponse);
}
// 处理普通用户认证
return handleUserAuth(httpRequest, httpResponse);
} catch (Exception e) {
log.error("认证处理异常", e);
writeAccessDenied(httpResponse);
return false;
}
}
/**
* 处理OpenAPI认证
*
* @param httpRequest 请求
* @param httpResponse 响应
* @return 认证结果
*/
private boolean handleOpenApiAuth(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
log.debug("处理OpenAPI认证,URI: {}", httpRequest.getRequestURI());
// Token获取请求不需要认证
if (openApiAuthHandler.isTokenRequest(httpRequest)) {
log.debug("Token获取请求,跳过认证");
return true;
}
// 执行OpenAPI认证
boolean authResult = openApiAuthHandler.authenticate(httpRequest, httpResponse);
if (!authResult) {
log.warn("OpenAPI认证失败,URI: {}", httpRequest.getRequestURI());
writeOpenApiAccessDenied(httpResponse);
return false;
}
log.debug("OpenAPI认证成功,API Key: {}", openApiAuthHandler.getCurrentApiKey());
return true;
}
/**
* 处理普通用户认证
*/
private boolean handleUserAuth(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
log.debug("处理普通用户认证,URI: {}", httpRequest.getRequestURI());
// 获取认证token
String authenticationToken = httpRequest.getHeader(AUTHORIZATION);
// 判断令牌是否存在
if (StringUtils.isBlank(authenticationToken)) {
log.warn("用户认证失败:缺少认证token");
writeAccessDenied(httpResponse);
return false;
}
// 解析token
try {
LoginUser loginUser = tokenService.getLoginUser(httpRequest);
if (loginUser == null) {
log.warn("用户认证失败:token解析失败");
writeAccessDenied(httpResponse);
return false;
}
LoginUserHandler.setLoginUser(loginUser);
log.debug("用户认证成功,用户: {}", loginUser.getUsername());
return true;
} catch (Exception e) {
log.error("用户token解析异常", e);
writeAccessDenied(httpResponse);
return false;
}
}
/**
* 拒绝访问
*
* @param response 响应
*/
private void writeAccessDenied(HttpServletResponse response) {
writeErrorResponse(response, ResponseCode.ACCESS_DENIED);
}
/**
* OpenAPI拒绝访问
*
* @param response 响应
*/
private void writeOpenApiAccessDenied(HttpServletResponse response) {
writeErrorResponse(response, ResponseCode.AUTHENTICATION_FAIL);
}
/**
* 写入错误响应
*
* @param response 响应
* @param responseCode 响应码
*/
private void writeErrorResponse(HttpServletResponse response, ResponseCode responseCode) {
try {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(objectMapper.writeValueAsString(CommonResult.response(responseCode)));
response.getWriter().flush();
} catch (IOException e) {
log.error("写入响应失败", e);
}
}
/**
* 验证OpenAPI请求
*
* @param request HTTP请求
* @param response HTTP响应
* @return 认证是否成功
*/
public boolean authenticate(HttpServletRequest request, HttpServletResponse response) {
try {
log.debug("开始OpenAPI认证,URI: {}", request.getRequestURI());
// 获取Token和版本信息
String token = request.getHeader(OPEN_API_TOKEN_HEADER);
String version = request.getHeader(OPEN_API_VERSION_HEADER);
// 验证必要的请求头
if (StringUtils.isBlank(token)) {
log.warn("OpenAPI认证失败:缺少Token请求头");
return false;
}
if (StringUtils.isBlank(version)) {
log.warn("OpenAPI认证失败:缺少版本请求头");
return false;
}
// 验证版本
if (!SUPPORTED_VERSION.equals(version)) {
log.warn("OpenAPI认证失败:不支持的版本 {}", version);
return false;
}
// 验证Token并获取上下文
OpenApiContext context = validateToken(token, version);
if (context == null) {
log.warn("OpenAPI认证失败:Token验证失败");
return false;
}
// 设置认证上下文
setAuthContext(context);
log.debug("OpenAPI认证成功,API Key: {}", context.getApiKey());
return true;
} catch (Exception e) {
log.error("OpenAPI认证异常", e);
return false;
}
}
}
LoginFilter:登录过滤器整体执行流程
该登录过滤器作为请求进入系统的认证关卡,整体执行流程如下:
1. 初始化阶段(init方法)
过滤器初始化时,会将配置的白名单URL(通过authPath.getUrls()获取)解析为PathPattern格式并缓存到ignorePatterns列表中,为后续路径匹配做准备。
2. 核心过滤阶段(doFilter方法)
所有请求进入过滤器后,执行核心逻辑:
步骤1:请求转换与基础设置
将ServletRequest/ServletResponse转换为HttpServletRequest/HttpServletResponse,并设置响应格式为application/json;charset=utf-8,记录请求URL。
步骤2:白名单路径校验
解析当前请求URL为PathContainer,匹配缓存的白名单ignorePatterns:
-
若匹配成功(属白名单),直接放行请求(
filterChain.doFilter),结束当前过滤器逻辑; -
若不匹配,进入认证处理流程。
步骤3:认证处理
调用handle方法执行具体认证逻辑,若认证通过则放行请求,否则拦截。
步骤4:上下文清理(finally块)
无论认证成功/失败,最终都会清理用户上下文(LoginUserHandler.removeLoginUser())和OpenAPI上下文(openApiAuthHandler.clearAuthContext()),避免内存泄漏。
3. 认证处理流程(handle方法)
handle方法是认证核心,区分两种认证类型:
步骤1:判断请求类型
先通过openApiAuthHandler.isOpenApiRequest判断是否为OpenAPI请求:
-
若是,执行
handleOpenApiAuth处理OpenAPI认证; -
若否,执行
handleUserAuth处理普通用户认证。
步骤2:异常兜底
若认证过程中抛出异常,记录错误日志,调用writeAccessDenied返回未授权响应,拦截请求。
4. OpenAPI认证流程(handleOpenApiAuth方法)
步骤1:特殊请求放行
若为Token获取请求(openApiAuthHandler.isTokenRequest),直接放行(无需认证)。
步骤2:执行OpenAPI认证
调用openApiAuthHandler.authenticate执行认证:
-
认证成功:放行请求;
-
认证失败:记录警告日志,调用
writeOpenApiAccessDenied返回未授权响应,拦截请求。
5. 普通用户认证流程(handleUserAuth方法)
步骤1:Token校验
从请求头获取认证Token,若Token为空,记录警告日志,返回未授权响应,拦截请求。
步骤2:Token解析与验证
调用tokenService.getLoginUser解析Token:
-
解析成功:将用户信息存入上下文(
LoginUserHandler.setLoginUser),放行请求; -
解析失败(
LoginUser为空)或抛出异常:记录错误日志,返回未授权响应,拦截请求。
6. 异常响应处理
认证失败时,通过writeErrorResponse统一返回标准化错误响应:
-
设置响应状态码为401(
SC_UNAUTHORIZED); -
将
CommonResult.response(responseCode)序列化为JSON写入响应体; -
区分普通用户认证失败和OpenAPI认证失败的响应码。
SecurityIgnoreUrls(配置绑定类)与 secure.ignored.urls(白名单 URL 配置)
java
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "secure.ignored")
public class SecurityIgnoreUrls {
private List<String> urls = new ArrayList<>();
}
XML
secure.ignored.urls[0]=/swagger-ui.html
secure.ignored.urls[1]=/swagger-resources/**
secure.ignored.urls[2]=/v3/**
secure.ignored.urls[3]=/swagger-ui/**
secure.ignored.urls[4]=/swagger-ui/index.html
secure.ignored.urls[5]=/web/sys/login
secure.ignored.urls[6]=/common/captcha
secure.ignored.urls[7]=/test/callback/test
secure.ignored.urls[8]=/api/socket/**
通过 Spring Boot 的@ConfigurationProperties注解,将配置文件中secure.ignored前缀的配置(即secure.ignored.urls)绑定到这个类的urls属性上,让代码可以通过注入该类直接获取白名单 URL 列表(无需手动解析配置),是配置与代码解耦的标准写法。
Token管理模块
java
@Slf4j
@Component
public class TokenService {
public static final String TOKEN_PREFIX = "Bearer ";
public static final String LOGIN_USER = "LOGIN_USER:";
public static final String TOKEN = "token";
public static final String USERNAME = "username";
public static final String FORMAT = "%s%s:%s";
private final String header;
private final byte[] secretKey;
private final Long expirationHours;
private final RedisService redisService;
@Autowired
public TokenService(
@Value("${auth.jwt.header}") String header,
@Value("${auth.jwt.secret}") String secret,
@Value("${auth.jwt.expiration}") Long expiration,
RedisService redisService) {
this.header = header;
this.secretKey = secret.getBytes(StandardCharsets.UTF_8);
this.expirationHours = expiration;
this.redisService = redisService;
}
public LoginUser getLoginUser(HttpServletRequest request) {
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String username = claims.get(USERNAME, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
return redisService.get(key, LoginUser.class);
} catch (Exception e) {
log.error("获取用户信息异常: {}", e.getMessage());
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL);
}
}
public Long getExpireTime(HttpServletRequest request) {
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String username = claims.get(USERNAME, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
return redisService.getExpire(key);
} catch (Exception e) {
log.error("获取用户过期时间异常: {}", e.getMessage());
return null;
}
}
public String createToken(LoginUser loginUser) {
String uuid = UUID.randomUUID().toString();
Map<String, Object> claims = new HashMap<>();
claims.put(TOKEN, uuid);
claims.put(USERNAME, loginUser.getUsername());
String token = Jwts.builder()
.claims(claims)
.expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000))
.signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512)
.compact();
loginUser.setToken(token);
refreshToken(loginUser, uuid);
return token;
}
public void refreshToken(LoginUser loginUser, String uuid) {
try {
// 1. 重新生成JWT Token(更新过期时间)
Map<String, Object> claims = new HashMap<>();
claims.put(TOKEN, uuid);
claims.put(USERNAME, loginUser.getUsername());
String newToken = Jwts.builder()
.claims(claims)
.expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000))
.signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512)
.compact();
loginUser.setToken(newToken);
// 3. 刷新Redis缓存(续期+更新LoginUser)
String key = String.format(FORMAT, LOGIN_USER, uuid, loginUser.getUsername());
redisService.set(key, loginUser, Duration.ofHours(expirationHours));
log.debug("Token刷新成功,用户名:{},新Token过期时间:{}小时", loginUser.getUsername(), expirationHours);
} catch (Exception e) {
log.error("Token刷新异常,用户名:{}", loginUser.getUsername(), e);
throw new BusinessException("Token续期失败!");
}
}
public String getUsernameFromToken(String token) {
return parseToken(token).get(USERNAME, String.class);
}
public String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
return token.substring(TOKEN_PREFIX.length());
}
throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR);
}
public void logout(String username) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(
RequestAttributes.REFERENCE_REQUEST);
if (request == null) {
return;
}
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
redisService.del(key);
} catch (Exception e) {
log.error("登出时删除Token异常: {}", e.getMessage());
}
}
}
1. Token 创建流程(createToken 方法)
-
步骤1:生成 UUID 作为 Token 的唯一标识(tokenId);
-
步骤2:构建 JWT 载荷(claims),存入 tokenId 和用户名;
-
步骤3:使用secretKey 进行 HS512 签名,设置 Token 过期时间,生成并返回 JWT Token;
-
步骤4:将 Token 绑定到 LoginUser 对象,调用 refreshToken 方法将 LoginUser 信息存入 Redis,并设置与 Token 相同的过期时间。
2. Token 解析验证流程(getLoginUser 方法)
-
步骤1:从请求头中提取 Token(去掉前缀 "Bearer ");
-
步骤2:用 secretKey 验证并解析 Token,获取载荷中的 tokenId 和用户名;
-
步骤3:拼接 Redis Key,从 Redis 中获取 LoginUser 对象;
-
步骤4:解析/获取失败则抛出认证失败异常,成功则返回 LoginUser。
3. Token 刷新流程(refreshToken 方法)
-
步骤1:重新生成JWT-Token令牌;
-
步骤2:将 LoginUser 重新存入 Redis,重置过期时间为配置的 expirationHours 小时。
4. Token 注销流程(logout 方法)
-
步骤1:从请求上下文获取 HttpServletRequest,提取并解析 Token,获取 tokenId;
-
步骤2:拼接 Redis Key(LOGIN_USER:{tokenId}:{username});
-
步骤3:删除 Redis 中该 Key 对应的 LoginUser 数据,完成 Token 失效。
5. 过期时间查询流程(getExpireTime 方法)
-
步骤1:解析 Token 获取 tokenId 和用户名,拼接 Redis Key;
-
步骤2:查询 Redis 中该 Key 的剩余过期时间并返回;
-
步骤3:异常则返回 null,不抛出异常(仅日志记录)。
Token 创建:生成 JWT Token + 存储用户信息到 Redis(带过期时间);
Token 验证:解析 JWT 并从 Redis 校验/获取用户信息;
Token 管理:支持刷新(重置 Redis 过期时间)、注销(删除 Redis 数据)、查询过期时间;
核心依赖:JWT 签名保证 Token 不被篡改,Redis 存储保证用户信息可追溯、可失效。
参与企业项目
企业管理模块
cacheAll
java
public List<Company> cacheAll() {
final String cacheKey = "company:all";
List<Company> cachedList = redisService.get(cacheKey, List.class);
if (cachedList != null) {
return cachedList;
}
synchronized (cacheLock) {
cachedList = redisService.get(cacheKey, List.class);
if (cachedList != null) {
return cachedList;
}
List<Company> list = list();
redisService.set(cacheKey, list, 3600); // 设置1小时过期
return list;
}
}
这段代码是企业级系统中 "全量数据缓存加载" 类功能的典型通用逻辑框架,核心遵循 "缓存查询→双重校验加锁→数据库查询→缓存写入→结果返回" 的标准化流程,具体可拆解为 4 个核心步骤:
-
一级缓存查询:定义固定缓存键(cacheKey = "company:all"),先从 Redis 中查询缓存数据,若缓存存在则直接返回(缓存功能的通用前置步骤,优先使用缓存提升查询性能);
-
双重校验 + 同步锁:缓存不存在时,通过 synchronized (cacheLock) 加本地同步锁,锁内再次查询缓存,若仍不存在再执行数据库查询(缓存加载的通用并发防护,避免缓存击穿);
-
数据库查询 + 缓存写入:锁内执行全量数据库查询,将查询结果写 Redis 并设置过期时间。
-
结果统一返回:无论从缓存还是数据库获取数据,最终统一返回全量企业列表。
getCompanyFromCache
java
public Optional<Company> getCompanyFromCache(Long companyId) {
return cacheAll().stream()
.filter(c -> c.getId().equals(companyId))
.findFirst();
}
这段代码是企业级系统中 "基于全量缓存的单条实体精准查询" 类功能的典型通用逻辑框架,核心遵循 "全量缓存加载→精准过滤→空值安全返回" 的标准化流程,具体可拆解为 3 个核心步骤:
-
全量缓存加载(复用缓存数据):调用 cacheAll () 方法加载全量企业缓存数据。
-
精准数据过滤(定位目标实体):通过 stream ().filter (c -> c.getId ().equals (companyId)) 过滤出匹配企业 ID 的实体,结合 findFirst () 获取单条结果。
-
空值安全返回:返回 Optional<Company>类型结果,未匹配到返回空 Optional 而非 null。
getCompanyList
@Validated 是 Spring 框架提供的参数校验注解,核心作用是自动校验接口入参的合法性,避免我们手动写大量 if-else 判断参数是否为空、格式是否正确。
java
@PostMapping("/list")
public CommonResult<SimplePage<CompanyListVO>> getCompanyList(@RequestBody @Validated CompanyListDTO dto){
return CommonResult.success(companyService.getCompanyList(dto,U.get()));
}
java
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "企业列表查询")
public class CompanyListDTO extends PageableQuery {
@Schema(description = "企业ID")
private Long companyId;
@Size(min = 3, max = 20, message = "查询最少三个字")
@Schema(description = "公司名称--模糊查询最少三个字")
private String name;
}
@Validated 加在 Controller 接口的 @RequestBody CompanyListDTO dto 参数上,核心是触发 CompanyListDTO 类中所有 JSR 380 校验注解的执行:
- 若
CompanyListDTO有生效的校验注解(@Size(min = 3, max = 20, message = "最少三个字")),前端调用/list接口时,会自动校验name字段的长度是否符合 3-20 字符的规则; - 若校验失败(比如
name传了 2 个字符),会直接抛出MethodArgumentNotValidException异常,拦截非法参数,不会进入companyService.getCompanyList()业务逻辑层。
java
public SimplePage<CompanyListVO> getCompanyList(CompanyListDTO dto, LoginUser loginUser) {
return PageUtil.getPage(new SimplePage<>(
companyRepository.getCompanyList(Page.of(dto.getPageNum(), dto.getPageSize()), dto, loginUser)),
r -> CompanyListVO.builder()
.id(r.getId())
.name(r.getName())
.shortName(r.getShortName())
.contact(r.getContact())
.phone(r.getPhone())
.address(r.getAddress())
.build());
}
java
public class PageUtil {
public static <T> SimplePage<T> parse(SimplePage<?> src, List<T> list) {
SimplePage<T> ret = new SimplePage<>();
ret.setPages(src.getPages());
ret.setPageNum(src.getPageNum());
ret.setPageSize(src.getPageSize());
ret.setTotal(src.getTotal());
ret.setList(list);
return ret;
}
public static <R, T> SimplePage<R> getPage(SimplePage<T> src, Function<T, R> mapper) {
SimplePage<R> ret = new SimplePage<>();
ret.setPages(src.getPages());
ret.setPageNum(src.getPageNum());
ret.setPageSize(src.getPageSize());
ret.setTotal(src.getTotal());
ret.setList(src.getList().stream().map(mapper).filter(Objects::nonNull).toList());
return ret;
}
}
java
public Page<Company> getCompanyList(Page<Company> page, CompanyListDTO dto, LoginUser loginUser) {
return page(page, Wrappers.<Company>lambdaQuery()
.eq(dto.getCompanyId() != null, Company::getId, dto.getCompanyId())
.like(StringUtils.isNotBlank(dto.getName()), Company::getName, dto.getName())
.eq(loginUser.isEnterprise(), Company::getId, loginUser.getAuthCompanyId())
.orderByDesc(Company::getCreateTime));
}
这段代码是企业级系统中 "分页列表查询 + 数据转换" 类功能的典型通用逻辑框架,核心遵循 "分页查询→数据转换→元数据透传→结果返回" 的标准化流程,具体可拆解为 4 个核心步骤:
-
**分页参数封装 + 仓储层查询:**通过 Page.of (dto.getPageNum (), dto.getPageSize ()) 封装统一的分页参数,调用 companyRepository.getCompanyList () 执行数据库分页查询,返回包含 "分页元数据 + 原始实体列表" 的 SimplePage 对象;
-
**通用工具类转换(DO→VO):**调用 PageUtil.getPage () 工具方法,结合 Function 函数式接口传入 VO 构建逻辑(r -> CompanyListVO.builder ()),将数据库实体(DO)转换为前端展示 VO;
-
**分页元数据透传 + 空值过滤:**工具类完整保留原分页对象的总页数、总条数、当前页 / 页大小等元数据,同时通过 stream ().filter (Objects::nonNull) 过滤转换后的空值;
-
**统一结果返回:**返回转换后的 SimplePage<CompanyListVO>对象,包含前端所需的展示列表和完整分页元数据。
getCompanyDetailById
java
@GetMapping("/detail/{companyId}")
@Operation(summary = "企业详情")
public CommonResult<Company> getCompanyDetailById(
@PathVariable(required = false)
@Parameter(description = "企业ID")
@Validated
@NotNull(message = "MERCHANT_ID_NOT_NULL") Long companyId) {
return CommonResult.success(companyService.getCompanyDetailById(companyId, U.get()));
}
-
@PathVariable负责接收路径参数 ,required = false控制参数是否可选; -
@Parameter仅用于接口文档说明,不影响参数逻辑; -
@Validated + @NotNull组合:前者激活校验,后者执行 "参数非 null" 的校验规则。
getCompanyDropDownList
java
@GetMapping("/drop/down/list")
@Operation(summary = "企业下拉列表", description = "企业下拉列表")
public CommonResult<List<CompanyDropDown>> getCompanyDropDownList(@RequestParam(value = "companyId", required = false)
@Parameter(description = "企业ID") Long companyId) {
return CommonResult.success(companyService.getCompanyDropDownList(companyId, U.get()));
}
java
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) {
return companyRepository.getCompanyDropDownList(companyId, loginUser);
}
java
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) {
return cacheAll().stream()
.filter(c -> companyId == null || c.getId().equals(companyId))
.filter(c -> !loginUser.isEnterprise() || c.getId().equals(loginUser.getAuthCompanyId()))
.filter(c -> c.getId() != 1L)
.sorted(Comparator.comparing(Company::getName))
.map(CompanyDropDown::from)
.collect(Collectors.toList());
}
这段代码是企业级系统中「下拉列表数据查询 + 全量缓存优化」类功能的标准化实现框架,核心遵循「缓存全量加载→多维度过滤→排序转换→结果返回」的四层核心流程,具体拆解如下:
- 缓存全量加载:优先调用
cacheAll()加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率; - 多维度条件过滤:通过流式过滤实现三层规则校验 ------ 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
- 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
- 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。
java
public Company getCompanyDetailById(Long companyId, LoginUser loginUser) {
// 步骤1:从缓存中查询
Optional<Company> cacheCompany = getCompanyFromCache(companyId);
if (cacheCompany.isPresent()) {
Company company = cacheCompany.get();
// 步骤2:权限校验
if (loginUser.isEnterprise() && !company.getId().equals(loginUser.getAuthCompanyId())) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
return company;
}
// 步骤3:缓存未命中,查库兜底
Company company = companyRepository.getByIdWithAuth(companyId, loginUser);
if (company == null) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
// 步骤4:更新缓存
companyRepository.removeCache();
companyRepository.cacheAll();
return company;
}
java
public Company getByIdWithAuth(Long companyId, LoginUser loginUser) {
return getOne(Wrappers.<Company>lambdaQuery()
.eq(Company::getId, companyId)
.eq(loginUser.isEnterprise(), Company::getId, loginUser.getAuthCompanyId())
.last("limit 1"));
}
这段代码是企业级系统中「单实体详情查询 + 数据权限管控 + 缓存优化」类功能的标准化实现框架,核心遵循五层核心流程,具体拆解如下:
- 缓存优先精准查询:优先调用
getCompanyFromCache(companyId)从全量企业缓存中过滤匹配目标企业 ID 的数据,替代直接查库,利用缓存提升高频详情查询的响应效率; - 权限二次校验:即使缓存命中目标企业数据,仍需校验登录用户数据权限;
- 数据库兜底查询:若缓存未命中目标数据,则通过
Wrappers.lambdaQuery()构建查询条件,并通过last("limit 1")限定单条结果返回,同时查询后更新缓存保证后续命中率; - 结果有效性校验:对缓存 / 数据库查询结果做非空校验,若返回
null则抛出标准化业务异常,作为详情查询的通用兜底逻辑,避免空值引发前端渲染异常或下游业务空指针; - 合规结果返回:经缓存 / 数据库查询、权限校验、有效性校验后,返回符合条件的企业实体对象,既保证查询性能,又满足数据权限管控和业务完整性要求。
getCompanyDropDownList
java
@GetMapping("/drop/down/list")
@Operation(summary = "企业下拉列表", description = "企业下拉列表")
public CommonResult<List<CompanyDropDown>> getCompanyDropDownList(@RequestParam(value = "companyId", required = false)
@Parameter(description = "企业ID") Long companyId) {
return CommonResult.success(companyService.getCompanyDropDownList(companyId, U.get()));
}
java
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) {
return companyRepository.getCompanyDropDownList(companyId, loginUser);
}
java
public List<CompanyDropDown> getCompanyDropDownList(Long companyId, LoginUser loginUser) {
return cacheAll().stream()
.filter(c -> companyId == null || c.getId().equals(companyId))
.filter(c -> !loginUser.isEnterprise() || c.getId().equals(loginUser.getAuthCompanyId()))
.filter(c -> c.getId() != 1L)
.sorted(Comparator.comparing(Company::getName))
.map(CompanyDropDown::from)
.collect(Collectors.toList());
}
这段代码是企业级系统中「下拉列表数据查询 + 全量缓存优化」类功能的标准化实现框架,核心遵循「缓存全量加载→多维度过滤→排序转换→结果返回」的四层核心流程,具体拆解如下:
- 缓存全量加载:优先调用
cacheAll()加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率; - 多维度条件过滤:通过流式过滤实现三层规则校验 ------ 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
- 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
- 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。
addCompany
java
@Log(title = "新增企业")
@Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER})
@PutMapping
@Operation(summary = "新增企业")
public CommonResult<Long> addOperatorCompanyUser(@RequestBody @Validated AddCompanyDTO dto) {
return CommonResult.success(companyService.addCompany(dto, U.get()));
}
java
@Lock4j(lockType = "addCompany", key = "#dto.name")
@Transactional(rollbackFor = Exception.class)
public Long addCompany(AddCompanyDTO dto, LoginUser loginUser) {
long count = companyRepository.countByName(dto.getName());
if (count > 0) {
throw new BusinessException(ResponseCode.MERCHANT_EXISTS);
}
Company company = Company.builder()
.name(dto.getName())
.shortName(dto.getShortName())
.contact(dto.getContact())
.phone(dto.getPhone())
.address(dto.getAddress())
.build();
companyRepository.save(company);
log.info("用户:{} 添加企业:{}", loginUser.getUsername(), dto.getName());
companyRepository.removeCache();
if (Objects.equals(dto.getCreateMainAccount(), true)) {
AddSysUserDTO userDTO = new AddSysUserDTO();
userDTO.setRoles(Collections.singletonList(STRAUTH.MANAGER));
userDTO.setCompanyId(company.getId());
userDTO.setUsername(dto.getName());
userDTO.setPassword("Aa123456");
userDTO.setNickname(dto.getName());
userDTO.setStatus(true);
SpringUtil.getBean(CompanyUserService.class).addSysUser(userDTO);
log.info("用户:{} 创建企业主账号:{}", loginUser.getUsername(), dto.getName());
}
return company.getId();
}
这段代码是企业级系统中 "新增实体" 类功能的典型通用逻辑框架,核心遵循 "校验→构建→保存→扩展→返回" 的标准化流程,具体可拆解为 6 个核心步骤:
-
唯一性校验(防重复): 通过
companyRepository.countByName(dto.getName())校验企业名称是否已存在,避免重复创建,若重复则抛出业务异常; -
参数转换(DTO→实体): 将入参
AddCompanyDTO(数据传输对象)转换为Company数据库实体对象,仅保留业务所需字段,完成 "前端参数→数据库模型" 的映射; -
数据持久化(事务保障): 通过
companyRepository.save(company)将实体保存数据库,结合@Transactional(rollbackFor = Exception.class)注解,确保保存失败时事务回滚,保证数据一致; -
缓存清理(性能优化): 调用
companyRepository.removeCache()清理缓存,避免新增数据与缓存数据不一致(适用于有缓存场景的通用操作); -
扩展业务处理(可选): 根据
createMainAccount参数判断是否创建企业主账号,通过 Spring 工具类调用其他服务完成关联业务,体现 "主业务 + 附属业务" 的扩展逻辑; -
**日志记录 + 结果返回:**记录关键操作日志,最终返回新增实体的主键 ID;
-
并发控制(额外保障): 通过
@Lock4j(lockType = "addCompany", key = "#dto.name")基于企业名称加分布式锁,解决高并发下的重复创建问题(新增类功能的进阶并发防护)。
updateCompany
java
@Log(title = "修改企业")
@PostMapping
@Operation(summary = "修改企业")
public CommonResult<Boolean> updateCompany(@RequestBody @Validated UpdateCompanyDTO dto) {
return CommonResult.success(companyService.updateCompany(dto, U.get()));
}
java
@Lock4j(lockType = "updateCompany", key = "#dto.companyId")
@Transactional(rollbackFor = Exception.class)
public Boolean updateCompany(UpdateCompanyDTO dto, LoginUser loginUser) {
Company company = companyRepository.getById(dto.getCompanyId());
if (company == null) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
if (Boolean.TRUE.equals(loginUser.isAdmin()) ||
Boolean.TRUE.equals(loginUser.isManager()) ||
company.getId().equals(loginUser.getCompanyId())) {
companyRepository.updateCompany(dto);
log.info("用户:{} 更新企业:{}", loginUser.getUsername(), dto.getName());
return true;
}
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
java
public void updateCompany(UpdateCompanyDTO dto) {
update(Wrappers.<Company>lambdaUpdate()
.set(StringUtils.isNotBlank(dto.getName()), Company::getName, dto.getName())
.set(StringUtils.isNotBlank(dto.getShortName()), Company::getShortName, dto.getShortName())
.set(StringUtils.isNotBlank(dto.getContact()), Company::getContact, dto.getContact())
.set(StringUtils.isNotBlank(dto.getPhone()), Company::getPhone, dto.getPhone())
.set(StringUtils.isNotBlank(dto.getAddress()), Company::getAddress, dto.getAddress())
.eq(Company::getId, dto.getCompanyId()));
removeCache();
}
这段代码是企业级系统中 "实体更新" 类功能的典型通用逻辑框架,核心遵循 "存在性校验→权限校验→数据更新→结果返回" 的标准化流程,具体可拆解为 5 个核心步骤:
-
存在性校验(防无效更新):通过 companyRepository.getById (dto.getCompanyId ()) 查询目标企业是否存在,若不存在则抛出业务异常(更新类功能的通用前置校验,避免更新不存在的实体);
-
权限校验(防越权操作):校验当前登录用户是否为管理员 / 经理,或是否归属该企业,仅满足权限条件才允许更新(企业级系统更新操作的核心权限控制,避免越权修改数据);
-
数据更新(事务保障):调用 companyRepository.updateCompany (dto) 执行企业信息更新,结合 @Transactional (rollbackFor = Exception.class) 注解,确保更新失败时事务回滚,保证数据一致性;
-
并发控制(额外保障):通过 @Lock4j (lockType = "updateCompany", key = "#dto.companyId") 基于企业 ID 加分布式锁,解决高并发下的重复更新问题(更新类功能的进阶并发防护);
-
日志记录 + 结果返回:记录用户更新企业的关键操作日志(便于审计和问题排查),权限校验通过则返回 true,未通过则抛出权限相关业务异常(更新功能的通用返回规范)。
delCompany
java
@Log(title = "删除企业")
@Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER})
@DeleteMapping("/{companyId}")
@Operation(summary = "删除企业")
public CommonResult<Boolean> delCompany(
@PathVariable(required = false) @Validated @NotNull(message = "企业id不能为空") Long companyId) {
return CommonResult.success(companyService.delCompany(companyId, U.get()));
}
java
@Lock4j(lockType = "delCompany", key = "#companyId")
@Transactional(rollbackFor = Exception.class)
public Boolean delCompany(Long companyId, LoginUser loginUser) {
Company company = companyRepository.getById(companyId);
if (company == null) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
if (company.getId() == 1L) {
throw new BusinessException(ResponseCode.MERCHANT_DISABLE_DELETED);
}
// 检查企业下是否有设备,检查企业是否存在用户子账号
if (deviceRepository.countByCompanyId(companyId) > 0) {
throw new BusinessException(ResponseCode.MERCHANT_DEVICE_EXISTS);
}
if (userRepository.countByCompanyId(companyId) > 0) {
throw new BusinessException(ResponseCode.MERCHANT_USER_EXISTS);
}
companyRepository.removeById(companyId);
log.info("用户:{} 删除企业:{}", loginUser.getUsername(), company.getName());
companyRepository.removeCache();
return true;
}
java
public void removeCache() {
redisService.del("company:all");
}
这段代码是企业级系统中 "实体删除" 类功能的典型通用逻辑框架,核心遵循 "存在性校验→特殊规则校验→关联数据校验→数据删除→结果返回" 的标准化流程,具体可拆解为 6 个核心步骤:
-
存在性校验(防无效删除):通过 companyRepository.getById (companyId) 查询目标企业是否存在,若不存在则抛出业务异常(ResponseCode.MERCHANT_NOT_EXISTS);
-
特殊规则校验(防误删核心数据):校验目标企业是否为系统核心企业(ID=1L),若是则抛出业务异常(ResponseCode.MERCHANT_DISABLE_DELETED);
-
关联数据校验(防数据脏删):依次检查企业下是否关联设备(deviceRepository.countByCompanyId)、是否存在用户子账号(userRepository.countByCompanyId),若存在关联数据则抛出对应业务异常;
-
数据删除(事务保障):调用 companyRepository.removeById (companyId) 执行企业删除操作,结合 @Transactional注解,确保删除失败时事务回滚,保证数据一致性;
-
并发控制 + 缓存清理(额外保障):通过 @Lock4j (lockType = "delCompany", key = "#companyId") 基于企业 ID 加分布式锁,解决高并发下的重复删除问题;删除后调用 companyRepository.removeCache () 清理缓存,避免删除数据与缓存数据不一致。
-
日志记录 + 结果返回:记录用户删除企业的关键操作日志,所有校验通过后返回 true。
企业用户账号管理模块
sysUserList
java
@PostMapping("/list")
@Operation(summary = "系统用户列表")
public CommonResult<SimplePage<SysUserListVO>> sysUserList(@RequestBody SysUserListDTO dto) {
return CommonResult.success(userService.userList(dto));
}
java
public SimplePage<SysUserListVO> userList(SysUserListDTO dto) {
return new SimplePage<>(userRepository.getUserList(Page.of(dto.getPageNum(), dto.getPageSize()), dto, U.get()));
}
java
public Page<SysUserListVO> getUserList(Page page, SysUserListDTO dto, LoginUser loginUser) {
Page<SysUserListVO> list = baseMapper.getUserList(page, dto, loginUser);
for (SysUserListVO vo : list.getRecords()) {
vo.setRoleList(Arrays.asList(vo.getRoleString().split(",")));
}
return list;
}
java
Page<SysUserListVO> getUserList(@Param("page") Page page, @Param("dto") SysUserListDTO dto, @Param("loginUser") LoginUser loginUser);
XML
<select id="getUserList" resultType="com.daochengtech.lock.platform.core.model.vo.SysUserListVO">
select u.id as userId,
u.u_username as username,
company_id AS companyId,
u_role AS roleString,
u_nickname AS nickname,
u_avatar AS avatar,
u_enable AS status,
c.c_name as companyName,
u.create_time AS createTime,
u.modify_time AS modifyTime
from (select *
from dc_user
where deleted = 0
<if test="dto.companyId != null">
company_id = #{dto.companyId}
</if>
<if test="dto.username != null">
and u_username like concat('%',#{dto.username},'%')
</if>
<if test="loginUser !=null and loginUser.isEnterprise() == true">
and company_id = #{loginUser.companyId}
</if>
<if test="dto.userId != null">
and id = #{dto.userId}
</if>
) u
left join dc_company c on u.company_id = c.id
</select>
这段代码是企业级系统中 "系统用户分页列表查询(多条件 + 权限过滤 + 结果格式化)" 类功能的典型通用逻辑框架,具体可拆解为 6 个核心步骤:
-
接口层接收请求:通过 @PostMapping ("/list") 定义用户列表接口;
-
服务层分页封装:调用 Page.of (dto.getPageNum (), dto.getPageSize ()) 封装分页参数,传入仓储层查询方法,返回 SimplePage 分页对象 ;
-
仓储层动态条件查询:Mapper 层通过 XML 构建动态 SQL,支持企业 ID、用户名、用户 ID 等业务条件过滤,同时追加登录用户权限过滤,并关联企业表查询企业名称;
-
结果格式化处理:查询结果返回后,遍历分页列表将角色字符串(roleString)拆分为角色列表(roleList),补充到 VO 中 ;
-
分层数据返回:Mapper 层返回 Page<SysUserListVO>,服务层封装为 SimplePage,接口层最终通过 CommonResult.success () 统一返回 ;
-
数据安全过滤:SQL 中默认追加 deleted = 0 条件,过滤已删除数据。
userDetail
java
@PostMapping("/detail")
@Operation(summary = "用户详情")
public CommonResult<UserDetailVO> userDetail(@RequestParam(value = "userId", required = false)
@Validated @NotNull(message = "ID_NOT_NULL") Long userId) {
return CommonResult.success(userService.userDetail(userId));
}
java
public UserDetailVO userDetail(Long userId) {
UserDetailVO userDetail = userRepository.getUserDetail(userId, U.get());
userDetail.setRoleList(Arrays.stream(userDetail.getRoleString().split(",")).toList());
return userDetail;
}
java
public UserDetailVO getUserDetail(Long userId, LoginUser loginUser) {
return baseMapper.getUserDetail(userId, loginUser);
}
java
UserDetailVO getUserDetail(@Param("userId") Long userId, @Param("loginUser") LoginUser loginUser);
XML
<select id="getUserDetail" resultType="com.daochengtech.lock.platform.core.model.vo.UserDetailVO">
select u.id as userId,
u.u_username as username,
c.id AS companyId,
c.c_name as companyName,
u_role AS roleString,
u.u_username AS username,
u_nickname AS nickname,
u_avatar AS avatar,
u_enable AS status,
u.create_time AS createTime,
u.modify_time AS modifyTime
from dc_user u
left join dc_company c on u.company_id = c.id
where u.deleted = 0
and u.id = #{userId}
</select>
这段代码是企业级系统中 "用户详情查询(参数校验 + 结果格式化)" 类功能的典型通用逻辑框架,具体可拆解为 5 个核心步骤:
-
接口层参数校验:通过 @RequestParam 接收用户 ID 参数,配合 @Validated + @NotNull 强制校验参数非空,若为空则抛出校验异常(详情查询的通用参数防护,避免无效查询);
-
服务层查询调用(逻辑转发):调用仓储层 getUserDetail () 方法,传入用户 ID 和当前登录用户信息,获取原始详情数据;
-
仓储层精准查询(数据兜底):Mapper 层通过 XML 构建精准 SQL,根据用户 ID 查询用户基础信息,并关联企业表查询所属企业名称,同时过滤已删除数据(deleted = 0);
-
结果格式化处理:将返回的角色字符串 roleString 拆分为角色列表 roleList ,补充 VO;
-
统一结果返回:接口层通过 CommonResult.success () 返回格式化后的 UserDetailVO。
systemUserResetPwd
java
@Log(title = "重设密码")
@PostMapping("/reset/pwd")
@Operation(summary = "重设密码")
public CommonResult<Boolean> systemUserResetPwd(@RequestBody @Validated SysUserRestPwdDTO dto) {
return CommonResult.success(userService.sysUserRestPwd(dto, U.get()));
}
java
@Transactional(rollbackFor = Exception.class)
@Lock4j(lockType = "SYS_USER_RESET_PWD", key = "#dto.getUserId()")
public Boolean sysUserRestPwd(SysUserRestPwdDTO dto, LoginUser loginUser) {
User user = userRepository.getById(dto.getUserId());
if (user == null) {
throw new BusinessException(ResponseCode.GET_USER_ERROR);
}
if (loginUser.isEnterprise() && !user.getId().equals(loginUser.getId())) {
throw new BusinessException(ResponseCode.USER_NOT_EXIST);
}
userRepository.sysUserRestPwd(dto);
tokenService.logout(user.getUsername());
return true;
}
java
public Boolean sysUserRestPwd(SysUserRestPwdDTO dto) {
return this.update(Wrappers.<User>lambdaUpdate()
.set(User::getPassword, passwordEncoder.encode(dto.getNewPassword()))
.eq(User::getId, dto.getUserId()));
}
java
public void logout(String username) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(
RequestAttributes.REFERENCE_REQUEST);
if (request == null) {
return;
}
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
redisService.del(key);
} catch (Exception e) {
log.error("登出时删除Token异常: {}", e.getMessage());
}
}
这段代码是企业级系统中 "用户密码重设(含权限校验 + 安全登出)" 类功能的典型通用逻辑框架,具体可拆解为 7 个核心步骤:
-
接口层参数校验(防非法请求):通过 @RequestBody + @Validated 触发 DTO 内部的参数校验规则,确保重置密码所需参数合法;
-
存在性校验:通过 getById () 查询目标用户是否存在,若不存在则抛出业务异常;
-
权限校验(防越权操作):校验当前登录用户是否为企业用户且非目标用户本人,若是则抛出业务异常(密码重置的核心权限控制,避免越权重置他人密码);
-
密码更新(事务保障):调用 userRepository.sysUserRestPwd (dto) 更新用户密码,密码通过 encode () 加密后存储,结合 @Transactional注解保证更新失败时事务回滚;
-
并发控制(额外保障):通过 @Lock4j () 基于用户 ID 加分布式锁,解决高并发下的重复重置密码问题(密码操作的进阶并发防护);
-
安全登出处理(避免旧密码 token 有效):调用 tokenService.logout () 方法,解析用户当前登录 Token 并删除 Redis 中对应的 Token 缓存,强制用户重新登录.
addSysUser
java
@Log(title = "添加系统用户")
@Auth(value = {STRAUTH.ADMIN, STRAUTH.MANAGER, STRAUTH.ENTERPRISE})
@PostMapping("/add/account")
@Operation(summary = "添加系统用户")
public CommonResult<Boolean> addSysUser(@RequestBody @Validated AddSysUserDTO dto) {
return CommonResult.success(userService.addSysUser(dto));
}
java
@Transactional(rollbackFor = Exception.class)
@Lock4j(lockType = "SYS_USER_ADD", key = "#dto.getUsername()")
public Boolean addSysUser(AddSysUserDTO dto) {
if (userRepository.checkUsername(dto.getUsername())) {
return userRepository.addSysUser(dto);
}
throw new BusinessException(ResponseCode.USERNAME_EXIST);
}
java
public boolean checkUsername(String username) {
return this.count(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username)) <= 0;
}
java
public Boolean addSysUser(AddSysUserDTO dto) {
Company company = companyRepository.getById(dto.getCompanyId());
if (company == null) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
User user = User.builder()
.username(dto.getUsername())
.password(passwordEncoder.encode(dto.getPassword()))
.companyId(company.getId())
.avatar(dto.getAvatar())
.role(String.join(",", dto.getRoles()).replace(" ", ""))
.nickname(Optional.ofNullable(dto.getNickname()).orElse(dto.getUsername()))
.enable(dto.getStatus())
.build();
return this.save(user);
}
这段代码是企业级系统中 "添加系统用户(含唯一性校验 + 权限控制)"类功能的典型通用逻辑框架,具体可拆解为 7 个核心步骤:
-
接口层参数校验(防非法请求):通过 @RequestBody + @Validated 触发 DTO 内部的参数校验规则,确保新增用户所需参数合法;
-
用户名唯一性校验(防重复创建):调用 userRepository.checkUsername () 方法,通过 count () 统计用户名是否已存在,若已存在则抛出业务异常;
-
企业存在性校验(防无效关联):通过 companyRepository.getById (dto.getCompanyId ()) 查询关联企业是否存在,若不存在则抛出业务异常;
-
用户实体构建(数据映射):将 AddSysUserDTO 转换为 User 数据库实体,密码通过 encode () 加密存储,角色列表拼接为字符串,昵称为空时兜底为用户名;
-
数据保存(事务保障):调用 this.save (user) 保存用户实体,结合 @Transactional (rollbackFor = Exception.class) 注解,确保保存失败时事务回滚.
第三方系统访问模块
getToken
java
/**
* 获取访问Token
*/
@Operation(summary = "获取访问Token", description = "使用API密钥和秘钥获取访问Token,用于后续API调用")
@PostMapping("/auth/token")
public CommonResult<TokenResponse> getToken(@Valid @RequestBody TokenRequest request) {
log.info("OpenAPI Token获取请求,API Key: {}", request.getApiKey());
try {
TokenResponse tokenResponse = openApiService.generateTokenResponse(request.getApiKey(), request.getSecret());
log.info("OpenAPI Token生成成功,API Key: {}", request.getApiKey());
return CommonResult.success(tokenResponse);
} catch (Exception e) {
log.error("OpenAPI Token生成失败,API Key: {}", request.getApiKey(), e);
throw e;
}
}
java
public TokenResponse generateTokenResponse(String apiKey, String secret) {
log.info("生成Token响应,apiKey: {}", apiKey);
TokenInfo tokenInfo = generateToken(apiKey, secret);
// 计算过期时间(秒)
long expiresIn = java.time.Duration.between(LocalDateTime.now(), tokenInfo.getExpiresAt()).getSeconds();
return TokenResponse.builder()
.token(tokenInfo.getToken())
.tokenType("Bearer")
.expiresIn(expiresIn)
.scope("device:control")
.build();
}
java
public TokenInfo generateToken(String apiKey, String secret) {
log.info("生成Token,apiKey: {}", apiKey);
// 验证API密钥和秘钥
ApiKey entity = apiKeyRepository.findByApiKey(apiKey);
if (entity == null) {
throw new BusinessException(ResponseCode.FAIL, "API密钥不存在");
}
if (!entity.getEnabled()) {
throw new BusinessException(ResponseCode.FAIL, "API密钥已禁用");
}
if (!SecretEncoder.matches(secret, entity.getSecret())) {
throw new BusinessException(ResponseCode.FAIL, "秘钥错误");
}
// 撤销现有Token(一个API密钥同时只能有一个有效Token)
tokenRepository.revokeTokensByApiKey(apiKey);
// 生成新Token
String token = ApiKeyGenerator.generateToken();
LocalDateTime now = LocalDateTime.now();
LocalDateTime expiresAt = now.plusHours(DEFAULT_TOKEN_EXPIRE_HOURS);
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setToken(token);
tokenInfo.setApiKey(apiKey);
tokenInfo.setCreatedAt(now);
tokenInfo.setExpiresAt(expiresAt);
// 保存到Redis
tokenRepository.saveToken(tokenInfo, DEFAULT_TOKEN_EXPIRE_HOURS);
log.info("Token生成成功,apiKey: {}, token: {}", apiKey, token);
return tokenInfo;
}
这段代码是企业级开放平台(OpenAPI)中访问 Token 生成与管控的核心业务逻辑,面向第三方系统 / 客户端提供 API 调用的身份认证能力,核心解决 "API 调用的合法性校验、Token 唯一性管控、过期自动失效" 三大问题,具体业务流程和设计思路拆解如下:
一、核心业务定位
该功能是 OpenAPI 的 "身份网关",第三方系统需先通过接口提交API Key(应用标识) 和Secret(应用秘钥) 获取 Token,后续所有 API 调用都需在请求头中携带该 Token,系统通过校验 Token 的有效性完成身份认证,避免 API 被非法调用。
二、完整业务流程
- 请求接收与日志记录 :前端 / 第三方系统调用
/auth/token接口,传入apiKey和secret,接口先记录请求日志(含apiKey),便于后续问题排查; - API Key 合法性校验 :根据
apiKey查询数据库中的ApiKey实体,校验三大规则:apiKey是否存在(不存在则抛 "API 密钥不存在" 异常);apiKey是否启用(禁用则抛 "API 密钥已禁用" 异常);secret是否匹配(通过matches()校验加密后的秘钥,错误则抛 "秘钥错误" 异常);
- 旧 Token 强制撤销 :为保证 "一个 API Key 同时仅存在一个有效 Token",先撤销该
apiKey关联的所有已存在 Token; - 新 Token 生成 :通过
ApiKeyGenerator.generateToken()生成随机、唯一的 Token 字符串; - Token 有效期设置 :设定 Token 过期时间,计算当前时间到过期时间的秒数(
expiresIn),用于告知第三方 Token 的有效时长; - Token 持久化存储 :将 Token、
apiKey、创建时间、过期时间封装为TokenInfo,保存到 Redis,利用 Redis 的过期机制实现 Token 自动失效,减轻数据库查询压力; - 响应数据封装 :构建
TokenResponse返回给调用方,包含核心字段:token:实际用于 API 调用的令牌字符串;tokenType:固定为Bearer;expiresIn:Token 过期剩余秒数;scope:Token 权限范围;
- 异常兜底与日志:全程捕获异常并记录错误日志,异常直接抛出由全局异常处理器返回标准化错误,保证调用方能清晰感知失败原因(如秘钥错误、密钥禁用)。
refreshToken
java
@PostMapping("/refreshToken")
@Operation(summary = "刷新token")
public CommonResult<String> refreshToken(HttpServletRequest request) {
return CommonResult.success(systemService.refreshToken(request));
}
java
/**
public String refreshToken(HttpServletRequest request) {
//秒
Long expireTime = tokenService.getExpireTime(request);
// 如果剩30分钟就刷新token
if (expireTime < 30 * 60) {
return tokenService.createToken(U.get());
}
return tokenService.getToken(request);
}
*/
public String refreshToken(HttpServletRequest request) {
//秒
Long expireTime = tokenService.getExpireTime(request);
// 如果剩30分钟就刷新token
if (expireTime < 30 * 60) {
String oldToken = tokenService.getToken(request);
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser == null) {
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "旧Token无效,无法刷新");
}
String newToken = tokenService.createToken(loginUser);
tokenService.invalidateOldToken(oldToken);
return newToken;
}
return tokenService.getToken(request);
}
这段代码是企业级系统中Token 刷新(refreshToken) 核心业务逻辑,属于用户认证会话管理的关键能力,核心解决 "Token 即将过期时无感刷新、保证用户登录态持续有效" 的问题,同时兼顾 Token 安全性管控。
代码规范
判空工具类
核心推荐:ObjectUtil(Hutool 的ObjectUtil)能覆盖业务中 99% 以上的常用类型判空场景
1.字符串判空
-
仅想判断 "字符串是不是 null":直接用
str == null; -
想判断 "字符串是 null,或者是空字符串("")":优先用
StringUtils.isEmpty(str),替代繁琐的str == null || str ==""; -
想判断 "字符串无任何有效字符"(包括 null、""、全空格):必须用
StringUtils.isBlank(str),比如用户输入的空格、换行符都能被识别; -
想判断 "字符串非空且有有效字符":用
StringUtils.isNotBlank(str),是业务中最常用非空判断。
2.基本数据类型判空
基本数据类型(int、long、boolean 等)没有 null 的概念,因为它们是值类型,不是对象:
-
不存在 "判 null" 的说法,只能判断 "是否为默认值":比如 int 默认值是 0,就用
num == 0;long 默认值是 0L,就用num == 0L;boolean 默认值是 false,就用flag == false; -
绝对不能写
int num == null,会直接编译报错。
3.包装类判空
包装类是对象,既有 "引用是否为空",也有 "值是否有效":
-
仅判断 "引用是不是 null":直接用
num == null(如Integer num == null); -
既判 null,又判值是否为默认值:先判
num == null,再判num == 0,或用 Hutool 的ObjectUtil.isEmpty(num) || num == 0; -
非空且值有效:如判断 Integer 非空且大于 0,用
ObjectUtil.isNotEmpty(num) && num > 0。
4.数组判空
数组是对象,判空要兼顾 "引用为空" 和 "数组长度为 0":
-
仅判断 "数组引用是不是 null":用
arr == null; -
既判 null,又判空数组:优先用 Hutool 的
ObjectUtil.isEmpty(arr); -
多维数组(比如 String [][]):需要逐层判空,先判外层数组非空,再判内层数组非空。
5.普通对象判空
-
自定义对象:仅判断 "引用是不是 null",直接用
obj == null(比如User user == null); -
集合(List、Map):判空要兼顾 "引用为空" 和 "集合为空",用
ObjectUtil.isEmpty(list); -
通用非空判断:不管是自定义对象、包装类还是数组,都能用
ObjectUtil.isNotEmpty(obj)。
判空注解
1. @NotNull
@NotNull 是通用性最强的非空注解,可作用于所有数据类型 (包括基本类型包装类、字符串、集合、自定义对象等)。它的核心校验规则仅判定目标值是否为 null,不关注值的内容:即便目标是字符串(如 "")、空集合(如 new ArrayList<>()),只要不是 null,该注解的校验就会通过。
2. @NotBlank
@NotBlank 是专门针对 String 类型 的强非空注解,校验规则比 @NotNull 更严格:它不仅要求目标字符串不能为 null,还要求去除首尾空格后字符串的长度必须大于 0。也就是说,null、空字符串 ""、仅包含空格的字符串 " " 都会被该注解判定为校验失败,只有包含实际有效字符的字符串(如 "张三")才能通过校验。
3. @NotEmpty
@NotEmpty 适用于 String 类型、集合(List/Set/Map)、数组 ,核心规则是:目标值不能为 null,且对应的 "长度 / 大小" 必须大于 0。对字符串而言,它仅判定是否为 null 或空字符串 ""(不处理空格,比如 " " 会被判定为通过);对集合 / 数组而言,它判定是否为 null 或空容器(无元素),只要容器中有至少一个元素,即便元素本身是 null,也能通过校验。
日志框架
XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 引入spirng boot默认的logback配置文件 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<!-- Simple file output -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- <File>/log/common-service.log</File> -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover -->
<FileNamePattern>/var/log/${appName}/%d{yyyy-MM-dd,aux}/%d{yyyy-MM-dd HH}.log</FileNamePattern>
<!-- keep 30 days' worth of history -->
<maxHistory>744</maxHistory>
</rollingPolicy>
<encoder>
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern>
</encoder>
</appender>
<!-- Console output -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- 采用Spring boot中默认的控制台彩色日志输出模板 -->
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
<!-- Only log level WARN and above -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- 异步输出 -->
<!--出现了丢失日志的问题-->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNC_STDOUT" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref="STDOUT"/>
</appender>
<!-- Enable FILE and STDOUT appenders for all log messages.
By default, only log at level INFO and above. -->
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ASYNC_STDOUT"/>
</root>
</configuration>
模块1:基础配置(全局开关)
XML
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>
核心作用:
-
是整个日志配置的 "基础底座",定义全局规则(自动刷新配置、关闭框架调试日志);
-
复用 Spring Boot 官方的默认日志规则(不用自己写基础格式);
-
读取项目名称,后续日志文件路径会用这名称,让日志文件和项目绑定。
模块2:文件输出模块(FILE Appender)
XML
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<FileNamePattern>/var/log/${appName}/%d{yyyy-MM-dd,aux}/%d{yyyy-MM-dd HH}.log</FileNamePattern>
<maxHistory>744</maxHistory>
</rollingPolicy>
<encoder>
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern>
</encoder>
</appender>
核心作用:
-
定义 "日志写进文件" 的规则,是最核心的文件日志输出器;
-
按 "小时" 切分日志文件(比如每小时生成一个新日志文件),按 "日" 建文件夹,避免单个文件过大;
-
日志只保留30天(744小时),自动清理旧日志,防止占满服务器磁盘;
-
定义日志内容的格式(包含时间、线程、级别、类名、日志内容)。
模块3:控制台输出模块(STDOUT Appender)
XML
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
核心作用:
-
定义"日志输出到控制台"的规则,方便开发时实时看日志;
-
用 Spring Boot 自带的彩色日志格式(ERROR 红、INFO 绿),视觉更清晰;
-
过滤掉 TRACE 级别的日志(只输出 DEBUG 及以上),减少控制台无用日志。
模块4:异步输出优化模块(ASYNC_* Appender)
java
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNC_STDOUT" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="STDOUT"/>
</appender>
核心作用:
-
给"文件输出"和 "控制台输出"加"异步缓冲",是性能优化模块;
-
主线程打日志时不用等日志写完,直接继续执行代码,避免日志写入拖慢程序;
-
配置"不丢日志"(队列满了也不丢弃),同时调整队列大小(512)平衡性能和内存。
模块5:全局日志开关模块(root)
java
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ASYNC_STDOUT"/>
</root>
核心作用:
-
整个项目的日志"总控制",定义全局日志级别为 INFO(只输出 INFO、WARN、ERROR);
-
绑定前面异步输出器,最终实现所有符合级别的日志,既异步写文件,又异步输出到控制台。
Optional 判空
传统判空写法
java
String message;
if (e == null || e.getMessage() == null) {
message = "";
} else {
message = e.getMessage();
}
Optional 简化写法
java
String message = Optional.ofNullable(e.getMessage()).orElse("");
切面优先级
java
@Aspect
@Slf4j
@Order(-10)
@Component
public class LockAop {}
@Transactional(rollbackFor = Exception.class)
@Lock4j(lockType = "addCompany", key = "#dto.name")
public Long addCompany(AddCompanyDTO dto, LoginUser loginUser) {
//业务逻辑...
}
debug调试:

@Transactional 对应的 Spring 事务切面默认优先级数值是 Integer.MAX_VALUE ,@Order 数值越小优先级越高,因此 -10 > 2147483647,LockAop 先执行前置逻辑、后执行后置逻辑。
同一方法上多个切面(含 Spring 内置事务切面)的执行逻辑:标注 @Order 的切面按数值越小优先级越高,无 @Order 的内置切面(如 @Transactional)默认优先级最低;高优先级切面的环绕通知会先执行前置逻辑,再触发低优先级切面 / 目标方法,最后执行高优先级切面的后置逻辑。
通用能力封装
RedisService 封装类
1. Redis 统一配置:序列化 + 缓存时效 + 自定义服务注入
java
@EnableCaching
@Configuration
@Slf4j
public class BaseRedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<Object> serializer = redisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisSerializer<Object> redisSerializer() {
//创建JSON序列化器
ObjectMapper objectMapper = new ObjectMapper();
//支持LocalDate
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//必须设置,否则无法将JSON转化为对象,会转化成Map类型
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
//设置Redis缓存有效期为1天
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
@Bean
public RedisService redisService(RedisTemplate<String, Object> redisTemplate) {
return new RedisServiceImpl(redisTemplate);
}
}
2. 定义 Redis 通用操作接口,封装 Redis 各类数据结构操作
java
public interface RedisService {
/**
* 保存属性 秒
*/
void set(String key, Object value, long time);
/**
* 保存属性 指定时间 Duration
*/
void set(String key, Object value, Duration duration);
/**
* 保存属性
*/
void set(String key, Object value);
Boolean setIfAbsent(String key, Object value, Duration duration);
Boolean setIfAbsent(String key, Object value);
/**
* 获取属性
*/
Object get(String key);
<T> T get(String key, Class<T> T);
/**
* 删除属性
*/
Boolean del(String key);
/**
* 批量删除属性
*/
Long del(List<String> keys);
/**
* 设置过期时间
*/
Boolean expire(String key, long time);
/**
* 获取过期时间
*/
Long getExpire(String key);
/**
* 判断是否有该属性
*/
Boolean hasKey(String key);
/**
* 按delta递增
*/
Long incr(String key, long delta);
/**
* 按delta递减
*/
Long decr(String key, long delta);
/**
* 获取Hash结构中的属性
*/
Object hGet(String key, String hashKey);
/**
* 向Hash结构中放入一个属性
* 单位:秒
*/
Boolean hSet(String key, String hashKey, Object value, long time);
void hSetIfAbsent(String key, String hashKey, Object value);
/**
* 向Hash结构中放入一个属性
*/
void hSet(String key, String hashKey, Object value);
/**
* 直接获取整个Hash结构
*/
Map<Object, Object> hGetAll(String key);
/**
* 直接设置整个Hash结构
*/
Boolean hSetAll(String key, Map<String, Object> map, long time);
/**
* 直接设置整个Hash结构
*/
void hSetAll(String key, Map<String, ?> map);
/**
* 删除Hash结构中的属性
*/
void hDel(String key, Object... hashKey);
/**
* 判断Hash结构中是否有该属性
*/
Boolean hHasKey(String key, String hashKey);
/**
* Hash结构中属性递增
*/
Long hIncr(String key, String hashKey, Long delta);
/**
* Hash结构中属性递减
*/
Long hDecr(String key, String hashKey, Long delta);
/**
* 获取Set结构
*/
Set<Object> sMembers(String key);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, Object... values);
/**
* 向Set结构中添加属性
*/
Long sAdd(String key, long time, Object... values);
/**
* 是否为Set中的属性
*/
Boolean sIsMember(String key, Object value);
/**
* 获取Set结构的长度
*/
Long sSize(String key);
/**
* 删除Set结构中的属性
*/
Long sRemove(String key, Object... values);
/**
* 获取List结构中的属性
*/
List<Object> lRange(String key, long start, long end);
/**
* 获取List结构的长度
*/
Long lSize(String key);
/**
* 根据索引获取List中的属性
*/
Object lIndex(String key, long index);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value);
/**
* 向List结构中添加属性
*/
Long lPush(String key, Object value, long time);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Object... values);
/**
* 向List结构中批量添加属性
*/
Long lPushAll(String key, Long time, Object... values);
/**
* 从List结构中移除属性
*/
Long lRemove(String key, long count, Object value);
void setExpire(String key);
Set<String> keys(String key);
/**
* 模糊匹配删除
*/
void delByPattern(String pattern);
}
3. 实现 RedisService 接口,封装多结构 Redis 操作逻辑
java
@Slf4j
@AllArgsConstructor
public class RedisServiceImpl implements RedisService {
private RedisTemplate<String, Object> redisTemplate;
@Override
public void set(String key, Object value, long time) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}
@Override
public void set(String key, Object value, Duration duration) {
redisTemplate.opsForValue().set(key, value, duration);
}
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public Boolean setIfAbsent(String key, Object value, Duration duration) {
return redisTemplate.opsForValue().setIfAbsent(key, value, duration);
}
@Override
public Boolean setIfAbsent(String key, Object value) {
return redisTemplate.opsForValue().setIfAbsent(key, value);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
@SuppressWarnings("unchecked")
public <T> T get(String key, Class<T> clazz) {
Object entity = redisTemplate.opsForValue().get(key);
try {
if (entity != null) {
return (T) entity;
}
} catch (Exception e) {
throw new RuntimeException("redis get key is error");
}
return null;
}
@Override
public Boolean del(String key) {
return redisTemplate.delete(key);
}
@Override
public Long del(List<String> keys) {
return redisTemplate.delete(keys);
}
@Override
public Boolean expire(String key, long time) {
return redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
@Override
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
@Override
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
@Override
public Long incr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
@Override
public Long decr(String key, long delta) {
return redisTemplate.opsForValue().increment(key, -delta);
}
@Override
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
@Override
public Boolean hSet(String key, String hashKey, Object value, long time) {
redisTemplate.opsForHash().put(key, hashKey, value);
return expire(key, time);
}
@Override
public void hSetIfAbsent(String key, String hashKey, Object value) {
redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
}
@Override
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
@Override
public Map<Object, Object> hGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
@Override
public Boolean hSetAll(String key, Map<String, Object> map, long time) {
redisTemplate.opsForHash().putAll(key, map);
return expire(key, time);
}
@Override
public void hSetAll(String key, Map<String, ?> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void hDel(String key, Object... hashKey) {
redisTemplate.opsForHash().delete(key, hashKey);
}
@Override
public Boolean hHasKey(String key, String hashKey) {
return redisTemplate.opsForHash().hasKey(key, hashKey);
}
@Override
public Long hIncr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, delta);
}
@Override
public Long hDecr(String key, String hashKey, Long delta) {
return redisTemplate.opsForHash().increment(key, hashKey, -delta);
}
@Override
public Set<Object> sMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
@Override
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
@Override
public Long sAdd(String key, long time, Object... values) {
Long count = redisTemplate.opsForSet().add(key, values);
expire(key, time);
return count;
}
@Override
public Boolean sIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
@Override
public Long sSize(String key) {
return redisTemplate.opsForSet().size(key);
}
@Override
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
@Override
public List<Object> lRange(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
@Override
public Long lSize(String key) {
return redisTemplate.opsForList().size(key);
}
@Override
public Object lIndex(String key, long index) {
return redisTemplate.opsForList().index(key, index);
}
@Override
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
@Override
public Long lPush(String key, Object value, long time) {
Long index = redisTemplate.opsForList().rightPush(key, value);
expire(key, time);
return index;
}
@Override
public Long lPushAll(String key, Object... values) {
return redisTemplate.opsForList().rightPushAll(key, values);
}
@Override
public Long lPushAll(String key, Long time, Object... values) {
Long count = redisTemplate.opsForList().rightPushAll(key, values);
expire(key, time);
return count;
}
@Override
public Long lRemove(String key, long count, Object value) {
return redisTemplate.opsForList().remove(key, count, value);
}
@Override
public void setExpire(String key) {
try {
redisTemplate.opsForValue().getAndExpire(key, Duration.ofDays(1));
} catch (Exception ignored) {
}
}
@Override
public Set<String> keys(String key) {
return redisTemplate.keys(key + "**");
}
/**
* 模糊匹配删除
*
* @param pattern
*/
@Override
public void delByPattern(String pattern) {
//使用scan进行删除
redisTemplate.execute((RedisCallback<Void>) connection -> {
try (connection; Cursor<byte[]> cursor = connection.keyCommands().scan(ScanOptions.scanOptions().match(pattern).count(1000).build())) {
while (cursor.hasNext()) {
byte[] key = cursor.next();
redisTemplate.delete(new String(key, StandardCharsets.UTF_8));
}
}
return null;
});
}
}
Redission 实现分布式锁
1. 定义 Lock4j 注解,配置分布式锁核心参数
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock4j {
/**
* 锁的前缀 指定是什么类型
*/
String lockType() default "";
/**
* 支持spEL表达式 锁的key
*/
String key() default "";
/**
* 锁超时时间,默认30000毫秒(可在配置文件全局设置)
*/
long lockWatchdogTimeout() default 0;
/**
* 等待加锁超时时间,默认10000毫秒 -1 则表示一直等待(可在配置文件全局设置)
*/
long attemptTimeout() default 0;
}
2. 基于 AOP 实现 Lock4j 注解,落地 Redisson 分布式锁
java
@Aspect
@Slf4j
@Order(-10)
@Component
public class LockAop {
public static final String SEPARATOR = ":";
/**
* 锁超时时间
*/
private final long lockWatchdog;
/**
* 等待加锁超时时间,默认2000毫秒 -1 则表示一直等待(可在配置文件全局设置)
*/
private final long attempt;
private final RedissonClient redissonClient;
public LockAop(@Value("${redisson.lockWatchdog}") long lockWatchdog,
@Value("${redisson.attempt}") long attempt,
RedissonClient redissonClient) {
this.lockWatchdog = lockWatchdog;
this.attempt = attempt;
this.redissonClient = redissonClient;
}
@Pointcut("@annotation(lock4j)")
public void controllerAspect(Lock4j lock4j) {
}
@Around(value = "controllerAspect(lock4j)", argNames = "proceedingJoinPoint,lock4j")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) throws Throwable {
String keys = lock4j.key();
if (!StringUtils.hasText(keys)) {
throw new BusinessException(ResponseCode.KEYS_EMPTY);
}
String key = getRedissonKey(proceedingJoinPoint, lock4j);
long attemptTimeout = lock4j.attemptTimeout() == 0 ? attempt : lock4j.attemptTimeout();
long lockWatchdogTimeout = lock4j.lockWatchdogTimeout() == 0 ? lockWatchdog : lock4j.lockWatchdogTimeout();
boolean res = false;
//创建了一个分布式锁的对象实例
RLock rLock = redissonClient.getLock(lock4j.lockType() + SEPARATOR + key);
//执行aop
if (rLock != null) {
try {
if (attemptTimeout == -1) {
res = true;
//一直等待加锁
rLock.lock(lockWatchdogTimeout, TimeUnit.MILLISECONDS);
} else {
// waitTime -- 获取锁的最长时间 leaseTime -- 租赁时间 unit -- 时间单位
res = rLock.tryLock(attemptTimeout, lockWatchdogTimeout, TimeUnit.MILLISECONDS);
}
log.info("Lock:{},interrupted:{},hold:{},threadId:{} ", rLock.getName(),
Thread.currentThread().isInterrupted(),
rLock.isHeldByCurrentThread(),
Thread.currentThread().threadId());
if (res) {
return proceedingJoinPoint.proceed();
} else {
throw new BusinessException(ResponseCode.GET_LOCK_ERROR);
}
} finally {
//获取到锁且在锁定状态
if (res && rLock.isLocked()) {
rLock.unlock();
}
}
}
throw new BusinessException(ResponseCode.REPEATED_SUBMIT);
}
/**
* 解析spEL 表达式获取实际的key
*
* @param proceedingJoinPoint 切点
* @param lock4j 注解
* @return 实际的key
*/
private String getRedissonKey(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) {
//spEL解析器
ExpressionParser parser = new SpelExpressionParser();
//spEL上下文
EvaluationContext context = new StandardEvaluationContext();
//拿到参数
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(method);
Object[] parameterValues = proceedingJoinPoint.getArgs();
//组装出对象
for (int i = 0; i < Objects.requireNonNull(parameterNames).length; i++) {
context.setVariable(parameterNames[i], parameterValues[i]);
}
Expression expression = parser.parseExpression(lock4j.key());
//EL表达式里的实际值
Object realValue = expression.getValue(context);
return lock4j.lockType() + realValue;
}
}
LockAop 核心流程
-
初始化配置:注入全局锁超时时间、等待加锁超时时间、Redisson ,定义锁 Key 分隔符;
-
切点定义 :匹配所有标注
@Lock4j注解的方法,作为分布式锁的增强目标; -
参数校验:校验注解中锁 Key 配置是否为空,为空则抛异常;
-
动态 Key 解析:解析注解中的 SpEL 表达式,结合方法参数生成实际业务锁 Key;
-
参数适配:优先使用注解配置的超时时间,无配置则用全局默认值;
-
锁对象创建:根据锁类型 + 分隔符 + 业务 Key,创建 Redisson 分布式锁对象;
-
加锁逻辑:
-
等待超时为 - 1:调用
lock()阻塞等待,直到获取锁; -
有等待超时:调用
tryLock()尝试加锁,超时未获取则失败;
-
-
业务执行:加锁成功则执行目标方法,失败则抛 "获取锁失败" 异常;
-
锁释放:最终在 finally 块中,仅当成功获取锁且锁未释放时,执行解锁操作;
-
异常兜底:锁对象创建失败时,抛 "重复提交" 异常。
CompletableUtil 工具包
封装 CompletableUtil 工具类,支持线程 / 虚拟线程异步执行
java
public class CompletableUtil {
public static <U> CompletableFuture<U> supply(Supplier<U> supplier) {
return CompletableFuture.supplyAsync(supplier, CustomThreadPool.getEXECUTOR());
}
public static CompletableFuture<Void> run(Runnable runnable) {
return CompletableFuture.runAsync(runnable, CustomThreadPool.getEXECUTOR());
}
/**
* 使用虚拟线程执行任务(适用于I/O密集型操作)
*/
public static <U> CompletableFuture<U> supplyVirtual(Supplier<U> supplier) {
return CompletableFuture.supplyAsync(supplier, CustomThreadPool.getVIRTUAL_EXECUTOR());
}
/**
* 使用虚拟线程执行任务(适用于I/O密集型操作)
*/
public static CompletableFuture<Void> runVirtual(Runnable runnable) {
return CompletableFuture.runAsync(runnable, CustomThreadPool.getVIRTUAL_EXECUTOR());
}
}
java
private void saveLogAsync(OperationLog operationLog) {
CompletableUtil.run(() -> {
try {
SpringUtil.getBean(OperationLogMapper.class).insert(operationLog);
} catch (Exception exception) {
log.error(LOG_EXCEPTION_MESSAGE, exception);
}
});
}
CompletableFuture 是 Java 8 引入的异步编程工具,基于 Future 增强,支持异步任务执行、结果回调、多任务组合等能力,能大幅简化异步编程逻辑(避免手动创建线程+回调地狱)。
核心优势
对比传统 Thread/Runnable/Future,它的核心优势:
-
异步执行+结果回调 :任务执行完自动触发回调,无需手动轮询
Future.get(); -
多任务组合:支持串行、并行、任意一个完成、全部完成等组合方式;
-
异常处理:内置异常捕获机制,避免异步任务异常导致线程挂掉;
-
线程池适配:可指定自定义线程池,控制异步任务的线程资源。
基础用法
1. 核心API分类
|----------|--------------------------------------------------------------------------------------------------------|--------------------------|
| 类型 | 核心方法 | 作用 |
| 异步执行无返回值 | runAsync(Runnable runnable)<br>runAsync(Runnable runnable, Executor executor) | 异步执行任务,无返回结果 |
| 异步执行有返回值 | supplyAsync(Supplier<U> supplier)<br>supplyAsync(Supplier<U> supplier, Executor executor) | 异步执行任务,有返回结果 |
| 结果回调 | thenApply(Function<T, R> fn)<br>thenAccept(Consumer<T> consumer)<br>thenRun(Runnable action) | 任务完成后处理结果 |
| 异常处理 | exceptionally(Function<Throwable, T> fn)<br>handle(BiFunction<T, Throwable, R> fn) | 捕获任务异常并返回默认值/同时处理正常结果和异常 |
2. 基础示例
1)异步执行无返回值(比如异步记录日志)
java
// 自定义线程池(推荐,避免用默认的ForkJoinPool)
private static final ExecutorService asyncPool = new ThreadPoolExecutor(
5, 10, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "async-task-" + (++count));
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 异步记录企业创建日志(无返回值)
public void asyncLogCompanyCreate(CompanyDTO dto) {
CompletableFuture.runAsync(() -> {
// 模拟日志记录逻辑
log.info("异步记录企业创建日志:{}", dto.getName());
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, asyncPool); // 指定自定义线程池
}
2)异步执行有返回值(比如异步查询企业详情)
java
// 异步查询企业详情(有返回值)
public CompletableFuture<CompanyDTO> asyncGetCompanyById(Long id) {
return CompletableFuture.supplyAsync(() -> {
CompanyDO companyDO = companyMapper.selectById(id);
return convertToDTO(companyDO);
}, asyncPool)
.exceptionally(e -> {
log.error("异步查询企业失败:id={}", id, e);
return new CompanyDTO();
});
}
public void testAsyncGetCompany() {
CompletableFuture<CompanyDTO> future = asyncGetCompanyById(1L);
// 同步获取结果:get()会阻塞,join()不会抛检查异常
CompanyDTO dto = future.join();
log.info("企业名称:{}", dto.getName());
}
3)结果回调(任务完成后处理结果)
java
// 异步查询企业后,自动回调处理结果(无需阻塞)
public void testThenApply() {
asyncGetCompanyById(1L)
// thenApply:处理结果并返回新值(串行执行)
.thenApply(dto -> {
dto.setShortName(dto.getName() + "-");
return dto;
})
// thenAccept:消费结果(无返回值)
.thenAccept(dto -> log.info("处理:{}", dto.getShortName()))
// thenRun:结果处理完后执行无参数操作
.thenRun(() -> log.info("处理完成"));
}
三、进阶用法
实际业务中常需要 "并行执行多个异步任务,再合并结果",CompletableFuture 提供丰富的API:
|-------------|-----------------------------------------------------------------------------|
| 组合场景 | 核心方法 |
| 串行执行 | thenCompose(Function<T, CompletableFuture<R>> fn)(嵌套异步任务) |
| 并行执行-全部完成 | allOf(CompletableFuture<?>... cfs)(所有任务完成后执行) |
| 并行执行-任意一个完成 | anyOf(CompletableFuture<?>... cfs)(任意一个任务完成后执行) |
| 结果合并 | thenCombine(CompletableFuture<U> other, BiFunction<T, U, R> fn)(两个任务结果合并) |
1. 串行异步任务(比如先查企业,再查该企业的用户)
场景:异步查企业 ,再异步查该企业的用户
java
public CompletableFuture<List<UserDTO>> asyncGetCompanyUser(Long companyId) {
return asyncGetCompanyById(companyId)
// thenCompose:嵌套异步任务
.thenCompose(dto -> CompletableFuture.supplyAsync(() -> {
List<UserDO> userDOList = userMapper.selectByCompanyId(companyId);
return userDOList.stream().map(this::convertToUserDTO).toList();
}, asyncPool));
}
2. 并行执行多个任务(全部完成后合并结果)
场景:创建企业时,并行异步初始化 "企业配置" + "企业权限",全部完成后返回结果。
java
// 并行执行多个异步任务,全部完成后合并结果
public CompletableFuture<Boolean> asyncInitCompany(Long companyId) {
//异步初始化企业配置
CompletableFuture<Boolean> configFuture = CompletableFuture.supplyAsync(() -> {
companyConfigMapper.init(companyId);
return true;
}, asyncPool);
//异步初始化企业权限
CompletableFuture<Boolean> permissionFuture = CompletableFuture.supplyAsync(() -> {
companyPermissionMapper.init(companyId);
return true;
}, asyncPool);
// 等待所有任务完成,再判断结果
return CompletableFuture.allOf(configFuture, permissionFuture)
.thenApply(v -> {
// 获取每个任务的结果
boolean configOk = configFuture.join();
boolean permissionOk = permissionFuture.join();
return configOk && permissionOk;
})
.exceptionally(e -> {
log.error("初始化企业失败:{}", companyId, e);
return false;
});
}
3. 并行执行多个任务(任意一个完成就返回)
场景:查询企业信息,同时从缓存和数据库查,哪个快用哪个。
java
// 并行查缓存+数据库,任意一个完成就返回
public CompletableFuture<CompanyDTO> asyncGetCompanyFast(Long companyId) {
//查缓存
CompletableFuture<CompanyDTO> cacheFuture = CompletableFuture.supplyAsync(() -> {
log.info("从缓存查企业:{}", companyId);
try {
Thread.sleep(200); // 模拟缓存查询耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return cacheService.getCompany(companyId);
}, asyncPool);
//查数据库
CompletableFuture<CompanyDTO> dbFuture = CompletableFuture.supplyAsync(() -> {
log.info("从数据库查企业:{}", companyId);
try {
Thread.sleep(500); // 模拟数据库查询耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
CompanyDO do = companyMapper.selectById(companyId);
return convertToDTO(do);
}, asyncPool);
// 任意一个任务完成就返回结果
return CompletableFuture.anyOf(cacheFuture, dbFuture)
.thenApply(result -> {
// 结果是Object类型,需要强转
CompanyDTO dto = (CompanyDTO) result;
if (dto == null) {
// 如果缓存没查到,返回数据库结果(兜底)
return dbFuture.join();
}
return dto;
});
}
定时任务线程池
java
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
private static final int POOL_SIZE = 20;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(POOL_SIZE);
scheduler.setThreadNamePrefix("Tplus-Scheduler-");
scheduler.setWaitForTasksToCompleteOnShutdown(true);
scheduler.setAwaitTerminationSeconds(60);
taskRegistrar.setTaskScheduler(scheduler);
scheduler.initialize();
}
}
该配置类是Spring 定时任务的线程池定制化配置,替代 Spring 默认的单线程调度器,解决定时任务串行执行、阻塞、宕机丢失任务等问题,核心价值是提升定时任务的并发能力和稳定性。
-
自定义线程池:创建核心线程数为 20 的 ThreadPoolTaskScheduler,替代默认单线程;
-
线程命名 :设置线程名前缀
Tplus-Scheduler-,便于日志排查线程归属; -
优雅停机 :
setWaitForTasksToCompleteOnShutdown(true):应用关闭时等待所有定时任务执行完成;setAwaitTerminationSeconds(60):最多等待 60 秒,超时则强制终止; -
绑定调度器:将自定义线程池绑定到 ScheduledTaskRegistrar,让所有 @Scheduled 注解的定时任务使用该线程池执行。
自定义线程池实现
java
public class CustomThreadPool {
private static final int CORE_POOL_SIZE = Math.max(4, Runtime.getRuntime().availableProcessors() * 2);
private static final int MAX_POOL_SIZE = 200;
private static final int QUEUE_CAPACITY = 1000;
// 60 秒
private static final long KEEP_ALIVE_TIME = 60L;
@Getter
public static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new CustomThreadFactory("event-handler-thread-"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
java
public class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public CustomThreadFactory(String namePrefix) {
this.namePrefix = namePrefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
t.setDaemon(false);
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
自定义线程池,根据 CPU 核心动态设置核心线程数,使用有界队列、自定义命名线程工厂与安全拒绝策略,避免资源耗尽与 OOM,保障异步任务稳定可控执行。
AOP实现日志记录
java
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 排除敏感属性字段
*/
private static final Set<String> EXCLUDE_PROPERTIES = new HashSet<>();
private static final String LOG_EXCEPTION_MESSAGE = "操作日志异常信息: ";
static {
EXCLUDE_PROPERTIES.add("password");
EXCLUDE_PROPERTIES.add("oldPassword");
EXCLUDE_PROPERTIES.add("newPassword");
EXCLUDE_PROPERTIES.add("confirmPassword");
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object result) {
handleLog(joinPoint, controllerLog, null, result);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object ret) {
try {
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
OperationLog operationLog = buildOperationLog(joinPoint, request, controllerLog, ret, e);
saveLogAsync(operationLog);
}
} catch (Exception exp) {
log.error(LOG_EXCEPTION_MESSAGE, exp);
}
}
private Map<String, String[]> getParamsWithoutSensitiveInfo(HttpServletRequest request, Log controllerLog) {
Map<String, String[]> params = new HashMap<>(ServletUtils.getParams(request));
// 使用Set优化敏感信息过滤
EXCLUDE_PROPERTIES.forEach(params::remove);
if (controllerLog.exclude() != null) {
Arrays.stream(controllerLog.exclude()).forEach(params::remove);
}
return params;
}
private OperationLog buildOperationLog(JoinPoint joinPoint, HttpServletRequest request, Log controllerLog, Object ret, Exception e) {
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
LoginUser currentUser;
try {
currentUser = U.get();
} catch (Exception ex) {
currentUser = new LoginUser();
}
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
Map<String, String[]> params = getParamsWithoutSensitiveInfo(request, controllerLog);
OperationLog operationLog = new OperationLog();
operationLog.setOperationModule(controllerLog.title());
operationLog.setOperationMethod(className + "." + methodName + "()");
operationLog.setOperationParams(JSON.toJSONString(params));
operationLog.setOperationMethodType(request.getMethod());
operationLog.setOperationUrl(request.getRequestURI());
if (e != null) {
System.out.println(e.getMessage());
operationLog.setOperationResult(e != null ? StringUtils.substring(e.getMessage(), 0, 20) : String.valueOf(ret));
}
operationLog.setOperationIp(IPUtil.getClientIP(request));
operationLog.setOperationBrowser(getUserAgentString(userAgent));
operationLog.setOperationOs(getOperatingSystemString(userAgent));
operationLog.setOperationUser(currentUser.getUsername());
operationLog.setOperationUserId(currentUser.getId());
operationLog.setOperationTime(LocalDateTime.now(ZoneId.systemDefault()));
operationLog.setCompanyId(currentUser.getAuthCompanyId());
List<Object> args = Arrays.stream(joinPoint.getArgs())
.filter(CommonDTO.class::isInstance).toList();
operationLog.setOperationBody(JSON.toJSONString(args));
return operationLog;
}
private void saveLogAsync(OperationLog operationLog) {
CompletableUtil.run(() -> {
try {
SpringUtil.getBean(OperationLogMapper.class).insert(operationLog);
} catch (Exception exception) {
log.error(LOG_EXCEPTION_MESSAGE, exception);
}
});
}
private String getUserAgentString(UserAgent userAgent) {
return userAgent.getBrowser().toString();
}
private String getOperatingSystemString(UserAgent userAgent) {
return userAgent.getOperatingSystem().toString();
}
}
Bug处理
缓存失效问题
java
@Lock4j(lockType = "delCompany", key = "#companyId")
@Transactional(rollbackFor = Exception.class)
public Boolean delCompany(Long companyId, LoginUser loginUser) {
Company company = companyRepository.getById(companyId);
if (company == null) {
throw new BusinessException(ResponseCode.MERCHANT_NOT_EXISTS);
}
if (company.getId() == 1L) {
throw new BusinessException(ResponseCode.MERCHANT_DISABLE_DELETED);
}
// 检查企业下是否有设备,检查企业是否存在用户子账号
if (deviceRepository.countByCompanyId(companyId) > 0) {
throw new BusinessException(ResponseCode.MERCHANT_DEVICE_EXISTS);
}
if (userRepository.countByCompanyId(companyId) > 0) {
throw new BusinessException(ResponseCode.MERCHANT_USER_EXISTS);
}
companyRepository.removeById(companyId);
log.info("用户:{} 删除企业:{}", loginUser.getUsername(), company.getName());
//companyRepository.removeCache();
return true;
}
缓存机制未接入实际业务流程,所有对外核心业务方法均直接操作数据库,既不读取也不写入缓存;而 removeCache () 仅删除 "company:all" 缓存键,该缓存键仅由内部方法设置且未被外部业务调用,即便增删改方法执行了 removeCache (),也因目标缓存从未被实际使用而无法产生效果,最终导致缓存逻辑形同虚设,removeCache () 方法丧失实际作用。
续期后旧 Token 未失效
java
@PostMapping("/refreshToken")
@Operation(summary = "刷新token")
public CommonResult<String> refreshToken(HttpServletRequest request) {
return CommonResult.success(systemService.refreshToken(request));
}
java
public String refreshToken(HttpServletRequest request) {
//秒
Long expireTime = tokenService.getExpireTime(request);
// 如果剩30分钟就刷新token
if (expireTime < 30 * 60) {
return tokenService.createToken(loginUser);
}
return tokenService.getToken(request);
}
java
public void syncToken2Redis(LoginUser loginUser, String uuid) {
String key = String.format(FORMAT, LOGIN_USER, uuid, loginUser.getUsername());
redisService.set(key, loginUser, Duration.ofHours(expirationHours));
}
在 Token 续期逻辑中,当检测到 Token 剩余有效期不足 30 分钟时,仅生成并返回新 Token,同时将新 Token 信息同步至 Redis,但未对 Redis 中存储的旧 Token 执行删除 / 过期等失效操作,导致旧 Token 未被禁用,仍能正常校验通过,存在同一账号多 Token 有效、权限管控失效的风险。
解决办法:在 Token 续期时,当检测到 Token 剩余有效期不足 30 分钟生成新 Token 并同步至 Redis 后,新增调用 invalidateOldToken 方法对旧 Token 执行失效处理:该方法通过登录用户唯一标识检索 Redis 中存储的旧 Token 缓存 Key,执行删除操作,彻底禁用旧 Token。
java
public String refreshToken(HttpServletRequest request) {
//秒
Long expireTime = tokenService.getExpireTime(request);
// 如果剩30分钟就刷新token
if (expireTime < 30 * 60) {
String oldToken = tokenService.getToken(request);
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser == null) {
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "旧Token无效,无法刷新");
}
String newToken = tokenService.createToken(loginUser);
tokenService.invalidateOldToken(oldToken);
return newToken;
}
return tokenService.getToken(request);
}
java
public void invalidateOldToken(String oldToken) {
if (StringUtils.isBlank(oldToken)) {
log.warn("失效旧Token失败:Token为空");
return;
}
try {
Claims claims = parseToken(oldToken);
String oldTokenId = claims.get(TOKEN, String.class);
String username = claims.get(USERNAME, String.class);
String oldKey = String.format(FORMAT, LOGIN_USER, oldTokenId, username);
if (redisService.del(oldKey)) {
log.info("失效旧Token成功,username:{},tokenId:{}", username, oldTokenId);
} else {
log.warn("失效旧Token失败:Redis中未找到该Token缓存,username:{},tokenId:{}", username, oldTokenId);
}
} catch (Exception e) {
log.error("失效旧Token异常:{}", e.getMessage(), e);
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "失效旧Token失败");
}
}
**感谢你的阅读!**🌹