手写Spring9(实现FactoryBean、对象作用域)

news2024/10/7 12:18:07

文章目录

  • 前言
  • 目标
  • 设计
  • 项目结构
  • 一、实现
    • 1、Bean的作用范围定义和xml解析
    • 2、创建和修改对象时候判断单例和原型模式
    • 3、定义 FactoryBean 接口
    • 4、实现一个 FactoryBean 注册服务
    • 5、扩展 AbstractBeanFactory 创建对象逻辑
  • 二、测试
    • 1、事先准备
    • 2、定义 FactoryBean 对象
    • 3、配置文件
    • 4、单元测试(单例&原型)
    • 5、单元测试(代理对象)
  • 总结


前言

在最早开始接触到spring里面的FactoryBean的时候,看过源码,也知道怎么使用,但是当年的我,知识匮乏,完全不理解他的使用场景,更是对他的底层实现,半知半解,看完这篇会对FactoryBean的实现有个清晰的认识


目标

在结合 Spring 框架下,我们使用的 MyBatis 框架中,仅仅是定义一个接口,然后通过 xml 或者注解配置的方式完成对数据库执行 CRUD 操作,那么在实现这样的 ORM 框架中,Spring是怎么把一个数据库操作的 Bean 对象交给 Spring 管理的呢

那么这一过程最核心待解决的问题,就是需要完成把复杂对象且以代理方式动态变化的对象,注册到 Spring 容器中。而为了满足这样的一个扩展组件开发的需求,就需要我们在现有手写的 Spring 框架中,添加这一能力

1、添加FactoryBean

2、添加对象的作用域(单例、原型)

设计

Srping在提供了FactoryBean(能让使用者定义复杂的 Bean 对象)功能后,就提供了谁家的框架都可以在此标准上完成自己服务的接入。

如MyBatis 就是实现了一个 MapperFactoryBean 类,在 getObject 方法中提供 SqlSession 对执行 CRUD 方法的操作

现整体设计结构如下图:

绿色部分为本次新增内容
在这里插入图片描述

1、整个的实现过程包括了两部分,一个解决单例还是原型对象**,另外一个处理 FactoryBean 类型对象创建过程**中关于获取具体调用对象的 getObject 操作。

2、SCOPE_SINGLETON、SCOPE_PROTOTYPE,对象类型的创建获取方式,主要区分在于 AbstractAutowireCapableBeanFactory#createBean 创建完成对象后是否放入到内存中,如果不放入则每次获取都会重新创建

3、createBean 执行对象创建、属性填充、依赖加载、前置后置处理、初始化等操作后,就要开始做执行判断整个对象是否是一个 FactoryBean 对象,如果是这样的对象,就需要再继续执行获取 FactoryBean 具体对象中的 getObject 对象了。整个 getBean 过程中都会新增一个单例类型的判断factory.isSingleton(),用于决定是否使用内存存放对象信息


项目结构

在这里插入图片描述

在这里插入图片描述

1、以上整个类关系图展示的就是添加 Bean 的实例化是单例还是原型模式以及 FactoryBean 的实现

2、其实整个实现的过程并不复杂,只是在现有的 AbstractAutowireCapableBeanFactory 类以及继承的抽象类 AbstractBeanFactory 中进行扩展。

3、不过这次我们把 AbstractBeanFactory 继承的 DefaultSingletonBeanRegistry 类,中间加了一层 FactoryBeanRegistrySupport,这个类在 Spring 框架中主要是处理关于 FactoryBean 注册的支撑操作


一、实现

1、Bean的作用范围定义和xml解析

public class BeanDefinition {

    String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;

    String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;

    private Class beanClass;

    private PropertyValues propertyValues;

    private String initMethodName;

    private String destroyMethodName;

    private String scope = SCOPE_SINGLETON;

    private boolean singleton = true;

    private boolean prototype = false;
    
    //在xml注册Bean定义时,通过scope字段来判断是单例还是原型
    public void setScope(String scope) {
        this.scope = scope;
        this.singleton = SCOPE_SINGLETON.equals(scope);
        this.prototype = SCOPE_PROTOTYPE.equals(scope);
    }
    
    // ...get/set
}

在BeanDefinition中,新增单例、原型变量,用于在xml解析注册时,填充到属性中

public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {

    protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException {
      
        for (int i = 0; i < childNodes.getLength(); i++) {
            // 判断元素
            if (!(childNodes.item(i) instanceof Element)) continue;
            // 判断对象
            if (!"bean".equals(childNodes.item(i).getNodeName())) continue;

            // 解析标签
            Element bean = (Element) childNodes.item(i);
            String id = bean.getAttribute("id");
            String name = bean.getAttribute("name");
            String className = bean.getAttribute("class");
            String initMethod = bean.getAttribute("init-method");
            String destroyMethodName = bean.getAttribute("destroy-method");
            String beanScope = bean.getAttribute("scope");

            // 获取 Class,方便获取类中的名称
            Class<?> clazz = Class.forName(className);
            // 优先级 id > name
            String beanName = StrUtil.isNotEmpty(id) ? id : name;
            if (StrUtil.isEmpty(beanName)) {
                beanName = StrUtil.lowerFirst(clazz.getSimpleName());
            }

            // 定义Bean
            BeanDefinition beanDefinition = new BeanDefinition(clazz);
            beanDefinition.setInitMethodName(initMethod);
            beanDefinition.setDestroyMethodName(destroyMethodName);

            if (StrUtil.isNotEmpty(beanScope)) {
                beanDefinition.setScope(beanScope);
            }
            
            // ...
            
            // 注册 BeanDefinition
            getRegistry().registerBeanDefinition(beanName, beanDefinition);
        }
    }

}

在解析 XML 处理类 XmlBeanDefinitionReader 中,新增加了关于 Bean 对象配置中 scope 的解析,并把这个属性信息填充到 Bean 定义中。beanDefinition.setScope(beanScope)


2、创建和修改对象时候判断单例和原型模式

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {

    private InstantiationStrategy instantiationStrategy = new CglibSubclassingInstantiationStrategy();

    @Override
    protected Object createBean(String beanName, BeanDefinition beanDefinition, Object[] args) throws BeansException {
        Object bean = null;
        try {
            bean = createBeanInstance(beanDefinition, beanName, args);
            // 给 Bean 填充属性
            applyPropertyValues(beanName, bean, beanDefinition);
            // 执行 Bean 的初始化方法和 BeanPostProcessor 的前置和后置处理方法
            bean = initializeBean(beanName, bean, beanDefinition);
        } catch (Exception e) {
            throw new BeansException("Instantiation of bean failed", e);
        }

        // 注册实现了 DisposableBean 接口的 Bean 对象
        registerDisposableBeanIfNecessary(beanName, bean, beanDefinition);

        // 判断 SCOPE_SINGLETON、SCOPE_PROTOTYPE
        if (beanDefinition.isSingleton()) {
            addSingleton(beanName, bean);
        }
        return bean;
    }

    protected void registerDisposableBeanIfNecessary(String beanName, Object bean, BeanDefinition beanDefinition) {
        // 非 Singleton 类型的 Bean 不执行销毁方法
        if (!beanDefinition.isSingleton()) return;

        if (bean instanceof DisposableBean || StrUtil.isNotEmpty(beanDefinition.getDestroyMethodName())) {
            registerDisposableBean(beanName, new DisposableBeanAdapter(bean, beanName, beanDefinition));
        }
    }
    
    // ... 其他功能
}

单例模式和原型模式的区别就在于是否存放到内存中,如果是原型模式那么就不会存放到内存中,每次获取都重新创建对象,另外非 Singleton 类型的 Bean 不需要执行销毁方法

所以这里的代码会有两处修改,一处是 createBean 中判断是否添加到 addSingleton(beanName, bean);,另外一处是 registerDisposableBeanIfNecessary 销毁注册中的判断 if (!beanDefinition.isSingleton()) return;


3、定义 FactoryBean 接口

public interface FactoryBean<T> {

	// 获取对象
    T getObject() throws Exception;
	// 对象类型
    Class<?> getObjectType();
	// 是否为单例
    boolean isSingleton();

}

FactoryBean 中需要提供3个方法,获取对象、对象类型,以及是否是单例对象,如果是单例对象依然会被放到内存中。


4、实现一个 FactoryBean 注册服务

public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanRegistry {

    /**
     * FactoryBean创建的单例对象缓存:FactoryBean名称——>对象
     */
    private final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<String, Object>();


    // 获取缓存对象
    protected Object getCachedObjectForFactoryBean(String beanName) {
        Object object = this.factoryBeanObjectCache.get(beanName);
        return (object != NULL_OBJECT ? object : null);

    }

    // 获取对象
    private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName) {
        try {
            return factory.getObject();
        } catch (Exception e) {
            throw new BeansException("FactoryBean threw exception on object[" + beanName + "] creation", e);
        }
    }

    // 从工厂Bean中获取对象
    protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName) {
        if (factory.isSingleton()) {
            Object object = this.factoryBeanObjectCache.get(beanName);
            if (object == null) {
                object = doGetObjectFromFactoryBean(factory, beanName);
                this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
            }
            return (object != NULL_OBJECT ? object : null);
        }else{
            return doGetObjectFromFactoryBean(factory, beanName);
        }
    }

}

1、FactoryBeanRegistrySupport 类主要处理的就是关于 FactoryBean 此类对象的注册操作,之所以放到这样一个单独的类里,就是希望做到不同领域模块下只负责各自需要完成的功能,避免因为扩展导致类膨胀到难以维护。

2、同样这里也定义了缓存操作 factoryBeanObjectCache,用于存放单例类型的对象,避免重复创建。在日常使用用,基本也都是创建的单例对象

3、doGetObjectFromFactoryBean 是具体的获取 FactoryBean#getObject() 方法,因为既有缓存的处理也有对象的获取,所以额外提供了 getObjectFromFactoryBean 进行逻辑包装,这部分的操作方式是不和你日常做的业务逻辑开发非常相似。从Redis取数据,如果为空就从数据库获取并写入Redis


5、扩展 AbstractBeanFactory 创建对象逻辑

public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {

    protected <T> T doGetBean(final String name, final Object[] args) {
        Object sharedInstance = getSingleton(name);
        if (sharedInstance != null) {
            // 如果是 FactoryBean,则需要调用 FactoryBean#getObject
            return (T) getObjectForBeanInstance(sharedInstance, name);
        }

        BeanDefinition beanDefinition = getBeanDefinition(name);
        Object bean = createBean(name, beanDefinition, args);
        return (T) getObjectForBeanInstance(bean, name);
    }  
   
    private Object getObjectForBeanInstance(Object beanInstance, String beanName) {
        if (!(beanInstance instanceof FactoryBean)) {
            return beanInstance;
        }

        Object object = getCachedObjectForFactoryBean(beanName);

        if (object == null) {
            FactoryBean<?> factoryBean = (FactoryBean<?>) beanInstance;
            object = getObjectFromFactoryBean(factoryBean, beanName);
        }

        return object;
    }
        
    // ...
}

1、首先这里把 AbstractBeanFactory 原来继承的 DefaultSingletonBeanRegistry,修改为继承 FactoryBeanRegistrySupport。因为需要扩展出创建 FactoryBean 对象的能力,所以这就想一个链条服务上,截出一个段来处理额外的服务,并把链条再链接上。

2、此处新增加的功能主要是在 doGetBean 方法中,添加了调用 (T) getObjectForBeanInstance(sharedInstance, name) 对获取 FactoryBean 的操作。

3、在 getObjectForBeanInstance 方法中做具体的 instanceof 判断,另外还会从 FactoryBean 的缓存中获取对象,如果不存在则调用 FactoryBeanRegistrySupport#getObjectFromFactoryBean,执行具体的操作。


二、测试

1、事先准备

public interface IUserDao {
    String queryUserName(String uId);
}

定义一个 IUserDao 接口,之所这样做是为了通过 FactoryBean 做一个自定义对象的代理操作。

public class UserService {

    private String uId;
    private String company;
    private String location;
    private IUserDao userDao;

    public String queryUserInfo() {
        return userDao.queryUserName(uId) + "," + company + "," + location;
    }

    // ...get/set
}

在 UserService 新修改了一个原有 UserDao 属性为 IUserDao,后面我们会给这个属性注入代理对象。


2、定义 FactoryBean 对象

public class ProxyBeanFactory implements FactoryBean<IUserDao> {

    @Override
    public IUserDao getObject() throws Exception {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Map<String, String> hashMap = new HashMap<>();
                hashMap.put("10001", "ljc");
                hashMap.put("10002", "yaya");
                hashMap.put("10003", "zz");
                return "你被代理了"+method.getName()+":"+hashMap.get(args[0].toString());
            }
        };
        return (IUserDao) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{IUserDao.class},handler);
    }

    @Override
    public Class<?> getObjectType() {
        return IUserDao.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

1、这是一个实现接口 FactoryBean 的代理类 ProxyBeanFactory 名称,主要是模拟了 UserDao 的原有功能,类似于 MyBatis 框架中的代理操作。

2、getObject() 中提供的就是一个 InvocationHandler 的代理对象,当有方法调用的时候,则执行代理对象的功能。


3、配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <bean id="userService" class="springframework.test.bean.UserService" scope="propertype">
        <property name="uId" value="10001"/>
        <property name="company" value="腾讯"/>
        <property name="location" value="深圳"/>
        <property name="userDao" ref="proxyUserDao"/>
    </bean>

    <bean id="proxyUserDao" class="springframework.test.bean.ProxyBeanFactory"/>

</beans>

在配置文件中,我们把 proxyUserDao 这个代理对象,注入到 userService 的 userDao 中。

把userService设置成了原型,方便后续测试


4、单元测试(单例&原型)

@Test
public void test_prototype() {
    // 1.初始化 BeanFactory
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
    applicationContext.registerShutdownHook();   

    // 2. 获取Bean对象调用方法
    UserService userService01 = applicationContext.getBean("userService", UserService.class);
    UserService userService02 = applicationContext.getBean("userService", UserService.class);
    
    // 3. 配置 scope="prototype/singleton"
    System.out.println(userService01);
    System.out.println(userService02);   
}

在 spring.xml 配置文件中,设置了 scope=“prototype” 这样就每次获取到的对象都应该是一个新的对象。

测试结果
在这里插入图片描述
可以看到是2个不同的对象


5、单元测试(代理对象)

@Test
    public void test_factory_bean() {
        // 1.初始化 BeanFactory
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring.xml");
        applicationContext.registerShutdownHook();

        // 2. 调用代理方法
        UserService userService = applicationContext.getBean("userService", UserService.class);
        System.out.println("测试结果:" + userService.queryUserInfo());

        System.out.println();

        IUserDao userDao = applicationContext.getBean("proxyUserDao", IUserDao.class);
        System.out.println("测试结果:"+ userDao.queryUserName("10002"));
    }

关于 FactoryBean 的调用并没有太多不一样,因为所有的不同都已经被 spring.xml 配置进去了。当然你可以直接调用 spring.xml 配置的对象springframework.test.bean.ProxyBeanFactory

测试结果

测试结果:你被代理了queryUserName:ljc, 公司:腾讯, 地点深圳

测试结果:你被代理了queryUserName:yaya

Process finished with exit code 0

从测试结果来看,我们的代理类 ProxyBeanFactory 已经完美替换掉了 UserDao 的功能。

虽然看上去这一点实现并不复杂,甚至有点简单。但就是这样一点点核心内容的设计了,解决了所有需要和 Spring 结合的其他框架交互链接问题

总结

1、手写spring到现在,发现前期各种抽象、各种接口疯狂添加,各种不明所以,但是到了现在去完善后续功能时,发现好像没那么难了,反而觉得深入理解后,添加新功能,变的很简单,只需要在涉及的相关范围进行相应的服务实现扩展即可。

2、仔细阅读完关于 FactoryBean 的实现以及测试过程的使用,以后再需要使用 FactoryBean 开发相应的组件时候,一定会非常清楚它是如何创建自己的复杂 Bean 对象以及在什么时候初始化和调用的。遇到问题也可以快速的排查、定位和解决。

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

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

相关文章

Win32多线程调用gdal库接口

作者:朱金灿 来源:clever101的专栏 为什么大多数人学不会人工智能编程?>>> 效果图和程序说明 效果图如下:   这个程序是基于MFC的GUI程序,用于给指定的文件夹批量创建金字塔。   效果图如下:   这个程序是基于Win32 API的GUI程序,用于给指定的文件创…

期末前端web大作业——动漫客栈响应式bootstarp(7页) 排版整洁,内容丰富,主题鲜明

HTML实例网页代码, 本实例适合于初学HTML的同学。该实例里面有设置了css的样式设置&#xff0c;有div的样式格局&#xff0c;这个实例比较全面&#xff0c;有助于同学的学习,本文将介绍如何通过从头开始设计个人网站并将其转换为代码的过程来实践设计。 ⚽精彩专栏推荐&#x1…

R语言用线性回归模型预测空气质量臭氧数据

尽管线性模型是最简单的机器学习技术之一&#xff0c;但它们仍然是进行预测的强大工具。 最近我们被客户要求撰写关于线性回归模型的研究报告&#xff0c;包括一些图形和统计输出。 这尤其是由于线性模型特别容易解释这一事实。在这里&#xff0c;我将讨论使用空气质量数据集…

Python学习基础笔记五十四——多继承

多继承中&#xff0c;我们子类对象调用的一个方法&#xff0c;默认是就近原则&#xff0c;找的顺序是什么&#xff1f; 在经典类中&#xff0c;是深度优先&#xff1b; 在新式类中&#xff0c;是广度优先&#xff1b; Python2.7是经典类和新式类共存&#xff0c;新式类要继承…

领域模型设计模式

前言&#xff1a; 领域是一个组织所做的事情以及其包含的一切&#xff0c;通俗地说&#xff0c;就是组织的业务范围和做事情的方式&#xff0c;也是软件开发的目标范围。比如说淘宝的电商业务&#xff0c;C2C就是电子商务的领域&#xff0c;领域驱动设计就是从领域出发&#x…

安装VS code

五 安装VS Code Visual Studio Code&#xff0c;简称VS Code&#xff0c;是一种简化且高效的代码编辑器&#xff0c;同时支持诸如调试&#xff0c;任务执行和版本管理之类的开发操作。它的目标是提供一种快速的编码编译调试工具。优势&#xff1a; 支持多种语言的编写&#xf…

【大数据处理技术】「#1」本地数据集上传到数据仓库Hive

文章目录实验数据集下载下载实验数据集建立一个用于运行本案例的目录dbtaobao数据集的预处理删除文件第一行记录&#xff0c;即字段名称获取数据集中双11的前100000条数据导入数据仓库实验数据集下载 下载实验数据集 data_format.zip数据集用户行为日志user_log.csv&#xff…

jsp+ssm计算机毕业设计房屋租赁系统【附源码】

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JSPSSM mybatis Maven等等组成&#xff0c;B/S模式 Mave…

node 使用 pm2 日志管理及使用 pm2-logrotate 进行日志分割

目录 1. 需求背景 2. 什么是 pm2-logrotate &#xff1f; 3. 查看 pm2 自带的日志管理 4. 安装 pm2-logrotate 5. 查看配置指令 6. pm2-logrotate 具体配置说明 7. 如何设置这些值&#xff1f; 8. 停止 pm2-logrotate 服务 9. 补充&#xff1a;pm2 常用命令 1. 需求…

Java学习笔记 --- MySQL-函数

一、合计/统计函数 count Count返回行的总数 SELECT COUNT(*) 列名 FROM table_name WHERE where_definition # 演示 mysql 的统计函数的使用 -- 统计一个班级共有多少学生&#xff1f; SELECT COUNT(*) FROM student -- 统计数学成绩大于90的学生有多少个 SELECT COUNT(*) FR…

Linux——vim的使用

实验5 vim的使用 一、两种模式&#xff1a; 命令行模式和编辑模式&#xff08;前者还有底行模式&#xff0c;命令行模式输入&#xff1a;就是底行模式&#xff09; 切换方法&#xff1a;进入vim后默认在命令模式&#xff0c;可以通过输入a后者i进入编辑模式&#xff0c;或者…

SQL学习day3

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 高级过滤Q1、检索供应商名称Q2、检索并列出已订购产品的清单(稍难&#xff09;Q3、返回所有价格在 3美元到 6美元之间的产品的名称和价格总结Q1、检索供应商名称 编写 SQL 语…

2022全年度吸尘器十大热门品牌销量榜单

近年来&#xff0c;随着社会经济的发展及人们生活水平的提升&#xff0c;吸尘器的市场需求得到不断地释放&#xff0c;行业规模也在不断扩大。但由于起步较晚&#xff0c;居民的消费能力尚未得到完全释放&#xff0c;目前我国吸尘器市场的渗透率还较低。 根据鲸参谋平台的数据统…

初识Go语言

Go是一种静态强类型、编译型、并发型语言。 一、Go语言的设计思维 尽可能少的方式去处理事情&#xff0c;减少选择的烦恼。 go的特点&#xff1a; 仅有25个关键字&#xff0c;简洁的语法内置垃圾回收器&#xff0c;大大降低程序员管理内存的负担去除隐式类型转换、去除指针…

UNIAPP实战项目笔记56 注册时验证手机号是否存在

UNIAPP实战项目笔记56 注册时验证手机号是否存在 注册时候需要拦截并验证登录 通过验证的直接跳转,未通过验证的提示手机号已存在 实际案例图片 后端接口文件 index.js var express require(express); var router express.Router(); var connection require(../db/sql.js);…

成端/接续功能使用说明

在nVisual系统中&#xff0c;接续/成端功能可以高效、准确、清晰的查看熔纤盒内的光纤连接情况。今天小编来给大家介绍一下成端和接续功能的使用。 一、成端 1. 功能入口 成端功能的入口有两种方式&#xff0c;第一个是鼠标右击选中线缆&#xff0c;出现连线右键菜单&#xf…

互联网新热——元宇宙带来了全新数字应用模式,推动数字经济的发展

大家平时电视上看到的在元宇宙中逛街、购物、社交&#xff0c;娱乐、看直播和表演……这些高度沉浸式地互动体验&#xff0c;都可以在“大唐灵境”实现了&#xff01; 近日&#xff0c;太一集团与大唐不夜城联手打造的中国首个文旅电商元宇宙“大唐灵境”将正式开放“东市”商…

阿里云轻量级服务器部署了项目外网无法访问

阿里云轻量级服务器部署了项目外网无法访问1、问题描述2、配置安全组权限2.1、阿里云控制台权限配置&#xff08;热加载&#xff0c;不用重启&#xff09;2.2、在服务器中查看防火墙中有哪些端口被开放&#xff08;非热加载&#xff0c;最后一定要手动重新加载&#xff09;3、如…

MAC Python 虚拟环境配置方法parcharm

MAC Python 虚拟环境配置方法&parcharm一、安装环境包1.找个风水宝地2.安装virtualenv包二、创建虚拟环境1.对比记录2.创建虚拟环境启动虚拟环境1.启动命令2.环境对比三、退出虚拟环境四、pycharm使用虚拟环境1.打开parcharm&#xff0c;新建一个项目&#xff0c;起个名字。…

excel账龄计算:两个经典公式快速制作账龄统计表

财务工作者对于账龄统计表都不陌生&#xff0c;我们之前也分享过很多关于账龄统计表的操作技巧和公式解析&#xff0c;但是今天遇到的这种账龄统计表&#xff0c;还是让财务小姐姐犯了难&#xff0c;到底是个怎样的账龄统计表呢&#xff0c;一起来看看吧&#xff1a; 如图所示&…