Understanding the @Scope Annotation in Spring: How Bean Scopes Work

What is @Scope

This is a class-level annotation that allows the developer to define the scope of a Spring bean. By default, all Spring beans are singletons, but by using this annotation, you can modify that to the following scopes:

  • singleton: the default one, where Spring will create a single instance and share it throughout the application wherever the bean is needed.
  • Prototype: This scope creates a new instance whenever the bean is requested.
  • Request: Spring will create an instance of the bean that will live until the HTTP request is done.
  • session: Spring will create a bean specific for that session; for each session, it will be a new bean.

Why is it important:

  • Resource management: This annotation will give you the flexibility you need to manage the resources of your application and help you have more control over your application beans.
  • Concurrency: Sometimes, you can have a problem with concurrency using a bean that is a singleton, especially in cases where your bean handles some state. So to avoid concurrency problems, it is wise to change the bean scope to one that better suits your case.

How to use @Scope

In every bean that you have, you can combine the bean definition with the @Scope definition.

Defining a service with @Scope in singleton mode

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON) // This is the default, but shown here for clarity
public class SingletonService {
    private static final Logger logger = LoggerFactory.getLogger(SingletonService.class);

    public SingletonService() {
        logger.info("SingletonService instance created");
    }

    public void doWork() {
        logger.info("Doing work in singleton service");
    }
}

Let’s test it out. The code below will get the bean after the application starts, and call the doWork function so we can check the scope behavior:

@Component
public class SingletonTest {

    @Autowired
    private ApplicationContext context;

    @PostConstruct
    public void usePrototype() {
        SingletonService service1 = context.getBean(SingletonService.class);
        SingletonService service2 = context.getBean(SingletonService.class);
        service1.doWork();
        service2.doWork();
    }
}

We should see only one creation log because it’s a singleton, and the other two logs about the execution of the method doWork should look like this:

INFO 11252 --- [           main] o.spring.mastery.scope.SingletonService  : SingletonService instance created
INFO 11252 --- [           main] o.spring.mastery.scope.SingletonService  : Doing work in singleton service
INFO 11252 --- [           main] o.spring.mastery.scope.SingletonService  : Doing work in singleton service

Defining a bean with scope set to prototype

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeService {
    private static final Logger logger = LoggerFactory.getLogger(PrototypeService.class);

    public PrototypeService() {
        logger.info("PrototypeService instance created");
    }

    public void doWork() {
        logger.info("Doing work in prototype service");
    }
}

Let’s make a test to see how this bean behaves:

import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

@Component
public class PrototypeTest {

    @Autowired
    private ApplicationContext context;

    @PostConstruct
    public void usePrototype() {
        PrototypeService service1 = context.getBean(PrototypeService.class);
        PrototypeService service2 = context.getBean(PrototypeService.class);
        service1.doWork();
        service2.doWork();
    }
}

As we explained, the prototype scope will create a new instance whenever the bean is requested. In the code above, we request 2 times before calling the doWork method, so the log lines should be two creation logs followed by two doWork logs. Like this:

INFO 23872 --- [           main] o.spring.mastery.scope.PrototypeService  : PrototypeService instance created
INFO 23872 --- [           main] o.spring.mastery.scope.PrototypeService  : PrototypeService instance created
INFO 23872 --- [           main] o.spring.mastery.scope.PrototypeService  : Doing work in prototype service
INFO 23872 --- [           main] o.spring.mastery.scope.PrototypeService  : Doing work in prototype service

Using a proxy and a custom scope (request-scoped example)

In this example, we will see how the request scope works in practice. First, we create a bean with request scope:

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Service;
import org.springframework.web.context.WebApplicationContext;

@Service
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedService {

    public RequestScopedService() {
        System.out.println("RequestScopedService instance created");
    }

    public void handleRequest() {
        System.out.println("Handling request in request scoped service");
    }
}

Then we use this bin in a controller

@RestController
@RequestMapping("/api")
public class SampleController {

    ****@Autowired
    private RequestScopedService requestScopedService;

    @GetMapping("/test")
    public ResponseEntity<String> test() {
        requestScopedService.handleRequest();
        return ResponseEntity.ok("Request handled");
    }
}

If you start the application, you will see that there won’t be any logs saying RequestScopedService instance created. This bean will only be created in an HTTP request scope. So if we call the controller with:

curl 'http://localhost:8080/api/test' -Method Get

Now you will see the logs of the bean being created:

INFO 10704 --- [nio-8080-exec-2] o.s.mastery.scope.RequestScopedService   : RequestScopedService instance created
INFO 10704 --- [nio-8080-exec-2] o.s.mastery.scope.RequestScopedService   : Handling request in request scoped service

Conclusion

This is a powerful annotation that allows you to modify the default bean creation behavior from Spring. This can be useful to have in your tool belt, especially when debugging concurrency problems, believe me, it happens.

If you found this topic interesting, follow me for more insights into Spring annotations and other valuable tips!

Willian Moya (@WillianFMoya) / X (twitter.com)

Willian Ferreira Moya | LinkedIn


Posted

in

,

by