johnlinp
johnlinp

Reputation: 933

What's the difference between @ComponentScan and @Bean in a context configuration?

There are at least 2 approaches to put Spring beans into a context configutation:

  1. Declare a method with @Bean inside the configuration class.
  2. Put @ComponentScan on the configuration class.

I was expecting that there are no difference between the 2 approaches in terms of the resulting Spring beans.

However, I found an example to demonstrate the difference:

// UserInfoService.java

public interface UserInfoService
{
    @PreAuthorize("isAuthenticated()")
    String getUserInfo ();
}
// UserInfoServiceTest.java

@RunWith(SpringJUnit4ClassRunner.class)
@ContextHierarchy({
        @ContextConfiguration(classes = TestSecurityContext.class),
        @ContextConfiguration(classes = UserInfoServiceTest.Config.class)
})
public class UserInfoServiceTest
{
    @Configuration
    public static class Config
    {
        @Bean
        public UserInfoService userInfoService ()
        {
            return new UserInfoServiceImpl();
        }
    }

    @Autowired
    private UserInfoService userInfoService;

    @Test
    public void testGetUserInfoWithoutUser ()
    {
        assertThatThrownBy(() -> userInfoService.getUserInfo())
                .isInstanceOf(AuthenticationCredentialsNotFoundException.class);
    }

    @Test
    @WithMockUser
    public void testGetUserInfoWithUser ()
    {
        String userInfo = userInfoService.getUserInfo();
        assertThat(userInfo).isEqualTo("info about user");
    }

The above code is to test the security annotation in the service UserInfoService. However, it will fail on testGetUserInfoWithoutUser(). The reason is that the bean userInfoService didn't get proxied by Spring Security. Therefore, the call userInfoService.getUserInfo() didn't get blocked by the annotation @PreAuthorize("isAuthenticated()").

However, if I replace the @Bean annotation with @ComponentScan, everything will start to work. That is, the bean userInfoService will get proxied and the call userInfoService.getUserInfo() will be blocked by the @PreAuthorize annotation.

Why are the approaches between the @Bean and @ComponentScan different? Did I miss something?

P.S. The full example is here and the above mentioned fix is here.

Upvotes: 3

Views: 898

Answers (1)

Ken Chan
Ken Chan

Reputation: 90527

It is because @ContextHierarchy will create multiple spring contexts with parent children hierarchy. In your case, TestSecurityContext defines the bean configuration for the parent context while UserInfoServiceTest.Config defines for the children context.

If there is no @ComponentScan on the UserInfoServiceTest.Config , the security related beans are defined in the parent context which are invisible to the UserInfoService bean in the children context , and hence it does not get proxied by Spring Security.

On the other hand , if you define @ComponentScan on the UserInfoServiceTest.Config , it will also scan all the @Configuration beans from the package that containing UserInfoService (and all of its sub package) . Because TestSecurityContext is also inside this package and hence it get scanned and the security related beans are also configured for the children context. UserInfoService in children context will then get proxied by Spring Security.(Note : In this case ,both the parent and children context has their own set of security related beans)

By the way , if you just need a single context , you can simply use @ContextConfiguration :

@ContextConfiguration(classes= {TestSecurityContext.class,UserInfoServiceTest.Config.class})
public class UserInfoServiceTest {

    public static class Config {
        @Bean
        public UserInfoService userInfoService() {
            return new UserInfoServiceImpl();
        }
    }
}

Upvotes: 3

Related Questions