java吸_结对编程(java)
結對成員 3117004646 陳浩民
3117004676 葉劍庭
二、PSP表格
Planning
計劃
40
35
· Estimate
· 估計這個任務需要多少時間
40
35
Development
開發
1480
1585
· Analysis
· 需求分析
150
150
· Design Spec
· 生成設計文檔
80
65
· Design Review
· 設計復審
60
70
· Coding Standard
· 代碼規范
30
30
· Design
· 具體設計
90
90
· Coding
· 具體編碼
900
1000
· Code Review
· 代碼復審
80
90
· Test
· 測試(自我測試,修改代碼,提交修改)
90
90
Reporting
報告
150
150
· Test Report
· 測試報告
70
70
· Size Measurement
· 計算工作量
40
40
· Postmortem & Process Improvement Plan
· 事后總結, 并提出過程改進計劃
40
40
合計
1670
1770
三、效能分析
在優化之前,生成10000條算式的時間:
優化之后,生成10000條算式的時間
優化內容:
在原來生成題目中,括號的數量是通過隨機生成得到的,但必須要生成該數量的括號,由于是隨機生成,所以可能需要很多次都生成出相同的或者是位置不正確的括號,需要刪掉重新再生成,后來將括號生成改成了同樣是隨機生成,但是不需要一定生成這個數量的括號,一旦生成失敗就跳過,直到循環結束,這樣能使循環的次數減少,提升運行速度,但是降低了括號嵌套的概率。
四、設計實現過程
流程圖
對于這個題目,我們將其分成了四個類:Main類,FileOperation類,GenerateTitle類,CheckResult類。
1、Main類:用正則表達式對輸入進行檢測,接收用戶輸入。
2、FileOperation類:對文件進行讀寫操作,并且對讀入的文件進行答案校對。
3、GenerateTitle類:使用隨機數,生成題目,并且對其添加括號和查重。
4、CheckResult類:專門負責計算題目的答案。
五、代碼說明
1、GenerateTitle類
這部分最主要是插入括號和查重,其他生成操作數和操作符是通過隨機數生成,然后將它們放到兩個數組中,這樣方便構建算式和查重,其他生成隨機數,真分數都不難,化簡只需要求出最大公因數,然后約分就行了。
括號方法,我們的思路是不管怎樣先生成隨機生成左括號,然后再隨機生成右括號。在生成左括號時,要判斷這個地方是否已經生成過左括號了,這里就要分兩類討論:①此處沒有生成過左括號,則在生成右括號時,需要判斷生成右括號的位置是否存在左括號或者左括號位置是否有右括號,如果有,則需要重新生成右括號。②此處已經生成過左括號,則在生成右括號時,需要判斷該位置是否有右括號,如果有,那肯定是兩個括號括住同一條式子,則需要重新生成,最后還是要判斷生成右括號的位置是否存在左括號或者左括號位置是否有右括號。這樣才能保證括號的正確加入。
但是這里有個缺陷,這里生成的括號不一定有意義,如1+(2+3),我們沒有做到將這種沒有意義的括號去除方法,因為要保證不能把程序寫死,要保留擴展性,這樣做這個確實很難,但是如果能做完,查重就變得簡單很多了。
/*** 生成括號
*@paramnum 操作數數組
*@paramop 操作符數組
*@paramoperatorNum 操作數個數
*@return
*/
public String Cover(String[] num, String[] op,intoperatorNum){
String title= null;inti, j, k, judge, lPoint, rPoint,midel,judge_left,judge_right;
Random ra= newRandom();if(operatorNum == 1)
title= num[0] + " " +op[0] + " " + num[1]; //只有一個操作符,不能生成括號
else{
k= ra.nextInt(operatorNum); //隨機生成括號數量
if(k >= operatorNum) //括號數量需要小于等于操作符數量減1,否則括號生成位置會出錯
k =ra.nextInt(operatorNum);if(k == 0) {//不生成括號,直接組裝算式
title = num[0];for (i = 1; i < operatorNum + 1; i++)
title= title + " " + op[i - 1] + " " +num[i];
}else{//采用兩個數組,長度為操作數個數,用來標志左右括號的位置
int[] leftBracket = new int[operatorNum + 1]; //左括號,元素值為該位置左括號數量
int[] rightBracket = new int[operatorNum + 1]; //右括號,元素值為該位置右括號數量
for(i = 0;i < operatorNum+1;i++) {//需要賦初值
leftBracket[i] = 0;
rightBracket[i]= 0;
}for(i = 0;i < k;i++){ //生成括號
lPoint = ra.nextInt(operatorNum); //隨機生成左括號位置
judge = 0; //標志作用//判斷該位置是否有左括號
if(leftBracket[lPoint] == 0) {
midel= 0; //標志該位置沒有左括號
leftBracket[lPoint]++;
}else{
midel= 1; //標志該位置存在左括號
leftBracket[lPoint]++;
}
rPoint= ra.nextInt(operatorNum) + 1; //隨機生成右括號
while(rPoint <= lPoint || (lPoint == 0 && rPoint ==operatorNum))//該右括號不能在左括號左邊,且不能和左括號在同一位置
rPoint = ra.nextInt(operatorNum) + 1;if(midel == 0){ //該位置上沒有左括號
if(leftBracket[rPoint] != 0) { //判斷在右括號的位置上是否存在左括號
while (leftBracket[rPoint] != 0 || (lPoint == 0 && rPoint == operatorNum) || rPoint <=lPoint) {//隨機再產生右括號,若當跳出循環前都沒有合適的右括號,則去掉左括號
rPoint = ra.nextInt(operatorNum) + 1;
judge++; //防止出現死循環
if(judge == operatorNum) //跳出循環
break;
}if(rightBracket[lPoint] != 0 || leftBracket[rPoint] != 0 || judge ==operatorNum) {//避免先生成左括號再生成右括號或者反過來生成的情況,即左括號位置有右括號或者右括號位置有左括號,如(3+(4)+5+6)
leftBracket[lPoint]--; //去掉該左括號
continue;
}
rightBracket[rPoint]++; //添加右括號
}elserightBracket[rPoint]++; //添加右括號
}else{ //在生成左括號位置已經有左括號
if(rightBracket[rPoint] != 0) { //右括號位置已經存在右括號,這種情況不合法,需要重新生成,如((3+4))+5
while (rightBracket[rPoint] != 0 || (lPoint == 0 && rPoint == operatorNum) || rPoint <=lPoint) {
rPoint= ra.nextInt(operatorNum) + 1;
judge++; //防止死循環
if(judge ==operatorNum)break;
}if(rightBracket[lPoint] != 0 || leftBracket[rPoint] != 0 || judge ==operatorNum) {
leftBracket[lPoint]--;continue;
}
rightBracket[rPoint]++;
}elserightBracket[rPoint]++;
}
judge_left= 0;
judge_right= 0;//由于為了避免增加復雜情況來進行判斷,不同情況用不同的生成方法(從內向外,從外向內)//這樣減少了括號嵌套的概率,但使代碼變簡單
for(j = lPoint;j <= rPoint;j++){if(leftBracket[j]!=0)//遇到左括號,加該位置左括號個數
judge_left+=leftBracket[j];if(rightBracket[j]!=0)//加右括號
judge_right+=rightBracket[j];
}if(judge_left!=judge_right){//如果左括號數量不等于右括號,則不生成括號
rightBracket[rPoint]--;
leftBracket[lPoint]--;continue;
}//添加括號
num[lPoint] = "(" +num[lPoint];
num[rPoint]= num[rPoint] + ")";
}
}
title= num[0];for(i = 1;i < operatorNum+1;i++)
title= title + " " + op[i - 1] + " " +num[i];returntitle;
}returntitle;
}
查重方法
為何這里要注釋掉呢,因為這個查重方法實在是太糟糕了,我們沒有想到更優的算法來實現查重,只能夠用最白癡的算法來做,它的復雜度是O(n^2),實際上算上外循環是O(n^3),這效率不敢想象,我們生成10000道題需要50s的時間才能生成完,如果沒有優化這個算法就不能實際用在我們的程序中,這就是沒用數據結構的悲哀。但是在查重時還需要考慮很多效率問題,當-r參數很大的時候,基本上是不會出現重復的,但是-n大于一定值的時候,就有很大可能或者一定會出現重復,我們需要花很大的精力去重新生成一個不重復的式子,甚至進入死循環,這兩種情況相當影響算法的效率。
/*else {
//檢查重復的算式
checkCopy = "no";
if(judgement == "yes" && i != 0)
for(k = 0;k < i;k++){
checkCop = answer[k].split(Integer.toString(k+1)+". ");
if(checkCop[1].equals(checkOut)){
//減少檢測數量,檢測答案相同的式子
checkCop = title[k].split(Integer.toString(k+1)+". ");
w = 0;
//檢測長度相同的式子
if(!checkCop[1].contains(num[0]) && checkCop[1].split(" ").length == (operationNumber + operatorNum))
continue;
while(w < operatorNum){
//檢測式子內部內容
if(!checkCop[1].contains(num[w+1]))
break;
else if(!checkCop[1].contains(operator[w]))
break;
w++;
}
checkCopy = "yes";
break;
}
else
checkCopy = "no";
}
if(checkCopy == "no") {
answer[i] = Integer.toString(i + 1) + ". " + checkOut;
title[i] = Integer.toString(i + 1) + ". " + title[i];
}
else
i--;
}*/
2、CheckResult類
這個類和GenetateTitle類是這個程序的兩大核心,而這個類的核心是計算,計算的方法也是相當經典,使用后綴表達式來計算,實際上這個算法思路并不難,關鍵是具體實現的方法問題,這里我們用了一個技巧,將自然數都當成分數(分母為1)來計算,這樣極大地方便程序的設計。
/*** 采用后綴表達式計算生成的式子,具體思路參照逆波蘭式算法
*@paramstr 題目
*@return
*/
publicString Calc(String str){
GenerateTitle gen= newGenerateTitle();char[] title =str.toCharArray();
Stack operator = new Stack(); //符號棧
operator.push('k'); //壓入標志位'k'
Stack number = new Stack(); //操作數棧
number.push("q"); //壓入標志位"q"
String molecule1, molecule2, denominator1, denominator2;
String molecule, midel, denominator;charoperating;
String[] result;
String[] finResult= new String[2];intjudgeXie, judgeFen, divisor;
outer:for(int i = 0;i < title.length;i++){
judgeXie= 0; //初始化'/'標志
judgeFen = 0; //初始化'\''標志
if(title[i] == '(')//若從題目取出"(",則入運算符棧
operator.push(title[i]);else if(title[i] == ')'){/*若從題目取出")",則從操作數棧取出兩個操作數,并從運算符棧取出一個運算符,并進行運算
*運算完的結果重新壓入操作數棧,重復以上過程,直到遇到第一個"(",最后將其出棧
* 計算時一次取兩個數,即為操作數的分子和分母*/
while(!operator.peek().equals('(')){
denominator2= number.pop(); //取第二個操作數分母
molecule2 = number.pop(); //取第二個操作數分子
denominator1 = number.pop(); //取第一個操作數分母
molecule1 = number.pop(); //取第一個操作數分母
operating = operator.pop(); //取操作符
result =CalcAndCheck(operating, molecule1, denominator1, molecule2, denominator2);//如果不符合規范(產生負數或除數為0),則返回null,重新生成一個新的算式
if(result == null)return null;else{//將計算結果壓棧
number.push(result[0]);
number.push(result[1]);
}
}//將左括號出棧
operator.pop();
}else if(title[i] == '+' || title[i] == '-' || title[i] == '×' || title[i] == '÷'){/*若從題目取出操作符,若棧頂操作符優先級高于該操作符,則從操作數棧取出兩個操作數,并從運算符棧取出一個
*運算符,并進行運算,運算完的結果重新壓入操作數棧,重復以上過程,直到操作符棧的棧頂元素優先級小于從題
*目取出的運算符的優先級(不含等于),則將該操作符壓入棧,否則直接入棧*/
if(operator.peek()!='k' && number.peek()!="q"){ //判斷棧是否為空
operating = operator.peek(); //取棧頂元素,但不出棧
while(operator.peek()!='k' && number.peek()!="q" && !JudgePriority(operating, title[i])){//做上述操作直到??栈蛘卟僮鞣麅炏燃壐哂跅m敳僮鞣?/p>
denominator2 =number.pop();
molecule2=number.pop();
denominator1=number.pop();
molecule1=number.pop();
operating=operator.pop();
result=CalcAndCheck(operating, molecule1, denominator1, molecule2, denominator2);if(result == null)return null;else{//將計算結果壓棧
number.push(result[0]);
number.push(result[1]);
}
operating=operator.peek();
}
}//操作符壓棧
operator.push(title[i]);
}else if(title[i] >= '0' && title[i] <= '9'){/*取出操作數,將操作數壓棧
*這里的將自然數當作分數來算,分母為1,這樣極大地減少了計算的復雜度
* 需要區分
* 若為真分數,則molecule代表分子部分,denominator代表分母部分
* 若為假分數,則molecule代表整數部分,midel代表分子部分,denominator代表分母部分*/molecule= midel = denominator = String.valueOf(title[i]); //取第一個數
i++;if(i ==title.length){//如果到達算式末尾,則直接壓入棧
number.push(molecule);
number.push("1"); //這種情況只能為整數,所以分母壓入1
continue;
}//取完整的操作數
while(i < title.length && title[i] != ' ' && title[i] != ')'){if(title[i] == '/') {//遇到'/',則為分母,取分母部分
judgeXie = 1;
i++;
denominator= String.valueOf(title[i]); //取分母的第一位
if(i == title.length - 1){ //到達算式末尾
i--;if(judgeFen == 1){//判斷是否為真分數,如果是,則將需要將帶分數轉化為假分數,再壓棧
number.push(String.valueOf(Integer.valueOf(molecule) * Integer.valueOf(denominator) +Integer.valueOf(midel)));
}elsenumber.push(molecule);
number.push(denominator);break outer; //下一個循環
}
i++;continue;
}if(title[i] == '\''){//遇到'\'',即為帶分數,則取分子部分
judgeFen = 1;
judgeXie= 2;
i++;
midel=String.valueOf(title[i]);
i++;continue;
}if(judgeXie == 0)
molecule= molecule +String.valueOf(title[i]);else if(judgeXie == 1)
denominator= denominator +String.valueOf(title[i]);else if(judgeFen == 1)
midel= midel +String.valueOf(title[i]);
i++;
}if(judgeXie == 1){
i--;if(judgeFen == 1) {
number.push(String.valueOf(Integer.valueOf(molecule)* Integer.valueOf(denominator) +Integer.valueOf(midel)));
}elsenumber.push(molecule);
number.push(denominator);
}else{
i--;
number.push(molecule);
number.push("1");
}
}
}//計算最終結果
while(operator.peek()!='k'){
denominator2=number.pop();
molecule2=number.pop();
denominator1=number.pop();
molecule1=number.pop();
operating=operator.pop();
result=CalcAndCheck(operating, molecule1, denominator1, molecule2, denominator2);if(result == null)//不符合規范,返回null重新生成新的算式
return null;else{//將計算結果壓棧
number.push(result[0]);
number.push(result[1]);
}
}
finResult[1] = number.pop(); //取最終結果分母
finResult[0] = number.pop(); //取最終結果分子
if(finResult[1].equals("1")) //若分母為1,則為整數
return finResult[0];else if(finResult[0].equals("0")) //若分子為0,則答案為0
return "0";else{
divisor= gen.CommonDivisor(Integer.valueOf(finResult[0]), Integer.valueOf(finResult[1])); //計算最大公約數//將結果化簡和化成分數,并返回
return gen.ChangePorper(Integer.valueOf(finResult[0]), Integer.valueOf(finResult[1]), divisor);
}
}/*** 判斷符號優先級,只有
*@parambefor 符號棧頂的符號
*@paramafter 算式的符號
*@return
*/
public boolean JudgePriority(char befor, charafter){if((befor == '+' || befor == '-') && (after == '×' || after == '÷'))return true;else if(befor == '(') {//'('標志著邊界,不能再往左計算
return true;
}else{return false;
}
}/*** 對操作數進行運算,具體就是簡單的兩個分數相運算
*@paramoperator 操作符
*@parammole1 操作數1分子
*@paramdemo1 操作數1分母
*@parammole2 操作數2分子
*@paramdemo2 操作數2分母
*@return
*/
public String[] CalcAndCheck(charoperator, String mole1, String demo1, String mole2, String demo2){
String[] result= new String[2];intm1, m2, d1, d2;
m1=Integer.valueOf(mole1);
m2=Integer.valueOf(mole2);
d1=Integer.valueOf(demo1);
d2=Integer.valueOf(demo2);if(operator == '+'){
result[0] = String.valueOf(m1 * d2 + m2 *d1);
result[1] = String.valueOf(d1 *d2);
}else if(operator == '-'){if((m1 * d2 - m2 * d1) < 0)//判斷在計算過程中是否會產生負數
return null;
result[0] = String.valueOf(m1 * d2 - m2 *d1);
result[1] = String.valueOf(d1 *d2);
}else if(operator == '×'){
result[0] = String.valueOf(m1 *m2);
result[1] = String.valueOf(d1 *d2);
}else{if(m2 == 0 || d2 == 0)//除數是否為0
return null;
result[0] = String.valueOf(m1 *d2);
result[1] = String.valueOf(d1 *m2);
}returnresult;
}
3、FileOperation類
這個類就是基本的IO操作,最主要但也相當簡單的是對答案的方法,直接調用CheckResult類的計算方法,然后用equals方法來檢測答案是否相同就行了。
/*** 寫入文件方法,將生成的題目和答案寫入文本文件
*@paramtitle 題目數組
*@paramanswer 答案數組*/
public voidFWriter(String[] title,String[] answer){
String titleFile= "src\\Exercises.txt";
String answerFile= "src\\Answers.txt";int length =title.length;int i = 0;try{//打開文件輸出流
BufferedWriter tFile = new BufferedWriter(new FileWriter(newFile(titleFile)));
BufferedWriter aFile= new BufferedWriter(new FileWriter(newFile(answerFile)));while(i
tFile.write(title[i]);
aFile.write(answer[i]);
tFile.newLine();
aFile.newLine();
i++;
}
tFile.close();
aFile.close();
}catch(Exception ex){
ex.getMessage();
}
}/*** 讀取文件方法,讀取用戶輸入的題目和答案文件并進行校對,將成績輸入到Grade文件
*@paramtitlePath 題目路徑
*@paramanswerPath 答案路徑
*@return
*/
public intFReader(String titlePath, String answerPath){
String title= null;
String answer= null;
String correct= "Correct:(";
String wrong= "Wrong:(";
String[] str= new String[2];int i = 1,correctNum = 0;try{//讀取文件內容
BufferedReader titleReader = new BufferedReader(new FileReader(newFile(titlePath)));
BufferedReader answerReader= new BufferedReader(new FileReader(newFile(answerPath)));while((title = titleReader.readLine()) != null){//每讀取一行,就進行判斷,并進行統計對錯數量
answer =answerReader.readLine();if(CheckAnswer(title, answer)){
correctNum++;if(correct.equals("Correct:("))
correct= correct +Integer.toString(i);elsecorrect= correct + ", " +Integer.toString(i);
}else
if(wrong.equals("Wrong:("))
wrong= wrong +Integer.toString(i);elsewrong= wrong + ", " +Integer.toString(i);
i++;
}
titleReader.close();
answerReader.close();
str= correct.split(":");
correct= str[0] + ":" + Integer.toString(correctNum) + str[1] + ")";
str= wrong.split(":");
wrong= str[0] + ":" + Integer.toString(i - correctNum - 1) + str[1] + ")";//將成績寫入Grade文檔
BufferedWriter gradeFile = new BufferedWriter(new FileWriter(new File("src\\Grade.txt")));
gradeFile.write(correct);
gradeFile.newLine();
gradeFile.write(wrong);
gradeFile.close();return 1;
}catch(Exception ex){
ex.getMessage();
System.out.println("文件路徑不正確");return 2;
}
}/*** 對答案方法,將計算題目答案并和用戶答案對比
*@paramtitle 題目
*@paramanswer 答案
*@return
*/
public booleanCheckAnswer(String title, String answer){
CheckResult check= newCheckResult();
String[] str;
str= title.split("\\. ",2);
title= str[str.length-1];
str= answer.split("\\. ",2);
answer= str[str.length-1];if(check.Calc(title).equals(answer))return true;else
return false;
}
4、Main類
檢測輸入,防止程序在某些地方死掉
public static voidmain(String[] args) {GenerateTitle generateTitle= newGenerateTitle();
CheckResult checkResult= newCheckResult();
FileOperation fileOperation= newFileOperation();
String fileHandle;
String[] fileSpit;
System.out.println("-------------------------------------------------------------------");
System.out.println("小學生四則運算生成器");
System.out.println("請按格式輸入來選擇以下功能:");
System.out.println("Myapp.exe -n x -r y x為生成題目個數,y為題目中自然數,真分數,真分數分母的范圍(x和y大于0)");
System.out.println("題目和答案分別在Exercises.txt和Answers.txt中生成");
System.out.println(" ");
System.out.println("Myapp.exe -e .txt -a .txt");
System.out.println("以上兩個txt文件分別為想要判定對錯的題目和答案");
System.out.println(" ");
System.out.println("-q 退出生成器");
System.out.println("-------------------------------------------------------------------");final String FILEMATCHONE = "(Myapp.exe|myapp.exe)(\\s+(-n))(\\s+\\d+)(\\s+(-r))(\\s+\\d+)";final String FILEMATCHTWO = "(Myapp.exe|myapp.exe)(\\s+(-e))(\\s+\\S+)(\\s+(-a))(\\s+\\S+)";int judge = 0;while(true){
judge= 0;
Scanner instruct= newScanner(System.in);
fileHandle=instruct.nextLine();
fileSpit= fileHandle.split("\\s+");if(fileHandle.equals("-q")){
System.out.println("謝謝使用");break;
}else if(Pattern.matches(FILEMATCHONE, fileHandle)){long startTime = System.currentTimeMillis(); //獲取開始時間
judge = generateTitle.creatTitle(Integer.valueOf(fileSpit[2]), Integer.valueOf(fileSpit[4]));if(judge == 0)
System.out.println("題目答案生成成功");else if(judge == 1) {
System.out.println("參數錯誤,n需要大于0");
System.out.println("請重新輸入,格式為:");
System.out.println("Myapp.exe -n x -r y x為生成題目個數,y為題目中自然數,真分數,真分數分母的范圍(x和y大于0)");
}else{
System.out.println("參數錯誤,r需要大于0");
System.out.println("請重新輸入,格式為:");
System.out.println("Myapp.exe -n x -r y x為生成題目個數,y為題目中自然數,真分數,真分數分母的范圍(x和y大于0)");
}long endTime = System.currentTimeMillis(); //獲取結束時間
System.out.println("程序運行時間:" + (endTime - startTime) + "ms"); //輸出程序運行時間
}else if(Pattern.matches(FILEMATCHTWO, fileHandle)){
judge= fileOperation.FReader(fileSpit[2], fileSpit[4]);if(judge == 1)
System.out.println("答案校對完畢,成績已在Grade.txt出生成");else{
System.out.println("請重新輸入正確的文件路徑");
}
}else{
System.out.println("參數輸入錯誤,請按照格式輸入");
System.out.println("-------------------------------------------------------------------");
System.out.println("請按格式輸入來選擇以下功能:");
System.out.println("Myapp.exe -n x -r y x為生成題目個數,y為題目中自然數,真分數,真分數分母的范圍(x和y大于0)");
System.out.println("題目和答案分別在Exercises.txt和Answers.txt中生成");
System.out.println(" ");
System.out.println("Myapp.exe -e .txt -a .txt");
System.out.println("以上兩個txt文件分別為想要判定對錯的題目和答案");
System.out.println(" ");
System.out.println("-q 退出生成器");
}
System.out.println("-------------------------------------------------------------------");
System.out.println("請輸入命令:");
}
}
六、測試運行
測試題目
Myapp.exe -n 10000 -r 40
更改(9982,9983,9990,9992,9996,9998,10000)的值,然后檢驗答案
由此可見,能夠檢測到錯誤的答案并進行統計。
七、項目小結
這次結對項目是我們之間第一次合作,因為彼此能力間的有所差距,所以開發的時間比較長,有一部分時間是在指導對方,雖然不能說編程的能力有明顯的增長,但是雙方在不斷交流中也學到了許多東西,也能夠把之前對方的建議用在后面的編程中。結對編程對于擴寬編程思路還是有很大幫助的,在自己想不到或者想到的思路不是很好的時候,對方能夠提出一些自己沒有想到的思路,或者指出自己存在的問題,這樣把思路擴寬了,編程也更有效率,準確率也大大提高了。
總結
以上是生活随笔為你收集整理的java吸_结对编程(java)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux配置http代理(原理)
- 下一篇: [转载]用 Apache Geronim