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
相关产品推荐
相关产品推荐

