Spring知识——IOC容器

  • 2020 年 1 月 16 日
  • 筆記

IOC概述

1、理解: (1)控制反转。将生成对象的控制权交IOC容器,由容器生成依赖的对象。调用类只依赖接口,而不依赖具体的实现类,减少了耦合。在运行的时候,才由容器将具体的实例注入到调用类的对象中。 (2)依赖注入,就是向Ioc容器索要bean的过程。getBean是依赖注入的起点。依赖注入的过程是用户第一次向Ioc容器索要Bean时触发的。 (3)生成bean的两种方式 a、通过反射调用构造函数 b、通过CGLib 2、优点: (1)依赖关系的管理被反转并交给容器,使复杂的依赖关系管理从应用中解放出来。 (2)代码解耦 3、启动过程(依赖注入的实现过程): a、Resource寻找资源(XML文件形式的beanDefinition) b、将XML文件载入内存中,解析成org.springframework.beans.factory.config.BeanDefinition对象 c、将org.springframework.beans.factory.config.BeanDefinition对象注册到HashMap容器中 d、客户想Ioc容器索要bean,触发依赖注入

基础使用

一、首先讲解依赖注入的3种方式: 1、set方式注入: 假设有一个SpringAction,类中需要实例化一个SpringDao对象,那么就可以定义一个private的SpringDao成员变量,然后创建SpringDao的set方法(这是ioc的注入入口):

package com.bless.springdemo.action;  public class SpringAction {          //注入对象springDao      private SpringDao springDao;          //一定要写被注入对象的set方法          public void setSpringDao(SpringDao springDao) {          this.springDao = springDao;      }            public void ok(){          springDao.ok();      }  }  

随后编写spring的xml文件,<bean>中的name属性是class属性的一个别名,class属性指类的全名,因为在SpringAction中有一个公共属性Springdao,所以要在<bean>标签中创建一个<property>标签指定SpringDao。<property>标签中的name就是SpringAction类中的SpringDao属性名,ref指下面<bean name="springDao"…>,这样其实是spring将SpringDaoImpl对象实例化并且调用SpringAction的setSpringDao方法将SpringDao注入。

<!--配置bean,配置后该类由spring管理-->  <bean name="springAction" class="com.bless.springdemo.action.SpringAction">          <!--(1)依赖注入,配置当前类中相应的属性-->          <property name="springDao" ref="springDao"></property>  </bean>  <bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>

2、构造器注入: 这种方式的注入是指带有参数的构造函数注入,看下面的例子,我创建了两个成员变量SpringDao和User,但是并未设置对象的set方法,所以就不能支持Set注入方式,这里的注入方式是在SpringAction的构造函数中注入,也就是说在创建SpringAction对象时要将SpringDao和User两个参数值传进来:

public class SpringAction {      //注入对象springDao      private SpringDao springDao;      private User user;        public SpringAction(SpringDao springDao,User user){          this.springDao = springDao;          this.user = user;          System.out.println("构造方法调用springDao和user");      }            public void save(){          user.setName("卡卡");          springDao.save(user);      }  } 

在XML文件中同样不用<property>的形式,而是使用<constructor-arg>标签,ref属性同样指向其它<bean>标签的name属性:

<!--配置bean,配置后该类由spring管理-->      <bean name="springAction" class="com.bless.springdemo.action.SpringAction">          <!--(2)创建构造器注入,如果主类有带参的构造方法则需添加此配置-->          <constructor-arg ref="springDao"></constructor-arg>          <constructor-arg ref="user"></constructor-arg>      </bean>      <bean name="springDao" class="com.bless.springdemo.dao.impl.SpringDaoImpl"></bean>      <bean name="user" class="com.bless.springdemo.vo.User"></bean> 

解决构造方法参数的不确定性,你可能会遇到构造方法传入的两参数都是同类型的,为了分清哪个该赋对应值,则需要进行一些小处理: 下面是设置index,就是参数位置:

<bean name="springAction" class="com.bless.springdemo.action.SpringAction">          <constructor-arg index="0" ref="springDao"></constructor-arg>          <constructor-arg index="1" ref="user"></constructor-arg>  </bean>  

另一种是设置参数类型:

<constructor-arg type="java.lang.String" ref=""/> 

3、接口注入: ClassA依赖于InterfaceB,在运行期, InterfaceB 实例将由容器提供。

public class ClassA {      private InterfaceB clzB;      public Object doSomething(InterfaceB b) {          clzB = b;          return clzB.doIt();      }  }  ……  }

二、Bean标签 1、scope属性: (1)singleton:单例模式,即该bean对应的类只有一个实例;在spring 中是scope(作用范围)参数的默认值 ; (2)prototype:表示每次从容器中取出bean时,都会生成一个新实例;相当于new出来一个对象; (3)request:基于web,表示每次接受一个HTTP请求时,都会生成一个新实例; (4)session:表示在每一个session中只有一个该对象. (5)global session global session作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义。Portlet规范定义了全局Session的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。在global session作用域中定义的bean被限定于全局portlet Session的生命周期范围内。如果你在web中使用global session作用域来标识bean,那么web会自动当成session类型来使用。 配置实例: 和request配置实例的前提一样,配置好web启动文件就可以如下配置: <bean id="role" class="spring.chapter2.maryGame.Role" scope="global session"/> (6)自定义bean装配作用域: 在spring2.0中作用域是可以任意扩展的,你可以自定义作用域,甚至你也可以重新定义已有的作用域(但是你不能覆盖singleton和prototype),spring的作用域由接口org.springframework.beans.factory.config.Scope来定义,自定义自己的作用域只要实现该接口即可,下面给个实例: 我们建立一个线程的scope,该scope在表示一个线程中有效,代码如下:

publicclass MyScope implements Scope {        privatefinal ThreadLocal threadScope = new ThreadLocal() {            protected Object initialValue() {               returnnew HashMap();             }       };       public Object get(String name, ObjectFactory objectFactory) {           Map scope = (Map) threadScope.get();           Object object = scope.get(name);          if(object==null) {             object = objectFactory.getObject();             scope.put(name, object);           }          return object;        }       public Object remove(String name) {           Map scope = (Map) threadScope.get();          return scope.remove(name);        }        publicvoid registerDestructionCallback(String name, Runnable callback) {        }      public String getConversationId() {         // TODO Auto-generated method stub          returnnull;       }             }

源码解析

一、IOC容器: 1、对于Spring的使用者而言,IOC容器实际上是什么呢?我们可以说BeanFactory就是我们看到的IoC容器,当然了Spring为我们准备了许多种IoC容器来使用,比如说ApplicationContext。这样可以方便我们从不同的层面,不同的资源位置,不同的形式的定义信息来建立我们需要的IoC容器。 在Spring中,最基本的IOC容器接口是BeanFactory – 这个接口为具体的IOC容器的实现作了最基本的功能规定 – 不管怎么着,作为IOC容器,这些接口你必须要满足应用程序的最基本要求,查看BeanFactory的源码:

public interface BeanFactory {        //这里是对FactoryBean的转义定义,因为如果使用bean的名字检索FactoryBean得到的对象是工厂生成的对象,      //如果需要得到工厂本身,需要转义      String FACTORY_BEAN_PREFIX = "&";          //这里根据bean的名字,在IOC容器中得到bean实例,这个IOC容器就是一个大的抽象工厂。      Object getBean(String name) throws BeansException;        //这里根据bean的名字和Class类型来得到bean实例,和上面的方法不同在于它会抛出异常:如果根据名字取得的bean实例的Class类型和需要的不同的话。      Object getBean(String name, Class requiredType) throws BeansException;        //这里提供对bean的检索,看看是否在IOC容器有这个名字的bean      boolean containsBean(String name);        //这里根据bean名字得到bean实例,并同时判断这个bean是不是单件      boolean isSingleton(String name) throws NoSuchBeanDefinitionException;        //这里对得到bean实例的Class类型      Class getType(String name) throws NoSuchBeanDefinitionException;        //这里得到bean的别名,如果根据别名检索,那么其原名也会被检索出来      String[] getAliases(String name);    }  

2、容器加载流程解析: 这里我们以ClassPathXmlApplicationContext的初始化为例 (1)首先从容器构造函数入口:

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)              throws BeansException {            super(parent);          setConfigLocations(configLocations);          if (refresh) {  //这里是IoC容器的初始化过程,其初始化过程的大致步骤由AbstractApplicationContext来定义              refresh();          }      }

(2)再看AbstractApplicationContext中refresh函数定义,这个方法包含了整个BeanFactory初始化的过程。这里使用到模板模式。

@Override      public void refresh() throws BeansException, IllegalStateException {          synchronized (this.startupShutdownMonitor) {              // 准备这个上下文来刷新              prepareRefresh();                // 告诉子类刷新其beanFactory              ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();                // 准备将要在上下文中使用的bean工厂              prepareBeanFactory(beanFactory);                try {                  // 允许在上下文子类中对bean工厂进行后处理                  postProcessBeanFactory(beanFactory);                    // 调用 factory processors注册为上下文中的bean                  invokeBeanFactoryPostProcessors(beanFactory);                    // 注册 拦截bean创建的bean处理器                  registerBeanPostProcessors(beanFactory);                    // 初始化此上下文的消息源                  initMessageSource();                    // 初始化此上下文的时间多播器                  initApplicationEventMulticaster();                    // 在特殊上下文子类中初始化其特殊的bean                  onRefresh();                    // 检查监听器bean,并且注册它们                  registerListeners();                    // 初始化所有剩下的(非懒加载)单例                  finishBeanFactoryInitialization(beanFactory);                    // 发布相应的事件                  finishRefresh();              }                catch (BeansException ex) {                  if (logger.isWarnEnabled()) {                      logger.warn("Exception encountered during context initialization - " +                              "cancelling refresh attempt: " + ex);                  }                    // 销毁已经创建的单例,以避免资源泄漏                  destroyBeans();                    // 重置active标志位                  cancelRefresh(ex);                    // 抛出异常给调用者                  throw ex;              }                finally {                  // 在Spring的核心中重置常见的自检缓存,因为我们可能不再需要单例对象的元数据了                  resetCommonCaches();              }          }      }

(4)进入obtainFreshBeanFactory()函数,发现调用refreshBeanFactory(),而refreshBeanFactory()里面调用了loadBeanDefinitions()函数,该函数描述了加载bean定义的过程,最终会调用”具体的解析和注册过程“。

@Override      protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {          // 为给定的BeanFactory创建一个新的XmlBeanDefinitionReader          XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);            // 使用此上下文的资源加载环境,去配置bean定义阅读器。          beanDefinitionReader.setEnvironment(this.getEnvironment());          beanDefinitionReader.setResourceLoader(this);          beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));            // 允许子类提供reader的自定义初始化,然后执行实际加载bean定义。          initBeanDefinitionReader(beanDefinitionReader);          loadBeanDefinitions(beanDefinitionReader);      }    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {          Resource[] configResources = getConfigResources();          if (configResources != null) {              // 调用XmlBeanDefinitionReader来载入bean定义信息。              reader.loadBeanDefinitions(configResources);          }          String[] configLocations = getConfigLocations();          if (configLocations != null) {              reader.loadBeanDefinitions(configLocations);          }  }    // XmlBeanDefinitionReader.java  public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {          Assert.notNull(encodedResource, "EncodedResource must not be null");          if (logger.isInfoEnabled()) {              logger.info("Loading XML bean definitions from " + encodedResource.getResource());          }            Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();          if (currentResources == null) {              currentResources = new HashSet<EncodedResource>(4);              this.resourcesCurrentlyBeingLoaded.set(currentResources);          }          if (!currentResources.add(encodedResource)) {              throw new BeanDefinitionStoreException(                      "Detected cyclic loading of " + encodedResource + " - check your import definitions!");          }          try {              InputStream inputStream = encodedResource.getResource().getInputStream();              try {                  InputSource inputSource = new InputSource(inputStream);                  if (encodedResource.getEncoding() != null) {                      inputSource.setEncoding(encodedResource.getEncoding());                  }                  //这里是具体的解析和注册过程                  return doLoadBeanDefinitions(inputSource, encodedResource.getResource());              }              finally {                  inputStream.close();              }          }          catch (IOException ex) {              throw new BeanDefinitionStoreException(                      "IOException parsing XML document from " + encodedResource.getResource(), ex);          }          finally {              currentResources.remove(encodedResource);              if (currentResources.isEmpty()) {                  this.resourcesCurrentlyBeingLoaded.remove();              }          }      }      protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)              throws BeanDefinitionStoreException {          try {              //通过解析得到DOM,然后完成bean在IOC容器中的注册              Document doc = doLoadDocument(inputSource, resource);              return registerBeanDefinitions(doc, resource);          }      ....

我们看到先把定义文件解析为DOM对象,然后进行具体的注册过程:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {  // 具体的注册过程,首先得到XmlBeanDefinitionReader,来处理xml的bean定义文件          BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();          int countBefore = getRegistry().getBeanDefinitionCount();          documentReader.registerBeanDefinitions(doc, createReaderContext(resource));          return getRegistry().getBeanDefinitionCount() - countBefore;      }

(5)在BeanDefinitionDocumentReader中完成bean定义文件的解析和IOC容器bean的初始化。

public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {          this.readerContext = readerContext;          logger.debug("Loading bean definitions");          Element root = doc.getDocumentElement();          doRegisterBeanDefinitions(root);  }    protected void doRegisterBeanDefinitions(Element root)          BeanDefinitionParserDelegate parent = this.delegate;          this.delegate = createDelegate(getReaderContext(), root, parent);            if (this.delegate.isDefaultNamespace(root)) {              String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);              if (StringUtils.hasText(profileSpec)) {                  String[] specifiedProfiles = StringUtils.tokenizeToStringArray(                          profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);                  if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {                      return;                  }              }          }            preProcessXml(root);          parseBeanDefinitions(root, this.delegate);          postProcessXml(root);            this.delegate = parent;  }    // 对配置文件(xml文件)进行解析  protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {          if (delegate.isDefaultNamespace(root)) {              NodeList nl = root.getChildNodes();              for (int i = 0; i < nl.getLength(); i++) {                  Node node = nl.item(i);                  if (node instanceof Element) {                      Element ele = (Element) node;                      if (delegate.isDefaultNamespace(ele)) {                      //这里是解析过程的调用,对缺省的元素进行分析比如bean元素                          parseDefaultElement(ele, delegate);                      }                      else {                          delegate.parseCustomElement(ele);                      }                  }              }          }          else {              delegate.parseCustomElement(root);          }  }    private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {          if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {              importBeanDefinitionResource(ele);          }          else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {              processAliasRegistration(ele);          }          else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {              //这里对我们最熟悉的bean元素进行处理              processBeanDefinition(ele, delegate);          }          else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {              // recurse              doRegisterBeanDefinitions(ele);          }      }    protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {  //委托给BeanDefinitionParserDelegate来完成对bean元素的处理,这个类包含了具体的bean解析的过程。          // 把解析bean文件得到的信息放到BeanDefinition里,他是bean信息的主要载体,也是IOC容器的管理对象。          BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);          if (bdHolder != null) {              bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);              try {                  // 这里是向IOC容器注册,实际上是放到IOC容器的一个map里                  BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());              }              catch (BeanDefinitionStoreException ex) {                  getReaderContext().error("Failed to register bean definition with name '" +                          bdHolder.getBeanName() + "'", ele, ex);              }              // 这里向IOC容器发送事件,表示解析和注册完成              getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));          }      }

(6)我们看到在parseBeanDefinition中对具体bean元素的解析式交给BeanDefinitionParserDelegate来完成的,下面我们看看解析完的bean是怎样在IOC容器中注册的: 在BeanDefinitionReaderUtils调用的是:

public static void registerBeanDefinition(              BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)              throws BeanDefinitionStoreException {            // Register bean definition under primary name.          String beanName = definitionHolder.getBeanName();          //这是调用IOC来注册的bean的过程,需要得到BeanDefinition          registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());            // 别名也是可以通过IOC容器和bean联系起来的进行注册          String[] aliases = definitionHolder.getAliases();          if (aliases != null) {              for (String alias : aliases) {                  registry.registerAlias(beanName, alias);              }          }  }

(7)我们看看XmlBeanFactory中的注册实现:

//---------------------------------------------------------------------  // 这里是IOC容器对BeanDefinitionRegistry接口的实现  //---------------------------------------------------------------------    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)          throws BeanDefinitionStoreException {        .....//这里省略了对BeanDefinition的验证过程      //先看看在容器里是不是已经有了同名的bean,如果有抛出异常。      Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);      if (oldBeanDefinition != null) {          if (!this.allowBeanDefinitionOverriding) {          ...........      }      else {          //把bean的名字加到IOC容器中去          this.beanDefinitionNames.add(beanName);      }      //这里把bean的名字和Bean定义联系起来放到一个HashMap中去,IOC容器通过这个Map来维护容器里的Bean定义信息。      this.beanDefinitionMap.put(beanName, beanDefinition);      removeSingleton(beanName);  } 

这样就完成了Bean定义在IOC容器中的注册,就可被IOC容器进行管理和使用了。

总结IOC容器初始化流程:

1、初始化的入口在容器实现中的refresh()调用来完成 2、将bean定义信息载入IOC容器。使用的方法是loadBeanDefinition,其中的大致过程如下: (1)通过ResourceLoader来完成资源文件位置的定位,DefaultResourceLoader是默认的实现,同时上下文本身就给出了ResourceLoader的实现,可以从类路径,文件系统, URL等方式来定为资源位置。如果是XmlBeanFactory作为IOC容器,那么需要为它指定bean定义的资源,也就是说bean定义文件时通过抽象成Resource来被IOC容器处理的 (2)容器通过BeanDefinitionReader来完成定义信息的解析和Bean信息的注册,往往使用的是XmlBeanDefinitionReader来解析bean的xml定义文件 – 实际的处理过程是委托给BeanDefinitionParserDelegate来完成的,从而得到bean的定义信息,这些信息在Spring中使用BeanDefinition对象来表示 – 这个名字可以让我们想到loadBeanDefinition,RegisterBeanDefinition这些相关的方法 – 他们都是为处理BeanDefinitin服务的,IoC容器解析得到BeanDefinition以后,需要把它在IOC容器中注册,这由IOC实现 BeanDefinitionRegistry接口来实现。注册过程就是在IOC容器内部维护的一个HashMap来保存得到的 BeanDefinition的过程。这个HashMap是IoC容器持有bean信息的场所,以后对bean的操作都是围绕这个HashMap来实现的。 (3)然后我们就可以通过BeanFactory和ApplicationContext来享受到Spring IOC的服务了.

Beanfactory 和Factory bean

1、BeanFactory 指的是IOC容器的编程抽象,比如ApplicationContext, XmlBeanFactory等,Bean工厂, 保存了所有的Bean并管理它们的生命周期和依赖关系。 2、Factory bean 是一个可以在IOC容器中被管理的一个bean,是对各种处理过程和资源使用的抽象,Factory bean在需要时产生另一个对象,而不返回FactoryBean本省,我们可以把它看成是一个抽象工厂,对它的调用返回的是工厂生产的产品。所有的 Factory bean都实现特殊的org.springframework.beans.factory.FactoryBean接口,当使用容器中factory bean的时候,该容器不会返回factory bean本身,而是返回其生成的对象。Spring包括了大部分的通用资源和服务访问抽象的Factory bean的实现,其中包括: 对JNDI查询的处理,对代理对象的处理,对事务性代理的处理,对RMI代理的处理等,这些我们都可以看成是具体的工厂,看成是SPRING为我们建立好的工厂。也就是说Spring通过使用抽象工厂模式为我们准备了一系列工厂来生产一些特定的对象,免除我们手工重复的工作,我们要使用时只需要在IOC容器里配置好就能很方便的使用了。

IOC高级特性

一、lazy-init延迟加载 1、执行原理: lazy-init属性:为true的话,在Ioc容器初始化过程中,会对BeanDefinitionMap中所有的Bean进行依赖注入,这样在初始化过程结束后,容器执行getBean得到的就是已经准备好的Bean,不需要进行依赖注入。 2、优点:当应用第一次向容器索取所需的Bean时,容器不再需要对Bean进行初始化,直接从已经完成实例化的Bean中取出需要的bean,这样就提高了第一次获取Bean的性能。

二、BeanPostProcessor后置处理器: 1、BeanPostProcessor后置处理器是Spring IoC容器经常使用到的一个特性,这个Bean后置处理器是一个监听器,可以监听容器触发的Bean声明周期事件。后置处理器想容器注册以后,容器中管理的Bean就具备了接收IoC容器事件回调的能力。 2、BeanPostProcessor的使用非常简单,只需要提供一个实现接口BeanPostProcessor的实现类,然后在Bean的配置文件中设置即可。 3、API解释:

public interface BeanPostProcessor {        /**       * Apply this BeanPostProcessor to the given new bean instance <i>before</i> any bean       * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}       * or a custom init-method). The bean will already be populated with property values.       */      //实例化、依赖注入完毕,在调用显示的初始化之前完成一些定制的初始化任务      Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;          /**       * Apply this BeanPostProcessor to the given new bean instance <i>after</i> any bean       * initialization callbacks (like InitializingBean's {@code afterPropertiesSet}       * or a custom init-method). The bean will already be populated with property values.       */      //实例化、依赖注入、初始化完毕时执行      Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;    }

三、自动装配: 1、概念:无须在Spring配置文件中描述javaBean之间的依赖关系(如配置<property>、<constructor-arg>)。IOC容器会自动建立java Bean之间的关联关系(通过autowire)。 2、在Spring中,支持 5 自动装配模式。 (1)no – 缺省情况下,自动配置是通过“ref”属性手动设定 (2)byName – 根据属性名称自动装配。如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 (3)byType – 按数据类型自动装配。如果一个bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。 (4)constructor – 在构造函数参数的byType方式。

参考: http://www.iteye.com/topic/86339 http://blessht.iteye.com/blog/1162131 https://blog.csdn.net/mastermind/article/details/1932787 https://blog.csdn.net/sugar_rainbow/article/details/76757383