小菜毛毛技术分享

与大家共同成长

  BlogJava :: 首页 :: 联系 :: 聚合  :: 管理
  164 Posts :: 141 Stories :: 94 Comments :: 0 Trackbacks

#

1)JSON简介
2)JSON/LIST转换
3)JSON/MAP转换
4)JSON/动态Bean转换
5)JSON/静态Bean转换
6)JSON/XML输出

1.JSON简介
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,基于JavaScript,但是不仅仅限于此。
详情可以参考www.json.org
例如一段XML
<?xml version="1.0" encoding="utf-8"?>
 <shop>
  <name>饭店</name>
    <city>北京</city>
 </shop>
用JSON表示如下:
{
  "name":"饭店",
  "city":"北京"
}
XML的解析得考虑子节点父节点关系,而JSON的解析难度相当低,很多控件,尤其是ajax相关的数据交换很多都用json.

2)JSON/LIST转换
本教程解析采用的json-lib,官方网站为http://json-lib.sourceforge.net/,本教程参考官方教程
环境需要配置的jar如下
commons-beanutils和ezmorph控制反射
commons-collections是apachecommons的子项目,扩展了java集合类
commons-lang扩展了java.lang包
commons-logging日志类
xom是xml解析类,可以参考www.xom.nu
junit单元测试用的jar
json-lib核心jar
项目文件夹中拥有货物参数(Shop)和货物参数列表(ShopList)两个实体
Shop包含name和property两个字段,ShopList包含Shop的列表
对应的json是
String s = "[{name:'重量',property:'p1'},{name:'尺寸',property:'p2'},{name:'显卡类型',property:'p3'},{name:'硬盘容量',property:'p4'},{name:'处理器',property:'p5'},{name:'内存',property:'p6'},{name:'型号',property:'p7'},{name:'货号',property:'p8'},{name:'品牌',property:'p9'}]";
把这样的数据结构作为用户定义个人信息存入数据库可以达到个性化参数的作用,
比如shopex的数据库中很多表就是用的json数据类型。因为用户自己添加的参数的长度是不固定的
比如上述例子就拥有9个用户自定义的参数
当用户需要填写这些参数的时候,需要转化为list,然后在struts2的view去显示
完成的代码可以参考附件的ArrayUtil文件
核心代码仅仅就一行JSONArray jsonArray = JSONArray.fromObject(s);
得到这个jsonArray后要转化为ArrayList,需要用循环遍历,如下
for (int i = 0; i < jsonArray.size(); i++) {
   Object o = jsonArray.get(i);
   JSONObject jsonObject = JSONObject.fromObject(o);
   Shop Person = (Shop) JSONObject.toBean(jsonObject, Shop.class);
   list.add(Person);
}
然后得到的list就是普通的ArrayList了

3)JSON/MAP转换
当我们初始化完一个map,放入json可以直接放入
Map<String, String> map = new HashMap<String, String>();
map.put("name", "重量");
map.put("property", "p1");
JSONObject jsonObject = JSONObject.fromObject(map);
核心代码为
JSONObject jsonObject = JSONObject.fromObject(map);
JsonLib会自动映射
完成例子见附件MapUtil.java


4)JSON/动态Bean转换
所谓动态bean即是java运行的时候根据情况创建的,而不是程序员已经好了的Bean
JsonLib会自动根据Json格式数据创建字段,然后创建一个包含这些字段的Object
本例子中采用JUNIT做单元测试验证,见DynamicBean.java
String s = "{name:'重量',property:'p1'}";
JSONObject jsonObject = JSONObject.fromObject(s);
Object bean = JSONObject.toBean(jsonObject);
assertEquals(jsonObject.get("name"), PropertyUtils.getProperty(bean,"name"));
assertEquals(jsonObject.get("property"), PropertyUtils.getProperty(bean,"property"));


5)JSON/静态Bean转换(StaticBean.java)
JSONLIB在转换的时候会自动查找关系,比如子类和父类
例如JSON数据源
String s = "{'shopList':[{name:'重量',property:'p1'},{name:'尺寸',property:'p2'},{name:'显卡类型',property:'p3'},{name:'硬盘容量',property:'p4'},{name:'处理器',property:'p5'},{name:'内存',property:'p6'},{name:'型号',property:'p7'},{name:'货号',property:'p8'},{name:'品牌',property:'p9'}]}";
存入Map
map.put("shopList", Shop.class);
ShopList shopList = (ShopList) JSONObject.toBean(JSONObject.fromObject(s), ShopList.class, map);
JSONObject.toBean()方法的三个参数分别表示数据源对应的JSON对象,转化后的对象ShopList和数据源map
然后这样也可以取得ShopList
这种方法和动态转换的区别在于,动态转换仅仅只是转为Object
而静态转换是转换为已经定义过的实体类,会自动映射(这点类似Ibatis)

6)JSON/XML输出
如果自己用String的方法转化为XML输出要写很多代码,然后条用JSONLIB,核心代码仅仅一步
String xmlObject = xmlSerializer.write(object);
比如
String s = "{name:'重量',property:'p1'}";
XMLSerializer xmlSerializer = new XMLSerializer();
JSONObject object = JSONObject.fromObject(s);
String xmlObject = xmlSerializer.write(object);
System.out.println(xmlObject);

输出结果为
<?xml version="1.0" encoding="UTF-8"?>
<o>
  <name type="string">重量</name>
  <property type="string">p1</property>
</o>



posted @ 2009-08-29 00:39 小菜毛毛 阅读(2434) | 评论 (1)编辑 收藏

Java EE应用的性能问题对严肃的项目和产品来说是一个非常重要的问题。特别是企业级的应用,并发用户多,数据传输量大,业务逻辑复杂,占用系统资源多,因此性能问题在企业级应用变得至关重要,它和系统的稳定性有着直接的联系。更加重要的是,性能好的应用在完成相同任务的条件下,能够占用更少的资源,获得更好的用户体验,换句话说,就是能够节省费用和消耗,获得更高的利润。

要获得更好的性能,就需要对原来的系统进行性能调优。对运行在Glassfish上的JavaEE应用,调优是一件相对复杂的事情。在调优以前必须要认识到:对JavaEE的系统,调优是多层次的。一个JavaEE的应用其实是整个系统中很少的一部分。开发人员所开发的JavaEE程序,无论是JSP还是 EJB,都是运行在JavaEE应用服务器(Glassfish)之上。而应用服务器本身也是Java语言编写的,需要运行在Java虚拟机之上。 Java虚拟机也只不过是操作系统的一个应用而已,和其他的应用(如Apache)对于操作系统来说没有本质的区别。而操作系统却运行在一定的硬件环境中,包括CPU,内存,网卡和硬盘等等。在这么多的层次中,每一个层次的因素都会影响整个系统的性能。因此,对一个系统的调优,事实上需要同时对每个层次都要调优。JavaEE应用性能调优不仅仅和Glassfish有关,Java语言有关,还要和操作系统以及硬件都有关系,需要调优者有综合的知识和技能。这些不同层面的方法需要综合纵效,结合在一起灵活使用,才能快速有效的定位性能瓶颈。下面是一些具体的案例分析:

 

内存泄漏问题

        某个JavaEE应用运行在8颗CPU的服务器上。上线运行发现性能不稳定。性能随着时间的增加而越来越慢。通过操作系统的工具(mpstat),发现在系统很慢的时候,只有一颗CPU很忙,其他的CPU都很空闲。因此怀疑是Java虚拟机经常进行内存回收,因为虚拟机在内存回收的时候,有的回收算法通常只能运行在一个CPU上。通过Java虚拟机的工具“jstat”可以清楚的看到,Java虚拟机进行内存回收的频率非常高,几乎每5秒中就有一次,每次回收的时间为2秒钟。另外,通过“jstat”的输出还发现每次回收释放的内存非常有限,大多数对象都无法回收。这种现象很大程度上暗示着内存泄漏。使用 Java虚拟机的工具“jmap”来获得当前的一个内存映象。发现有很多(超过10000)个的session对象。这是不正常的一个现象。一般来说, session对应于一个用户的多次访问,当用户退出的时候,session就应该失效,对象应该被回收。当我们和这个系统的开发工程师了解有关 session的设置,发现当他们部署应用的时候,竟然将session的timeout时间设置为50分钟,并且没有提供logout的接口。这样的设置下,每个session的数据都会保存50分钟才会被回收。根据我们的建议,系统提供了logout的链接,并且告诉用户如果退出应用,应该点击这个 logout的链接;并且将session的timeout时间修改为5分钟。通过几天的测试,证明泄漏的问题得到解决。

 

数据库连接池问题

        某财务应用运行在JavaEE服务器上,后台连接Oracle数据库。并发用户数量超过100人左右的时候系统停止响应。通过操作系统层面的进程监控工具发现进程并没有被杀死或挂起,而CPU使用率几乎为零。那么是什么原因导致系统停止响应用户请求呢?我们利用Java虚拟机的工具(kill -3 pid)将当前的所有线程状态DUMP出来,发现JavaEE服务器的大部分处理线程都在等待数据库连接池的连接,而那些已经获得数据库连接的线程却处于阻塞状态。数据库管理员应要求检查了数据库的状态,发现所有的连接的session都处于死锁状态。显然,这是因为数据库端出现了死锁的操作,阻塞了那些有数据库操作的请求,占用了所有数据库连接池中的连接。后续的请求如果还要从连接池中获取连接,就会阻塞在连接池上。当解决数据库死锁的问题之后,性能问题迎刃而解。

 

大对象缓存问题

        电信应用运行在64位Java虚拟机上,系统运行得很不稳定,系统经常停止响应。使用进程工具查看,发现进程并没有被杀死或挂起。利用Java虚拟机的工具发现系统在长时间的进行内存回收,内存回收的时间长达15分钟,整个系统在内存回收的时候就像挂起一样。另外还观察到系统使用了12G的内存(因为是 64位虚拟机所以突破了4G内存的限制)。从开发人员那里了解到,这个应用为了提高性能,大量使用了对象缓存,但是事与愿违,在Java中使用过多的内存,虽然在正常运行的时候能够获得很好的性能,但是会大大增加内存回收的时间。特别是对象缓存,本系统使用了8G的缓存空间,共缓存了6000多万个对象,对这些对象的遍历导致了长时间的内存回收。根据我们的建议,将缓存空间减少到1G,并调整回收算法(使用增量回收的算法),使得系统由于内存回收而造成的最大停顿时间减少到4秒,基本满足用户的需求。


外部命令问题

        数字校园应用运行在4CPU的Solaris10服务器上,中间件为JavaEE服务器。系统在做大并发压力测试的时候,请求响应时间比较慢,通过操作系统的工具(mpstat)发现CPU使用率比较高。并且系统占用绝大多数的CPU资源而不是应用本身。这是个不正常的现象,通常情况下用户应用的CPU占用率应该占主要地位,才能说明系统是正常工作。通过Solaris 10的Dtrace脚本,我们查看当前情况下哪些系统调用花费了最多的CPU资源,竟然发现最花费CPU的系统调用是“fork”。众所周知, “fork”系统调用是用来产生新的进程,在Java虚拟机中只有线程的概念,绝不会有进程的产生。这是个非常异常的现象。通过本系统的开发人员,我们找到了答案:每个用户请求的处理都包含执行一个外部shell脚本,来获得系统的一些信息。这是通过Java的“Runtime.getRuntime ().exec”来完成的,但是这种方法在Java中非常消耗资源。Java虚拟机执行这个命令的方式是:首先克隆一个和当前虚拟机一样的进程,再用这个新的进程去执行外部命令,最后再退出这个进程。如果频繁执行这个操作,系统的消耗会很大,不仅在CPU,内存操作也很重。用户根据建议去掉这个shell 脚本执行的语句,系统立刻回复了正常。


文件操作问题

        内容管理(CMS)系统运行在JavaEE服务器上,当系统长时间运行以后,性能非常差,用户请求的延时比系统刚上线的时候要大很多,并且用户的并发量很小,甚至是单个用户也很慢。通过操作系统的工具观察,一切都很正常,CPU利用率不高,IO也不是很大,内存很富余,网络几乎没有压力(因为并发用户少)。先不考虑线程互锁的问题,因为单个用户性能也不好。通过Java虚拟机观察也没有发现什么问题(内存回收很少发生)。这使得我们不得不使用代码跟踪器来全程跟踪代码。我们采用了Netbeans的Profiler,跟踪的结果非常意外,用户请求的90%的时间在创建新文件。从系统设计人员了解到,此系统使用了一个目录用于保存所有上传和共享的文件,文件用其命名方式来唯一区别于其他文件。我们查看了那个文件目录,发现该目录下已经拥有80万个文件了。这时候我们才定位到问题了:在同个目录下放置太多的文件,在创建新文件的时候,系统的开销是比较大的,例如为了防止重名,文件系统会遍历当前目录下所有的文件名等等。根据我们的建议,将文件分类保存在不同的目录下,性能有了大幅度的提高。


高速缓存命中率问题

        运行在JavaEE服务器上的ERP系统,在CPU充分利用的情况下性能仍然不太好。从操作系统层面上观察不到什么大问题,而且ERP系统过于复杂,代码跟踪比较困难。于是进行了CPU状态的进一步检查,发现CPU的TLB命中率不是很高,于是对Java虚拟机的启动参数进行了修改,强迫虚拟机使用大尺寸的内存页面,提高TLB的命中率。下面的参数是在Sun的HOTSPOT中调整大尺寸(4M)页面的设置:
-XX:+AggressiveHeap
-XX:LargePageSizeInBytes=256m
通过调整,TLB命中明显提高,性能也得到近40%的提升。


转载之:http://developers.sun.com.cn/blog/yutoujava/entry/8

posted @ 2009-08-29 00:30 小菜毛毛 阅读(319) | 评论 (0)编辑 收藏

1.使用DNS轮询.
2.使用Apache R-proxy方式。
3.使用Apache mod_jk方式.
 
DNS轮询的缺点是,当集群中某台服务器停止之后,用户由于dns缓存的缘故,便无法访问服务,
必须等到dns解析更新,或者这台服务器重新启动。
还有就是必须把集群中的所有服务端口暴露给外界,没有用apache做前置代理的方式安全,
并且占用大量公网IP地址,而且tomcat还要负责处理静态网页资源,影响效率。
优点是集群配置最简单,dns设置也非常简单。
 
R-proxy的缺点是,当其中一台tomcat停止运行的时候,apache仍然会转发请求过去,导致502网关错误。
但是只要服务器再启动就不存在这个问题。
 
mod_jk方式的优点是,Apache 会自动检测到停止掉的tomcat,然后不再发请求过去。
缺点就是,当停止掉的tomcat服务器再次启动的时候,Apache检测不到,仍然不会转发请求过去。
 
R-proxy和mod_jk的共同优点是.可以只将Apache置于公网,节省公网IP地址资源。
可以通过设置来实现Apache专门负责处理静态网页,让Tomcat专门负责处理jsp和servlet等动态请求。
共同缺点是:如果前置Apache代理服务器停止运行,所有集群服务将无法对外提供。
R-proxy和mod_jk对静态页面请求的处理,都可以通设置来选取一个尽可能优化的效果。
这三种方式对实现最佳负载均衡都有一定不足,mod_jk相对好些,可以通过设置lbfactor参数来分配请求任务,但又因为mod_jk2方式不被推荐,mod_jk2已经不再被更新了。郁闷中……
posted @ 2009-08-29 00:29 小菜毛毛 阅读(593) | 评论 (0)编辑 收藏

为应用程序添加搜索能力经常是一个常见的需求。本文介绍了一个框架,开发者可以使用它以最小的付出实现搜索引擎功能,理想情况下只需要一个配置文件。该框架基于若干开源的库和工具,如 Apache Lucene,Spring 框架,cpdetector 等。它支持多种资源。其中两个典型的例子是数据库资源和文件系统资源。Indexer 对配置的资源进行索引并传输到中央服务器,之后这些索引可以通过 API 进行搜索。Spring 风格的配置文件允许清晰灵活的自定义和调整。核心 API 也提供了可扩展的接口。
引言

为应用程序添加搜索能力经常是一个常见的需求。尽管已经有若干程序库提供了对搜索基础设施的支持,然而对于很多人而言,使用它们从头开始建立一个搜索引擎将是一个付出不小而且可能乏味的过程。另一方面,很多的小型应用对于搜索功能的需求和应用场景具有很大的相似性。本文试图以对多数小型应用的适用性为出发点,用 Java 语言构建一个灵活的搜索引擎框架。使用这个框架,多数情形下可以以最小的付出建立起一个搜索引擎。最理想的情况下,甚至只需要一个配置文件。特殊的情形下,可以通过灵活地对框架进行扩展满足需求。当然,如题所述,这都是借助开源工具的力量。


基础知识

Apache Lucene 是开发搜索类应用程序时最常用的 Java 类库,我们的框架也将基于它。为了下文更好的描述,我们需要先了解一些有关 Lucene 和搜索的基础知识。注意,本文不关注索引的文件格式、分词技术等话题。

什么是搜索和索引

从用户的角度来看,搜索的过程是通过关键字在某种资源中寻找特定的内容的过程。而从计算机的角度来看,实现这个过程可以有两种办法。一是对所有资源逐个与关键字匹配,返回所有满足匹配的内容;二是如同字典一样事先建立一个对应表,把关键字与资源的内容对应起来,搜索时直接查找这个表即可。显而易见,第二个办法效率要高得多。建立这个对应表事实上就是建立逆向索引(inverted index)的过程。
Lucene 基本概念

Lucene 是 Doug Cutting 用 Java 开发的用于全文搜索的工具库。在这里,我假设读者对其已有基本的了解,我们只对一些重要的概念简要介绍。要深入了解可以参考 参考资源 中列出的相关文章和图书。下面这些是 Lucene 里比较重要的类。
Document:索引包含多个 Document。而每个 Document 则包含多个 Field 对象。Document 可以是从数据库表里取出的一堆数据,可以是一个文件,也可以是一个网页等。注意,它不等同于文件系统中的文件。
Field:一个 Field 有一个名称,它对应 Document的一部分数据,表示文档的内容或者文档的元数据(与下文中提到的资源元数据不是一个概念)。一个 Field 对象有两个重要属性:Store ( 可以有 YES, NO, COMPACT 三种取值 ) 和 Index ( 可以有 TOKENIZED, UN_TOKENIZED, NO, NO_NORMS 四种取值 )
Query:抽象了搜索时使用的语句。
IndexSearcher:提供Query对象给它,它利用已有的索引进行搜索并返回搜索结果。
Hits:一个容器,包含了指向一部分搜索结果的指针。
使用 Lucene 来进行编制索引的过程大致为:将输入的数据源统一为字符串或者文本流的形式,然后从数据源提取数据,创建合适的 Field 添加到对应该数据源的 Document 对象之中。


系统概览

要建立一个通用的框架,必须对不同情况的共性进行抽象。反映到设计需要注意两点。一是要提供扩展接口;二是要尽量降低模块之间的耦合程度。我们的框架很简单地分为两个模块:索引模块和搜索模块。索引模块在不同的机器上各自进行对资源的索引,并把索引文件(事实上,下面我们会说到,还有元数据)统一传输到同一个地方(可以是在远程服务器上,也可以是在本地)。搜索模块则利用这些从多个索引模块收集到的数据完成用户的搜索请求。

图 1 展现了整体的框架。可以看到,两个模块之间相对是独立的,它们之间的关联不是通过代码,而是通过索引和元数据。在下文中,我们将会详细介绍如何基于开源工具设计和实现这两个模块。


图 1. 系统架构图


建立索引

可以进行索引的对象有很多,如文件、网页、RSS Feed 等。在我们的框架中,我们定义可以进行索引的一类对象为资源。从实现细节上来说,从一个资源中可以提取出多个 Document 对象。文件系统资源和数据库结果集资源都是资源的代表性例子。

前面提到,从资源中收集到的索引被统一传送到同一个地方,以被搜索模块所用。显然除了索引之外,搜索模块需要有对资源更多的了解,如资源的名称、搜索该资源后搜索结果的呈现格式等。这些额外的附加信息称为资源的元数据。元数据和索引数据一同被收集起来,放置到某个特定的位置。

简要地介绍过资源的概念之后,我们首先为其定义一个 Resource 接口。这个接口的声明如下。


清单 1. Resource 接口
public interface Resource {
// RequestProcessor 对象被动地从资源中提取 Document,并返回提取的数量
public int extractDocuments(ResourceProcessor processor);

// 添加的 DocumentListener 将在每一个 Document 对象被提取出时被调用
public void addDocumentListener(DocumentListener l);

// 返回资源的元数据
public ResourceMetaData getMetaData();
}


其中元数据包含的字段见下表。在下文中,我们还会对元数据的用途做更多的介绍。


表 1. 资源元数据包含的字段
属性 类型 含义
resourceName String 资源的唯一名称
resourceDescription String 资源的介绍性文字
hitTextPattern String 当文档被搜索到时,这个 pattern 规定了结果显示的格式
searchableFields String[] 可以被搜索的字段名称

而 DocumentListener 的代码如下。


清单 2. DocumentListener 接口
public interface DocumentListener extends EventListener {
public void documentExtracted(Document doc);
}



为了让索引模块能够知道所有需要被索引的资源,我们在这里使用 Spring 风格的 XML 文件配置索引模块中的所有组件,尤其是所有资源。您可以在 下载部分 查看一个示例配置文件。

为什么选择使用 Spring 风格的配置文件?

这主要有两个好处:

仅依赖于 Spring Core 和 Spring Beans 便免去了定义配置机制和解析配置文件的负担;
Spring 的 IoC 机制降低了框架的耦合性,并使扩展框架变得简单;



基于以上内容,我们可以大致描述出索引模块工作的过程:

首先在 XML 配置的 bean 中找出所有 Resource 对象;
对每一个调用其 extractDocuments() 方法,这一步除了完成对资源的索引外,还会在每次提取出一个 Document 对象之后,通知注册在该资源上的所有 DocumentListener;
接着处理资源的元数据(getMetaData() 的返回值);
将缓存里的数据写入到本地磁盘或者传送给远程服务器;

在这个过程中,有两个地方值得注意。

第一,对资源可以注册 DocumentListener 使得我们可以在运行时刻对索引过程有更为动态的控制。举一个简单例子,对某个文章发布站点的文章进行索引时,一个很正常的要求便是发布时间更靠近当前时间的文章需要在搜索结果中排在靠前的位置。每篇文章显然对应一个 Document 对象,在 Lucene 中我们可以通过设置 Document 的 boost 值来对其进行加权。假设其中文章发布时间的 Field 的名称为 PUB_TIME,那么我们可以为资源注册一个 DocumentListener,当它被通知时,则检测 PUB_TIME 的值,根据距离当前时间的远近进行加权。

第二点很显然,在这个过程中,extractDocuments() 方法的实现依不同类型的资源而各异。下面我们主要讨论两种类型的资源:文件系统资源和数据库结果集资源。这两个类都实现了上面的 接口。

文件系统资源

对文件系统资源的索引通常从一个基目录开始,递归处理每个需要进行索引的文件。该资源有一个字符串数组类型的 excludedFiles 属性,表示在处理文件时需要排除的文件绝对路径的正则表达式。在递归遍历文件系统树的同时,绝对路径匹配 excludedFiles 中任意一项的文件将不会被处理。这主要是考虑到一般我们只需要对一部分文件夹(比如排除可能存在的备份目录)中的一部分文件(如 doc, ppt 文件等)进行索引。

除了所有文件共有的文件名、文件路径、文件大小和修改时间等 Field,不同类型的文件需要有不同的处理方法。为了保留灵活性,我们使用 Strategy 模式封装对不同类型文件的处理方式。为此我们抽象出一个 DocumentBuilder 的接口,该接口仅定义了一个方法如下:


清单 3. DocumentBuilder 接口
public interface DocumentBuilder {
Document buildDocument(InputStream is);
}

什么是 Strategy 模式?

根据 Design patterns: Elements of reusable object orientated software 一书:Strategy 模式“定义一系列的算法,把它们分别封装起来,并且使它们相互可以替换。这个模式使得算法可以独立于使用它的客户而变化。”


不同的 DocumentBuilder(Strategy) 用于从一个输入流中读取数据,处理不同类型的文件。对于常见的文件格式来说,都有合适的开源工具帮助进行解析。在下表中我们列举一些常见文件类型的解析办法。

文件类型 常用扩展名 可以使用的解析办法
纯文本文档 txt 无需类库解析
RTF 文档 rtf 使用 javax.swing.text.rtf.RTFEditorKit 类
Word 文档(非 OOXML 格式) doc Apache POI (可配合使用 POI Scratchpad)
PowerPoint 演示文稿(非 OOXML 格式) xls Apache POI (可配合使用 POI Scratchpad)
PDF 文档 pdf PDFBox(可能中文支持欠佳)
HTML 文档 htm, html JTidy, Cobra

这里以 Word 文件为例,给出一个简单的参考实现。


清单 4. 解析纯文本内容的实现
// WordDocument 是 Apache POI Scratchpad 中的一个类
Document buildDocument(InputStream is) {
String bodyText = null;
try {
WordDocument wordDoc = new WordDocument(is);
StringWriter sw = new StringWriter();
wordDoc.writeAllText(sw);
sw.close();
bodyText = sw.toString();
} catch (Exception e) {
throw new DocumentHandlerException("Cannot extract text from a Word document", e);
}
if ((bodyText != null) && (bodyText.trim().length() > 0)) {
Document doc = new Document();
doc.add(new Field("body", bodyText, Field.Store.YES, Field.Index.TOKENIZED));
return doc;
}
return null;
}



那么如何选择合适的 Strategy 来处理文件呢?UNIX 系统下的 file(1) 工具提供了从 magicnumber 获取文件类型的功能,我们可以使用 Runtime.exec() 方法调用这一命令。但这需要在有 file(1) 命令的情况下,而且并不能识别出所有文件类型。在一般的情况下我们可以简单地根据扩展名来使用合适的类处理文件。扩展名和类的映射关系写在 properties 文件中。当需要添加对新的文件类型的支持时,我们只需添加一个新的实现 DocumentBuilder 接口的类,并在映射文件中添加一个映射关系即可。

数据库结果集资源

大多数应用使用数据库作为永久存储,对数据库查询结果集索引是一个常见需求。

生成一个数据库结果集资源的实例需要先提供一个查询语句,然后执行查询,得到一个结果集。这个结果集中的内容便是我们需要进行索引的对象。extractDocuments 的实现便是为结果集中的每一行创建一个 Document 对象。和文件系统资源不同的是,数据库资源需要放入 Document 中的 Field 一般都存在在查询结果集之中。比如一个简单的文章发布站点,对其后台数据库执行查询 SELECT ID, TITLE, CONTENT FROM ARTICLE 返回一个有三列的结果集。对结果集的每一行都会被提取出一个 Document 对象,其中包含三个 Field,分别对应这三列。

然而不同 Field 的类型是不同的。比如 ID 字段一般对应 Store.YES 和 Index.NO 的 Field;而 TITLE 字段则一般对应 Store.YES 和 Index.TOKENIZED 的 Field。为了解决这个问题,我们在数据库结果集资源的实现中提供一个类型为 Properties 的 fieldTypeMappings 属性,用于设置数据库字段所对应的 Field 的类型。对于前面的情况来说,这个属性可能会被配置成类似这样的形式:

ID = YES, NO
TITLE = YES, TOKENIZED
CONTENT = NO, TOKENIZED


配合这个映射,我们便可以生成合适类型的 Field,完成对结果集索引的工作。


收集索引

完成对资源的索引之后,还需要让索引为搜索模块所用。前面我们已经说过这里介绍的框架主要用于小型应用,考虑到复杂性,我们采取简单地将分布在各个机器上的索引汇总到一个地方的策略。

汇总索引的传输方式可以有很多方案,比如使用 FTP、HTTP、rsync 等。甚至索引模块和搜索模块可以位于同一台机器上,这种情况下只需要将索引进行本地拷贝即可。同前面类似,我们定义一个 Transporter 接口。


清单 5. Transporter 接口
public interface Transporter {
public void transport(File file);
}


以 FTP 方式传输为例,我们使用 Commons Net 完成传输的操作。

public void transport(File file) throws TransportException {
FTPClient client = new FTPClient();
client.connect(host);
client.login(username, password);
client.changeWorkingDirectory(remotePath);
transportRecursive(client, file);
client.disconnect();
}

public void transportRecursive(FTPClient client, File file) {
if (file.isFile() && file.canRead()) {
client.storeFile(file.getName(), new FileInputStream(file));
} else if (file.isDirectory()) {
client.makeDirectory(file.getName());
client.changeWorkingDirectory(file.getName());
File[] fileList = file.listFiles();
for (File f : fileList) {
transportRecursive(client, f);
}
}
}



对其他传输方案也有各自的方案进行处理,具体使用哪个 Transporter 的实现被配置在 Spring 风格的索引模块配置文件中。传输的方式是灵活的。比如当需要强调安全性时,我们可以换用基于 SSL 的 FTP 进行传输。所需要做的只是开发一个使用 FTP over SSL 的 Transporter 实现,并在配置文件中更改 Transporter 的实现即可。

进行搜索

在做了这么多之后,我们开始接触和用户关联最为紧密的搜索模块。注意,我们的框架不包括一个基于已经收集好的索引进行搜索是个很简单的过程。Lucene 已经提供了功能强大的 IndexSearcher 及其子类。在这个部分,我们不会再介绍如何使用这些类,而是关注在前文提到过的资源元数据上。元数据从各个资源所在的文件夹中读取得到,它在搜索模块中扮演重要的角色。

构建一个查询

对不同资源进行搜索的查询方法并不一样。例如搜索一个论坛里的所有留言时,我们关注的一般是留言的标题、作者和内容;而当搜索一个 FTP 站点时,我们更多关注的是文件名和文件内容。另一方面,我们有时可能会使用一个查询去搜索多个资源的结果。这正是之前我们在前面所提到的元数据中 searchableFields 和 resourceName 属性的作用。前者指出一个资源中哪些字段是参与搜索的;后者则用于在搜索时确定使用哪个或者哪些索引。从技术细节来说,只有有了这些信息,我们才可以构造出可用的 Query 对象。

呈现搜索结果

当从 IndexSearcher 对象得到搜索结果(Hits)之后,当然我们可以直接从中获取需要的值,再格式化予以输出。但一来格式化输出搜索结果(尤其在 Web 应用中)是个很常见的需求,可能会经常变更;二来结果的呈现格式应该是由分散的资源各自定义,而不是交由搜索模块来定义。基于上面两个原因,我们的框架将使用在资源收集端配置结果输出格式的方式。这个格式由资源元数据中的 hitTextPattern 属性定义。该属性是一个字符串类型的值,支持两种语法

形如 ${field_name} 的子字符串都会被动态替换成查询结果中各个 Document 内 Field 的值。
形如 $function(...) 的被解释为函数,括号内以逗号隔开的符号都被解释成参数,函数可以嵌套。
例如搜索“具体”返回的搜索结果中包含一个 Document 对象,其 Field 如下表:

Field 名称 Field 内容
url http://example.org/article/1.html
title 示例标题
content 这里是具体的内容。

那么如果 hitTextPatten 被设置为“${title}
$highlight(${content}, 5, "", "")”,返回的结果经浏览器解释后可能的显示结果如下(这只是个演示链接,请不要点击):

示例标题
这里是具体...

上面提到的 $highlight() 函数用于在搜索结果中取得最匹配的一段文本,并高亮显示搜索时使用的短语,其第一个参数是高亮显示的文本,第二个参数是显示的文本长度,第三和第四个参数是高亮文本时使用的前缀和后缀。

可以使用正则表达式和文本解析来实现前面所提到的语法。我们也可以使用 JavaCC 定义 hitTextPattern 的文法,进而生成词法分析器和语法解析器。这是更为系统并且相对而言不易出错的方法。对 JavaCC 的介绍不是本文的重点,您可以在下面的 阅读资源 中找到学习资料。

下面列出的是一些与我们所提出的框架所相关或者类似的产品,您可以在 学习资料 中更多地了解他们。

IBM?OmniFind?Family

OmniFind 是 IBM 公司推出的企业级搜索解决方案。基于 UIMA (Unstructured Information Management Architecture) 技术,它提供了强大的索引和获取信息功能,支持巨大数量、多种类型的文档资源(无论是结构化还是非结构化),并为 Lotus?Domino?和 WebSphere?Portal 专门进行了优化。

Apache Solr

Solr 是 Apache 的一个企业级的全文检索项目,实现了一个基于 HTTP 的搜索服务器,支持多种资源和 Web 界面管理,它同样建立在 Lucene 之上,并对 Lucene 做了很多扩展,例如支持动态字段及唯一键,对查询结果进行动态分组和过滤等。

Google SiteSearch

使用 Google 的站点搜索功能可以方便而快捷地建立一个站内搜索引擎。但是 Google 的站点搜索基于 Google 的网络爬虫,所以无法访问受保护的站点内容或者 Intranet 上的资源。另外,Google 所支持的资源类型也是有限的,我们无法对其进行扩展。

SearchBlox?

SearchBlox 是一个商业的搜索引擎构建框架。它本身是一个 J2EE 组件,和我们的框架类似,也支持对网页和文件系统等资源进行索引,进而进行搜索。


还需考虑的问题

本文介绍的思想试图利用开源的工具解决中小型应用中的常见问题。当然,作为一个框架,它还有很多不足,下面列举出一些可以进行改进的地方。

性能考虑

当需要进行索引的资源数目不多时,隔一定的时间进行一次完全索引不会占用很长时间。使用一台 2G 内存,Xeon 2.66G 处理器的服务器进行实际测试,发现对数据库资源的索引占用的时间很少,一千多条记录花费的时间在 1 秒到 2 秒之内。而对 1400 多个文件进行索引耗时大约十几秒。但在大型应用中,资源的容量是巨大的,如果每次都进行完整的索引,耗费的时间会很惊人。我们可以通过跳过已经索引的资源内容,删除已不存在的资源内容的索引,并进行增量索引来解决这个问题。这可能会涉及文件校验和索引删除等。

另一方面,框架可以提供查询缓存来提高查询效率。框架可以在内存中建立一级缓存,并使用如 OSCache 或 EHCache 实现磁盘上的二级缓存。当索引的内容变化不频繁时,使用查询缓存更会明显地提高查询速度、降低资源消耗。

分布式索引

我们的框架可以将索引分布在多台机器上。搜索资源时,查询被 flood 到各个机器上从而获得搜索结果。这样可以免去传输索引到某一台中央服务器的过程。当然也可以在非结构化的 P2P 网络上实现分布式哈希表 (DHT),配合索引复制 (Replication),使得应用程序更为安全,可靠,有伸缩性。在阅读资料中给出了 一篇关于构建分布式环境下全文搜索的可行性的论文。

安全性

目前我们的框架并没有涉及到安全性。除了依赖资源本身的访问控制(如受保护的网页和文件系统等)之外,我们还可以从两方面增强框架本身的安全性:

考虑到一个组织的搜索功能对不同用户的权限设置不一定一样,可以支持对用户角色的定义,实行对搜索模块的访问控制。
在资源索引模块中实现一种机制,让资源可以限制自己暴露的内容,从而缩小索引模块的索引范围。这可以类比 robots 文件可以规定搜索引擎爬虫的行为。


通过上文的介绍,我们认识了一个可扩展的框架,由索引模块和搜索模块两部分组成。它可以灵活地适应不同的应用场景。如果需要更独特的需求,框架本身预留了可以扩展的接口,我们可以通过实现这些接口完成功能的定制。更重要的是这一切都是建立在开源软件的基础之上。希望本文能为您揭示开源的力量,体验用开源工具组装您自己的解决方案所带来的莫大快乐。
posted @ 2009-08-29 00:25 小菜毛毛 阅读(398) | 评论 (0)编辑 收藏

方法一:
本人解决的方法,保证可用。
添加过滤器(代码如下)
package com.cn.util;

import java.io.* ;
import javax.servlet.* ;
import javax.servlet.http.HttpServletResponse;

public class ForceNoCacheFilter implements Filter {    
 
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException    
{    
  ((HttpServletResponse) response).setHeader("Cache-Control","no-cache");    
  ((HttpServletResponse) response).setHeader("Pragma","no-cache");    
  ((HttpServletResponse) response).setDateHeader ("Expires", -1);    
  filterChain.doFilter(request, response);    
}    
public void destroy()    
{    
}    
   public void init(FilterConfig filterConfig) throws ServletException    
{    
}    
}    

然后在web.xml中添加这个过滤器
<filter>
    <filter-name>NoCache</filter-name>
    <filter-class>com.cn.util.ForceNoCacheFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>NoCache</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

com.cn.util.ForceNoCacheFilter为刚才过滤器的包名.类名,/*为匹配所有请求。

这样你所有的请求都将会传到服务器处理,不会查看缓存了。

方法二:
inComeHttp.url="familyGroup.do?method=query&tmp="+Math.random();
url上随意传一个随机数
posted @ 2009-08-26 13:06 小菜毛毛 阅读(1737) | 评论 (0)编辑 收藏

W0105010055
posted @ 2009-08-25 12:18 小菜毛毛 阅读(90) | 评论 (0)编辑 收藏

flex多module切换问题
错误描述:

typeError: Error #
1034: 强制转换类型失败:无法将 mx.graphics::Stroke@b945581 转换为 mx.graphics.IStroke。
 at mx.charts::AxisRenderer
/measure()[C:\Work\flex\dmv_automation\projects\datavisualisation\src\mx\charts\AxisRenderer.as:1091]
 at mx.core::UIComponent
/measureSizes()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\core\UIComponent.as:5819]
 at mx.core::UIComponent
/validateSize()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\core\UIComponent.as:5765]
 at mx.managers::LayoutManager
/validateSize()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\managers\LayoutManager.as:559]
 at mx.managers::LayoutManager
/doPhasedInstantiation()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\managers\LayoutManager.as:648]
 at Function
/http://adobe.com/AS3/2006/builtin::apply()
 at mx.core::UIComponent/callLaterDispatcher2()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\core\UIComponent.as:8460]
 at mx.core::UIComponent
/callLaterDispatcher()[E:\dev\3.0.x\frameworks\projects\framework\src\mx\core\UIComponent.as:8403]

错误说明:当我在多module切换的时候就抱这个错,特别要说明的是在切换时的连个module显示的图形,一个是自己画的,一个用的flex自带的,自己画的中里面用的是IStroke,但是自带的图形是Stroke,所以切换的时候就抱错,

解决方法:

如果你是用的是IModuleInfo的话的load的时候添加ApplicationDomain.currentDomain参数就可以了,

如:info.load(ApplicationDomain.currentDomain);

如果你用的是loadModule的话,则这样

aa.applicationDomain
=ApplicationDomain.currentDomain;
    aa.loadModule();


方法二:
 /*  Create dummy variables.  */
             // 避免出现:无法将 mx.managers::PopUpManagerImpl@52a09a1 转换为 mx.managers.IPopUpManager 错误
             private  var dragManager : DragManager;
             private  var popUpManager : IPopUpManager;
在应用中添加上如上代码
posted @ 2009-08-19 13:35 小菜毛毛 阅读(1080) | 评论 (0)编辑 收藏

flex开发中将各个功能分解到模块中,但在加载各个模块的时候需要注意一下问题:
加载方法:
private function init():void
{
module = mx.modules.ModuleManager.getModule("UIModule/HR/Holiday/Config/frmHolidayMain.swf");
module.addEventListener(mx.events.ModuleEvent.READY,ready);
module.load();


// general=ModuleLoader(mx.managers.PopUpManager.createPopUp(this,ModuleLoader));
//
//           general.url="test3.swf";
//          
//           general.loadModule();
}
private function ready(e:ModuleEvent):void
{
var moduleInfo:IModuleInfo = e.target as IModuleInfo
var wind:MDIWindow = new MDIWindow();


wind.addChild(moduleInfo.factory.create() as DisplayObject);


testcanvas.windowManager.add(wind);

}
需要注意的一点是 module 对象的定义一定要定义为全局的否则ready事件是不能执行的。具体原因不知道,个人理解为到ready方法中无法找到module对象了


flex 装载多个module出现的问题Error #1034: 强制转换类型失败 收藏
摘自http://bzhang.javaeye.com/blog/322148
TypeError: Error #1034: 强制转换类型失败:无法将 Object@1aee90b1 转换为 mx.messaging.messages.IMessage。


需求背景 :
通过树形菜单加载多个不同的module。
问题现象 :module页面存在拖动,Popup,Alert或者colorpicker出现错误信息:
TypeError: Error #1034: 强制转换类型失败:无法将 mx.managers::PopUpManagerImpl@7155ac1 转换为 mx.managers.IPopUpManager。
解决方案 :
在Application加入如下代码引用:
     < mx:Script >
         <! [CDATA[
             import  mx.managers.DragManager;
             import  mx.managers.IPopUpManager;           
           
             /*  Create dummy variables.  */
             // 避免出现:无法将 mx.managers::PopUpManagerImpl@52a09a1 转换为 mx.managers.IPopUpManager 错误
             private  var dragManager : DragManager;
             private  var popUpManager : IPopUpManager;
          
            //process....

        ]]>
    </mx:Script>
问题原因分析 :
属于ModuleLoader shared code problem .
当Module中使用managers时(如PopUpManager,DragManager, HistoryManager等)则可能出现这个问题(当application里在loader之前没有引入这些manager的引用时)。
manager 的方法是静态方法,整个应用程序中创建了一个该manager接口的singleton实例,但module仅在自己的 Application domain中使用该单例, 当多个module使用同一个单例manager且main application没有使用时,就会出现这个空对象引用问题:第一个引入某manager的module不能将该manager接口的 singleton跟其他module共享,其他module调用该Manager的方法时,应用程序不会再创建该manager接口的实例,这个 module就无法引用到该manager接口的实例,就出现了空对象引用问题.
参考资料:Flex sdk源码。

目前在Application创建了些Application范围内没有使用到的"木偶变量",从代码可读性上来说不是很好。有其他比较好的解决方案的同学麻烦请告之下,:)

posted on 2008-11-22 17:33 钩子 阅读(1118) 评论(1)  编辑  收藏 所属分类: jee 、ria 、工作笔记

<noscript type="text/javascript"> //<![CDATA[ Sys.WebForms.PageRequestManager._initialize('AjaxHolder$scriptmanager1', document.getElementById('Form1')); Sys.WebForms.PageRequestManager.getInstance()._updateControls(['tAjaxHolder$UpdatePanel1'], [], [], 90); //]]> </noscript>
Feedback
#   re: FLEX:multiple moduleloader occur #1034 error 2008-11-24 10:14 钩子
同事推荐了个更好的办法:
在ModuleLoader 的creationComplete方法中加入如下代码:
moduleLoader.applicationDomain = ApplicationDomain.currentDomain;
就可以在Application里切换多个module而不需要在Application里明文引用单例manager声明。比我上面所说的方法更好的能解决问题而且,代码可读性更好。
另外,推荐在moduleloader做切换的时候,加上:
moduleLoader.unloadModule再做moduleLoader.loadModule().

在这里做个小记。

http://blog.csdn.net/yzsind/archive/2009/03/27/4031066.aspx
posted @ 2009-08-18 17:31 小菜毛毛 阅读(2677) | 评论 (0)编辑 收藏

目标:本文主要介绍联系的定义及使用。

 一、 联系
联系(Relationship)是指实体集这间或实体集内部实例之间的连接。

 实体之间可以通过联系来相互关联。与实体和实体集对应,联系也可以分为联系和联系集,联系集是实体集之间的联系,联系是实体之间的联系,联系是具有方向性的。联系和联系集在含义明确的情况之下均可称为联系。

 按照实体类型中实例之间的数量对应关系,通常可将联系分为4类,即一对一(ONE TO ONE)联系、一对多(ONE TO MANY)联系、多对一(MANY TO ONE)联系和多对多联系(MANY TO MANY)。

 二、 建立联系
在CDM工具选项板中除了公共的工具外,还包括如下图所示的其它对象产生工具。

 在图形窗口中创建两个实体后,单击“实体间建立联系”工具,单击一个实体,在按下鼠标左键的同时把光标拖至别一个实体上并释放鼠标左键,这样就在两个实体间创建了联系,右键单击图形窗口,释放Relationship工具。如下图所示


三、 四种基本的联系
即一对一(ONE TO ONE)联系、一对多(ONE TO MANY)联系、多对一(MANY TO ONE)联系和多对多联系(MANY TO MANY)。如图所示

四、 其他几类特殊联系

除了4种基本的联系之外,实体集与实体集之间还存在标定联系(Identify Relationship)、非标定联系(Non-Identify RelationShip)和递归联系(Recursive Relationship)。

标定联系:
每个实体类型都有自己的标识符,如果两个实体集之间发生联系,其中一个实体类型的标识符进入另一个实体类型并与该实体类型中的标识符共同组成其标识符时,这种联系则称为标定联系,也叫依赖联系。反之称为非标定联系,也叫非依赖联系。
 注意:
在非标定联系中,一个实体集中的部分实例依赖于另一个实例集中的实例,在这种依赖联系中,每个实体必须至少有一个标识符。而在标定联系中,一个实体集中的全部实例完全依赖于另个实体集中的实例,在这种依赖联系中一个实体必须至少有一个标识符,而另一个实体却可以没有自己的标识符。没有标识符的实体用它所依赖的实体的标识符作为自己的标识符。


换句话来理解,在标定联系中,一个实体(选课)依赖 一个实体(学生),那么(学生)实体必须至少有一个标识符,而(选课)实体可以没有自己的标识符,没有标标识符的实体可以用实体(学生)的标识符作为自己的标识符。


 递归联系:
递归联系是实体集内部实例之间的一种联系,通常形象地称为自反联系。同一实体类型中不同实体集之间的联系也称为递归联系。

例如:在“职工”实体集中存在很多的职工,这些职工之间必须存在一种领导与被领导的关系。又如“学生”实体信中的实体包含“班长”子实体集与“普通学生”子实体集,这两个子实体集之间的联系就是一种递归联系。创建递归联系时,只需要单击“实体间建立联系”工具从实体的一部分拖至该实体的别一个部分即可。如图


五、 定义联系的特性

在两个实体间建立了联系后,双击联系线,打开联系特性窗口,如图所示。


 六、 定义联系的角色名
在联系的两个方向上各自包含有一个分组框,其中的参数只对这个方向起作用,Role Name为角色名,描述该方向联系的作用,一般用一个动词或动宾组表。
如:“学生 to 课目 ” 组框中应该填写“拥有”,而在“课目To 学生”组框中填写“属于”。(在此只是举例说明,可能有些用词不太合理)。

七、 定义联系的强制性
Mandatory 表洋这个方向联系的强制关系。选中这个复选框,则在联系线上产生一个联系线垂直的竖线。不选择这个复选框则表示联系这个方向上是可选的,在联系线上产生一个小圆圈。

八、 有关联系的基数
联系具有方向性,每个方向上都有一个基数。

举例,
“系”与“学生”两个实体之间的联系是一对多联系,换句话说“学生”和“系”之间的联系是多对一联系。而且一个学生必须属于一个系,并且只能属于一个系,不能属于零个系,所以从“学生”实体至“系”实体的基数为“1,1”,从联系的另一方向考虑,一个系可以拥有多个学生,也可以没有任何学生,即零个学生,所以该方向联系的基数就为“0,n”,如图所示

待续。

posted @ 2009-08-13 15:23 小菜毛毛 阅读(307) | 评论 (0)编辑 收藏

目标:
本文主要介绍数据项、新增数据项、数据项的唯一性代码选项和重用选项等。

一、数据项
数据项(Data Item)是信息存储的最小单位,它可以附加在实体上作为实体的属性。
注意:模型中允许存在没有附加至任何实体上的数据项。

二、新建数据项
1)使用“Model”---> Data Items 菜单,在打开的窗口中显示已有的数据项的列表,点击 “Add a Row”按钮,创建一个新数据项,如图所示


2)当然您可以继续设置具体数据项的Code、DataType、Length等等信息。这里就不再详细说明了。

三、数据项的唯一性代码选项和重用选项
使用Tools--->Model Options->Model Settings。在Data Item组框中定义数据项的唯一性代码选项(Unique Code)与重用选项(Allow Reuse)。
注意:
如果选择Unique Code复选框 ,每个数据项在同一个命名空间有唯一的代码,而选择Allow reuse ,一个数据项可以充当多个实体的属性。


四、在实体中添加数据项
1)双击一个实体符号,打开该实体的属性窗口。
2)单击Attributes选项卡,打开如下图所示窗口


注意:
Add a DataItem 与 Reuse a DataItem的区别在于
Add a DataItem 情况下,选择一个已经存在的数据项,系统会自动复制所选择的数据项。如果您设置了UniqueCode选项,那系统在复制过程中,新数据项的Code会自动生成一个唯一的号码,否则与所选择的数据项完全一致。


Reuse a DataItem情况下,只引用不新增,就是引用那些已经存在的数据项,作为新实体的数据项。

待续。
0
0
(请您对文章做出评价)
posted @ 2009-08-13 15:23 小菜毛毛 阅读(316) | 评论 (0)编辑 收藏

仅列出标题
共17页: First 上一页 9 10 11 12 13 14 15 16 17 下一页