@TaikiTkwkbysh (WAKA Engineer)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

The dependencies of some of the beans in the application context form a cycleが解決できない

解決したいこと

The dependencies of some of the beans in the application context form a cycleというspring bootのエラーを解決したいです。

現在Spring bootを学習している者です。
Spring解体新書という参考書を学習中、下記のようなエラー起きてしまい、
つまづいてしまいました。

発生している問題・エラー

***************************
APPLICATION FAILED TO START
***************************

Description:

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

┌─────┐
|  securityConfig (field private org.springframework.security.core.userdetails.UserDetailsService com.example.demo.config.SecurityConfig.userDetailsService)
↑     ↓
|  userDetailServiceImpl (field private com.example.demo.domain.user.service.UserService com.example.demo.domain.user.service.impl.UserDetailServiceImpl.service)
↑     ↓
|  userServiceImpl (field private org.springframework.security.crypto.password.PasswordEncoder com.example.demo.domain.user.service.impl.UserServiceImpl.encoder)
└─────┘


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.

上記のエラーの原因は調べたところ、
userServiceImpl クラス、
userDetailServiceImpl クラス、
securityConfig クラス
の3つのクラスのフィールドがそれぞれのクラスのフィールドを参照しており、中でサイクルを起こしているというものだということが分かりましたが、解決方法がわかりません。

該当するソースコード

SecurityConfigクラス
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /** セキュリティの対象外を設定 */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // セキュリティを適用しない
        web
            .ignoring()
                .antMatchers("/webjars/**")
                .antMatchers("/css/**")
                .antMatchers("/js/**")
                .antMatchers("/h2-console/**");
    }

    /** セキュリティの各種設定 */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // ログイン不要ページの設定
        http
            .authorizeRequests()
                .antMatchers("/login").permitAll() //直リンクOK
                .antMatchers("/user/signup").permitAll() //直リンクOK
                .antMatchers("/admin").hasAuthority("ROLE_ADMIN") // 権限制御
                .anyRequest().authenticated(); // それ以外は直リンクNG

        // ログイン処理
        http
            .formLogin()
                .loginProcessingUrl("/login") // ログイン処理のパス
                .loginPage("/login") // ログインページの指定
                .failureUrl("/login?error") // ログイン失敗時の遷移先
                .usernameParameter("userId") // ログインページのユーザーID
                .passwordParameter("password") // ログインページのパスワード
                .defaultSuccessUrl("/user/list", true); // 成功後の遷移先

        // ログアウト処理
        http
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout");

        // CSRF対策を無効に設定(一時的)
        //http.csrf().disable();
    }

    /** 認証の設定 */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        PasswordEncoder encoder = passwordEncoder();
        // インメモリ認証
        /*
        auth
            .inMemoryAuthentication()
                .withUser("user") // userを追加
                    .password(encoder.encode("user"))
                    .roles("GENERAL")
                .and()
                .withUser("admin") // adminを追加
                    .password(encoder.encode("admin"))
                    .roles("ADMIN");
        */

        // ユーザーデータ認証
        auth
            .userDetailsService(userDetailsService)
            .passwordEncoder(encoder);
    }
}

UserServiceImplクラス

package com.example.demo.domain.user.service.impl;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.example.demo.domain.user.model.MUser;
import com.example.demo.domain.user.service.UserService;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserService service;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // ユーザー情報取得
        MUser loginUser = service.getLoginUser(username);

        // ユーザーが存在しない場合
        if(loginUser == null) {
            throw new UsernameNotFoundException("user not found");
        }

        // 権限List作成
        GrantedAuthority authority = new SimpleGrantedAuthority(loginUser.getRole());
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(authority);

        // UserDetails生成
        UserDetails userDetails = (UserDetails) new User(loginUser.getUserId(), loginUser.getPassword(), authorities);

        return userDetails;
    }
}

UserServiceクラス
package com.example.demo.domain.user.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.domain.user.model.MUser;
import com.example.demo.domain.user.service.UserService;
import com.example.demo.repository.UserMapper;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper mapper;

    @Autowired
    private PasswordEncoder encoder;

    /** ユーザー登録 */
    @Override
    public void signup(MUser user) {
        user.setDepartmentId(1); // 部署
        user.setRole("ROLE_GENERAL"); // ロール

        // パスワード暗号化
        String rawPassword = user.getPassword();
        user.setPassword(encoder.encode(rawPassword));

        mapper.insertOne(user);
    }

    /** ユーザー取得 */
    @Override
    public List<MUser> getUsers(MUser user) {
        return mapper.findMany(user);
    }

    /** ユーザー取得(1件) */
    @Override
    public MUser getUserOne(String userId) {
        return mapper.findOne(userId);
    }

    /** ユーザー更新(1件) */
    @Transactional
    @Override
    public void updateUserOne(String userId,
            String password,
            String userName) {

        // パスワード暗号化
        String encryptPassword = encoder.encode(password);

        mapper.updateOne(userId, encryptPassword, userName);

        // 例外を発生させる
        // int i = 1 / 0;
    }

    /** ユーザー削除(1件) */
    @Override
    public void deleteUserOne(String userId) {
        int count = mapper.deleteOne(userId);
    }

    /** ログインユーザー情報取得 */
    @Override
    public MUser getLoginUser(String userId) {
        return mapper.findLoginUser(userId);
    }
}

自分で試したこと

解決方法を調べたところ、どうやら@Lazyというアノテーションを使用すれば解決できるとのことなのですが、
あまり詳しい利用方法が載っておらず、困っている状況です。

@Lazyで解決で切るのであれば、よろしければ具体的な使用方法や解説も含めてご教示いただけますと幸いです。
また、@Lazy以外に解決方法があるのであれば、そちらでも結構ですので、ご教示の程よろしくお願い致します。

0 likes

1Answer

UserDetailServiceImplクラスの記載を

    @Autowired
    private UserService service;

から

    private UserService service;

    @Autowired
    public UserDetailServiceImpl(@Lazy UserService service){
        this.service = service;
    }

としてみたら動きませんか??
現状だと起動時にすべてのインスタンスを生成するので循環参照が起きていますが、
@Lazyを使用することで必要になった時のみにインスタンスが生成されるようになり、当事象は解消するかと思います。

1Like

Comments

  1. @TaikiTkwkbysh

    Questioner

    @MrMs様

    この度はご教示頂き、誠にありがとうございます。
    ご教示頂いた内容で起動させたところ、無事正常に機能させることができました。

    MrMs様にお伺いしたい事が3つございます。
    もしよろしければ、お時間のある時にでも結構でございますので、ご教示頂けますと幸いです。

    ◆循環参照の原因について
    今回起きた循環参照の原因を、自分なりに解釈し、まとめてみました。
    自分の解釈に間違いがないかをご教示頂きたいです。

    ①SecurityConfigクラスにはUserDetailsServiceクラスのインスタンスのAutowired、passwordEncoderインスタンスを生成するBeanがある。

    ②SecurityConfigクラスが参照しているUserDetailsServiceクラスには、UserServiceインスタンスがある。

    ③UserDetailsServiceクラスで参照しているUserService(UserServiceImpl)クラスには、PasswordEncoderインスタンスがAutowiredがある。

    ④UserService(UserServiceImpl)クラスにあるPasswordEncoderインスタンスは、SecurityConfigクラスのpasswordEncoderインスタンスを生成するBeanを参照しているので、これにより循環参照が発生する。

    ◆@Lazyを使用するクラスについて
    今回@Lazyを使用したのはUserService(UserServiceImpl)クラスでしたが、このクラスに使用したのにはどういう理由がありますでしょうか。
    現在自分の中では、UserService(UserServiceImpl)クラスがSecurityConfigクラスに戻す原因であるから?かと考えておりますが、何分まだSpring Bootを学習したてであまり理解ができていないものでして....
    よろしければ、ご教示頂きたいです。

    ◆@Lazyの利用方法について
    MrMs様は、この@Lazyクラスの利用方法をどこで学ばれましたでしょうか。
    やはりドキュメント等を見て学ばれましたでしょうか。
    もしおすすめの参考書等がございましたら、ご教示頂きたいです。


    質問が多く、申し訳ございません。
    ご教示のほど、お願い致します!
  2. 問題なく動いてよかったです!

    >◆循環参照の原因について

    こちらに関しては、記載いただいた内容で問題ないと思います。
    passwordEncoderインスタンスを生成するBeanを別クラスに定義しても循環参照は解決できそうですね。

    >◆@Lazyを使用するクラスについて

    Bean生成時の循環参照を解消させればいいので、SecurityConfigクラスのuserDetailsService、またはUserServiceImplクラスのencoderに付与しても問題ないと思います。
    なので、解消できるのであれば上記のどれでも問題ないと思います(試していないので動かなかったらすいません)。
    今回はその中の一つを回答いたしました。

    >◆@Lazyの利用方法について

    こちらに関しては、以前自分も同じ事象に遭遇した際に以下のサイトを参考にしました。
    https://www.baeldung.com/circular-dependencies-in-spring
  3. @TaikiTkwkbysh

    Questioner

    @MrMs様

    お忙しい中早期ご対応いただき、誠にありがとうございました。
    各質問にお答えいただき、大変ありがたいです。

    参考サイト、拝見させていただきました。
    私も参考にさせていただきます。

    また機会がございましたら、ご教示頂けますと幸いです。
    今後とも何卒、宜しくお願い致します。

Your answer might help someone💌