|
<!-- START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- this content will be automatically generated across all content areas --><!-- END RESERVED FOR FUTURE USE INCLUDE FILES-->
|
級別: 中級
David Geary
, 總裁, Clarity Training, Inc.
2009 年 8 月 03 日
Java?Server Faces (JSF) 2 專家組成員 David Geary 將在這一期文章中結束這部有關 JSF 2 新特性的
系列文章(共 3 部分)
。本文介紹如何使用該框架的新事件模型和內置 Ajax 支持來增強可重用組件的功能。
<!-- START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --> <!-- END RESERVED FOR FUTURE USE INCLUDE FILES-->
JSF 的最大賣點在于它是一種基于組件的框架。這意味著您可以實現供其他人重用的組件。這種強大的重用機制在 JSF 1 中基本上是不可能實現的,因為在 JSF 1 中實現組件是非常困難的事情。
然而,正如
第 2 部分
所述,JSF 2 通過一種名為
復合組件
的新特性簡化了組件的實現 — 無需 Java 代碼和配置。這一特性可以說是 JSF 2 中最重要的部分,因為它真正實現了 JSF 組件的潛力。
在這份有關 JSF 2 的第三篇也是最后一篇文章中,我將展示如何利用新的 Ajax 和事件處理功能(也在 JSF 2 中引入)構建復合組件特性,要從 JSF 2 中獲得最大收益,需要遵循下面的技巧:
-
技巧 1:組件化
-
技巧 2:Ajax 化
-
技巧 3:展示進度
對于第一個技巧,我將簡要回顧已在
第 2 部分
中詳細描述過的兩個組件。對于后面的技巧,我將展示如何使用 Ajax 和事件處理功能來改造這些組件。
技巧 1:組件化
我在
第 1 部分
中引入的 places 應用程序包含有大量復合組件。其中之一便是
map
組件,它顯示一個地址地圖,其中包含一個縮放級別下拉菜單,如圖 1 所示:
圖 1. places 應用程序的
map
組件
清單 1 顯示了經過刪減的
map
組件列表:
清單 1.
map
組件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<html xmlns="http://www.w3.org/1999/xhtml"
...
xmlns:composite="http://java.sun.com/jsf/composite"
xmlns:places="http://java.sun.com/jsf/composite/components/places">
<!-- INTERFACE -->
<composite:interface>
<composite:attribute name="title"/>
</composite:interface>
<!-- IMPLEMENTATION -->
<composite:implementation">
<div class="map">
...
<h:panelGrid...>
<h:panelGrid...>
<
h:selectOneMenu
onchange="submit()"
value="#{cc.parent.attrs.location.
zoomIndex
}"
valueChangeListener="#{cc.parent.attrs.location.zoomChanged}"
style="font-size:13px;font-family:Palatino">
<f:selectItems value="#{places.zoomLevelItems}"/>
</h:selectOneMenu>
</h:panelGrid>
</h:panelGrid>
<
h:graphicImage
url="#{cc.parent.attrs.location.mapUrl}"
style="border: thin solid gray"/>
...
</div>
...
</composite:implementation>
</html>
|
組件的一大優點就是可以使用更有效的替代方法替換它們,同時不會影響到相關的功能。比如,在圖 2 中,我使用一個 Google Maps 組件替換了
清單 1
中的
image
組件,Google Maps 組件由 GMaps4JSF 提供(見
參考資料
):
圖 2. GMaps4JSF 的 map 圖像
map
組件的更新后的代碼(進行了刪減)如清單 2 所示:
清單 2. 使用一個 GMaps4JSF 組件替換 map 圖形
<h:selectOneMenu
onchange="submit()"
value
="#{cc.parent.attrs.location.
zoomIndex
}"
valueChangeListener="#{cc.parent.attrs.location.zoomChanged}"
style="font-size:13px;font-family:Palatino">
<f:selectItems value="#{places.zoomLevelItems}"/>
</h:selectOneMenu>
...
<
m:map
id="map" width="420px" height="400px"
address="#{cc.parent.attrs.location.streetAddress}, ..."
zoom
="#{cc.parent.attrs.location.
zoomIndex
}"
renderOnWindowLoad="false">
<m:mapControl id="smallMapCtrl"
name="GLargeMapControl"
position="G_ANCHOR_TOP_RIGHT"/>
<m:mapControl id="smallMapTypeCtrl" name="GMapTypeControl"/>
<m:marker id="placeMapMarker"/>
</m:map>
|
要使用 GMaps4JSF 組件,我從 GMaps4JSF 組件集合中使用
<m:map>
標記替換了
<h:graphicImage>
標記。將 GMaps4JSF 組件與縮放下拉菜單連接起來也很簡單,只需為
<m:map>
標記的
zoom
屬性指定正確的 backing-bean 屬性。
關于縮放級別需要注意一點,那就是當一名用戶修改縮放級別時,我將通過
<h:selectOneMenu>
的
onchange
屬性強制執行表單提交,如
清單 1
中第一處使用粗體顯示的代碼行所示。這個表單提交將觸發 JSF 生命周期,這實際上將把新的縮放級別推入到保存在父復合組件中的
location
bean 的
zoomIndex
屬性中。這個 bean 屬性被綁定到輸入組件,如
清單 2
中的第一行所示。
由于我沒有為與縮放級別修改相關的表單提交指定任何導航,JSF 在處理請求后刷新了同一頁面,重新繪制地圖以反映新的縮放級別。然而,頁面刷新還重新繪制了整個頁面,即使只修改了地圖圖像。在
技巧 2:Ajax 化
中,我將展示如何使用 Ajax,只對圖像部分重新繪制,以響應縮放級別的修改。
login
組件
places 應用程序中使用的另一個組件是
login
組件。圖 3 展示了這個 login 組件的實際使用:
圖 3.
login
組件
清單 3
展示了創建
圖 3
所示的
login
組件的標記:
清單 3. 最基礎的
login
:只包含必需的屬性
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:util="http://java.sun.com/jsf/composite/components/util">
<util:login
loginAction
="#{user.login}"
managedBean
="#{user}"/>
</ui:composition>
|
login
組件只包含兩個必需的屬性:
-
loginAction
:登錄 action 方法
-
managedBean
:包含名稱和密碼屬性的托管
清單 3
中指定的托管 bean 如清單 4 所示:
清單 4. User.groovy
package com.clarity
import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
@ManagedBean()
@SessionScoped
public class User {
private final String VALID_NAME = "Hiro"
private final String VALID_PASSWORD = "jsf"
private String
name, password
;
public String getName() { name }
public void setName(String newValue) { name = newValue }
public String getPassword() { return password }
public void setPassword(String newValue) { password = newValue }
public String
login()
{
"/views/places"
}
public String logout() {
name = password = nameError = null
"/views/login"
}
}
|
清單 4
中的托管 bean 是一個 Groovy bean。在這里使用 Groovy 替代 Java 語言并不會帶來多少好處,只是減少了處理分號和返回語句的麻煩。然而,在技巧 2 的
Validation
部分中,我將展示一個對
User
托管 bean 使用 Groovy 的更有說服力的原因。
大多數情況下,您將需要使用提示和按鈕文本來配置登錄組件,如圖 4 所示:
圖 4. 充分配置的
login
組件
清單 5 展示了生成
圖 4
所示的
login
組件的標記:
清單 5. 配置
login
組件
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:util="http://java.sun.com/jsf/composite/components/util">
<util:login loginPrompt="#{msgs.loginPrompt}"
namePrompt="#{msgs.namePrompt}"
passwordPrompt="#{msgs.passwordPrompt}"
loginButtonText="#{msgs.loginButtonText}"
loginAction="#{user.login}"
managedBean="#{user}"/>
</ui:composition>
|
在
清單 5
中,我從一個資源包中獲取了用于提示的字符串和登錄按鈕的文本。
清單 6 定義了
login
組件:
清單 6. 定義
login
組件
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Usage:
<util:login loginPrompt="#{msgs.loginPrompt}"
namePrompt="#{msgs.namePrompt}"
passwordPrompt="#{msgs.passwordPrompt}"
loginButtonText="#{msgs.loginButtonText}"
loginAction="#{user.login}"
managedBean="#{user}">
<f:actionListener for="loginButton"
type="com.clarity.LoginActionListener"/>
</util:login>
managedBean must have two properties: name and password.
The loginAction attribute must be an action method that takes no
arguments and returns a string. That string is used to navigate
to the page the user sees after logging in.
This component's loginButton is accessible so that you can
add action listeners to it, as depicted above. The class specified
in f:actionListener's type attribute must implement the
javax.faces.event.ActionListener interface.
-->
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:composite="http://java.sun.com/jsf/composite">
<!-- INTERFACE -->
<
composite:interface
>
<!-- PROMPTS -->
<composite:attribute name="loginPrompt"/>
<composite:attribute name="namePrompt"/>
<composite:attribute name="passwordPrompt"/>
<!-- LOGIN BUTTON -->
<composite:attribute name="loginButtonText"/>
<!-- loginAction is called when the form is submitted -->
<composite:attribute name="loginAction"
method-signature="java.lang.String login()"
required="true"/>
<!-- You can add listeners to this actionSource: -->
<composite:actionSource name="loginButton" targets="form:loginButton"/>
<!-- BACKING BEAN -->
<composite:attribute name="managedBean" required="true"/>
</composite:interface>
<!-- IMPLEMENTATION -->
<
composite:implementation
>
<div class="prompt">
#{
cc.attrs.loginPrompt
}
</div>
<!-- FORM -->
<h:form id="form">
<h:panelGrid columns="2">
<!-- NAME AND PASSWORD FIELDS -->
#{
cc.attrs.namePrompt
}
<h:inputText id="name"
value="#{
cc.attrs.managedBean.name
}"/>
#{
cc.attrs.passwordPrompt
}
<h:inputSecret id="password" size="8"
value="#{
cc.attrs.managedBean.password
}"/>
</h:panelGrid>
<p>
<!-- LOGIN BUTTON -->
<h:commandButton id="loginButton"
value="#{
cc.attrs.loginButtonText
}"
action="#{
cc.attrs.loginAction
}"/>
</p>
</h:form>
</composite:implementation>
</html>
|
和
map
組件一樣,
login
也可以使用一個 Ajax 升級。在下一個技巧介紹
Validation
時,我將展示如何將 Ajax 驗證添加到 login 組件中。
技巧 2:Ajax 化
與非 Ajax HTTP 請求相比,Ajax 請求通常需要額外執行兩個步驟:在服務器中對表單進行局部處理,接著在客戶機上對 Document Object Model (DOM) 進行局部呈現。
局部處理和呈現
通過將 JSF 生命周期分解為兩個不同的邏輯部分 —— 執行和呈現,JSF 2 現在支持局部處理和局部呈現。圖 5 突出顯示了執行部分:
圖 5. JSF 生命周期的執行部分
圖 6 突出顯示了 JSF 生命周期的呈現部分:
圖 6. JSF 生命周期的呈現部分
將生命周期劃分為執行和呈現部分的原理很簡單:您可以指定 JSF 在服務器上執行(處理)的組件,以及在返回 Ajax 調用時 JSF 呈現的組件。將使用 JSF 2 中新增的
<f:ajax>
實現這個目的,如清單 7 所示:
清單 7. 一個 Ajax 縮放菜單
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
value
="#{cc.parent.attrs.location.zoomIndex}"
style="font-size:13px;font-family:Palatino">
<f:ajax event="change" execute="@this" render="map"/>
<f:selectItems value="#{places.zoomLevelItems}"/>
</h:selectOneMenu>
<m:map id="
map
"...>
|
清單 7
對
清單 2
中的第一行所示的菜單進行了修改:我從
清單 2
中刪除了
onchange
屬性,并添加了一個
<f:ajax>
標記。這個
<f:ajax>
標記指定了以下內容:
-
觸發 Ajax 調用的事件
-
在服務器上執行的組件
-
在客戶機上呈現的組件
當用戶從縮放菜單中選擇一個菜單項時,JSF 將對服務器發出 Ajax 調用。隨后,JSF 將菜單傳遞給生命周期的執行部分(
@this
表示
<f:ajax>
周圍的組件),并在生命周期的 Update Model Values 階段更新菜單的
zoomIndex
。當 Ajax 調用返回后,JSF 呈現地圖組件,后者使用(新設置的)縮放指數重新繪制地圖,現在您就有了一個 Ajax 化的縮放菜單,其中添加了一行 XHTML。
但是還可以進一步簡化,因為 JSF 為
event
和
execute
屬性提供了默認值。
每個 JSF 組件都有一個默認事件,當在組件標記內部嵌入
<f:ajax>
標記時,該事件將觸發 Ajax 調用。對于菜單,該事件為
change
事件。這意味著我可以刪除
清單 7
中的
<f:ajax>
的
event
屬性。
<f:ajax>
的
execute
屬性的默認值是
@this
,這表示圍繞在
<f:ajax>
周圍的組件。在本例中,該組件為菜單,因此還可以刪除
execute
屬性。
通過對
<f:ajax>
使用默認屬性值,我可以將
清單 7
簡化為清單 8:
清單 8. 簡化后的 Ajax 縮放菜單
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
value="#{cc.parent.attrs.location.zoomIndex}"
style="font-size:13px;font-family:Palatino">
<f:ajax render="map"/>
<f:selectItems value="#{places.zoomLevelItems}"/>
</h:selectOneMenu>
<m:map id="map"...>
|
這演示了使用 JSF 2 向組件添加 Ajax 有多么容易。當然,前面的例子非常簡單:我僅僅是在用戶選擇某個縮放級別時重新繪制了地圖而不是整個頁面。驗證表單中的各個字段等操作要更加復雜一些,因此接下來我將討論這些用例。
驗證
當用戶移出某個字段后對字段進行驗證并提供即時的反饋,這始終是一個好的做法。例如,在圖 7 中,我使用了 Ajax 對名稱字段進行了驗證:
圖 7. Ajax 驗證
該名稱字段的標記如清單 9 所示:
清單 9. 名稱字段
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:panelGrid columns="2">
#{cc.attrs.namePrompt}
<h:panelGroup>
<h:inputText id="name" value="#{cc.attrs.managedBean.name}"
valueChangeListener
="#{cc.attrs.managedBean.validateName}">
<
f:ajax
event="
blur
" render="
nameError
"/>
</h:inputText>
<h:outputText id="
nameError
"
value="#{cc.attrs.managedBean.
nameError
}"
style="color: red;font-style: italic;"/>
</h:panelGroup>
...
</h:panelGrid>
|
我再一次使用了
<f:ajax>
,只不過這一次沒有執行輸入的默認事件 —
change
,因此我將
blur
指定為觸發 Ajax 調用的事件。當用戶移出名稱字段時,JSF 將對服務器發出 Ajax 調用并在生命周期的執行部分運行
name
輸入組件。這意味著 JSF 將在生命周期的 Process Validations 階段調用
name
輸入的值修改監聽程序(在
清單 9
中指定)。清單 10 展示了這個值修改監聽程序:
清單 10.
validateName()
方法
package com.clarity
import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
import javax.faces.event.ValueChangeEvent
import javax.faces.component.UIInput
@ManagedBean()
@SessionScoped
public class User {
private String name, password, nameError;
...
public void
validateName
(ValueChangeEvent e) {
UIInput nameInput = e.getComponent()
String name = nameInput.getValue()
if (name.contains("_"))
nameError
= "Name cannot contain underscores"
else if (name.equals(""))
nameError
= "Name cannot be blank"
else
nameError
= ""
}
...
}
|
這個修改值的監聽程序(
user
托管 bean 的
validateName()
方法)將驗證名稱字段并更新
user
托管 bean 的
nameError
屬性。
返回 Ajax 調用后,借助
清單 9
中的
<f:ajax>
標記的
render
屬性,JSF 呈現
nameError
輸出。該輸出顯示了
user
托管 bean 的
nameError
屬性。
多字段驗證
在前面的小節中,我展示了如何對單一字段執行 Ajax 驗證。但是,有些情況下,需要同時對多個字段進行驗證。比如,圖 8 展示了 places 應用程序同時驗證名稱和密碼字段:
圖 8. 驗證多個字段
我在用戶提交表單時同時驗證了名稱和密碼字段,因此對這個例子不需要用到 Ajax。相反,我將使用 JSF 2 的新事件系統,如清單 11 所示:
清單 11. 使用
<f:event>
<h:form id="form" prependId="false">
<
f:event
type="
postValidate
"
listener="#{cc.attrs.managedBean.validate}"/>
...
</h:form>
<div class="error" style="padding-top:10px;">
<h:messages layout="table"/>
</div>
|
在
清單 11
中,我使用了
<f:event>
— 類似于
<f:ajax>
,它是 JSF 2 中新增的內容。
<f:event>
標記在另一方面還類似于
<f:ajax>
:使用起來很簡單。
將一個
<f:event>
標記放到組件標記的內部,當該組件發生指定的事件(使用
type
屬性指定)時,JSF 將調用一個使用
listener
屬性指定的方法。因此,
<f:event>
標記在
清單 11
中的含義就是:對表單進行驗證后,對用戶傳遞給這個復合組件的托管 bean 調用
validate()
方法。該方法如清單 12 所示:
清單 12.
validate()
方法
package com.clarity
import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
import javax.faces.event.ValueChangeEvent
import javax.faces.component.UIInput
@ManagedBean()
@SessionScoped
public class User {
private final String VALID_NAME = "Hiro";
private final String VALID_PASSWORD = "jsf";
...
public void
validate
(ComponentSystemEvent e) {
UIForm form = e.getComponent()
UIInput nameInput = form.findComponent("name")
UIInput pwdInput = form.findComponent("password")
if ( ! (nameInput.getValue().equals(VALID_NAME) &&
pwdInput.getValue().equals(VALID_PASSWORD))) {
FacesContext fc = FacesContext.getCurrentInstance()
fc.addMessage(form.getClientId(),
new FacesMessage("Name and password are invalid. Please try again."))
fc.renderResponse()
}
}
...
}
|
JSF 將一個組件系統事件傳遞給
清單 12
中的
validate()
方法,方法從這個事件中獲得對(適用于事件的)組件的引用 — 登錄表單。對于這個表單,我使用
findComponent()
方法獲得名稱和密碼組件。如果這些組件的值不為 Hiro 和 jsf,那么我將把一條消息存儲到 faces 上下文并要求 JSF 繼續處理生命周期的 Render Response 階段。通過這種方法,就可以避免 Update Model Values 階段,后者會將壞的名稱和密碼傳遞給模型(見
圖 5
)。
您可能已經注意到,
清單 10
和
清單 12
中的驗證方法是使用 Groovy 編寫的。與
清單 4
不同,后者使用 Groovy 的惟一好處就是避免了分號和返回語句,
清單 10
和
清單 12
中的 Groovy 代碼使我不必進行類型轉換。例如,在
清單 10
中,
ComponentSystemEvent.getComponent()
和
UIComponent.findComponent()
都返回類型
UIComponent
。對于 Java 語言,我需要轉換這些方法的返回值。Groovy 為我做了這一轉換工作。
技巧 3:展示進度
在
Ajax 化
中,我展示了如何為
map
組件 Ajax 化縮放菜單,因此,當用戶修改縮放級別時,places 應用程序將只重新繪制頁面的地圖部分。另一個常見 Ajax 用例是向用戶提供反饋,表示一個 Ajax 事件正在處理中,如圖 9 所示:
圖 9. 進度條
在
圖 9
中,我將使用一個動畫 GIF 替換縮放菜單,這個動畫 GIF 將在 Ajax 調用期間顯示。當 Ajax 調用完成后,我將使用縮放菜單替換進度指示器。清單 13 展示了這一過程:
清單 13. 監視 Ajax 調用
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
value="#{cc.parent.attrs.location.zoomIndex}"
style="font-size:13px;font-family:Palatino">
<f:ajax render="map"
onevent="zoomChanging"
/>
<f:selectItems value="#{places.zoomLevelItems}"/>
...
</h:selectOneMenu>
...
<h:graphicImage id="
progressbar
" style="display: none"
library="images" name="orange-barber-pole.gif"/>
|
在
清單 13
中,我添加了一個進度條圖像(該圖像最初不會顯示出來),并為
<f:ajax>
指定了
onevent
屬性。該屬性引用一個 JavaScript 函數,如
清單 14
所示,這個函數將在
清單 13
中的 Ajax 調用被初始化時由 JSF 調用:
清單 14. 響應 Ajax 請求的 JavaScript
function zoomChanging(data) {
var menuId = data.source.id;
var progressbarId = menuId.substring(0, menuId.length - "menu".length)
+ "progressbar";
if (data.name == "begin") {
Element.hide(menuId);
Element.show(progressbarId);
}
else if (data.name == "success") {
Element.show(menuId);
Element.hide(progressbarId);
}
}
|
JSF 向
清單 14
中的函數傳遞一個對象,該對象中包含有一些信息,比如觸發了事件的組件的客戶機標識符(在本例中為縮放級別菜單),以及 Ajax 請求的當前狀態,使用
name
屬性表示。
清單 14
中的
zoomChanging()
函數計算進度條圖像的客戶機標識符,然后使用 Prototype
Element
對象在 Ajax 調用期間隱藏和顯示對應的 HTML 元素。
結束語
在過去幾年中,人們認為 JSF 1 是一個難以使用的框架。在許多方面上,這種評價是有一定道理的。JSF 1 在開發期間完全沒有考慮到實際使用中遇到的問題。因此,JSF 在實現應用程序和組件方面比預先設想更加困難。
另一方面,JSF 2 經歷了那些在 JSF 1 基礎之上實現過開源項目的開發人員的實際體驗。總結經驗之后,JSF 2 是一個更加合理的框架,可以輕松地實現健壯的、Ajax 化的應用程序。
貫 穿本系列文章,我展示了一些最突出的 JSF 2 特性,比如注釋和替換配置約定、簡化后的導航、資源支持、復合組件、內置 Ajax 以及內嵌的事件模型。但是仍然有許多 JSF 2 特性未在本系列提及,比如 View 和 Page 范圍、為頁面添加書簽的支持和 Project Stage。所有這些特性以及其他更多特性讓 JSF 2 在 JSF 1 的基礎上實現了巨大的改進。
下載
描述 名字 大小 下載方法
本文示例的源代碼
j-jsf2-fu-3.zip
|
7.7MB
|
HTTP
|
參考資料
學習
獲得產品和技術
討論
關于作者
|
|
|
David Geary 是一名作家、演講家和顧問,也是
Clarity Training, Inc.
的總裁,他指導開發人員使用 JSF 和 Google Web Toolkit (GWT) 實現 Web 應用程序。他是 JSTL 1.0 和 JSF 1.0/2.0 專家組的成員,與人合作編寫了 Sun 的 Web Developer 認證考試的內容,并且為多個開源項目作出貢獻,包括 Apache Struts 和 Apache Shale。David 的
Graphic Java Swing
一直是關于 Java 的暢銷書籍,而
Core JSF
(與 Cay Horstman 合著)是關于 JSF 的暢銷書。David 經常在各大會議和用戶組發表演講。他從 2003 年開始就一直是 NFJS tour 的固定演講人,并且在 Java University 教授課程,兩次當選為 JavaOne 之星。
|
|