飞道的博客

Spring入门学习一——控制反转(IoC)和依赖注入(DI)

456人阅读  评论(0)

一、认识Spring

1.Spring是什么

Spring 是分层的 Java SE/EE应用 full-stack 轻量级开源框架,以 IoC (Inverse Of Control:
反转控制〉和AOP (Aspect Oriented Programming:面向切面编程〉为内核,提供了展现层Spring MVC和持久层Spring JDBC以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多 著名的第三方框架和类库。

Spring之父Rod Johnson是轮子理论的推崇者,Spring框架的宗旨:不重新发明技术,让原有的技术使用起来更加方便.

2.Spring的发展历程

  • 1997年IBM提出了 EJB的思想
  • 1998年,SUN制定开发标准规范EJB1. 0
  • 1999 年,EJB1. 1 发布
  • 2001 年,EJB2. 0 发布
  • 2003 年,EJB2. 1 发布
  • 2006 年,EJB3. 0 发布
  • Rod Johnson (spring 之父)Expert One-to-One J2EE Design and Development(2002)
    阐述了 J2EE使用EJB开发设计的优点及解决方案
  • Expert One-to-One J2EE Development without EJB(2004)
    阐述了 J2EE开发不使用EJB的解决方式(Spring雏形)
  • 2017年9月份发布了 spring的最新版本sprinq5. 0通用版(GA)

3.Spring的优势

方便解耦,简化开发

通过Spring提供的IoC容器,可以将对象间的依赖关系交由Spring进行控制,避免硬编码所造 成的过度程序耦合。用户也不必再为单例模式类、属性文件解析等这些很底层的需求编写代码,可 以更专注于上层的应用。

AOP编程的支持

通过Spring的AOP功能,方便进行面向切面的编程,许多不容易用传统OOP实现的功能可以通过AOP轻松应对。

声明式事务的支持

可以将我们从单调烦闷的事务管理代码中解脱出来,通过声明式方式灵活的进行事务的管理, 提高开发效率和质量。

方便程序的测试

可以用非容器依赖的编程方式进行几乎所有的测试工作,测试不再是昂贵的工作,而是随手可做的事情。

方便集成各种优秀框架

Spring可以降低各种框架的使用难度,提供了对各种优秀框架(Struts、Hibernate、Hessian、 Quartz 等)的直接支持。

降低JavaEE API的使用难度

Spring对: FavaEEAPI (如JDBC、JavaMail、远程调用等)进行了薄薄的封装层,使这些API的使用难度大为降低 。

Java源码是经典学习范例

Spring的源代码设计稍妙、结构淸晰、匠心独用,处处体现着大师对Java设计模式灵活运用,以及对Java技术的高深造诣。它的源代码无意是Java技术的最佳实践的范例。

二、SpringIoC的引入

1.当前程序中存在的耦合问题

耦合

程序间的的依赖关系,包括类之间的依赖,方法之间的依赖。而在开发中应该解决编译期的依赖,即做到编译期不依赖,运行时才依赖

解耦思路
  • 1.使用反射来创建对象,避免使用new关键字
  • 2.通过读取配置文件的方式来获取创建对象的全类名
解耦实例

1.JDBC的驱动注册

 public static void main(String[] args) throws SQLException, ClassNotFoundException {
        //1.通过new驱动类
        DriverManager.registerDriver(new com.mysql.jdbc.Driver());
        //2.目前我们所采用的反射加载驱动类
        Class.forName("com.mysql.jdbc.Driver");
    }

当我们的依赖库中没有mysql-connector-java.jar时,编译器会提示第一种方式报错。

2.简单工厂模式获取实体类对象

假设我们需要创建Customer和Producer两个实例对象,我们先在配置文件中配置好它们的全限定类名

Custumer=com.xxbb.pojo.Customer
Producer=com.xxbb.pojo.Producer

创建一个静态工厂读取properties文件,在静态代码块中通过加载实体类,通过getBean方法传入参数获取对应实例,同时实现了实体类的单例

package com.xxbb.test;

import com.xxbb.pojo.Customer;

import java.io.InputStream;
import java.util.*;

/**
 * @author xxbb
 */
public class SimpleFactory {
    /**
     * 定义一个properties对象
     */
    private static Properties pro;

    /**
     * 定义一个Map,用来存放我们要创建的对象,我们把他称之为容器
     */
    private static Map<String,Object> beans;
    //使用静态代码块加载Properties对象并加载实体类
    static{
        try {
            pro=new Properties();
            //获取文件流对象
            InputStream is=SimpleFactory.class.getClassLoader().getResourceAsStream("po.properties");
            pro.load(is);
            //实例化容器
            beans=new HashMap<>();
            //取出配置文件中所有的key值
            Enumeration keys=pro.keys();
            while(keys.hasMoreElements()){
                //取出每个key
                String key=keys.nextElement().toString();
                //根据key值获取value
                String beanPath=pro.getProperty(key);
                //反射创建对象
                Object  value=Class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }

        }catch (Exception e){
            throw new RuntimeException("加载对象工厂失败!!!"+e.getMessage());
        }
    }
    public static Object getBean(String key){
        return beans.get(key);
    }

    public static void main(String[] args) {
        Customer c= (Customer) SimpleFactory.getBean("Customer");
        System.out.println(c);

    }

}

控制台:

通过静态工厂我们实现了不直接操作创建实体类。在静态工厂中通过读取配置文件来加载实体类,通过Class.forName(“beanPath”).newInstance()来创建实体类对象,再存入静态的map中。实体类只在工厂类加载时创建一次,实现了单例效果。降低了操作实体类的其他类与实体类之间的耦合度。

2.耦合问题的解决方案,SpringIoC的引入

IoC概念

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。能够降低代码之间的耦合

Spring框架为我们提供了一套完善的IoC方案

SpringIoC的配置

在maven项目的pom.xml中添加依赖,或去maven仓库下载

 <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency> 
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>
SpringIoC实现静态工厂功能

1.在resours目录下创建一个bean.xml,访问Spring官网的Spring Framework帮助文档设置配置xml。帮助文档地址:点击这里

配置好和之前properties文件相同的属性

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- 把对象创建交给Spring来管理-->
    <bean id="customer" class="com.xxbb.pojo.Customer"/>
    <bean id="producer" class="com.xxbb.pojo.Producer"/>


</beans>

2.创建Test类测试SpringIoC的功能,这里在初次使用遇到一个坑,我在pom.xml中只配置了spring-core的坐标,在使用ApplicationContext类时提示类未找到,于是我又去maven仓库中将spring-context、spring-beans的坐标都添加上去了,添加了spring-context坐标后会自动下载spring-expression依赖。

package com.xxbb.test;

import com.xxbb.pojo.Customer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @author xxbb
 */
public class IOCTest {
    /**
     * 获取Spring的IOC容器,并根据id获取对象
     * @param args 参数
     */
    public static void main(String[] args) {
        //1.获取核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        Customer c1= (Customer) ac.getBean("customer");
        Customer c2=ac.getBean("customer",Customer.class);
        System.out.println(c1);
        System.out.println(c2);
    }
}

控制台:

可见 ApplicationContext默认是以单例模式创建对象

3.查看接口的实现类

在要查看的对象上右键

我们看到

再在该接口上右键

可以看到

图中红色方框所画的即为ApplicationContext常用的三个接口实现类:

  • AnnotationConfigApplicationContext——读取注解创建容器

  • ClassPathXmlApplicationContext——加载类路径下的配置文件

  • FileSystemXmlApplicationContext——加载磁盘任意位置的配置文件(必须有访问权限)

4.ApplicationContext和BeanFactory的区别

共同点:

都能够用于读取配置文件创建对象

区别:

  • ApplicationContext

它在构建核心容器时,创建对象采取的策略是采用立即加载的方式。也就是说,只要一读取完配置文件马上就创建配置文件中SE配置的对象。适用场景:单例对象,在开发中多采用此接口。

  • BeanFactory

它在构建核心容器时,创建对象釆取的策略是釆用延迟加载的方式。也就是说,什么时候根据id获取对象了,什么时候才真正的创建对象。适用场景:多例对象。作为顶层接口,功能相较于ApplicationContext而言更不完善。

3.bean对象的管理细节

创建bean对象的三种方式
  • 默认构造函数创建对象

在spring的配置文件中使用bean标签, 配以id和class属性之后, 且没有其他属性和标签时。采用的就是默认构造函数创建bean对象, 此时如果类重写了构造方法而没有添加空的构造方法, 则对象无法直接创建。需要在bean标签内给构造方法的参数赋值,即后面会提到的构造函数注入。

<bean id="producer" class="com.xxbb.pojo.Producer">
    <!--构造函数注入 -->
    <constructor-arg type="java.lang.String" name="name" value="tools"/>
    <constructor-arg name="id" value="123"/>

   </bean>
  • 普通工厂创建对象

    模拟在jar包中的工厂类中,对于该工厂生产的类,我们无法直接调用它的默认构造方法来创建该类对象,只能通过它的工厂来获取

    ​ 假设普通工厂

    public class InstanceFactory {
    
        public UserServiceImpl getUserService(){
            return new UserServiceImpl();
        }
    }
    
    

    ​ bean.xml中配置

     <bean id="instanceFactory" class="com.xxbb.service.InstanceFactory"/>
        <bean id="userService" factory-bean="instanceFactory" factory-method="getUserService"/>
    

    ​ 可以理解为

    InstanceFactory factory=new InstanceFactory();
    UserService userService=factory.getUserService();
    
  • 静态工厂创建对象

​ 假设静态工厂

public class StaticFactory {


    public static UserServiceImpl getUserService(){
        return new UserServiceImpl();
    }
}

​ bean.xml中配置

<bean id="userServices" class="com.xxbb.service.StaticFactory" factory-method="getUserService"/>

​ 可以理解为

UserService userServices=StaticFactory.getUserService();
bean对象的作用范围

bean标签的scope属性,用于指定bean的作用范围,常用为指定单例或者多例

取值:

  • singleton:单例的(默认值)
  • prototype:多例的
  • request:作用于web应用的请求范围
  • session:作用于web应用的会话范围
  • global-session:作用于集群环境的会话范围(全局会话范围) ,当不是集群环境时,它就是session
bean对象的生命周期
  • 单例对象

    出生:当容器创建时对象出生

    存活:只要容器还在,对象一直活着

    死亡:容器销毁,对象消亡

    总结:单例对象的生命周期和容器相同

  • 多例对象

    出生:当我们使用对象时spring框架为我们创建

    活着:对象只要是在使用过程中就一直活着。

    死亡:当对象长时间不用, 且没有别的对象引用时, 由Java的垃圾回收器回收

注:当我们创建初始化方法和销毁方法观察单例对象的生命周期时,当在一个简单的main方法里测试,可能只能看到对象的初始化方法被调用。那是因为main方法执行完后立刻释放资源,而单例对像还没来得及调用销毁方法就被释放掉了。

三、依赖注入

DI(Dependency Injection,依赖注入)用于降低程序间的耦合。当我们在一个类对象中需要调用其他类对象或数据时,由Spring为我们提供,我们只需要在配置文件中说明。依赖关系的维护就称之为依赖注入。

能够依赖注入的数据有三类:1.基本数据类型和String 2.配置文件中的声明过的bean类型 3.集合类型

依赖注入有以下三种方式:

1.构造函数注入

当我们的声明的bean对象存在有参构造方法时,进行如下配置可将参数注入并实例化对象。

假设目标对象

public class UserServiceImpl implements UserService {
    private String username;
    private UserDao userDao;
    public UserServiceImpl(String username,UserDao userDao){
        this.username=username;
        this.userDao=userDao;
    }
    @Override
    public void test() {
        System.out.println("UserService中的test方法执行了");
    }
}

bean.xml配置

    <bean id="userDao" class="com.xxbb.dao.UserDaoImpl"/>
    <bean id="userService" class="com.xxbb.service.impl.UserServiceImpl">
        <constructor-arg name="username" value="xxbb"/>
        <constructor-arg name="userDao" ref="userDao"/>
        
    </bean>

使用的标签:< constructor-arg >
标签出现的位置:bean标签的内部
标签中的属性:

  • type:用于指定要注入的数据的数据类型, 该数据类型也是构造函数中某个或某些参数的类型,当构造函数中该参数的数据类型是唯一时可以使。
  • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
  • name:用于指定給构造函数中指定名称的参数赋值,如上方示例中的构造方法中的传入参数名为username和userDao,则在标签属性中填入对应的参数名则可将数据注入,一般都是使用该方式注入数据。
  • value:用于提供基本数据类型和String类型的数据的注入
  • ref:用于指定其他的bean类型数据。

优势:解决了bean对象创建是默认使用无参构造方法而无法通过有参构造器初始化对象的问题

劣势:改变了bean对象的实例化方式, 使我们在创建对象时, 如果用不到这些数据,也必须提供。

2.使用set方法注入

涉及的标签:< property >
出现的位置:bean标签的内部
标签的属性:

  • name:用于指定注入时所调用的set方法名称

  • value:用于提供基本类型和String类型的数据

  • ref:用于指定其他的bean类型数据。它指的就是在spring的IoC核心容器中出声明过的bean对象

前提:属性有set方法

bean.xml配置

<!-- 导入Src目录下的druid.properties文件 -->
<context:property-placeholder location="druid.properties"/>
<bean id="druid" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbcUrl}"/>
        <property name="username" value="${user}"/>
        <property name="password" value="${password}"/>
        <property name="initialSize" value="${initialSize}"/>
        <property name="maxActive" value="${maxActive}"/>
        <property name="minIdle" value="${minIdle}"/>
        <property name="maxWait" value="${maxWait}"/>
</bean>

在xml的配置文件中也可以使用EL表达式,他会在Spring的内存空间中去寻找需要的值

集合类型注入:

前提也是类中有对应的set方法

bean.xml配置

<bean id="customer" class="com.xxbb.pojo.Customer">
        <property name="arrayValue">
            <array>第一个值</array>
            <array>第二个值</array>
            <array>第三个值</array>
        </property>
        <property name="listValue">
            <list>第一个值</list>
            <list>第二个值</list>
            <list>第三个值</list>
        </property>
        <property name="setValue">
            <set>第一个值</set>
            <set>第二个值</set>
            <set>第三个值</set>
        </property>
 </bean>

< array>、< list >、< set >用于对list结构集合类型注入数据, < map >、< props >用于map类结构注入数据

bean.xml配置

    <bean id="customer" class="com.xxbb.pojo.Customer">
        <property name="mapValue">
            <map>
                <entry name="first" value="第一个值"/>
                <entry name="second">
                    <value>第二个值</value>
                </entry>
            </map>
        </property>
        <property name="propertiesValue">
            <props>
                <prop key="first">第一个值</prop>
            </props>
        </property>
    </bean>

3.注解注入

首先去官方文档查询xmlns:context,将内容复制到bean.xml中,并告诉Spring要去哪些包下找注解

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
     <!-- 设定Spring 去哪些包中找Annotation -->
    <context:component-scan base-package="com.xxbb"/>
</beans>
创建对象的注解

@Component:对应< bean > 标签,相当于把当前类对象存入Spring容器中

@Controller@Service@Repository 三者的功能和@Component相同,是spring框架为我们提供明确的三层使用的注解, 使我们的三层对象更加清晰

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    
}
注入数据的注解
  • autoWrite

自动按照类型注入。只要容器中有唯一的一个bean对象类型和要注入的变量类型匹配, 就可以注入成功,假设容器中有多个该类型对象这会再根据该对象的名称去匹配bean的id,若没有相匹配的id则会报错。

    @Autowired
    private QueryRunner runner=null;

Spring会去IoC容器中寻找QveryRunner类型的对象,如果容器中有多个该类型对象则根据其名称runner取匹配bean的id。该注解可以出现在变量上,也可以出现在方法上。

当使用@Autowired时,可以不设置和该对象对应的set方法

  • Qualifier

在按照类中注入的基础之上再按照名称注入。它在给类成员注入时不能单独使用。但是在给方法上使用时可以。

 	@Autowired
    @Qualifier("runner")
    private QueryRunner runnersss=null;

pring会去IoC容器中寻找id为runner的bean对象

  • Resource

直接按照bean的id注入。它可以独立使用

	@Resource(name = "runner")
    private QueryRunner runner=null;
  • Value

给基本数据类型和String注入数据

    @Value("account")
    private String name=null;
改变作用范围

@Scope和bean标签中scope用法相同,常用取值:singleton、prototype

@Scope(value = "singleton")
public class AccountDaoImpl implements AccountDao{}
生命周期相关

@PreDestroy:用于指定初始化方法

@PostConstruct:用于指定销毁方法

 @PostConstruct
    public void init(){
        
    }
    @PreDestroy
    public void destory(){
        
    }

四、全注解配置

1.配置类代替bean.xml

涉及的注解:

@Configuration:指定该类为配置类
@ComponentScan(basePackages = “com”):指定需要Spring扫描其中注解的包名
@PropertySource(“classpath:druid.properties”):导入配置文件。classpath表示在当前项目路径下寻找配置文件,如果不加则会在当前类所在文件夹下寻找配置文件
@Import(JdbcConfiguration.class):导入其他配置类,被导入的配置类上可以不写@Configuration注解
@Component(""):声明自己所创建的bean对象
@Bean(name=""):写在配置类内部,用于把当前方法的返回值作为bean对象存入SpringIoc容器中。如果方法有参数,Spring框架回去容器中查找是否有可用的bean对象,查找方式与@Autowired相同,按其传入值类型查找。

示例:

以SpringConfiguration类为配置类,导入JdbcConfiguration配置类。

在SpringConfiguration配置类中声明了一个dbutils工具类下的QueryRunner对象,对象的传入值由JdbcConfiguration配置类中的bean对象提供

在JdbcConfiguration配置类中使用@PropertySource导入配置文件,通过@Value给成员变量赋值,使用EL表达式直接获取配置文件的值。这里要注意配置文件内属性的key不要与Spring自身的key重复,比如这里的username属性,我使用的EL标签是$ {user},如果使用$ {username<}Spring会去获取他自己的username而非导入properties文件中的同名属性。

在使用时创建Spring容器采用如下方式:

 ApplicationContext ac=new AnnotationConfigApplicationContext(SpringConfiguration.class);

该实现类的构造方法的参数可以传入多个class对象,传入class对象的类上可以不添加@Configuration注解。

package com.xxbb.config;

import org.apache.commons.dbutils.QueryRunner;
import org.springframework.context.annotation.*;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;


/**
 * 配置类 它的作用和bean.xml一样,指定当前类为一个配置类
 * @author xxbb
 * 注解:
 *      Configuration 指定为配置类
 *      ComponentScan 指定扫描包
 *      Bean 用于把当前方法的返回值作为bean对象存入SpringIoc容器中
 *          属性: name,用于指定bean的id,不写时默认使用方法名
 *          当使用注解配置方法是,如果方法有参数,Spring框架回去容器中查找是否有可用的bean对象
 *          查找方式和Autowired相同
 */
@ComponentScan(basePackages = "com.xxbb")
@Import(JdbcConfiguration.class)
@Component("SpringConfiguration")
@Configuration
public class SpringConfiguration {
    @Bean(name="runners")
    public QueryRunner createQueryRunner(DataSource dataSource){
        return new QueryRunner(dataSource);
    }
}
package com.xxbb.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.commons.dbutils.QueryRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * @author xxbb
 */
@PropertySource(value = "classpath:druid.properties")
public class JdbcConfiguration {
    @Value("${jdbcUrl}")
    private String url;
    @Value("${user}")
    private String username;
    @Value("${password}")
    private String password;
    @Bean(name="runner")
    @Scope(value = "prototype")
    public QueryRunner createQueryRunner(DataSource dataSource){
        return new QueryRunner(dataSource);
    }
    @Bean(name="dataSource")
    public DataSource createDataSource(){
        DruidDataSource dds=new DruidDataSource();
        dds.setUrl(url);
        dds.setUsername(username);
        dds.setPassword(password);
        return dds;
    }
}

2.整合junit

无论是否使用Spring框架,junit都可以使用。在其测试类中直接使用 @Autowired等标签是不会被junit识别的。需要导入Spring的test包并添加两个注解。示例如下:

package com.xxbb.test;

import com.xxbb.config.SpringConfiguration;
import com.xxbb.dao.AccountDao;
import com.xxbb.dao.UserDao;
import org.apache.commons.dbutils.QueryRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @author xxbb
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
//@ContextConfiguration(classes = SpringConfiguration.class)
public class SpringConfigurationTest {
    @Autowired
    @Qualifier("runners")
    private QueryRunner runner;
    @Autowired
    @Qualifier("accountDao")
    private AccountDao accountDao;
    @Test
    public void ConnectTest(){

        System.out.println(runner);
        System.out.println(accountDao);

    }
}

代码中我们添加了@RunWith(SpringJUnit4ClassRunner.class)将junit原有的测试方案替换成Spring提供的方案。

通过@ContextConfiguration(locations = “classpath:bean.xml”)则指明我们导入的IoC容器配置。该注解有两个常用的参数

  • locations:引入xml类型的配置文件
  • locations:引入配置类类型的配置文件

五、回顾思考

1.SpringIoC解决的问题

2.如何搭建基于xml的开发环境

3.如何实现类之间的依赖和维护


转载:https://blog.csdn.net/weixin_44804750/article/details/105164681
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场