活到老,学到老

  BlogJava :: 首页 :: 联系 :: 聚合  :: 管理
  9 Posts :: 1 Stories :: 2 Comments :: 0 Trackbacks

2010年2月22日 #

     摘要:     概念:观察者模式定义了一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。     举个网上商城的例子,比如很多顾客对某个商品感兴趣,把商品收藏,当该商品降价、促销、有货了等事件发生时,就会发Email通知顾客。     UML图...  阅读全文
posted @ 2011-04-07 22:29 simon.shen 阅读(309) | 评论 (1)编辑 收藏

    从今天开始,把常用的设计模式都简单的整理一遍,希望每个星期能至少整理2个模式吧,先从简单的策略模式开始。
    
    概念:它定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法的变化不会影响到使用算法的客户。
    
    策略模式很简单,实际上就是OO中的多态,举个例子,某商场要进行促销,对于普通顾客打88折,对于银卡客户在88折基础上再满400减160,对于金卡客户在88折基础上再满400减200。
    
    
UML图如下所示:
    

    接口DiscountStrategy代码如下:
    
public interface DiscountStrategy {
    
public double discount(double
 sum);
}

    类GeneralDiscountStrategy类代码如下:
    
public class GeneralDiscountStrategy implements DiscountStrategy {

    @Override
    
public double discount(double sum) 
{
        
return sum * 0.88
;
    }


}

    类SilverDiscountStrategy类代码如下:
    
public class SilverDiscountStrategy implements DiscountStrategy {

    @Override
    
public double discount(double sum) 
{
        sum 
= sum * 0.88
;
        
int t = (int) sum / 400
;
        sum 
= sum - t * 160
;
        
return
 sum;
    }

}

    类GoldenDiscountStrategy代码如下:
    
public class GoldenDiscountStrategy implements DiscountStrategy {

    @Override
    
public double discount(double sum) 
{
        sum 
= sum * 0.88
;
        
int t = (int) sum / 400
;
        sum 
= sum - t * 200
;
        
return
 sum;
    }

}

    类Cashier代码如下:
    
public class Cashier {
    
private DiscountStrategy discountStrategy = new
 GeneralDiscountStrategy();

    
public void getDiscountStrategy(CustomerLevel customerLevel) 
{
        
switch (customerLevel) 
{
            
case
 GENERAL:
                discountStrategy 
= new
 GeneralDiscountStrategy();
                
break
;
            
case
 SILVER:
                discountStrategy 
= new
 SilverDiscountStrategy();
                
break
;
            
case
 GOLDEN:
                discountStrategy 
= new
 GeneralDiscountStrategy();
                
break
;
        }

    }


    
public double calculate(double price, int num) {
        
return discountStrategy.discount(price *
 num);
    }

}
posted @ 2011-04-06 22:56 simon.shen 阅读(332) | 评论 (0)编辑 收藏

     摘要:     这篇写一个简单的HelloWorld例子。     首先准备环境,我使用的JDK1.6,1.5应该也可以。还需要去oracle下载JMX RI包,地址为:http://www.oracle.com/technetwork/java/javase/tech/download-jsp-141676.html,下载...  阅读全文
posted @ 2011-04-05 16:20 simon.shen 阅读(2939) | 评论 (1)编辑 收藏

    这是我读了JMX In Action以后的总结,这篇文章是这个系列的第一篇,主要介绍一下什么是JMX,为什么要使用JMX?以及简单阐述一下JMX的架构。
    什么是JMX?
    首先看一下维基百科的定义:JMX(Java Management Extensions,即Java管理扩展)是Java平台上为应用程序、设备、系统等植入管理功能的框架。
JMX可以跨越一系列异构操作系统平台、系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
    随着企业 IT 规模的不断增长,IT 资源(IT resource)数量不断增加,IT 资源的分布也越来越分散。IT资源主要包括网络和应用等。管理网络(硬件)的主要工具是
简单网络管理协议(SNMP),硬件厂商一般都会在设备中提供SNMP hooks,但SNMP开发不是件容易的事情,而且需要掌握特定的语言,对JAVA开发者绝非易事。
    有些企业开发的应用的时候,是不会考虑运行时配置和管理问题。有些企业直接在产品中开发管理工具和界面。也有一些企业直接使用应用服务器或web服务器的管理工具。
    现实世界的IT资源管理需求主要有以下一些:
    1、监控平台和硬件的健康。硬件可以通过SNMP,WEB服务器和应用服务器可以通过他们自己的管理工具。数据库也是一样等。
   2、配置应用层面的资源。比如配置应用在查询结果中每页显示的数量,配置数据库连接池,或者外部请求数等。
   3、收集应用程序的统计信息。如多少成功订单,多少失败订单等。
   4、日志级别,改变日志级别。出问题的时候,可以通过改变日志级别来打印调试信息,快速定位问题。
   5、监视服务器的性能、负载,通过email、sms等通知关键性事件,比如服务器负载超过预设的界限。
    要满足这些需求往往是非常昂贵和困难的,管理员通常需要通过不同的工具来管理不同的服务和硬件。 如果使用JMX,创建一个满足上面需求的管理系统将是廉价、更加灵活、所需时间更短。    
 
    
    使用JMX的好处
     1、非常容易使用。特别是对一个JAVA程序员,要理解SNMP是困难的。而JMX对他来说非常容易。
      2、利用现有的技术:现有的管理工具可以插入JMX,JMX提供了很多协议(SNMP、HTTP)和传输方式(如RMI),如果原有的应用和设备没有提供管理能力,则可以创建JMX代理提供管理能力。
      3、模块化。以模块化的方式创建你的管理环境。
      4、警告、事件和统计信息。
        JMX提供了一套通知系统,充分利用了面向对象优势,通知提供了不仅仅是数据,而是一个有分布式JAVA对象,封装了数据和行为。例如,你发送了一个服务器负载的通知,里面还包含了一个显示负载重要性的级别的机制,说白了就是对象里面有个方法,可以判定负载的严重程度。
      5、快速的监控解决方案。不同的开发人员只需要自己开发MBean,而不需要花很多的时间在相互的协作上面,这些MBean分布在不同的主机上,但可以通过一个管理工具就可以管理所有的这些应用。

    JMX的一些术语
    1、可管理资源(Manageable resource
    可以是任何的应用、设备、或者其他存在的实体,能够被java访问和包装。是被JMX MBean管理的资源。
    2、MBean(Managed Bean)
    是满足某些命名规则和继承JMX 规范的java类,为可管理资源的管理和访问暴露接口。通过属性和行为来暴露接口。有这3类:Standard, Dynamic, and Model MBeans。
    3、MBean Server
    管理一组MBean的JAVA类。是JMX 管理环境核心。是MBean的注册器。
    
4、JMX Agent
    JMX代理是为管理一组MBean提供一系列服务的java进程。是一个MBean Server的容器,它还提供了一些有用的服务:创建MBean之间的关系,动态加载类,简单的监控服务,定时器服务。Agent有一个协议适配器和连接器集合能使外部程序连接到他们。
    
5、Protocol adapters and connectors
    协议适配器和连接器是位于JMX Agent内部的对象。把Agent暴露给管理程序和协议。一个Agent可以有很多适配器和连接器。
    
6、Management application
    连接到JMX Agent的用户应用程序。
    
7、Notification
    通知是Mbean或者Mbean server发送的java对象,他们封装了事件、警告、或者其他的一般信息。其他的Mbean或者java对象可以注册成为监听器来接受通知。
    8、Instrumentation(设备化)
    用MBean暴露一个可管理资源的过程。
    
    JMX架构
    JMX架构主要分为三层:Distributed layerAgent layerInstrumentation layer,如下图所示:
     
 

    1、Distributed layer(分布式层)
      属于JMX架构的最外层,这层主要负责使JMX Agent能对外部程序可用。    又分为两种:一种通过不同的协议(如SNMP,HTTP)来为MBean提供可见性。
另一种是把Agent API暴露给其他的分布式技术如RMI。
    2、The agent layer(代理层)
    它包含的最主要的组件是MBean Server,它还包括4个代理服务使管理MBean更加容易,它们分别是:    定时器(timer)、监控服务、动态MBean加载、和MBean关系服务。Agent可以和被管理的资源在同一个主机上,也可以是远程的。
    3、The instrumentation layer
    这是最靠近资源的一层,它包含了注册在Agent里面的MBean。
    4、Notifications
    除了架构中的三层以外,JMX提供了一个通知机制,类似于JAVA事件模型。通知机制是管理系统的最后必须的组件。Agent和MBean可以使用通知机制来发送警告或信息给管理应用。
    以上就是JMX架构的主要内容,下一篇构建一个HelloWorld的JMX程序。
posted @ 2011-04-03 23:08 simon.shen 阅读(2249) | 评论 (0)编辑 收藏

    这本书的前面三章主要讲了一下基本概念,客户端程序,和Amazon的S3,这篇博客总结一下第四章,个人感觉有很多重要的概念。
    面向资源的架构(The Resource-Oriented Architecture),这里的资源必须要有一个URI,资源和URI的关系:一个资源可能有一个或多个URI,而一个URI只能指定一个资源。

    Restful WS的特性:
    1、可寻址性(Addressability)
        资源通过URI来暴露给用户,可寻址性是最基本的特性。由于可寻址性,你可以把URI保存在你的书签里,你可以把链接发给别人,而不用把Html文件下载下来发给别人,也可以通过URI对资源进行缓存。

    2、无状态性(Statelessness
        无状态性意味着每个HTTP请求是完全隔离的。每次客户端发送请求都必须带上所有服务器端需要的信息。    
        无状态的应用更容易分布到有负责均衡的多台服务器上;无状态性也更容易缓存:缓存工具只需要看这一个请求,和任何其他请求无关。
        应用状态和资源状态(Application State Versus Resource State
        应用状态位于客户端,而资源状态位于服务器端,对于客户端,每个客户端都有各自的应用状态,例如:在google搜索,你可能搜索某个单词且当前页是第3页,我可能搜索另一个单词且在第一页,所以每个客户端都有一个应用状态。当客户端发起请求的时候,必须告诉服务器你的应用状态,比如你当前要看某个单词搜索结果的第几页,服务器端返回结果上有其他链接,这些链接客户端可能作为未来的请求。
        而对于资源状态,对于每个客户端都是相同的,就是服务器上的资源。  
  
    3、表述性(Representations
        表述性,就是资源的表现形式,相同的资源可以有不同的表述性,比如同一个bug列表可以用XML文档表示,也可以用文本方式表示等等。对于同一资源的不同的Representation,如何知道客户端请求哪一种呢?作者建议不同的Representation使用不同的URI。

    4、连通性(Links and Connectedness
        简单点说,就是返回的结果中有对其他资源的链接(URI),比如google搜索,搜索结果可能有其他页的链接。

    5、统一的接口(The Uniform Interface
        也就是说Restful WS使用HTTP的基本方法作为他的方法的表示,主要使用HTTP的四个方法:GET,PUT,DELETE,POST。HEAD和OPTIONS用的比较少。
        取得某个资源的表述的时候使用GET。
        创建一个新的资源的时候,PUT到一个新的URI,或者POST到一个已经存在的URI。
        修改资源,使用PUT到存在的URI。
        删除资源使用DELETE。
        PUT和POST都可以创建新的资源,那有什么区别呢?POST可以创建从属资源,如一个webblog程序通过资源(/weblogs/myweblog)暴露每个blog,而某个blog下面的条目作为从属资源为/weblogs/myweblog/entries/1,当你需要增加一个条目的时候,你可以POST到父资源/weblogs/myweblog,同样PUT也可以完成这个工作,在这里POST和PUT的区别是:当客户端可以控制新资源的URI的时候,则使用PUT,比如blog的下面的某篇文章使用名字来访问,如/weblogs/myweblog/entries/restful_ws_1(这样某个博客下面的文章不能重复),则当你发表一篇新文章的时候,可以PUT到新的URI如/weblogs/myweblog/entries/restful_ws_2来创建资源。而如果客户端不能控制URI的时候,比如blog是通过服务器端某个序列号来访问,客户端是无法知道下一个序号是什么,这时只能使用POST,这种POST如果创建成功,则返回201,响应头中的Location可以保护新创建资源的URI。
        还有一个区别,POST对某个存在的资源更新时,一般是追加(append),比如说对某个日志文件做POST,则把日志追加到原日志的后面。如果是PUT则进行的是替换,所以PUT是等幂的,而POST不是(后面会讲)。

        安全(Safety)
        GET和HEAD方法只是获取资源的表述,所以是安全的。当然也可能有一些副作用,比如有些服务端会记录GET的次数等。

        等幂性(Idempotence)
        等幂性简单点说就是一次请求和多次请求,资源的状态是一样。比如GET和HEAD,不论你请求多少次,资源还是在那里。请注意,DELETE和PUT也是等幂的,以为对同一个资源删除一次或者多次,结果是一样的,就是资源被删除了,不存在了。为什么说PUT也是等幂的?当你PUT一个新资源的时候,资源被创建,再次PUT这个URI的时候,资源还是没变。当你PUT一个存在的资源时,更新了资源,再次PUT的时候,还是更新成这个样子。在PUT更新的时候,不能做相对的更新(依赖资源现在的状态),比如每次对一个数加1,这样资源状态就会变化。应该每次更新成某个数,比如把某个数变成4,则无论多少次PUT,值都是4,这样就是等幂了。
        我们设计Restful WS的时候,GET,HEAD, PUT, DELETE一定要设计成等幂的。由于网络是不可靠的,安全性和等幂性就显得特别重要。如果一次请求,服务器收到处理以后,客户端没有收到相应,客户端会再次请求,如果没有等幂性保障,就会发生意想不到的问题。
        POST是不安全也不等幂的,还是拿weblog的例子,如果两次POST相同的博文,则会产生两个资源,URI可能是这样/weblogs/myweblog/entries/1和/weblogs/myweblog/entries/2,尽管他们的内容是一摸一样的。
        

    

posted @ 2011-03-30 21:59 simon.shen 阅读(2016) | 评论 (0)编辑 收藏

    双亲委派模型
    Java从1.2开始引入双亲委派模型。除了启动类装载器,每个类装载器都有一个双亲。当类装载器装载某个类的时候,首先会委派它的双亲去装载这个类,它的双再委派自己的双亲,直到启动类装载器。
    Java类装载器的结构如下:
     

    1、启动类装载器
    主要负责装载jdk_home/lib目录下的核心api  或 -Xbootclasspath 选项指定的jar包。处于双亲委派的最顶层,该类其实是由C语言编写。
    2、扩展类装载器
    主要负责装载jdk_home/lib/ext目录下的jar包或 -Djava.ext.dirs 指定目录下的jar包
    3、系统类装载器
    主要负责装载ClassPath下的类。
    4、自定义类装载器
    自定义类继承ClassLoader或其子类。可以运行时动态装载某些类。
    
    


    
   
   

posted @ 2010-02-28 18:54 simon.shen 阅读(632) | 评论 (0)编辑 收藏

其实网上已经有很多java Class文件的解析实例的文章,写这篇博客,只是为了自己仔仔细细的按照jvm spec看一边,别无其他。

先上class文件的格式。

ClassFile {
        u4 magic;
        u2 minor_version;
        u2 major_version;
        u2 constant_pool_count;
        cp_info constant_pool[constant_pool_count
-1
];
        u2 access_flags;
        u2 this_class;
        u2 super_class;
        u2 interfaces_count;
        u2 interfaces[interfaces_count];
        u2 fields_count;
        field_info fields[fields_count];
        u2 methods_count;
        method_info methods[methods_count];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }

其中,u2代表2个字节的无符号整数。u4代表4个字节的无符号整数,其他如cp_infofield_info
是一些结构数据,接下去会讲。
这次要解析的是一个非常简单的类:TJ.java,代码如下:
public class TJ
{
    
private final int f1 = 2
;

    
public int m1(int
 i){
        
return i+1
;
    }

    
private void
 m2(){
    }
}

使用jdk1.6编译,产生的二进制类文件如下:

CA FE BA BE 00 00 00 32 00 16 0A 00 04 00 12 09
00 03 00 13 07 00 14 07 00 15 01 00 02 66 31 01
00 01 49 01 00 0D 43 6F 6E 73 74 61 6E 74 56 61
6C 
75 65 03 00 00 00 02 01 00 06 3C 69 6E 69 74
3E 
01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00
0F 4C 
69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
01 00 02 6D 31 01 00 04 28 49 29 49 01 00 02 6D
32 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00

07 54 4A 2E 6A 61 76 61 0C 00 09 00 0A 0C 00 05
00 06 01 00 02 54 4A 01 00 10 6A 61 76 61 2F 6C
61 6E 67 2F 4F 62 6A 65 63 74 00 21 00 03 00 04

00 00 00 01 00 12 00 05 00 06 00 01 00 07 00 00
00 02 00 08 00 03 00 01 00 09 00 0A 00 01 00 0B
00 00 00 26 00 02 00 01 00 00 00 0A 2A B7 00 01

2A 
05 B5 00 02 B1 00 00 00 01 00 0C 00 00 00 0A
00 02 00 00 00 01 00 04 00 03 00 01 00 0D 00
 0E
00 01 00 0B 00 00 00 1C 00 02 00 02 00 00 00 04

1B 
04 60 AC 00 00 00 01 00 0C 00 00 00 06 00 01
00 00 00 06 00 02 00 0F 00 0A 00 01 00 0B 00 00
00 19 00 00 00 01 00 00 00 01 B1 00 00 00 01 00
0C 
00 00 00 06 00 01 00 00 00 0B 00 01 00 10 00
00 00 02 00 11
下面对照上面的格式结构一点点的解析。
CA FE BA BE:头四个字节是魔数,表示这是java class文件。
00 00:次版本为0。
00 32:主版本0x32,表示jdk1.6编译的。Jdk1.5为0x31,jdk1.4为0x30。
00 16:常量池的入口(entry)数量。包括自己本身(这里很奇怪),所以接下来有21项的常量池入口。
我会在每个常量池项的前面表上索引。常量池的第一个字节表示类型。具体类型对照表如下:
Constant Type Value
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
0A 00 04 00 12:【1】,第一个字节为10,所以是CONSTANT_Methodref,它的结构如下:
CONSTANT_Methodref_info {

u1 tag;

u2 class_index;

u2 name_and_type_index;

}

所以,class_index=4,name_and_type_index=12,这两个代表常量池第4项和第12项。

09 00 03 00 13:【2】 这是一个CONSTANT_Fieldref,他的结构和上面的类似class_index=3,name_and_type_index=13

07 00 14:【3】这个是CONSTANT_Class,它的结构如下:

CONSTANT_Class_info {

    
u1 tag;

    
u2 name_index;

    }

name_index为20,指向的是一个utf8的字节码,即TJ,这个后面会看到。

07 00 15: 【4】 也是一个CONSTANT_Class,name_index为21,即java/lang/Object

01 00 02 66 31: 【5】CONSTANT_Utf8,结构如下:

CONSTANT_Utf8_info {

u1 tag;

u2 length;

u1 bytes[length];

}

最后两个字节代表字符串“f1”的utf-8字节码。

01 00 01 49:【6】字符串I

01 00 0D 43 6F 6E 73 74 61 6E 74 56 61 6C 75 65 :【7】字符串ConstantValue

03 00 00 00 02:【8】CONSTANT_Integer,整数值2

01 00 06 3C 69 6E 69 74 3E:【9】字符串<init>

01 00 03 28 29 56:【10】字符串()V

01 00 04 43 6F 64 65:【11】字符串code

01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65:【12】字符串LineNumberTable

01 00 02 6D 31:【13】字符串m1

01 00 04 28 49 29 49 :【14】字符串(I)I,表示一个整数参数且返回整数的方法。

01 00 02 6D 32 :【15】字符串m2

01 00 0A 53 6F 75 72 63 65 46 69 6C 65 :【16】字符串SourceFile

01 00 07 54 4A 2E 6A 61 76 61:【17】字符串TJ.java

0C 00 09 00 0A:【18】CONSTANT_NameAndType,结构如下:

CONSTANT_NameAndType_info {

u1 tag;

u2 name_index;

u2 descriptor_index;

}
name_index=9,代表方法<init>,descriptor_index=10,()V,代表无参且返回void的方法。


0C 00 05 00 06:【19】结构同上,name_index=5,即f1,descriptor_index=6,即整数。

01 00 02 54 4A :【20】字符串TJ

01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74:【21】字符串java/lang/Object

到此,常量池结束。
00 21:类的描述符为public。
00 03 :this class为常量池第三个,TJ,即这个类的名字为TJ
00 04:super class为常量池第四个,java/lang/Object,即它的超类为java.lang.Object
00 00:接口个数0。
00 01:field数量1。
00 12 00 05 00 06 00 01 00 07 00 00 00 02 00 08:field的结构如下
field_info {
     u2 access_flags;
     u2 name_index;
     u2 descriptor_index;
     u2 attributes_count;
     attribute_info attributes[attributes_count];
    }
access_flags为00 12,代表ACC_PRIVATE+ ACC_FINAL

name_index:常量池索引为5的入口,即f1,即类成员的名字为f1
descriptor_index:I,代表integer。
 attributes_count:1个。
attribute_info:
attribute_info {
     u2 attribute_name_index;
     u4 attribute_length;
     u1 info[attribute_length];
}

attribute_name_index:7,即ConstantValue,结构如下
ConstantValue_attribute {
     u2 attribute_name_index;
     u4 attribute_length;
     u2 constantvalue_index;
    }
attribute_length:2
constantvalue_index:2
----------------------------------------下面开始方法

00 03:3个方法。
method_info {
     u2 access_flags;
     u2 name_index;
     u2 descriptor_index;
     u2 attributes_count;
     attribute_info attributes[attributes_count];
    }
--------------------------------------------第一个方法<init>,这个是编译器产生的生成实例的初始化方法。
access_flags:public
name_index:00 09,<init>
descriptor_index:00 0A,()V表示无参数,返回void
attributes_count :00 01,1个
attribute_name_index :00 0B ,code
attribute_length:38个
Code_attribute {
     u2 attribute_name_index;
     u4 attribute_length;
     u2 max_stack;
     u2 max_locals;
     u4 code_length;
     u1 code[code_length];
     u2 exception_table_length;
     {     u2 start_pc;
            u2 end_pc;
            u2  handler_pc;
            u2  catch_type;
     } exception_table[exception_table_length];
     u2 attributes_count;
     attribute_info attributes[attributes_count];
    }
max_stack: 00 02
max_locals: 00 01
code_length: 00 00 00 0A,10
code: 2A B7 00 01 2A 05 B5 00 02 B1,指令
exception table length:00 00
attributes_count:1
attribute_name_index:00 0C,LineNumberTable
LineNumberTable_attribute {
     u2 attribute_name_index;
     u4 attribute_length;
     u2 line_number_table_length;
     {  u2 start_pc;     
        u2 line_number;     
     } line_number_table[line_number_table_length];
    }
attribute_length:10
line_number_table_length:2
start_pc:00 00
line_number:00 01
tart_pc:00 04
line_number:00 03
到此第一个方法结束。
----------------------------------------------------------------------第二个方法开始
access_flags00 01public
name_index:00 0D,m1
desc_index:00 0E,(I)I,有一个整数参数,返回一个整数。
00 01:一个attr
00 0B:code
00 00 00 1C:attr_length:28
Code_atrr:28个字节,不分析了和上面的方法相同。

----------------------------------------------------------------------第三个方法
00 02:private
00 0F:m2
00 0A: ()V,无参,返回void
00 01:一个attr
00 0B:code
00 00 00 19:attr_length  25
接下去的25个字节是Code_atrr,同样不分析了。
------------------------------------------------------------------
00 01:1个类的attr
00 10:SourceFile
00 00 00 02:len=2
00 11:17,TJ.java

posted @ 2010-02-22 19:30 simon.shen 阅读(1393) | 评论 (0)编辑 收藏