博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

http://www.blogjava.net/tonyzhou00/articles/131061.html

在日本,Seasar2这个框架十分的流行。Seasar2其实就是类似于Spring的一个开源框架

大家有兴趣的话,可以去官方网站看看: http://www.seasar.org/index.html

中文版现在还没完善,大家可能要用日文或则英文来了解

下面简单介绍一下:


所谓“Seasar2”就是一个“轻量级容器”,面向无法摆脱“Java 应用开发”之烦恼的所谓“开发者”,它能够保证开发的“高生产率和高品质”。并且同“其它轻量级容器”不同的是,“完全不需要书写设定文件”,“就算是应 用程序发生改动也无需再次起动即可直接识别变更,因此具有脚本语言的灵活性”。

为了不用写设定文件也能够运行,Convention over Configuration的思想得以采用。Convention over Configuration就是指,“只要遵守一个适当的规约,即使不用进行非常麻烦的设定,框架结构也可以自动替我们搞定的思想”,这一思想是Ruby on Rails中所倡导的。Seasar2的Convention over Configuration是从Ruby on Rails 那里得到的提示而产生的。

使用Seasar2的话,对于仅仅需要维护数据表这样简单的应用,可以在不到3分钟的时间里作成。

应用程序发生改动之时也无需启动便可立即识别变更的机能在Seasar2里被称为HOT deploy。

安装:

S2需要安装JDK1.4 or JDK1.5。

将S2xxx.zip解压之后的seasar2目录引入到Eclipse、「文件→导入→既存的工程」。

使用Seasar2基本功能(S2Container, S2AOP)的时候、CLASSPATH的下面必须包含以下文件。
lib/aopalliance-1.0.jar
lib/commons-logging-1.1.jar
lib/javassist-3.4.ga.jar
lib/ognl-2.6.9-patch-20070624.jar
lib/s2-framework-2.x.x.jar
lib/geronimo-j2ee_1.4_spec-1.0.jar (参考下面)
lib/portlet-api-1.0.jar (任选项)
lib/log4j-1.2.13.jar (任选项)
resources/log4j.properties (任选项)
resources/aop.dicon (任选项)

使用Seasar2的扩张机能(S2JTA, S2DBCP, S2JDBC, S2Unit, S2Tx, S2DataSet)的时候必须要将以下文件追加到CLASSPATH里面。
lib/junit-3.8.2.jar
lib/poi-2.5-final-20040804.jar
lib/s2-extension-2.x.x.jar
lib/geronimo-jta_1.1_spec-1.0.jar (参考下面)
lib/geronimo-ejb_2.1_spec-1.0.jar (参考下面)
resources/jdbc.dicon

根据应用软件所需的执行环境、选择以下需要引用的文件[geronimo-j2ee_1.4_spec-1.0.jar、geronimo-jta_1.0.1B_spec-1.0.jar、geronimo-ejb_2.1_spec-1.0.jar]
环境 geronimo-j2ee_1.4_spec-1.0.jar geronimo-jta_1.1_spec-1.0.jar geronimo-ejb_2.1_spec-1.0.jar
不完全对应J2EE的Servlet container
(Tomcat等) 不要 要
(使用S2JTA,S2Tx的时候) 要
(使用S2Tiger的时候)
完全对应J2EE的应用服务器
(JBoss, WebSphere, WebLogic等) 不要 不要 不要
独立 要
(使用S2JTA,S2Tx时候) 不要 不要

为了让大家更简单的体验数据库机能、使用了HSQLDB作为RDBMS。为了能够体验Oracle机能、准备了hsql/sql/demo- oracle.sql。SQL*Plus等执行了之后、请根据环境的需要改写jdbc.dicon的XADataSourceImpl的设定项目。

请使用S2Container用的插件Kijimuna。

想使用EJB3anoteshon的情况下、将 S2TigerXXX.zip解压缩后的s2-tiger目录引入Eclipse、「文件→导入→既存的工程」。 在Seasar2的设定基础上、必需要将以下的文件追加到CLASSPATH里面。
lib/s2-tiger-x.x.x.jar
resources/jdbc.dicon

想使用Tigeranoteshon的情况、将S2TigerXXX.zip解冻后的s2-tiger目录引入Eclipse、「文件→进口→既存的项目」。 在Seasar2的设定基础上、必需要将以下的文件追加到CLASSPATH里面。
lib/s2-tiger-x.x.x.jar


快速上手

S2Container,就是进行Dependency Injection(注:依赖注入——译者)(以后略称为DI)的一个轻量级容器。DI,就是Interface和实装分离,程序相互之间仅通过Interface来会话的一种思考方式。
最初的一步

让我们赶快试一试吧。登场人物如下。
问候语类
返回问候语的字符串。
问候客户端类
从问候类获得问候语(字符串)并输出到终端屏幕。
问候语应用主类
启动用的类。用来组织问候语类和问候语使用者类的组成方式。
Greeting.java

问侯语的Interface。
package examples.di;

public interface Greeting {

    String greet();
}
GreetingImpl.java

问候语的实装。
package examples.di.impl;

import examples.di.Greeting;

public class GreetingImpl implements Greeting {

    public String greet() {
        return "Hello World!";
    }
}
GreetingClient.java

使用问候语的使用者客户端Interface。
package examples.di;

public interface GreetingClient {

    void execute();
}
GreetingClientImpl.java

使用问候语的客户端的实装。不是直接使用这个GreetngImpl(实装),而是通过Greeting(Interface)来实现问候的机能。
package examples.di.impl;

import examples.di.Greeting;
import examples.di.GreetingClient;

public class GreetingClientImpl implements GreetingClient {

    private Greeting greeting;

    public void setGreeting(Greeting greeting) {
        this.greeting = greeting;
    }

    public void execute() {
        System.out.println(greeting.greet());
    }
}

机能提供端和使用端的准备都完成了。下面我们就执行一下试试吧。
GreetingMain.java
package examples.di.main;

import examples.di.Greeting;
import examples.di.impl.GreetingClientImpl;
import examples.di.impl.GreetingImpl;

public class GreetingMain {

    public static void main(String[] args) {
        Greeting greeting = new GreetingImpl();
        GreetingClientImpl greetingClient = new GreetingClientImpl();
        greetingClient.setGreeting(greeting);
        greetingClient.execute();
    }
}

实行结果如下。
Hello World!

象这样机能的使用者(GreetingClientImpl)经由Interface(Greeting)的中介来使用机能,具体的机能对象(既 Interface的实装类)在实行的时候由第三者(在这里是GreetingMain)来提供的情况,就是DI的基本思考方法。

但是,如果象GreetingMain中那样实装类的设定内容直接被写出来的话,一旦实装类需要变更的时候源代码也必须跟着修正。为了避免这个麻 烦,DIContainer就登场了。把实装设定抽出到一个设定文件中,由DIContainer把这个设定文件读入并组织对象运行。

那么,让我们试着把刚才的提到的那个设定文件的内容写一下。S2Container中,设定文件的后缀是".dicon"。
GreetingMain2.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC
    "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">
<components>
    <component name="greeting"
        class="examples.di.impl.GreetingImpl"/>
    <component name="greetingClient"
        class="examples.di.impl.GreetingClientImpl">
        <property name="greeting">greeting</property>
    </component>
</components>



<component name="greeting"
    class="examples.di.impl.GreetingImpl"/>

上文记载的是组件的定义。在这里,相当于如下的Java代码。
Greeting greeting = new GreetingImpl();

component标签的name属性指定了组件的名称,class属性指定了组件的Java类文件名。下文就是greetingClient的设定。
<component name="greetingClient"
    class="examples.di.impl.GreetingClientImpl">
    <property name="greeting">greeting</property>
</component>

property标签的name属性指定了组件Java类中的属性名,标签的定义体则指定了一个组件名称。这个设定相当于如下Java代码。组件名要注意不要用["]括起来。用["]括起来的话就会被当作字符串来处理了。
GreetingClientImpl greetingClient = new GreetingClientImpl();
greetingClient.setGreeting(greeting);

利用S2Container的起动类的内容如下。
GreetingMain2.java
package examples.di.main;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

import examples.di.GreetingClient;

public class GreetingMain2 {

    private static final String PATH =
        "examples/di/dicon/GreetingMain2.dicon";

    public static void main(String[] args) {
        S2Container container =
            S2ContainerFactory.create(PATH);
        container.init();
        GreetingClient greetingClient = (GreetingClient)
            container.getComponent("greetingClient");
        greetingClient.execute();
    }
}

S2Container,是由S2ContainerFactory#create(String path)做成的。更加详细的内容请参照S2Container的生成。

组件(greetingClient),是由S2Container#getComponent(String componentName)的方法取得的。详细内容请参照组件的取得。

实行结果同先前一样表示如下。
Hello World!

经常同DI一起使用的是AOP。AOP是指、将日志等的输出分散到复数个类中的逻辑模块化的一种技术。那么、让我们不修改已经作成的GreetingImpl、GreetingClinetImpl的源代码?试着将日志(追踪)输出。 适用于AOP的设定文件如下。
GreetingMain3.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC
    "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">
<components>
    <include path="aop.dicon"/>
    <component name="greeting"
        class="examples.di.impl.GreetingImpl">
        <aspect>aop.traceInterceptor</aspect>
    </component>
    <component name="greetingClient"
        class="examples.di.impl.GreetingClientImpl">
        <property name="greeting">greeting</property>
        <aspect>aop.traceInterceptor</aspect>
    </component>
</components>

 aop.dicon 文件设定,(在GreetingMain3.dicon上一层目录,否则报找不到aop.dicon的错误)

 文件名: aop.dicon

代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd"
>
<components namespace="aop">
    
<component name="traceInterceptor"
        class
="org.seasar.framework.aop.interceptors.TraceInterceptor"/>
    
<component name="traceThrowsInterceptor"
        class
="org.seasar.framework.aop.interceptors.TraceThrowsInterceptor"/>
</components>


 为了能够正常的显示log信息,请配置好log4j文件log4j.properties(与aop.dicon在同一目录)

 文件名:log4j.properties

log4j.category.org.seasar=DEBUG, C
log4j.additivity.org.seasar=false

log4j.appender.C=org.apache.log4j.ConsoleAppender
log4j.appender.C.Target=System.out
log4j.appender.C.ImmediateFlush=true
log4j.appender.C.layout=org.apache.log4j.PatternLayout
log4j.appender.C.layout.ConversionPattern=%-5p %d [%t] %m%n

 

 

Seasar2中,经常使用的AOP模块在aop.dicon中预先定义。 象下面这样、使用include标签。 更加详细的?敬请参照S2Container定义的分解和引入。
<include path="aop.dicon"/>

对于在组件中适用的AOP来说?我们component标签的字标签 aspect标签的正文中指定AOP的模块名称。aop.traceInterceptor是AOP模块的名字。
<aspect>aop.traceInterceptor</aspect>

AOP的设定如上所述。那么就让我们执行一下GreetingMain3吧。同GreetingMain2不同的仅仅是设定文件的路径而已。
GreetingMain3.java
package examples.di.main;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

import examples.di.GreetingClient;

public class GreetingMain3 {

    private static final String PATH =
        "examples/di/dicon/GreetingMain3.dicon";

    public static void main(String[] args) {
        S2Container container =
            S2ContainerFactory.create(PATH);
        GreetingClient greetingClient = (GreetingClient)
            container.getComponent("greetingClient");
        greetingClient.execute();
    }
}

执行结果如下。可以明白一点,没有修改源代码,日志就被输出了。
DEBUG 2005-10-11 21:01:49,655 [main] BEGIN examples.di.impl.GreetingClientImpl#execute()
DEBUG 2005-10-11 21:01:49,665 [main] BEGIN examples.di.impl.GreetingImpl#greet()
DEBUG 2005-10-11 21:01:49,665 [main] END examples.di.impl.GreetingImpl#greet() : Hello World!
Hello World!
DEBUG 2005-10-11 21:01:49,675 [main] END examples.di.impl.GreetingClientImpl#execute() : null

这样、S2Container的基本使用方法就被掌握了。
更进一步

但是,不管怎么说书写设定文件都是一件麻烦的事啊。在S2Container中,为了尽可能的减少设定文件的记述量、采用了如下的概念。
Convention over Configuration

就是说制定一个适当的规约,遵守这个规约的话?无需什么设定也可以运作。比如说,刚才的设定文件中,象下面这样明确地指定属性的部分存在着。
<component name="greetingClient"
    class="examples.di.impl.GreetingClientImpl">
    <property name="greeting">greeting</property>
</component>

S2Container中、属性的类型是Interface的情形下? 如果要将属性类型的实装组件注册进软件容器中, 不需要什么特殊的设定也可以自动得运作DI的机能。 这就是,如果遵守DI中推荐的所谓“属性类型用Interface定义”的规则,S2Container会自动地处理一切。

虽然一说到规约就容易产生麻烦之类的想法,“推荐而已,如果遵守的话就能使开发愉快”的话,遵守规约的动机就产生了。这才是问题的重点。

如上的设定,可以做如下化简
<component name="greetingClient"
    class="examples.di.impl.GreetingClientImpl">
</component>

实际上?刚才的AOP的例子也适用“Convention over Configuration”。 通常在AOP中,AOP的模块在什么地方适用是由pointcut指定的,S2AOP的情况下? 如果遵守所谓“使用Interface”这个推荐的规约,不指定pointcut,自动的适用于在Interface中定义的所有方法。因为有这个机能, 在刚才的那个例子中,就没有必要指定pointcut。

虽然根据“Convention over Configuration”,DI和AOP的设定可以得以简化,需要处理的组件数增加了、仅仅组件的注册也会变成一个非常累的作业。那么这个组件注册自 动化就叫做组件自动注册机能。 刚才的GreetingImpl、GreetingClientImpl的注册自动化如下。
<component
class="org.seasar.framework.container.autoregister.FileSystemComponentAutoRegister">
    <initMethod name="addClassPattern">
        <arg>"examples.di.impl"</arg>
        <arg>".*Impl"</arg>
    </initMethod>
</component>

FileSystemComponentAutoRegister组件将addClassPattern方法指定的类从文件系统中探寻出来,自动注册到S2Container中。关于initMethod标签,请参照方法函数注入。

addClassPattern方法的第一个参数是想要注册的组件的包的名字。 子包的内容也会用回归的方式检索。第二个参数是类的名字。可以使用正则表达式。也可以用“,”做分隔符指定复数个设定。

根据组件自动注册原则,即使后续追加组件的情况下,也没有必要追加设定,这样手续就大大地简化了。

如果组件的自动化注册可以了,接下来就会想让AOP的注册也自动化了吧。刚才的GreetingImpl、GreetingClientImp的AOP注册自动化的设定如下。
<include path="aop.dicon"/>
...
<component
class="org.seasar.framework.container.autoregister.AspectAutoRegister">
    <property name="interceptor">aop.traceInterceptor</property>
    <initMethod name="addClassPattern">
        <arg>"examples.di.impl"</arg>
        <arg>".*Impl"</arg>
    </initMethod>
</component>

用interceptor属性指定AOP的名称。addClassPattern方法同组件的自动化注册时的用法一样,这里就不做特殊的说明了。 组件自动化注册和AOP自动化注册的例子如下。
GreetingMain4.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN"
    "http://www.seasar.org/dtd/components23.dtd">
<components>
    <include path="aop.dicon"/>

    <component
      class="org.seasar.framework.container.autoregister.FileSystemComponentAutoRegister">
        <initMethod name="addClassPattern">
            <arg>"examples.di.impl"</arg>
            <arg>".*Impl"</arg>
        </initMethod>
    </component>

    <component
      class="org.seasar.framework.container.autoregister.AspectAutoRegister">
        <property name="interceptor">aop.traceInterceptor</property>
        <initMethod name="addClassPattern">
            <arg>"examples.di.impl"</arg>
            <arg>".*Impl"</arg>
        </initMethod>
    </component>
</components>

那么来执行一下GreetingMain4吧。 自动注册的情况下,S2Container#init()和S2Container#destroy()的调用是必要的。
GreetingMain4.java
package examples.di.main;

import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.S2ContainerFactory;

import examples.di.GreetingClient;

public class GreetingMain4 {

    private static final String PATH =
        "examples/di/dicon/GreetingMain4.dicon";

    public static void main(String[] args) {
        S2Container container =
            S2ContainerFactory.create(PATH);
        container.init();
        try {
            GreetingClient greetingClient = (GreetingClient)
                container.getComponent("greetingClient");
            greetingClient.execute();
        } finally {
            container.destroy();
        }
    }
}

执行的结果同GreetingMain3一样如下列出。
DEBUG 2005-10-12 16:00:08,093 [main] BEGIN examples.di.impl.GreetingClientImpl#execute()
DEBUG 2005-10-12 16:00:08,103 [main] BEGIN examples.di.impl.GreetingImpl#greet()
DEBUG 2005-10-12 16:00:08,103 [main] END examples.di.impl.GreetingImpl#greet() : Hello World!
Hello World!
DEBUG 2005-10-12 16:00:08,103 [main] END examples.di.impl.GreetingClientImpl#execute() : null

大多数的情况下?自动注册和自动绑定的组合方式都能顺利的进行下去。不想要自动注册的组件存在的情况下,自动注册组件中准备了addIgnoreClassPattern方法,可以指定自动注册外的组件。

不想要自动绑定的属性存在的情况下,使用Binding备注码,不使用设定文件也可以做细节的调整。

使用Hotswap的话?应用程序在运行中重新书写更换类文件,马上就能够直接测试结果。不需要一个一个地将应用程序在启动,因此开发的效率能够得到大幅度的提高。

现在,关于S2Container的高级使用方法也可以掌握了。这之后嘛,只要根据需要参照对应的操作手册就可以了。
S2Container参考
需要作成的文件

为了使用S2Container,定义文件的做成是必要的。定义文件就像是为了组织组件而制作的设计书一样的东西。形式为XML,后缀为dicon。
S2Container的定义

S2Container的定义、象下面这样。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN"
"http://www.seasar.org/dtd/components23.dtd">
<components>
    <component name="..." class="...">
            ...
    </component>
    <component name="..." class="...">
            ...
    </component>
</components>

DOCTYPE是不能省略的。dicon做成的时候、请将前述例子拷贝粘贴。根是components标签。每一个组件用component标签定 义。用component标签的class属性指定组件的类的全名。在name属性中、指定组件的名称。详细内容请参照S2Container定义标签参 考。
<components>
    <component name="hoge" class="examples.dicon.HogeImpl"/>
</components>
S2Container的生成

S2Container的生成方法有两种。
使用SingletonS2ContainerFactory。
使用S2ContainerFactory。
使用SingletonS2ContainerFactory

使用SingletonS2ContainerFactory的情况下,使用如下方法。

- org.seasar.framework.container.factory.SingletonS2ContainerFactory#init()

定义文件使用的是CLASSPATH所指定的路径中存在的app.dicon。

做成的S2Container,无论在什么地方都是可以从如下方法中取得。

- org.seasar.framework.container.factory.SingletonS2ContainerFactory#getContainer()
SingletonS2ContainerFactory.init();
...
S2Container container = SingletonS2ContainerFactory.getContainer();

定义文件的路径需要被指定的情况下应在调用init()之前执行如下方法。

- org.seasar.framework.container.factory.SingletonS2ContainerFactory#setConfigPath(String Path)

参数path是相对于以CLASSPATH指定的路径为根的定义文件的绝对路径。例如,WEB-INF/classes/aaa.dicon 的情况下就是aaa.dicon,WEB-INF/classes/aaa/bbb/ccc.dicon的情况下就是aaa/bbb /ccc.dicon。分隔符在Windows和Unix下都是/。
private static final String PATH = "aaa/bbb/ccc.dicon";
...
SingletonS2ContainerFactory.setConfigPath(PATH);
SingletonS2ContainerFactory.init();
...
S2Container container = SingletonS2ContainerFactory.getContainer();
使用S2ContainerFactory

使用S2ContainerFactory的场合下,使用如下方法。

- org.seasar.framework.container.factory.S2ContainerFactory#create(String path)

S2Container生成之后需要许呼叫下一个方法。

- org.seasar.framework.container.S2Container#init()
private static final String PATH = "aaa/bbb/ccc.dicon";
...
S2Container container = S2ContainerFactory.create(PATH);
container.init();

用这个方法取得的组件的实例,有必要进行在应用中的管理。
组件的取得

从S2Container中将组件取出来,使用下面的方法。

- org.seasar.framework.container.S2Container#getComponent(Object componentKey)

参数中指定的是组件的类或者是组件的名称。详细的请参照component标签。要指定组件的类,只要是 组件 instanceof 类 的操作返回为true的类就能够指定。但是、S2Container中所指定的类对应了好几个实装的组件的时候,S2Container将不能判断返回哪 一个组件为好,这样就会发生TooManyRegistrationRuntimeException。请指定实装组件为唯一的类。也可以用组件名称取得 组件。这种情况下也是同样,用一个名称的复数个组件被注册的情况下,将发生TooManyRegistrationRuntimeException。指 定组件名的场合下,因为也可能发生拼写错误,所以尽可能的指定组件的类为好。


例)通过指定类来取得组件的场合
S2Container container = S2ContainerFactory.create(PATH);
Hoge hoge = (Hoge) container.getComponent(Hoge.class);

例)通过指定组件名来取得组件场合
S2Container container = S2ContainerFactory.create(PATH);
Hoge hoge = (Hoge) container.getComponent("hoge");
Dependency Injection的类型

在Dependency Injection中,组件的构成所必要的值是用构造函数来设定(Constructor Injection),还是用设定函数来设定(Setter Injection),或者是用初始化函数来设定(Method Injection),这样进行分类。Method Injection是S2Container的本源。S2Container支持以上所有类型和混合类型。
构造函数?注入

对构造函数的参数进行DI,这就是构造函数注入。
S2Container的定义文件中,记述如下内容。
组件的指定
组件,用component标签来组建。用class指定对应的类。
也可以用name属性给组件起名称。
构造函数的参数的指定
组件的构造函数的参数用component标签的子标签arg标签来指定。
值为字符串的时候,用双引号(")括起来。
<components>
    <component name="..." class="...">
          <arg>...</arg>
    </component>
</components>
设定函数?注入

设定函数注入是指对于任意一个属性变量使用设定函数来行使DI。
S2Container的定义文件中作如下内容的记述。
组件的指定
组件的指定同构造函数注入相同。
属性变量的指定
组件的属性变量用component标签的子标签property来指定。
用name属性来指定变量的名称。
<components>
    <component name="..." class="...">
          <property name="...">...</property>
    </component>
</components>
方法函数?注入

方法函数注入是指,通过任意一个函数的调用来完成DI的功能。
S2Container的定义文件中,记述如下内容。
组件的指定
组件的指定同构造函数注入相同。
初始化方法函数的指定
使用initMethod标签,调用组件的任意一个方法函数。在name属性中,指定方法函数的名称。 用arg标签指定参数,name属性省略,在正文中,使用OGNL式也可以。
<components>
    <component name="..." class="...">
        <initMethod name="...">
            <arg>...</arg>
        </initMethod>
    </component>
</components>
S2Container定义的分割和引入

所有的组件用一个文件来设定的话,很快就会变得臃肿而难以管理。因此,S2Container就具有了将组件的定义进行复数个分割的机能和将多个分割的定义文件引入而组织成一个文件的机能。S2Container定义文件的引入方法如下。
<components>
    <include path="bar.dicon"/>
</components>

 

 

英文链接

http://s2container.seasar.org/en/DIContainer.html

http://www.seasar.org/en/tutorial/seasar0.html

 

posted on 2010-06-18 17:06  Likwo  阅读(1111)  评论(0编辑  收藏  举报