You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot自定义JWT过期时间引发Bean循环依赖问题求助

Spring Boot JWT自定义过期时间引发的循环依赖问题分析与解决方案

问题场景

在Spring Boot项目中实现根据用户偏好自定义JWT过期时间时,出现循环依赖错误,错误信息如下:

The dependencies of some of the beans in the application context form a cycle:

   jwtAuthFilter defined in file [C:\Users\hp\Desktop\supfit\backend\supplement\target\classes\com\project\supplement\security\config\JwtAuthFilter.class]
┌─────┐
|  jwtService defined in file [C:\Users\hp\Desktop\supfit\backend\supplement\target\classes\com\project\supplement\security\config\JwtService.class]
↑     ↓
|  userServiceImpl defined in file [C:\Users\hp\Desktop\supfit\backend\supplement\target\classes\com\project\supplement\service\impl\UserServiceImpl.class]
└─────┘


Action:

Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.

相关代码片段

JwtService.java

@Service
@RequiredArgsConstructor
public class JwtService {

    @Value("${jwt.key}")
    private String SECRET;

    @Value("${jwt.expiration-time}")
    private long defaultExpirationTime;

    private final UserService userService;

    public String generateToken(Map<String, Object> extraClaims,
                                UserDetails userDetails){

        User user = userService.findUserByEmail(userDetails.getUsername());

        long expirationTime;

        if(user == null){
            expirationTime = defaultExpirationTime;
        }else{
            expirationTime = System.currentTimeMillis() + (user.getTokenValidation() * 60 * 1000);
        }

        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(expirationTime))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

     ...
}

UserServiceImpl.java

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final ModelMapper modelMapper;
    private final PasswordEncoder passwordEncoder;
    private final JwtService jwtService;

@Override
    public AuthResponse updateTokenValidation(Long userId, Long tokenValidation) {

        User user = userRepository.findById(userId)
                .orElseThrow(UserNotExistsException::new);
        user.setTokenValidation(tokenValidation);
        userRepository.save(user);

        var jwtToken = jwtService.generateToken(user);
        return AuthResponse.builder()
                .token(jwtToken)
                .build();
    }

    ...
}

JwtAuthFilter.java

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");
        final String token;
        final String userEmail;
        if(authHeader == null || !authHeader.startsWith("Bearer ")){
            filterChain.doFilter(request, response);
            return;
        }
        token = authHeader.substring(7);
        userEmail = jwtService.extractUsername(token);
        if(userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null){
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
            if(jwtService.isTokenValid(token, userDetails)){
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );
                authToken.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                );
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

成因分析

循环依赖的核心在于两个服务的互相引用:

  • JwtService 通过构造注入依赖 UserService(实际实现为 UserServiceImpl),用于生成Token时查询用户的过期时间偏好
  • UserServiceImpl 又通过构造注入依赖 JwtService,用于更新用户过期时间后重新生成Token
  • 这种双向依赖导致Spring容器初始化Bean时无法确定创建顺序,从而触发循环依赖错误

解决方案

方案1:重构代码解耦(最佳实践)

核心思路是移除JwtService对UserService的依赖,将获取用户过期时间的逻辑转移到调用方,让JwtService只专注于Token生成的核心逻辑。

修改JwtService

移除UserService依赖,新增接收自定义过期时间的重载方法:

@Service
@RequiredArgsConstructor
public class JwtService {

    @Value("${jwt.key}")
    private String SECRET;

    @Value("${jwt.expiration-time}")
    private long defaultExpirationTime;

    // 核心方法:接收自定义过期时间,无需依赖UserService
    public String generateToken(Map<String, Object> extraClaims,
                                UserDetails userDetails,
                                Long customExpirationMinutes){

        long expirationTime = customExpirationMinutes != null 
            ? System.currentTimeMillis() + (customExpirationMinutes * 60 * 1000) 
            : defaultExpirationTime;

        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(expirationTime))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // 保留默认重载方法,兼容原有调用场景
    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return generateToken(extraClaims, userDetails, null);
    }

     ...
}

修改UserServiceImpl的updateTokenValidation方法

在调用generateToken时直接传入用户的过期时间偏好:

@Override
public AuthResponse updateTokenValidation(Long userId, Long tokenValidation) {

    User user = userRepository.findById(userId)
            .orElseThrow(UserNotExistsException::new);
    user.setTokenValidation(tokenValidation);
    userRepository.save(user);

    // 直接传入用户的tokenValidation,无需JwtService查询用户
    var jwtToken = jwtService.generateToken(Collections.emptyMap(), user, tokenValidation);
    return AuthResponse.builder()
            .token(jwtToken)
            .build();
}

对于其他需要生成Token的场景(比如登录),同样在调用方先查询用户,再将过期时间传入JwtService,彻底解耦两个服务。

方案2:使用@Lazy延迟加载(妥协方案)

在其中一个Bean的依赖上添加@Lazy注解,让Spring延迟初始化该依赖Bean,打破初始化时的循环:

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;
    private final ModelMapper modelMapper;
    private final PasswordEncoder passwordEncoder;
    @Lazy // 添加延迟加载,JwtService会在第一次使用时才初始化
    private final JwtService jwtService;

    // ... 其他代码
}

该方案无需大幅修改代码,但会保留代码耦合,仅适合临时解决问题。

方案3:允许循环依赖(不推荐)

在application.properties或application.yml中开启循环依赖允许:

spring.main.allow-circular-references=true

此方法最简单,但会让代码耦合度更高,不利于后续维护,且可能隐藏其他设计问题,仅作为最后手段使用。


内容的提问来源于stack exchange,提问作者Requiet

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 22:24:57