双非 Java 后端首次实习 | 个人经验分享总结

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

实习核心流程(结合实际经历)

由于自己进入的是一个小公司实习,当时项目刚好启动,参与了较多基础模块的开发。

一、基础准备与环境搭建阶段(入职 1-3 天)

  1. 公司基础配置:进入公司飞书、拥有个人邮箱等基础办公权限
  2. 代码拉取与环境搭建:
    • 学习并使用 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 个核心步骤:

  1. 一级缓存查询:定义固定缓存键(cacheKey = "company:all"),先从 Redis 中查询缓存数据,若缓存存在则直接返回(缓存功能的通用前置步骤,优先使用缓存提升查询性能);

  2. 双重校验 + 同步锁:缓存不存在时,通过 synchronized (cacheLock) 加本地同步锁,锁内再次查询缓存,若仍不存在再执行数据库查询(缓存加载的通用并发防护,避免缓存击穿);

  3. 数据库查询 + 缓存写入:锁内执行全量数据库查询,将查询结果写 Redis 并设置过期时间。

  4. 结果统一返回:无论从缓存还是数据库获取数据,最终统一返回全量企业列表。

getCompanyFromCache
java 复制代码
public Optional<Company> getCompanyFromCache(Long companyId) {
    return cacheAll().stream()
            .filter(c -> c.getId().equals(companyId))
            .findFirst();
}

这段代码是企业级系统中 "基于全量缓存的单条实体精准查询" 类功能的典型通用逻辑框架,核心遵循 "全量缓存加载→精准过滤→空值安全返回" 的标准化流程,具体可拆解为 3 个核心步骤:

  1. 全量缓存加载(复用缓存数据):调用 cacheAll () 方法加载全量企业缓存数据。

  2. 精准数据过滤(定位目标实体):通过 stream ().filter (c -> c.getId ().equals (companyId)) 过滤出匹配企业 ID 的实体,结合 findFirst () 获取单条结果。

  3. 空值安全返回:返回 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 个核心步骤:

  1. **分页参数封装 + 仓储层查询:**通过 Page.of (dto.getPageNum (), dto.getPageSize ()) 封装统一的分页参数,调用 companyRepository.getCompanyList () 执行数据库分页查询,返回包含 "分页元数据 + 原始实体列表" 的 SimplePage 对象;

  2. **通用工具类转换(DO→VO):**调用 PageUtil.getPage () 工具方法,结合 Function 函数式接口传入 VO 构建逻辑(r -> CompanyListVO.builder ()),将数据库实体(DO)转换为前端展示 VO;

  3. **分页元数据透传 + 空值过滤:**工具类完整保留原分页对象的总页数、总条数、当前页 / 页大小等元数据,同时通过 stream ().filter (Objects::nonNull) 过滤转换后的空值;

  4. **统一结果返回:**返回转换后的 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()));
}
  1. @PathVariable负责接收路径参数required = false控制参数是否可选;

  2. @Parameter仅用于接口文档说明,不影响参数逻辑;

  3. @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());
}

这段代码是企业级系统中「下拉列表数据查询 + 全量缓存优化」类功能的标准化实现框架,核心遵循「缓存全量加载→多维度过滤→排序转换→结果返回」的四层核心流程,具体拆解如下:

  1. 缓存全量加载:优先调用 cacheAll() 加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率;
  2. 多维度条件过滤:通过流式过滤实现三层规则校验 ------ 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
  3. 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
  4. 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。
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"));
}

这段代码是企业级系统中「单实体详情查询 + 数据权限管控 + 缓存优化」类功能的标准化实现框架,核心遵循五层核心流程,具体拆解如下:

  1. 缓存优先精准查询:优先调用 getCompanyFromCache(companyId) 从全量企业缓存中过滤匹配目标企业 ID 的数据,替代直接查库,利用缓存提升高频详情查询的响应效率;
  2. 权限二次校验:即使缓存命中目标企业数据,仍需校验登录用户数据权限;
  3. 数据库兜底查询:若缓存未命中目标数据,则通过 Wrappers.lambdaQuery() 构建查询条件,并通过 last("limit 1") 限定单条结果返回,同时查询后更新缓存保证后续命中率;
  4. 结果有效性校验:对缓存 / 数据库查询结果做非空校验,若返回 null 则抛出标准化业务异常,作为详情查询的通用兜底逻辑,避免空值引发前端渲染异常或下游业务空指针;
  5. 合规结果返回:经缓存 / 数据库查询、权限校验、有效性校验后,返回符合条件的企业实体对象,既保证查询性能,又满足数据权限管控和业务完整性要求。
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());
}

这段代码是企业级系统中「下拉列表数据查询 + 全量缓存优化」类功能的标准化实现框架,核心遵循「缓存全量加载→多维度过滤→排序转换→结果返回」的四层核心流程,具体拆解如下:

  1. 缓存全量加载:优先调用 cacheAll() 加载全量企业缓存数据,替代直接查询数据库,提升高频下拉列表查询的响应效率;
  2. 多维度条件过滤:通过流式过滤实现三层规则校验 ------ 匹配指定企业 ID、校验用户数据权限、排除系统默认企业;
  3. 排序与数据转换:按企业名称升序排序,再将企业实体映射为下拉列表专用 VO;
  4. 结构化结果返回:将过滤、排序、转换后的结果封装为列表,满足前端下拉框的业务需求。
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 个核心步骤:

  1. 唯一性校验(防重复): 通过companyRepository.countByName(dto.getName())校验企业名称是否已存在,避免重复创建,若重复则抛出业务异常;

  2. 参数转换(DTO→实体): 将入参AddCompanyDTO(数据传输对象)转换为Company数据库实体对象,仅保留业务所需字段,完成 "前端参数→数据库模型" 的映射;

  3. 数据持久化(事务保障): 通过companyRepository.save(company)将实体保存数据库,结合@Transactional(rollbackFor = Exception.class)注解,确保保存失败时事务回滚,保证数据一致;

  4. 缓存清理(性能优化): 调用companyRepository.removeCache()清理缓存,避免新增数据与缓存数据不一致(适用于有缓存场景的通用操作);

  5. 扩展业务处理(可选): 根据createMainAccount参数判断是否创建企业主账号,通过 Spring 工具类调用其他服务完成关联业务,体现 "主业务 + 附属业务" 的扩展逻辑;

  6. **日志记录 + 结果返回:**记录关键操作日志,最终返回新增实体的主键 ID;

  7. 并发控制(额外保障): 通过@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 个核心步骤:

  1. 存在性校验(防无效更新):通过 companyRepository.getById (dto.getCompanyId ()) 查询目标企业是否存在,若不存在则抛出业务异常(更新类功能的通用前置校验,避免更新不存在的实体);

  2. 权限校验(防越权操作):校验当前登录用户是否为管理员 / 经理,或是否归属该企业,仅满足权限条件才允许更新(企业级系统更新操作的核心权限控制,避免越权修改数据);

  3. 数据更新(事务保障):调用 companyRepository.updateCompany (dto) 执行企业信息更新,结合 @Transactional (rollbackFor = Exception.class) 注解,确保更新失败时事务回滚,保证数据一致性;

  4. 并发控制(额外保障):通过 @Lock4j (lockType = "updateCompany", key = "#dto.companyId") 基于企业 ID 加分布式锁,解决高并发下的重复更新问题(更新类功能的进阶并发防护);

  5. 日志记录 + 结果返回:记录用户更新企业的关键操作日志(便于审计和问题排查),权限校验通过则返回 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 个核心步骤:

  1. 存在性校验(防无效删除):通过 companyRepository.getById (companyId) 查询目标企业是否存在,若不存在则抛出业务异常(ResponseCode.MERCHANT_NOT_EXISTS);

  2. 特殊规则校验(防误删核心数据):校验目标企业是否为系统核心企业(ID=1L),若是则抛出业务异常(ResponseCode.MERCHANT_DISABLE_DELETED);

  3. 关联数据校验(防数据脏删):依次检查企业下是否关联设备(deviceRepository.countByCompanyId)、是否存在用户子账号(userRepository.countByCompanyId),若存在关联数据则抛出对应业务异常;

  4. 数据删除(事务保障):调用 companyRepository.removeById (companyId) 执行企业删除操作,结合 @Transactional注解,确保删除失败时事务回滚,保证数据一致性;

  5. 并发控制 + 缓存清理(额外保障):通过 @Lock4j (lockType = "delCompany", key = "#companyId") 基于企业 ID 加分布式锁,解决高并发下的重复删除问题;删除后调用 companyRepository.removeCache () 清理缓存,避免删除数据与缓存数据不一致。

  6. 日志记录 + 结果返回:记录用户删除企业的关键操作日志,所有校验通过后返回 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 个核心步骤:

  1. 接口层接收请求:通过 @PostMapping ("/list") 定义用户列表接口;

  2. 服务层分页封装:调用 Page.of (dto.getPageNum (), dto.getPageSize ()) 封装分页参数,传入仓储层查询方法,返回 SimplePage 分页对象 ;

  3. 仓储层动态条件查询:Mapper 层通过 XML 构建动态 SQL,支持企业 ID、用户名、用户 ID 等业务条件过滤,同时追加登录用户权限过滤,并关联企业表查询企业名称;

  4. 结果格式化处理:查询结果返回后,遍历分页列表将角色字符串(roleString)拆分为角色列表(roleList),补充到 VO 中 ;

  5. 分层数据返回:Mapper 层返回 Page<SysUserListVO>,服务层封装为 SimplePage,接口层最终通过 CommonResult.success () 统一返回 ;

  6. 数据安全过滤: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 个核心步骤:

  1. 接口层参数校验:通过 @RequestParam 接收用户 ID 参数,配合 @Validated + @NotNull 强制校验参数非空,若为空则抛出校验异常(详情查询的通用参数防护,避免无效查询);

  2. 服务层查询调用(逻辑转发):调用仓储层 getUserDetail () 方法,传入用户 ID 和当前登录用户信息,获取原始详情数据;

  3. 仓储层精准查询(数据兜底):Mapper 层通过 XML 构建精准 SQL,根据用户 ID 查询用户基础信息,并关联企业表查询所属企业名称,同时过滤已删除数据(deleted = 0);

  4. 结果格式化处理:将返回的角色字符串 roleString 拆分为角色列表 roleList ,补充 VO;

  5. 统一结果返回:接口层通过 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 个核心步骤:

  1. 接口层参数校验(防非法请求):通过 @RequestBody + @Validated 触发 DTO 内部的参数校验规则,确保重置密码所需参数合法;

  2. 存在性校验:通过 getById () 查询目标用户是否存在,若不存在则抛出业务异常;

  3. 权限校验(防越权操作):校验当前登录用户是否为企业用户且非目标用户本人,若是则抛出业务异常(密码重置的核心权限控制,避免越权重置他人密码);

  4. 密码更新(事务保障):调用 userRepository.sysUserRestPwd (dto) 更新用户密码,密码通过 encode () 加密后存储,结合 @Transactional注解保证更新失败时事务回滚;

  5. 并发控制(额外保障):通过 @Lock4j () 基于用户 ID 加分布式锁,解决高并发下的重复重置密码问题(密码操作的进阶并发防护);

  6. 安全登出处理(避免旧密码 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 个核心步骤:

  1. 接口层参数校验(防非法请求):通过 @RequestBody + @Validated 触发 DTO 内部的参数校验规则,确保新增用户所需参数合法;

  2. 用户名唯一性校验(防重复创建):调用 userRepository.checkUsername () 方法,通过 count () 统计用户名是否已存在,若已存在则抛出业务异常;

  3. 企业存在性校验(防无效关联):通过 companyRepository.getById (dto.getCompanyId ()) 查询关联企业是否存在,若不存在则抛出业务异常;

  4. 用户实体构建(数据映射):将 AddSysUserDTO 转换为 User 数据库实体,密码通过 encode () 加密存储,角色列表拼接为字符串,昵称为空时兜底为用户名;

  5. 数据保存(事务保障):调用 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 被非法调用。


二、完整业务流程

  1. 请求接收与日志记录 :前端 / 第三方系统调用/auth/token接口,传入apiKeysecret,接口先记录请求日志(含apiKey),便于后续问题排查;
  2. API Key 合法性校验 :根据apiKey查询数据库中的ApiKey实体,校验三大规则:
    • apiKey是否存在(不存在则抛 "API 密钥不存在" 异常);
    • apiKey是否启用(禁用则抛 "API 密钥已禁用" 异常);
    • secret是否匹配(通过matches()校验加密后的秘钥,错误则抛 "秘钥错误" 异常);
  3. 旧 Token 强制撤销 :为保证 "一个 API Key 同时仅存在一个有效 Token",先撤销该apiKey关联的所有已存在 Token;
  4. 新 Token 生成 :通过ApiKeyGenerator.generateToken()生成随机、唯一的 Token 字符串;
  5. Token 有效期设置 :设定 Token 过期时间,计算当前时间到过期时间的秒数(expiresIn),用于告知第三方 Token 的有效时长;
  6. Token 持久化存储 :将 Token、apiKey、创建时间、过期时间封装为TokenInfo,保存到 Redis,利用 Redis 的过期机制实现 Token 自动失效,减轻数据库查询压力;
  7. 响应数据封装 :构建TokenResponse返回给调用方,包含核心字段:
    • token:实际用于 API 调用的令牌字符串;
    • tokenType:固定为Bearer
    • expiresIn:Token 过期剩余秒数;
    • scope:Token 权限范围;
  8. 异常兜底与日志:全程捕获异常并记录错误日志,异常直接抛出由全局异常处理器返回标准化错误,保证调用方能清晰感知失败原因(如秘钥错误、密钥禁用)。
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 核心流程

  1. 初始化配置:注入全局锁超时时间、等待加锁超时时间、Redisson ,定义锁 Key 分隔符;

  2. 切点定义 :匹配所有标注@Lock4j注解的方法,作为分布式锁的增强目标;

  3. 参数校验:校验注解中锁 Key 配置是否为空,为空则抛异常;

  4. 动态 Key 解析:解析注解中的 SpEL 表达式,结合方法参数生成实际业务锁 Key;

  5. 参数适配:优先使用注解配置的超时时间,无配置则用全局默认值;

  6. 锁对象创建:根据锁类型 + 分隔符 + 业务 Key,创建 Redisson 分布式锁对象;

  7. 加锁逻辑

    1. 等待超时为 - 1:调用lock()阻塞等待,直到获取锁;

    2. 有等待超时:调用tryLock()尝试加锁,超时未获取则失败;

  8. 业务执行:加锁成功则执行目标方法,失败则抛 "获取锁失败" 异常;

  9. 锁释放:最终在 finally 块中,仅当成功获取锁且锁未释放时,执行解锁操作;

  10. 异常兜底:锁对象创建失败时,抛 "重复提交" 异常。

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,它的核心优势:

  1. 异步执行+结果回调 :任务执行完自动触发回调,无需手动轮询 Future.get()

  2. 多任务组合:支持串行、并行、任意一个完成、全部完成等组合方式;

  3. 异常处理:内置异常捕获机制,避免异步任务异常导致线程挂掉;

  4. 线程池适配:可指定自定义线程池,控制异步任务的线程资源。

基础用法

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 默认的单线程调度器,解决定时任务串行执行、阻塞、宕机丢失任务等问题,核心价值是提升定时任务的并发能力和稳定性。

  1. 自定义线程池:创建核心线程数为 20 的 ThreadPoolTaskScheduler,替代默认单线程;

  2. 线程命名 :设置线程名前缀Tplus-Scheduler-,便于日志排查线程归属;

  3. 优雅停机setWaitForTasksToCompleteOnShutdown(true):应用关闭时等待所有定时任务执行完成;setAwaitTerminationSeconds(60):最多等待 60 秒,超时则强制终止;

  4. 绑定调度器:将自定义线程池绑定到 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失败");
    }
}

**感谢你的阅读!**🌹

相关推荐
AMoon丶2 小时前
C++新特性-智能指针
linux·服务器·c语言·开发语言·c++·后端·tcp/ip
计算机徐师兄2 小时前
Java基于SSM的校园顺路代送微信小程序【附源码、文档说明】
java·微信小程序·ssm·校园顺路代送微信小程序·校园顺路代送·顺路代送微信小程序·java校园顺路代送微信小程序
rannn_1112 小时前
【Redis|实战篇2】黑马点评|商户查询缓存
java·redis·后端·缓存
huohuopro3 小时前
idea使用教程
java·ide·intellij-idea
NGC_66113 小时前
ArrayList扩容机制
java·前端·算法
一方热衷.9 小时前
YOLO26-Seg ONNXruntime C++/python推理
开发语言·c++·python
靓仔建10 小时前
Vue3导入组件出错does not provide an export named ‘user_setting‘ (at index.vue:180:10)
开发语言·前端·typescript
HalvmånEver10 小时前
7.高并发内存池大页内存申请释放以及使用定长内存池脱离new
java·spring boot·spring