Spring源码解析(29)之AOP动态代理对象创建过程分析

news2024/11/22 8:48:45

一、前言

        在上一节中我们已经介绍了在createBean过程中去执行AspectJAutoProxyCreator的after方法,然后去获取当前bean适配的advisor,如果还不熟悉的可以去看下之前的博客,接下来我们分析Spring AOP是如何创建代理对象的,在此之前如果对于JDK动态代理以及cglib源码不熟悉的也先去看一下之前对于这两个代理的源码分析博客。Spring源码解析(24)之JDK动态代理与cglib动态代理源码解析_spring 选择 jdk 代理 cglib 代理源码-CSDN博客

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		// 如果已经处理过,直接返回
		if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
			return bean;
		}
		// 这里advisedBeans缓存了已经进行了代理的bean,如果缓存中存在,则可以直接返回
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		// 这里isInfrastructureClass()用于判断当前bean是否为Spring系统自带的bean,自带的bean是
		// 不用进行代理的;shouldSkip()则用于判断当前bean是否应该被略过
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			// 对当前bean进行缓存
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}

		// Create proxy if we have advice.
		// 获取当前bean的Advices和Advisors
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		// 对当前bean的代理状态进行缓存
		if (specificInterceptors != DO_NOT_PROXY) {
			// 对当前bean的代理状态进行缓存
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			// 根据获取到的Advices和Advisors为当前bean生成代理对象
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			// 缓存生成的代理bean的类型,并且返回生成的代理bean
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

二、源码分析 

        接下来我们继续分析createProxy方法,来看下Spring AOP是如何创建代理对象的。

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		// 给bean定义设置暴露属性
		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}

		// 创建代理工厂
		ProxyFactory proxyFactory = new ProxyFactory();
		// 获取当前类中相关属性
 		proxyFactory.copyFrom(this);
		// 决定对于给定的bean是否应该使用targetClass而不是他的接口代理,检查proxyTargetClass设置以及preserverTargetClass属性
		if (!proxyFactory.isProxyTargetClass()) {
			// 判断是 使用jdk动态代理 还是cglib代理
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				// 添加代理接口
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

		// 构建增强器
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		proxyFactory.addAdvisors(advisors);
		// 设置到要代理的类
		proxyFactory.setTargetSource(targetSource);
		// 定制代理
		customizeProxyFactory(proxyFactory);

		// 控制代理工程被配置之后,是否还允许修改通知,默认值是false
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}
		// 真正创建代理对象
		return proxyFactory.getProxy(getProxyClassLoader());
	}

        设置一系列属性,然后判断是否有proxyTargetClass来决定是使用cglib代理还是JDK代理。 

	public Object getProxy(@Nullable ClassLoader classLoader) {
		// createAopProxy() 用来创建我们的代理工厂
		return createAopProxy().getProxy(classLoader);
	}

        首先会在createAopProxy中判断是使用cglib代理还是jdk动态代理。

	protected final synchronized AopProxy createAopProxy() {
		if (!this.active) {
			// 监听调用AdvisedSupportListener实现类的activated方法
			activate();
		}
		// 通过AopProxyFactory获得AopProxy,这个AopProxyFactory是在初始化函数中定义的,使用的是DefaultAopProxyFactory
		return getAopProxyFactory().createAopProxy(this);
	}


	public AopProxyFactory getAopProxyFactory() {
		return this.aopProxyFactory;
	}

	/**
	 * Create a new ProxyCreatorSupport instance.
	 */
	public ProxyCreatorSupport() {
		this.aopProxyFactory = new DefaultAopProxyFactory();
	}

         所以最终会调到DefaultAopProxyFactory中去判断,继续往下跟。

	@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		// 这段代码用来判断选择哪种创建代理对象的方式
		// config.isOptimize()   是否对代理类的生成使用策略优化 其作用是和isProxyTargetClass是一样的 默认为false
		// config.isProxyTargetClass() 是否使用Cglib的方式创建代理对象 默认为false
		// hasNoUserSuppliedProxyInterfaces目标类是否有接口存在 且只有一个接口的时候接口类型不是SpringProxy类型
		if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
			// 上面的三个方法有一个为true的话,则进入到这里
			// 从AdvisedSupport中获取目标类 类对象
			Class<?> targetClass = config.getTargetClass();
			if (targetClass == null) {
				throw new AopConfigException("TargetSource cannot determine target class: " +
						"Either an interface or a target is required for proxy creation.");
			}
			// 判断目标类是否是接口 如果目标类是接口的话,则还是使用JDK的方式生成代理对象
			// 如果目标类是Proxy类型 则还是使用JDK的方式生成代理对象
			if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
				return new JdkDynamicAopProxy(config);
			}
			// 配置了使用Cglib进行动态代理或者目标类没有接口,那么使用Cglib的方式创建代理对象
			return new ObjenesisCglibAopProxy(config);
		}
		else {
			// 使用JDK的提供的代理方式生成代理对象
			return new JdkDynamicAopProxy(config);
		}
	}

        上面就能够判断得到是使用cglib动态还是还是jdk动态代理,如果使用的是cglib动态代理则返回ObjenesisCglibAopProxy,否则则返回JkdDynamicAopProxy对象。

        我们继续往下跟getProxy方法,看他是如果创建代理对象的。

@Override
	public Object getProxy(@Nullable ClassLoader classLoader) {
		if (logger.isTraceEnabled()) {
			logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource());
		}

		try {
			// 从advised中获取ioc容器中配置的target对象
			Class<?> rootClass = this.advised.getTargetClass();
			Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

			Class<?> proxySuperClass = rootClass;
			//如果目标对象已经是CGLIB 生成代理对象(就是比较类名称中有 $$ 字符串),那么就取目标对象的父类作为目标对象的类
			if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) {
				proxySuperClass = rootClass.getSuperclass();
				// 获取原始父类的接口
				Class<?>[] additionalInterfaces = rootClass.getInterfaces();
				for (Class<?> additionalInterface : additionalInterfaces) {
					this.advised.addInterface(additionalInterface);
				}
			}

			// Validate the class, writing log messages as necessary.
			// 打印出不能代理的方法名,CGLIB 是使用继承实现的,所以final , static 的方法不能被增强
			validateClassIfNecessary(proxySuperClass, classLoader);

			// Configure CGLIB Enhancer...
			// 创建及配置Enhancer
			Enhancer enhancer = createEnhancer();
			if (classLoader != null) {
				enhancer.setClassLoader(classLoader);
				if (classLoader instanceof SmartClassLoader &&
						((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {
					enhancer.setUseCache(false);
				}
			}
			// 配置超类,代理类实现的接口,回调方法等
			enhancer.setSuperclass(proxySuperClass);
			enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
			enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
			enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader));

			// 获取callbacks
			Callback[] callbacks = getCallbacks(rootClass);
			Class<?>[] types = new Class<?>[callbacks.length];
			for (int x = 0; x < types.length; x++) {
				types[x] = callbacks[x].getClass();
			}
			// fixedInterceptorMap only populated at this point, after getCallbacks call above
			enhancer.setCallbackFilter(new ProxyCallbackFilter(
					this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));
			enhancer.setCallbackTypes(types);

			// Generate the proxy class and create a proxy instance.
			// 通过 Enhancer 生成代理对象,并设置回调
			return createProxyClassAndInstance(enhancer, callbacks);
		}
		catch (CodeGenerationException | IllegalArgumentException ex) {
			throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() +
					": Common causes of this problem include using a final class or a non-visible class",
					ex);
		}
		catch (Throwable ex) {
			// TargetSource.getTarget() failed
			throw new AopConfigException("Unexpected AOP exception", ex);
		}
	}

        首先看一下AopProxyUtils.completeProxiedInterfaces(this.advised)。

        这个方法会给我们的代理类加上两个接口,一个是SpringProxy,是一个advised对象,我们之前了解过advisor对象是一个通知器,他里面包含有advice和pointcut对象,而advice对象就是具体的某一个消息通知,那么advised是干啥的?我们来看下他的方法以及他是在那里生成的。

        其实他就是ProxyFactory对象,就是用来配置代理的,那么他是在哪里赋值的呢?我们回到我们判断是使用哪种代理方式那里。

        而我们的ProxyFactory就是继承ProxyCreatorSupport的。

 

        然后看一下他是如何设置我们的回调方法的,继续往下跟 getCallbacks方法。

private Callback[] getCallbacks(Class<?> rootClass) throws Exception {
		// Parameters used for optimization choices...
		// 对于expose-proxy属性的处理,是否暴露当前对象为ThreadLocal模式,在当前上下文中能够进行引用
		boolean exposeProxy = this.advised.isExposeProxy();
		boolean isFrozen = this.advised.isFrozen();
		boolean isStatic = this.advised.getTargetSource().isStatic();

		// Choose an "aop" interceptor (used for AOP calls).
		// 将拦截器封装在DynamicAdvisedInterceptor中
		Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);

		// Choose a "straight to target" interceptor. (used for calls that are
		// unadvised but can return this). May be required to expose the proxy.
		Callback targetInterceptor;
		if (exposeProxy) {
			targetInterceptor = (isStatic ?
					new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) :
					new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource()));
		}
		else {
			targetInterceptor = (isStatic ?
					new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) :
					new DynamicUnadvisedInterceptor(this.advised.getTargetSource()));
		}

		// Choose a "direct to target" dispatcher (used for
		// unadvised calls to static targets that cannot return this).
		Callback targetDispatcher = (isStatic ?
				new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp());

		Callback[] mainCallbacks = new Callback[] {
				// 将拦截器链加入Callback中
				aopInterceptor,  // for normal advice
				targetInterceptor,  // invoke target without considering advice, if optimized
				new SerializableNoOp(),  // no override for methods mapped to this
				targetDispatcher, this.advisedDispatcher,
				new EqualsInterceptor(this.advised),
				new HashCodeInterceptor(this.advised)
		};

		Callback[] callbacks;

		// If the target is a static one and the advice chain is frozen,
		// then we can make some optimizations by sending the AOP calls
		// direct to the target using the fixed chain for that method.
		if (isStatic && isFrozen) {
			Method[] methods = rootClass.getMethods();
			Callback[] fixedCallbacks = new Callback[methods.length];
			this.fixedInterceptorMap = new HashMap<>(methods.length);

			// TODO: small memory optimization here (can skip creation for methods with no advice)
			for (int x = 0; x < methods.length; x++) {
				Method method = methods[x];
				List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass);
				fixedCallbacks[x] = new FixedChainStaticTargetInterceptor(
						chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass());
				this.fixedInterceptorMap.put(method, x);
			}

			// Now copy both the callbacks from mainCallbacks
			// and fixedCallbacks into the callbacks array.
			callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length];
			System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length);
			System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length);
			this.fixedInterceptorOffset = mainCallbacks.length;
		}
		else {
			callbacks = mainCallbacks;
		}
		return callbacks;
	}

         最后他封装了这些回调方法。第一个Callback是DynamicAdvisorInterceptor.

        然后就会调具体的createProxyClassAndInstance来创建代理对象了,继续往下跟。

	@Override
	protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
		Class<?> proxyClass = enhancer.createClass();
		Object proxyInstance = null;

		if (objenesis.isWorthTrying()) {
			try {
				proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
			}
			catch (Throwable ex) {
				logger.debug("Unable to instantiate proxy using Objenesis, " +
						"falling back to regular proxy construction", ex);
			}
		}

		if (proxyInstance == null) {
			// Regular instantiation via default constructor...
			try {
				Constructor<?> ctor = (this.constructorArgs != null ?
						proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
						proxyClass.getDeclaredConstructor());
				ReflectionUtils.makeAccessible(ctor);
				proxyInstance = (this.constructorArgs != null ?
						ctor.newInstance(this.constructorArgs) : ctor.newInstance());
			}
			catch (Throwable ex) {
				throw new AopConfigException("Unable to instantiate proxy using Objenesis, " +
						"and regular proxy instantiation via default constructor fails as well", ex);
			}
		}

		((Factory) proxyInstance).setCallbacks(callbacks);
		return proxyInstance;
	}

	public Class createClass() {
		classOnly = true;
		return (Class) createHelper();
	}
	private Object createHelper() {
		// 校验callbackTypes、filter是否为空,以及为空时的处理
		preValidate();
		// 通过newInstance方法来创建EnhancerKey对象,正常情况下,只需要new一个对象就可以调用方法了,但是Key_Factory是一个EnhancerKey类型,是一个内部接口,需要动态代理来实现,最终是为了调用newInstance方法
		Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
				ReflectUtils.getNames(interfaces),
				filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
				callbackTypes,
				useFactory,
				interceptDuringConstruction,
				serialVersionUID);
		// 设置当前enhancer的代理类的key标识
		this.currentKey = key;
		// 调用父类即AbstractClassGenerator的创建代理类
		Object result = super.create(key);
		return result;
	

        最后就会返回一个创建好的代理对象.

        然后回到我们获取得到这个代理对象,然后调用对应的代理方法,这时候会怎么去执行呢?很明显因为我们第一个放入的是DynamicAdvisorInterceptor对象,所以第一个就会进入到这个拦截器中。

    public static void main(String[] args) throws Exception {
//        saveGeneratedCGlibProxyFiles(System.getProperty("user.dir")+"/proxy");
        ApplicationContext ac = new ClassPathXmlApplicationContext("aop.xml");
        MyCalculator bean = ac.getBean(MyCalculator.class);
        System.out.println(bean.toString());
        bean.add(1,1);
        bean.sub(1,1);

    }

        这里想一下,我们有这么多通知切面,他是如何去判断这些通知的执行顺序 。

        在spring他是通过一个责任链模式来组装一个链式结构,不知道还是否继续要之前获取当前bean适配的advisor后会给所有的advisor集合中添加一个exposeInvocationInterceptor对象,是通过这个对象来判断当前执行的是哪个advisor对象,这个具体会在下一节中具体介绍。

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

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

相关文章

38. 115.不同的子序列,583. 两个字符串的删除操作,72. 编辑距离,编辑距离总结篇

确定dp数组以及下标的含义。dp[i][j]&#xff1a;以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。确定递推公式。这一类问题&#xff0c;基本是要分析两种情况&#xff1a;s[i - 1] 与 t[j - 1]相等&#xff1b;s[i - 1] 与 t[j - 1] 不相等。当s[i - 1] 与 t[j -…

【屏驱MCU】RT-Thread 文件系统接口解析

本文主要介绍【屏驱MCU】基于RT-Thread 系统的文件系统原理介绍与代码接口梳理 目录 0. 个人简介 && 授权须知1. 文件系统架构1.1 虚拟文件系统目录架构 2. menuconfig 分析3. 代码接口分析3.1 DFS框架挂载目录3.2 【FAL抽象层】分区表和设备表3.3 如何将【文件路径】挂…

计算机毕业设计PySpark+Django考研推荐系统 考研分数线预测 中公考研爬虫 混合神经网络推荐算法 考研可视化 机器学习 深度学习 大数据毕业设计

《PySparkDjango考研推荐系统》开题报告 一、研究背景与意义 1.1 研究背景 随着社会对高学历人才需求的不断增加&#xff0c;研究生入学考试&#xff08;考研&#xff09;已成为众多大学毕业生追求深造的重要途径。然而&#xff0c;考研涉及的知识面广泛且复杂&#xff0c;考…

Unity补完计划 之Tilemap

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正 1.Tilemap 是什么 Q&#xff1a;和 SpriteShape有什么区别&#xff1f; A&#xff1a;tilemap强项在于做重的复背景&…

产品经理-​桌面端、手机端、电视端、平板端在设计上的异同(29)

在互联网产品当中,产品形态,pc网页端,客户端,安卓,苹果端,小程序端等 不同的设备,交互设计、产品设计是不一样的,面对的用户群体不一样,产品的设计,规则也是不一样的 这个考查的是PM的交互设计知识&#xff0c;需要知道一般性的交互设计原则与各端设计差异 互联网的各端产品&am…

树莓派新版本在interface options中找不到camera选项

文章目录 问题原因&#xff1a; 操作方法&#xff1a; 1.系统升级 2. 安装libcamera 3. 测试拍照 4. 拍照和视频 5. 查看图片 问题原因&#xff1a; 版本问题&#xff0c;自2023.10之后的新版本中&#xff0c;树莓派去除了原先使用的picamera库&#xff0c;所以不能通过…

Unity补完计划之 Tile Palette

1.Tile Palette Creating a Tile Palette - Unity 手册 瓦片调色板&#xff08;Tile Palette&#xff09;是 Unity 引擎中用于在瓦片地图上进行绘制的工具。它允许您选择和管理颜色、纹理和瓦片&#xff0c;以便在游戏场景中创建地图、背景和其他2D元素 说白了&#xff0c;Ti…

win11启动IIS服务配置成web服务器·

为什么要将本地配置为 Web 服务器&#xff1f; 在 Web 开发过程中&#xff0c;将本地配置为 Web 服务器有诸多好处&#xff1a; 实时预览: 修改代码后&#xff0c;无需每次都上传到远程服务器&#xff0c;即可在本地浏览器中实时查看效果。 离线开发: 无需依赖网络连接&#x…

如何搭建一个圈子社区系统?开源社交陪玩交友圈子论坛帖子系统保姆级搭建教程!

整体部署流程如下&#xff1a; 1.获取源码/前后端分离&#xff0c;前端Uniapp vue2.0 后端thinkphp6&#xff08;Gitee直达&#xff09; 2.服务器安装宝塔&#xff08;已有宝塔请安装环境&#xff0c;Nginx或者Apache/ php 7.3/ mysql 5.6 &#xff09; 3.进入宝塔添加网站&…

实验8-1-5 使用函数的选择法排序

本题要求实现一个用选择法对整数数组进行简单排序的函数。 函数接口定义&#xff1a; void sort( int a[], int n );其中a是待排序的数组&#xff0c;n是数组a中元素的个数。该函数用选择法将数组a中的元素按升序排列&#xff0c;结果仍然在数组a中。 输入样例&#xff1a; …

XFS超级块介绍-xfs_sb

XFS超级块介绍 与其他 Unix 文件系统一样&#xff0c;XFS 的超级块在文件系统的第一个扇区&#xff0c;它帮助解码文件系统。超级块占用每个 XFS AG 的前 512 个字节。主超级块是位于文件系统前面的 AG 0 中的超级块&#xff0c;其他 AG 中的超级块用于冗余。目前仅使用超级块…

leetcode-238. 除自身以外数组的乘积

题目描述 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(…

【Nuxt】自定义插件和生命周期

自定义插件 方式一&#xff1a; app.vue // 创建插件(在app.vue中创建全局可以使用 而在某个页面中创建只有该页面可以使用) // 方式一&#xff1a; const nuxtApp useNuxtApp(); nuxtApp.provide("formDate", () > {return "2023-12-12"; }) nuxtAp…

vue相关

1,Object.defineProperty和Proxy区别 定义、优缺点、区别 Object.defineProperty&#xff1a;vue2中实现数据监听/响应式的核心API。 Object.defineProperty遍历监听&#xff08;observer&#xff09;vue对象中的所有属性&#xff1a;data&#xff0c;props&#xff0c;comp…

基于深度学习的太阳暗条检测(2020年以来)

A universal method for solar filament detection from Hα observations using semi-supervised deep learning A&A, 686, A213 (2024) A universal method for solar filament detection from Hα observations using semi-supervised deep learning (aanda.org) ABS…

SpringBoot依赖之Quartz Scheduler定时调度器

Quartz Scheduler 依赖名称: Quartz Scheduler功能描述: Schedule jobs using Quartz.使用 Quartz 安排作业。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency&…

论文笔记: 视频关键帧抽取相关工作

2024-08-06&#xff0c;星期二&#xff0c;北京&#xff0c;晴 今天来看视频关键帧抽取主题。随着最近Sora文生视频的火热&#xff0c;视频相关的数据获得逐渐变得重要起来。这也就涉及到提取视频关键帧的问题。 从文本生成视频&#xff0c;我们拆分来看&#xff0c;一般流程…

泳泳馆押金原路退回系统,一键操作秒到账 押金+手牌+电子押金单

一、游泳馆手牌收押金必要性 游泳馆手牌收押金有以下必要性&#xff1a; 1. 防止手牌丢失&#xff1a;手牌是顾客在游泳馆内存储个人物品和进出更衣室的重要凭证。收押金可以让顾客更加重视手牌&#xff0c;降低丢失的概率。比如说&#xff0c;有的顾客可能会因为粗心大意随手…

用Python进行数据可视化——seaborn库详解

内容&#xff1a; 安装和导入Seaborn使用Seaborn创建基本图形了解Seaborn的默认数据集 代码&#xff1a; # 安装和导入Seaborn !pip install seaborn import seaborn as sns import matplotlib.pyplot as plt# 使用Seaborn创建基本图形 tips sns.load_dataset("tips&q…

python库(19):flashtext库实现高效文本查找与替换

1 FlashText简介 FlashText 是一个用于高效查找和替换关键词的 Python 库。它的优势在于能够在 O(n) 时间复杂度内完成任务&#xff0c;而不是像正则表达式那样需要 O(m*n) 的时间复杂度。FlashText 使用 Aho-Corasick 自动机算法&#xff0c;在大量关键词匹配时表现尤为出色。…