作用域代理是一种在使用 Spring 框架中控制 bean 作用域的机制。它通常与原型(Prototype)作用域一起使用。
在 Spring 中,bean 的作用域定义了 bean 的生命周期和可见范围。默认情况下,Spring 中的 bean 是单例(Singleton)作用域,意味着在整个应用程序中只有一个共享的实例。但有时候我们希望在每次注入时都获得一个新的实例,这就是原型作用域。
然而,当将一个原型作用域的 bean 注入到单例作用域的 bean 中时,由于单例 bean 的初始化只会发生一次,因此注入的原型 bean 将不能正常工作,每次获取的都是同一个实例。
这就引入了作用域代理。作用域代理机制通过在单例 bean 上创建一个代理对象,并将实际的依赖对象延迟到运行时才创建。当调用单例 bean 的方法时,代理对象会负责从原型容器中获取一个新的实例并返回。这样,尽管单例 bean 本身的初始化只发生一次,但每次使用时都可以获得一个新的原型实例,保证了原型作用域的有效性。
在 Spring 中,可以通过在 @Scope
注解中设置 proxyMode = ScopedProxyMode.TARGET_CLASS
,使用基于类的作用域代理。这表示生成一个动态代理对象,代理的是目标类而不仅仅是接口。还可以使用其他的代理模式,例如 ScopedProxyMode.INTERFACES
代表基于接口的代理模式。
总而言之,作用域代理是一种机制,允许将原型作用域的 bean 注入到单例作用域的 bean 中,并确保每次注入都获得一个新的原型实例。这在某些场景下非常有用,例如在 Web 应用程序中管理用户请求的会话信息时。
org.springframework
spring-context
5.3.23
ch.qos.logback
logback-classic
1.4.5
UserService 接口
public interface UserService {
void add();
}
UserServiceImpl 实现类
@Slf4j
@Service
/**
* 当一个单例的 bean 注入原型 bean 的时候,如果想让原型生效
* 那么可以设置 proxyModel 属性,这个属性就是代理作用域,
* 其原理是当初始化单例 bean 的时候,并不会立即完成注入,而是
* 将一个代理对象(暂时理解为替身)设置到单例中,
* 当调用注入对象的方法时,此时替身会自动从原型容器中创建
* 一个实例并返回,保证原型的有效
*/
@Scope(value = "prototype",
proxyMode = ScopedProxyMode.TARGET_CLASS
)
public class UserServiceImpl implements UserService {
@Override
public void add() {
log.info("添加用户");
}
}
这段代码展示了一个使用作用域代理的示例。
首先,通过
@Slf4j
注解引入了日志记录器。接下来,在
UserServiceImpl
类上使用@Service
注解将该类标记为 Spring 的服务(Service)组件。在
UserServiceImpl
类上添加了注释,解释了如何让原型作用域生效。通过在@Scope
注解中设置value = "prototype"
,表示将UserServiceImpl
类声明为原型(Prototype)作用域的 bean。同时,通过设置proxyMode = ScopedProxyMode.TARGET_CLASS
,将代理模式设置为基于类的代理。这样,在初始化单例 bean(用到原型 bean 的地方)时,会生成一个代理对象,代理的是目标类UserServiceImpl
,而不仅仅是接口UserService
。当调用代理对象的方法时,代理对象会负责从原型容器中获取一个新的实例并返回,确保了原型作用域的有效性。在
UserServiceImpl
类中,覆写了add()
方法,并在方法中打印了一条日志信息,表示执行了添加用户的操作。这样,当调用
UserServiceImpl
的add()
方法时,实际上会通过代理对象来调用原型 bean 的方法,从而获得一个新的原型实例并执行相应的操作。每次调用都会得到一个新的原型实例,保证了原型作用域的有效性。
/**
* @Date 2023-10-07
* @Author qiu
* @Scope 注解用于设置 bean 的作用域,等效于 xml 中的 scope 属性
* 当不指定 value 属性时,默认是单例,如果要使用原型
* 就必须指定为 prototype
* 注意:当一个单例的 bean 注入一个原型 bean 的时候,
* 原型会失效,因为在容器在初始化单例的 Bena 时,为了正确注入实例
* 会将需要注入的对象预先创建出来并注入到当前的单例 bean 中
* 因此只要单例的 bean 不销毁,Name 被注入的这个对象也一并存在
*/
@Scope("prototype")
@Controller
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
public void addUser() {
userService.add();
}
}
这段代码展示了一个使用作用域设置和注入的示例。
首先,在类上方添加了一些注释,记录了日期和作者信息。
接下来,使用
@Scope("prototype")
注解将UserController
类声明为原型(Prototype)作用域的 bean。这意味着每次从容器中获取UserController
的实例时,都会创建一个新的实例。在
UserController
类上使用@Controller
注解将其标记为 Spring 的控制器组件。通过构造函数注入方式,在
UserController
类中声明了一个userService
成员变量,并使用final
关键字确保它在初始化后不可修改。在
addUser()
方法中,调用userService.add()
方法,即调用UserService
接口的add()
方法。这样就实现了在UserController
中调用UserService
的功能。需要注意的是,当一个单例的 bean(比如
UserController
)注入一个原型 bean(比如UserService
)时,原型 bean 的作用域会失效。因为在容器初始化单例 bean 时,为了正确注入依赖,容器会预先创建需要注入的对象并保存到当前单例 bean 中。所以只要单例 bean 不销毁,注入的原型 bean 对象也会一直存在。综上所述,这段代码展示了如何使用
@Scope
注解来设置 bean 的作用域,以及在UserController
中注入UserService
的示例。
@Slf4j
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserController bean = context.getBean(UserController.class);
UserController bean2 = context.getBean(UserController.class);
log.info("控制器1:" + bean);
log.info("控制器2:" + bean2);
bean.addUser();
}
}
运行结果
实际上,当一个单例 bean 注入一个原型 bean 时,每次从单例 bean 中获取原型 bean 的引用时,都会得到一个新的实例。
在 Spring 容器中,默认情况下,单例 bean 在初始化时只会创建一次,并在整个应用程序的生命周期内重用。而原型 bean 在每次被请求时都会创建一个新的实例。
当单例 bean 注入原型 bean 时,Spring 会创建一个代理对象来管理原型 bean 的创建与销毁。这个代理对象在每次引用原型 bean 时,会从容器中获取一个新的原型 bean 实例并返回。
所以,使用作用域代理的方式可以确保每次从单例 bean 中获取原型 bean 都是一个新的实例,避免了原型作用域失效的问题。
对于原型 bean 注入其他 bean 的情况,原型作用域仍然有效,因为每次注入都会创建一个新的实例。
使用作用域代理的主要好处是可以保证原型作用域的有效性。
当一个原型 bean 被注入到一个单例 bean 中,由于单例 bean 只会创建一次,因此只有第一次获取原型 bean 时会创建一个新的实例。之后每次获取都会返回同一个实例,无法满足原型作用域的要求。
而使用作用域代理可以确保每次从单例 bean 中获取原型 bean 都是一个新的实例。代理对象会负责从原型容器中获取一个新的实例并返回,使得每次获取都是一个全新的实例。
另外,作用域代理还可以提高性能。在注入原型 bean 的场景下,每次获取原型 bean 都需要从容器中获取一个新的实例,这个过程可能比较耗时。而使用作用域代理后,每次获取原型 bean 实际上是访问代理对象,代理对象会缓存最新的实例并返回,避免了频繁地从容器中获取实例,提高了性能。
综上所述,使用作用域代理可以确保原型作用域的有效性,避免了单例 bean 注入原型 bean 后出现的实例重用问题,并且可以提高性能,是一个非常实用的功能。
案例完整地址:https://gitee.com/qiu-feng1/spring-framework.git