飞翔吧

不做不说,说到做到

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
用 Hibernate 和 Spring 开发事务持久层

作者:Richard     来自:IBM

  当您自以为已经了解了所有开发工具时,肯定又会冒出一个新的工具。在本文中,developerWorks 的固定撰稿人 Rick Hightower 用一个真实世界的例子向您介绍两个最激动人心的企业新技术。Hibernate 是一个对象关系映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。Rick 介绍了如何结合这两者,为企业应用程序构建一个事务持久层。

  如果关心开发人员的最新热点,那么您可能听说过 IOC (控制倒置,Inversion of Control)容器和 AOP (面向方面编程)。不过,像许多开发人员一样,您可能不清楚在自己的开发工作中如何使用这些技术。在本文中,通过具体介绍使用 Hibernate 和 Spring 在企业应用程序中构建一个事务持久层,您会认识到这些技术。

  Hibernate 是 Java 平台上的一种流行的、容易使用的开放源代码对象关系(OR)映射框架。Spring 是一个 AOP 框架和 IOC 容器。这两种技术一起提供了本文中介绍的开发工作的基础。将使用 Hibernate 把一些持久性对象映射到关系数据库中,用 Spring 使 Hibernate 更容易使用并提供声明性事务支持。由于为示例类编写测试代码时使用了 DbUnit,我还附带介绍了一点 TDD (测试驱动的开发)的内容。

  注意,本文假定读者熟悉 Java 平台上的企业开发,包括 JDBC、OR 映射内容、J2EE 设计模式如 DAO,以及声明性事务支持,如 Enterprise JavaBean (EJB)技术所提供的事务支持。理解这里的讨论不需要成为这些技术的专家,也不需要熟悉 AOP、IOC 或者 TDD,因为在本文中对这三者都做了介绍。

  我将首先介绍两种开发技术,然后分析例子。

  Hibernate 简介

  Hibernate 是 Java 平台上的一种全功能的、开放源代码 OR 映射框架。Hibernate 在许多方面类似于 EJB CMP CMR (容器管理的持久性/容器管理的关系)和 JDO(Java Data Objects)。与 JDO 不同,Hibernate 完全着眼于关系数据库的 OR 映射,并且包括比大多数商业产品更多的功能。大多数 EJB CMP CMR 解决方案使用代码生成实现持久性代码,而 JDO 使用字节码修饰。与之相反,Hibernate 使用反射和运行时字节码生成,使它对于最终用户几乎是透明的(以前 Hibernate 的实现只使用反射,它有助于调试,当前版本保留了这种选项)。

  移植基于 Hibernate 的应用程序

  如果应用程序必须在多个 RDBMS 系统上运行 ,那么基于 Hibernate 的应用程序可以毫不费力地移植到 IBM DB2、MySQL、PostgreSQL、Sybase、Oracle、HypersonicSQL 和许多其他数据库。我最近甚至将一个应用程序从 MySQL 移植到 Hibernate 没有很好支持的 Firebird,而这种移植是很容易的。
Hibernate 可以模拟继承(有几种方式)、关联(一对一或者一对多、containment 和 aggregation)和 composition。我将在本文中讨论每种关系类型的几个例子。

  Hibernate 提供了一种称为 Hibernate Query Language (HQL) 的 查询语言,它类似于 JDO 的 JDOQL 和 EJB 的 EJB QL,尽管它更接近于前者。但是 Hibernate 没有就此止步:它还可以进行直接的 SQL 查询和/或使用 object criteria 很容易地在运行时构成查询条件。在本文的例子中我将只使用 HQL。

  与 EJB CMP CMR 不同,Hibernate 像 JDO 一样可以在 J2EE 容器内部或者外部工作,这可以让那些进行 TDD 和敏捷开发的人受益。

  Spring 简介

  AOP 专家 Nicholas Lesiecki 第一次向我解释 AOP 时,他说的我一个词也没理解,我觉得就像第一次考虑使用 IOC 容器的可能性时一样。每一种技术的概念基础本身就需要很好地消化,每一种技术所使用的各种各样的缩写让事情更糟了——特别是其中许多术语与我们已经使用的根本不一样了。

  像许多技术一样,理解这两种技术的实际使用比学习理论更容易。经过自己对 AOP 和 IOC 容器实现(即 XWork、PicoContainer 和 Spring)的分析,我发现这些技术可以帮助我获得功能,而不会在多框架中添加基于代码的依赖性。它们都将成为我后面开发项目的一部分。

  简单地说,AOP 让开发人员可以创建非行为性的关注点,称为横切关注点,并将它们插入到应用程序代码中。使用 AOP 后,公共服务(比如日志、持久性、事务等)就可以分解成方面并应用到域对象上,同时不会增加域对象的对象模型的复杂性。

  IOC 允许创建一个可以构造对象的应用环境,然后向这些对象传递它们的协作对象。正如单词 倒置 所表明的,IOC 就像反过来的 JNDI。没有使用一堆抽象工厂、服务定位器、单元素(singleton)和直接构造(straight construction),每一个对象都是用其协作对象构造的。因此是由容器管理协作对象(collaborator)。

  Spring 既是一个 AOP 框架、也是一个 IOC 容器。我记得 Grady Booch 说过,对象最好的地方是可以替换它们,而 Spring 最好的地方是它有助于您替换它们。有了 Spring,只要用 JavaBean 属性和配置文件加入依赖性(协作对象)。然后可以很容易地在需要时替换具有类似接口的协作对象。

  Spring 为 IOC 容器和 AOP 提供了很好的入口(on-ramp)。因此,不需要熟悉 AOP 就可以理解本文中的例子。所需要知道的就是将要用 AOP 为示例应用程序声明式地添加事务支持,与使用 EJB 技术时的方式基本相同。

  具体到业务

  在本文的其余部分,所有的讨论都将基于一个实际的例子。起点是一个企业应用程序,要为它实现一个事务持久层。持久层是一个对象关系数据库,它包括像 User、User Group、Roles 和 ContactInfo 这些熟悉的抽象。

  在深入到数据库的要素——查询和事务管理——之前,需要建立它的基础:对象关系映射。我将用 Hibernate 设置它,并只使用一点 Spring。

  用 Hibernate 进行 OR 映射

  Hibernate 使用 XML (*.hbm.xml) 文件将 Java 类映射到表,将 JavaBean 属性映射到数据库表。幸运的是,有一组 XDoclet 标签支持 Hibernate 开发,这使得创建所需要的 *.hbm.xml 文件更容易了。清单 1 中的代码将一个 Java 类映射到数据库表。

  清单 1. 将 Java 类映射到 DB 表
  [User.java]

/**
 * @hibernate.class table="TBL_USER"
 * ..
 * ..
 * ...
 */
public class User {

 private Long id = new Long(-1);
 private String email;
 private String password;
 
 .
 .
 .

 /**
  * @return
  * @hibernate.id column="PK_USER_ID"
  * unsaved-value="-1"
  * generator-class="native" 
  */
 public Long getId() {
  return id;
 }

 ...

 /**
  * @hibernate.property column="VC_EMAIL"
  * type="string"
  * update="false"
  * insert="true"
  * unique="true"
  * not-null="true"
  * length="82"
  * @return
  */
 public String getEmail() {
  return email;
 }

 /**
  * @hibernate.property column="VC_PASSWORD"
  * type="string"
  * update="false"
  * insert="true"
  * unique="true"
  * not-null="true"
  * length="20"
  * @return
  */
 public String getPassword() {
  return password;
 }

 ...
 ...
 ...
}

  可以看到,@hibernate.class table="TBL_USER" 标签将 User 映射到 TBL_USER 表。@hibernate.property column="VC_PASSWORD" 将 JavaBean 属性 password 映射到 VC_PASSWORD 列。@hibernate.id column="PK_USER_ID" 标签声明id 属性是主键,它将使用本机(generator-class="native")数据库机制生成键(例如,Oracle sequences 和 SQL Server Identity 键)。Hibernate 可以指定 generator-class="native" 以外的、其他可以想象的得到主键获得策略,不过我更愿意使用 native。type 和 length 属性用于从 Hibernate *.hbm.xml OR 映射文件生成表。这些 final 属性是可选的,因为使用的可能不是 green-field 数据库。在这个例子中,已经有数据库了,所以不需要额外的属性。(green-field 应用程序 是一个新的应用程序, green-field 数据 是新应用程序的一个新数据库。不会经常开发一个全新的应用程序,不过偶尔有一两次也不错)。

  看过了表如何映射到类以及列如何映射到 JavaBean 属性,该使用 Hibernate 在 OR 数据库中设置一些关系了。

  设置对象关系

  在本节中,我将只触及 Hibernate 提供的设置对象间关系的选项的一小部分。首先设置像 User、User Group、Roles 和 ContactInfo 这些类之间的关系。其中一些关系如图 1 所示,这是数据库的验证对象模型。


图 1. 关系的图示

  如您所见,在上述抽象中存在各种各样的关系。User 与 ContactInfo 有一对一关系。ContactInfo 的生命周期与 User 相同(用数据库的术语,UML 中的组成 aka 级联删除)。如果删除 User,则相应的 ContactInfo 也会删除。在 Users 与 Roles 之间存在多对多关系(即与独立生命周期相关联)。在 Groups 与 Users 之间存在一对多关系,因为组有许多用户。用户可以存在于组外,即是 aggregation 而不是 composition (用数据库的说法,在 Groups 和 Users 之间没有级联删除关系)。此外,User 和 Employee 有子类关系,就是说,Employee 的类型为 User。表 1 显示了如何用 XDoclet 标签创建一些不同类型的对象关系。

表 1. 用 XDoclet 创建对象关系
关系 Java/XDoclet SQL DDL(由 Hibernate Schema Export 生成的 MySQL)
组包含用户
一对多
Aggregation
双向
(Group<-->Users)
[Group.java]
/**
*
* @return
*
* @hibernate.bag name="users"
* cascade="save-update"
* lazy="true"
* inverse="true"
*
* @hibernate.collection-key
* column="FK_GROUP_ID"
*
* @hibernate.collection-one-to-many
* class="net.sf.hibernateExamples.User"
*/
public List getUsers() {
return users;
}

[User.java]
/**
* @hibernate.many-to-one
* column="FK_GROUP_ID"
* class="net.sf.hibernateExamples.Group"
*/
public Group getGroup() {
return group;
}

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)


create table TBL_GROUP (
PK_GROUP_ID BIGINT NOT NULL AUTO_INCREMENT,
VC_DESCRIPTION VARCHAR(255),
VC_NAME VARCHAR(40) unique,
primary key (PK_GROUP_ID)
)

alter table TBL_USER add index (FK_GROUP_ID),
add constraint FK_111 foreign key (FK_GROUP_ID)
references TBL_GROUP (PK_GROUP_ID)

用户有联系信息
一对一
Composition
单向
(User-->ContactInfo)
[User.java]
/**
* @return
*
* @hibernate.one-to-one cascade="all"
*
*/
public ContactInfo getContactInfo() {
return contactInfo;
}

[ContactInfo.java]
(Nothing to see here. Unidirectional!)

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

create table TBL_CONTACT_INFO (
PK_CONTACT_INFO_ID BIGINT not null,
...
...
...
primary key (PK_CONTACT_INFO_ID)
)

用户与角色关联
多对多
Association
单向
(Users-->Roles)
[User.java]
/**
* @return
* @hibernate.bag
* table="TBL_JOIN_USER_ROLE"
* cascade="all"
* inverse="true"
*
* @hibernate.collection-key
* column="FK_USER_ID"
*
* @hibernate.collection-many-to-many
* class="net.sf.hibernateExamples.Role"
* column="FK_ROLE_ID"
*
*/
public List getRoles() {
return roles;
}

[Role.java]
Nothing to see here. Unidirectional!

create table TBL_ROLE (
PK_ROLE_ID BIGINT NOT NULL AUTO_INCREMENT,
VC_DESCRIPTION VARCHAR(200),
VC_NAME VARCHAR(20),
primary key (PK_ROLE_ID)
)

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

create table TBL_JOIN_USER_ROLE (
FK_USER_ID BIGINT not null,
FK_ROLE_ID BIGINT not null
)

雇员是用户
Inheritance
用户
雇员
[User.java]
/**
* @hibernate.class table="TBL_USER"
* discriminator-value="2"
* @hibernate.discriminator column="USER_TYPE"
*
...
...
...
*/
public class User {

[Employee.java]
/**
* @hibernate.subclass discriminator-value = "1"
*/
public class Employee extends User{

create table TBL_USER (
PK_USER_ID BIGINT NOT NULL AUTO_INCREMENT,
USER_TYPE VARCHAR(255) not null,
FK_GROUP_ID BIGINT,
VC_EMAIL VARCHAR(82) not null unique,
primary key (PK_USER_ID)
)

  Hibernate 中的查询

  Hibernate 有三种类型的查询:

  ☆ Criteria, object composition
  ☆ SQL
  ☆ HQL

  在下面的例子中将只使用 HQL。本节还要使用 Spring,用它的 AOP-driven HibernateTemplate 简化 Hibernate 会话的处理。在本节将开发一个 DAO(Data Access Object)。要了解更多关于 DAO 的内容,请参阅 参考资料。

  清单 2 展示了两个方法:一个使用 HQL 查询的组查询,另一个是后面接一个操作的组查询。注意在第二个方法中,Spring HibernateTemplate 是如何简化会话管理的。

  清单 2. 使用查询

import net.sf.hibernate.HibernateException;
import net.sf.hibernate.Session;
import net.sf.hibernate.Query;
import org.springframework.orm.hibernate.HibernateCallback;
import org.springframework.orm.hibernate.support.HibernateDaoSupport;

/**
 * @author Richard Hightower
 * ArcMind Inc. http://www.arc-mind.com
 */
public class UserDAO extends HibernateDaoSupport{

  .
  .
  .

  /**
 * Demonstrates looking up a group with a HQL query
 * @param email
 * @return
 */ 
 public Group findGroupByName(String name) {
    return (Group) getHibernateTemplate().find("from Group g where g.name=?",name).get(0);
 }
 
 /**
  * Demonstrates looking up a group and forcing it to populate users (relationship was lazy)
  * @param email
  * @return
  */ 
 public Group findPopulatedGroupByName(final String name) {
    HibernateCallback callback = new HibernateCallback(){

     public Object doInHibernate(Session session) throws HibernateException, SQLException {
   Group group =null;
   String query = "from Group g where g.name=?";
   Query queryObject = getHibernateTemplate().createQuery(session, query);
   queryObject.setParameter(0, name);
   group = (Group) queryObject.list().get(0);
   group.getUsers().size();//force load
   return group;
   }
   
  };
  
  return (Group) getHibernateTemplate().execute(callback);
 }
  .
  .
  .
}   

  您可能会注意到第二个方法比第一个方法复杂得多,因为它强迫加载 users 集合。因为 Group->Users 之间的关系设置为 lazy initialize(即表 2 中 lazy="true"),组对象需要一个活跃的会话以查询用户。在定义 Group 和 Users 之间关系时设置这个属性为 lazy="false",则不需要第二个方法。在这种情况下,可能使用第一种方法 (findGroupByName) 列出组,用第二种方法(findPopulatedGroupByName)查看组细节。

  Spring IOC 和 Hibernate

  使用 Spring 时,在 J2EE 容器内和容器外工作一样容易。比如在最近的项目中,我在 Eclipse 中,使用 HSQL 和本地数据库对使用 Hibernate 事务管理器的 Hypersonic SQL 数据库进行持久性单元测试。然后,在部署到 J2EE 服务器时,将持久层转换为使用 J2EE 数据源(通过 JNDI)、JTA 事务和使用 FireBird (一个开放源代码版本的 Interbase)。这是用 Spring 作为 IOC 容器完成的。

  从清单 3 中可以看出,Spring 允许加入依赖性。注意清单中应用程序上下文文件是如何配置 dataSource 的。dataSource 传递给 sessionFactory,sessionFactory 传递给 UserDAO。

  清单 3. Spring IOC 和 Hibernate

<beans>

 <!-- Datasource that works in any application server
  You could easily use J2EE data source instead if this were
  running inside of a J2EE container.
  -->
 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName"><value>com.mysql.jdbc.Driver</value></property>
  <property name="url"><value>jdbc:mysql://localhost:3306/mysql</value></property>
  <property name="username"><value>root</value></property>
  <property name="password"><value></value></property>
 </bean>

 <!-- Hibernate SessionFactory -->
 <bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean">
  <property name="dataSource"><ref local="dataSource"/></property>
  
  <!-- Must references all OR mapping files. -->
  <property name="mappingResources">
   <list>
          <value>net/sf/hibernateExamples/User.hbm.xml</value>
          <value>net/sf/hibernateExamples/Group.hbm.xml</value>
          <value>net/sf/hibernateExamples/Role.hbm.xml</value>         
          <value>net/sf/hibernateExamples/ContactInfo.hbm.xml</value>
   </list>
  </property>
  
  <!-- Set the type of database; changing this one property will port this to Oracle,
    MS SQL etc. -->
  <property name="hibernateProperties">
   <props>
     <prop key="hibernate.dialect">net.sf.hibernate.dialect.MySQLDialect</prop>
   </props>
  </property>
 </bean>
 
 <!-- Pass the session factory to our UserDAO -->
 <bean id="userDAO" class="net.sf.hibernateExamples.UserDAO">
  <property name="sessionFactory"><ref local="sessionFactory"/></property>
 </bean>
 
</beans>   

  设置了 UserDAO 后,下一步就是定义并使用更多的查询以展示可以完成的操作。Hibernate 可以用预定义查询将查询存储到源代码之外,如清单 4 所示。

  清单 4. 预定义查询
  [User.java]

/**
 * @author Richard Hightower
 * ArcMind Inc. http://www.arc-mind.com
 * @hibernate.class table="TBL_USER" discriminator-value="2"
 * @hibernate.discriminator column="USER_TYPE"
 *
 * @hibernate.query name="AllUsers" query="from User user order by user.email asc"
 *
 * @hibernate.query name="OverheadStaff"
 * query="from Employee employee join employee.group g where g.name not in ('ENGINEERING','IT')"
 *
 * @hibernate.query name="CriticalStaff"
 * query="from Employee employee join employee.group g where g.name in ('ENGINEERING','IT')"
 *
 * @hibernate.query name="GetUsersInAGroup"
 * query="select user from Group g join g.users user"
 *
 * @hibernate.query name="GetUsersNotInAGroup"
 * query="select user from User user where user.group is null"
 *
 * @hibernate.query name="UsersBySalaryGreaterThan"
 * query="from User user inner join user.contactInfo info where info.salary > ?1"
 *
 * @hibernate.query name="UsersBySalaryBetween"
 * query="from User user join user.contactInfo info where info.salary between ?1 AND ?2"
 *
 * @hibernate.query name="UsersByLastNameLike"
 * query="from User user join user.contactInfo info where info.lastName like ?1"
 *
 * @hibernate.query name="GetEmailsOfUsers"
 * query="select user.email from Group g join g.users as user where g.name = ?1"
 *
 */
public class User {
   .
   .
   .   

  上述代码定义了几个预定义查询。预定义查询 是存储在 *.hbm.xml 文件中的查询。在清单 5 中,可以看到如何执行预定义查询。

  清单 5. 使用预定义查询
  [UserDAO.java]

 /**
  * Demonstrates a query that returns a String.
  */               
 public String[] getUserEmailsInGroup(String groupName){
  List emailList =
  getHibernateTemplate().findByNamedQuery("GetEmailsOfUsers");
  return (String [])
   emailList.toArray(new String[emailList.size()]);
 }

 /**
  * Demonstrates a query that returns a list of Users
  *
  * @return A list of emails of all of the users in the authentication system.
  *
  */               
 public List getUsers(){
  return getHibernateTemplate().findByNamedQuery("AllUsers");
 }

      /**
 * Demonstrates passing a single argument to a query.
 *
 * @return A list of UserValue objects.
 *
 */                   
 public List getUsersBySalary(float salary){
    return getHibernateTemplate()
        .findByNamedQuery("UsersBySalaryGreaterThan",
             new Float(salary));
 }

 /**
  * Demonstrates passing multiple arguments to a query
  *
  * @return A list of UserValue objects.
  *
  */                   
 public List getUsersBySalaryRange(float start, float stop){
                return getHibernateTemplate()
   .findByNamedQuery("UsersBySalaryBetween",
   new Object[] {new Float(start), new Float(stop)});
 }

  查询进行时,可以在持久层中加上最后一层:使用 Spring 的事务管理。

  用 Spring 管理事务

  Spring 可以声明式地管理事务。例如,UserDAO.addUser 方法当前不是在单个事务中执行的。因此,组中的每一个用户都插入到自己的事务中,如清单 6 所示。

  清单 6. 添加一组用户
  [UserDAO.java]

/**
 * @param group
 */
public void addGroup(Group group) {
 getHibernateTemplate().save(group);
 
}

[UserDAOTest.java]

public void testAddGroupOfUsers(){
 Group group = new Group();
 
 for (int index=0; index < 10; index++){
  User user = new User();
  user.setEmail("rick"+index+"@foobar.com" );
  user.setPassword("foobar");
  group.addUser(user); 
 }
 
 group.setName("testGroup");
 
 userDAO.addGroup(group);
 assertNotNull(group.getId());
 
 Group group2 = userDAO.findPopulatedGroupByName("testGroup");
 
 assertEquals("testGroup",group2.getName());
 assertEquals(10, group2.getUsers().size());
 String email = ((User)group2.getUsers().get(0)).getEmail();
 assertEquals("rick0@foobar.com", email);

}

  不建议使用上述解决方案,因为每一个 User 都要在自己的事务中插入到数据库中。如果出现问题,那么只能添加部分用户。如果希望保留 ACID 属性(即保证所有都发生或者所有都不发生),可以通过程序进行事务管理,但是它很快就会变得一团糟了。相反,应使用 Spring 的 AOP 来支持声明式的事务,如清单 7 所示。

  清单 7. 声明式管理事务
  [applicationContext.xml]

<!-- Pass the session factory to our UserDAO -->
   <bean id="userDAOTarget" class="net.sf.hibernateExamples.UserDAOImpl">
 <property name="sessionFactory"><ref local="sessionFactory"/></property>
   </bean>
 
   <bean id="transactionManager"
    class="org.springframework.orm.hibernate.HibernateTransactionManager">
      <property name="sessionFactory"><ref bean="sessionFactory"/></property>
   </bean>

<bean id="userDAO"
     class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
 <property name="transactionManager"><ref local="transactionManager"/></property>
 <property name="target"><ref local="userDAOTarget"/></property>
 <property name="transactionAttributes">
  <props>
   <prop key="addGroup">PROPAGATION_REQUIRED</prop>
  </props>
 </property>
</bean>

  注意在准备清单 7 的代码时,我重新改写了 UserDAO 并提取了其接口。这个接口现在是 UserDAO,它的实现类是 UserDAOImpl。这样清单 7 中的事务代码就使用了带有事务属性 (PROPAGATION_REQUIRED) 的 UserDAO.addGroup() 方法。现在只要底层数据库支持,就可以在一个事务中添加所有用户。

  结束语

  在本文中,介绍了如何使用 Hibernate 和 Spring 实现一个事务持久层。Hibernate 是一种先进的 OR 映射工具,而 Spring 是一个 AOP 框架和 IOC 容器。这两种技术的综合使用,使得开发人员可以编写媲美数据库厂商的代码,它可以在 J2EE 容器中运行,也可以单独运行。使用了 DbUnit (JUnit 的扩展)构建和测试本文中例子的所有代码,虽然这不是讨论的重点。

posted on 2005-07-10 21:20  飞翔  阅读(2332)  评论(0编辑  收藏  举报