首页 > 其他分享 >Spring Bean 名称暗藏玄机,这样取名就不会被代理

Spring Bean 名称暗藏玄机,这样取名就不会被代理

时间:2023-10-29 13:05:41浏览次数:32  
标签:return Spring shouldSkip beanName bean 暗藏玄机 Bean 方法 beanClass


一些使用小细节就是在不断的源码探索中逐步发现的,今天就来和小伙伴们聊一下通过 beanName 的设置,可以让一个 bean 拒绝被代理!

1. 代码实践

假设我有如下一个切面:

@Aspect
@EnableAspectJAutoProxy
@Component
public class LogAspect {
    @Pointcut("execution(* org.javaboy.demo.service.*.*(..))")
    public void pc() {

    }

    @Before("pc()")
    public void before(JoinPoint jp) {
        String name = jp.getSignature().getName();
        System.out.println(name + " 方法开始执行了...");
    }
}

这个切面要拦截的方法是 org.javaboy.demo.service 包下的所有类的所有方法,现在,这个包下有一个 BookService 类,内容如下:

@Service("org.javaboy.demo.service.BookService.ORIGINAL")
public class BookService {

    public void hello() {
        System.out.println("hello bs");
    }
}

这个 BookService 的 beanName 我没有使用默认的 beanName,而是自己配置了一个 beanName,这个 beanName 的配置方式是 类名的完整路径+.ORIGINAL

当我们按照这样的规则给 bean 取名之后,那么即使当前 bean 已经包含在切点所定义的范围内,这个 bean 也不会被代理了。

这是 Spring5.1 开始的新玩法。

这种写法的原理是什么呢?

2. 原理分析

在 Spring 创建 Bean 的时候,小伙伴们都知道,bean 创建完成之后会去各种后置处理器(BeanPostProcessor)中走一圈,所以一般我们认为 BeanPostProcessor 是在 bean 实例创建完成之后执行。但是,BeanPostProcessor 中有一个特例 InstantiationAwareBeanPostProcessor,这个接口继承自 BeanPostProcessor,但是在 BeanPostProcessor 的基础之上,增加了额外的能力:

  1. 在 bean 实例化之前先做一些预处理,例如直接创建代理对象,代替后续的 bean 生成。
  2. 在 bean 实例化之后但是属性填充之前,可以自定义一些属性注入策略。

大致上就是这两方面的能力。

具体到代码上,就是在创建 bean 的 createBean 方法中:

@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
		throws BeanCreationException {
	//省略。。。
	try {
		// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
		Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
		if (bean != null) {
			return bean;
		}
	}
	catch (Throwable ex) {
		throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
				"BeanPostProcessor before instantiation of bean failed", ex);
	}
	try {
		Object beanInstance = doCreateBean(beanName, mbdToUse, args);
		if (logger.isTraceEnabled()) {
			logger.trace("Finished creating instance of bean '" + beanName + "'");
		}
		return beanInstance;
	}
    //省略。。。
}

小伙伴们看,这里的 resolveBeforeInstantiation 方法就是给 BeanPostProcessor 一个返回代理对象的机会,在这个方法中,最终就会触发到 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation 方法,我们来看看这里涉及到的跟 AOP 相关的 AbstractAutoProxyCreator#postProcessBeforeInstantiation 方法:

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
	Object cacheKey = getCacheKey(beanClass, beanName);
	if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
		if (this.advisedBeans.containsKey(cacheKey)) {
			return null;
		}
		if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return null;
		}
	}
	// Create proxy here if we have a custom TargetSource.
	// Suppresses unnecessary default instantiation of the target bean:
	// The TargetSource will handle target instances in a custom fashion.
	TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
	if (targetSource != null) {
		if (StringUtils.hasLength(beanName)) {
			this.targetSourcedBeans.add(beanName);
		}
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
		Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
		this.proxyTypes.put(cacheKey, proxy.getClass());
		return proxy;
	}
	return null;
}

这个方法实际上干了两件事:

  1. 检查当前 bean 是否需要代理,如果不需要代理,那么就存入到一个 map 集合 advisedBeans 中,key 是 bean 的名字,value 如果为 true 则表示这个 bean 是需要代理的,value 为 false,则表示这个 bean 是不需要代理的。
  2. 如果有我们有自定义的 TargetSource,则根据自定义的 TargetSource 去创建代理对象。

这里我要和大家说的是第一点。

在判断一个 bean 是否需要代理的时候,主要依据两个方法:

  1. isInfrastructureClass:这个方法主要是检查当前 bean 是否是 Advice/Advisor/Pointcut 等类型,或者这个类上是否有 @Aspect 注解,这个松哥在之前的文章中其实和大家介绍过了:听说 Spring Bean 的创建还有一条捷径?。
  2. shouldSkip:如果 isInfrastructureClass 方法返回 false,那么就要执行 shouldSkip 了,我们来仔细看下 shouldSkip 方法。
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
	// TODO: Consider optimization by caching the list of the aspect names
	List<Advisor> candidateAdvisors = findCandidateAdvisors();
	for (Advisor advisor : candidateAdvisors) {
		if (advisor instanceof AspectJPointcutAdvisor pointcutAdvisor &&
				pointcutAdvisor.getAspectName().equals(beanName)) {
			return true;
		}
	}
	return super.shouldSkip(beanClass, beanName);
}

这里首先找到系统中所有的切面,找到之后挨个遍历,遍历的时候判断如果当前要创建的 bean 刚好就是切面,那切面肯定是不需要代理的,直接返回 true。否则就会去调用父类的 shouldSkip 方法,我们再来瞅一眼父类的 shouldSkip 方法:

protected boolean shouldSkip(Class<?> beanClass, String beanName) {
	return AutoProxyUtils.isOriginalInstance(beanName, beanClass);
}
static boolean isOriginalInstance(String beanName, Class<?> beanClass) {
	if (!StringUtils.hasLength(beanName) || beanName.length() !=
			beanClass.getName().length() + AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX.length()) {
		return false;
	}
	return (beanName.startsWith(beanClass.getName()) &&
			beanName.endsWith(AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX));
}

父类 shouldSkip 方法主要是调用了一个静态工具方法 isOriginalInstance 来判断当前 bean 是否是一个不需要代理的 bean,这个具体的判断逻辑就是检查这个 beanName 是否按照 类名的完整路径+.ORIGINAL 的方式命名的,如果是则返回 true。

当 shouldSkip 方法返回 true 的时候,就会进入到 postProcessBeforeInstantiation 方法的 if 分支中,该分支将当前 beanName 存入到 advisedBeans 集合中,存储的 key 就是 beanName,value 则是 false,然后将方法 return。

当 bean 创建完成之后,再进入到 AbstractAutoProxyCreator#postProcessAfterInitialization 方法中处理的时候,就会发现这个 bean 已经存入到 advisedBeans 集合中,并且 value 是 false,这就意味着这个 bean 不需要代理,那么就针对该 bean 就不会进行 AOP 处理了,直接 return 即可。

好啦,一个小小细节,加深大家对 Spring AOP 的理解,感兴趣的小伙伴可以去试试哦~


标签:return,Spring,shouldSkip,beanName,bean,暗藏玄机,Bean,方法,beanClass
From: https://blog.51cto.com/u_9806927/8079877

相关文章

  • 收藏从未停止,练习从未开始——MYSQL游标存储过程IDEA快捷键配置SpringBoot入门到入土G
    “收藏从未停止,练习从未开始”,或许有那么一些好题好方法,在被你选中收藏后却遗忘在收藏夹里积起了灰?今天请务必打开你沉甸甸的收藏重新回顾,分享一下那些曾让你拍案叫绝的好东西吧!本人详解:(提醒:随笔)本人详解作者:王文峰,参加过2020年度博客之星,《Java王大师王天师》作者采购供应链共享......
  • 【SpringBoot】Docker部署
    docker部署是主流的部署方式,极大的方便了开发部署环境,保持了环境的统一,也是实现自动化部署的前提。1项目的目录结构package:点击打包,生成xxx-SNAPSHOT.jartarget目录:打包生成目录,生成的jar存放位置Dockerfile:跟项目根目录同级2创建Dockerfile#Docker镜像构......
  • springboot+vue2+element学生信息管理系统
    效果:  .vue<template><div><el-containerstyle="height:700px;border:1pxsolid#eee"><el-headerstyle="font-size:40px;background-color:rgb(238,241,246)">学生管理</el-header&......
  • springboot 断点上传、续传、秒传实现
    前言springboot断点上传、续传、秒传实现。保存方式提供本地上传(单机)和minio上传(可集群)本文主要是后端实现方案,数据库持久化采用jpa一、实现思路前端生成文件md5,根据md5检查文件块上传进度或秒传需要上传分片的文件上传分片文件分片合并后上传服务器二、数据库表对象说明:Abs......
  • Spring为什么建议构造器注入?看看和你所理解的一样吗?
    Spring框架鼓励使用构造器注入的主要原因是出于设计原则和最佳实践的考虑,这有助于提高代码的可维护性、可测试性和整体质量。以下是详细说明为什么Spring建议构造器注入以及相关实例代码:1.易于测试构造器注入使得对类的单元测试变得更加容易。通过将依赖项作为构造函数的参数传递,......
  • SpringBoot使用Redis分布式缓存
    Redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sortedset有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基......
  • springboot项目实现断点续传功能
    这篇文章主要介绍了springboot项目实现断点续传,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 java代码packagecom.ruoyi.web.upload.controller;importcom.ruoyi.web.upload.dto.FileChunkDTO;importcom.ruoyi.......
  • 整合spring security自定义认证
    一、认证1.自定义组件  (1)UserDetails自定义,实现用户登录方法;publicinterfaceUserDetailsServiceextendsorg.springframework.security.core.userdetails.UserDetailsService{/***根据用户名获取用户对象(获取不到直接抛异常)*/@OverrideUser......
  • springMVC controller控制器方法HttpServletRequest等参数的是谁传递进来的
    SpringMVC中两个重要的接口:请求方法参数的处理、响应返回值的处理,分别是HandlerMethodArgumentResolver和HandlerMethodReturnValueHandlerHandlerMethodArgumentResolver的实现类 ServletRequestMethodArgumentResolver 参数类型是实现或继承或是WebRequest、ServletRequest......
  • 一些研发工程师在Springboot注意点
    一些研发工程师在Springboot注意点1.正确设计代码目录结构虽然您有很大的自由度,但有一些基本规则值得遵循来设计您的源代码结构。避免使用默认包。确保所有内容(包括入口点)都在命名良好的包中,这样您就可以避免与组装和组件扫描相关的意外情况;将Application.java(应用程序的入口类)......