【Java EE】Spring IoC的概念

Spring IoC的概念

Spring框架是Java世界最为成功的框架,它最为核心的理念是IoC(控制反转)和AOP(面向切面编程)。

Spring的概述

在Spring中,它会认为一切Java类都是资源,而资源都是Bean,容纳这些Bean的是Spring所提供的IoC容器,所以Spring是一种基于Bean的编程。

Spring提供了以下策略:

  1. 对于POJO的潜力开发,提供轻量级和低侵入的编程,可以通过配置(XML、注解等)来扩展POJO的功能,通过依赖注入的理念去扩展功能,建议通过接口编程,强调OOD的开发模式理念,降低系统耦合度,提高系统可读性和可扩展性。
  2. 提供切面编程,尤其是把企业的核心应用——数据库应用,通过切面消除了以前复杂的try…catch…finally…代码结构,使得开发人员能够把精力更加集中于业务开发而不是技术本身,也避免了try…catch…finally…语句的滥用。
  3. 为了整合各个框架和技术的应用,Spring提供了模板类,通过模板可以整合各个框架和技术,比如支持Hibernate开发的HibernateTemplate、支持MyBatis开发的SqlSessionTemplate、支持Redis开发的RedisTemplate等,这样就把各种企业用到的技术框架整合到Spring中,提供了统一的模板,从而使得各种技术用起来更简单。

Spring IoC概述

主动创建对象

如果需要橙汁,那么就等于需要橙子、开水、糖,这些都是原料,而搅拌机是工具。如果需要主动创建对象,那么要对此创建对应的对象——JuiceMaker和Blender。

public class Blender {

	/**
	 * 搅拌
	 * 
	 * @param water
	 *            水描述
	 * @param fruit
	 *            水果描述
	 * @param sugar
	 *            糖描述
	 * @return 果汁
	 */
	public String mix(String water, String fruit, String sugar) {
		String juice = "这是一杯由液体:" + water + "\n水果:" + fruit + "\n糖量:" + sugar + "\n组成的果汁";
		return juice;
	}
}
public class JuiceMaker {
	private Blender blender = null;// 搅拌机
	private String water;// 水描述
	private String fruit;// 水果
	private String sugar;// 糖分描述

	public Blender getBlender() {
		return blender;
	}

	public void setBlender(Blender blender) {
		this.blender = blender;
	}

	public String getWater() {
		return water;
	}


	public void setWater(String water) {
		this.water = water;
	}

	public String getFruit() {
		return fruit;
	}

	public void setFruit(String fruit) {
		this.fruit = fruit;
	}

	public String getSugar() {
		return sugar;
	}

	public void setSugar(String sugar) {
		this.sugar = sugar;
	}

	/**
	 * 果汁生成
	 */
	public String makeJuice() {
		blender = new Blender();
		return blender.mix(water, fruit, sugar);
	}
}

被动创建对象

假设已经提供了果汁制造器(JuiceMaker2),那么只需要对其进行描述就可以得到果汁了。假设饮品店还会给我们提供这样的一个描述(Source),清单如下

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class JuiceMaker2 implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean {
	private String beverageShop = null;
	private Source source = null;

	public String getBeverageShop() {
		return beverageShop;
	}

	public void setBeverageShop(String beverageShop) {
		this.beverageShop = beverageShop;
	}

	public Source getSource() {
		return source;
	}

	public void setSource(Source source) {
		this.source = source;
	}

	public String makeJuice() {
		String juice = "这是一杯由" + beverageShop + "饮品店,提供的" + source.getSize() + source.getSugar() + source.getFruit();
		return juice;
	}

	public void init() {
		System.out.println("【" + this.getClass().getSimpleName() + "】执行自定义初始化方法");
	}

	public void destroy() {
		System.out.println("【" + this.getClass().getSimpleName() + "】执行自定义销毁方法");
	}

	@Override
	public void setBeanName(String arg0) {
		System.out.println("【" + this.getClass().getSimpleName() + "】调用BeanNameAware接口的setBeanName方法");

	}

	@Override
	public void setBeanFactory(BeanFactory arg0) throws BeansException {
		System.out.println("【" + this.getClass().getSimpleName() + "】调用BeanFactoryAware接口的setBeanFactory方法");
	}

	@Override
	public void setApplicationContext(ApplicationContext arg0) throws BeansException {
		System.out.println(
				"【" + this.getClass().getSimpleName() + "】调用ApplicationContextAware接口的setApplicationContext方法");
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("【" + this.getClass().getSimpleName() + "】调用InitializingBean接口的afterPropertiesSet方法");
	}
}
public class Source {
	private String fruit;// 类型
	private String sugar;// 糖分描述
	private String size;// 大小杯
	public String getFruit() {
		return fruit;
	}
	public void setFruit(String fruit) {
		this.fruit = fruit;
	}
	public String getSugar() {
		return sugar;
	}
	public void setSugar(String sugar) {
		this.sugar = sugar;
	}
	public String getSize() {
		return size;
	}
	public void setSize(String size) {
		this.size = size;
	}
	
}

显然并不需要去关注果汁是如何制造出来的,系统采用XML这个清单进行描述:

<bean id="Source" class="com.ssm.learn.pojo.Source">
	<property name="fruit" value="橙汁" />
	<property name="sugar" value="少糖" />
	<property name="size" value="大杯" />
</bean>

这里对果汁进行了描述,接着需要选择饮品店,假设选择的是贡茶,那么会有代码:

<bean id="juiceMaker2" class="com.ssm.learn.pojo.JuiceMaker2">
	<property name="beverageShop" value="贡茶" />
	<property name="source" ref="source" />
</bean>

这里将饮品店设置为贡茶,这样就指定了贡茶为我们提供服务,而订单则引用我们之前的定义,使用下面的代码就能得到一杯果汁了。

JuiceMaker2 juiceMaker2 = (JuiceMaker2) ctx.getBean("juiceMaker2");
String juice = juiceMaker2.makeJuice();

Spring IoC阐述

控制反转是一种通过描述(在Java中可以是XML或者注解)并通过第三方去产生或获取特定对象的方式。
Spring会提供IoC容器来管理对应的资源。
主动创建的模式,责任归于开发者,被动的模式下,责任归于IoC容器。基于这样的被动形式,就说对象被控制反转了。

Spring IoC容器

Spring IoC容器的设计

Spring IoC容器的设计主要是基于BeanFactory和ApplicationContext两个接口,其中ApplicationContext是BeanFactory的子接口之一,换句话说BeanFactory是Spring IoC容器所定义的最底层接口,而ApplicationContext是其高级接口之一,并且对BeanFactory功能做了许多有用的扩展,所以在绝大部分的工作场景下,都会使用ApplicationContext作为Spring IoC容器。
BeanFactory提供了Spring IoC最底层的设计,其源码如下:

public interface BeanFactory{
	String FACTORY_BEAN_PREFIX = "&";
	Object getBean(String name) throws BeanException;
	<T> T getBean(Class<T> requiredType) throws BeansException;
	Object getBean(String name, Object... args) throws BeansException;
	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
	boolean containsBean(String name);
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	boolean isTypeMatch(String name, ResolveableType typeToMatch) throws NoSuchBeanDefinitionException;
	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;
	String[] getAliases(String name);
}

其中:

  • getBean的多个方法用于获取配置给SpringIoC容器的Bean。
  • isSingleton用于判断是否单例,如果为真,其意思是该Bean在容器中作为一个唯一单例存在的。
  • isPrototype如果判断为真,意思是当从容器中获取Bean,容器就生成了一个新的实例,默认情况下Spring会为Bean创建一个单例,也就是说默认情况下isSingleton返回true,而isPrototype返回false。
  • 关于type的匹配,这是一个按Java类型匹配的方式
  • getAliases方法是获取别名的方法。
    这里认识一个ApplicationContext的子类——ClassPathXmlApplicationContext。先创建一个xml文件:
<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
	<!--BeanPostProcessor定义 -->
	<bean id="beanPostProcessor" class="com.ssm.chapter9.bean.BeanPostProcessorImpl" />

	<!--DisposableBean定义 -->
	<bean id="disposableBean" class="com.ssm.chapter9.bean.DisposableBeanImpl" />

	<bean id="source" class="com.ssm.chapter9.pojo.Source">
		<property name="fruit" value="橙汁" />
		<property name="sugar" value="少糖" />
		<property name="size" value="大杯" />
	</bean>

	<bean id="juiceMaker2" class="com.ssm.chapter9.pojo.JuiceMaker2"
		destroy-method="destroy" init-method="init">
		<property name="beverageShop" value="贡茶" />
		<property name="source" ref="source" />
	</bean>

</beans>

这里定义了两个Bean,这样Spring IoC容器在初始化的时候就能找到它们,然后使用ClassPathXmlApplicationContext容器就可以将其初始化,代码如下:

ApplicationContext ctx = new ClassPathXmlApplicationContext("Spring-cfg.xml");
JuiceMaker2 juiceMaker2 = (JuiceMaker2) ctx.getBean("juiceMaker2");
System.out.println(juiceMaker2.makeJuice());

这样就会使用Application的实现类ClassPathXmlApplicationContext去初始化Spring IoC容器,然后开发者就可以通过IoC容器来获得资源了。

Spring IoC容器的初始化和依赖注入

Bean的定义和初始化在Spring IoC容器中是两大步骤,它是先定义,然后初始化和依赖注入的。
Bean的定义分为3步:

  1. Resource定位,这步是Spring IoC容器根据开发者的配置,进行资源定位,在Spring的开发中,通过XML或者注解都是十分常见的方式,定位的内容是由开发者所提供的。
  2. BeanDefinition的载入,这个时候只是将Resource定位到的信息,保存到Bean定义中(BeanDefinition)中,此时并不会创建Bean的实例。
  3. BeanDefinition的注册,这个过程就是将BeanDefinition的信息发布到SpringIoC容器中,注意,此时仍旧没有对应的Bean的实例创建。

做完了这3步,Bean就在Spring IoC容器中被定义了,而没有被初始化,更没有完成依赖注入,也就是没有注入其配置的资源给Bean,那么它还不能完全使用。对于初始化和依赖注入,Spring Bean还有一个配置选项——lazy-init,其含义就是是否初始化Spring Bean。在没有任何配置的情况下,它的默认值为default,实际值为false,也就是Spring IoC默认会自动初始化Bean。如果将其设置为true,那么只有当我们使用Spring IoC容器的getBean方法获取它,它才会进行Bean的初始化,完成依赖注入。

Spring Bean的生命周期

Spring IoC容器的本质目的就是为了管理Bean。对于Bean而言,在容器中存在其生命周期,它的初始化和销毁也需要一个过程。
Spring Bean的完整生命周期从创建Spring容器开始,直到最终Spring容器销毁Bean,这其中包含了一系列关键点。
Bean的生命周期
若容器注册了以上各种接口,程序那么将会按照以上的流程进行。下面将仔细讲解各接口作用。

各种接口方法分类

Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:

  1. Bean自身的方法:这个包括了Bean本身调用的方法和通过配置文件中的init-method和destroy-method指定的方法
  2. Bean级生命周期接口方法:这个包括了BeanNameAware、BeanFactoryAware、InitializingBean和DiposableBean这些接口的方法
  3. 容器级生命周期接口方法:这个包括了InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”。
  4. 工厂后处理器接口方法:这个包括了AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

具体过程描述

  • 如果Bean实现了接口BeanNameAware的setBeanName方法,那么它就会调用这个方法
  • 如果Bean实现了接口BeanFactoryAware的setBeanFactory方法,那么它就会调用这个方法
  • 如果Bean实现了接口ApplicationContextAware的setApplicationContext方法,且Spring IoC容器也必须是一个ApplicationContext接口的实现类,那么才会调用这个方法,否则是不调用的。
  • 如果Bean实现了接口BeanPostProcessor的postProcessBeforeInitialization方法,那么它就会调用这个方法。
  • 如果Bean实现了接口BeanFactoryPostProcessor的afterPropertiesSet方法,那么它就会调用这个方法。
  • 如果Bean自定义了初始化方法,它就会调用已定义的初始化方法。
  • 如果Bean实现了接口BeanPostProcessor的postProcessAfterInitialization方法,完成了这些调用,这个时候Bean就完成了初始化,那么Bean就生存在Spring IoC容器中了,使用者就可以从中获取Bean的服务。当服务器正常关闭,或者遇到其他关闭Spring IoC容器的事件,它就会调用对应的方法完成Bean的销毁,其步骤如下:
    1. 如果Bean实现了接口DisposableBean的destory方法,那么就会调用它。
    1. 如果定义了自定义销毁方法,那么就会调用它。

注意:所有的Spring IoC容器最低的要求是实现BeanFactory接口而已,而非ApplicationContext接口。如果采用了非ApplicationContext子类创建SpringIoC容器,那么即使是实现了ApplicationContextAware的setApplicationContext方法,它也不会在生命周期之中被调用。
当一个Bean实现了上述的接口,我们只需要在Spring IoC容器中定义它就可以了,Spring IoC容器会自动识别。

代码演示过程

1、首先是一个简单的Spring Bean,调用Bean自身的方法和Bean级生命周期接口方法,为了方便演示,它实现了BeanNameAware、BeanFactoryAware、InitializingBean和DiposableBean这4个接口,同时有2个方法,对应配置文件中的init-method和destroy-method。如下:

package com.ssm.learn.pojo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.*;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;

public class Person implements BeanFactoryAware, BeanNameAware, InitializingBean, DisposableBean {

    private String name;
    private String address;
    private int phone;

    private BeanFactory beanFactory;
    private String beanName;

    public Person() {
        System.out.println("【构造器】调用Person的构造器实例化");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("【注入属性】注入属性name");
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        System.out.println("【注入属性】注入属性address");
        this.address = address;
    }

    public int getPhone() {
        return phone;
    }

    public void setPhone(int phone) {
        System.out.println("【注入属性】注入属性phone");
        this.phone = phone;
    }

    @Override
    public String toString() {
        return "Person [address=" + address + ", name=" + name + ", phone="
                + phone + "]";
    }

    // 这是BeanFactoryAware接口方法
    @Override
    public void setBeanFactory(BeanFactory arg0) throws BeansException {
        System.out
                .println("【BeanFactoryAware接口】调用BeanFactoryAware.setBeanFactory()");
        this.beanFactory = arg0;
    }

    // 这是BeanNameAware接口方法
    @Override
    public void setBeanName(String arg0) {
        System.out.println("【BeanNameAware接口】调用BeanNameAware.setBeanName()");
        this.beanName = arg0;
    }

    // 这是InitializingBean接口方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out
                .println("【InitializingBean接口】调用InitializingBean.afterPropertiesSet()");
    }

    // 这是DiposibleBean接口方法
    @Override
    public void destroy() throws Exception {
        System.out.println("【DiposibleBean接口】调用DiposibleBean.destory()");
    }

    // 通过<bean>的init-method属性指定的初始化方法
    public void myInit() {
        System.out.println("【init-method】调用<bean>的init-method属性指定的初始化方法");
    }

    // 通过<bean>的destroy-method属性指定的初始化方法
    public void myDestory() {
        System.out.println("【destroy-method】调用<bean>的destroy-method属性指定的初始化方法");
    }
}

2、接下来是演示BeanPostProcessor接口的方法,如下:

package com.ssm.learn.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class BeanPostProcessorImpl implements BeanPostProcessor {
    public BeanPostProcessorImpl() {
        super();
        System.out.println("这是BeanPostProcessor实现类构造器!!");
        // TODO Auto-generated constructor stub
    }

    @Override
    public Object postProcessAfterInitialization(Object arg0, String arg1)  throws BeansException {
        System.out.println("BeanPostProcessor接口方法postProcessAfterInitialization对属性进行更改!");
        return arg0;
    }

    @Override
    public Object postProcessBeforeInitialization(Object arg0, String arg1)  throws BeansException {
        System.out.println("BeanPostProcessor接口方法postProcessBeforeInitialization对属性进行更改!");
        return arg0;
    }
}

如上,BeanPostProcessor接口包括2个方法postProcessAfterInitialization和postProcessBeforeInitialization,这两个方法的第一个参数都是要处理的Bean对象,第二个参数都是Bean的name。返回值也都是要处理的Bean对象。这里要注意。

  1. InstantiationAwareBeanPostProcessor 接口本质是BeanPostProcessor的子接口,一般我们继承Spring为其提供的适配器类InstantiationAwareBeanPostProcessor Adapter来使用它,如下:
package com.ssm.learn.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;

import java.beans.PropertyDescriptor;

public class InstantiationAwareBeanPostProcessorImpl extends InstantiationAwareBeanPostProcessorAdapter {
    public InstantiationAwareBeanPostProcessorImpl() {
        super();
        System.out.println("这是InstantiationAwareBeanPostProcessorAdapter实现类构造器!!");
    }

    // 接口方法、实例化Bean之前调用
    @Override
    public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException {
        System.out.println("InstantiationAwareBeanPostProcessor调用postProcessBeforeInstantiation方法");
        return null;
    }

    // 接口方法、实例化Bean之后调用
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("InstantiationAwareBeanPostProcessor调用postProcessAfterInitialization方法");
        return bean;
    }

    // 接口方法、设置某个属性时调用
    @Override
    public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
        System.out.println("InstantiationAwareBeanPostProcessor调用postProcessPropertyValues方法");
        return pvs;
    }
}

这个有3个方法,其中第二个方法postProcessAfterInitialization就是重写了BeanPostProcessor的方法。第三个方法postProcessPropertyValues用来操作属性,返回值也应该是PropertyValues对象。

  1. 演示工厂后处理器接口方法,如下:
package com.ssm.learn.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;

public class BeanFactoryPostProcessorImpl implements BeanFactoryPostProcessor {
    public BeanFactoryPostProcessorImpl() {
        super();
        System.out.println("这是BeanFactoryPostProcessor实现类构造器!!");
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {
        System.out.println("BeanFactoryPostProcessor调用postProcessBeanFactory方法");
        BeanDefinition bd = arg0.getBeanDefinition("person");
        bd.getPropertyValues().addPropertyValue("phone", "110");
    }

}
  1. 配置文件如下beans.xml,使用ApplicationContext,处理器不用手动注册:
<?xml version='1.0' encoding='UTF-8' ?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<bean id="beanPostProcessor" class="com.ssm.learn.bean.BeanPostProcessorImpl">
</bean>

<bean id="instantiationAwareBeanPostProcessor" class="com.ssm.learn.bean.InstantiationAwareBeanPostProcessorImpl">
</bean>

<bean id="beanFactoryPostProcessor" class="com.ssm.learn.bean.BeanFactoryPostProcessorImpl">
</bean>

<bean id="person" class="com.ssm.learn.pojo.Person" init-method="myInit"
     destroy-method="myDestory" scope="singleton">
   <property name="name" value="张三"/>
   <property name="address" value="广州"/>
   <property name="phone" value="159000000"/>
</bean>
</beans>
  1. 测试一下:
package com.ssm.learn.main;

import com.ssm.learn.pojo.Person;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class main {
    public static void main(String[] args) throws BeansException {
        System.out.println("现在开始初始化容器");

        ApplicationContext factory = new ClassPathXmlApplicationContext("spring-cfg.xml");
        System.out.println("容器初始化成功");
        //得到Preson,并使用
        Person person = factory.getBean("person", Person.class);
        System.out.println(person);

        System.out.println("现在开始关闭容器!");
        ((ClassPathXmlApplicationContext)factory).registerShutdownHook();
    }
}

关闭容器使用的是实际是AbstractApplicationContext的钩子方法。
结果如下:

现在开始初始化容器
305, 2020 1:07:35 上午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@3d012ddd: startup date [Thu Mar 05 01:07:35 CST 2020]; root of context hierarchy
305, 2020 1:07:35 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [spring-cfg.xml]
这是BeanFactoryPostProcessor实现类构造器!!
BeanFactoryPostProcessor调用postProcessBeanFactory方法
这是BeanPostProcessor实现类构造器!!
这是InstantiationAwareBeanPostProcessorAdapter实现类构造器!!
InstantiationAwareBeanPostProcessor调用postProcessBeforeInstantiation方法
【构造器】调用Person的构造器实例化
InstantiationAwareBeanPostProcessor调用postProcessPropertyValues方法
【注入属性】注入属性name
【注入属性】注入属性address
【注入属性】注入属性phone
【BeanNameAware接口】调用BeanNameAware.setBeanName()
【BeanFactoryAware接口】调用BeanFactoryAware.setBeanFactory()
BeanPostProcessor接口方法postProcessBeforeInitialization对属性进行更改!
【InitializingBean接口】调用InitializingBean.afterPropertiesSet()
【init-method】调用<bean>的init-method属性指定的初始化方法
BeanPostProcessor接口方法postProcessAfterInitialization对属性进行更改!
InstantiationAwareBeanPostProcessor调用postProcessAfterInitialization方法
容器初始化成功
Person [address=广州, name=张三, phone=110]
现在开始关闭容器!
【DiposibleBean接口】调用DiposibleBean.destory()
【destroy-method】调用<bean>的destroy-method属性指定的初始化方法

参考资料:

  1. https://www.cnblogs.com/zrtqsk/p/3735273.html
  2. https://zhuanlan.zhihu.com/p/52537298
发布了279 篇原创文章 · 获赞 169 · 访问量 32万+

猜你喜欢

转载自blog.csdn.net/ARPOSPF/article/details/104521676