@Scope 注解失效了?咋回事

news2024/12/24 18:50:15

scope 属性,相信大家都知道,一共有六种:

取值含义生效条件
singleton表示这个 Bean 是单例的,在 Spring 容器中,只会存在一个实例。
prototype多例模式,每次从 Spring 容器中获取 Bean 的时候,才会创建 Bean 的实例出来。
request当有一个新的请求到达的时候,会创建一个 Bean 的实例处理。web 环境下生效
session当有一个新的会话的时候,会创建一个 Bean 的实例出来。web 环境下生效
application这个表示在项目的整个生命周期中,只有一个 Bean。web 环境下生效
gloablsession有点类似于 application,但是这个是在 portlet 环境下使用的。web 环境下生效

这个用法也很简单,通过配置就可以设置一个 Bean 是否为单例模式。

1. 问题呈现

今天我要说的不是基础用法,是另外一个问题,假设我现在有如下两个 Bean:

@Service
public class UserService {
    @Autowired
    UserDao userDao;
}
@Repository
public class UserDao {
}

在 UserService 中注入 UserDao,由于两者都没有声明 scope,所以默认都是单例的。

现在,如果我给 UserDao 设置 Scope,如下:

@Repository
@Scope(value = "prototype")
public class UserDao {
}

这个 prototype 表示如果我们从 Spring 容器中多次获取 UserDao 的实例,拿到的是同一个实例。

但是!!!

我现在是在 UserService 里边注入 UserDao 的,UserService 是单例的,也就是 UserService 只初始化了一次,按理说 UserService 也只跟 Spring 容器要了一次 UserDao,这就导致我们最终从 UserService 中拿到的 UserDao 始终是同一个。

测试方式如下:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
System.out.println(us.userDao == us2.userDao);

最终打印结果为 true。

其实,这个也没啥问题,因为你确实只跟 Spring 容器只要了一次 UserDao。但是现在如果我的需求就是 UserService 是单例,UserDao 每次都获取不同的实例呢?阁下该如何应对?

2. 解决方案

Spring 已经考虑到这个问题了,解决方案就是通过代理来实现。

在我们使用 @Scope 注解的时候,该注解还有另外一个属性 proxyMode,这个属性的取值有四种,如下:

public enum ScopedProxyMode {
	DEFAULT,
	NO,
	INTERFACES,
	TARGET_CLASS
}
  • DEFAULT:这个是默认值,默认就是 NO,即不使用代理。
  • NO:不使用代理。
  • INTERFACES:使用 JDK 动态代理,要求当前 Bean 得有接口。
  • TARGET_CLASS:使用 CGLIB 动态代理。

可以通过设置 proxyMode 属性来为 Bean 产生动态代理对象,进而实现 Bean 的多例。

现在我修改 UserDao 上的注解,如下:

@Repository
@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserDao {
}

此时,再去执行测试:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
System.out.println(us==us2);
System.out.println("us.userDao = " + us.userDao);
System.out.println("us2.userDao = " + us2.userDao);
System.out.println("us.userDao.getClass() = " + us.userDao.getClass());

最终打印结果如下:

可以看到,UserService 是单例,userDao 确实是不同实例了,并且 userDao 是一个 CGLIB 动态代理对象。

那么,如果是 XML 配置该怎么配置呢?

<bean class="org.javaboy.demo.p2.UserDao" id="userDao" scope="prototype">
    <aop:scoped-proxy/>
</bean>
<bean class="org.javaboy.demo.p2.UserService">
    <property name="userDao" ref="userDao"/>
</bean>

这个跟普通的 AOP 配置方式不一样,不过也很好理解,对照上面的注解配置来理解即可。

3. 源码分析

那么这一切是怎么实现的呢?

Spring 中提供了专门的工具方法 AnnotationConfigUtils#applyScopedProxyMode 来处理此事:

static BeanDefinitionHolder applyScopedProxyMode(
		ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) {
	ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode();
	if (scopedProxyMode.equals(ScopedProxyMode.NO)) {
		return definition;
	}
	boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS);
	return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass);
}

从这里我们可以看到,如果代理模式是 NO/Default 的话,那么直接返回原本的 definition,否则就要调用 ScopedProxyCreator.createScopedProxy 方法去生成代理对象了,这里还涉及到一个 proxyTargetClass 参数,这个参数是用来判断是 JDK 动态代理还是 CGLIB 动态代理的,如果设置了 proxyMode = ScopedProxyMode.TARGET_CLASS 那么 proxyTargetClass 变量就为 true,表示 CGLIB 动态代理,否则就是 JDK 动态代理。

来继续看 ScopedProxyCreator.createScopedProxy 方法,该方法内部调用到了 ScopedProxyUtils#createScopedProxy 方法:

public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition,
		BeanDefinitionRegistry registry, boolean proxyTargetClass) {
	String originalBeanName = definition.getBeanName();
	BeanDefinition targetDefinition = definition.getBeanDefinition();
	String targetBeanName = getTargetBeanName(originalBeanName);
	// Create a scoped proxy definition for the original bean name,
	// "hiding" the target bean in an internal target definition.
	RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class);
	proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName));
	proxyDefinition.setOriginatingBeanDefinition(targetDefinition);
	proxyDefinition.setSource(definition.getSource());
	proxyDefinition.setRole(targetDefinition.getRole());
	proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName);
	if (proxyTargetClass) {
		targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
		// ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here.
	}
	else {
		proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
	}
	// Copy autowire settings from original bean definition.
	proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
	proxyDefinition.setPrimary(targetDefinition.isPrimary());
	if (targetDefinition instanceof AbstractBeanDefinition abd) {
		proxyDefinition.copyQualifiersFrom(abd);
	}
	// The target bean should be ignored in favor of the scoped proxy.
	targetDefinition.setAutowireCandidate(false);
	targetDefinition.setPrimary(false);
	// Register the target bean as separate bean in the factory.
	registry.registerBeanDefinition(targetBeanName, targetDefinition);
	// Return the scoped proxy definition as primary bean definition
	// (potentially an inner bean).
	return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases());
}

这个里边的代码其实没啥好解释的,就是创建了一个新的 RootBeanDefinition 对象,变量名就是 proxyDefinition,从这里也能看出来这就是用来创建代理对象的,然后把之前旧的 BeanDefinition 对象的各个属性值都拷贝进去,最后把新的代理的 proxyDefinition 返回。

这里有一个值得关注的点就是创建 proxyDefinition 的时候,构造方法传入的参数是 ScopedProxyFactoryBean,意思就是这个 BeanDefinition 将来要产生的对象是 ScopedProxyFactoryBean 的对象,那我们继续来看 ScopedProxyFactoryBean,从名字上可以看出来这是一个 FactoryBean:

public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {
	private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
	@Nullable
	private String targetBeanName;
	@Nullable
	private Object proxy;
	public ScopedProxyFactoryBean() {
		setProxyTargetClass(true);
	}
	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		this.scopedTargetSource.setBeanFactory(beanFactory);
		ProxyFactory pf = new ProxyFactory();
		pf.copyFrom(this);
		pf.setTargetSource(this.scopedTargetSource);
		Class<?> beanType = beanFactory.getType(this.targetBeanName);
		if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) {
			pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader()));
		}
		ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName());
		pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));
		pf.addInterface(AopInfrastructureBean.class);
		this.proxy = pf.getProxy(cbf.getBeanClassLoader());
	}
	@Override
	public Object getObject() {
		return this.proxy;
	}
	@Override
	public Class<?> getObjectType() {
		if (this.proxy != null) {
			return this.proxy.getClass();
		}
		return this.scopedTargetSource.getTargetClass();
	}
	@Override
	public boolean isSingleton() {
		return true;
	}
}

这里的 getObject 方法返回的就是 proxy 对象,而 proxy 对象是在 setBeanFactory 方法中初始化的(setBeanFactory 方法是在 Bean 初始化之后,属性填充完毕之后触发调用的)。

setBeanFactory 方法中就是去创建代理对象,设置的 targetSource 就是 scopedTargetSource,这个里边封装了被代理的对象,scopedTargetSource 是一个 SimpleBeanTargetSource 类型的 Bean,SimpleBeanTargetSource 的特点就是每次获取代理对象的时候,都会重新去调用 getTarget 方法,而在 SimpleBeanTargetSource 的 getTarget 方法中就是根据原始的 Bean 名称去 Spring 容器中查找 Bean 并返回,也就是说,在这里代理对象中,被代理的对象实际上就是原始的 Bean,对应上文案例来说,被代理的对象就是 userDao。

另外一个需要关注的点就是添加的拦截器 DelegatingIntroductionInterceptor 了,这是为代理对象增强的内容(setBeanFactory 方法中其他内容都是常规的 AOP 代码,我就不多说了,不熟悉的小伙伴可以看看松哥最近录制的 Spring 源码视频哦Spring源码应该怎么学?)。

DelegatingIntroductionInterceptor 拦截器传入了 scopedObject 作为参数,这个参数实际上就表示了被代理的对象,也就是被代理的对象是一个 ScopedObject。

public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport
		implements IntroductionInterceptor {
	@Nullable
	private Object delegate;
	public DelegatingIntroductionInterceptor(Object delegate) {
		init(delegate);
	}
	protected DelegatingIntroductionInterceptor() {
		init(this);
	}
	private void init(Object delegate) {
		this.delegate = delegate;
		implementInterfacesOnObject(delegate);
		suppressInterface(IntroductionInterceptor.class);
		suppressInterface(DynamicIntroductionAdvice.class);
	}
	@Override
	@Nullable
	public Object invoke(MethodInvocation mi) throws Throwable {
		if (isMethodOnIntroducedInterface(mi)) {
			Object retVal = AopUtils.invokeJoinpointUsingReflection(this.delegate, mi.getMethod(), mi.getArguments());
			if (retVal == this.delegate && mi instanceof ProxyMethodInvocation pmi) {
				Object proxy = pmi.getProxy();
				if (mi.getMethod().getReturnType().isInstance(proxy)) {
					retVal = proxy;
				}
			}
			return retVal;
		}
		return doProceed(mi);
	}
	@Nullable
	protected Object doProceed(MethodInvocation mi) throws Throwable {
		return mi.proceed();
	}
}

DelegatingIntroductionInterceptor 实现了 IntroductionInterceptor 接口,这就是典型的引介增强,这个松哥之前也写过文章专门跟大家讲过:Spring 中一个少见的引介增强 IntroductionAdvisor,看过之前的文章这里的内容应该都能懂。由于是引介增强,所以最终生成的代理对象,既是 UserDao 的实例,也是 ScopedObject 的实例。

4. 小结

经过上面的分析,我们可以得出如下几个结论:

  1. 从 UserService 中多次获取到的 UserDao,其实也是 ScopedObject 对象。
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService us1 = ctx.getBean(UserService.class);
UserService us2 = ctx.getBean(UserService.class);
UserDao userDao1 = us1.getUserDao();
UserDao userDao2 = us2.getUserDao();
ScopedObject scopedObject1 = (ScopedObject) userDao1;
ScopedObject scopedObject2 = (ScopedObject) userDao2;
System.out.println("userDao1 = " + userDao1);
System.out.println("userDao2 = " + userDao2);
System.out.println("scopedObject1 = " + scopedObject1);
System.out.println("scopedObject2 = " + scopedObject2);

上面这段代码不会报错,这就是引介增强。

  1. 生成的代理对象本身其实是同一个,因为 UserService 是单例的,毕竟只注入一次 UserDao,但是代理对象中被代理的 Bean 则是会变化的。

表现出来的现象就是第一点中的四个对象,如果去比较其内存地址,userDao1、userDao2、scopedObject1 以及 scopedObject2 是同一个内存地址,因为是同一个代理对象。

但是被代理的对象则是不同的。DEBUG 之后大家可以看到,前面四个表示代理对象的地址都是同一个,后面被代理的 UserDao 则是不同的对象。

出现这个现象的原因,就是在 ScopedProxyFactoryBean 的 setBeanFactory 方法中,我们设置的 TargetSource 是一个 SimpleBeanTargetSource,这个 TargetSource 的特点就是每次代理的时候,都会去 Spring 容器中查找 Bean,而由于 UserDao 在 Spring 容器中是多例的,因此 Spring 每次返回的 UserDao 就不是同一个,就实现了 UserDao 的多例:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
	@Override
	public Object getTarget() throws Exception {
		return getBeanFactory().getBean(getTargetBeanName());
	}

}

对于第二点的内容,如果小伙伴们还不理解,可以翻看松哥之前的文章:AOP 中被代理的对象是单例的吗?。

好啦,现在小伙伴们搞明白怎么回事了吧~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1084516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何解决MidJourney错过付费后被暂停

问题 假定你已经成功订阅购买了 MidJourney 一段时间&#xff0c;下个月扣费周期到了。 如果你卡里余额不足&#xff0c;卡被封或失效了&#xff0c;或者你想着最近没啥用得上 MidJourney 的地方先省着不续费&#xff0c;等要用的时候就用不了。 如果想要去官网的续费页&…

软文撰写技巧:中小企业品牌形象塑造新方法

在激烈的市场竞争中&#xff0c;企业仅靠产品质量和服务态度很难站稳脚跟&#xff0c;在互联网时代下&#xff0c;品牌形象才是中小企业的取胜法宝&#xff0c;良好的品牌形象能够赢得消费者的信赖&#xff0c;还能让企业占据市场资源优势&#xff0c;软文推广就是帮助企业塑造…

机器人控制算法——两轮差速驱动运动模型

1.Introduction 本文主要介绍针对于两轮差速模型的逆运动学数学推导。因为在机器人控制领域&#xff0c;决策规划控制层给执行器输出的控制指令v(车辆前进速度)和w(角速度)&#xff0c;因此&#xff0c;我们比较关心&#xff0c;当底层两个驱动电机接收到此信息&#xff0c;如何…

企业可以申请ov泛域名SSL证书吗

OV泛域名SSL证书是企事业单位和其他组织合法申请的数字证书&#xff0c;具有组织实名认证和域名解析授权的双重功能。与DV泛域名SSL证书相比&#xff0c;它的申请流程更加复杂&#xff0c;需要提交更多的资质和证明文件&#xff0c;当然&#xff0c;OV泛域名证书的安全等级也更…

如何提取视频文件中的元数据

最近在用gyroflow做视频稳定&#xff0c;但需要校准镜头&#xff0c;而校准需要知道拍摄时的一些设置参数&#xff0c;而用一些普通的工具获取到的视频元数据不全&#xff0c;所以就希望能有什么工具能获取到所有的元数据。找来找去发现Exiftool满足我的需求。 ExifTool 这是个…

机器人控制算法——移动机器人横向控制最优控制LQR算法

1.Introduction LQR (外文名linear quadratic regulator)即线性二次型调节器,LQR可得到状态线性反馈的最优控制规律,易于构成闭环最优控制。LQR最优控制利用廉价成本可以使原系统达到较好的性能指标(事实也可以对不稳定的系统进行整定) ,而且方法简单便于实现 ,同时利用 Ma…

vue3 封装自定义指令,监听元素宽高的变化

最近做一个项目&#xff0c;涉及到echart图&#xff0c;要去根据父元素去自适应宽高&#xff0c;所以需要监听到元素的宽高变化、 因为是监听某一元素的宽高变化&#xff0c;所以这里用的是ResizeObserver. ResizeObserver是可以监听到DOM元素&#xff0c;宽高的变化&#xf…

HarmonyOS/OpenHarmony原生应用开发-华为Serverless服务支持情况(三)

文档中的TS作者认为就是ArkTS之意。 一、云函数&#xff0c;从开发文档上已经说明&#xff0c;是已经支持HarmonyOS/OpenHarmony(Stage模型-API9)&#xff0c;但是在开发语言上&#xff0c;没有ArkTS&#xff0c;是否支持&#xff0c;正在实践测试中。 文档地址&#xff1a; …

【PCIE732】基于Kintex UltraScale系列FPGA的2路40G光纤通道适配器(5GByte/s带宽)

PCIE732是一款基于PCIE总线架构的高性能数据传输卡&#xff0c;板卡具有1个PCIe x8主机接口、2个QSFP 40G光纤接口&#xff0c;可以实现2路QSFP 40G光纤的数据实时采集、传输。板卡采用Xilinx的高性能Kintex UltraScale系列FPGA作为实时处理器&#xff0c;板载2组独立的72位DDR…

计算机网络面试常问问题--保研及考研复试

前言&#xff1a; Hello大家好&#xff0c;我是Dream。今年保研上岸山东大学人工智能专业 &#xff08;经验贴&#xff09;&#xff0c;现在将我自己的专业课备考知识点整理出来&#xff0c;分享给大家&#xff0c;希望可以帮助到大家&#xff01;这是重点知识总结&#xff0c;…

idea插件开发javax.net.ssl.SSLException: No PSK available. Unable to resume.

idea插件开发,编译出错 javax.net.ssl.SSLException: No PSK available. Unable to resume.at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:129)at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117)at java.base/sun.security.ssl.…

【网络基础】IP 子网划分(VLSM)

目录 一、 为什么要划分子网 二、如何划分子网 1、划分两个子网 2、划分多个子网 一、 为什么要划分子网 假设有一个B类IP地址172.16.0.0&#xff0c;B类IP的默认子网掩码是 255.255.0.0&#xff0c;那么该网段内IP的变化范围为 172.16.0.0 ~ 172.16.255.255&#xff0c;即…

202、RabbitMQ 之 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型---fanout类型就是广播类型

目录 ★ 使用 fanout 类型的Exchange 实现 Pub-Sub 消息模型代码演示&#xff1a;生产者&#xff1a;producer消费者&#xff1a;Consumer01消费者&#xff1a;Consumer02测试结果 完整代码ConnectionUtilPublisherConsumer01Consumer02pom.xml ★ 使用 fanout 类型的Exchange …

干货|小白也能自制电子相册赶紧码住~

你是否想拥有一个独一无二的电子相册&#xff0c;却又苦于不知道如何下手&#xff1f;今天教你一个简单的方法&#xff0c;即使你是小白&#xff0c;也能轻松自制电子相册&#xff01; 一、选择合适的工具 首先&#xff0c;你需要选择一个合适的工具来制作电子相册。有很多工具…

四化智造MES(WEB)与金蝶云星空对接集成原材料/标准件采购查询(待采购)连通采购订单新增(原材料采购-采购订单(变更)-TEST)

四化智造MES&#xff08;WEB&#xff09;与金蝶云星空对接集成原材料/标准件采购查询&#xff08;待采购&#xff09;连通采购订单新增(原材料采购-采购订单(变更)-TEST) 对接系统四化智造MES&#xff08;WEB&#xff09; MES建立统一平台上通过物料防错防错、流程防错、生产统…

Python接口自动化测试之【测试函数、测试类/测试方法的封装】

前言 在pythonpytest 接口自动化系列中&#xff0c;我之前的文章基本都没有将代码进行封装&#xff0c;但实际编写自动化测试脚本中&#xff0c;我们都需要将测试代码进行封装&#xff0c;才能被测试框架识别执行。 例如单个接口的请求代码如下&#xff1a; import requests…

echarts 中国地图效果,并附上小旗子

地图的基础部分 使用echarts开发中国地图&#xff0c;并修改地图默认颜色&#xff0c;以及hover效果以及背景色 可以放大缩小 以此文章记录 首先安装echarts npm install echarts 并引入 import * as echarts from echarts 然后去下载中国地图的 json数据 import * as echarts…

免费在线真好用的思维脑图

大家好这里是tony4geek 。 今天给大家介绍一个工具。思维脑图生成器。最近写文章需要用到思维脑图&#xff0c;如果手上没有xmind 这种类工具是挺麻烦的。下载xmind 还得破解注册很费时间。 看看有没有在线生成的&#xff0c;找了好久没有找到合适的&#xff0c;最后在国外一…

鱼哥赠书活动第②期:《AWD特训营:技术解析、赛题实战与竞赛技巧》《ATTCK视角下的红蓝对抗实战指南》《智能汽车网络安全权威指南》上下册

鱼哥赠书活动第①期&#xff1a; 《AWD特训营&#xff1a;技术解析、赛题实战与竞赛技巧》1.1介绍&#xff1a; 《ATT&CK视角下的红蓝对抗实战指南》1.1介绍&#xff1a; 《Kali Linux高级渗透测试》1.1介绍&#xff1a; 《智能汽车网络安全权威指南》上册1.1介绍&#xff…

外汇天眼:Bitcore与Amtop Markets Ltd──诓称数据分析操盘稳赚不赔,指控账户违法逼缴校验金

近几年网络投资风气盛行&#xff0c;但市面上充斥许多诈骗平台&#xff0c;透过保证获利、稳赚不赔等话术诱导民众投资&#xff0c;如今已成为社会不可忽视的严重威胁。 日前&#xff0c;外汇天眼就收到一位受害者爆料&#xff0c;分享她遭到Bitcore假交友诈骗的详细经过。 一开…