跳到主要內容

探 Spring Security 用戶帳號資料儲存於DB

 前言


承襲 {初探 Spring Security 文章},使用 InMemoryUserDetailsManager,建立帳號與密碼並儲存於記憶體中。

現實中,我們不會將帳號與密碼這們做,一般情況下,都會存放在資料庫,或者LDAP。

以下我們將改寫使用 MySQL 來管理我們的使用者帳戶。



專案實作

(本次代碼有點多,請細看)


1. 新增 pom.xml 相關 Dependencies 

Pom.xml

<dependency>

            <groupId>com.mysql</groupId>

            <artifactId>mysql-connector-j</artifactId>

            <scope>runtime</scope>

        </dependency>



2.增修相關代碼

修改 Web 安全性, 網路安全配置類別 WebSecurityConfig(使用 HTTP Basic Authentication)


增修 SecurityConfig


//SecurityConfig.java

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.http.HttpMethod;

import org.springframework.security.config.Customizer;

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;

import org.springframework.security.config.http.SessionCreationPolicy;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.SecurityFilterChain;


@Configuration

@EnableWebSecurity

@EnableMethodSecurity

public class SecurityConfig {


    @Bean

    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http

            .csrf(AbstractHttpConfigurer::disable)

            .authorizeHttpRequests(auth - > auth

                // Read permissions (Guest, User, Admin)

                .requestMatchers(HttpMethod.GET, "/api/users", "/api/user/{uid}")

                .hasAnyAuthority("read") // , "ROLE_GUEST")


                // Create permissions (User, Admin)

                .requestMatchers(HttpMethod.POST, "/api/user").hasAnyAuthority("create")


                // Requirement : Admin (CRUD) - DELETE/PUT will be handled by @PreAuthorize

                .requestMatchers(HttpMethod.PUT, "/api/users/{uid}").hasAnyAuthority("update")


                .requestMatchers(HttpMethod.DELETE, "/users/{uid}").hasAnyAuthority("delete")


                .anyRequest().authenticated())

            .httpBasic(Customizer.withDefaults())

            // .formLogin(Customizer.withDefaults())

            .sessionManagement(sess - > sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));


        return http.build();

    }


    @Bean

    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();

    }


}


增修 CustomUserDetailsService

//CustomUserDetailsService.java

@Service

@Transactional(readOnly = true)

public class CustomUserDetailsService implements UserDetailsService {


    @Autowired

    private UserRepository userRepository;


    @Override

    public UserDetails loadUserByUsername(String username)

            throws UsernameNotFoundException {


        User user = userRepository.findByUsername(username)

                .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));


        Set<GrantedAuthority> authorities = new HashSet<>();


        for (UserRole ur : user.getUserRoles()) {

            Role role = ur.getRole();


            authorities.add(new SimpleGrantedAuthority(

                    "ROLE_" + role.getName().name()));


            // permission-based authority

            for (String p : role.getPermissions()) {

                authorities.add(new SimpleGrantedAuthority(p));

            }

        }


        return new org.springframework.security.core.userdetails.User(

                user.getUsername(),

                user.getPassword(),

                authorities);

    }

}




增修 Entity

// Role.java

@Entity

@Getter

@Setter

@AllArgsConstructor

@Builder

@Table(name = "roles")

public class Role {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    @Enumerated(EnumType.STRING)

    @Column(nullable = false, unique = true)

    private RoleName name;


    public Role() {

    }


    public Role(RoleName role) {

        this.name = role;

    }


    public Role(RoleName name, Set<String> permissions) {

        this.name = name;

        this.permissions = permissions;

    }


    @Builder.Default

    @ElementCollection(fetch = FetchType.EAGER)

    @CollectionTable(name = "role_permissions", joinColumns = @JoinColumn(name = "role_id"))

    @Column(name = "permission")

    private Set<String> permissions = new HashSet<>();


    @Builder.Default

    @JsonBackReference

    @OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)

    private Set<UserRole> userRoles = new HashSet<>();


}



// RoleName.java

public enum RoleName {

    ADMIN,

    USER,

    GUEST;


}


// 角色權限 RolePermission.java

@Entity

@Getter

@Setter

@NoArgsConstructor

@AllArgsConstructor

@Builder

@Table(name = "role_permissions")

public class RolePermission {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    @Column(name = "role_id")

    private Long role_id;


    @Column(name = "permission", length = 255)

    private String permission;

}


// 使用者 User.java

@Entity

@Getter

@Setter

@AllArgsConstructor

@Builder

@Table(name = "users")

public class User {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;


    @Column(name = "username", nullable = false, unique = true)

    private String username;


    @Column(name = "password", nullable = false)

    private String password;


    @Column(name = "first_name", nullable = true)

    private String firstName;


    @Column(name = "last_name", nullable = true)

    private String lastName;


    @Column(name = "email", nullable = false, unique = true)

    private String email;


    @JsonManagedReference

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)

    private Set<UserRole> userRoles = new HashSet<>();


    public User() {

    }


    public User(String username, String password, String firstName, String lastName, String email) {

        this.username = username;

        this.password = password;

        this.firstName = firstName;

        this.lastName = lastName;

        this.email = email;

    }


    public void addRole(UserRole role) {

        userRoles.add(role);

        role.setUser(this);

    }


    public void removeRole(UserRole role) {

        userRoles.remove(role);

        role.setUser(null);

    }


}


//UserRole.java

/**

 * 中間實體定義(UserRole)

 */

@Entity

@Getter

@Setter

@NoArgsConstructor

@AllArgsConstructor

@Builder

@Table(name = "users_roles")

public class UserRole implements Serializable {


    @Id

    @GeneratedValue(strategy = GenerationType.IDENTITY)

    private Long id;



    // ManyToOne 關係到 User

    @JsonBackReference

    @ManyToOne(fetch = FetchType.LAZY)

    @JoinColumn(name = "user_id")

    private User user;


    // ManyToOne 關係到 Role

    @ManyToOne(fetch = FetchType.LAZY)

    @JoinColumn(name = "role_id")

    private Role role;


    @Column(name = "assigned_at")

    private LocalDateTime assignedAt = LocalDateTime.now();


    public UserRole(User user, Role role) {

        this.user = user;

        this.role = role;

    }

}


增修  Repository

// UserRepository.java

@Repository

public interface UserRepository extends JpaRepository<User, Long> {


    @Query("SELECT u FROM User u " +

            "LEFT JOIN FETCH u.userRoles ur " + // 載入 UserRole 集合

            "LEFT JOIN FETCH ur.role " + // 透過 ur 載入 Role 實體本身

            "WHERE u.id = :id")

    Optional<User> findByIdWithRolesAndRoleDetails(@Param("id") Long id);


    Optional<User> findByUsername(String username);


}



@Repository

public interface RoleRepository extends JpaRepository<Role, Long> {

    Optional<Role> findByName(RoleName name);

}


@Repository

public interface UserRoleRepository extends JpaRepository<UserRole, Long> {


}



增修  Service

// UserService.java

@Slf4j

@Service

public class UserService {


    @Autowired

    private PasswordEncoder passwordEncoder; // Used for hashing passwords


    @Autowired

    private UserMapper userMapper;


    @Autowired

    private UserRepository userRepository;


    @Transactional

    public User createUser(User newUser) {////////////////////////////

        if (newUser == null) {

            throw new IllegalArgumentException("User must not be null");

        }

        newUser.setPassword(passwordEncoder.encode(newUser.getPassword()));


        User user = userRepository.save(newUser);

        return user;

    }


    /**

     * 尋找單一使用者,並返回 DTO

     */

    public Optional<UserDto> findByIdDto(Long id) {

        return userRepository.findById(id)

                // 使用 mapper to DTO

                .map(userMapper::toUserDto);

    }


    public List<User> findAll() {

        return userRepository.findAll();

    }


    /**

     * 查找所有使用者,並返回 DTO 列表

     */

    public List<UserDto> findAllDto() {////////////////////////////

        return userRepository.findAll().stream()

                // 使用 mapper to DTO

                .map(userMapper::toUserDto)

                .collect(Collectors.toList());

    }


    public Optional<User> findById(Long id) {////////////////////////////

        return userRepository.findByIdWithRolesAndRoleDetails(id);

    }


    public User getUserById(Long uid) {

        if (uid == null) {

            throw new UserNotFoundException(null);

        }

        User user = userRepository.findById(uid)

                .orElseThrow(() -> new UserNotFoundException(uid));

        return user;

    }


    @Transactional

    public User updateUser(@PathVariable Long id, @RequestBody User newUser) {

        log.info("Updating user with id: " + id);

        return userRepository.findById(id)

                .map(user -> {

                    user.setUsername(newUser.getUsername());

                    // Update password only if provided

                    if (newUser.getPassword() != null && !newUser.getPassword().isEmpty()) {

                        user.setPassword(passwordEncoder.encode(newUser.getPassword()));

                    }

                    user.setFirstName(null == newUser.getFirstName() ? user.getFirstName() : newUser.getFirstName());

                    user.setLastName(null == newUser.getLastName() ? user.getLastName() : newUser.getLastName());

                    user.setEmail(null == newUser.getEmail() ? user.getEmail() : newUser.getEmail());


                    return userRepository.save(user);

                }).orElseThrow(() -> new RuntimeException("User not found with id " + id));

    }


    /**

     * 刪除使用者

     */

    @Transactional

    public void deleteUser(Long uid) {

        if (uid == null) {

            throw new UserNotFoundException(null);

        }

        userRepository.deleteById(uid);

    }


}



// UserRoleService.java

@Service

public class UserRoleService {


    @Autowired

    private UserRepository userRepository;


    @Autowired

    private RoleRepository roleRepository;


    @Autowired

    private UserRoleRepository userRoleRepository;


    @Transactional

    public User addRole(Long userId, RoleName roleName) {


        // 1. 查找使用者

        User user = userRepository.findById(userId)

                .orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));


        // 強制初始化集合,避免潛在的 LazyInitializationException

        Hibernate.initialize(user.getUserRoles());


        // 2. 查找角色

        Role role = roleRepository.findByName(roleName)

                .orElseThrow(() -> new RuntimeException("Role not found: " + roleName));


        // 3. 檢查是否已存在角色

        boolean alreadyExists = user.getUserRoles().stream()

                .anyMatch(userRole -> userRole.getRole().getName().equals(roleName));


        if (alreadyExists) {

            return user;

        }


        // 4. 建立並設定 UserRole 關聯實體

        UserRole userRole = new UserRole();


        userRole.setUser(user);

        userRole.setRole(role);


        user.getUserRoles().add(userRole);


        userRoleRepository.save(userRole);


        return userRepository.save(user);

    }

}



// RoleService.java

@Service

public class RoleService {


    private final RoleRepository roleRepository;


    @Autowired

    public RoleService(RoleRepository roleRepository) {

        this.roleRepository = roleRepository;

    }


    /**

     * 創建一個新的角色。

     *

     * @param role 欲儲存的角色實體

     * @return 儲存後的角色實體

     */

    @Transactional

    public Role createRole(Role role) {

        // 可以在此處添加驗證,例如檢查角色名稱是否已存在

        // RoleName roleName = RoleName.valueOf(role.getName().name().toUpperCase());

        Optional<Role> existingRole = roleRepository.findByName(role.getName());

        if (existingRole.isPresent()) {

            throw new IllegalArgumentException("Role name already exists: " +

                    role.getName());

        }

        return roleRepository.save(role);

    }


}



增修  Controller

// UserController.java

@RestController

@RequestMapping("/api")

public class UserController {


    @Autowired

    private UserService userService;


    // Create (User/Admin)

    @PostMapping("/user")

    public ResponseEntity<?> createUser(@RequestBody User newUser) {

        User user = userService.createUser(newUser);

        return new ResponseEntity<>(user, HttpStatus.CREATED);

    }


    // Read One (Guest/User/Admin)

    @GetMapping("/user/{uid}")

    public ResponseEntity<User> getUserById(@PathVariable Long uid) {

        return userService.findById(uid)

                .map(ResponseEntity::ok)

                .orElse(ResponseEntity.notFound().build());

    }


    @GetMapping("/users")

    @PreAuthorize("hasAuthority('read')")

    public ResponseEntity<List<UserDto>> getAllUsers() {

        List<UserDto> userDtos = userService.findAllDto();

        return ResponseEntity.ok(userDtos);

    }


    // Update (Admin)

    @PutMapping("/users/{uid}")

    public ResponseEntity<User> updateUser(@PathVariable Long uid, @RequestBody User userDetails) {

        User updatedUser = userService.updateUser(uid, userDetails);

        return ResponseEntity.ok(updatedUser);

    }


    @DeleteMapping("/users/{uid}")

    @PreAuthorize("hasAuthority('delete') or hasRole('ADMIN')")

    public ResponseEntity<Void> deleteUser(@PathVariable Long uid) {

        userService.deleteUser(uid);

        return ResponseEntity.noContent().build();

    }


}




增修 DTO Mapper

// UserMapper.java

@Component

public class UserMapper {


    public RoleDto toRoleDto(Role role) {

        if (role == null) {

            return null;

        }

        RoleDto roleDto = new RoleDto();

        roleDto.setId(role.getId());

        roleDto.setName(role.getName());

        return roleDto;

    }


    public Set<RoleDto> toRoleDtoSet(Set<UserRole> userRoles) {

        if (userRoles == null) {

            return Collections.emptySet();

        }


        return userRoles.stream()

                .map(UserRole::getRole)

                .map(this::toRoleDto)

                .filter(Objects::nonNull)

                // .sorted(Comparator.comparing(RoleDto::getName))

                .collect(Collectors.toSet());

    }


    /**

     * 將 User 實體轉換為 UserDto

     */

    public UserDto toUserDto(User user) {

        if (user == null) {

            return null;

        }

        UserDto userDto = new UserDto();

        userDto.setId(user.getId());

        userDto.setUsername(user.getUsername());

        userDto.setFirstName(user.getFirstName());

        userDto.setLastName(user.getLastName());

        userDto.setEmail(user.getEmail());

        userDto.setRoles(toRoleDtoSet(user.getUserRoles()));


        return userDto;

    }


}


增修 自定 Exception

// UserNotFoundException.java

public class UserNotFoundException extends RuntimeException {


    public UserNotFoundException(Long id) {

        String errString = "";

        if (id == null) {

            errString = "User ID must not be null";

        } else {

            errString = "User with ID " + id + " not found";

        }

        super(errString);

    }

}



增修 初始資料,測試用

// DataInitializer.java
@Slf4j
@Component
public class DataInitializer implements CommandLineRunner {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private RoleRepository roleRepository;
    @Autowired
    private UserRoleRepository userRoleRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public void run(String... args) throws Exception {
        userRoleRepository.deleteAll();
        userRepository.deleteAll();
        roleRepository.deleteAll();
        // --- 1. Create Roles ---
        Role adminRole = createRole(RoleName.ADMIN, Set.of("create", "read",
                "update", "delete"));
        Role userRole = createRole(RoleName.USER, Set.of("create", "read"));
        Role guestRole = createRole(RoleName.GUEST, Set.of("read"));
        // --- 2. Create Users ---
        User adminUser = createUser("admin", "password", "admin", "user",
                "admin@example.com");
        User standardUser = createUser("user", "password", "standard", "user",
                "standard@example.com");
        User guestUser = createUser("guest", "password", "guest", "user",
                "guest@example.com");
        // --- 3. Link Users to Roles (UserRole) ---
        linkUserToRole(adminUser, adminRole);
        linkUserToRole(standardUser, userRole);
        linkUserToRole(guestUser, guestRole);
    }
    @Transactional
    private Role createRole(RoleName name, Set<String> permissions) {
        Role role = new Role();
        role.setName(name);
        role.setPermissions(permissions);
        return roleRepository.save(role);
    }
    @Transactional
    private User createUser(String username, String rawPassword, String firstName, String lastName, String email) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(rawPassword));
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setEmail(email);
        return userRepository.save(user);
    }
    @Transactional
    private void linkUserToRole(User user, Role role) {
        UserRole userRole = new UserRole();
        userRole.setUser(user);
        userRole.setRole(role);
        userRoleRepository.save(userRole);
        user.getUserRoles().add(userRole);
        userRepository.save(user);
    }
}



啟動App

初始寫入測試資料

  .   ____          _            __ _ _

 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \

( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \

 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )

  '  |____| .__|_| |_|_| |_\__, | / / / /

 =========|_|==============|___/=/_/_/_/


 :: Spring Boot ::                (v3.5.8)


21:18:10.278 WARN  [com.dannyyu.backend.SpringbootBackendApplication.main()][deprecation.constructDialect\(DialectFactoryImpl.java:153\21:18:10.974 WARN  [com.dannyyu.backend.SpringbootBackendApplication.main()][JpaBaseConfiguration$JpaWebConfiguration.openEntityManagerInViewInterceptor\(JpaBaseConfiguration.java:258\Hibernate: select ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,ur1_0.user_id from users_roles ur1_0


. . .

Hibernate: insert into roles (name) values (?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into roles (name) values (?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into roles (name) values (?)

Hibernate: insert into role_permissions (role_id,permission) values (?,?)

Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)

Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)

Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)

Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)

Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?

Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)

Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?

Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)

Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?


確認測試資料已存入DB











測試

測試案例

User 資料

{

    "username": "test",

    "password": "123456",

    "firstName": "test",

    "lastName": "yu",

    "email": "test@example.com"

}



GUEST:不能 POST,不能新增 User



















回應


















ADMIN:新增 User 
















回應

















確認數據已進DB














GUEST:可以 GET


























測試工具 Postman

Postman 設定方式(HTTP Basic)

  1. 開 Postman
  2. 選擇 API(例如 GET /api/users)
  3. Authorization
  4. 設定:
    • Type:Basic Auth
    • Username:你的帳號
    • Password:你的密碼(明碼)

Postman 會自動幫你產生 Header





常見錯誤 & 解法

401 Unauthorized

原因:

.沒送 Authorization

.帳號或密碼錯

檢查:

.postman Authorization 是否設定

.密碼是否為「明碼」而不是 BCrypt


403 Forbidden

原因:

.有登入成功

.但 authority 不符合

檢查:

.hasAnyAuthority("read")

.是否真的有回傳 "read"(不是 "ROLE_READ"








留言

這個網誌中的熱門文章

初探 Vue 呼叫 API 出現 CORS 跨來源資源共享 問題原因

提要:   在 {初探Vue 與 Spring boot 的對話} 專案 ,前端 Vue 應用程式 串接 後端 API 伺服器 ,axios 呼叫 API 時出現以下,”無法取得回應內容 (No 'Access-Control-Allow-Origin' header is present on the requested resource):” 錯誤訊息,根據查找相關資料 ,出現以下原因。 瀏覽器開發工具 錯誤訊息 畫面 錯誤原因: “ Access to XMLHttpRequest at ” from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource” 瀏覽器為了安全考量,實施了同源政策。 當您的前端應用程式 (http://localhost:8080) 嘗試呼叫一個不同來源 (不同協議、不同域名或不同埠號) 的 API 伺服器 (http://localhost:8088) 時,瀏覽器會主動阻止這個請求,除非伺服器明確地允許這個跨來源的存取。 同源政策限制(Same-Origin Policy): 同源政策限制了程式碼和不同網域資源間的互動,同源是指兩份網頁具備相同協定、埠號(如果有指定)以及主機位置 範例: 表列哪些 URL 與 URL http://www.example.com/api/p1 屬於同源: URL                                                   | 結果   | 原因 --------------------------------------------------------------------- http://www.example.com/api/p2     |...

初探 Vue 與 Spring boot 的對話之Frontend (Vue-Frontend)

  Front-end Vue 使用 REST API 建立 Vite 專案 可參考 { Vue 3 初探}  文章 danny@Danny-Yu projects % npm create vite@latest Need to install the following packages: create-vite@8.2.0 Ok to proceed? (y) y > npx > "create-vite" │ ◇   Project name: │   vue-frontend │ ◇   Select a framework: │   Vue │ ◇   Select a variant: │   TypeScript │ ◇   Use rolldown-vite (Experimental)?: │   No │ ◇   Install with npm and start now? │   Yes │ ◇   Scaffolding project in /Users/danny/Desktop/projects/vue-frontend... │ ◇   Installing dependencies with npm... added 47 packages, and audited 48 packages in 27s 6 packages are looking for funding   run `npm fund` for details found 0 vulnerabilities │ ◇   Starting dev server... > vue-frontend@0.0.0 dev > vite   VITE v7.2.4   ready in 411 ms   ➜   Local:   http://localhost:5173/   ➜   Network: use --host to expose   ➜   press h + enter to show...

初探 Spring 中的循環依賴

原因: 當兩個或多個 bean 直接或間接地相互依賴時, 就會出現 Circular Dependency (循環依賴) 如: Bean A -> Bean B -> Bean A import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanA {          @Autowired     private BeanB beanB;     public String sayHi() {         return "Hi! 我是 Class A.";     } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class BeanB {          @Autowired     private BeanA beanA;     public String sayHi() {         return "Hi! 我是 Class B.";     } } 編譯時不會出現問題 danny@Danny-Yu demo % mvn clean install -Dmaven.test.skip=true                           ... ... [INFO] Installing /Users/danny/Desktop/projects/demo/target/dem...