跳到主要内容
Java 后端实习复盘:企业级项目实战与核心架构解析 | 极客日志
Java java
Java 后端实习复盘:企业级项目实战与核心架构解析 Java 后端实习期间深入参与了企业级后台项目开发,涵盖权限管理、Token 认证、缓存策略及分布式锁等核心模块。文章复盘了从环境搭建到业务落地的全流程,重点解析了基于 Spring AOP 的注解式权限校验、JWT + Redis 的无感续期方案、双重校验加锁防止缓存击穿以及自定义线程池优化异步任务等实战经验。同时总结了常见 Bug 处理思路,如缓存失效修复与 Token 旧值清理,为初级开发者提供了一套完整的企业开发规范参考。
**摘要:**实习期间参与企业后台项目开发,熟悉企业开发流程与代码规范。
实习核心流程(结合实际经历)
由于进入的是一个小公司实习,当时项目刚好启动,参与了较多基础模块的开发。
一、基础准备与环境搭建阶段(入职 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 并发编程工具类等业务场景时,解决实际问题的方法。
熟悉企业项目
权限管理模块
Auth {
String[] value() ;
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public
@interface
default
""
**Auth:**定义了一个自定义的 Java 注解,名为 Auth,通常用于标记需要权限校验的类或方法。
@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);
}
}
@Pointcut :Spring AOP 的注解,用于声明一个切点,参数是「切点表达式」,指定拦截规则。
@annotation() 是 AOP 内置的切点表达式,含义是拦截所有方法上标注了指定注解的方法 。
这里的 auth 是参数名,对应后面方法的 Auth auth,表示拦截标注 @Auth 的方法,并把该注解实例传入切点方法。
public void controllerAspect(Auth auth) :
这是一个「切点签名方法」,本身无业务逻辑,仅用于承载 @Pointcut 注解和参数声明。
参数 Auth auth:表示将拦截到的方法上的 @Auth 注解实例注入到该参数中,后续通知方法可直接使用。
@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;
@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);
}
}
}
private boolean handle (HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
try {
if (openApiAuthHandler.isOpenApiRequest(httpRequest)) {
return handleOpenApiAuth(httpRequest, httpResponse);
}
return handleUserAuth(httpRequest, httpResponse);
} catch (Exception e) {
log.error("认证处理异常" , e);
writeAccessDenied(httpResponse);
return false ;
}
}
private boolean handleOpenApiAuth (HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
log.debug("处理 OpenAPI 认证,URI: {}" , httpRequest.getRequestURI());
if (openApiAuthHandler.isTokenRequest(httpRequest)) {
log.debug("Token 获取请求,跳过认证" );
return true ;
}
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());
String authenticationToken = httpRequest.getHeader(AUTHORIZATION);
if (StringUtils.isBlank(authenticationToken)) {
log.warn("用户认证失败:缺少认证 token" );
writeAccessDenied(httpResponse);
return false ;
}
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 ;
}
}
private void writeAccessDenied (HttpServletResponse response) {
writeErrorResponse(response, ResponseCode.ACCESS_DENIED);
}
private void writeOpenApiAccessDenied (HttpServletResponse response) {
writeErrorResponse(response, ResponseCode.AUTHENTICATION_FAIL);
}
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);
}
}
public boolean authenticate (HttpServletRequest request, HttpServletResponse response) {
try {
log.debug("开始 OpenAPI 认证,URI: {}" , request.getRequestURI());
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 ;
}
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 ;
}
}
}
该登录过滤器作为请求进入系统的认证关卡,整体执行流程如下:
过滤器初始化时,会将配置的白名单 URL(通过 authPath.getUrls() 获取)解析为 PathPattern 格式并缓存到 ignorePatterns 列表中,为后续路径匹配做准备。
将 ServletRequest/ServletResponse 转换为 HttpServletRequest/HttpServletResponse,并设置响应格式为 application/json;charset=utf-8,记录请求 URL。
解析当前请求 URL 为 PathContainer,匹配缓存的白名单 ignorePatterns:
若匹配成功(属白名单),直接放行请求(filterChain.doFilter),结束当前过滤器逻辑;
若不匹配,进入认证处理流程。
调用 handle 方法执行具体认证逻辑,若认证通过则放行请求,否则拦截。
无论认证成功/失败,最终都会清理用户上下文(LoginUserHandler.removeLoginUser())和 OpenAPI 上下文(openApiAuthHandler.clearAuthContext()),避免内存泄漏。
先通过 openApiAuthHandler.isOpenApiRequest 判断是否为 OpenAPI 请求:
若是,执行 handleOpenApiAuth 处理 OpenAPI 认证;
若否,执行 handleUserAuth 处理普通用户认证。
若认证过程中抛出异常,记录错误日志,调用 writeAccessDenied 返回未授权响应,拦截请求。
4. OpenAPI 认证流程(handleOpenApiAuth 方法)
若为 Token 获取请求(openApiAuthHandler.isTokenRequest),直接放行(无需认证)。
调用 openApiAuthHandler.authenticate 执行认证:
认证成功:放行请求;
认证失败:记录警告日志,调用 writeOpenApiAccessDenied 返回未授权响应,拦截请求。
5. 普通用户认证流程(handleUserAuth 方法)
从请求头获取认证 Token,若 Token 为空,记录警告日志,返回未授权响应,拦截请求。
调用 tokenService.getLoginUser 解析 Token:
解析成功:将用户信息存入上下文(LoginUserHandler.setLoginUser),放行请求;
解析失败(LoginUser 为空)或抛出异常:记录错误日志,返回未授权响应,拦截请求。
认证失败时,通过 writeErrorResponse 统一返回标准化错误响应:
设置响应状态码为 401(SC_UNAUTHORIZED);
将 CommonResult.response(responseCode) 序列化为 JSON 写入响应体;
区分普通用户认证失败和 OpenAPI 认证失败的响应码。
SecurityIgnoreUrls(配置绑定类)与 secure.ignored.urls(白名单 URL 配置)
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 前缀的配置绑定到这个类的 urls 属性上,让代码可以通过注入该类直接获取白名单 URL 列表(无需手动解析配置)。
Token 管理模块 @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 {
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);
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 小时。
步骤 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 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 );
return list;
}
}
这段代码是企业级系统中 "全量数据缓存加载" 类功能的典型通用逻辑框架,核心遵循 "缓存查询→双重校验加锁→数据库查询→缓存写入→结果返回" 的标准化流程,具体可拆解为 4 个核心步骤:
一级缓存查询 :定义固定缓存键(cacheKey = "company:all"),先从 Redis 中查询缓存数据,若缓存存在则直接返回(缓存功能的通用前置步骤,优先使用缓存提升查询性能);
双重校验 + 同步锁 :缓存不存在时,通过 synchronized (cacheLock) 加本地同步锁,锁内再次查询缓存,若仍不存在再执行数据库查询(缓存加载的通用并发防护,避免缓存击穿);
数据库查询 + 缓存写入 :锁内执行全量数据库查询,将查询结果写 Redis 并设置过期时间。
结果统一返回 :无论从缓存还是数据库获取数据,最终统一返回全量企业列表。
getCompanyFromCache 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类型结果,未匹配到返回空 Optional 而非 null。
getCompanyList @Validated 是 Spring 框架提供的参数校验注解,核心作用是自动校验接口入参的合法性,避免我们手动写大量 if-else 判断参数是否为空、格式是否正确。
@PostMapping("/list")
public CommonResult<SimplePage<CompanyListVO>> getCompanyList (@RequestBody @Validated CompanyListDTO dto) {
return CommonResult.success(companyService.getCompanyList(dto,U.get()));
}
@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 字符的规则;
若校验失败,会直接抛出 MethodArgumentNotValidException 异常,拦截非法参数,不会进入 companyService.getCompanyList() 业务逻辑层。
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());
}
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;
}
}
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 () 封装统一的分页参数,调用 getCompanyList () 执行数据库分页查询,返回包含 "分页元数据 + 原始实体列表" 的 SimplePage 对象;
**通用工具类转换(DO→VO):**调用 PageUtil.getPage () 工具方法,结合 Function 函数式接口传入 VO 构建逻辑(r -> CompanyListVO.builder ()),将 Entity 转换为前端展示 VO;
**分页元数据透传 + 空值过滤:**工具类完整保留原分页对象的总页数、总条数、当前页 / 页大小等元数据,同时通过 stream ().filter (Objects::nonNull) 过滤转换后的空值;
**统一结果返回:**返回转换后的 SimplePage对象,包含前端所需的展示列表和完整分页元数据。
getCompanyDetailById @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 @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()));
}
public List<CompanyDropDown> getCompanyDropDownList (Long companyId, LoginUser loginUser) {
return companyRepository.getCompanyDropDownList(companyId, loginUser);
}
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;
结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。
public Company getCompanyDetailById (Long companyId, LoginUser loginUser) {
Optional<Company> cacheCompany = getCompanyFromCache(companyId);
if (cacheCompany.isPresent()) {
Company company = cacheCompany.get();
if (loginUser.isEnterprise() && !company.getId().equals(loginUser.getAuthCompanyId())) {
throw new BusinessException (ResponseCode.MERCHANT_NOT_EXISTS);
}
return company;
}
Company company = companyRepository.getByIdWithAuth(companyId, loginUser);
if (company == null ) {
throw new BusinessException (ResponseCode.MERCHANT_NOT_EXISTS);
}
companyRepository.removeCache();
companyRepository.cacheAll();
return company;
}
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 则抛出标准化业务异常,作为详情查询的通用兜底逻辑,避免空值引发前端渲染异常或下游业务空指针;
合规结果返回:经缓存 / 数据库查询、权限校验、有效性校验后,返回符合条件的企业实体对象,既保证查询性能,又满足数据权限管控和业务完整性要求。
addCompany @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()));
}
@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();
}
这段代码是 "新增实体" 类功能的典型实现,具体如下:
**唯一性校验:**通过 companyRepository.countByName(dto.getName()) 校验企业名称是否已存在,避免重复创建,若重复则抛出业务异常;
**参数转换:**将入参 AddCompanyDTO 转换为 Company 实体对象,仅保留业务所需字段;
**数据持久化:**将实体保存数据库,结合 @Transactional 注解,确保保存失败时事务回滚;
**缓存清理:**调用 removeCache() 清理缓存,避免新增数据与缓存数据不一致;
**扩展业务处理:**根据 createMainAccount 参数判断是否创建企业主账号;
**日志记录 + 结果返回:**记录关键操作日志,最终返回新增实体的主键 ID;
**并发控制(额外保障):**通过 @Lock4j() 基于企业名称加锁,解决高并发下的重复创建问题。
updateCompany @Log(title = "修改企业")
@PostMapping
@Operation(summary = "修改企业")
public CommonResult<Boolean> updateCompany (@RequestBody @Validated UpdateCompanyDTO dto) {
return CommonResult.success(companyService.updateCompany(dto, U.get()));
}
@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);
}
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();
}
这段代码是 "实体更新" 类功能的典型实现,具体如下:
存在性校验 :通过 companyRepository.getById () 查询目标企业是否存在,否则抛出异常;
权限校验 :校验当前登录用户是否为管理员,或是否归属企业,仅满足权限条件才更新;
数据更新 :调用 updateCompany (dto) 执行企业信息更新,结合 @Transactional 注解,确保更新失败时事务回滚,保证数据一致性;
并发控制 :通过 @Lock4j () 基于企业 ID 加分布式锁,解决高并发下的重复更新问题;
日志记录 + 结果返回 :记录用户更新企业的关键操作日志,权限校验通过则返回 true,未通过则抛出权限相关业务异常。
delCompany @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()));
}
@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 ;
}
public void removeCache () {
redisService.del("company:all" );
}
这段代码是 "实体删除" 类功能的典型实现,具体如下:
存在性校验 :通过 getById (companyId) 查询企业是否存在,若不存在则抛出业务异常;
特殊规则校验 :校验目标企业是否为系统核心企业(ID=1L),若是则抛出业务异常;
关联数据校验 :依次检查企业下是否关联设备、是否存在用户子账号,否则抛出业务异常;
数据删除 :调用 removeById () 执行删除操作,删除失败时事务回滚,保证数据一致性;
并发控制 + 缓存清理 :通过 @Lock4j () 基于企业 ID 加分布式锁,解决高并发下的重复删除问题;删除后调用 removeCache () 清理缓存,避免删除数据与缓存数据不一致。
日志记录 + 结果返回 :记录用户删除企业的关键操作日志,所有校验通过后返回 true。
企业用户账号管理模块
sysUserList @PostMapping("/list")
@Operation(summary = "系统用户列表")
public CommonResult<SimplePage<SysUserListVO>> sysUserList (@RequestBody SysUserListDTO dto) {
return CommonResult.success(userService.userList(dto));
}
public SimplePage<SysUserListVO> userList (SysUserListDTO dto) {
return new SimplePage <>(userRepository.getUserList(Page.of(dto.getPageNum(), dto.getPageSize()), dto, U.get()));
}
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;
}
Page<SysUserListVO> getUserList (@Param("page") Page page, @Param("dto") SysUserListDTO dto, @Param("loginUser") LoginUser loginUser) ;
<select 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 >
这段代码是 "系统用户分页列表查询(多条件 + 权限过滤 + 结果格式化)" 典型实现,具体如下:
接口层接收请求 :通过 @PostMapping ("/list") 定义用户列表接口;
服务层分页封装 :调用 Page.of () 封装分页参数,传入查询方法,返回 SimplePage 对象;
仓储层动态条件查询 :Mapper 层通过 XML 构建动态 SQL,支持企业 ID、用户名、用户 ID 等业务条件过滤,同时追加登录用户权限过滤,并关联企业表查询企业名称;
结果格式化 :遍历分页列表将角色字符串(String)拆为角色列表(List),补充到 VO 中;
分层数据返回 :Mapper 层返回 Page,服务层封装为 SimplePage 对象。
userDetail @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));
}
public UserDetailVO userDetail (Long userId) {
UserDetailVO userDetail = userRepository.getUserDetail(userId, U.get());
userDetail.setRoleList(Arrays.stream(userDetail.getRoleString().split("," )).toList());
return userDetail;
}
public UserDetailVO getUserDetail (Long userId, LoginUser loginUser) {
return baseMapper.getUserDetail(userId, loginUser);
}
UserDetailVO getUserDetail (@Param("userId") Long userId, @Param("loginUser") LoginUser loginUser) ;
<select 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 >
这段代码是 "用户详情查询(参数校验 + 结果格式化)" 典型实现,具体如下:
接口层参数校验 :通过 @RequestParam 接收用户 ID 参数,配合 @Validated + @NotNull 强制校验参数非空,若为空则抛出校验异常;
服务层查询调用 :调用 getUserDetail () 方法,获取原始详情数据;
仓储层精准查询 :Mapper 层通过 XML 构建精准 SQL,根据用户 ID 查询用户基础信息,并关联企业表查询所属企业名称,同时过滤已删除数据(deleted = 0);
结果格式化处理 :将返回的角色字符串 roleString 拆分为角色列表 roleList,补充 VO。
systemUserResetPwd @Log(title = "重设密码")
@PostMapping("/reset/pwd")
@Operation(summary = "重设密码")
public CommonResult<Boolean> systemUserResetPwd (@RequestBody @Validated SysUserRestPwdDTO dto) {
return CommonResult.success(userService.sysUserRestPwd(dto, U.get()));
}
@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 ;
}
public Boolean sysUserRestPwd (SysUserRestPwdDTO dto) {
return this .update(Wrappers.<User>lambdaUpdate()
.set(User::getPassword, passwordEncoder.encode(dto.getNewPassword()))
.eq(User::getId, dto.getUserId()));
}
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());
}
}
这段代码是 "用户密码重设(含权限校验 + 安全登出)" 类功能的典型实现,具体如下:
接口层参数校验 :通过@Validated 触发 DTO 的参数校验,确保重置密码所需参数合法;
存在性校验 :通过 getById () 查询目标用户是否存在,若不存在则抛出业务异常;
权限校验 :校验当前登录用户是否为企业用户且非目标用户本人,若是则抛出业务异常;
密码更新 :调用 sysUserRestPwd (dto) 更新用户密码,密码通过 encode () 加密后存储;
并发控制 :通过 @Lock4j () 基于用户 ID 加分布式锁,解决高并发下的重复重置密码问题;
安全登出处理 :调用 tokenService.logout () 方法,解析用户当前登录 Token 并删除 Redis 中对应的 Token 缓存,强制用户重新登录。
addSysUser @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));
}
@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);
}
public boolean checkUsername (String username) {
return this .count(Wrappers.<User>lambdaQuery()
.eq(User::getUsername, username)) <= 0 ;
}
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);
}
这段代码是 "添加系统用户" 类功能的典型实现,具体如下:
接口层参数校验 :通过 @Validated 触发 DTO 的参数校验,确保新增用户所需参数合法;
用户名唯一性校验 :调用 checkUsername () 方法,统计用户是否已存在,否则抛出异常;
企业存在性校验 :通过 getById () 查询关联企业是否存在,若不存在则抛出业务异常;
用户实体构建 :将 AddSysUserDTO 转换为 User 数据库实体,密码通过 encode () 加密存储,角色列表拼接为字符串,昵称为空时兜底为用户名;
数据保存 :调用 save (user) 保存用户实体,结合 @Transactional 确保失败时事务回滚。
第三方系统访问模块
getToken
@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;
}
}
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();
}
public TokenInfo generateToken (String apiKey, String secret) {
log.info("生成 Token,apiKey: {}" , apiKey);
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, "秘钥错误" );
}
tokenRepository.revokeTokensByApiKey(apiKey);
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);
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 @PostMapping("/refreshToken")
@Operation(summary = "刷新 token")
public CommonResult<String> refreshToken (HttpServletRequest request) {
return CommonResult.success(systemService.refreshToken(request));
}
public String refreshToken (HttpServletRequest request) {
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% 以上的常用类型判空场景
仅想判断 "字符串是不是 null":直接用 str == null;
想判断 "字符串是 null,或者是空字符串("")":优先用 StringUtils.isEmpty(str),替代繁琐的 str == null || str =="";
想判断 "字符串无任何有效字符"(包括 null、""、全空格):必须用 StringUtils.isBlank(str),比如用户输入的空格、换行符都能被识别;
想判断 "字符串非空且有有效字符":用 StringUtils.isNotBlank(str),是业务中最常用非空判断。
基本数据类型(int、long、boolean 等)没有 null 的概念,因为它们是值类型,不是对象:
不存在 "判 null" 的说法,只能判断 "是否为默认值":比如 int 默认值是 0,就用 num == 0;long 默认值是 0L,就用 num == 0L;boolean 默认值是 false,就用 flag == false;
绝对不能写 int num == null,会直接编译报错。
包装类是对象,既有 "引用是否为空",也有 "值是否有效":
仅判断 "引用是不是 null":直接用 num == null(如 Integer num == null);
既判 null,又判值是否为默认值:先判 num == null,再判 num == 0,或用 Hutool 的 ObjectUtil.isEmpty(num) || num == 0;
非空且值有效:如判断 Integer 非空且大于 0,用 ObjectUtil.isNotEmpty(num) && num > 0。
数组是对象,判空要兼顾 "引用为空" 和 "数组长度为 0":
仅判断 "数组引用是不是 null":用 arr == null;
既判 null,又判空数组:优先用 Hutool 的 ObjectUtil.isEmpty(arr);
多维数组(比如 String[][]):需要逐层判空,先判外层数组非空,再判内层数组非空。
自定义对象:仅判断 "引用是不是 null",直接用 obj == null(比如 User user == null);
集合(List、Map):判空要兼顾 "引用为空" 和 "集合为空",用 ObjectUtil.isEmpty(list);
通用非空判断:不管是自定义对象、包装类还是数组,都能用 ObjectUtil.isNotEmpty(obj)。
判空注解 @NotNull 是通用性最强的非空注解,可作用于所有数据类型 (包括基本类型包装类、字符串、集合、自定义对象等)。它的核心校验规则仅判定目标值是否为 null,不关注值的内容:即便目标是字符串(如 "")、空集合(如 new ArrayList<>()),只要不是 null,该注解的校验就会通过。
@NotBlank 是专门针对 String 类型 的强非空注解,校验规则比 @NotNull 更严格:它不仅要求目标字符串不能为 null,还要求去除首尾空格后字符串的长度必须大于 0。也就是说,null、空字符串 ""、仅包含空格的字符串 " " 都会被该注解判定为校验失败。
@NotEmpty 适用于 String 类型、集合(List/Set/Map)、数组 ,核心规则是:目标值不能为 null,且对应的 "长度 / 大小" 必须大于 0。对字符串而言,它仅判定是否为 null 或空字符串 "";对集合 / 数组而言,它判定是否为 null 或空容器,只要容器中有至少一个元素,即便元素本身是 null,也能通过校验。
日志框架 <?xml version="1.0" encoding="UTF-8" ?>
<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" />
<appender name ="FILE" >
<rollingPolicy >
<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 >
<appender name ="STDOUT" >
<encoder >
<pattern > ${CONSOLE_LOG_PATTERN}</pattern >
</encoder >
<filter >
<level > DEBUG</level >
</filter >
</appender >
<appender name ="ASYNC_FILE" >
<discardingThreshold > 0</discardingThreshold >
<queueSize > 512</queueSize >
<appender-ref ref ="FILE" />
</appender >
<appender name ="ASYNC_STDOUT" >
<discardingThreshold > 0</discardingThreshold >
<queueSize > 512</queueSize >
<appender-ref ref ="STDOUT" />
</appender >
<root level ="INFO" >
<appender-ref ref ="ASYNC_FILE" />
<appender-ref ref ="ASYNC_STDOUT" />
</root >
</configuration >
<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)
<appender name ="FILE" >
<rollingPolicy >
<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)
<appender name ="STDOUT" >
<encoder >
<pattern > ${CONSOLE_LOG_PATTERN}</pattern >
</encoder >
<filter >
<level > DEBUG</level >
</filter >
</appender >
定义 "日志输出到控制台" 的规则,方便开发时实时看日志;
用 Spring Boot 自带的彩色日志格式(ERROR 红、INFO 绿),视觉更清晰;
过滤掉 TRACE 级别的日志(只输出 DEBUG 及以上),减少控制台无用日志。
模块 4:异步输出优化模块(ASYNC_ Appender) *
<appender name ="ASYNC_FILE" >
<discardingThreshold > 0</discardingThreshold >
<queueSize > 512</queueSize >
<appender-ref ref ="FILE" />
</appender >
<appender name ="ASYNC_STDOUT" >
<discardingThreshold > 0</discardingThreshold >
<queueSize > 512</queueSize >
<appender-ref ref ="STDOUT" />
</appender >
给 "文件输出" 和 "控制台输出" 加 "异步缓冲",是性能优化模块;
主线程打日志时不用等日志写完,直接继续执行代码,避免日志写入拖慢程序;
配置 "不丢日志"(队列满了也不丢弃),同时调整队列大小(512)平衡性能和内存。
<root level ="INFO" >
<appender-ref ref ="ASYNC_FILE" />
<appender-ref ref ="ASYNC_STDOUT" />
</root >
整个项目的日志 "总控制",定义全局日志级别为 INFO(只输出 INFO、WARN、ERROR);
绑定前面异步输出器,最终实现所有符合级别的日志,既异步写文件,又异步输出到控制台。
Optional 判空 String message;
if (e == null || e.getMessage() == null ) {;
} else {
message = e.getMessage();
}
String message = Optional.ofNullable(e.getMessage()).orElse("" );
切面优先级 @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) {
}
@Transactional 对应的 Spring 事务切面默认优先级数值是 Integer.MAX_VALUE ,@Order 数值越小优先级越高,因此 -10 > 2147483647 ,LockAop 先执行前置逻辑、后执行后置逻辑。
同一方法上多个切面(含 Spring 内置事务切面)的执行逻辑:标注 @Order 的切面按数值越小优先级越高,无 @Order 的内置切面(如 @Transactional)默认优先级最低;高优先级切面的环绕通知会先执行前置逻辑,再触发低优先级切面 / 目标方法,最后执行高优先级切面的后置逻辑。
通用能力封装
RedisService 封装类 1. Redis 统一配置:序列化 + 缓存时效 + 自定义服务注入
@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 () {
ObjectMapper objectMapper = new ObjectMapper ();
objectMapper.registerModule(new JavaTimeModule ());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
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);
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 各类数据结构操作
public interface RedisService {
void set (String key, Object value, long time) ;
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) ;
Long incr (String key, long delta) ;
Long decr (String key, long delta) ;
Object hGet (String key, String hashKey) ;
Boolean hSet (String key, String hashKey, Object value, long time) ;
void hSetIfAbsent (String key, String hashKey, Object value) ;
void hSet (String key, String hashKey, Object value) ;
Map<Object, Object> hGetAll (String key) ;
Boolean hSetAll (String key, Map<String, Object> map, long time) ;
void hSetAll (String key, Map<String, ?> map) ;
void hDel (String key, Object... hashKey) ;
Boolean hHasKey (String key, String hashKey) ;
Long hIncr (String key, String hashKey, Long delta) ;
Long hDecr (String key, String hashKey, Long delta) ;
Set<Object> sMembers (String key) ;
Long sAdd (String key, Object... values) ;
Long sAdd (String key, long time, Object... values) ;
Boolean sIsMember (String key, Object value) ;
Long sSize (String key) ;
Long sRemove (String key, Object... values) ;
List<Object> lRange (String key, long start, long end) ;
Long lSize (String key) ;
Object lIndex (String key, long index) ;
Long lPush (String key, Object value) ;
Long lPush (String key, Object value, long time) ;
Long lPushAll (String key, Object... values) ;
Long lPushAll (String key, Long time, Object... values) ;
Long lRemove (String key, long count, Object value) ;
void setExpire (String key) ;
Set<String> keys (String key) ;
void delByPattern (String pattern) ;
}
3. 实现 RedisService 接口,封装多结构 Redis 操作逻辑
@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 + "**" );
}
@Override
public void delByPattern (String pattern) {
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 注解,配置分布式锁核心参数
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock4j {
String lockType () default "" ;
String key () default "" ;
long lockWatchdogTimeout () default 0 ;
long attemptTimeout () default 0 ;
}
2. 基于 AOP 实现 Lock4j 注解,落地 Redisson 分布式锁
@Aspect
@Slf4j
@Order(-10)
@Component
public class LockAop {
public static final String SEPARATOR = ":" ;
private final long lockWatchdog;
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);
if (rLock != null ) {
try {
if (attemptTimeout == -1 ) {
res = true ;
rLock.lock(lockWatchdogTimeout, TimeUnit.MILLISECONDS);
} else {
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);
}
private String getRedissonKey (ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) {
ExpressionParser parser = new SpelExpressionParser ();
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());
Object realValue = expression.getValue(context);
return lock4j.lockType() + realValue;
}
}
初始化配置 :注入全局锁超时时间、等待加锁超时时间、Redisson,定义锁 Key 分隔符;
切点定义 :匹配所有标注 @Lock4j 注解的方法,作为分布式锁的增强目标;
参数校验 :校验注解中锁 Key 配置是否为空,为空则抛异常;
动态 Key 解析 :解析注解中的 SpEL 表达式,结合方法参数生成实际业务锁 Key;
参数适配 :优先使用注解配置的超时时间,无配置则用全局默认值;
锁对象创建 :根据锁类型 + 分隔符 + 业务 Key,创建 Redisson 分布式锁对象;
加锁逻辑 :
等待超时为 - 1:调用 lock() 阻塞等待,直到获取锁;
有等待超时:调用 tryLock() 尝试加锁,超时未获取则失败;
业务执行 :加锁成功则执行目标方法,失败则抛 "获取锁失败" 异常;
锁释放 :最终在 finally 块中,仅当成功获取锁且锁未释放时,执行解锁操作;
异常兜底 :锁对象创建失败时,抛 "重复提交" 异常。
CompletableUtil 工具包 封装 CompletableUtil 工具类,支持线程 / 虚拟线程异步执行
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());
}
public static <U> CompletableFuture<U> supplyVirtual (Supplier<U> supplier) {
return CompletableFuture.supplyAsync(supplier, CustomThreadPool.getVIRTUAL_EXECUTOR());
}
public static CompletableFuture<Void> runVirtual (Runnable runnable) {
return CompletableFuture.runAsync(runnable, CustomThreadPool.getVIRTUAL_EXECUTOR());
}
}
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();
多任务组合 :支持串行、并行、任意一个完成、全部完成等组合方式;
异常处理 :内置异常捕获机制,避免异步任务异常导致线程挂掉;
线程池适配 :可指定自定义线程池,控制异步任务的线程资源。
类型
核心方法
作用
异步执行无返回值
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)
捕获任务异常并返回默认值/同时处理正常结果和异常
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);
}
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 );
CompanyDTO dto = future.join();
log.info("企业名称:{}" , dto.getName());
}
public void testThenApply () {
asyncGetCompanyById(1L )
.thenApply(dto -> {
dto.setShortName(dto.getName() + "-" );
return dto;
})
.thenAccept(dto -> log.info("处理:{}" , dto.getShortName()))
.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. 串行异步任务(比如先查企业,再查该企业的用户)
public CompletableFuture<List<UserDTO>> asyncGetCompanyUser (Long companyId) {
return asyncGetCompanyById(companyId)
.thenCompose(dto -> CompletableFuture.supplyAsync(() -> {
List<UserDO> userDOList = userMapper.selectByCompanyId(companyId);
return userDOList.stream().map(this ::convertToUserDTO).toList();
}, asyncPool));
}
场景:创建企业时,并行异步初始化 "企业配置" + "企业权限",全部完成后返回结果。
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 ;
});
}
场景:查询企业信息,同时从缓存和数据库查,哪个快用哪个。
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 -> {
CompanyDTO dto = (CompanyDTO) result;
if (dto == null ) {
return dbFuture.join();
}
return dto;
});
}
定时任务线程池 @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 注解的定时任务使用该线程池执行。
自定义线程池实现 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 ;
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()
);
}
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 实现日志记录 @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" );
}
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "result")
public void doAfterReturning (JoinPoint joinPoint, Log controllerLog, Object result) {
handleLog(joinPoint, controllerLog, null , result);
}
@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));
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 处理
缓存失效问题 @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());
return true ;
}
缓存机制未接入实际业务流程,所有对外核心业务方法均直接操作数据库,既不读取也不写入缓存;而 removeCache () 仅删除 "company:all" 缓存键,该缓存键仅由内部方法设置且未被外部业务调用,即便增删改方法执行了 removeCache (),也因目标缓存从未被实际使用而无法产生效果,最终导致缓存逻辑形同虚设,removeCache () 方法丧失实际作用。
续期后旧 Token 未失效 @PostMapping("/refreshToken")
@Operation(summary = "刷新 token")
public CommonResult<String> refreshToken (HttpServletRequest request) {
return CommonResult.success(systemService.refreshToken(request));
}
public String refreshToken (HttpServletRequest request) {
if (expireTime < 30 * 60 ) {
return tokenService.createToken(loginUser);
}
return tokenService.getToken(request);
}
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。
public String refreshToken (HttpServletRequest request) {
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);
}
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 失败" );
}
}
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online