从零开始 Spring Boot 46:@Lookup
在前文中,我介绍了 Spring Bean 的作用域(Scope),且讨论了将一个短生命周期的 bean (比如request
作用域的 bean)注入到长生命周期的 bean (比如singleton
作用域的 bean)时所面临的问题,此类问题都需要我们对短生命周期的 bean 通过代理注入来解决。
实际上,即使都是长生命周期的bean,比如singleton
作用域和prototype
作用域的 bean,注入也存在一些问题。
注入问题
这里用一个示例说明将 prototype
作用域的 bean 注入 singleton
作用域的 bean 会出现什么问题:
@Value
public class Book {
String name;
String author;
String isbn;
}
public class BookStore {
@Autowired
private Book book;
public Book getBook(){
return book;
}
}
@Configuration
public class WebConfig {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean
public Book book() {
return new Book("哈利波特与魔法石", "JK罗琳", "9787020033430");
}
@Bean
public BookStore bookStore() {
return new BookStore();
}
}
在这个例子中,BookStore
bean 的作用域是单例,Book
的作用域是原型。这是我们故意为之,因为我们想通过getBook
方法从书店中获取图书时每次都获取到一本新书。
但实际测试就会发现结果并不是我们预期的那样:
@SpringJUnitConfig(classes = {
WebConfig.class})
public class BookStoreTests {
@Test
void testBookInject(@Autowired BookStore bookStore) {
var book1 = bookStore.getBook();
var book2 = bookStore.getBook();
Assertions.assertSame(book1, book2);
}
}
两次调用获取到的是同一个Book
对象。
这是因为虽然Book
bean 的作用域是原型,但将Book
注入到BookStore
这个单例 bean 中的行为仅会发生一次——在BookStore
bean 被创建后。之后每次调用getBook
获取Book
对象都是直接获取BookStore
中的book
依赖,而不会再触发注入或者从ApplicationContext
中获取 bean。
当然,解决的方式也很容易,只需要改为从ApplicationContext
中获取 bean 即可:
public class BookStore2 {
@Autowired
private ApplicationContext applicationContext;
public Book getBook() {
return applicationContext.getBean(Book.class);
}
}
现在每次获取到的都是新的Book
对象:
@TestConfiguration
public class BookConfig {
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Bean
public Book book() {
return new Book("哈利波特与魔法石", "JK罗琳", "9787020033430");
}
}
@SpringJUnitConfig
public class BookStore2Tests {
@Configuration
@Import(BookConfig.class)
static class Config {
@Bean
public BookStore2 bookStore2() {
return new BookStore2();
}
}
@Test
void testBookInject(@Autowired BookStore2 bookStore2) {
var book1 = bookStore2.getBook();
var book2 = bookStore2.getBook();
Assertions.assertNotSame(book1, book2);
}
}
就像我们之前提到的,虽然这样可以解决问题,但并不建议直接使用ApplicationContext
,这样会导致我们的代码与 Spring 框架“强耦合”。
为了方便后续的测试用例编写,这里将
Book
bean 的相关配置拆分出来,并用@Import
导入到当前测试用例中,更多的 Spring 测试相关内容,可以阅读我的这篇文章。
为此,Spring 提供了一个@Lookup
注解来解决上述问题。
@Lookup
直接看示例:
@Component
public class BookStore3 {
@Lookup
public Book getBook() {
return null;
}
}
用@Lookup
标记的 bean 方法,在调用时会被代理,实际上 Spring 会通过ApplicationContext.getBean(Book.class)
获取一个 bean 并返回。