Spring IoC控制反转(Spring中最重要的话题)

1. Spring IoC简介

IoC:控制反转(Inversion of Control),在传统开发模式下,都是由开发人员自行创建对象,并管理对象(也包括属性赋值的问题),当使用了Spring框架后,这些事情都交给框架来完成(在加载spring配置文件时,创建对象,ac.close的时候销毁),可以理解“将开发人员对对象的管理权交给了框架”,所以,就出现了“管理权”的移交,则称之为“控制反转”。所以控制反转的意思是,开发人员把控制权交给了框架,所以叫做控制翻转。

DI:依赖注入(Dependency Injection),具体表现为通过框架为对象的属性赋值!注入:其实就是一个属性赋值的过程。

IoC与DI的区别:Spring框架通过DI实现了IoC,DI是实现手段,而IoC是希望实现的目标!

关于Spring IoC,其实质是如何通过Spring框架确定某个类的对象的属性值

2. 通过SET方式注入属性的值

假设存在User类,其中有String password属性,希望Spring在创建User类的对象时,还能为password属性注入值(赋值),则先在 src/main/java 下面准备好User类,例如:

(1)注入User类中的password属性值:

首先,在User了中添加两个属性:password,并且生成get和set方法:

package cn.tedu.spring;
public class User {
	private String password;
    
    //然后添加get set方法
	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		System.out.println("User.setPassword()");
		this.password = password;
	}

}

然后,在src/main/resources中的Spring的配置文件中进行配置:

如果一个类要用Spring来管理,那么在中就要写上这个类的名字和包名

只要是使用Set方式为属性注入值,一定是在的子级内去加property节点

<bean id="user" class="cn.tedu.spring.User">
    	在bean的头和尾中间加入一行代码
    <!-- 在bean的内部(子集)写:property节点:用于配置属性的值。翻译:property:属性 -->
		<property name="password" value="123456"/>
    	<!-- property节点:用于配置属性的值 -->
		<!-- name属性:需要配置值的属性名称,实际是set方法对应的名称 -->
		<!-- value属性:需要配置值的属性的值 -->
    
    <!-- 如果标签内部不需要写子集的话,可以这样写:
			<property name="password" value="123456"/> -->
    
   	  	<property name="age" value="23"/>
    
    <!-- Spring做法: -->
	<!-- password 对应的是——>setPassword -->
	<!-- value 对应的是——>setPassword(123456)-->
    
</bean>

然后,在SpringDemo类中:

package cn.tedu.spring;
/**
 * SET方式注入属性值测试
 */
import java.util.Arrays;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringDemo {
	public static void main(String[] args) {
		//1.加载Spring配置文件,获取Spring容器
		ClassPathXmlApplicationContext ac = 
                              new ClassPathXmlApplicationContext("spring.xml");
		
        //2.从Spring容器中获取对象
		User user = ac.getBean("user",User.class);
        //User user = (User) ac.getBean("user");能不转换,就不转换
        
		//3.测试
		System.out.println(user); //cn.tedu.spring.User@727803de
		System.out.println(user.getPassword());//123456
		System.out.println(user.getAge());//23

        //4.关闭,释放资源
		ac.close();

最后,运行时,当获取对象后就可以直接输出属性值,可以看到,该属性已经存在所配置的值!

注意:在<property>属性中,name属性其实是SET方法对应的名称,例如以上配置为name="password",则Spring框架会基于这个配置值,将首字母大写,并在左侧拼上set,得到setPassword,以此为方法名称,结合value属性值,调用了setPassword("000111"),从而使得password属性被赋值!所以,本质是要求name属性的值与类的SET方法是对应的!但是,规范的SET方法名称与Spring组织出SET方法的方式是完全相同的,所以,也可以简单的理解为name属性指的就是属性名称!

建立一个类:SampleBean

package cn.tedu.spring;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
//属性是集合
public class SampleBean {
	//David, Lucy, Tom, Lily, Alex
	private List<String> names;
	
	//9527
	private int[] numbers;
	
	//Beijing, Shanghai, Guangzhou, Shenzhen
	private Set<String> cities;//city复数:cities
	
	// {username=root,password:1234, from:Beijing}
	private Map<String, String> session;
	
	//name:Jack, age:25, gender:Male
	private Properties profile;//property的复数形态,多个属性的意思
	
	//数据来自于jdbc.properties文件
	private Properties jdbc;
	
    //然后生成所有的get、set方法
	public List<String> getNames() {
		return names;
	}

	public void setNames(List<String> names) {
		this.names = names;
	}

	public int[] getNumbers() {
		return numbers;
	}

	public void setNumbers(int[] numbers) {
		this.numbers = numbers;
	}

	public Set<String> getCities() {
		return cities;
	}

	public void setCities(Set<String> cities) {
		this.cities = cities;
	}

	public Map<String, String> getSession() {
		return session;
	}

	public void setSession(Map<String, String> session) {
		this.session = session;
	}

	public Properties getProfile() {
		return profile;
	}

	public void setProfile(Properties profile) {
		this.profile = profile;
	}

	public Properties getJdbc() {
		return jdbc;
	}

	public void setJdbc(Properties jdbc) {
		this.jdbc = jdbc;
	}
	
}

(2)注入List集合类型的值names:

首先,在SampleBean类中:

// David, Lucy, Tom, Lily, Alex
private List<String> names;

然后,在src/main/resources中的Spring的配置文件中进行配置:

当要为一个list集合类型的属性赋值的时候,在property内部加入list节点,在list节点的子集再加value节点

<bean id="sampleBean" class="cn.tedu.spring.SampleBean">
		<property name="names">因为里面的属性名字是names,实际上是set方法是getNames
			<list>
				<value>David</value>
				<value>Lucy</value>
				<value>Tom</value>
				<value>Lily</value>
				<value>Alex</value>
			</list>
		</property>
</bean>

最后,在SpringDemo类的main方法中:

		System.out.println(sampleBean.getNames());//[David, Lucy, Tom, Lily, Alex]
		System.out.println(sampleBean.getNames().get(3));//Lily
		System.out.println(sampleBean.getNames().getClass());//class java.util.ArrayList

注意:

List集合在Spring中是用什么实现类的来实现的?

通过代码:System.out.println(sampleBean.getNames().getClass());测试可知,是class java.util.ArrayList,那么在spring.xml文件中:

		<property name="numbers">
			<list>这里用<array>配置也可以,但不提倡。
				<value>9</value>
				<value>5</value>
				<value>2</value>
				<value>7</value>
			</list>
		</property>

在配置List集合属性的时候,用array和list都是可以的,同理,在配置array的时候,也可以用list,因为ArrayList的本质,在内存中的数据表现就是数组,所以array和list这两个节点可以混为一谈,想怎么用就怎么用,但是不提倡,只是了解即可。

(3)注入数组类型的值numbers:

首先,在SampleBean类中:

// 9, 5, 2, 7
private int[] numbers;

然后,在src/main/resources中的Spring的配置文件中进行配置:

数组对应的是<array>节点

<property name="numbers">
			<array>
				<value>9</value>
				<value>5</value>
				<value>2</value>
				<value>7</value>
			</array>
</property>

最后,在SpringDemo类的main方法中:

		System.out.println(sampleBean.getNumbers());//[I@704921a5
		/**
		 * 注意:
		 * 数组与其他的数据类型不同,并不继承自Object,虽然可以用Object去声明,但是却没有里面的
		 * toString方法
		 * 如果要让数组显示合理,就要用到Arrays的工具类,这个工具类有toString方法,然后把
		 * 数组作为参数放进去
		 * 所以如果输出数组,那么一般用Arrays.toString()方法,如下:
		 */
		System.out.println(Arrays.toString(sampleBean.getNumbers()));//[9, 5, 2, 7]

(4)注入Set集合类型的值cities:

首先,在SampleBean类中:

// Beijing, Shanghai, Guangzhou, Shenzhen
private Set<String> cities;

//一般如果是数组或者集合的变量,变量名一般是用复数形式

然后,在src/main/resources中的Spring的配置文件中进行配置:

set集合类型的属性,对应的就是节点

<property name="cities">
			<set>
				<value>Beijing</value>
				<value>Shanghai</value>
				<value>Guangzhou</value>
				<value>Shenzhen</value>
			</set>
</property>

最后,在SpringDemo类的main方法中:

		//3.测试
		System.out.println(sampleBean.getCities());
		//[Beijing, Shanghai, Guangzhou, Shenzhen]

		/*
		 * 注意:输出的城市顺序是和放入的城市顺序一致的
		 * 原因是:Spring框架默认的用的是LinkedHashSet,要验证的话,用下面验证:
		 */
		System.out.println(sampleBean.getCities().getClass());
		//class java.util.LinkedHashSet

(5)注入Map类型的值session

首先,如果需要注入的是Map类型的数据,在SampleBean类中:

// {username:root, password:1234, from:Beijing}
private Map<String, String> session;//session翻译:阶段;一场;一节;一段时间;

然后,在src/main/resources中的Spring.xml的配置文件中进行配置:

property标签中——map标签中——entry标签

<property name="session">
			<map>
				<entry key="username" value="root"/>
				<entry key="password" value="1234"/>
				<entry key="from" value="Beijing"/>
			</map>
</property>

最后,在SpringDemo类的main方法中:

		System.out.println(sampleBean.getSession().getClass());
		//class java.util.LinkedHashMap
		//依然是通过链表的形式来存储数据的,所以输出的顺序是和配置的顺序是保持一致的。
		System.out.println(sampleBean.getSession());
		//{username=root, password=1234, from=Beijing}
Set与Map的实质:

世上本没有Set,每一个Set都是只关心Key,不关心Value的Map

看HashSet的源代码:

    public HashSet() {
        map = new HashMap<>();
    }

当new一个HashSet的时候,其本质是new一个HashMap。

并且:

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{//HashSet有一个声明,用的是占位符E,而内部声明了一个HashMap,HashMap的泛型Key也是E,同时,它的Value是Object,Object是随便声明的一个类型
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

并且在add方法中:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

其本质是往Map中put了一个数据,而添加进来的参数就变成了Map这个数据的Key。

所以,Map内的Key的表现特征,与Set非常相似,或者说就是一样的。而实际上,Set本身就是基于Map来实现的!

这是HashSet的源代码,而TreeSet也是一样的。

    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

所以,Set实际上一直都是用Map来作为一个内部实现的工具,看到用的是Set,其实本质上都是Map,所以才有:世上本没有Set,每一个Set都是只关心Key,不关心Value的Map

(6)注入Properties类型的数据:

1.以前老师没有明确的说Properties是一种集合类型,但是它也是能放很多数据的。

2.在哪出现过:以前在学JDBC的时候,写的一个数据库连接的jdbc.properties文件,读到程序中,就是一个Properties类型的数据,写配置文件的时候用的。这个配置文件读出来之后,和Map差不多,与Map有相似之处,但不是Map。

3.如果需要注入的是java.util.Properties类型的数据,在SampleBean类中:

// name:Jack, age:25, gender:Male
private Properties profile;
//属性是Properties,是property的复数形态,多个属性的意思
//profile翻译:概述、简介。里面准备一些个人资料。
//然后生成set、get方法

4.注意,这个Properties属性类型的数据,很少在xml里面写死,更多的还是像以前JDBC一样,在项目中会有一个jdbc.properties这样的一个配置文件,那么private Properties profile;这个对象的值就应该是来自于配置文件中,所以,关于Properties的注入就有这两种方式:常用:使用配置文件注入属性值,不常用:直接在Spring.xml中写死。

不常用方式:直接在Spring.xml写,在src/main/resources中的Spring的配置文件中进行配置:

<property name="profile">
			<props>
                <!-- props是Properties的简写 -->
                <!-- 子集只有一个,注意,没有value -->
				<prop key="name">Jack</prop>
				<prop key="age">25</prop>
				<prop key="gender">Male</prop>
			</props>
</property>

然后在SpringDemo中新增:

		System.out.println(sampleBean.getProfile().getClass());
		//class java.util.Properties
		//不是其他实现类,Properties类型就是一种直接可用的类型
		System.out.println(sampleBean.getProfile());
		//{age=25, name=Jcak, gender=Male}
		//输出顺序和写的顺序不一样,数据在内存中是散列存储的,在实际使用中,也并不关心先后顺序
		/*因为在实际使用这样数据的时候,更多的是读配置文件,去获取某一个属性的值,比如获取数据库连接的用户名,另外一处再获取数据库连接的密码,是取出其中的某一个数据来用,其实并不关心先后顺序*/

常用方式:当然,Properties类型的数据也可以从.properties类型的文件中读取!可以先在src/main/resources下创建jdbc.properties文件,右键new出File(搜File的话,是在General下面的),并在其中进行属性与值的配置:

url=jdbc:mysql://localhost:3306/database_name
driver=com.mysql.jdbc.Driver
username=root
password=root
initialSize=2
maxActive=10

#initialSize=2:表示如果用数据库连接池的话,初始化连接数有几个,这个值一般就是随便写,正式编程的时候也是随便写,因为这个值我们自己无法直接确定,后期根据运营情况要一直调整的。

这种方式配置Spring时,就和之前的不太一样了

<bean>节点并列:在Spring的配置文件中,需要使用<util:properties>节点来读取这个文件,这个节点与<bean>节点是同一个级别的节点,不要将这个节点写在<bean>的子级!在配置时:

<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>

<!-- util:properties节点:用于读取.properties文件中的配置信息 -->
<!-- location属性:需要读取的文件的位置。 (location翻译:位置)-->
<!-- classpath:指的就是resources文件夹 -->

如果这些数据需要被读取到程序中或者某个属性中,是希望类当中是有这个属性的,比如读到SampleBeanProperties jdbc属性中,那么要先在SampleBean中添加:

// 数据来自于jdbc.properties文件
private Properties jdbc;
//现在是需要为这个属性注入值
//然后生成set、get方法

那么现在要配置的是SampleBean中的jdbc的这个属性,我希望这个属性是有值的,

那么,在Spring的配置文件中,在配置SampleBean<bean>节点子级添加:(在后面补充)

<property name="jdbc"/>

name为jdbc之后,那么jdbc的值来自下面这个properties节点:

<util:properties location="classpath:jdbc.properties"></util:properties>

只需要为这个properties节点指定一个location,它就会自动的把jdbc.properties这个文件给读了,自动的读取这个文件中的所有信息,并且把它封装到一个propertie类型的对象里面去,如果这个jdbc要来自上面properties节点读取的结果,首先,这个读取结果就要给他一个名字,否则他们两个之间就无法有对应关系,所以给名字:依然是用id给名字,一般建议:描述的是同一个数据,就用同样的名字就行

所以写成:

<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>
关于ref属性:

然后,既然希望数据是来自于这个properties节点,那么在中加入一个新的属性ref,ref表示引用的意思,引用的是"jdbc",所以最后写成:

<property name="jdbc" ref="jdbc"/>

写到这里,配置就写完了。

最终配置代码,在spring.xml文件中写成:

<property name="jdbc" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>

知识点:在为属性注入值时,如果该属性值是另一个Bean,则应该通过ref属性引用到另一个Bean的id。

其实可以理解为,使用的<util:properties>是一种比较特殊的<bean>

然后,在SpringDemo中添加:

		System.out.println(sampleBean.getJdbc().getClass());
		System.out.println(sampleBean.getJdbc());
	    //输出结果
		//class java.util.Properties
		//{maxActive=10, password=root, url=jdbc:mysql://localhost:3306/database_name, driver=com.mysql.jdbc.Driver, initialSize=2, username=root}
//这些属性就是在jdbc.properties文件中写的属性。

以后,凡是直接能写的值,就写value,不能直接写的值,如果是来自于另外一个Bean,就写ref

小结:如果某个类中的某个属性需要被注入值,

1.首先,需要为这个属性添加匹配的SET方法(一般GET方法也都直接添加了),

2.在Spring的配置文件中,在该类对应的<bean>节点的子级,添加<property>节点,用于为属性注入值,如果有多个属性都需要注入值,则同一个<bean>节点的子级应该有多个<property>节点,

3.在每个<property>节点中,都通过name属性指定需要注入值的属性名称,假设需要注入值的属性叫password,则应该配置为name="password"

(1)如果该属性的值是可以直接写出来的(例如数值、字符串、布尔值),则在<property>节点中添加value属性来配置值,

(2)如果该属性的值是另一个Bean对象,则在<property>节点添加ref属性来引用另一个<bean>节点或等同节点的id值,

(3)如果该属性的值是集合类型的,则需要在<property>节点的子级添加<list> / <set> / <array> / <map> / <props>进行配置,具体配置方式参考以上笔记中的示例代码。

jdbc.properties文件的配置

使用配置文件访问数据库的优点是:一次编写随时调用,数据库类型发生变化只需要修改配置文件。

配置文件的设置:在配置文件中,key-value对应的方式编写。

开发中获得连接的4个参数(驱动、URL、用户名、密码)通常都存在配置文件中,方便后期维护,程序如果需要更换数据库,只需要修改配置文件即可。

使用properties文件,Java中有专门用来装载配置文件的类Properties(Java.util.Properties),配置文件用来保存一些在程序中可能修改的值,修改时只要修改配置文件即可,而不用修改程序本身。Java配置文件常为.properties文件,格式为文本文件,文件的内容的格式是“键=值”的格式。在properties的文件中,在行首加上 # 就是注释这行,或者是用 包括这行也是注释

加载配置文件

 public static void loadPro() throws Exception {
    //从文件中读取输入流
   FileInputStream fis = new FileInputStream("src/database.properties");
    //创建Properties对象
   Properties pro = new Properties();
    //从流中加载数据
   pro.load(fis);
    //关闭流
   fis.close();
     
   //从Properties对象中根据键读取值
   String driverClass = pro.getProperty("driverClass");
   String url = pro.getProperty("url");
   String username = pro.getProperty("username");
   String password = pro.getProperty("password");
   //打印值
   System.out.println(driverClass);
   System.out.println(url);
   System.out.println(username);
   System.out.println(password);
 }

1.通过IO读取文件

2.创建Properties对象

3.使用Properties对象的load(流)方法加载数据

4.使用Properties对象的getProperty(键)方法获取对应值

用JDBC建立连接:

/* 假设已经取得 driverClass,url, username, password 的值 */
// 注册驱动
Class.forName(driverClass);
// 建立连接
Connection con = DriverManager.getConnection(url, username, password);
// 获取SQL语句执行对象
Statement stat = con.createStatement();
// 调用执行者对象方法,执行SQL语句获取结果集
String sql = "SELECT * FROM sort";
ResultSet rs = stat.executeQuery(sql);
// ResultSet接口方法 boolean next() 返回true,有结果集,返回false没有结果集
while(rs.next()){// 获取每列数据,使用是ResultSet接口的方法 getXX方法参数中,建议写String列名
    System.out.println(rs.getInt("sid")+"   "+rs.getString("sname")+"   "+rs.getDouble("sprice")+"   "+rs.getString("sdesc"));
}
// 关闭对象,先开后闭rs.close();
stat.close();
con.close();
Properties对象的常用方法
  1. getProperty ( String key),用指定的键在此属性列表中搜索属性。也就是通过参数 key ,得到 key 所对应的 value。

  2. load ( InputStream inStream),从输入流中读取属性列表(键和元素对)。通过对指定的文件(比如说上面的 test.properties 文件)进行装载来获取该文件中的所有键 - 值对。以供 getProperty ( String key) 来搜索。

  3. setProperty ( String key, String value) ,调用 Hashtable 的方法 put 。他通过调用基类的put方法来设置 键 - 值对。

  4. store ( OutputStream out, String comments),以适合使用 load 方法加载到 Properties 表中的格式,将此 Properties 表中的属性列表(键和元素对)写入输出流。与 load 方法相反,该方法将键 - 值对写入到指定的文件中去。

  5. clear (),清除所有装载的 键 - 值对。该方法在基类中提供。

  6. keySet(),获取键集合(Properties继承Hashtable,内部通过Map实现)。

3. 通过构造方法注入属性的值(仅了解)

一般在写一个类的时候,都是非常提倡这个类是有无参数构造方法的,这样才便于框架对其进行管理,但是:

如果某个类没有无参数的构造方法,并且,希望通过构造方法为某些属性注入值,例如:

package cn.tedu.spring;

// 使用构造方法注入属性的值
// 使用构造方法注入时,并不要求每个属性都有SET/GET方法
public class Person {
	
	// Zoe
	private String username;
    //拓展:Spring框架对于权限的要求是不严格的,因为它的核心工作底层工作原理都是通过反射机制来操作的,所以即使是写成私有的也能正常用,所以对权限没有太多的要求,但是从开发规范上来说,建议还是用私有的。
    
	// 26
	private int age;
	
    //生成构造方法:快捷键Alt+Shift+S,O
	public Person(String username, int age) {
        //此构造方法有两个参数,第一个是username,第二个是age
		super();
		this.username = username;
		this.age = age;
	}
	
    //为了好看一会的执行效果,生成toString,快捷键:Alt+Shift+S, S
	@Override
	public String toString() {
		return "Person [username=" + username + ", age=" + age + "]";
	}

}

在Spring的配置文件中,需要配置为:

<bean id="person" class="cn.tedu.spring.Person">
		<!-- constructor-arg节点:配置构造方法的参数 -->
		<!-- index属性:参数的位置,即第几个参数 -->
		<constructor-arg index="0" value="Zoe"/>
		<constructor-arg index="1" value="26"/>
    	<!-- 子级代码是不需要写后续的,所以用/结尾 -->
</bean>

然后在SpringDemo中写:

由于新写了个Person类,要获取这个Person对象,所以在第2步,从Spring容器中获取对象中补充:

Person person = ac.getBean("person",Person.class);
System.out.println(person);
//Person [username=Zoe, age=26]

构造方法注入属性值-小节

所以 ,如果构造方法中有多个参数,在<bean>节点的子级就需要添加多个<constructor-arg>节点进行配置,在<constructor-arg>节点中,index属性表示参数的位置,是从0开始顺序编号,也就是:第1个参数的index值是0,第2个参数的index值是1,以此类推,如果对应的参数值是数值、字符串、布尔值,就通过value属性进行配置,如果对应的参数值是另一个Bean对象,就需要通过ref属性引用到另一个<bean>节点的id值,如果对应的参数值是某种集合类型的,则需要在<constructor-arg>子级添加集合节点进行配置!

如下:

		<constructor-arg index="0" value="Zoe"/>
		<constructor-arg index="1" value="26"/>
		假设还有第三个参数,如果第三个参数的类型是数组或者集合,如下:
		<constructor-arg index="2">
			<list>
				<value>11</value>
				<value>22</value>
				<value>33</value>
			</list>
		</constructor-arg>

value取值的实质

不管是通过Set方式还是通过构造方法来注入属性值,关于“值”应该怎么取,它的原则是不变的:

1.能够直接写,就是value="";

比如

<property name="password" value="123456"/>

<constructor-arg index="0" value="Zoe"/>

2.如果值是另外一个<bean>,那就写ref

<property name="jdbc" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>

<constructor-arg index="0" ref="jdbc"/>
<util:properties id="jdbc" location="classpath:jdbc.properties"></util:properties>

3.如果是集合,就在子级添加节点再来配置就行,而其他类型,比如数组、Set、Map、Properties等,也是参照上述写法来写。

        <bean id="sampleBean" class="cn.tedu.spring.SampleBean">
                <property name="names">
                    <list>
                        <value>David</value>
                        <value>Lucy</value>
                        <value>Tom</value>
                        <value>Lily</value>
                        <value>Alex</value>
                    </list>
                </property>
        </bean>


		<constructor-arg index="0" value="Zoe"/>
		<constructor-arg index="1" value="26"/>
		假设还有第三个参数,如果第三个参数的类型是数组或者集合,如下:
		<constructor-arg index="2">
			<list>
				<value>11</value>
				<value>22</value>
				<value>33</value>
			</list>
		</constructor-arg>
posted @ 2022-02-15 14:59  Charles博客  阅读(68)  评论(0编辑  收藏  举报