qileilove

blog已经转移至github,大家请访问 http://qaseven.github.io/

嵌入式linux应用程序移植方法总结

 前段时间一直在做openCapwap的移植和调试工作,现在工作已接近尾声,编写本文档对前段工作进行一个总结,分享下openCapwap移植过程中的经验和感悟。江浩写的《CAPWAP移植进展.docx》对openCapwap的移植过程有了比较详细的描述,所以在此就不涉及技术细节了,本文档主要以openCapwap的移植为例,总结嵌入式linux应用程序移植的一般方法和步骤,为以后可能需要的移植工作提供一些的思路。
  嵌入式linux应用程序移植的步骤包括:
  1、准备好交叉编译环境
  在安装有Linux操作系统的PC上安装对于平台的交叉编译器,并将交叉编译器加到环境变量中,如export PATH=$PATH:/opt /toolchain/rsdk-1.3.6-5281-EB-2.6.30-0.9.30/bin, 然后在终端界面里看交叉编译器版本,如敲mips-linux-gcc –v,显示版本号则表示安装成功。
  (交叉编译器一般是平台厂商提供的,比如我们的交叉编译器就是mips-linux-gcc,是瑞昱提供的。)
  2、准备好源代码
  准备好需要编译的源代码库包,如capwap-0.93.3,需要注意的是,不仅需要准备要编译的源代码库包,还需要准备该源码包依赖的包,例如capwap-0.93.3依赖安全加密相关的包openssl和多线程相关的包pthread,这些包也需通过编译成静态库或动态库供主承销包调用。
  3、修改Makefile
  一般的源码库可以通过运行./Configure来制定编译器gcc,目标板的架构已经生产应用程序和库的目录。如果没有Configure文件就需要手动打开Makefile文件来修改,主要需要修改的地方有:(1)编译器的类型,(2)需要库的头文件路径;(3)需要库的链接路径(4)生成应用程序的路径。如openCapwap移植过程,将CC=gcc行用CC=mips-linux-gcc替换。
  4、编译源代码
  在源码包的主路径下运行Make,除非运气特别好,一般情况下是会报错的,需要根据报错的类型进行相应的修改。常见的报错类型有:(1)依赖的库包不支持该CPU架构,需要更换该架构的库包,如Capwap自带的openssl库不支持mips。#error "This openssl-devel package does not work your architecture?"(2)依赖的库没有经过交叉编译就拿来用了,如#error“./static/libssl.a: could not read symbols: File in wrong format”即libssl.a库文件格式是X86架构下的不支持mips架构。(3)源代码中有c语言方面的错误,一般是和交叉编译器版本不匹配引起的。
  一步步解决完这些错误后,然后终于可以生产对应目标板的应用程序了。但是生产相应的应用程序才是万里长征的第一步,让程序正确的运行才是最终目标。
  5、安装应用程序
  安装应用程序有两种方法,一是将应用程序放到目标板的文件系统中,通过烧镜像的方法将程序下载到目标板上;另一种是通过像tftp的方法下载到目标板上。后一种方法便捷灵活,在调试程序的过程中应用较多。需要注意的是,还需要将应用程序需要的动态库也下载到目标板上,应用程序才能跑。例如在我们移植capwap中出现,在完成烧录后,运行WTP报错。Error:系统化找不到pthread.so。分析:在终端中进入lib目录,发现缺少libpthread.so动态库。故原因在于RTL8198目标板SDK编译时没有将libpthread.so动态库添加到目标板系统的lib库文件当中。
  6、运行调试应用程序
  调试应用程序让其能够正确的工作,才是移植工作最重要的部分,这需要对应用程序的流程很熟悉,然后通过打印日志的方法看程序运行的路径,分析日志与正确的流程的差异来确定出错的地方。常见的出错地方有:(1)内存分配函数;(2)系统位数不一样;(3)字节顺序问题(大端小端);(4)浮点数的表示问题等。如我们在移植过程中遇到的malloc函数行为不同的问题。以上都是平时移植过程中需要重点注意的地方。

posted @ 2013-12-24 11:51 顺其自然EVO 阅读(421) | 评论 (0)编辑 收藏

Java基础加强之类加载器

 学习概述:本模块深入讲解了Java类加载方面的知识,Java类加载器和类加载机制以及类加载原理
  学习目标:掌握类加载机制和原理,能够独立开发自己的类加载器。
  1.类的加载
  什么是类加载? 类加载是指将类的class文件读入内存,并为之创建一个Java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
  类加载器负责加载所有类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类不会再被再次载入。
  思考问题:怎么样才算同一个类?
  当JVM启动时,会形成三个类加载器组成的原始类加载器层次结构:
  【BootStrap ClassLoader】根类加载器 这是一个特殊的加载器,他并不是有Java编写,而是JVM自身实现的
  【Extension Classloader】扩展类加载器
  【System Classloader】系统类加载器
  类加载器的父子关系:
  实验获得类加载器以及了解类加载器的层次结构:
public  class ClassloaderDemo{
public static  void main(String[] args){
System.out.printlb(ClassLoaderDemo.class.getClassLoader().getName());
System.out.println(System.class.getClassloader());
ClassLoader classloader = ClassLoaderDemo.class.getClassLoader());
while(loader!=null){
System.out.println(loader.getClass().getName());
loader=loader.getParent();
}
}
  注意:程序会抛出异常,因为JVM根类加载器不是Java类。
  2.类的加载机制,如图所示:
  <1>全盘负责:所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个                            类加载器来实现载入。
  <2>父类委托:意思是先让父类加载器试图加载该Class,只有父类加载器无法加载该类是才尝试从自己的路径中加载该类。
  <3>缓存机制:缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜索该Class,只有当缓存中不存在该Class对象              时,系统才重新读取该类对应的二进制数据。这就是为什么我们修改Class后,JVM必须重新启动,修改才生效的原因。
  类加载器的父子关系:用户类加载器—>系统类加载器—>扩展类加载器—>根类加载器
  类加载机制:
  <1>当JVM需要加载一个类是,到底指派哪个类加载器去加载呢?
  首先当前线程的类加载器去加载线程中的第一个类,如果A类中引用了B类,JVM将使用加载A类的加载器来加载B类,最后还可以调用ClassLoader。loadeClass方法指定        某个类加载器去加载某个类。
  <2>每个类加载器在加载类时,先委托给其上级加载器。
  注意两点:
  当所有的祖宗类加载器都没有加载到类,回到发起类加载器,还加载不了,那么程序将抛出ClassNotFoundExcetpion,而不是去找发起类加载器的儿子,因为没有                     getChild ()方法,即使有,那么选择哪一个儿子加载器呢?
  面试题:能不能自己写一个类叫Java.lang.System?
  答案:可以写,但是因为JVM委托机制的存在,会先找到JVM根类加载器,我自己写也可以,那么我要抛开委托加载机制,我自己指定一个ClassLoader。


 3.自定义类加载器
  JVM中除了根类加载器之外的所有类加载器都是classloader的子类实例,我们完全可以通过扩展ClassLoader的子类,并重写ClassLoader所包含的的方法来实现自定义类       加载器,ClassLoader有两个关键的方法:loadClass(),findClass()。
  不过我们一般推荐重写findClass()方法,而不是loadClass()方法,因为重写findClass()可以避免覆盖默认类加载器的父类委托,缓存机制两种策略。
  下面是我自己编写的一个类加载器:
package snippet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
/**
*
* @author Administrator
*自定义类加载器
*/
public class MyClassLoader extends ClassLoader {
// 获取java源文件的二进制码
public byte[] getBytes(String filename){
File file = new File(filename);
InputStream ips=null;
byte[] b = new byte[(int) file.length()];
try {
ips = new FileInputStream(file);
int raw =ips.read(b);
if(raw!=file.length()){
throw new IOException("无法完整读取文件");
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally{
if(ips!=null){
try {
ips.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return b;
}
public boolean compile(String javaFile){
System.out.println("正在编译");
Process p=null;
try {
//调用系统javac命令
p=Runtime.getRuntime().exec("javac" + javaFile);
try {
p.waitFor();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int ret = p.exitValue();
return ret==0;
}
@Override
protected Class<?> findClass(String name) {
Class<?> clazz=null;
String fileStub = name.replace(".", "/");
String javaFileName = fileStub + ".java";
String classFileName = fileStub + ".class";
File javaFile = new File(javaFileName);
File classFile = new File(classFileName);
//如果java源文件存在并且class文件不存在,或者java源文件比class文件修改的时间晚
if(javaFile.exists()&&(!classFile.exists()||javaFile.lastModified()>classFile.lastModified())){
if(!compile(javaFileName)||!classFile.exists()){
try {
throw new ClassNotFoundException("未发现class文件");
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//如果class文件已经存在,那么直接生成字节码
if(classFile.exists()){
byte[] b = getBytes(classFileName);
clazz = defineClass(name, b, 0, b.length);
}
//如果为空,标明加载失败
if(clazz==null){
try {
throw new ClassNotFoundException(name);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return clazz;
}
}
  上面代码重写了findClass方法,通过重写该方法就可以实现自定义的类加载机制。
  学习总结:1.了解了JVM三种类加载器(根类加载器,系统类加载器,扩展类加载器),明白了三种类加载器的作用和范围
  2.学习了JVM三种类加载机制(父类委托,缓存,全盘负责)
  3.学习了如何自定义类加载器,通过继承ClassLoader类,特别要注意两个关键方法:loadClass()和findClass()两种方法的机制和不同。

posted @ 2013-12-24 11:50 顺其自然EVO 阅读(178) | 评论 (0)编辑 收藏

在QTP Test中利用vbs和cmd实现重新启动QTP

  连续长时间执行过QTP或使用QC连续长时间调用QTP的朋友应该会知道,QTP在长时间执行过程中会产生内存泄露。而QTP自身也有一个工具Remote Agent来实现每执行N次来重新启动测试工具释放内存。
  但是,不知道是不是我用破解版的原因,如果使用RunTest来调用QC执行QTP时,这个设置的Remote Agent会使得测试失败,并卡死在某一步。所以在这里需要进行手动清理QTP长时间运行过程中产生的内存泄露。
  在这里,我所知的清理内存有两种方式,一种是利用一些内存清理工具,一种是通过脚本控制QTP重新启动。
  首先说说内存清理工具。今天在群里下到了一个兄弟共享的内存清理工具memempty.exe,它可以进行定时清理。但是由于QTP执行每个Test的时间都不相同,并且QTP在执行过程中,这个工具也无法有效的清理干净内存部分。对这些不是很懂,点下就行了,免得出丑。
  使用脚本控制,这里我用的是创建外部vbs并通过cmd命令来执行的方式实现的,脚本如下:
dim restartcode
set?fso?=?CreateObject("Scripting.FileSystemObject")
set?file?=?fso.CreateTextFile("D:\RestartQTP.vbs")
restartcode?=?"'浣跨敤涓€涓瓑寰呮椂闂翠娇寰梣tp鑳借繑鍥瀜c涓€涓墽琛屾垚鍔熺殑缁撴灉"&vbnewline
restartcode?=?restartcode?&"WScript.Sleep?3000"&vbnewline
restartcode?=?restartcode?&"'浣跨敤AOM鏉ユ帶鍒秖tp鐨勯噸鍚搷浣?"&vbnewline
restartcode?=?restartcode?&"Dim?qtp"&vbnewline
restartcode?=?restartcode?&"Set?qtp?=?CreateObject(""QuickTest.Application"")"&vbnewline
restartcode?=?restartcode?&"qtp.Quit"&vbnewline
restartcode?=?restartcode?&"WScript.sleep?2000"&vbnewline
restartcode?=?restartcode?&"qtp.Launch"&vbnewline
restartcode?=?restartcode?&"qtp.visible?=?true"&vbnewline
restartcode?=?restartcode?&"set?qtp?=?nothing"&vbnewline
restartcode?=?restartcode?&"set?fso?=?CreateObject(""Scripting.FileSystemObject"")"&vbnewline
restartcode?=?restartcode?&"fso.deletefile?""D:\RestartQTP.vbs"""&vbnewline
restartcode?=?restartcode?&"set?fso?=?nothing"&vbnewline
file.write?restartcode
set?fso?=?nothing
set?WshShell?=?CreateObject("WScript.Shell")
WshShell.run?"cmd.exe?/c?D:\RestartQTP.vbs"
set?WshShell?=?nothing?
  这样就可以实现重新启动QTP了,不过在执行下一个test之前需要一个等待时间,这个在调用脚本中很简单,就不罗嗦了

posted @ 2013-12-24 11:40 顺其自然EVO 阅读(305) | 评论 (0)编辑 收藏

phpunit 单元测试案例--签到任务

工作需要,最近要写单元测试了,这里算是一个记录的过程吧,慢慢记录,慢慢学习,慢慢总结,早点把这块的信息熟悉起来~~
  之前也写过简单的单元测试的一些小的说明,但是现在的是比较具体的例子了!
  这里要列举的一个例子是如下的描述:
  名称:签到任务,领金币。
  规则:
  1、可以每天签到,签到一天领取一个金币,连续3天或者7天有额外的几个金币。
  2、某一段时间内可以做一起签到任务,连续签到3天,可以额外给50金币,每个用户只能做一次。
  首先简单介绍下表结构,这里用的是MongoDB:
  第一个表就是每天签到的表里面很重要的2个字段是:
"last_time" : 1385545551,#最后签到时间
"sign_time" : [
1385545551  #连续签到天数,数组,存放连续签到的时间
],
  第二张表是任务表,每个用户对应一条记录,这条记录里包括不同的任务执行状态:
"sign" : { #sign代表签到任务
"last_time" : 1385545357,#最后一次更新时间
"status" : 4 #1第一天签到,2第二天,3第三天 4已领奖
}

 单元测试中会有一些调用方法,这里我不列出具体代码,只解释方法的作用:
<?php
/**
* 签到任务单元测试
*
* @author    shayang88
* @since     2013-11-27
* @copyright Copyright (c) 2005-2012 Inc.
* @desc
*
*/
require_once dirname(__FILE__) . '/../../webroot/bootstrap.php';
class test_sign_task extends PHPUnit_Framework_TestCase
{
public $testUid = 5888882; //这个是测试的用户id
/**
* 这个函数主要用来清除用户在2个表中的任务状态,重置为下面做准备
*/
public function testClear(){
//清除现有表签到任务,主要是任务的状态归零
$taskModel = new GuaziTaskModel();
$taskModel->clearTask($this->testUid, 'sign');
//清除签到表,清除用户的连续签到天和最后签到时间
$signModel = new GuaziSignModel();
$signModel->getCollection()->update(array('uid' => $this->testUid ), array('$unset' => array ('last_time' => 1, 'sign_time' => 1)));
}
/**
* 这个函数用来断言上一步清除任务状态是否完成
*/
public function testEmpty(){
//断言是否任务状态已清空
$objSignTask = Task_Factory::factory('sign');
$signStatus = $objSignTask->getStatus($this->testUid);
//清除完成,则任务状态归零,所以用0来断言
$this->assertEquals(0, $signStatus);
//断言是否任务已清空
$signModel = new GuaziSignModel();
$arrsign = $signModel->getSignByUid($this->testUid);
//这里因为清空是直接清除了字段,所以用NULL来断言
$this->assertNull($arrsign['n_t']);
$this->assertNull($arrsign['t']);
}
/**
* 这个函数是签到数据的供给器,主要是为了模拟签到连续的天数,为下面的testSign提供数据,这里
* 需要了解数据供给器的用法@dataProvider
* @return array
*/
public function dataProducer() {
//提供签到数据
//第1个参数是签到时间,第2个是连续签到的天数,第3个是任务的完成状态
return array(
array('2013-11-25 19:30', 1, 1),
array('2013-11-26 18:30', 2, 2),
array('2013-11-27 10:30', 3, 3),
array('2013-11-28 23:59', 4, 4),
array('2013-11-29 22:59', 5, 4),
);
}
/**
* @dataProvider dataProducer
*/
public function testSign($nowTime,$signCount,$taskStatus)
{
//循环接收dataProducer给的值开始执行任务,3个参数就是上面数组的个数
$now = strtotime($nowTime);
$signModel = new GuaziSignModel();
//更新任务表状态并断言
$objSignTask = Task_Factory::factory('sign');
//更新任务状态,当然内部会区分每一步不同的操作
$doTaskRes = $objSignTask->doTaskForPhpunit($this->testUid, $now);
//断言执行结果
$this->assertEquals(1, $doTaskRes);
//获取任务状态
$signStatus = $objSignTask->getStatus($this->testUid);
//断言任务状态
$this->assertEquals($taskStatus, $signStatus);
//更新签到表并断言
$ret = $signModel->updateLastSign($this->testUid,$now);
//断言连续签到天数
$this->assertCount($signCount, $ret['t']);
}
}

posted @ 2013-12-24 11:39 顺其自然EVO 阅读(282) | 评论 (0)编辑 收藏

软件工程之软件测试

 软件是人类思维的产物,是群体性活动的结果,所以在软件开发的各个阶段都不可避免的会产生错误。例如,在需求分析时没有很好的理解用户的意图;在系统设计时对实体之间的关系没有正确地描述;在编码阶段出现了不易发现的逻辑错误;软件开发人员之间的配合出现失误等。尽管在每个阶段结束之前都对软件进行严格的技术评审,但经验表明审查并不能发现所有的差错。因此,软件编码完成后,在交付用户使用前,必须对软件进行严格的测试软件测试是软件质量保证的关键步骤。
  所谓软件测试,是为了发现错误而执行程序的过程。或者说,软件测试是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期结果),并利用这些测试用例去运行程序,以发现程序错误的过程。软件测试在软件生存周期中横跨两个阶段:通常在编写出每个模块之后对它做必要的测试(单元测试)。模块的编写者与测试者是同一个人。编码与单元测试属于软件生存周期的同一个阶段。在这个阶段结束之后,软件系统还要进行各种综合测试,这是软件生存周期的另一个独立的阶段,即测试阶段,通常由专门的测试人员承担这项工作
  说了这么多,大家可能急切的想知道,软件测试到底包含哪些内容,下面我用一张思维导图给大家简单总结一下软件工程中软件测试阶段的一些理论和方法。
  PS:由于图比较大,请用鼠标点击图中内容,拖到空白处即可在浏览器中查看
  作为软件工程的一个必要阶段,软件测试有自己的理论和严格的方法,在进行测试的时候一定要重视起来,避免软件测试的随意性。另外,软件测试人员的经验也是十分重要的,一个经验丰富的测试人员能够显著提高软件测试工作的效率。

posted @ 2013-12-24 11:39 顺其自然EVO 阅读(170) | 评论 (0)编辑 收藏

DAO test

至此,一个基于MVC的基本Android应用程序已经初步形成了。
  下面我们来实现一个具有TabHost的布局的典型Android应用,由于我们基本上可以不考虑Android 4.x以前的版本,因此我对TabHost布局的实现将采用Fragment来实现,而不是采用旧的ActivityGroup来实现。
  同时,我们希望我们的应用程序可以适用于不同的项目,因此需要TabHost上的图片及文字可以非常方便的进行更换。我们采用下部有5个选项的布局,其中中间的选项可以突出显示,选中某个选项,目前仅显示对应Fragmentation的名字。
  好了,需求基本说清楚了,下面我们就开始一步步实现吧。
  首先是基于Fragment的TabHost布局实现,原理很简单,在MainActivity的布局文件里添加如下代码即可:
<?xml version="1.0" encoding="utf-8"?>
<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/tabhost"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<!-- 实现Tab标签的居底主要是通过设置属性 android:layout_weight="1" -->
<!-- 还要注意FrameLayout标签的位置,要写在TabWidget标签的前面 -->
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_gravity="center_horizontal"
android:layout_weight="1">
<fragment
android:id="@+id/j_dynamicFragment"
android:name="com.bjcic.wkj.gui.DynamicFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<fragment
android:id="@+id/j_findFragment"
android:name="com.bjcic.wkj.gui.FindFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<fragment
android:id="@+id/j_shareFragment"
android:name="com.bjcic.wkj.gui.ShareFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<fragment
android:id="@+id/j_snsFragment"
android:name="com.bjcic.wkj.gui.SnsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<fragment
android:id="@+id/j_moreFragment"
android:name="com.bjcic.wkj.gui.MoreFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
<TabWidget
android:id="@android:id/tabs"
android:layout_width="fill_parent"
android:layout_height="60dip"
android:layout_gravity="center_horizontal"
android:layout_marginLeft="-2dp"
android:layout_marginRight="-2dp"
android:background="@null" />
</LinearLayout>
</TabHost>
好的单元测试应该是原子性的,独立的,不应依赖其他测试和上下文,但是要测试数据读写是否正确,就必须涉及初始数据的加载,数据修改的还原等操作。对于初始数据的加载,手动输入很麻烦,一个解决方案就是使用Dbunit,从Xml文件甚至Excel中加载初始数据到数据库,是数据库的值达到一个已知状态。同时还可以使用Dbunit,对数据库的结果状态进行判断,保证和期望的一致。数据修改的还原,可以依赖Spring TransactionalTests,在测试完成后回滚数据库。
  Dbunit还可以对数据的现有数据进行备份,还原,清空现有数据,一个好的测试实践是每一个开发人员一个测试数据库,进而对数据库的数据状态有更好的控制,但现实可能会是共享同一个测试库,所以这种情况下,测试的编写必须多做一些考虑。
  待测试的类:
package com.test.dbunit.dao.impl;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.jdbc.core.RowMapper;
import com.test.dbunit.dao.UserDao;
import com.test.dbunit.entity.User;
public class DefaultUserDao extends BaseDao implements UserDao {
private static String QUERY_BY_NICK = "select * from user where user.nick = ?";
private static String REMOVE_USER = "delete from user where user.nick = ?";
private static String INSERT_USER = "insert into user(nick,password) values(?, ?)";
private static String UPDATE_USER = "update user set user.password = ? where user.nick = ?";
@Override
public User getUserByNick(String nick) {
return (User) getJdbcTemplate().queryForObject(QUERY_BY_NICK,new Object[]{nick}, new RowMapper(){
@Override
public Object mapRow(ResultSet rs, int index) throws SQLException {
User user = new User();
user.setNick(rs.getString("nick"));
user.setPassword(rs.getString("password"));
return user;
}
});
}
@Override
public void remove(String nick) {
getJdbcTemplate().update(REMOVE_USER, new Object[]{nick});
}
@Override
public void save(User user) {
getJdbcTemplate().update(INSERT_USER, new Object[]{user.getNick(), user.getPassword()});
}
@Override
public void update(User user) {
getJdbcTemplate().update(UPDATE_USER, new Object[]{user.getPassword(), user.getNick()});
}
}
  单元测试:
  需要注意的地方就是,DataSourceUtils.getConnection(datasource) , 通过这种方式获得数据库连接初始化Dbunit,能够保证Dbunit使用的数据连接和当前事务的数据库连接相同,保证能够在参与到事务中。Spring的TransactionManager会在开始事务时把当前连接保存到ThreadLocal中,DataSourceUtils.getConnection方法,首先从ThreadLocal中获取连接。
  user001.xml
  Xml代码
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user nick="user001" password="password001" />
</dataset>
  使用dbunit,可以通过xml文件定义数据集,也可以使用其他方式定义,比如Excel,编程方式。
  Dbunit的主要构件
  IDatabaseConnection
  数据库链接。实现类有DatabaseConnection 和DatabaseDataSourceConnection ,执行数据库操作时需要一个连接。
  IDataSet
  数据集,数据集可以从Xml文件Excel等外部文件获取,也可以从数据库查询,或者编程方式构件,数据集可以作为初始数据插入到数据库,也可以作为断言的依据。另外还有IDatatable等辅助类。
  比如在updateUser测试中,使用了QueryDataSet,从数据库中构建一个Dataset,再通过FlatXmlDataSet从Xml文件中构建一个Dataset,断言这两个Dataset相同。
QueryDataSet actual = new QueryDataSet(conn);
actual.addTable("user", "select * from user where user.nick = 'user001'");
IDataSet expected = new FlatXmlDataSet(new ClassPathResource(
"com/taobao/dbunit/dao/user001_updated.xml").getFile());
Assertion.assertEquals(expected, actual);
DatabaseOperation


  通过定义的静态字段可以获取一组代表一个数据操作的子类对象,比如DatabaseOperation .INSERT,返回 InsertOperation,通过执行execute方法把数据集插入到数据库。例如:
  IDataSet origen = new FlatXmlDataSet(new ClassPathResource(
  "com/taobao/dbunit/dao/user001.xml").getFile());
  DatabaseOperation.INSERT.execute(conn, origen);
  从Xml文件中构建DataSet,使用Insert插入到数据库,初始化测试数据。
  Assertion
  唯一的方法,assertEqual,断言两个数据集或数据表相同。
  更多关于Dbunit的组件的介绍:http://www.dbunit.org/components.html
  PS:使用Oracle的时候,初始化DatabaseConnection需要传入scheme。new DatabaseConnection(conn,SCHEMA_NAME ) ,SCHMEA_NAME需要大写。
  附件提供所有代码下载
package com.taobao.dbunit.dao;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.dbunit.Assertion;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.Assert;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.transaction.TransactionConfiguration;
@ContextConfiguration(locations = { "classpath:testApplicationContext.xml" })
@TransactionConfiguration(defaultRollback = true)
public class BaseDaoTest extends AbstractTransactionalJUnit4SpringContextTests {
@Autowired
private DataSource dataSource;
private IDatabaseConnection conn;
@Before
public void initDbunit() throws Exception {
conn = new DatabaseConnection(DataSourceUtils.getConnection(dataSource));
}
/**
* 清空file中包含的表中的数据,并插入file中指定的数据
*
* @param file
* @throws Exception
*/
protected void setUpDataSet(String file) throws Exception {
IDataSet dataset = new FlatXmlDataSet(new ClassPathResource(file)
.getFile());
DatabaseOperation.CLEAN_INSERT.execute(conn, dataset);
}
/**
* 验证file中包含的表中的数据和数据库中的相应表的数据是否一致
*
* @param file
* @throws Exception
*/
protected void verifyDataSet(String file) throws Exception {
IDataSet expected = new FlatXmlDataSet(new ClassPathResource(file)
.getFile());
IDataSet dataset = conn.createDataSet();
for (String tableName : expected.getTableNames()) {
Assertion.assertEquals(expected.getTable(tableName), dataset
.getTable(tableName));
}
}
/**
* 清空指定的表中的数据
*
* @param tableName
* @throws Exception
*/
protected void clearTable(String tableName) throws Exception {
DefaultDataSet dataset = new DefaultDataSet();
dataset.addTable(new DefaultTable(tableName));
DatabaseOperation.DELETE_ALL.execute(conn, dataset);
}
/**
* 验证指定的表为空
*
* @param tableName
* @throws DataSetException
* @throws SQLException
*/
protected void verifyEmpty(String tableName) throws DataSetException,
SQLException {
Assert.assertEquals(0, conn.createDataSet().getTable(tableName)
.getRowCount());
}
}
  使用:
@Test
public void updateUser() throws Exception {
setUpDataSet("com/taobao/dbunit/dao/user001.xml");
User user = new User();
user.setNick("user001");
user.setPassword("password002");
userDao.update(user);
verifyDataSet("com/taobao/dbunit/dao/user001_updated.xml");
}

posted @ 2013-12-23 10:13 顺其自然EVO 阅读(223) | 评论 (0)编辑 收藏

关于 Java Web 项目性能提升的一些思路

 使用 Nginx 作为前端接入
  用 Nginx 进行动静分离。这个不用多讲,新浪、网易、淘宝、腾讯等巨头的使用已经说明了一切。
  保持最简单的架构
  遵守 KISS 原则(Keep it simple and stupid)。尽量不要考虑项目外的重用。过多的考虑项目外的重用,必然会增加项目的复杂度。避免过度集成,让每个模块只做自己的事,这对于日后的维护和模块复用都有好处。
  精心设计缓存处理、毫不吝啬代码(对象、列表、片段)
  对于门户网站的首页来说,往往可能会有近百个 SQL。用户并发上去以后,光首页就足以让服务器 down 掉。缓存不但有利于降低负载,而且还能提高响应速度。
  调整使用聚集索引
  对于每个表来讲,聚集索引只有一个,利用好了,查询速度会有意想不到的提升效果。
  以 MySql 为例,InnoDB选取聚集索引参照列的顺序是
  1. 如果声声明了主键(primary key),则这个列会被做为聚集索引;
  2. 如果没有声明主键,则会用一个唯一且不为空的索引列做为主键,成为此表的聚集索引;
  3. 上面二个条件都不满足,InnoDB会自己产生一个虚拟的聚集索引。
CREATE TABLE `timeline_raw` (
`rawId` bigint(20) NOT NULL AUTO_INCREMENT,
`uid` bigint(20) DEFAULT NULL,
`did` bigint(20) DEFAULT NULL,
`channelId` char(1) NOT NULL DEFAULT '1' COMMENT '1:qvga; 2:720p',
`fileId` bigint(20) DEFAULT NULL,
`sectionId` bigint(20) DEFAULT NULL,
`headerFilePath` varchar(120) DEFAULT NULL,
`startTime` bigint(20) DEFAULT NULL,
`endTime` bigint(20) DEFAULT NULL,
`updateTime` datetime DEFAULT NULL,
`createTime` datetime DEFAULT NULL,
PRIMARY KEY (`rawId`),
KEY `index_uid_did_startTime` (`uid`,`did`,`startTime`) USING BTREE,
KEY `index_uid_did_endTime` (`uid`,`did`,`endTime`) USING BTREE,
KEY `index_time` (`startTime`) USING BTREE,
KEY `index_uid_did_fileId` (`uid`,`did`,`sectionId`) USING BTREE,
KEY `index_sectionId` (`sectionId`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
  这个表有四个索引:主键 rawId、sectionId、`uid`,`did`、startTime。

 项目的 iBatis2 中有这样一条查询语句:
<select id="getRawFileList" parameterClass="java.util.HashMap" resultClass="com.defonds.mysql.raw.entity.TimelineRaw">
SELECT * FROM timeline_raw_
WHERE uid=#uid#
AND did=#did#
AND channelId=#channelId#
<isNotNull  property="sectionId"> AND sectionId = #sectionId#</isNotNull>
AND
(
(startTime BETWEEN #startTime# and #endTime#)
OR
(endTime BETWEEN #startTime# and #endTime#)
OR
(
<![CDATA[
startTime<=#startTime#
]]>
AND
<![CDATA[
endTime>=#endTime#
]]>
)
)
ORDER BY startTime;
</select>
  根据实际业务向 timeline_raw 表注入一千万条数据,进行模拟测试(参考《sql 性能测试例子》),发现 getRawFileList 的执行平均时间为 160 ms 以上。这是不能接受的。
  考虑到实际业务中对于主键 rawId 查询条件甚少,我们把rawId主键索引取消掉,改为唯一约束,却把sectionId+startTime+endTime作为主键(业务上能够保证其唯一性,根据InnoDB索引规则,这个索引将成为我们新表的聚集索引)。然后把sectionId、startTime两个索引也取消掉,仅保留`uid`,`did`索引。
  这样子,我们新表的索引实际上只有两个了:一个聚集索引(sectionId+startTime+endTime)一个非聚集索引(`uid`,`did`)。
  再次进行模拟测试,同样的数据、数据量,同样的查询结果集,getRawFileList 执行平均时间已经降到了 11 ms。结果是令人振奋的,不是么?
  使用 /dev/shm 来存储缓存的磁盘文件
  在网站运维中,利用好了这一点,往往有意想不到的收获。以 tomcat 为例,可以通过修改 catalina.sh 中的 CATALINA_TMPDIR 值的路径来将缓存设置为 /dev/shm。
  以 OSC 为例,他们就是纯 Java 写的,部署在 tomcat 下。在长时间的在线运行之后,管理员发现网站响应速度奇慢,服务器负载正常,又找不出是哪里的问题。后来 df 一下,发现 tomcat 临时目录下的文件足足有 8G 之多,原来是 CPU 等待磁盘操作造成响应速度加长。于是他们将临时目录映射到 /dev/shm,网站响应速度从此奇快。
  分析系统中每一个 SQL 的执行效率
  以 MySql 为例,对于每个 SQL 最好都 explain 一下。对于有明显效率问题的,通过 sql 优化、调索引等方法进行改进。
  健康慢查询日志,检查所有执行超过 100 毫秒的 SQL
  对于上线了的项目,健康慢查询日志,检查所有执行超过 100 毫秒的 SQL,看看有没有优化余地。对于没有上线的项目,可以进行场景模拟对嫌疑 SQL,或者对频繁使用的 SQL 进行性能测试,统计它们执行时间,得出平均值,画出曲线分析图,对于单表千万数据,执行时间超过 50ms 的 SQL 要重点关注。参考《sql 性能测试例子》。

posted @ 2013-12-23 09:59 顺其自然EVO 阅读(684) | 评论 (0)编辑 收藏

Java 使用 TCP 和 UDP 传输文件

  引言
  本项目的目的是实现两个应用,通过网络连接在不同的主机之间传输一个文件的功能。两个应用应该分别利用 UDP 和 TCP 协议,以具有传输至少 1 MB 文件的能力。
  实现和说明
  源代码
  两个应用都由单个程序实现,源代码下载地址。
  说明
  程序使用以下命令行进行编译:
  javac *.java
  然后使用以下两个命令行运行:
Receiver:
java FileReceiver [protocol] [port]
Sender:
# java FileSender [protocol] [host] [port] [filename]
  其中 [protocol] 参数可以是 "udp" 或者 "tcp",但 sender 和 receiver 必须一致。
  文件将会在 receiver 启动的目录下生成,默认指定名为 "Received-[filename]"。
  TCP 实现
  实现概述
  在 TCP 实现中,Receiver 打开了一个 ServerSocket,并对定义好的端口进行监听。Sender 启动后将会为监听者 Receiver 打开一个新的 Socket,这导致了 socket 两端 InputStream 和 OutputStream 对象的创建。
  一个包含了文件名和文件大小的初始信息将由 Sender 发送给 Receiver。这样 Receiver 可以使用一个有意义的名字来存储接收到的文件,并可以判断什么时候文件完全传输完毕。此信息并不是必须的,当 Receiver 无法接收文件时停止 Sender 占用不必要的带宽。
  文件通过一个 FileInputstream 对象对它的读取进行传输,然后将数据写到一个 Socket 返回的 OutputStream 对象。为提高应用效率,每次读取和中继的数据是 8 kb,使用一个字节数组作为缓存。
  TCP 使用经验
  实践证明,TCP 文件传输是简单可靠的。程序的效率取决于使用的缓存大小,但传输的文件在所有执行的测试中都准确地被接收和保存。
  UDP 实现
  实现概述
  UDP 文件传输的实现使用的是标准 Java datagram 类:DatagramPacket 和 DatagramSocket。
  当 receiver 被执行时,它打开一个指定端口号的 socket 并等待,监听传入的数据包。sender 启动后,它打开一个连接到指定主机和端口的 socket,并传输包含有文件名以及将要传输文件大小等信息的单个 packet。当这个 package  发送以后,这个 socket 将等待并监听 package。
  基于接收到的初始 package,receiver 为文件创建一 outputStream 对象,并给监听着的 sender 发送一个含有 "OK" 单词的 package。收到这个 "OK" 包以后,sender 开始读取文件内容,并将其通过 UDP 数据包发送,每次含有 512 字节的块。receiver 将这些块按照接收到的次序写入文件,并重复接收,直到接收到的字节达到它所期望数字。之后程序终止。
  UDP 使用经验
  UDP 是一种不可靠的传输连续数据的协议。这意味着传输过程中会有丢包,而且接收到包的次序也是随机的。上面的例子并没有解决文件传输中的这些问题。这意味着以上应用在其每次运行时(所得到的文件)并不是正确的和完整的。以下是关于两个经常发生的问题的原因以及可行解决方案的描述。
  如果在文件传输过程中两个包接收顺序错误,而写入文件的顺序是按接收顺序来的。这将造成接收文件损坏。对于这种问题的解决方案是每次传输时定义一个序列号。这可以让 Receiver 按照正确的顺序来存储这些包,不管它们到达的先后次序。
  如果传输文件时出现丢包,Receiver 将不能收到它所期望数量的数据。在上面的示例中,这会导致 Receiver 继续运行,等待剩余的数据。对于这个问题一个可行的解决方案是,receiver 在给定时间跨度之后进行每次传输,调用超时。但为了使此次请求具有目的性,我们要像上面说的那样为包扩展序列号。否则我们无法接收到给定数量的数据,并局限于请求文件的完整传输。
  另一个关于这两个问题的解决方案,在每次正确接收包之后再向 sender 发起接收请求。这个方法消除了丢包的可能性,但却会使传输异常缓慢。
  结语
  上面的实现让文件在主机之间传输变得可行。但如果使用的是 UDP 协议的话,我们就无法保证文件的完整性和接收(顺序)的正确性。我们对解决这些问题进行了大体说明,但具体在实际的文件传输中,对这些问题的最简单的解决方案就是,用 TCP 取代 UDP。

posted @ 2013-12-23 09:47 顺其自然EVO 阅读(412) | 评论 (0)编辑 收藏

数据库注册两种方式的比较

在学习JDBC的时候,通常有两种方式去注册数据库驱动程序(这里采用MySQL数据库),分别为:
DriverManager.registerDriver(new Driver());
Class.forName("com.mysql.jdbc.Driver");
  那么这两种方式有什么异同呢?首先我们到DriverManager中去看一下,
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
if (!initialized) {
initialize();
}
DriverInfo di = new DriverInfo();
di.driver = driver;
di.driverClass = driver.getClass();
di.driverClassName = di.driverClass.getName();
// Not Required -- drivers.addElement(di);
writeDrivers.addElement(di);
println("registerDriver: " + di);
/* update the read copy of drivers vector */
readDrivers = (java.util.Vector) writeDrivers.clone();
}
  很明显,DriverManager将我们需要注册的驱动程序信息封装成DriverInfo放进了一个writeDrivers中,这个writeDrivers是DriverManager中声明的一个static类型Vector变量。在getConnection的时候会再用到。
  那么Class.forName("com.mysql.jdbc.Driver")是如何注册驱动程序的呢,我们知道Class.forName("类名")的主要作用是向虚拟机实例化一个Class实例,我们看一下com.mysql.jdbc.Driver的源代码。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// ~ Static fields/initializers
// ---------------------------------------------
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
  在 com.mysql.jdbc.Driver中有一段静态代码块,是向 DriverManager注册一个Driver实例。这样在 Class.forName("com.mysql.jdbc.Driver")的时候,就会首先去执行这个静态代码块,于是和DriverManager.registerDriver(new Driver())有了相同的效果。
  那么对于这两种方法,在这里,我推荐使用第二种,即Class.forName("类名")的方式。原因有两点
  1、在我们执行DriverManager.registerDriver(new Driver())的时候,静态代码块也已经执行了,相当于是实例化了两个Driver对象。
  2、 DriverManager.registerDriver(new Driver())产生了一种对MySQL的一种依赖。而Class.forName的方式我们完全可以在运行的时候再动态改变。

posted @ 2013-12-23 09:39 顺其自然EVO 阅读(240) | 评论 (0)编辑 收藏

实例解析—用Linux操作系统构建的路由器

一.网络整体框架
  网络结构如图,Linux路由器这台计算机标识为A,它与三个网段相连,192.168.1.0/24,10.0.0.0/8,172.16.0.0/16.
  A计算机必须拥有三块网卡,分别与三个网段相连。
  假设:
  eth0与172.16.0.0相连,
  eth1与10.0.0.0相连,
  eth2与192.168.1.0相连。
  二.具体网络设置
  2.1配置eth0
  首先来配置eth0.给这个网络接口分配地址172.16.1.1,运行下列命令:
  # ifconfig eth0 172.16.1.1 netmask 255.255.0.0
  为了使这个地址不再计算机重新启动后消失,编辑/etc/sysconfig/network-scripts/ifcfg-eth0文件,修改为如下格式:
DEVICE = eth0
ONBOOT = yes
BROADCAST = 172.16.255.255
NETWORK = 172.16.0.0
NETMASK = 255.255.0.0
IPADDR = 172.16.1.1
  增加一条静态路由:
  # route add -net 172.16.0.0 netmask 255.255.0.0
  这样系统中就增加了一条静态路由:
  # route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface
  172.16.0.0 * 255.255.0.0 U 0 0 0 eth0
  2.2配置eth1
  接下来,配置eth1,eth1与10.0.0.0网段相连,分配给它的地址是10.254.254.254,使用ifconfig命令为它配置参数:
  # ifconfig eth1 10.254.254.254 netmask 255.0.0.0
  同样编辑/etc/sysconfig/network-scripts目录下的ifcfg-eth1文件,内容如下:
DEVICE = eth1
ONBOOT = yes
BROADCAST = 10.255.255.255
NETWORK = 10.0.0.0
NETMASK = 255.0.0.0
IPADDR = 10.254.254.254
  再增加一条静态路由:
  # route add -net 10.0.0.0 netmask 255.0.0.0
  网络中当前的路由表为
# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface
172.16.0.0 * 255.255.0.0 U 0 0 0 et
 2.3配置eth3
  最后配置eth3,它连接192.168.1.0网段,分配的IP地址是192.168.1.254,执行下列命令:
  # ifconfig eth2 192.168.1.254 netmask 255.255.255.0
  编辑/etc/sysconfig/network-scripts目录下的ifcfg-eth2文件,内容如下:
DEVICE = eth2
ONBOOT = yes
BROADCAST = 192.168.1.255
NETWORK = 192.168.1.0
NETMASK = 255.255.255.0
IPADDR = 192.168.1.254
  再增加一条静态路由:
  # route add -net 192.168.1.0 netmask 255.255.255.0
  这样网络中就有三条静态路由记录了:
# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface
172.16.0.0 * 255.255.0.0 U 0 0 0 eth0
10.0.0.0 * 255.0.0.0 U 0 0 0 eth1
192.168.1.0 * 255.255.255.0 U 0 0 0 eth2
  2.4配置缺省路由和系统转发
  2.4.1建立缺省路由
  还要为系统增加一条缺省路由,因为缺省的路由是把所有的数据包都发往它的上一级网关(假设地址是172.16.1.100,这个地址依赖于使用的网络而定,由网络管理员分配),因此增加如下的缺省路由记录:
  # route add default gw 172.16.1.100
  或者
  #ip route add default via 172.16.1.100 dev eth0
  这样系统的静态路由表建立完成,它的内容是
# route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface
172.16.0.0 * 255.255.0.0 U 0 0 0 eth0
10.0.0.0 * 255.0.0.0 U 0 0 0 eth1
192.168.1.0 * 255.255.255.0 U 0 0 0 eth2
default 172.16.1.100 0.0.0.0 UG 0 0 0 eth0
  2.4.2建立路由转发
  最后一步,要增加系统的IP转发功能。这个功能由/proc/sys/net/ipv4目录下的ip_forward文件控制,执行如下命令打开ip转发功能:
  echo 1 > /proc/sys/net/ipv4/ip_forward
  或者
  /etc/sysctl.conf文件,让包转发功能在系统启动时自动生效:
  # Controls IP packet forwarding
  net.ipv4.ip_forward = 1
  iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
  #iptables -t nat -A POSTROUTING -s 192.168.1.0/255.255.255.0 -o eth1 -j MASQUERADE
  参数说明:
  -t nat:调用NAT表,调用这个说明遇到了产生新的连接的包。
  -A POSTROUTIN:指定信息包打算离开防火墙时改变它们的规则,意即使用NAT。
  -s 192.168.1.0/255.255.255.0:要进行NAT转换的源地址,也就是内网地址。
  -o eht0:输出接口为eth0。
  -j MASQUERADE:指定进行地址伪装,意即将内网地址伪装成外接口eth0的地址进行传输。
  整条语句的意思就是将内网向外连接的数据包的地址转换为外网接口的地址并从外接口转发,同时外
  网访问内网的连接将被映射到相应的内网地址。
  Iptables上的设置可用命令iptables -t nat –nvL查看。
  保存iptables配置。
  /etc/rc.d/init.d/iptables save
  使用这条命令就可以将iptables配置写入到文件/etc/sysconfig/iptables中去了。
  重启iptables使配置生效。
  service iptables restart
  2.4.3 常用命令
ip route del default
iptables -t filter –nvL
iptables -t nat -nvL
ip route ls
ip route delete 169.254.0.0/16
service iptables save
watch iptables -nvL
  三.测试路由器的工作情况。
  在linux路由器上测试:
  第一步,测试自身网络是否工作正常,执行如下命令
  ping 172.16.1.1
  ping 192.168.1.254
  ping 10.254.254.254
  如果这些地址都能ping通,则第一步通过,否则查找原因,排除错误。
  第二步,测试与上一级网关之间是否连通
  ping 172.16.1.100
  如果通畅,则正常;否则查找错误原因。
  在192.168.1.0网络内测试,假设它的地址是192.168.1.1,
  第一步,测试自身是否工作正常, ping 192.168.1.1
  第二步,测试与网关是否连通, ping 192.168.1.254
  第三步,测试与10.254.254.99是否连通,这是10.0.0.0网段内的一台计算机 ping 10.254.254.99
  第四步,测试与外网地址是否连通 ping 172.16.1.100 如果连通了,表示路由器配置正确,否则,查找原因,并排除之。

posted @ 2013-12-23 09:37 顺其自然EVO 阅读(304) | 评论 (0)编辑 收藏

仅列出标题
共394页: First 上一页 167 168 169 170 171 172 173 174 175 下一页 Last 
<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

导航

统计

常用链接

留言簿(55)

随笔分类

随笔档案

文章分类

文章档案

搜索

最新评论

阅读排行榜

评论排行榜