Tested on Spring Framework from 4.3.x.RELEASE till latest available.
Following request
scope bean definition does not work correctly or at least there is no description that such case is not valid in documentation (same applies to other custom
scopes):
@Configuration
public class BeansConfiguration {
public static String NON_TRANSACTIONAL_BEAN = "nonTransactionalBean";
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public BeanInterface createBean() {
var key = KeyThreadHolder.getKey();
if (NON_TRANSACTIONAL_BEAN.equals(key)) {
return new NonTransactionalBean();
}
return new TransactionalBean();
}
}
public interface BeanInterface {
boolean method();
}
public class NonTransactionalBean implements BeanInterface {
@Override
public boolean method() {
return false;
}
}
@Transactional(propagation = Propagation.REQUIRED)
public class TransactionalBean implements BeanInterface {
@Override
@Transactional(readOnly = true)
public boolean method() {
return TransactionSynchronizationManager.isActualTransactionActive();
}
}
public final class KeyThreadHolder {
private static final ThreadLocal<String> KEY = new ThreadLocal<>();
public static void setKey(String key) {
KEY.set(key);
}
public static String getKey() {
return KEY.get();
}
}
Creation of the first instance of the bean will determine whether all instances will be advised or not. (In this example whether transaction will be created or not).
I would expect all of the following tests to pass, although the first one fails:
@SpringBootTest
@AutoConfigureMockMvc
public class ScopedBeanControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@DirtiesContext
public void transactionalShouldNotBeIgnored() throws Exception {
mockMvc.perform(get("/nonTransactionalBean"))
.andExpect(status().isOk())
.andExpect(content().string("false"));
mockMvc.perform(get("/transactionalBean"))
.andExpect(status().isOk())
.andExpect(content().string("true"));
}
@Test
@DirtiesContext
public void transactionalIsNotIgnored() throws Exception {
mockMvc.perform(get("/transactionalBean"))
.andExpect(status().isOk())
.andExpect(content().string("true"));
mockMvc.perform(get("/nonTransactionalBean"))
.andExpect(status().isOk())
.andExpect(content().string("false"));
}
}
@RestController
class ScopedBeanController {
@Autowired
private BeanInterface beanInterface;
@GetMapping(path = "/{key}", produces = "application/json")
public boolean scopedBean(@PathVariable String key) {
KeyThreadHolder.setKey(key);
return beanInterface.method();
}
}
Attaching Spring Boot project with above, working example. scopes.zip
Comment From: snicoll
The first type the bean is created, its target type and aspects are cached. A scope is meant to return several instances of the same bean type, you're not supposed to swap to different features.
The first paragraph of the bean scopes section states:
When you create a bean definition, you create a recipe for creating actual instances of the class defined by that bean definition. The idea that a bean definition is a recipe is important, because it means that, as with a class, you can create many object instances from a single recipe.