2006年7月10日星期一

Hibernate JPetstore 系列之一: 总体架构


文档内容


  • 概览

  • 应用组件结构图

  • 组件剖析

    • 表示层

      • 前端跳转首页

      • WEB安全目录

      • Struts WEB应用基本配置

    • 控制层

      • Spring (“春天”)来到

      • 在 web.xml 文件中的配置

        • 1. 应用上下文的机动性

        • 2. Log4J 日志属性文件

        • 3. Spring 的上下文配置文件位置

      • Spring 上下文件配置

        • applicationContext.xml 门面的配置

        • 属性占位符(placeholder) 配置

      • Struts 控制层组件Action 访问 applicationContext.xml 配置的所有 Bean


    • 数据层


      • 数据源及其所使用支持的连接池定义

      • Hibernate 会话厂SessionFactory

      • hibernate.hbm2ddl.auto配置的值

      • JDBC 属性配置


概览


JPetstore原始的实现(来自于Spring 的 Sample) 包含了两个WEB表示层的实现,一个是 Struts, 另一个则是
SpringMVC。我们主要介绍 基于Struts 的实现,因这种方案至少目前来说更普遍。
另一方面,原版的DAO实现采用的是 iBatis, 同样是因为框架的采用率问题,我们将它移植成
基于 Hibernate 的版本。 项目最终的源码将在系列结束时一并给出。

应用组件结构图







组件剖析





表示层


表示层一般也叫视图层,主要是用户接口。
此应用是基于JSP 和标准标记库(JSTL),以及Struts 的实现,主要包括:

  1. $hibernateJpetstore/web/WEB-INF/jsp/struts/ 目录下的所有文件,

  2. 前端自动跳转首页
    $hibernateJpetstore/web/index.jsp. (该目录下的 index.html 及 help.html
    是我用于测试的文件,产品时应该移除)

  3. $hibernateJpetstore/web/WEB-INF/struts-config.xml 文件



这里有两个重要的概念:前端跳转首页和WEB安全目录。

前端跳转首页

前端跳转页是用来当你未指定具体的页面,而直接输入类似于 http://www.site.com/ 时能直接进入到网站的首页。

WEB安全目录




如果细心观察 struts-config.xml 文件,我们可以看到,大部分(其实是除了跳回首页的跳转)跳转的目的地都是位于
/WEB-INF/ 目录下的JSP页面。根据Servlet 规范,这个目录是不能让用户直接访问的,比如在浏览器中直接输入
http://localhost:8080/应用上下文/WEB-INF/abc.jsp,服务器是不能让用户直接查看此目录下的所有文件的。因为该目
录是整个应用的配置的生死攸关的文件。不可以随便查看。
但是,这些文件是可以由服务器端组件调用的。所以目前大部分的框架,特别是SpringMVC
都是将它所控制的页面也一同放入到这个目录下,这样一来就避免了用户(或都恶意者)跳过应用的控制流程直接请求某个页面的危险,虽然,在每个页面中都可以
进行某些逻辑判断,比如用户是否登录了。但是如果这一逻辑被新手忘记了的话,如果这个页面被放在与了首页所在的同一位置(即所谓的
public-html) 目录下的话,这将是系统的安全漏洞了。
所以之前的一些书上的所谓的 jarkata 目录方案其实是有缺陷的。(即我们的 NetBeans IDE中所默认生成的目录结构是有缺陷的)。
<forward name="global-signon" path="/WEB-INF/jsp/struts/SignonForm.jsp"/>

Struts WEB应用基本配置




所以,对于基于 Struts 的WEB应用,表示层最基于的配置为:
web.xml 文件:

  1. 指定 struts 的前端 Servlet, 及其的URL映射(即其的控制范围)


  2. 通过一个首页(或者一个链接)跳转到Struts 的管辖范围中。

  3. WEB-INF/struts-config.xml 文件


关于表示层的详细介绍将会在后续的系列-- 表示层技术中介绍。比如: Struts 的 ActionForm 就是相当于 JSP +
Javabeans 设计中的Javabeans, 因此,ActionForm 是属于 表示层的组件。

控制层




控制层处在表示层与数据层之间的部分,其目的是为了使表示层与数据层脱耦(即表示层不能直接跟数据层通信--
将用户提交的表单数据直接更新到数据层,数据层也不能在数据更新后直接通知表示层)。
在 HibernateJpetstore 中,主要由以下几部分组成:
(值得提前说明的是,由于Spring 框架是一个“粘合剂”,所以在我们的这个应用中,它跨越了控制层和数据层的配置,如果使用了SpringMVC
的话,那么它将在应用的所有层中起作用。)

  1. $hibernateJpetstore/src/org.springframework/samples/jpetstore/domain/logic/
    (我个人认为这个目录不应该放在 domain
    下,应该为$
    hibernateJpetstore/src/org.springframework/samples/jpetstore/logic,
    因为domain 是商业逻辑层范畴--
    一般为数据层的上层,但又不在控制层中,所以这样就出现了四层了,但如果严格按照MVC来划分的话,一般是把实体对象/领域对象 domain 规作
    数据层,因为它与DAO紧密相关的)


  2. $hibernateJpetstore/src/org.springframework/samples/jpetstore/web/struts/
    目录下的所有 Action 的子类

  3. $hibernateJpetstore/web/WEB-INF/struts-config.xml

  4. $hibernateJpetstore/web/WEB-INF/applicationContext.xml

  5. $hibernateJpetstore/web/WEB-INF/mail.properties


$hibernateJpetstore/src/org.springframework/samples/jpetstore/domain/logic/
目录只有 PetStoreFacade.java, PetStoreImpl.java 和
SendOrderConfirmationEmailAdvice.java 对我们有用,忽略其它文件,因为它们被用在 SpringMVC 中。
PetStoreFacade.java, PetStoreImpl.java 分别为门面接口及其实现
SendOrderConfirmationEmailAdvice.java 是一个 SpringAOP(面向方面) 的的一个
Advice, 在Spring 的主配置文件 applicationContext.xml中指定在门面接口的
PetStoreFacade.insertOrder() 方法成功调用后会调用此类的 afterReturning
方法,此方法是向用户发一封定单确认邮件。
struts-config.xml 即是 struts 的配置核心文件
applicationContext.xml 是 Spring 的配置核心文件,正如前面所说,由于 Spring
支持模块化配置,所以此文件并不包含所有的配置。
mail.properties 邮件主机配置文件,因为在发邮件时需要 SMTP
(简单邮件传输协议)服务,你们可能会象我当初一样问,为什么J2EE 不内置一个 SMTP服务器呢,如果是这样,我们又会想要
POP/POP3服务器,协议有成千上万种,不可能在J2EE中包含一切。
最后,我们看看Spring是怎样将这几部分“粘合”起来的:

Spring (“春天”)来到





在 web.xml 文件中的配置




1. 应用上下文的机动性



  <context-param>
<param-name>webAppRootKey</param-name>
<param-value>petstore.root</param-value>
</context-param>


这个配置是将应用的上下文路径机动性的处理,也就是其位置是最终应用放置的位置,比如:如果最终应用部署到 Tomcat 中,如果Tomcat
被装在
D:\Tomcat\ 中,再假设应用的上下文被设置为 /hjpetstore 的话,则最终 petstore.root 的值为
D:\Tomcat\webapps\hjpetstore\,
这样一来,所有依赖这一位置的配置就有了一个相对的基础(我们都应该知道,文件的相对路径比绝对路径方便处理 )。比如应用的日志文件,因此在
log4j 的配置文件(WEB-INF\log4j.properties)中有:
log4j.appender.logfile.File=${petstore.root}/WEB-INF/petstore.log

这个配置指定了日志文件最终的位置,其实原版本不是这样的,我之所以这样配是在测试阶段方便,真实的产品环境可以考虑这样子,放在应用的一个专门的日志目
录下:
log4j.appender.logfile.File=${petstore.root}/log/petstore.log


2. Log4J 日志属性文件

如下配置指定了 log4j 配置文件所在地:
<context-param>

<param-name>log4jConfigLocation</param-name>

<param-value>/WEB-INF/log4j.properties</param-value>

</context-param>


这个文件Spring 特有的,用于支持 log4j 的类,注意如果应用是配置到 JBoss 当中去的话,则不能在 web.xml
中配置这个类,因为 JBoss 有自己的一套用于支持 log4j 的配置,如果
在两个地方都指定,那么就会出现冲突。所以在使用 JBoss 时,就不需要这个配置了(注释掉,或者移除掉即可)
  <listener>

<listener-class>
org.springframework.web.util.Log4jConfigListener<
/listener-class>

</listene>

3. Spring 的上下文配置文件位置




  <context-param>

<param-name>contextConfigLocation</param-name>

<!-- local datasource -->

<param-value>

/WEB-INF/dataAccessContext-hibernate.xml /WEB-INF/applicationContext.xml

</param-value>

</context-param>;


这里把应用的两个Spring 配置文件一同指定了,重要的是这个值一定是以 '/' 开头,且多个文件间
是以空格隔开的(不是以逗号分隔的).
注意,有些地方也把 applicationContext.xml 文件叫做 Spring 根上下文配置文件,因为通常,如果Spring
不是用在WEB环境下,则不可能有个 web.xml 文件,所以Spring通过只会在CLASSPATH 中找
applicationContext.xml 文件,其它的配置文件是通过在 applicationContext.xml
文件中导入的,如下语句:
<import resource="dataAccessContext-hibernate.xml"/>

Spring 上下文件配置


applicationContext.xml
门面的配置

<bean id="petStore" class="org.springframework.samples.jpetstore.domain.logic.PetStoreImpl">

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

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

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

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

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

</bean>


属性占位符(placeholder) 配置

属性占位符(placeholder) 定义,这样在后续的配置中就可以使用指定的属性文件(这里是,mail.properties 和
jdbc.properties) 配置的 key=value 中的 ${key} 来得到属性的值。
语法 ${"jdbc.username"}
的意思是使用属性 "jdbc.username" 的值($ {...
}
在规则表达式中总是“求值”的意思),

  <!-- Configurer that replaces ${...} placeholders with values from properties files -->

<!-- (in this case, mail and JDBC related properties) -->

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

<property name="locations">

<list>

<value>WEB-INF/mail.properties</value>

<value>WEB-INF/jdbc.properties</value>

</list>

</property>

</bean>



Struts 控制层组件Action
访问
applicationContext.xml 配置的所有 Bean


注意,Spring 是一个“粘合剂”角色,但如果为了引入 Spring 的功能,我们需要在
表示层,控制层,或数据层做太多手脚的话,这样是不理想的,因为模块化的原则就是要尽量达到不让使用者知道自己的实现细节。在我们的应用中,的确有几个地
方出现了 Spring 的类:
在应用的所有 Action 的基类 BaseAction中 定义

import javax.servlet.ServletContext;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionServlet;

import org.springframework.samples.jpetstore.domain.logic.PetStoreFacade;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

/**

* 我们在这个基类中得到了 Spring 定义的 WebApplicationContext (ApplicationContext的子类),
* 有了它,就可以调用 getBean("传入配置文件中该Bean 的 ID"); 来获得 Bean 的实例. 这里我们只想
* 获得门面的实现类,有了它就可以在此类的所有子类,即 XXXAction 中调用门面的方法进行DAO操作, 如下:
* getPetStore().XXX();
*
*
* <p>另一种方法可以直接使用 Spring's 自带为 Struts 准备的 ActionSupport.
* 有兴趣可以试一下。
*
*
*/
public abstract class BaseAction extends Action {

private PetStoreFacade petStore;

public void setServlet(ActionServlet actionServlet) {
super.setServlet(actionServlet);
if (actionServlet != null) {
// fixed by pprun, must be synchronized
synchronized (this) {
ServletContext servletContext = actionServlet.getServletContext();
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
this.petStore = (PetStoreFacade) wac.getBean("petStore"); // 门面 Bean 在 applicationContext.xml 文件中的 id="petStore"
}
}
}

protected PetStoreFacade getPetStore() {
return petStore;
}

}



关于控制层的详细介绍将会在后续的系列-- 控制层技术中介绍

数据层




数据层,有些书上偶尔把商业逻辑层与之混淆在一起,因为商业实体(或者叫领域对象 Domail)通过是与数据库中的表存在着很紧密的联系的。
在此应用中,由以下几部分组成:

  1. $hibernateJpetstore/src/org.springframework/samples/jpetstore/dao/

  2. $hibernateJpetstore/src/org.springframework/samples/jpetstore/domain/

  3. $hibernateJpetstore/src/org.springframework/samples/jpetstore/dao.hibernate/


  4. $hibernateJpetstore/web/WEB-INF/applicationContext.xml

  5. $hibernateJpetstore/web/WEB-INF/dataAccessContext-hibernate.xml

  6. $hibernateJpetstore/web/WEB-INF/jdbc.properties


$hibernateJpetstore/src/org.springframework/samples/jpetstore/dao/
包中定义的所有DAO接口(每个接口所支持的增、删、改、查方法)
$hibernateJpetstore/src/org.springframework/samples/jpetstore/domain/
包中定义的是商业实体对象及每个对象的 hbm.xml 文件
$hibernateJpetstore/src/org.springframework/samples/jpetstore/dao.hibernate/
是采用 Hibernate 实现的DAO接口
$hibernateJpetstore/web/WEB-INF/dataAccessContext-hibernate.xml
数据访问层的属性配置(Spring 上下文配置的一部分),由于DAO实现是采用 hibernate 实现的,所以这里还包括了传统的
Hibernate.cfg.xml 及所有实体的 hbm.xml 文件的配置:

数据源及其所使用支持的连接池定义




这里采用的局部的(local)连接池框架是 C3P0

, (此外还有类似的连接池框架,如 Apache Commons DBCP),之所以称之为局部的,是因为它
不依赖于特定的应用服务器所支持的内置的连接池实现(每个应用服务器都有自已的对数据库的连接方案),那个连接池一般叫做 JNDI
数据源,因为在应用服务器启动后,会将配置的所有数据源(当然还包含其他对象)把它们存放到 JNDI( Java Name Directory
Interface, Java 名称目录接口,相当于注册表,用于存入全局配置信息)中供部署的应用使用。

 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<!-- 以下配置都是使用 jdbc.properties 属性文件中的配置,而之所以可以这样写,就是因为有 属性占位符配置的原因 -->
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!-- 连接池维持的最小的连接个数 -->
<property name="minPoolSize" value="5"/>
<!-- 连接池维持的最大的连接个数 -->
<property name="maxPoolSize" value="20"/>
<!-- 最大空闲时间, 当某个连接在这个时间内没活动后将从池中移除,前提是池中至少多于最少的连接数: minPoolSize -->
<property name="maxIdleTime" value="1800"/>
<!-- 为加强准备语句的执行性能,此参数指定被缓存的 PreparedStatement 的个数 -->
<property name="maxStatements" value="50"/>
</bean>


这些属性在下面的 jdbc.properties 文件中指定。

Hibernate 会话厂 SessionFactory




Session 就是用于每次与数据库会话的,因此需要:
数据库的配置参数,这些参数就是 上面的数据源指定的! 因此我们只需引用即可: ref="dataSource";
实体映射配置
hibernate.cfg.xml 配置
结果缓存配置(这里使用的是开源的 ehcache)

  <!-- Hibernate SessionFactory -->
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 引用前面定义的数据源 -->
<property name="dataSource" ref="dataSource"/>

<!-- 所有实体映射文件列表, 所有的 hbm.xml 文件 -->
<property name="mappingResources">
<list>
<value>org/springframework/samples/jpetstore/domain/Account.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Banner.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Category.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Inventory.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Item.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/LineItem.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Order.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Product.hbm.xml</value>
<value>org/springframework/samples/jpetstore/domain/Supplier.hbm.xml</value>
</list>
</property>

<!-- 传统上的 hibernate.cfg.xml 文件的参数放在这里 -->
<property name="hibernateProperties">
<props>
<!-- 指定数据库方言 -->
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<!-- 是否在日志中输出所有Hibernate与数据库交互的SQL语句 -->
<prop key="hibernate.show_sql">true</prop>
<!-- 是否在日志中输出的SQL 语句格式化成易读形式 -->
<prop key="hibernate.format_sql">true</prop>
<!-- 是否显示统计形式,一般在测试阶段使用 -->
<prop key="hibernate.generate_statistics">true</prop>
<!-- 对于级联查询,一次性获取的级联深度, @todo 需进一步研究 -->
<prop key="hibernate.max_fetch_depth">2</prop>
<!-- 见下面的解释 -->
<prop key="hibernate.hbm2ddl.auto">update</prop>

<!--
  结果缓存配置:
- 将ehcache.xml 置于 classpath 中
- 如果不设置“查询缓存”,那么hibernate只会缓存使用load()方法
获得的单个持久化对象,如果想缓存使用findall()、 list()、Iterator()、
createCriteria()、createQuery()等方法获得的数据结果集的话,就需要设置
hibernate.cache.use_query_cache true 才行
- 在Hbm文件中添加<cache usage="read-only"/>
- 如果需要“查询缓存”,还需要在使用
Query或Criteria()时设置其setCacheable(true);属性
-->
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</prop>
</props>
</property>

<!-- 为解决 merge()方法语义的歧义 @todo 以后进一步解析或者你可以看一下相应的文档 -->
<property name="eventListeners">
<map>
<entry key="merge">
<bean class="org.springframework.orm.hibernate3.support.IdTransferringMergeEventListener"/>
</entry>
</map>
</property>
</bean>;



hibernate.hbm2ddl.auto
配置的值





  1. create 在每次
    SesstionFactory 构建时(一般是应用重启时,或者伴随着应用服务器重启时),先将之前数据库中的所有数据全部清空,后紧跟着根据所有的
    hbm.xml 映射文件重新创建新的数据库表

  2. create-drop 除了
    create 的所有含义之外,在每次应用的退出前,将进行一次数据空清空。因此这个配置将有两次清空操作,一次是退出,一次是启动时。

  3. update

    如果在开发阶段理发了实体对象的映射文件(hbm.xml) 的定义后,此配置将后台的数据库表进行更新(如增加表的列)

  4. validate
    用于校验现有的表与现有的配置是否一致。


值得注意的是 hibernate.hbm2ddl.auto
的配置:
Usually you only leave it turned on
in continous unit testing, but another run of hbm2ddl would drop
everything you have stored - the create
configuration setting actually translates into "drop all tables
from the schema, then re-create all tables, when the SessionFactory is
build".
Most new Hibernate
users fail at this point and we see questions about Table not found
error messages regularly.

我这里翻译一下:

一般情况下,只有在连续的测试阶段时需要设置这个值,因为在测试时一般都希望在测试完后,数据与测试前并无变化。
将hibernate.hbm2ddl.auto 配置成 "create" 是意思是在每次 SesstionFactory
构建时(一般是应用重启时,或者伴随着应用服务器重启时),先将之前数据库中的所有数据全部清空,后紧跟着根据所有的 hbm.xml
映射文件重新创建新的数据库表。大我数 Hibernate 的新手总是没有理解这一点,他们以为 "create" 都是简单地创建,而忽略了
"drop" 之前的丢弃副作用。
从上可以看出,除了 validate
在产品阶段看来有点用之外(但是肯定会影响应用的启动速度的),其它的并不适合已经部署了的产品。特别是对于已存在数据的数据库。
试想如果采用了 create 或 create-drop,在不经意(应用重启)间可能会将所有的数据库的数据全部清空。

JDBC 属性配置




$hibernateJpetstore/web/WEB-INF/jdbc.properties 配置数据库的属性,如下为 mysql 的配置:
######## mysql for Hibernate ########
# the database is hjpetstore
#####################################

# 数据库驱动
jdbc.driverClassName=com.mysql.jdbc.Driver

# 连接数据库的 jdbc url
jdbc.url=jdbc:mysql://localhost:3306/hjpetstore?useUnicode=true&characterEncoding=UTF-8

# 用户名
jdbc.username=jpetstore

# 密码
jdbc.password=jpetstore

# 指定数据库方言,这样 hibernate 就可以最大限度地针对特定的数据库进行优化
hibernate.dialect=org.hibernate.dialect.MySQLDialec


关于数据层的详细介绍, 如事务管理配置,DAO反向注入等,将会在后续的系列-- 数据层技术中介绍。
(待续)

14 条评论:

Rick 说...

好文章!没人顶哇~~可惜可惜,要不是NB的每日Blog提示中显示了这篇文章,我也不知道你的这篇文章这么精彩!我在我的空间里把你的Blog记在友情链接里。

P.P.Run 说...

rick,
很高兴你喜欢我写的文章!
这个系列总共有五篇文章,已经发表了三篇。剩下的表示层和部署相关的两个系列在有空时抓紧时间发表。

感谢关注。

P.p.run

匿名 说...

牛人....只有真正研究过jpetstore的人才知道这文章的价值 希望看到您另两篇文章 MyMSN:microinsky@hotmail.com TKs!!
BTW:hjpetstore是您自己改过的hibernate工程吧?希望向您请教下

P.P.Run 说...

做 J2EE 或 .NET 开发,在技术层面上的要求并不是太难。关键是理解流行的框架的机理(为是说要理解这些框架呢?因为SUN/J2EE 的领衔者们似乎总是站在高处看问题,所以他们定出的规范在一线的程序员看来总是离完美有段距离。这就促使例如 Struts, Hibernate, Spring 之类的作者不得不去完善它。),做这种项目最大的挑战是在对商业逻辑的把握上。
有了一个现成的模板在手边,加上理解了自己的项目将要做什么。这种程序生活应该还是会比较轻松。

是的,这个工程是我在学习 Hibernate 和 Spring 后自己改的。我说过,在系列完成后,我会把代码整理一下(去掉没用到的东西),最终与部署系列的文章给大家。

我在MSN加了你。 但这个帐号不是我日常使用的帐号。因为我还不大习惯在工作时有太多的干扰。有急事我们可以以邮件的方式讨论。

大头 说...

谁有博主的代码?发一份给我吧:
bigheadgp@gmail.com

P.P.Run 说...

你请求的角色已经批准了,
你现在可以 check out 所有源码了!

不好意思,我一般一周内会不定时地检查这个 Blog, 所以难免会有延时,
希望没让你失望。

Good luck!

匿名 说...

博主好,能把源代码发我一份吗???谢谢

tessyxj@yahoo.com.cn

P.P.Run 说...

你请求的角色已经批准!

好运!

匿名 说...

会ibatis,不会hibernate,
看不到你的src源文件,老大能否发给我一份
hjetstore项目源文件,不要class文件的那种
hf20041001@163.com
感激不尽

P.P.Run 说...

你的申请已经批准!

匿名 说...

好文章!辛苦了!谢谢!

P.P.Run 说...

谢谢你的认可!

匿名 说...

您好,能给我一份源码吗?niu_hit@126.com使用CVS没有check out出来,已经有java.net的帐号;

P.P.Run 说...

niumd,
由于最近比较心,老板又赶回中国过圣诞(开玩笑,来安排下年的工作)。
所以几天没有收邮件的。
我已经 approve 你申请的角色。
如果还不能check out 的话,可以通知我,MSN在那,你可以加我,直接联系。
由于源代码还是比较大(自包含所有的依赖),所以发邮件不是很方便。

谢谢大家的喜欢!