JBUILDER的光標(biāo)定位不準(zhǔn)確的最佳解決方案是:進(jìn)入%JBUILDER_HOME%/bin目錄下,用寫(xiě)字板編輯jbuilder.config,把下面的配置加進(jìn)去: vmparam -Dprimetime.editor.useVariableWidthFont=true |
Ant的簡(jiǎn)介:類(lèi)似make工具,但可以支持多平臺(tái)
Ant的安裝:配置ant的準(zhǔn)備工作:ant_home 指Ant的安裝目錄,在path中加入%ant_home%/bin,用于命令行下
運(yùn) 行ant
Ant的結(jié)構(gòu):主要是通過(guò)對(duì)build.xml的配置,
Ant內(nèi)置任務(wù): 描述
property 設(shè)置name/value的屬性
mkdir 創(chuàng)建目錄
copy 拷貝
delete 刪除
javac 編繹
war 打包
下面是一個(gè)簡(jiǎn)單build.xml的示例:
<project name="bookstore" default="about" basedir=".">
<target name="init">
<tstamp/>
<property name="build" value="build" />
<property name="src" value="src" />
<property environment="myenv" />
<property name="servletpath" value="${myenv.CATALINA_HOME}/common/lib/servlet-api.jar" />
<property name="mysqlpath" value="WEB-INF/lib/mysqldriver.jar" />
<mkdir dir="${build}" />
<mkdir dir="${build}\WEB-INF" />
<mkdir dir="${build}\WEB-INF\classes" />
<copy todir="${build}" >
<fileset dir="${basedir}" >
<include name="*.jsp" />
<include name="*.bmp" />
<include name="WEB-INF/**" />
<exclude name="build.xml" />
</fileset>
</copy>
</target>
<target name="compile" depends="init">
<javac srcdir="${src}"
destdir="${build}/WEB-INF/classes"
classpath="${servletpath}:${mysqlpath}">
</javac>
</target>
<target name="bookstorewar" depends="compile">
<war warfile="${build}/bookstore.war" webxml="${build}/WEB-INF/web.xml">
<lib dir="${build}/WEB-INF/lib"/>
<classes dir="${build}/WEB-INF/classes"/>
<fileset dir="${build}"/>
</war>
</target>
<target name="about" >
<echo>
This build.xml file contains targets
for building bookstore web application
</echo>
</target>
</project>
從示例我們看出來(lái),整個(gè)xml是一個(gè)project,project下有幾個(gè)為init,compile,的target
運(yùn)行時(shí)首先在這個(gè)目錄下打開(kāi)dos窗口,以這個(gè)xml為準(zhǔn),如果你只運(yùn)行ant那么只會(huì)輸出echo中的內(nèi)容
因?yàn)閜roject的default是about; 如果運(yùn)行ant complie 它會(huì)執(zhí)行兩個(gè)target: init 和complie,因?yàn)閏omplie是依靠init
的。
用了以后發(fā)現(xiàn)ant 原來(lái)很簡(jiǎn)單,當(dāng)然現(xiàn)在只是學(xué)了個(gè)皮毛而已。
RMI,遠(yuǎn)程方法調(diào)用(Remote Method Invocation)是Enterprise JavaBeans的支柱,是建立分布式Java應(yīng)用程序的方便途徑。RMI是非常容易使用的,但是它非常的強(qiáng)大。
RMI的基礎(chǔ)是接口,RMI構(gòu)架基于一個(gè)重要的原理:定義接口和定義接口的具體實(shí)現(xiàn)是分開(kāi)的。下面我們通過(guò)具體的例子,建立一個(gè)簡(jiǎn)單的遠(yuǎn)程計(jì)算服務(wù)和使用它的客戶程序
一個(gè)正常工作的RMI系統(tǒng)由下面幾個(gè)部分組成:
● 遠(yuǎn)程服務(wù)的接口定義
● 遠(yuǎn)程服務(wù)接口的具體實(shí)現(xiàn)
● 樁(Stub)和框架(Skeleton)文件
● 一個(gè)運(yùn)行遠(yuǎn)程服務(wù)的服務(wù)器
● 一個(gè)RMI命名服務(wù),它允許客戶端去發(fā)現(xiàn)這個(gè)遠(yuǎn)程服務(wù)
● 類(lèi)文件的提供者(一個(gè)HTTP或者FTP服務(wù)器)
● 一個(gè)需要這個(gè)遠(yuǎn)程服務(wù)的客戶端程序
下面我們一步一步建立一個(gè)簡(jiǎn)單的RMI系統(tǒng)。首先在你的機(jī)器里建立一個(gè)新的文件夾,以便放置我們創(chuàng)建的文件,為了簡(jiǎn)單起見(jiàn),我們只使用一個(gè)文件夾存放客戶端和服務(wù)端代碼,并且在同一個(gè)目錄下運(yùn)行服務(wù)端和客戶端。
如果所有的RMI文件都已經(jīng)設(shè)計(jì)好了,那么你需要下面的幾個(gè)步驟去生成你的系統(tǒng):
1、 編寫(xiě)并且編譯接口的Java代碼
2、 編寫(xiě)并且編譯接口實(shí)現(xiàn)的Java代碼
3、 從接口實(shí)現(xiàn)類(lèi)中生成樁(Stub)和框架(Skeleton)類(lèi)文件
4、 編寫(xiě)遠(yuǎn)程服務(wù)的主運(yùn)行程序
5、 編寫(xiě)RMI的客戶端程序
6、 安裝并且運(yùn)行RMI系統(tǒng)
1、 接口
第一步就是建立和編譯服務(wù)接口的Java代碼。這個(gè)接口定義了所有的提供遠(yuǎn)程服務(wù)的功能,下面是源程序:
//Calculator.java
//define the interface
import java.rmi.Remote;
public interface Calculator extends Remote
{
public long add(long a, long b)
throws java.rmi.RemoteException;
public long sub(long a, long b)
throws java.rmi.RemoteException;
public long mul(long a, long b)
throws java.rmi.RemoteException;
public long div(long a, long b)
throws java.rmi.RemoteException;
}
注意,這個(gè)接口繼承自Remote,每一個(gè)定義的方法都必須拋出一個(gè)RemoteException異常對(duì)象。
建立這個(gè)文件,把它存放在剛才的目錄下,并且編譯。
>javac Calculator.java
2、 接口的具體實(shí)現(xiàn)
下一步,我們就要寫(xiě)遠(yuǎn)程服務(wù)的具體實(shí)現(xiàn),這是一個(gè)CalculatorImpl類(lèi)文件:
//CalculatorImpl.java
//Implementation
import java.rmi.server.UnicastRemoteObject
public class CalculatorImpl extends UnicastRemoteObject implements Calculator
{
// 這個(gè)實(shí)現(xiàn)必須有一個(gè)顯式的構(gòu)造函數(shù),并且要拋出一個(gè)RemoteException異常
public CalculatorImpl()
throws java.rmi.RemoteException {
super();
}
public long add(long a, long b)
throws java.rmi.RemoteException {
return a + b;
}
public long sub(long a, long b)
throws java.rmi.RemoteException {
return a - b;
}
public long mul(long a, long b)
throws java.rmi.RemoteException {
return a * b;
}
public long div(long a, long b)
throws java.rmi.RemoteException {
return a / b;
}
}
同樣的,把這個(gè)文件保存在你的目錄里然后編譯他。
這個(gè)實(shí)現(xiàn)類(lèi)使用了UnicastRemoteObject去聯(lián)接RMI系統(tǒng)。在我們的例子中,我們是直接的從UnicastRemoteObject這個(gè)類(lèi)上繼承的,事實(shí)上并不一定要這樣做,如果一個(gè)類(lèi)不是從UnicastRmeoteObject上繼承,那必須使用它的exportObject()方法去聯(lián)接到RMI。
如果一個(gè)類(lèi)繼承自UnicastRemoteObject,那么它必須提供一個(gè)構(gòu)造函數(shù)并且聲明拋出一個(gè)RemoteException對(duì)象。當(dāng)這個(gè)構(gòu)造函數(shù)調(diào)用了super(),它久激活UnicastRemoteObject中的代碼完成RMI的連接和遠(yuǎn)程對(duì)象的初始化。
3、 樁(Stubs)和框架(Skeletons)
下一步就是要使用RMI編譯器rmic來(lái)生成樁和框架文件,這個(gè)編譯運(yùn)行在遠(yuǎn)程服務(wù)實(shí)現(xiàn)類(lèi)文件上。
>rmic CalculatorImpl
在你的目錄下運(yùn)行上面的命令,成功執(zhí)行完上面的命令你可以發(fā)現(xiàn)一個(gè)Calculator_stub.class文件,如果你是使用的Java2SDK,那么你還可以發(fā)現(xiàn)Calculator_Skel.class文件。
4、 主機(jī)服務(wù)器
遠(yuǎn)程RMI服務(wù)必須是在一個(gè)服務(wù)器中運(yùn)行的。CalculatorServer類(lèi)是一個(gè)非常簡(jiǎn)單的服務(wù)器。
//CalculatorServer.java
import java.rmi.Naming;
public class CalculatorServer {
public CalculatorServer() {
try {
Calculator c = new CalculatorImpl();
Naming.rebind("rmi://localhost:1099/CalculatorService", c);
} catch (Exception e) {
System.out.println("Trouble: " + e);
}
}
public static void main(String args[]) {
new CalculatorServer();
}
}
建立這個(gè)服務(wù)器程序,然后保存到你的目錄下,并且編譯它。
5、 客戶端
客戶端源代碼如下:
//CalculatorClient.java
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
public class CalculatorClient {
public static void main(String[] args) {
try {
Calculator c = (Calculator)
Naming.lookup(
"rmi://localhost
/CalculatorService");
System.out.println( c.sub(4, 3) );
System.out.println( c.add(4, 5) );
System.out.println( c.mul(3, 6) );
System.out.println( c.div(9, 3) );
}
catch (MalformedURLException murle) {
System.out.println();
System.out.println(
"MalformedURLException");
System.out.println(murle);
}
catch (RemoteException re) {
System.out.println();
System.out.println(
"RemoteException");
System.out.println(re);
}
catch (NotBoundException nbe) {
System.out.println();
System.out.println(
"NotBoundException");
System.out.println(nbe);
}
catch (
java.lang.ArithmeticException
ae) {
System.out.println();
System.out.println(
"java.lang.ArithmeticException");
System.out.println(ae);
}
}
}
保存這個(gè)客戶端程序到你的目錄下(注意這個(gè)目錄是一開(kāi)始建立那個(gè),所有的我們的文件都在那個(gè)目錄下),并且編譯他。
6、 運(yùn)行RMI系統(tǒng)
現(xiàn)在我們建立了所有運(yùn)行這個(gè)簡(jiǎn)單RMI系統(tǒng)所需的文件,現(xiàn)在我們終于可以運(yùn)行這個(gè)RMI系統(tǒng)啦!來(lái)享受吧。
我們是在命令控制臺(tái)下運(yùn)行這個(gè)系統(tǒng)的,你必須開(kāi)啟三個(gè)控制臺(tái)窗口,一個(gè)運(yùn)行服務(wù)器,一個(gè)運(yùn)行客戶端,還有一個(gè)運(yùn)行RMIRegistry。
首先運(yùn)行注冊(cè)程序RMIRegistry,你必須在包含你剛寫(xiě)的類(lèi)的那么目錄下運(yùn)行這個(gè)注冊(cè)程序。
>rmiregistry
好,這個(gè)命令成功的話,注冊(cè)程序已經(jīng)開(kāi)始運(yùn)行了,不要管他,現(xiàn)在切換到另外一個(gè)控制臺(tái),在第二個(gè)控制臺(tái)里,我們運(yùn)行服務(wù)器CalculatorService,因?yàn)镽MI的安全機(jī)制將在服務(wù)端發(fā)生作用,所以你必須增加一條安全策略。以下是對(duì)應(yīng)安全策略的例子
grant {
permission java.security.AllPermission "", "";
};
注意:這是一條最簡(jiǎn)單的安全策略,它允許任何人做任何事,對(duì)于你的更加關(guān)鍵性的應(yīng)用,你必須指定更加詳細(xì)安全策略。
現(xiàn)在為了運(yùn)行服務(wù)端,你需要除客戶類(lèi)(CalculatorClient.class)之外的所有的類(lèi)文件。確認(rèn)安全策略在policy.txt文件之后,使用如下命令來(lái)運(yùn)行服務(wù)器。
> java -Djava.security.policy=policy.txt CalculatorServer
這個(gè)服務(wù)器就開(kāi)始工作了,把接口的實(shí)現(xiàn)加載到內(nèi)存等待客戶端的聯(lián)接。好現(xiàn)在切換到第三個(gè)控制臺(tái),啟動(dòng)我們的客戶端。
為了在其他的機(jī)器運(yùn)行客戶端程序你需要一個(gè)遠(yuǎn)程接口(Calculator.class) 和一個(gè)stub(CalculatorImpl_Stub.class)。 使用如下命令運(yùn)行客戶端
prompt> java -Djava.security.policy=policy.txt CalculatorClient
如果所有的這些都成功運(yùn)行,你應(yīng)該看到下面的輸出:
1
9
18
3
如果你看到了上面的輸出,恭喜你,你成功了,你已經(jīng)成功的創(chuàng)建了一個(gè)RMI系統(tǒng),并且使他正確工作了。即使你運(yùn)行在同一個(gè)計(jì)算機(jī)上,RMI還是使用了你的網(wǎng)絡(luò)堆棧和TCP/IP去進(jìn)行通訊,并且是運(yùn)行在三個(gè)不同的Java虛擬機(jī)上。這已經(jīng)是一個(gè)完整的RMI系統(tǒng)。
在一個(gè)有密碼保護(hù)的Web應(yīng)用中,正確處理用戶退出過(guò)程并不僅僅只需調(diào)用HttpSession的invalidate()方法。現(xiàn)在大部分瀏覽器上都有后退和前進(jìn)按鈕,允許用戶后退或前進(jìn)到一個(gè)頁(yè)面。如果在用戶在退出一個(gè)Web應(yīng)用后按了后退按鈕瀏覽器把緩存中的頁(yè)面呈現(xiàn)給用戶,這會(huì)使用戶產(chǎn)生疑惑,他們會(huì)開(kāi)始擔(dān)心他們的個(gè)人數(shù)據(jù)是否安全。許多Web應(yīng)用強(qiáng)迫用戶退出時(shí)關(guān)閉整個(gè)瀏覽器,這樣,用戶就無(wú)法點(diǎn)擊后退按鈕了。還有一些使用javascript,但在某些客戶端瀏覽器這卻不一定起作用。這些解決方案都很笨拙且不能保證在任一情況下100%有效,同時(shí),它也要求用戶有一定的操作經(jīng)驗(yàn)。
這篇文章以示例闡述了正確解決用戶退出問(wèn)題的方案。作者Kevin Le首先描述了一個(gè)密碼保護(hù)Web應(yīng)用,然后以示例程序解釋問(wèn)題如何產(chǎn)生并討論解決問(wèn)題的方案。文章雖然是針對(duì)JSP頁(yè)面進(jìn)行闡述,但作者所闡述的概念很容易理解切能夠?yàn)槠渌鸚eb技術(shù)所采用。最后作者展示了如何用Jakarta Struts優(yōu)雅地解決這一問(wèn)題。
大部分Web應(yīng)用不會(huì)包含象銀行賬戶或信用卡資料那樣機(jī)密的信息,但一旦涉及到敏感數(shù)據(jù),我們就需要提供一類(lèi)密碼保護(hù)機(jī)制。舉例來(lái)說(shuō),一個(gè)工廠中工人通過(guò)Web訪問(wèn)他們的時(shí)間安排、進(jìn)入他們的訓(xùn)練課程以及查看他們的薪金等等。此時(shí)應(yīng)用SSL(Secure Socket Layer)有點(diǎn)殺雞用牛刀的感覺(jué),但不可否認(rèn),我們又必須為這些應(yīng)用提供密碼保護(hù),否則,工人(也就是Web應(yīng)用的使用者)可以窺探到工廠中其他雇員的私人機(jī)密信息。
與上述情形相似的還有位處圖書(shū)館、醫(yī)院等公共場(chǎng)所的計(jì)算機(jī)。在這些地方,許多用戶共同使用幾臺(tái)計(jì)算機(jī),此時(shí)保護(hù)用戶的個(gè)人數(shù)據(jù)就顯得至關(guān)重要。設(shè)計(jì)良好編寫(xiě)優(yōu)秀的應(yīng)用對(duì)用戶專(zhuān)業(yè)知識(shí)的要求少之又少。
我們來(lái)看一下現(xiàn)實(shí)世界中一個(gè)完美的Web應(yīng)用是如何表現(xiàn)的:一個(gè)用戶通過(guò)瀏覽器訪問(wèn)一個(gè)頁(yè)面。Web應(yīng)用展現(xiàn)一個(gè)登陸頁(yè)面要求用戶輸入有效的驗(yàn)證信息。用戶輸入了用戶名和密碼。此時(shí)我們假設(shè)用戶提供的身份驗(yàn)證信息是正確的,經(jīng)過(guò)了驗(yàn)證過(guò)程,Web應(yīng)用允許用戶瀏覽他有權(quán)訪問(wèn)的區(qū)域。用戶想退出時(shí),點(diǎn)擊退出按鈕,Web應(yīng)用要求用戶確認(rèn)他是否則真的需要退出,如果用戶確定退出,Session結(jié)束,Web應(yīng)用重新定位到登陸頁(yè)面。用戶可以放心的離開(kāi)而不用擔(dān)心他的信息會(huì)泄露。另一個(gè)用戶坐到了同一臺(tái)電腦前,他點(diǎn)擊后退按鈕,Web應(yīng)用不應(yīng)該出現(xiàn)上一個(gè)用戶訪問(wèn)過(guò)的任何一個(gè)頁(yè)面。事實(shí)上,Web應(yīng)用在第二個(gè)用戶提供正確的驗(yàn)證信息之前應(yīng)當(dāng)一直停留在登陸頁(yè)面上。
通過(guò)示例程序,文章向您闡述了如何在一個(gè)Web應(yīng)用中實(shí)現(xiàn)這一功能。
JSP示例
為了更為有效地闡述實(shí)現(xiàn)方案,本文將從展示一個(gè)示例應(yīng)用logoutSampleJSP1中碰到的問(wèn)題開(kāi)始。這個(gè)示例代表了許多沒(méi)有正確解決退出過(guò)程的Web應(yīng)用。logoutSampleJSP1包含了下述jsp頁(yè)面:login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, and logoutAction.jsp。其中頁(yè)面home.jsp, secure1.jsp, secure2.jsp, 和logout.jsp是不允許未經(jīng)認(rèn)證的用戶訪問(wèn)的,也就是說(shuō),這些頁(yè)面包含了重要信息,在用戶登陸之前或者退出之后都不應(yīng)該出現(xiàn)在瀏覽器中。login.jsp包含了用于用戶輸入用戶名和密碼的form。logout.jsp頁(yè)包含了要求用戶確認(rèn)是否退出的form。loginAction.jsp和logoutAction.jsp作為控制器分別包含了登陸和退出代碼。
第二個(gè)示例應(yīng)用logoutSampleJSP2展示了如何解決示例logoutSampleJSP1中的問(wèn)題。然而,第二個(gè)應(yīng)用自身也是有疑問(wèn)的。在特定的情況下,退出問(wèn)題還是會(huì)出現(xiàn)。
第三個(gè)示例應(yīng)用logoutSampleJSP3在第二個(gè)示例上進(jìn)行了改進(jìn),比較完善地解決了退出問(wèn)題。
最后一個(gè)示例logoutSampleStruts展示了Struts如何優(yōu)美地解決登陸問(wèn)題。
注意:本文所附示例在最新版本的Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox和Avant瀏覽器上測(cè)試通過(guò)。
Login action
Brian Pontarelli的經(jīng)典文章《J2EE Security: Container Versus Custom》討論了不同的J2EE認(rèn)證途徑。文章同時(shí)指出,HTTP協(xié)議和基于form的認(rèn)證并未提供處理用戶退出的機(jī)制。因此,解決途徑便是引入自定義的安全實(shí)現(xiàn)機(jī)制。
自定義的安全認(rèn)證機(jī)制普遍采用的方法是從form中獲得用戶輸入的認(rèn)證信息,然后到諸如LDAP (lightweight directory access protocol)或關(guān)系數(shù)據(jù)庫(kù)的安全域中進(jìn)行認(rèn)證。如果用戶提供的認(rèn)證信息是有效的,登陸動(dòng)作往HttpSession對(duì)象中注入某個(gè)對(duì)象。HttpSession存在著注入的對(duì)象則表示用戶已經(jīng)登陸。為了方便讀者理解,本文所附的示例只往HttpSession中寫(xiě)入一個(gè)用戶名以表明用戶已經(jīng)登陸。清單1是從loginAction.jsp頁(yè)面中節(jié)選的一段代碼以此闡述登陸動(dòng)作:
Listing 1 //... //initialize RequestDispatcher object; set forward to home page by default RequestDispatcher rd = request.getRequestDispatcher("home.jsp");
//Prepare connection and statement rs = stmt.executeQuery("select password from USER where userName = '" + userName + "'"); if (rs.next()) { //Query only returns 1 record in the result set; only 1 password per userName which is also the primary key if (rs.getString("password").equals(password)) { //If valid password session.setAttribute("User", userName); //Saves username string in the session object } else { //Password does not match, i.e., invalid user password request.setAttribute("Error", "Invalid password.");
rd = request.getRequestDispatcher("login.jsp"); } } //No record in the result set, i.e., invalid username else {
request.setAttribute("Error", "Invalid user name."); rd = request.getRequestDispatcher("login.jsp"); } }
//As a controller, loginAction.jsp finally either forwards to "login.jsp" or "home.jsp" rd.forward(request, response); //... |
本文所附示例均以關(guān)系型數(shù)據(jù)庫(kù)作為安全域,但本文所闡述的觀點(diǎn)對(duì)任何類(lèi)型的安全域都是適用的。
Logout action
退出動(dòng)作就包含了簡(jiǎn)單的刪除用戶名以及對(duì)用戶的HttpSession對(duì)象調(diào)用invalidate()方法。清單2是從loginoutAction.jsp頁(yè)面中節(jié)選的一段代碼以此闡述退出動(dòng)作:
Listing 2 //... session.removeAttribute("User"); session.invalidate(); //... |
阻止未經(jīng)認(rèn)證訪問(wèn)受保護(hù)的JSP頁(yè)面
從form中獲取用戶提交的認(rèn)證信息并經(jīng)過(guò)驗(yàn)證后,登陸動(dòng)作簡(jiǎn)單地往 HttpSession對(duì)象中寫(xiě)入一個(gè)用戶名,退出動(dòng)作則做相反的工作,它從用戶的HttpSession對(duì)象中刪除用戶名并調(diào)用invalidate()方法銷(xiāo)毀HttpSession。為了使登陸和退出動(dòng)作真正發(fā)揮作用,所有受保護(hù)的JSP頁(yè)面都應(yīng)該首先驗(yàn)證HttpSession中是否包含了用戶名以確認(rèn)當(dāng)前用戶是否已經(jīng)登陸。如果HttpSession中包含了用戶名,也就是說(shuō)用戶已經(jīng)登陸,Web應(yīng)用則將剩余的JSP頁(yè)發(fā)送給瀏覽器,否則,JSP頁(yè)將跳轉(zhuǎn)到登陸頁(yè)login.jsp。頁(yè)面home.jsp, secure1.jsp, secure2.jsp和logout.jsp均包含清單3中的代碼段:
Listing 3 //... String userName = (String) session.getAttribute("User"); if (null == userName) { request.setAttribute("Error", "Session has ended. Please login."); RequestDispatcher rd = request.getRequestDispatcher("login.jsp"); rd.forward(request, response); } //... //Allow the rest of the dynamic content in this JSP to be served to the browser //... |
在這個(gè)代碼段中,程序從HttpSession中減縮username字符串。如果字符串為空,Web應(yīng)用則自動(dòng)中止執(zhí)行當(dāng)前頁(yè)面并跳轉(zhuǎn)到登陸頁(yè),同時(shí)給出Session has ended. Please log in.的提示;如果不為空,Web應(yīng)用則繼續(xù)執(zhí)行,也就是把剩余的頁(yè)面提供給用戶。
運(yùn)行l(wèi)ogoutSampleJSP1
運(yùn)行l(wèi)ogoutSampleJSP1將會(huì)出現(xiàn)如下幾種情形:
1) 如果用戶沒(méi)有登陸,Web應(yīng)用將會(huì)正確中止受保護(hù)頁(yè)面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行,也就是說(shuō),假如用戶在瀏覽器地址欄中直接敲入受保護(hù)JSP頁(yè)的地址試圖訪問(wèn),Web應(yīng)用將自動(dòng)跳轉(zhuǎn)到登陸頁(yè)并提示Session has ended.Please log in.
2) 同樣的,當(dāng)一個(gè)用戶已經(jīng)退出,Web應(yīng)用也會(huì)正確中止受保護(hù)頁(yè)面home.jsp, secure1.jsp, secure2.jsp和logout.jsp的執(zhí)行
3) 用戶退出后,如果點(diǎn)擊瀏覽器上的后退按鈕,Web應(yīng)用將不能正確保護(hù)受保護(hù)的頁(yè)面——在Session銷(xiāo)毀后(用戶退出)受保護(hù)的JSP頁(yè)重新在瀏覽器中顯示出來(lái)。然而,如果用戶點(diǎn)擊返回頁(yè)面上的任何鏈接,Web應(yīng)用將會(huì)跳轉(zhuǎn)到登陸頁(yè)面并提示Session has ended.Please log in.
阻止瀏覽器緩存 上述問(wèn)題的根源在于大部分瀏覽器都有一個(gè)后退按鈕。當(dāng)點(diǎn)擊后退按鈕時(shí),默認(rèn)情況下瀏覽器不是從Web服務(wù)器上重新獲取頁(yè)面,而是從瀏覽器緩存中載入頁(yè)面。基于Java的Web應(yīng)用并未限制這一功能,在基于PHP、ASP和.NET的Web應(yīng)用中也同樣存在這一問(wèn)題。
在用戶點(diǎn)擊后退按鈕后,瀏覽器到服務(wù)器再?gòu)姆?wù)器到瀏覽器這樣通常意思上的HTTP回路并沒(méi)有建立,僅僅只是用戶,瀏覽器和緩存進(jìn)行了交互。所以,即使包含了清單3上的代碼來(lái)保護(hù)JSP頁(yè)面,當(dāng)點(diǎn)擊后退按鈕時(shí),這些代碼是不會(huì)執(zhí)行的。
緩存的好壞,真是仁者見(jiàn)仁智者見(jiàn)智。緩存的確提供了一些便利,但通常只在使用靜態(tài)的HTML頁(yè)面或基于圖形或影響的頁(yè)面你才能感受到。而另一方面,Web應(yīng)用通常是基于數(shù)據(jù)的,數(shù)據(jù)通常是頻繁更改的。與從緩存中讀取并顯示過(guò)期的數(shù)據(jù)相比,提供最新的數(shù)據(jù)才是更重要的!
幸運(yùn)的是,HTTP頭信息“Expires”和“Cache-Control”為應(yīng)用程序服務(wù)器提供了一個(gè)控制瀏覽器和代理服務(wù)器上緩存的機(jī)制。HTTP頭信息Expires告訴代理服務(wù)器它的緩存頁(yè)面何時(shí)將過(guò)期。HTTP1.1規(guī)范中新定義的頭信息Cache-Control可以通知瀏覽器不緩存任何頁(yè)面。當(dāng)點(diǎn)擊后退按鈕時(shí),瀏覽器重新訪問(wèn)服務(wù)器已獲取頁(yè)面。如下是使用Cache-Control的基本方法:
1) no-cache:強(qiáng)制緩存從服務(wù)器上獲取新的頁(yè)面
2) no-store: 在任何環(huán)境下緩存不保存任何頁(yè)面
HTTP1.0規(guī)范中的Pragma:no-cache等同于HTTP1.1規(guī)范中的Cache-Control:no-cache,同樣可以包含在頭信息中。
通過(guò)使用HTTP頭信息的cache控制,第二個(gè)示例應(yīng)用logoutSampleJSP2解決了logoutSampleJSP1的問(wèn)題。logoutSampleJSP2與logoutSampleJSP1不同表現(xiàn)在如下代碼段中,這一代碼段加入進(jìn)所有受保護(hù)的頁(yè)面中:
//... response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale" response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility String userName = (String) session.getAttribute("User"); if (null == userName) { request.setAttribute("Error", "Session has ended. Please login."); RequestDispatcher rd = request.getRequestDispatcher("login.jsp"); rd.forward(request, response); } //... |
通過(guò)設(shè)置頭信息和檢查HttpSession中的用戶名確保了瀏覽器不緩存頁(yè)面,同時(shí),如果用戶未登陸,受保護(hù)的JSP頁(yè)面將不會(huì)發(fā)送到瀏覽器,取而代之的將是登陸頁(yè)面login.jsp。
運(yùn)行l(wèi)ogoutSampleJSP2 運(yùn)行l(wèi)ogoutSampleJSP2后將回看到如下結(jié)果:
1) 當(dāng)用戶退出后試圖點(diǎn)擊后退按鈕,瀏覽器并不會(huì)顯示受保護(hù)的頁(yè)面,它只會(huì)現(xiàn)實(shí)登陸頁(yè)login.jsp同時(shí)給出提示信息Session has ended. Please log in.
2) 然而,當(dāng)按了后退按鈕返回的頁(yè)是處理用戶提交數(shù)據(jù)的頁(yè)面時(shí),IE和Avant瀏覽器將彈出如下信息提示:
警告:頁(yè)面已過(guò)期……(你肯定見(jiàn)過(guò))
選擇刷新后前一個(gè)JSP頁(yè)面將重新顯示在瀏覽器中。很顯然,這不是我們所想看到的因?yàn)樗`背了logout動(dòng)作的目的。發(fā)生這一現(xiàn)象時(shí),很可能是一個(gè)惡意用戶在嘗試獲取其他用戶的數(shù)據(jù)。然而,這個(gè)問(wèn)題僅僅出現(xiàn)在后退按鈕對(duì)應(yīng)的是一個(gè)處理POST請(qǐng)求的頁(yè)面。
記錄最后登陸時(shí)間 上述問(wèn)題之所以出現(xiàn)是因?yàn)闉g覽器將其緩存中的數(shù)據(jù)重新提交了。這本文的例子中,數(shù)據(jù)包含了用戶名和密碼。無(wú)論是否給出安全警告信息,瀏覽器此時(shí)起到了負(fù)面作用。
為了解決logoutSampleJSP2中出現(xiàn)的問(wèn)題,logoutSampleJSP3的login.jsp在包含username和password的基礎(chǔ)上還包含了一個(gè)稱(chēng)作lastLogon的隱藏表單域,此表單域動(dòng)態(tài)的用一個(gè)long型值初始化。這個(gè)long型值是調(diào)用System.currentTimeMillis()獲取到的自1970年1月1日以來(lái)的毫秒數(shù)。當(dāng)login.jsp中的form提交時(shí),loginAction.jsp首先將隱藏域中的值與用戶數(shù)據(jù)庫(kù)中的值進(jìn)行比較。只有當(dāng)lastLogon表單域中的值大于數(shù)據(jù)庫(kù)中的值時(shí)Web應(yīng)用才認(rèn)為這是個(gè)有效的登陸。
為了驗(yàn)證登陸,數(shù)據(jù)庫(kù)中l(wèi)astLogon字段必須以表單中的lastLogon值進(jìn)行更新。上例中,當(dāng)瀏覽器重復(fù)提交數(shù)據(jù)時(shí),表單中的lastLogon值不比數(shù)據(jù)庫(kù)中的lastLogon值大,因此,loginAction轉(zhuǎn)到login.jsp頁(yè)面,并提示Session has ended.Please log in.清單5是loginAction中節(jié)選的代碼段:
清單5
//... RequestDispatcher rd = request.getRequestDispatcher("home.jsp"); //Forward to homepage by default //... if (rs.getString("password").equals(password)) { //If valid password long lastLogonDB = rs.getLong("lastLogon"); if (lastLogonForm > lastLogonDB) { session.setAttribute("User", userName); //Saves username string in the session object stmt.executeUpdate("update USER set lastLogon= " + lastLogonForm + " where userName = '" + userName + "'"); } else { request.setAttribute("Error", "Session has ended. Please login."); rd = request.getRequestDispatcher("login.jsp"); } } else { //Password does not match, i.e., invalid user password request.setAttribute("Error", "Invalid password."); rd = request.getRequestDispatcher("login.jsp"); } //... rd.forward(request, response); //... |
為了實(shí)現(xiàn)上述方法,你必須記錄每個(gè)用戶的最后登陸時(shí)間。對(duì)于采用關(guān)系型數(shù)據(jù)庫(kù)安全域來(lái)說(shuō),這點(diǎn)可以可以通過(guò)在某個(gè)表中加上lastLogin字段輕松實(shí)現(xiàn)。LDAP以及其他的安全域需要稍微動(dòng)下腦筋,但很顯然是可以實(shí)現(xiàn)的。
表示最后登陸時(shí)間的方法有很多。示例logoutSampleJSP3利用了自1970年1月1日以來(lái)的毫秒數(shù)。這個(gè)方法在許多人在不同瀏覽器中用一個(gè)用戶賬號(hào)登陸時(shí)也是可行的。
運(yùn)行l(wèi)ogoutSampleJSP3 運(yùn)行示例logoutSampleJSP3將展示如何正確處理退出問(wèn)題。一旦用戶退出,點(diǎn)擊瀏覽器上的后退按鈕在任何情況下都不會(huì)是受保護(hù)的頁(yè)面在瀏覽器上顯示出來(lái)。這個(gè)示例展示了如何正確處理退出問(wèn)題而不需要額外的培訓(xùn)。
為了使代碼更簡(jiǎn)練有效,一些冗余的代碼可以剔除掉。一種途徑就是把清單4中的代碼寫(xiě)到一個(gè)單獨(dú)的JSP頁(yè)中,通過(guò)標(biāo)簽<jsp:include>其他頁(yè)面也可以引用。
Struts框架下的退出實(shí)現(xiàn) 與直接使用JSP或JSP/servlets相比,另一個(gè)可選的方案是使用Struts。為一個(gè)基于Struts的Web應(yīng)用添加一個(gè)處理退出問(wèn)題的框架可以優(yōu)雅地不費(fèi)氣力的實(shí)現(xiàn)。這部分歸功于Struts是采用MVC設(shè)計(jì)模式的因此將模型和視圖清晰的分開(kāi)。另外,Java是一個(gè)面向?qū)ο蟮恼Z(yǔ)言,其支持繼承,可以比JSP中的腳本更為容易地實(shí)現(xiàn)代碼重用。在Struts中,清單4中的代碼可以從JSP頁(yè)面中移植到Action類(lèi)的execute()方法中。
此外,我們還可以定義一個(gè)繼承Struts Action類(lèi)的基本類(lèi),其execute()方法中包含了清單4中的代碼。通過(guò)使用類(lèi)繼承機(jī)制,其他類(lèi)可以繼承基本類(lèi)中的通用邏輯來(lái)設(shè)置HTTP頭信息以及檢索HttpSession對(duì)象中的username字符串。這個(gè)基本類(lèi)是一個(gè)抽象類(lèi)并定義了一個(gè)抽象方法executeAction()。所有繼承自基類(lèi)的子類(lèi)都應(yīng)實(shí)現(xiàn)exectuteAction()方法而不是覆蓋它。清單6是基類(lèi)的部分代碼:
清單6
public abstract class BaseAction extends Action { public ActionForward execute(ActionMapping mapping, ActionForm form,HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setHeader("Cache-Control","no-cache"); //Forces caches to obtain a new copy of the page from the origin server response.setHeader("Cache-Control","no-store"); //Directs caches not to store the page under any circumstance response.setDateHeader("Expires", 0); //Causes the proxy cache to see the page as "stale" response.setHeader("Pragma","no-cache"); //HTTP 1.0 backward compatibility
if (!this.userIsLoggedIn(request)) { ActionErrors errors = new ActionErrors(); errors.add("error", new ActionError("logon.sessionEnded")); this.saveErrors(request, errors); return mapping.findForward("sessionEnded"); } return executeAction(mapping, form, request, response); }
protected abstract ActionForward executeAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException;
private boolean userIsLoggedIn(HttpServletRequest request) { if (request.getSession().getAttribute("User") == null) { return false; }
return true; } } |
清單6中的代碼與清單4中的很相像,僅僅只是用ActionMapping findForward替代了RequestDispatcher forward。清單6中,如果在HttpSession中未找到username字符串,ActionMapping對(duì)象將找到名為sessionEnded的forward元素并跳轉(zhuǎn)到對(duì)應(yīng)的path。如果找到了,子類(lèi)將執(zhí)行其實(shí)現(xiàn)了executeAction()方法的業(yè)務(wù)邏輯。因此,在配置文件struts-web.xml中為所有子類(lèi)聲明個(gè)一名為sessionEnded的forward元素是必須的。清單7以secure1 action闡明了這樣一個(gè)聲明:
清單7
<action path="/secure1" type="com.kevinhle.logoutSampleStruts.Secure1Action" scope="request"> <forward name="success" path="/WEB-INF/jsps/secure1.jsp"/> <forward name="sessionEnded" path="/login.jsp"/> </action> |
繼承自BaseAction類(lèi)的子類(lèi)Secure1Action實(shí)現(xiàn)了executeAction()方法而不是覆蓋它。Secure1Action類(lèi)不執(zhí)行任何退出代碼,如清單8:
public class Secure1Action extends BaseAction { public ActionForward executeAction(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
HttpSession session = request.getSession(); return (mapping.findForward("success")); } } |
只需要定義一個(gè)基類(lèi)而不需要額外的代碼工作,上述解決方案是優(yōu)雅而有效的。不管怎樣,將通用的行為方法寫(xiě)成一個(gè)繼承StrutsAction的基類(lèi)是許多Struts項(xiàng)目的共同經(jīng)驗(yàn),值得推薦。
結(jié)論 本文闡述了解決退出問(wèn)題的方案,盡管方案簡(jiǎn)單的令人驚訝,但卻在所有情況下都能有效地工作。無(wú)論是對(duì)JSP還是Struts,所要做的不過(guò)是寫(xiě)一段不超過(guò)50行的代碼以及一個(gè)記錄用戶最后登陸時(shí)間的方法。在Web應(yīng)用中混合使用這些方案能夠使擁護(hù)的私人數(shù)據(jù)不致泄露,同時(shí),也能增加用戶的經(jīng)驗(yàn)。
Beanutils用了魔術(shù)般的反射技術(shù),實(shí)現(xiàn)了很多夸張有用的功能,都是C/C++時(shí)代不敢想的。無(wú)論誰(shuí)的項(xiàng)目,始終一天都會(huì)用得上它。我算是后知后覺(jué)了,第一回看到它的時(shí)候居然錯(cuò)過(guò)。
1.屬性的動(dòng)態(tài)getter,setter
在這框架滿天飛的年代,不能事事都保證執(zhí)行g(shù)etter,setter函數(shù)了,有時(shí)候?qū)傩允且枰鶕?jù)名字動(dòng)態(tài)的取得的,就像這樣:
BeanUtils.getProperty(myBean,"code");
而B(niǎo)eanUtils更強(qiáng)的功能是直接訪問(wèn)內(nèi)嵌對(duì)象的屬性,只要使用逗號(hào)分割。
BeanUtils.getProperty(orderBean, "address.city");
其他類(lèi)庫(kù)的BeanUtils通常都很簡(jiǎn)單,不能訪問(wèn)內(nèi)嵌的對(duì)象,所以經(jīng)常要用Jakata的BeanUtils替換它們。
BeanUtils還支持List和Map類(lèi)型的屬性。如下面的語(yǔ)法即可取得顧客列表中第一個(gè)顧客的名字
BeanUtils.getProperty(orderBean, "customers[1].name");
其中BeanUtils會(huì)使用ConvertUtils類(lèi)把字符串轉(zhuǎn)為Bean屬性的真正類(lèi)型,方便從HttpServletRequest等對(duì)象中提取bean,或者把bean輸出到頁(yè)面。
而PropertyUtils就會(huì)原色的保留Bean原來(lái)的類(lèi)型。
2.beanCompartor 動(dòng)態(tài)排序
還是通過(guò)反射,動(dòng)態(tài)設(shè)定Bean按照哪個(gè)屬性來(lái)排序,而不再需要在bean的Compare接口進(jìn)行復(fù)雜的條件判斷。 List peoples = ...; // Person對(duì)象的列表 Collections.sort(peoples, new BeanComparator("age")); }}} 如果要支持多個(gè)屬性的復(fù)合排序,如"Order By lastName,firstName" ArrayList sortFields = new ArrayList();
sortFields.add(new BeanComparator("lastName"));
sortFields.add(new BeanComparator("firstName"));
ComparatorChain multiSort = new ComparatorChain(sortFields);
Collections.sort(rows,multiSort);
其中ComparatorChain屬于jakata commons-collections包。
如果age屬性不是普通類(lèi)型,構(gòu)造函數(shù)需要再傳入一個(gè)comparator對(duì)象為age變量排序。
3.Converter 把Request中的字符串轉(zhuǎn)為實(shí)際類(lèi)型對(duì)象
經(jīng)常要從request,resultSet等對(duì)象取出值來(lái)賦入bean中,下面的代碼誰(shuí)都寫(xiě)膩了,如果不用MVC框架的綁定功能的話。
String a = request.getParameter("a");
bean.setA(a);
String b = ....
不妨改為
MyBean bean = ...;
HashMap map = new HashMap();
Enumeration names = request.getParameterNames();
while (names.hasMoreElements())
{
String name = (String) names.nextElement();
map.put(name, request.getParameterValues(name));
}
BeanUtils.populate(bean, map);
其中BeanUtils的populate方法或者getProperty,setProperty方法其實(shí)都會(huì)調(diào)用convert進(jìn)行轉(zhuǎn)換。
但Converter只支持一些基本的類(lèi)型,甚至連java.util.Date類(lèi)型也不支持。而且它比較笨的一個(gè)地方是當(dāng)遇到不認(rèn)識(shí)的類(lèi)型時(shí),居然會(huì)拋出異常來(lái)。
對(duì)于Date類(lèi)型,我參考它的sqldate類(lèi)型實(shí)現(xiàn)了一個(gè)Converter,而且添加了一個(gè)設(shè)置日期格式的函數(shù)。要把這個(gè)Converter注冊(cè),需要如下語(yǔ)句:
ConvertUtilsBean convertUtils = new ConvertUtilsBean();
DateConverter dateConverter = new DateConverter();
convertUtils.register(dateConverter,Date.class);
//因?yàn)橐?cè)converter,所以不能再使用BeanUtils的靜態(tài)方法了,必須創(chuàng)建BeanUtilsBean實(shí)例
BeanUtilsBean beanUtils = new BeanUtilsBean(convertUtils,new PropertyUtilsBean());
beanUtils.setProperty(bean, name, value);
!4 其他功能
4.1 PropertyUtils,當(dāng)屬性為Collection,Map時(shí)的動(dòng)態(tài)讀取:
Collection: 提供index
{{{ BeanUtils.getIndexedProperty(orderBean,"items",1);
或者
BeanUtils.getIndexedProperty(orderBean,"items[1]");
Map: 提供Key Value
BeanUtils.getMappedProperty(orderBean, "items","111");//key-value goods_no=111
或者
BeanUtils.getMappedProperty(orderBean, "items(111)");
4.2 PropertyUtils,獲取屬性的Class類(lèi)型
public static Class getPropertyType(Object bean, String name)
4.3 ConstructorUtils,動(dòng)態(tài)創(chuàng)建對(duì)象
public static Object invokeConstructor(Class klass, Object arg)
4.4 DynaClass動(dòng)態(tài)類(lèi)
其中ComparatorChain屬于jakata commons-collections包。 經(jīng)常要從request,resultSet等對(duì)象取出值來(lái)賦入bean中,下面的代碼誰(shuí)都寫(xiě)膩了,如果不用MVC框架的綁定功能的話。 不妨改為 其中BeanUtils的populate方法或者getProperty,setProperty方法其實(shí)都會(huì)調(diào)用convert進(jìn)行轉(zhuǎn)換。 或者 4.2 PropertyUtils,獲取屬性的Class類(lèi)型
Struts的Token(令牌)機(jī)制能夠很好的解決表單重復(fù)提交的問(wèn)題,基本原理是:服務(wù)器端在處理到達(dá)的請(qǐng)求之前,會(huì)將請(qǐng)求中包含的令牌值與保存在當(dāng)前用戶會(huì)話中的令牌值進(jìn)行比較,看是否匹配。在處理完該請(qǐng)求后,且在答復(fù)發(fā)送給客戶端之前,將會(huì)產(chǎn)生一個(gè)新的令牌,該令牌除傳給客戶端以外,也會(huì)將用戶會(huì)話中保存的舊的令牌進(jìn)行替換。這樣如果用戶回退到剛才的提交頁(yè)面并再次提交的話,客戶端傳過(guò)來(lái)的令牌就和服務(wù)器端的令牌不一致,從而有效地防止了重復(fù)提交的發(fā)生。
這時(shí)其實(shí)也就是兩點(diǎn),第一:你需要在請(qǐng)求中有這個(gè)令牌值,請(qǐng)求中的令牌值如何保存,其實(shí)就和我們平時(shí)在頁(yè)面中保存一些信息是一樣的,通過(guò)隱藏字段來(lái)保存,保存的形式如: 〈input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="6aa35341f25184fd996c4c918255c3ae"〉,這個(gè)value是TokenProcessor類(lèi)中的generateToken()獲得的,是根據(jù)當(dāng)前用戶的session id和當(dāng)前時(shí)間的long值來(lái)計(jì)算的。第二:在客戶端提交后,我們要根據(jù)判斷在請(qǐng)求中包含的值是否和服務(wù)器的令牌一致,因?yàn)榉?wù)器每次提交都會(huì)生成新的Token,所以,如果是重復(fù)提交,客戶端的Token值和服務(wù)器端的Token值就會(huì)不一致。下面就以在數(shù)據(jù)庫(kù)中插入一條數(shù)據(jù)來(lái)說(shuō)明如何防止重復(fù)提交。
在Action中的add方法中,我們需要將Token值明確的要求保存在頁(yè)面中,只需增加一條語(yǔ)句:saveToken(request);,如下所示:
public ActionForward add(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
//前面的處理省略
saveToken(request);
return mapping.findForward("add");
}在Action的insert方法中,我們根據(jù)表單中的Token值與服務(wù)器端的Token值比較,如下所示:
public ActionForward insert(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
if (isTokenValid(request, true)) {
// 表單不是重復(fù)提交
//這里是保存數(shù)據(jù)的代碼
} else {
//表單重復(fù)提交
saveToken(request);
//其它的處理代碼
}
}
其實(shí)使用起來(lái)很簡(jiǎn)單,舉個(gè)最簡(jiǎn)單、最需要使用這個(gè)的例子:
一般控制重復(fù)提交主要是用在對(duì)數(shù)據(jù)庫(kù)操作的控制上,比如插入、更新、刪除等,由于更新、刪除一般都是通過(guò)id來(lái)操作(例如:updateXXXById, removeXXXById),所以這類(lèi)操作控制的意義不是很大(不排除個(gè)別現(xiàn)象),重復(fù)提交的控制也就主要是在插入時(shí)的控制了。
先說(shuō)一下,我們目前所做項(xiàng)目的情況:
目前的項(xiàng)目是用Struts+Spring+I(xiàn)batis,頁(yè)面用jstl,Struts復(fù)雜View層,Spring在Service層提供事務(wù)控制,Ibatis是用來(lái)代替JDBC,所有頁(yè)面的訪問(wèn)都不是直接訪問(wèn)jsp,而是訪問(wèn)Structs的Action,再由Action來(lái)Forward到一個(gè)Jsp,所有針對(duì)數(shù)據(jù)庫(kù)的操作,比如取數(shù)據(jù)或修改數(shù)據(jù),都是在Action里面完成,所有的Action一般都繼承BaseDispatchAction,這個(gè)是自己建立的類(lèi),目的是為所有的Action做一些統(tǒng)一的控制,在Struts層,對(duì)于一個(gè)功能,我們一般分為兩個(gè)Action,一個(gè)Action里的功能是不需要調(diào)用Struts的驗(yàn)證功能的(常見(jiàn)的方法名稱(chēng)有add,edit,remove,view,list),另一個(gè)是需要調(diào)用Struts的驗(yàn)證功能的(常見(jiàn)的方法名稱(chēng)有insert,update)。
就拿論壇發(fā)貼來(lái)說(shuō)吧,論壇發(fā)貼首先需要跳轉(zhuǎn)到一個(gè)頁(yè)面,你可以填寫(xiě)帖子的主題和內(nèi)容,填寫(xiě)完后,單擊“提交”,貼子就發(fā)表了,所以這里經(jīng)過(guò)兩個(gè)步驟:
1、轉(zhuǎn)到一個(gè)新增的頁(yè)面,在Action里我們一般稱(chēng)為add,例如:
public ActionForward add(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
//這一句是輸出調(diào)試信息,表示代碼執(zhí)行到這一段了
log.debug(":: action - subject add");
//your code here
//這里保存Token值
saveToken(request);
//跳轉(zhuǎn)到add頁(yè)面,在Structs-config.xml里面定義,例如,跳轉(zhuǎn)到subjectAdd.jsp
return mapping.findForward("add");
}
2、在填寫(xiě)標(biāo)題和內(nèi)容后,選擇 提交 ,會(huì)提交到insert方法,在insert方法里判斷,是否重復(fù)提交了。
public ActionForward insert(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response){
if (isTokenValid(request, true)) {
// 表單不是重復(fù)提交
//這里是保存數(shù)據(jù)的代碼
} else {
//表單重復(fù)提交
saveToken(request);
//其它的處理代碼
}
}
下面更詳細(xì)一點(diǎn)(注意,下面所有的代碼使用全角括號(hào)):
1、你想發(fā)貼時(shí),點(diǎn)擊“我要發(fā)貼”鏈接的代碼可以里這樣的:
〈html:link action="subject.do?method=add"〉我要發(fā)貼〈/html:link〉
subject.do 和 method 這些在struct-config.xml如何定義我就不說(shuō)了,點(diǎn)擊鏈接后,會(huì)執(zhí)行subject.do的add方法,代碼如上面說(shuō)的,跳轉(zhuǎn)到subjectAdd.jsp頁(yè)面。頁(yè)面的代碼大概如下:
〈html:form action="subjectForm.do?method=insert"〉
〈html:text property="title" /〉
〈html:textarea property="content" /〉
〈html:submit property="發(fā)表" /〉
〈html:reset property="重填" /〉
〈html:form〉
如果你在add方法里加了“saveToken(request);”這一句,那在subjectAdd.jsp生成的頁(yè)面上,會(huì)多一個(gè)隱藏字段,類(lèi)似于這樣〈input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="6aa35341f25184fd996c4c918255c3ae"〉,
2、點(diǎn)擊發(fā)表后,表單提交到subjectForm.do里的insert方法后,你在insert方法里要將表單的數(shù)據(jù)插入到數(shù)據(jù)庫(kù)中,如果沒(méi)有進(jìn)行重復(fù)提交的控制,那么每點(diǎn)擊一次瀏覽器的刷新按鈕,都會(huì)在數(shù)據(jù)庫(kù)中插入一條相同的記錄,增加下面的代碼,你就可以控制用戶的重復(fù)提交了。
if (isTokenValid(request, true)) {
// 表單不是重復(fù)提交
//這里是保存數(shù)據(jù)的代碼
} else {
//表單重復(fù)提交
saveToken(request);
//其它的處理代碼
}
注意,你必須在add方法里使用了saveToken(request),你才能在insert里判斷,否則,你每次保存操作都是重復(fù)提交。
記住一點(diǎn),Struts在你每次訪問(wèn)Action的時(shí)候,都會(huì)產(chǎn)生一個(gè)令牌,保存在你的Session里面,如果你在Action里的函數(shù)里面,使用了saveToken(request);,那么這個(gè)令牌也會(huì)保存在這個(gè)Action所Forward到的jsp所生成的靜態(tài)頁(yè)面里。
如果你在你Action的方法里使用了isTokenValid,那么Struts會(huì)將你從你的request里面去獲取這個(gè)令牌值,然后和Session里的令牌值做比較,如果兩者相等,就不是重復(fù)提交,如果不相等,就是重復(fù)提交了。
由于我們項(xiàng)目的所有Action都是繼承自BaseDispatchAction這個(gè)類(lèi),所以我們基本上都是在這個(gè)類(lèi)里面做了表單重復(fù)提交的控制,默認(rèn)是控制add方法和insert方法,如果需要控制其它的方法,就自己手動(dòng)寫(xiě)上面這些代碼,否則是不需要手寫(xiě)的,控制的代碼如下:
public abstract class BaseDispatchAction extends BaseAction {
protected ActionForward perform(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
String parameter = mapping.getParameter();
String name = request.getParameter(parameter);
if (null == name) { //如果沒(méi)有指定 method ,則默認(rèn)為 list
name = "list";
}
if ("add".equals(name)) {
if ("add".equals(name)) {
saveToken(request);
}
} else if ("insert".equals(name)) {
if (!isTokenValid(request, true)) {
resetToken(request);
saveError(request, new ActionMessage("error.repeatSubmit"));
log.error("重復(fù)提交!");
return mapping.findForward("error");
}
}
return dispatchMethod2(mapping, form, request, response, name);
}
}
SELECT COUNT(*) AS Expr1 FROM History
DELETE FROM History WHERE (Id > 0)
truncate table youtable;
為什么要用TRUNCATE TABLE 語(yǔ)句代替DELETE語(yǔ)句?當(dāng)你使用TRUNCATE TABLE語(yǔ)句時(shí),記錄的刪除是不作記錄的。也就是說(shuō),這意味著TRUNCATE TABLE 要比DELETE快得多。
我今年39歲了,25歲研究生畢業(yè),工作14年,回頭看看,應(yīng)該說(shuō)走了不少的彎路,有一些經(jīng)驗(yàn)和教訓(xùn)。現(xiàn)在開(kāi)一個(gè)小公司,賺的錢(qián)剛夠養(yǎng)家糊口的。看看這些剛畢業(yè)的學(xué)生,對(duì)前景也很迷茫,想拋磚引玉,談?wù)勛约旱目捶ǎ窒抻诶砉た频膶W(xué)生,我對(duì)文科的不懂,身邊的朋友也沒(méi)有這一類(lèi)型的。
91年研究生畢業(yè),那時(shí)出路就是1種:留在北京的國(guó)營(yíng)單位,搞一個(gè)北京戶口,這是最好的選擇。到后來(lái)的2~3年內(nèi),戶口落定了,又分成4條出路:
1、 上國(guó)內(nèi)的大企業(yè),如:華為
2、 自己做公司,做產(chǎn)品開(kāi)發(fā);
3、 上外企,比如:愛(ài)立信、諾基亞
4、 自己做公司,做買(mǎi)賣(mài);
5、 移民加拿大
我想,首先要看自己適合做什么?做技術(shù)還是做買(mǎi)賣(mài)。
做技術(shù),需要你對(duì)技術(shù)感興趣。我掰著數(shù)了一遍,我們研究生班的30來(lái)號(hào)人,實(shí)際上,適合做技術(shù)的,大概只有3、4個(gè)人,這幾個(gè)人,1個(gè)現(xiàn)在還在華為,3個(gè)移民加拿大了,現(xiàn)在這4個(gè)人混的還可以,在華為的同學(xué)也移民加拿大了,他在華為呆了6年,在華為獎(jiǎng)金工資加起來(lái)大概30萬(wàn)吧,還有華為的股票,再過(guò)幾年,華為的股票一上市,也能值個(gè)100~200萬(wàn)。要是一畢業(yè)就去華為,那現(xiàn)在就絕對(duì)不是這個(gè)數(shù)字了。
要是做技術(shù),最好的就是上大公司,國(guó)內(nèi)的大型企業(yè),象華為中興肯定是首選,能學(xué)到很多東西。華為雖然累,但是,年輕人不能怕累,要是到老了,還需要去打拼,那才是真的累啊。
在外企,我想他們主要就是技術(shù)支持和銷(xiāo)售,但是技術(shù)是學(xué)不到的,當(dāng)然不能一概而論,我指的是象愛(ài)立信和諾基亞,真正的研發(fā)不會(huì)在中國(guó)做的,學(xué)到的也不如在華為多,其它的中興我不是很了解,我想應(yīng)該也不錯(cuò)啊。
一個(gè)人都有一技之長(zhǎng),有傍身之技,那是最好的,走到哪里,都能有一口飯吃,還吃的不錯(cuò),這是傳統(tǒng)的觀點(diǎn)。
任何技術(shù)都是要在某個(gè)行業(yè)去應(yīng)用,這個(gè)行業(yè)市場(chǎng)越大當(dāng)然越好;要在一個(gè)領(lǐng)域之內(nèi),做深做精,成為絕對(duì)的專(zhuān)家,這是走技術(shù)道路的人的選擇。不要跳來(lái)跳去,在中國(guó),再小的行業(yè)你要做精深了,都可以產(chǎn)生很大的利潤(rùn)。
研究生剛畢業(yè)的時(shí)候,做產(chǎn)品開(kāi)發(fā)的有不少人,都是自己拍拍腦子,覺(jué)得這個(gè)產(chǎn)品有市場(chǎng),就自己出來(lái)做。現(xiàn)在看來(lái),我的這些同學(xué),做產(chǎn)品開(kāi)發(fā)的成功的沒(méi)有一例,為什么?資源不足。
1. 資金,剛畢業(yè)的學(xué)生啊,就是沒(méi)錢(qián);沒(méi)錢(qián),也意味著你開(kāi)發(fā)的東西都是小產(chǎn)品;而且只能哥幾個(gè)自己上,研發(fā)、生產(chǎn)、銷(xiāo)售都是一個(gè)人或者幾個(gè)人自己來(lái),沒(méi)有積累,什么都是重新來(lái)過(guò)。
2. 人脈,任何一個(gè)行業(yè),要想進(jìn)去,需要有很深的人脈,否則,誰(shuí)會(huì)用你的東西啊?誰(shuí)敢用你的東西啊?
我看到的,我這個(gè)班上開(kāi)發(fā)產(chǎn)品的,自己還在堅(jiān)持的,只剩下一個(gè)人了,說(shuō)實(shí)在的,到現(xiàn)在,沒(méi)有自己的汽車(chē),也沒(méi)有自己的房子,混的挺慘的。現(xiàn)在出國(guó)的不說(shuō)了,在外企、在華為,至少都是幾十萬(wàn)的年薪了,還有各種福利,就是產(chǎn)品開(kāi)發(fā)成功了,又能如何?也就是這樣了,但是以前那些年,都沒(méi)有金錢(qián)的積累,等于白干。
我身邊的一個(gè)自動(dòng)化系的研究生班的同學(xué),能靠自己開(kāi)發(fā)產(chǎn)品活得還可以的,也只有2個(gè)人。說(shuō)明這條路不是那么好走的啊。
其次就是上外企。我的2個(gè)同學(xué),一個(gè)上了愛(ài)立信,一個(gè)先到愛(ài)立信后到諾基亞,都混的不錯(cuò)。到諾基亞的后來(lái)利用在諾基亞結(jié)識(shí)的人脈(就是哪些電信的頭頭腦腦),自己開(kāi)了公司,也賺了不少的錢(qián)。
外企最大的好處就是除了能學(xué)到比較規(guī)范的管理外,還能給你的職業(yè)生涯鍍金。到了一個(gè)外企外,再到同行業(yè)的外企我想就很容易了。而且外企的收入高啊。
自己做公司,做買(mǎi)賣(mài),一開(kāi)始有3~4個(gè)人走這條路,但是真正發(fā)財(cái)?shù)闹挥幸粋€(gè)人,其他人后來(lái)上外企了。做買(mǎi)賣(mài),還是要有一定的天賦,還有機(jī)遇。要有對(duì)金錢(qián)的赤裸裸的欲望,要有商業(yè)上的頭腦。后來(lái)我們同學(xué)在一起談,說(shuō),我們即使給自己這個(gè)機(jī)遇,也未必能做的好。何況當(dāng)時(shí)那個(gè)同學(xué)看好的產(chǎn)品(做一個(gè)臺(tái)灣產(chǎn)品的代理),我們大家都沒(méi)有看好,說(shuō)明,真理還是掌握在少數(shù)人手里。
到后來(lái),同學(xué)們紛紛移民移民加拿大。
移民加拿大對(duì)搞技術(shù)的人來(lái)說(shuō),還是一個(gè)不錯(cuò)的選擇,但是要盡早,練了幾年的技術(shù),就趕緊出去,大概是在1996年走了不到10個(gè),現(xiàn)在都還可以,買(mǎi)了房子和車(chē)了。要是晚了,語(yǔ)言再學(xué)也難了,而且在國(guó)內(nèi)都混的還可以了,也就沒(méi)有必要出去了。
我自己呢,先是在國(guó)營(yíng)的研究所混了4年,后來(lái)到一家公司干了6年,2002年出來(lái)自己做公司,現(xiàn)在也就是混了一個(gè)溫飽吧,算是有房有車(chē),有點(diǎn)積蓄,但是不多,還有一個(gè)可愛(ài)的女兒。回首這10來(lái)年,有一些經(jīng)驗(yàn)和教訓(xùn)。
1. 要有一個(gè)職業(yè)生涯的規(guī)劃。首先需要定位自己做什么合適,是做買(mǎi)賣(mài)還是做技術(shù),一條路走到黑;當(dāng)然,做了技術(shù),后來(lái)改行也行;
2. 做技術(shù),就是要做精做深,成為這個(gè)行業(yè)的這個(gè)技術(shù)的專(zhuān)家;最好就是去國(guó)內(nèi)的大公司,才能全面學(xué)到東西,能夠給你培訓(xùn)的機(jī)會(huì);如果大公司進(jìn)不去,先到小公司練技術(shù),找機(jī)會(huì)再到大公司去鍍金,學(xué)高深的技術(shù)。千萬(wàn)不要自己做產(chǎn)品,要做也是對(duì)這個(gè)行業(yè)熟悉了,再去做。
3. 積極爭(zhēng)取機(jī)會(huì)。積極爭(zhēng)取學(xué)習(xí)和進(jìn)步的機(jī)會(huì)。比如,做技術(shù),就需要多鍛煉,多學(xué)習(xí),來(lái)提高自己的水平。一門(mén)技術(shù),只要有機(jī)會(huì)去學(xué)習(xí),都能學(xué)的會(huì);要是沒(méi)有機(jī)會(huì),天才也沒(méi)有辦法學(xué)到這個(gè)技術(shù)。柳傳志就說(shuō),楊元慶就是“哭著喊著要進(jìn)步”,實(shí)際上,就是爭(zhēng)取自己的機(jī)會(huì);當(dāng)然,這種強(qiáng)烈的進(jìn)步欲望,也是領(lǐng)導(dǎo)看重的地方。每一步都走在前面,積累10年,你就有了比其他人更多的機(jī)會(huì)了。
4. 積累個(gè)人的信譽(yù)。從你的職業(yè)生涯的第一天,就要按照誠(chéng)信的原則辦事。要做到,當(dāng)人們提起你的名字的時(shí)候,說(shuō),這哥們還不錯(cuò),做事還行。
5. 注意利用資源。如果你有有錢(qián)的親戚、成功的長(zhǎng)輩或者朋友,可以充分利用這些機(jī)會(huì),得到更加順利的發(fā)展前景。
6. 注意財(cái)富的不斷積累。人生要想得到自由,財(cái)富是很關(guān)鍵的。否則,永遠(yuǎn)仰人鼻息,永遠(yuǎn)看人臉色。人都是勢(shì)利眼。今后的家庭、職業(yè)生涯,金錢(qián)的積累很重要,沒(méi)有錢(qián),永遠(yuǎn)不能開(kāi)張自己的事業(yè),得到更多的機(jī)會(huì);財(cái)富要做到逐年積累,你才能家庭生活幸福。沒(méi)有錢(qián)是不可能有幸福的家庭的。
7. 注意人脈的積累。最終,事業(yè)要靠在社會(huì)上的人脈的資源。要注意認(rèn)識(shí)在你這個(gè)行業(yè)的人,結(jié)交他們,最終他們會(huì)成為你事業(yè)上的助力。
8. 尋求貴人相助。要找大老板來(lái)幫助你,得到大老板的賞識(shí)。想想看,大蛋糕,切一點(diǎn)就夠了,小蛋糕,都給你也吃不飽啊。
9. 多聽(tīng)聽(tīng)成功的前輩和成功的朋友的意見(jiàn)。注意少聽(tīng)家里長(zhǎng)輩的意見(jiàn),尤其是都已經(jīng)退休的長(zhǎng)輩,他們對(duì)社會(huì)的認(rèn)識(shí)還停留在很久以前,而這個(gè)社會(huì)已經(jīng)發(fā)生很大的變化呢。最重要的是,長(zhǎng)輩有時(shí)候會(huì)強(qiáng)求你做一些事情,但是,最終的結(jié)果他們是不負(fù)責(zé)的。只有你才能對(duì)自己負(fù)責(zé)。
滾動(dòng)公告欄的實(shí)現(xiàn)
用到如下html標(biāo)簽:
《MARQUEE id=mar onmouseout=this.start(); direction=up height=150 name="mar"
onmouseover="this.stop();" scrollAmount="1" scrollDelay="0"
style="LINE-HEIGHT: 100%; PADDING-LEFT: 0px; PADDING-RIGHT:0px"width="100%">
1凈化社會(huì)環(huán)境
2也是凈化社會(huì)平步青云
3凈化社會(huì)環(huán)境
4也是凈化社會(huì)平步青云
5凈化社會(huì)環(huán)境
6也是凈化社會(huì)平步青云
7凈化社會(huì)環(huán)境
8也是凈化社會(huì)平步青云
其中marquee中height中的屬性可以更改每次顯示的行數(shù),……
天涼好個(gè)秋
對(duì)我來(lái)說(shuō),如果只是降溫的話,我未必會(huì)覺(jué)得秋來(lái)了,可是昨天早上走在路上,看到葉子在空中飛舞,地上布滿了落葉,秋終于還是來(lái)了,這一年也快要過(guò)去了,我做了什么,我還能做什么。
昨天新裝了個(gè)系統(tǒng),換了個(gè)mac的主題,用著很舒服,不過(guò)裝別的軟件又浪費(fèi)了半天的時(shí)候,終究還是忘了做個(gè)鏡像了,

裝好Sever-u后,測(cè)試什么問(wèn)題都沒(méi)有,后來(lái)從別的機(jī)子那里一訪問(wèn),居然不行,懷疑卡巴的事,這個(gè)殺毒軟件太BT,不懷疑不行啊,關(guān)了以后還是不行,難道是windows防火墻?禁掉果然OK
,那總不能禁掉它吧,暫且不說(shuō)那個(gè)提示很煩,別外要裝防火墻是讓我很不爽的事,一共就這么點(diǎn)內(nèi)存,哪夠用啊

。看看防火墻里面有個(gè)例外選項(xiàng),我把端口20,21,22,全都加上了,結(jié)果只能連上,但是不能建立socket,讓人不爽的是,居然不能范圍添加端口。
然后選例外程序,是在這里,server-u目錄下有三個(gè)程序文件tray.exe有來(lái)系統(tǒng)托盤(pán)監(jiān)控,dameon主程序后臺(tái)運(yùn)行,admini管理,這里我把dameon加上以后,一連接,果然ok,
起先加錯(cuò)了,加成tray.exe和admin,就沒(méi)加dameon,結(jié)果怎么試都不行,還上網(wǎng)查了好長(zhǎng)時(shí)間,真是……
看來(lái)我的英語(yǔ),