• 单点突破:Spring(上)


    Spring概述

    ​ 我们常说的 Spring 实际上是指 Spring Framework,而 Spring Framework 只是 Spring 家族中的一个分支而已。Spring 是为了解决企业级应用开发的复杂性而创建的。

    ​ 如果我们想实现某个功能,代码量一般都是固定的,要么全自己写,要么用已有的优秀框架,而Spring不仅已经给我们提供了各种优秀组件,还提供了良好的代码组织逻辑跟业务开发流程规范框架,我们主要学习Spring中以下几点:

    • IOC/DI
    • AOP
    • 声明式事务
    • JdbcTemplate

    Spirng组件

    ​ Spring框架具有很多组件,大约有20多个模块 。我们要记住以下7个核心组件和它的含义。

    1. Spring Core:Spring核心,它是框架最基础的部分,提供IOC和依赖注入DI特性
    2. Spring Context:Spring上下文容器,它是 BeanFactory 功能加强的一个子接口
    3. Spring Web:它提供Web应用开发的支持。
    4. Spring MVC:它针对Web应用中MVC思想的实现。
    5. Spring DAO:提供对JDBC抽象层,简化了JDBC编码,同时,编码更具有健壮性。
    6. Spring ORM:它支持用于流行的ORM框架的整合,比如:Spring + Hibernate、Spring + iBatis、Spring + JDO的整合等。
    7. Spring AOP:即面向切面编程,它提供了与AOP联盟兼容的编程实现。

    ​ 下图就是maven导入spring后的组件:

    img

    IOC

    ​ IOC(Inversion of Control),中文叫做控制反转

    ​ Spring提出了一种思想:由Spring来负责控制对象的生命周期和对象间的关系。所有的类都会在Spring容器中登记,告诉Spring这这个类是什么,需要什么,然后Spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的Bean。所有的类的创建、销毁都由Spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是Spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转(Inversion of Controller),也可以叫依赖注入 DI(Dependency Injection)。

    ​ 举个例子:

    public class Book {
        private Integer id;
        private String name;
        private Double price;
    //省略getter/setter
    }
    public class User {
        private Integer id;
        private String name;
        private Integer age;
    
        public void doSth() {
            Book book = new Book();
            book.setId(1);
            book.setName("故事新编");
            book.setPrice((double) 20);
        }
    }
    

    ​ 上面这个例子中,Book对象的控制权在User里面,Book和User高度耦合,如果在其他对象中需要使用 Book 对象,得重新创建,也就是说,对象的创建、初始化、销毁等操作,统统都要开发者自己来完成。

    使用 Spring 之后,我们可以将对象的创建、初始化、销毁等操作交给 Spring 容器来管理。就是说,在项目启动时,所有的 Bean 都将自己注册到 Spring 容器中去(如果有必要的话),然后如果其他 Bean 需要使用到这个 Bean ,则不需要自己去 new,而是直接去 Spring 容器去要。

    什么是Bean

    ​ 我们本篇会一直提到Bean,先在前文给Bean做一个大概的介绍。Spring Bean是被实例的,组装的及被Spring 容器管理的Java对象。Spring 容器会自动完成@bean对象的实例化。创建应用对象之间的协作关系的行为称为:装配(wiring),这就是依赖注入的本质。

    何为控制

    ​ 是 bean 的创建、管理的权利,控制 bean 的整个生命周期。

    何为反转

    ​ 把这个权利交给了 Spring 容器,而不是自己去控制,就是反转。由之前的自己主动创建对象,变成现在被动接收别人给我们的对象的过程,这就是反转。

    何为依赖

    ​ 程序运行需要依赖外部的资源,提供程序内对象的所需要的数据、资源。

    何为注入

    ​ 配置文件把资源从外部注入到内部,容器加载了外部的文件、对象、数据,然后把这些资源注入给程序内的对象,维护了程序内外对象之间的依赖关系。

    实例1:进一步了解IOC

    ​ 我们在IDEA创建一个普通Maven项目,然后再pom文件中引入spring-context 依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>SpringDemo</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.1.9.RELEASE</version>
            </dependency>
        </dependencies>
    
    
    </project>
    

    ​ 然后在 resources 目录下创建一个 spring 的配置文件spring.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.xsd">
    
    </beans>
    

    ​ 文件中我们可以配置bean,把我们的book配置进去

    <?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.xsd">
    
        <bean class="org.javaboy.Book" id="book"/>
    </beans>
    

    说明:class 属性表示需要注册的 bean 的全路径,id 则表示 bean 的唯一标记,也开可以 name 属性作为 bean 的标记,在超过 99% 的情况下,id 和 name 其实是一样的。

    ​ 最后,我们来测试一下,创建一个Main方法来加载这个配置文件,通过 getBean 方法,从容器中去获取对象:

    public class Main {
        public static void main(String[] args) {
            ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
            Book book = (Book) ctx.getBean("book");
            System.out.println(book);
        }
    }
    

    ​ 打印的结果为:Book{id=null, name='null', price=null}

    说明:上面getBean中传入的是我们命名的name或id,这种方式好处是我们给它起了个名,在引入两个或多个相同对象时,有别名不至于混淆。有些教程喜欢在getBean中直接通过Class 去获取一个 Bean,如传入Book.class,如果spring.xml中引入两个book的bean,那么这种做法就会报错,所以我个人不推荐通过Class去获取Bean。

    name与id之间的一些注意点

    ​ 上文中提到了name与id,确实,大部分时候name和id是一样的,也很少在开发中又用name又用id,但是还是要注意一些细节。

    • 配置两个相同的 id 或者 name 都不能通过。
    • 如果既配置了 id ,也配置了 name ,则两个都生效。如果id和name都没有指定,则用类全名作为name,如<bean class="com.stamen.BeanLifeCycleImpl">,则你可以通过getBean("com.stamen.BeanLifeCycleImpl")返回该实例。
    • 如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 相同的时候, 则两个冲突,配置文件生效; 如果配置基本类的时候,注解和配置文件都使用的时候,注解和配置文件中 name 不相同的时候, 则两个不冲突,都能够生效。

    属性注入

    ​ 上一个例子中我们明白了Spirng的IOC可以帮我们管理Bean,将对象的创建、初始化、销毁等操作交给Spring管理,使开发更加方便。此外,上一个例子中我们在spring.xml中配置了Book,最后打印的结果中我们看到Book中的属性并没有值,是null。这一小节就是了解spring是如何注入属性的,有以下几种方式:

    • 构造方法注入
    • set方法注入
    • p命名空间注入
    • 自动装配和@Autowired注解注入
    • 静态工厂注入
    • 实例工厂注入

    构造方法注入

    1. 给Book添加一个有参和无参构造方法:

    2. 在xml中注入bean

      <?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.xsd">
      
          <bean class="com.wms.Demo01.Book" id="book">
              <constructor-arg index="0" value="1"/>
              <constructor-arg index="1" value="宇宙未解之谜"/>
              <constructor-arg index="2" value="39.9"/>
          </bean>
      </beans>
      

      或者使用name标签,更加清晰

      <bean class="com.wms.Demo01.Book" id="book">
          <constructor-arg name="id" value="1"/>
          <constructor-arg name="name" value="宇宙未解之谜"/>
          <constructor-arg name="price" value="39.9"/>
      </bean>
      
    3. 说明:

    • constructor-arg --> 指定构造函数的参数
    • index --> 参数的顺序
    • name --> 参数的名称
    • value --> 给参数赋值
    • 注意,你也可以按index2, 0, 1等顺序来给属性注入值,但记得 index 的值和 value 要一一对应,id就要注入id相符的值。

    set方法注入

    ​ set方法注入方式如下:

    <bean class="com.wms.Demo01.Book" id="book">
        <property name="id" value="1"/>
        <property name="name" value="宇宙未解之谜"/>
        <property name="price" value="39.9"/>
    </bean>
    

    ​ 注意这里使用的是property标签,而且name并不是Book这个对象中定义的属性,而是通过get/set方法分析出来的属性名,只是我们规范get/set方法就是get/set + 属性名。

    p命名空间注入

    ​ p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。

    <bean class="com.wms.Demo01.Book" id="book" p:id="1" p:bookName="宇宙未解之谜" p:price="39.9"></bean>
    

    自动装配和@Autowired注解注入

    ​ 我将这两个放在一起讲,因为要介绍到byName和byTpye的区别。Spring提供了自动装配的功能,简化了我们的配置,自动装配默认是不打开的,常用的方式有两种,byName和byType。方式是在bean标签中加入autowire="byName"autowire="byType"

    <bean class="com.wms.Demo01.Book" id="book" autowire="byType"/>
    

    ​ 后续我们还会接触到@Autowired注解,@Autowired注解可以实现自动装配,只要在对应的属性上标记该注解,但是@Autowired注解只按照byType注入。这个注解常见于Controller层。如下:

    public class UserController {
    
        @Autowired
        private IUserService userService;
    }
    

    ​ 这里我们主要还是熟悉byName和byType的区别。其实byName根据被注入的名称作为bean名称作为依赖查找,并将对象设置到该属性。byType通过属性的类型查找javabean依赖的对象并为其注入。

    byName和byType

    • byName按名称自动装配,当一个bean节点带有 autowire byName的属性时。

      • 将查找其类中所有的set方法名,例如setCat,获得将set去掉并且首字母小写的字符串,即cat。
      • 去spring容器中寻找是否有此字符串名称id的对象。
      • 如果有,就取出注入;如果没有,就报空指针异常。
    • byType按类型自动装配,使用autowire byType首先需要保证:同一类型的对象,在spring容器中唯一。如果不唯一,会报不唯一的异常。

    @Autowired、@Qualifier和@Resource

    ​ 既然讲到了@Autowired,就展开讲一下一些相关的注解。(下面出现的代码来自狂神说的spring教程)

    • @Autowired

      • @Autowired是按类型自动转配的,不支持id匹配。
      • 需要导入 spring-aop的包!
      • @Autowired(required=false) 说明:false,对象可以为null;true,对象必须存对象,不能为null。
    • @Qualifier

      • @Autowired是根据类型自动装配的,加上@Qualifier则可以根据byName的方式自动装配。

      • @Qualifier不能单独使用。

        • 在属性上添加Qualifier注解

        • 示例:

          @Autowired
          @Qualifier(value = "cat2")
          private Cat cat;
          @Autowired
          @Qualifier(value = "dog2")
          private Dog dog;
          
    • @Resource

      • @Resource如有指定的name属性,先按该属性进行byName方式查找装配;
      • 其次再进行默认的byName方式进行装配;
      • 如果以上都不成功,则按byType的方式自动装配。
      • 都不成功,则报异常。

    实体类:

    public class User {
       //如果允许对象为null,设置required = false,默认为true
       @Resource(name = "cat2")
       private Cat cat;
       @Resource
       private Dog dog;
       private String str;
    }
    

    beans.xml

    <bean id="dog" class="com.kuang.pojo.Dog"/>
    <bean id="cat1" class="com.kuang.pojo.Cat"/>
    <bean id="cat2" class="com.kuang.pojo.Cat"/>
    
    <bean id="user" class="com.kuang.pojo.User"/>
    

    测试:结果OK

    配置文件2:beans.xml , 删掉cat2

    <bean id="dog" class="com.kuang.pojo.Dog"/>
    <bean id="cat1" class="com.kuang.pojo.Cat"/>
    

    实体类上只保留注解

    @Resource
    private Cat cat;
    @Resource
    private Dog dog;
    

    结果:OK

    结论:先进行byName查找,失败;再进行byType查找,成功。

    @Autowired与@Resource异同:

    1、@Autowired与@Resource都可以用来装配bean。都可以写在字段上,或写在setter方法上。

    2、@Autowired默认按类型装配(属于spring规范),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用

    3、@Resource(属于J2EE复返),默认按照名称进行装配,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

    静态工厂注入(TODO)

    实例工厂注入(TODO)

    更多属性注入方式

    ​ 上述的属性注入方式是比较基础的注入,实际开发中遇到的问题往往更加复杂,比如在开发过程中可能会注入跟多类型的数据,如:

    • 对象
    • 数组
    • Map

    对象注入

    可以通过 ref 来引用一个对象。

    <bean class="com.wms.Demo01.User" id="user">
        <property name="cat" ref="cat"/>
    </bean>
    <bean class="org.javaboy.Cat" id="cat">
        <property name="name" value="小白"/>
        <property name="color" value="白色"/>
    </bean>
    

    数组注入

    • array

      <bean class="com.wms.Demo01.User" id="user">
          <property name="cat" ref="cat"/>
          <property name="favorites">
              <array>
                  <value>足球</value>
                  <value>篮球</value>
                  <value>乒乓球</value>
              </array>
          </property>
      </bean>
      <bean class="com.wms.Demo01.Cat" id="cat">
          <property name="name" value="小白"/>
          <property name="color" value="白色"/>
      </bean>
      
    • list

      • array 节点,也可以被 list 节点代替。

      • array 或者 list 节点中也可以是对象。

      • 即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。

        <bean class="com.wms.Demo01.User" id="user">
            <property name="cat" ref="cat"/>
            <property name="favorites">
                <list>
                    <value>足球</value>
                    <value>篮球</value>
                    <value>乒乓球</value>
                </list>
            </property>
            <property name="cats">
                <list>
                    <ref bean="cat"/>
                    <ref bean="cat2"/>
                    <bean class="com.wms.Demo01.Cat" id="cat3">
                        <property name="name" value="小花"/>
                        <property name="color" value="花色"/>
                    </bean>
                </list>
            </property>
        </bean>
        <bean class="com.wms.Demo01.Cat" id="cat">
            <property name="name" value="小白"/>
            <property name="color" value="白色"/>
        </bean>
        <bean class="com.wms.Demo01.Cat" id="cat2">
            <property name="name" value="小黑"/>
            <property name="color" value="黑色"/>
        </bean>
        

    Map 注入

    <property name="map">
        <map>
            <entry key="age" value="100"/>
            <entry key="name" value="abc"/>
        </map>
    </property>
    

    Properties 注入

    <property name="info">
        <props>
            <prop key="age">100</prop>
            <prop key="name">abc</prop>
        </props>
    </property>
    

    补充

    • 如果炫技,上面的都可以说。
    • 如果问Spring对象创建方式,要说到构造方法、静态工厂、实例工厂
    • 如果问到Spring注入方式,要说到构造方法、set方法、自动注入、p命名空间

    Context

    IOC 容器只是提供一个管理对象的空间而已,如何向容器中放入我们需要容器代为管理的对象呢?这就涉及到Spring的应用上下文Context

    ​ 工作中通过XML配置或注解 将需要管理的Bean跟Bean之间的协作关系配置好,然后利用应用上下文对象Context加载进Spring容器,容器就能为你的程序提供你想要的对象管理服务了。Spring 框架本身就提供了很多个容器的实现。我们在实例1中的Main中出现了ClassPathXmlApplicationContext,就是一种容器,还有很多容器,如下:

    1. AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式。
    2. ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式。
    3. FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件。
    4. AnnotationConfigWebApplicationContext:专门为web应用准备的,适用于注解方式。
    5. XmlWebApplicationContext:从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式。

    ​ 比如ClassPathXmlApplicationContext,来自于我们常提到的ApplicationContext,但如果你点开源码看,就知道ClassPathXmlApplicationContext并不是直接实现ApplicationContext的,而是一层一层的递进,这是为了IOC全面性而考虑。此处的ApplicationContext也是面试中经常出现的问题,经常与BeanFactory一起作比较。

    ApplicationContext & BeanFactory区别

    BeanFactory接口

    • spring的原始接口,针对原始接口的实现类功能较为单一, 可以理解为 HashMap:它一般只有 get, put 两个功能。
      • Key - bean name
      • Value - bean object
    • BeanFactory接口实现类的容器,特点是每次在获得对象时才会创建对象
    • 优缺点:
      • 优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
      • 缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创建的Bean生命周期会简单一些。

    ApplicationContext接口

    • 每次容器启动时就会创建容器中配置的所有对象
    • 它是 BeanFactory 的子类,更好的补充并实现了 BeanFactory.ApplicationContext 多了很多功能,因为它继承了多个接口。 ApplicationContext 的里面有两个具体的实现子类,用来读取配置配件的,上面列举了5个。
    • 优缺点:
      • 优点:所有的Bean在启动的时候都进行了加载,系统运行的速度快;在系统启动的时候,可以发现系统中的配置问题。
      • 缺点:把费时的操作放到系统启动中完成,所有的对象都可以预加载,缺点就是内存占用较大。

    BeanFactory & FactoryBean的区别

    ​ 既然提到了BeanFactory,面试中往往会用FactoryBean来“坑”面试者,这里就讲一下两者的区别。

    • BeanFactory
      • BeanFactory 以 Factory 结尾,表示它是一个工厂类(接口),BeanFacotry 是 Spring 中比较原始的Factory。
      • BeanFactory 无法支持 Spring 的许多插件,如AOP功能、Web应用等。ApplicationContext 接口由BeanFactory接口派生而来,提供了国际化访问、事件传播等多个功能。
      • BeanFactory 是 IOC 容器的核心,负责生产和管理 Bean 对象。
    • FactoryBean
      • FactoryBean 以 Bean 结尾,表示它是一个Bean。
      • FactoryBean 是工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑。FactoryBean 接口对于 Spring 框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。
      • 当在IOC容器中的Bean实现了 FactoryBean 后,通过getBean(String BeanName)获取到的 Bean 对象并不是 FactoryBean 的实现类对象,而是这个实现类中的 getObject() 方法返回的对象。要想获取FactoryBean的实现类,就要 getBean(String &BeanName),在BeanName之前加上 &

    循环依赖

    ​ 从字面上来理解就是A依赖B的同时B也依赖了A,例如

    img

    自动化配置

    ​ 例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。

    ​ 和 @Service 注解功能类似的注解,一共有四个:

    • @Component

    • @Repository

    • @Service

    • @Controller

      ​ 这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。

    • 在 Service 层上,添加注解时,使用 @Service

    • 在 Dao 层,添加注解时,使用 @Repository

    • 在 Controller 层,添加注解时,使用 @Controller

    • 在其他组件上添加注解时,使用 @Component

      ​ 添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。

    Java 代码配置自动扫描

    ​ 在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类)

    XML 配置自动化扫描

    <context:component-scan base-package="org.javaboy.javaconfig"/>
    

    ​ 上面这行配置表示扫描 org.javaboy.javaconfig 下的所有 Bean。当然也可以按照类来扫描。

    Bean的生命周期和作用域

    ​ 这两个问题也是面试中常客

    Bean的生命周期

    ​ Spring IOC 初始化跟销毁 Bean 的过程大致分为Bean定义、Bean初始化、Bean的生存期 跟 Bean的销毁4个部分。流程图如下:

    imgimg

    浓缩一下:

    Bean的生命周期,从Spring容器的创建开始,到Spring容器销毁结束。

    1. ​ 实例化Bean对象
    2. ​ 装配:填充属性
    3. ​ 回调:(可选,如果实现了Aware系列的接口,则会调用回调函数)
    4. ​ 调用预初始化方法(可选,如果实现了BeanPost-Processor的预初始化方法)
    5. ​ 初始化(init-method)
    6. ​ 调用预初始化后置方法(如果实现了BeanPost-Processor的初始化后方法)
    7. ​ 使用bean
    8. ​ 容器关闭
    9. ​ 如果实现了DisposableBean接口,则调用该方法的destory()方法。
    10. ​ 调用自定义的destory方法。

    Bean的作用域

    ​ 在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?

    ​ 答案是是,因为Spring中Bean默认是单例的,所以多次获取的Bean都是同一个。这里就涉及到Bean的作用域的知识点,

    ​ 四种常见的 Spring Bean 的作用域:

    • singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。

    • prototype : 每次请求都会创建一个新的 bean 实例。

    • request : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。

    • session : 每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

      怎么更改作用域?

      xml中:

      <bean class="org.javaboy.User" id="user" scope="prototype" />
      

      注解:

      @Repository
      @Scope("prototype")
      public class UserDao {
          public String hello() {
              return "userdao";
          }
      }
      
  • 相关阅读:
    团队冲刺第二十三天
    团队冲刺第二十二天
    团队冲刺第二十一天
    团队冲刺第二十天
    第十四周周总结
    团队冲刺第十九天
    团队冲刺第十八天
    团队冲刺第十七天
    团队冲刺第十六天
    keeprunning的使用说明
  • 原文地址:https://www.cnblogs.com/kylinxxx/p/14433103.html
Copyright © 2020-2023  润新知