GWT 入门介绍
GWT是 Google Web Toolkit的简称。
GWT是一个以Java语言为工具,以类似Swing的方式编写UI组件,之后通过GWT Compiler编译
为JavaScritp和HTML在客户端浏览器中运行的一个开发工具和编程模型。
GWT应用程序有两种方式运行:
Hosted Model
在Hosted Model方式下,Java程序并不会被编译成JavaScript,GWT只是创造了一个类似
浏览器的环境,直接运行使用GWT开发的程序。这种模式最具生产力(这种模式下可以Debug所有的Java代码),
所以开发环境通常使用Hosted Model,但是真正的程序执行不使用这种模式。
Web Model
就是将使用GWT编写的Java程序整整编译成JavaScript,在Web容器上运行,使用浏览器
访问的模式,这种方式只有在测试或者运行的时候才使用,每次对Java类的修改必须重新编译
这些Java类,之后重新部署应用。所以生产力比较低。
安装GWT
从如下位置下载GWT的最新版本:
http://code.google.com/webtoolkit/download.html
当前最新的Release版本为1.4.60, 上一个稳定的版本为1.3.3
第一步,安装JDK, 1.4 以上的版本都可以。
第二步,下载GWT压缩文件,这里使用1.4.59 RC2(gwt-windows-1.4.59.zip)
第三布,将GWT压缩文件解压缩到制定的目录,这里我使用的目录为,C:/程序开发/Java/gwt-windows-1.4.59
第四步,安装成功
安装之后的目录结构为:
C:/程序开发/Java/gwt-windows-1.4.59
doc(文档目录,开发文档和Java API文档)
samples(示例代码目录,非常有名的KitchenSink示例代码即在此目录中)
about.html
about.txt
applicationCreator.cmd
benchmarkViewer.cmd
COPYING
COPYING.html
gwt-benchmark-viewer.jar
gwt-dev-windows.jar
gwt-ll.dll
gwt-module.dtd
gwt-servlet.jar
gwt-user.jar
i18nCreator.cmd
index.html
junitCreator.cmd
projectCreator.cmd
release_notes.html
swt-win32-3235.dll
使用GWT建立开发
GWT的安装目录下有一个名叫Samples的目录,里边有很多的例子,其中KitchenSink比较全面的展现了GWT的Web组件。
applicationCreator创建一个可以以Hosted Mode形式运行的GWT应用程序,
以下命令将建立一个工程
C:/程序开发/Java/gwt-windows-1.4.59>applicationCreator.cmd -eclipse GWTLogon -out GWTLogon com.jpleausre.gwt.logon.client.GWTLogon
Created directory GWTLogon/src
Created directory GWTLogon/src/com/jpleausre/gwt/logon
Created directory GWTLogon/src/com/jpleausre/gwt/logon/client
Created directory GWTLogon/src/com/jpleausre/gwt/logon/public
Created file GWTLogon/src/com/jpleausre/gwt/logon/GWTLogon.gwt.xml
Created file GWTLogon/src/com/jpleausre/gwt/logon/public/GWTLogon.html
Created file GWTLogon/src/com/jpleausre/gwt/logon/client/GWTLogon.java
Created file GWTLogon/GWTLogon.launch
Created file GWTLogon/GWTLogon-shell.cmd
Created file GWTLogon/GWTLogon-compile.cmd
运行GWTLogon-shell.cmd可以看到GWT启动的服务端的Google Web Toolkit Development Shell,如下图:
和如下的Host Mode的浏览器:
点击其中的 Click Me 按钮,可以看到输出的Hello World!。
projectCreator建立一个基于ant构建的,或者基于eclipse的GWT开发工程,
例如:
C:/程序开发/Java/gwt-windows-1.4.59>projectCreator.cmd -ant GWTLogon -eclipse GWTLogon -out GWTLogon
Created directory GWTLogon/src
Created directory GWTLogon/test
Created file GWTLogon/GWTLogon.ant.xml
Created file GWTLogon/.project
Created file GWTLogon/.classpath
其中的GWTLogon.ant.xml构建文件的内容为:
<?xml version="1.0" encoding="utf-8" ?>
<project name="GWTLogon" default="compile" basedir=".">
<description>
GWTLogon build file. This is used to package up your project as a jar,
if you want to distribute it. This isn't needed for normal operation.
</description>
<!-- set classpath -->
<path id="project.class.path">
<pathelement path="${java.class.path}/"/>
<pathelement path="C:/程序开发/Java/gwt-windows-1.4.59/gwt-user.jar"/>
<!-- Additional dependencies (such as junit) go here -->
</path>
<target name="compile" description="Compile src to bin">
<mkdir dir="bin"/>
<javac srcdir="src:test" destdir="bin" includes="**" debug="on" debuglevel="lines,vars,source" source="1.4">
<classpath refid="project.class.path"/>
</javac>
</target>
<target name="package" depends="compile" description="Package up the project as a jar">
<jar destfile="GWTLogon.jar">
<fileset dir="bin">
<include name="**/*.class"/>
</fileset>
<!-- Get everything; source, modules, html files -->
<fileset dir="src">
<include name="**"/>
</fileset>
<fileset dir="test">
<include name="**"/>
</fileset>
</jar>
</target>
<target name="clean">
<!-- Delete the bin directory tree -->
<delete file="GWTLogon.jar"/>
<delete>
<fileset dir="bin" includes="**/*.class"/>
</delete>
</target>
<target name="all" depends="package"/>
</project>
applicationCreator和projectCreator的区别是
applicationCreator创建了src目录和Demo代码,projectCreator不创建Demo代码,但是创建src目录和test目录。
applicationCreator创建了启动脚本GWTLogon-shell.cmd和GWT编译脚本GWTLogon-compile.cmd,而projectCreator不创建。
applicationCreator创建了eclipse launch文件,而projectCreator创建.classpath和.project文件。
通常情况下,我们先使用projectCreator创建按project,projectCreator创建的工程可以轻松的import到eclipse中,
之后我们使用applicationCreator创建需要的Java示例代码,eclipse launch文件,启动脚本和编译脚本。
使用Eclipse导入的GWT工程如下:
命令列表
projectCreator
生成基本项目框架,可以选择使用eclipse或者使用ant构建文件
applicationCreator
生成代码示例和一个可运行的应用程序
junitCreator
生成一个JUnit测试Case
i18nCreator
生成一个i18n属性文件和对应的Javascript脚本。
benchmarkViewer
显示benchmark结果
功能介绍(通用)
GWT体系结构
GWT Java-to-JavaScript Compiler
将Java程序翻译为JavaScript,通过GWT Compiler可以让GWT程序在Web 模式下运行
GWT Hosted Web Browser
GWT Hosted Web Brower让你的程序可以在Hosted模式下运行,在Hosted模式下运行的是Java代码而不是编译出来的JavaScript代码,在Hosted模式下可以轻松的做Debug。
JRE emulation library
可以称为JRE简化的类库。在客户端(用来编译成JavaScript代码的客户端Java代码)不是所有的java类库都被gwt支持,只有部分被支持,这些类库是几乎所有的java.lang包,java.util包的一部分。
GWT Web UI class library
使用GWT Web UI类库可以创建web 浏览器的组件,例如按钮,文本框,图片等。这是GWT的核心UI类库。
功能介绍(Web控件)
Web控件是GWT表示层的核心,通过使用GWT提供的Web控件可以创建丰富的客户端画面。
GWT的web组件主要分为两类:输入控件和Layout控件。
输入控件主要是指向服务器提交数据,处理操作,显示服务器数据的空间,主要包括:
Button,RadionButton,PushButton,ToggleButton,CheckBox,TextBox,PasswordBox
TextArea, HyperLink,ListBox,MenuBar,Tree,Table, TabBar,DialogBox, PopupPanel
RichTextArea, DisclosurePanel, SuggestBox
Layout 空间主要用来有规律地放置输入控件,主要包括:
Stackpanel,HorizontalPanel,VerticalPanel, FlowPanel, VerticalSplitPanel, HorizontalSplitPanel,
DockPanel, TabPanel,
事件模型(Event & Listener)
如何处理页面的事件呢,例如点击按钮,Textbox失去焦点等?
如下的代码建立了一个按钮,按钮的名字是“Click Me”,当你点击这个按钮的时候处罚一个Click事件,Click事件触发onClick(Widget sender)方法。
sender表示触发onClick方法的组件,这里只是简单的在页面上显示Hello World信息(相当于javascript的alert方法)。
Button b = new Button("Click Me");
b.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
Window.alert("Hello World");
}
});
常见的Listener如下:
ChangeListener
ClickListener
FocusListener
KeyboardListener
MouseListener
MouseWheelListener
PopupListener
ScrollListener
TableListener
TreeListener
功能介绍(远过程调用RPC)
体系结构
GWT应用中页面一旦加载,就再也不会向服务器请求HTML内容,所有的画面迁移,转换都在客户端进行,但是数据还是会向服务器提交,或者从服务器获取。
服务器上负责处理数据的对象在GWT中叫做Service,每个Service有三个类组成:服务方法定义接口(Service),异步调用接口(ServiceAsync)和服务器方法实现类ServiceImpl。
以Login为例子说明:
// 服务方法定义接口
public interface LoginService extends RemoteService {
public boolean login(LoginSO login) throws ApplicationException;
}
// 异步调用接口
public interface LoginServiceAsync {
void login(LoginSO login, AsyncCallback async);
}
// 服务器方法实现类
public class LoginServiceImpl extends RemoteServiceServlet implements LoginService {
public boolean login(LoginSO login) throws ApplicationException {
...
return true;
}
}
其中前两个接口在client包内部,最后一个实现在server包内部。
客户端调用一个服务类的方法的代码如下:
LoginServiceAsync ourInstance = (LoginServiceAsync) GWT.create(LoginService.class); //
((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "/LoginService"); //
ourInstance.login(loginSO, new AsyncCallback() { //
public void onFailure(Throwable caught) { //
if(caught instanceof InvocationException) {
// system exception
} else {
Window.alert(" " + GWTShowConstants.Messages.constants.maxQueryCount());
// aplication exception
}
}
public void onSuccess(Object result) { //
Window.alert("success");
}
});
//
远程调用
获得服务器方法的调用接口(skeleton)。
设置服务位置。
远程调用服务器上的方法,注意这里是异步调用,在和调用之前代码可能先被调用了。
调用出错,或者调用方法抛出异常的时候调用的方法。
调用成功返回时候调用的方法。
参数和返回值系列化类型
这里的参数指的是Service方法调用的参数和返回值。
由于GWT的客户端代码都是JavaScript,而服务器代码都是使用Java编写的,这就涉及到JavaScript调用Java方法的时候
如何传递参数,如何取得返回值的问题。
可序列化的类型包括:
(1) 原始类型,例如:char, byte, short, int, long, boolean, float, double;
(2) String,java.util.Date,或者原始类型的包装类型,例如: Character, Byte, Short, Integer, Long, Boolean, Float, or Double;
(3) 可序列化类型数组(包含(4)和(5)定义的类型)
(4) 用户定义的可序列化类型
(5) 该类型至少有一个可序列化的子类型
针对上述(4)中说明的,什么是用户自定义的可序列化类型呢?必须满足以下亮点:
第一,必须直接或者间接(例如,父类型实现了这个接口)的实现了IsSerializable接口
第二,所有非transient类型都是可序列化的(final类型的属性在GWT中被视为transient类型)
是否支持容器类型呢?那么又如何声明呢?
支持容器类型,GWT可以使用Type 参数来表示容器类型内部的元素的类型,例如:
注意GWT暂时不支持使用 JDK 5.0 的模板容器
//用户自定义序列化类型
public class MyClass implements IsSerializable {
/**
* 这个Set中的元素的类型必须都是String类型
*
* @gwt.typeArgs <java.lang.String>
*/
public Set setOfStrings;
/**
* Map中的元素的Key和Value的类型都是String类型。
*
* @gwt.typeArgs <java.lang.String,java.lang.String>
*/
public Map mapOfStringToString;
}
// 服务器方法实现类
public interface MyService extends RemoteService {
/**
* 第一个类型参数表示方法的参数c是一个List,并且其中只能放置Integer类型。
* 第二个类型参数表示返回值为List,并且其中的原书的类型为String类型。
*
* @gwt.typeArgs c <java.lang.Integer>
* @gwt.typeArgs <java.lang.String>
*/
List reverseListAndConvertToStrings(List c);
}
异常
在调用方法的时候异常怎么处理呢?
调用方法的过程中的异常可以分为两类:第一类,调用方法的过程中出现了异常,例如网络故障,服务类不存在等。
第二类,服务器方法抛出了异常。
在客户端调用的过程中这两种异常都在onFailure(Throwable caught)方法中处理,但是caught的类型有所区别,
第一种情况下,caught为InvocationException的子类,第二种情况下caught为用户自定义的异常。
由于异常也需要在客户端(JavaScript)和服务器端(Java)传递,所以Exception的定义也要满足可序列化的要求。
但是在GWT中已经定义了一个基本的异常类型来提一个Exception类的基础类,这个类是SerializableException,例如:
public class ApplicationException extends SerializableException {
public ApplicationException() {
super();
}
public ApplicationException(String msg) {
super(msg);
}
public Throwable getCause() {
return super.getCause();
}
public String getMessage() {
return super.getMessage();
}
public Throwable initCause(Throwable cause) {
return super.initCause(cause);
}
}
异常消息内容建议,服务器端的错误消息内容在服务器保存,客户端的错误消息内容在客户端保存(参看后续的国际化部分),两个地方
都需要的,出于GWT技术建议使用两份,分别放在客户端和服务器端。
为什么不能重用呢?
客户端的消息会被编译为JavaScript,所以服务器端通常无法使用。(还有另外的原因,参看后续的国际化部分)。
功能介绍(集成JUnit)
@TODO
功能介绍(国际化)
项目开发过程中经常需要一些可配置的常量,例如查询最大条数,目录位置等。在传统的Java应用程序中这些内容通常会放在
属性文件中(Properties文件),但是使用属性文件有些弊端,第一,不支持类型,所有的内容都是String,第二是,只有在具体使用
的时候才能发现有些属性没有定义,而不能在编译的时候发现。
那么GWT如何处理这个问题呢?GWT中有一个特殊的接口com.google.gwt.i18n.client.Constants可以使用这个接口达到
定义常量的效果,并且这些常量在编译的时候被绑定,而且可以支持类型。
使用GWT主要有以下几步:
第一步,建立一个集成于Constants的接口,例如:
public interface NumberFormatConstants extends Constants {
/**
* @return the localized decimal separator
*/
String decimalSeparator();
/**
* @return the localized thousands separator
*/
String thousandsSeparator();
}
第二步,根据接口中定义的方法定义一个跟接口同名的属性文件,例如:
#NumberFormatConstants.properties
decimalSeparator = ,
thousandsSeparator = .
第三步,获取文件中定义的内容,例如:
public void useNumberFormatConstants() {
NumberFormatConstants constants = (NumberFormatConstants) GWT.create(NumberFormatConstants.class);
String decimalSep = constants.decimalSeparator();
String thousandsSep = constants.thousandsSeparator();
String msg = "Decimals are separated using '" + decimalSep + "'";
msg += ", and thousands are separated using '" + thousandsSep + "'";
showMessage(msg);
}
上述三步中在第二步和第三步中间隐含了伊特特殊的步骤,就是GWT编译器结合接口文件和属性文件编译出了一个
类,这个类实现了这个接口,每一个方法返回属性文件中的值。
其中GWT.create()方法可以获得生成的中间类的引用。
通常情况下,接口方法明和属性文件中的名字相同,例如:
String decimalSeparator(); 和 thousandsSeparator = .
但是也可以自定义接口方法和属性文件中内容的映射,例如:
public interface NumberFormatConstantsWithAltKey extends Constants {
/**
* @gwt.key fmt.sep.decimal
* @return the localized decimal separator
*/
String decimalSeparator();
/**
* @gwt.key fmt.sep.decimal
* @return the localized thousands separator
*/
String thousandsSeparator();
}
@gwt.key fmt.sep.decimal 定义了属性文件中key的内容,所以属性文件应该为:
#NumberFormatConstants.properties
fmt.sep.decimal = .
fmt.sep.thousands = ,
Constants子接口中定义的方法必须满足如下形式:
T methodName()
这里T是一个返回值,T可以使用如下表中的所有类型:
T类型 属性文件定义
String 简单的字符串
String[] 使用逗号分割的字符串,如果某个字符串中包含逗号需要使用//作为转移字符,例如:'//,'
int int值,在编译的时候做类型检查
float float值,在编译的时候做类型检查
double double值,在编译的时候做类型检查
boolean boolean值"true" 或者 "false"), 在编译的时候做类型检查
Map 使用逗号分隔的字符产,每一个字符产在属性文件中有一条定义,定义了一个Key-Value值
Map示例:
a = X
b = Y
c = Z
someMap = a, b, c
Map someMap();方法得到的内容为:{a:X, b:Y, c:Z}
ConstantsWithLookup
ConstantsWithLookup是Constants的子接口,用法一样,只不过ConstantsWithLookup有一组通过属性名字获取属性值的方法:
getBoolean(String) 通过名字找到boolean型内容
getDouble(String) 通过名字找到double型内容
getFloat(String) 通过名字找到float型内容
getInt(String) 通过名字找到int型内容
getMap(String) 通过名字找到Map型内容
getString(String) 通过名字找到String型内容
getStringArray(String) 通过名字找到String[]型内容
效率问题:Constants效率比ConstantsWithLookup高,为什么呢?Constants在编译的时候会生成对应的JavaScript代码,
GWT Compiler会根据程序中是否使用了某些属性来决定这些内容是否会被编译为JavaScript,所以及时在Constants中声明
了某些方法,如果在代码中不使用的话,不会被编译为JavaScript代码的。
但是ConstantsWithLookup有根据属性名字查找属性内容的方法,所以,GWT Compiler不能根据上述方法确定属性是否被使用,
所以所有的属性内容都回被编译为JavaScript代码。
这是ConstantsWithLookup的优点,也是缺点!
Message类
在使用Constants(或者ConstantsWithLookup)的时候,我们只能使用预定义的消息,有些时候我们需要可变的消息。
例如:
我们需要一个通用的消息再加上一个功能名字的参数怎么实现呢?
Message类相当于Java中的Properties,ResourceBundle和MessageFormat的联合体,例如:
消息文件类:
public interface GameStatusMessages extends Messages {
/**
* @param username the name of a player
* @param numTurns the number of turns remaining
* @return a message specifying the remaining turns for a player
*/
String turnsLeft(String username, int numTurns);
/**
* @param numPoints the number of points
* @return a message describing the current score for the current player
*/
String currentScore(int numPoints);
}
属性文件定义:
turnsLeft = Turns left for player ''{0}'': {1}
currentScore = Current score: {0}
使用:
public void beginNewGameRound(String username) {
GameStatusMessages messages = (GameStatusMessages) GWT.create(GameStatusMessages.class);
// Tell the new player how many turns he or she has left.
int turnsLeft = computeTurnsLeftForPlayer(username);
showMessage(messages.turnsLeft(username, turnsLeft));
// Tell the current player his or her score.
int currentScore = computeScore(username);
setCurrentPlayer(username);
showMessage(messages.currentScore(currentScore));
}
我们可以看到在使用的时候基本一致,但是,可以使用参数配置原有的消息。
另外Message的方法的格式为:
String methodName(optional-params)
从中我们也可以看出区别,Message只能使用String类型的参数。
Constants(或者ConstantsWithLookup)和Message的区别是:
Constants用来定义系统的常量,支持多种类型。
Message用来定义系统的消息,可以支持参数化消息,但是只支持String类型的内容。
在使用Constants和Message的时候,可以将属性文件的编码设置为UTF-8这样,就不用
使用Native2ascii将正常的文件转移为utf-8的替换文件了。
当然如果你觉得不麻烦也可以使用传统的Java属性文件(使用native2ascii处理过得文件)。
功能介绍(JavaScript Native Interface)
JavaScript Native Interface = JSNI
JSNI定义了在GWT环境下,Java与JavaScript交互的一种方法。
虽然GWT的一些核心的方法是用JavaScript编写的,但是这里还是不推荐使用JNI,应为这样做与GWT的初衷相悖,
并且,有一定的难度,开发调试也相对困难。
Java调用JavaScript方法:
JSNI方法定义需要使用native关键字,并且需要在参数列表之后,结尾的分号之前定义。JSNI方法的开始使用/*-{
结尾使用}-*/,例如:
public static native void alert(String msg) /*-{
$wnd.alert(msg);
}-*/;
当上述方法在Java中调用的时候,实际上将会调用Window的alert方法,将传入的内容打印出来。
在Hosted Mode下,断点可以设置在上述方法中,可以方便的查看传入的参数。
JavaScript调用Java方法:
方法调用方式:
[instance-expr.]@class-name::method-name(param-signature)(arguments)
属性访问方式:
[instance-expr.]@class-name::field-name
[instance-expr.]
用来区分实例方法调用还是静态方法调用。在调用实例方法的时候必须出现,在调用静态方法的时候不能出现。
class-name
类的名字。
method-name
方法的名字
param-signature
方法的参数列表,这里使用的是内部形式(参考Java虚拟机Class格式),但是不需要写返回值类型。
arguments
调用方法的实际参数。
例如:
public class JSNIExample {
String myInstanceField;
static int myStaticField;
void instanceFoo(String s) {
// use s
}
static void staticFoo(String s) {
// use s
}
// 该方法被调用的时候将在JavaScript中执行,并且
// 可以使用JavaScript中的内容。
public native void bar(JSNIExample x, String s) /*-{
// 调用这个实例本身的instanceFoo方法
this.@com.google.gwt.examples.JSNIExample::instanceFoo(Ljava/lang/String;)(s);
// 调用x实例(输入参数)上的instanceFoo实例方法
x.@com.google.gwt.examples.JSNIExample::instanceFoo(Ljava/lang/String;)(s);
// 调用静态方法 staticFoo()
@com.google.gwt.examples.JSNIExample::staticFoo(Ljava/lang/String;)(s);
// 读取这个实例的变量
var val = this.@com.google.gwt.examples.JSNIExample::myInstanceField;
// 设置x上的实例变量
x.@com.google.gwt.examples.JSNIExample::myInstanceField = val + " and stuff";
// Read static field (no qualifier)
@com.google.gwt.examples.JSNIExample::myStaticField = val + " and stuff";
}-*/;
}
Java和JavaScript之间参数的传递:
Java -> JavaScript
Java type JavaScript Type
numeric primitive a JavaScript numeric value, as in var x = 42;
String a JavaScript string, as in var s = "my string";
boolean a JavaScript boolean value, as in var b = true;
JavaScriptObject (see notes) a JavaScriptObject that must have originated from JavaScript code, typically as the return value of some other JSNI method
Java array an opaque value that can only be passed back into Java code
any other Java Object an opaque value accessible through special syntax
异常
调用JSNI方法的时候会抛出一个JavaScriptException的异常,但是由于JavaScript不是一个强类型的语言,所以
无法想Java一样处理JavaScript异常。一个好的方式是在Java中处理Java异常,在JavaScript中处理JavaScript异常。
另外在JSNI方法,Java普通方法混掉的过程中,异常可以从最底层移植抛到最想的调用层,例如:
1. Java method foo() calls JSNI method bar()
2. JavaScript method bar() calls Java method baz()
3. Java method baz() throws an exception
baz()中抛出的异常可以蔓延到bar方法,可以在foo方法中捕获。
从Host Model到 Web Model
在Host Model方式下,GWT并不将Java代码编译为JavaScript,而是在GWT环境中直接运行Java bytecode,
但是项目正式部署之后使用的是Web Model,那么如何从Host Model迁移到Web Model呢?
首先需要将Java代码编译为JavaScript代码。
使用如下命令可以将Java代码编译为JavaScript代码:
java -cp "%~dp0/src;%~dp0/bin;%~dp0/../../gwt-user.jar;%~dp0/../../gwt-dev-windows.jar" com.google.gwt.dev.GWTCompiler -out "%~dp0/www" %* com.google.gwt.sample.hello.Hello
-cp 指定源代码目录,Class目录,和GWT的jar文件的路径
-out 指定JavaScript代码的输出路径
com.google.gwt.sample.hello.Hello 指定编译的Module,一般是gwt.xml文件中entry-point类去掉client之后的内容。
当代码量比较大的时候,需要指定Java使用内存的大小,否则会内存溢出。
java -Xmx512m -Xms128m -cp "%~dp0/src;%~dp0/bin;%~dp0/../../gwt-user.jar;%~dp0/../../gwt-dev-windows.jar" com.google.gwt.dev.GWTCompiler -out "%~dp0/www" %* com.google.gwt.sample.hello.Hello
之后将编译成的JavaScript代码拷贝到Web项目的根目录中,与WEB-INF相同层次的目录。
最后需要将gwt.xml文件中定义的service编程对应的Servlet。
<servlet path='/calendar' class='com.google.gwt.sample.dynatable.server.SchoolCalendarServiceImpl'/>
=>
<servlet>
<servlet-name>Calendar</servlet-name>
<servlet-class>com.google.gwt.sample.dynatable.server.SchoolCalendarServiceImpl</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Calendar</servlet-name>
<url-pattern>/calendar</url-pattern>
</servlet-mapping>
使用数据源
Hosted Mode 虽然开发起来很方便,但是也有缺点,例如,数据源的配置就有问题。
在GWT Hosted Mode下无法配置数据源,一种可选的方式是使用一个假的数据库链接
管理类,这个类的接口返回Connection,内部以DriverManager的方式实现,等待
后续部署之后再切换到数据源模式。
日志处理(Log4J)
回想GWT应用程序,client包内部的代码将会被编译为客户端JavaScript代码,所以这里
不需要记录日志,也不可能使用Log4j。
但是Server包内的内容在服务器上运行,需要合理的使用日志。
一个简单的Login示例
代码结构如下:
└─src
└─com
└─jpleasure
└─gwt
└─logon
│ LogonDemo.gwt.xml GWT配置模块文件
│
├─client 客户端代码包
│ │ LogonDemo.java GWT代码的入口点
│ │ LogonDemoController.java 画面迁移控制类
│ │
│ ├─exception 异常定义包
│ │ ApplicationException.java 应用程序异常
│ │
│ ├─panel 页面Panel包
│ │ BasePanel.java 基类Panel
│ │ LogonPanel.java Logon Panel
│ │ WelcomePanel.java Welcome Panel
│ │
│ ├─service 客户端服务定义包
│ │ LogonService.java 服务接口
│ │ LogonServiceAsync.java 服务异步调用接口
│ │
│ └─so Serializable Object 包
│ LogonSO.java Logon SO
│
├─public GWT HTML包
│ LogonDemo.css CSS定义
│ LogonDemo.html 主HTML页面
│
└─server 服务端Service包
└─service
LogonServiceImpl.java Logon Service
// ApplicationException
package com.jpleasure.gwt.logon.client.exception;
import com.google.gwt.user.client.rpc.SerializableException;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:16:17
* To change this template use File | Settings | File Templates.
*/
public class ApplicationException extends SerializableException {
public ApplicationException() {
super();
}
public ApplicationException(String msg) {
super(msg);
}
}
//BasePanel
package com.jpleasure.gwt.logon.client.panel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.jpleasure.gwt.logon.client.LogonDemoController;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:01:14
* To change this template use File | Settings | File Templates.
*/
public class BasePanel extends VerticalPanel {
protected LogonDemoController ldc;
public LogonDemoController getLdc() {
return ldc;
}
public void setLdc(LogonDemoController ldc) {
this.ldc = ldc;
}
}
// LogonPanel
package com.jpleasure.gwt.logon.client.panel;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import com.jpleasure.gwt.logon.client.service.LogonService;
import com.jpleasure.gwt.logon.client.service.LogonServiceAsync;
import com.jpleasure.gwt.logon.client.so.LogonSO;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 21:35:13
* To change this template use File | Settings | File Templates.
*/
public class LogonPanel extends BasePanel {
private TextBox name;
private PasswordTextBox password;
private Button logonButton;
public LogonPanel() {
HorizontalPanel msgPanel = new HorizontalPanel();
msgPanel.add(new Label("Please input logon information here!"));
HorizontalPanel namePanel = new HorizontalPanel();
Label nameLabel = new Label("Name:");
nameLabel.setPixelSize(100, 20);
namePanel.add(nameLabel);
name = new TextBox();
namePanel.add(name);
HorizontalPanel pwdPanel = new HorizontalPanel();
Label passwordLabel = new Label("Password:");
passwordLabel.setPixelSize(100, 20);
pwdPanel.add(passwordLabel);
password = new PasswordTextBox();
pwdPanel.add(password);
HorizontalPanel btnPanel = new HorizontalPanel();
logonButton = new Button("Logon");
logonButton.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
LogonServiceAsync logonService = LogonService.App.getInstance();
LogonSO logonSO = new LogonSO();
logonSO.setName(getName());
logonSO.setPassword(getPassword());
logonService.logon(logonSO, new AsyncCallback() {
public void onFailure(Throwable caught) {
Window.alert(caught.getMessage());
}
public void onSuccess(Object result) {
boolean isLogon = ((Boolean)result).booleanValue();
if(isLogon) {
ldc.gotoWelcome();
} else {
Window.alert("logon failed!");
}
}
}) ;
}
});
btnPanel.add(logonButton);
this.add(msgPanel);
this.add(namePanel);
this.add(pwdPanel);
this.add(btnPanel);
}
public String getName() {
return this.name.getText();
}
public String getPassword() {
return this.password.getText();
}
}
// WelcomePanel
package com.jpleasure.gwt.logon.client.panel;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.Widget;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 21:49:53
* To change this template use File | Settings | File Templates.
*/
public class WelcomePanel extends BasePanel {
public WelcomePanel() {
Label welcomeLabel = new Label("Welcome to LogonDemo!");
this.add(welcomeLabel);
Button logoutButton = new Button("Logout");
logoutButton.addClickListener(new ClickListener() {
public void onClick(Widget sender) {
ldc.gotoLogon();
}
});
this.add(logoutButton);
}
}
//LogonService
package com.jpleasure.gwt.logon.client.service;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.jpleasure.gwt.logon.client.exception.ApplicationException;
import com.jpleasure.gwt.logon.client.so.LogonSO;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:13:09
* To change this template use File | Settings | File Templates.
*/
public interface LogonService extends RemoteService {
/**
* Utility/Convenience class.
* Use LogonService.App.getInstance() to access static instance of LogonServiceAsync
*/
public static class App {
private static LogonServiceAsync ourInstance = null;
public static synchronized LogonServiceAsync getInstance() {
if (ourInstance == null) {
ourInstance = (LogonServiceAsync) GWT.create(LogonService.class);
((ServiceDefTarget) ourInstance).setServiceEntryPoint(GWT.getModuleBaseURL() + "com.jpleasure.gwt.logon.LogonDemo/LogonService");
}
return ourInstance;
}
}
public boolean logon(LogonSO logonSO) throws ApplicationException;
}
//LogonServiceAsync
package com.jpleasure.gwt.logon.client.service;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.jpleasure.gwt.logon.client.so.LogonSO;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:13:09
* To change this template use File | Settings | File Templates.
*/
public interface LogonServiceAsync {
void logon(LogonSO logonSO, AsyncCallback async);
}
// LogonSO
package com.jpleasure.gwt.logon.client.so;
import com.google.gwt.user.client.rpc.IsSerializable;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:13:52
* To change this template use File | Settings | File Templates.
*/
public class LogonSO implements IsSerializable {
private String name;
private String password;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
// LogonDemo
package com.jpleasure.gwt.logon.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.DeckPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.jpleasure.gwt.logon.client.panel.LogonPanel;
import com.jpleasure.gwt.logon.client.panel.WelcomePanel;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 21:34:12
* To change this template use File | Settings | File Templates.
*/
public class LogonDemo implements EntryPoint {
public void onModuleLoad() {
DeckPanel mainPanel = new DeckPanel();
LogonDemoController ldc = new LogonDemoController(mainPanel);
LogonPanel logonPanel = new LogonPanel();
logonPanel.setLdc(ldc);
mainPanel.add(logonPanel);
WelcomePanel welcomePanel = new WelcomePanel();
welcomePanel.setLdc(ldc);
mainPanel.add( welcomePanel );
mainPanel.showWidget(0);
RootPanel.get().add(mainPanel);
}
}
// LogonDemoController
package com.jpleasure.gwt.logon.client;
import com.google.gwt.user.client.ui.DeckPanel;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 21:58:02
* To change this template use File | Settings | File Templates.
*/
public class LogonDemoController {
private DeckPanel mainPanel;
public LogonDemoController(DeckPanel panel) {
this.mainPanel = panel;
}
public void gotoWelcome() {
if (mainPanel != null) {
mainPanel.showWidget(1);
}
}
public void gotoLogon() {
if (mainPanel != null) {
mainPanel.showWidget(0);
}
}
}
// LogonServiceImpl
package com.jpleasure.gwt.logon.server.service;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import com.jpleasure.gwt.logon.client.exception.ApplicationException;
import com.jpleasure.gwt.logon.client.service.LogonService;
import com.jpleasure.gwt.logon.client.so.LogonSO;
/**
* Created by IntelliJ IDEA.
* User: ma.zhao@dl.cn
* Date: 2007-8-27
* Time: 22:13:10
* To change this template use File | Settings | File Templates.
*/
public class LogonServiceImpl extends RemoteServiceServlet implements LogonService {
public boolean logon(LogonSO logonSO) throws ApplicationException {
if (logonSO.getName() != null && logonSO.getName().length() > 0 &&
logonSO.getPassword() != null && logonSO.getPassword().length() > 0 &&
logonSO.getName().equals(logonSO.getPassword())) {
return true;
} else if ("ex".equals(logonSO.getName())) {
throw new ApplicationException("Logon Exception!");
} else {
return false;
}
}
}
//LogonDemo.gwt.xml
<module>
<inherits name='com.google.gwt.user.User'/>
<entry-point class='com.jpleasure.gwt.logon.client.LogonDemo'/>
<servlet path="/com.jpleasure.gwt.logon.LogonDemo/LogonService"
class="com.jpleasure.gwt.logon.server.service.LogonServiceImpl"/>
</module>
安全相关
参看:http://groups.google.com/group/Google-Web-Toolkit/web/security-for-gwt-applications
在GWT中所有的画面都是由Panel实现的,而所有的Panel都会被编译为JavaScript和Html代码,这些代码在程序运行的开始就会
下载到客户的浏览器中,虽然这些JavaScript代码很难阅读,但是毕竟以影下载到了客户的环境中,所以在本质上GWT是不安全的。(请大家讨论)
另外由于JavaScript的灵活性,可以动态的在画面上创建链接,image等,所以数据也有可能别提交到其他的服务器,而非下载的服务器。
另外,由于Panel,Action等最终都编译为了JavaScript,所以关于用户权限等信息最好不要放在Panel,Action内部。最好能放在server包内部,
在客户端对Service的每次调用的开始确认用户的权限。
GWT开发使用的工具
免费
Netbeans + gwt4nb
参看:http://www.javapassion.com/handsonlabs/ajaxgwtintro/
优点:直接屏蔽了GWT Shell(Hosted Mode)开发的方式,可以方便的以普通Java Web 应用程序开发的凡是进行GWT开发。
缺点:不支持界面的拖拽编辑,同时继承了NB 5.5 的缺点,对JSP,HTML,JavaScript编辑器支持不足。
Eclipse + Cypal Studio for GWT
参看:http://www.ibm.com/developerworks/library/os-eclipse-ajaxcypal/index.html
优点:可以方便的以普通Java Web 应用程序开发的凡是进行GWT开发。支持两种运行方式:GWT Shell运行和Web方式运行。
收费:
Ingellij IDEA 6
参看:http://www.jetbrains.com/idea/training/demos/GWT.html
Eclipse + GWT Builder
http://www.instantiations.com/gwtdesigner/
一些技巧/注意事项
PasswordTextBox 不像TextBox那样有长度限制,所以在需要限制PasswordTextBox输入长度的时候,可以有两种实现方式,第一是,添加一个KeyboradListener,在KeyPress(KeyUp,KeyDown等)事件的时候判断长度,然后substring,另一种方式是,在提交的时候判断,让客户自己修改。确实有些不太方便,呵呵。
(未完待续)