2010年11月24日

Application Architecture

Application Architecture

As mentioned before, I live mostly in the Application Architecture area. Even within this one area, there are several views of the architecture. I like Rational's Four Plus One views.  Rational has refined the definitions several times since Phillipe Kruchten's original paper, but I stick with Frank Dupont, my first Rational trainer when RUP was still in beta:
Logical View Allocates classes and responsibilities to packages and subsystems.
Development View Allocates packages and subsystems to layers and components. Also defines the development structure.
Process View Allocates components to processes.
Physical View Allocates processes to processors, nodes, networks.
Use Case View (The Plus One) Captures requirements used to develop and validate each of the other views.
Application architecture can be considered alone, but it makes most sense as part of the pyramid. Applications spring from needs and requirements coming from the business and information layers. Application architects must work with data and infrastructure designers to get their systems working and deployed to their users. Increasingly, new systems use services of existing systems, so application architects must work together to integrate. While Application Architects are more focused on individual systems than Enterprise Architects, the scope of their interactions and interests are often the same.

By the Way, What is an Application?

Back in mainframe and Client-Server days, we knew what "application" meant. It was a collection of programs, or maybe one gigantic client program, that delivered a set of related business functions to the user. One project team usually built an application from end-to-end in one cohesive effort. In my COBOL days, the company imposed a project code in the name space, so for example we knew that every file and program starting with E0186 was part of the Work In Process application.
Nowadays things are more complex. A solid Enterprise Application Integration architecture allows interoperation between nearly any two platforms. A cohesive set of screens or web pages may still feel like an application to the user, but the back-end may combine parts from different systems, platforms and languages with purchased components and software hosted by business partners. It is becoming less useful to talk about "an application" and more useful to deliver increasingly integrated functionality to end users.
This kind of environment has great management and political challenges - who builds what and how does the next system pay to use it - but it really makes architecture fun.

Application Architecture Goals

Since the earliest days of data processing, coders and their managers have struggled to define programming goodness. We can see this in an arc over the years, from Edsger W. Dijkstra's letter "Go-To Statement Considered Harmful" through structured concepts of coupling, cohesion and information hiding, and object oriented encapsulation and whole books of design principles.
Architecture, thinking in larger terms than code, components or individual systems, has its own vocabulary of goodness. I have a list of about 100 possible measures of quality that I pull out for review before starting a project. An organization should decide which ones it believes are important, and set clear priorities, perhaps as enterprise defaults modified by business needs on a project-by-project basis. Here are just a few:
  • Meet business goals by enabling software to reduce costs, increase profits or differentiate the company.
  • RASP: Reliability, Availability, Scalability, Performance, 
  • Usability.
  • Extensibility.
  • Freedom from defects.
  • Low cost and rapid delivery.
  • Layered architectures, wherein ...
    • Each layer has a clear, focused purpose (high cohesion)
    • Layers are cleanly separated and independent (low coupling)
  • Conformance to team, department, company and industry standards.
  • Practice reuse.
  • Contribute components to future reuse.
  • Channel independence - use the same components for Internet, fat-client, voice response units, wireless, etc.
  • Consistent data and rules across all channels
  • Interoperability between disparate platforms, languages, etc.
  • Portability
  • And many more ...

The Architect's Job

The Application Architect operates nearly as a peer to the project manager. While the project manager deals with budgets, plans, resources and tracking progress, the architect sets the technical vision for the project, mentors the technical staff, and monitors design and implementation artifacts for quality and compliance to standards.

2010年11月10日

What is the IBAction and IBOutlet

Apress - Learn Cocoa on the Mac (Feb 2010) (ATTiCA)


You might be wondering just what IBAction and IBOutlet are. Are they part of the Objective-C language?
Nope. They're good old-fashioned C pre-processor macros. If you go into the AppKit.framework and look at the NSNibDeclarations.h header file, you'll see that they're defined like this:

#ifndef IBOutlet
#define IBOutlet
#endif

#ifndef IBAction
#define IBAction void
#endif

Confused? These two keywords do absolutely nothing as far as the compiler is concerned. IBOutlet gets entirely removed from the code before the compiler ever sees it. IBAction resolves to a void return type, which just means that action methods do not return a value. So, what's going on here?
The answer is simple, really: IBOutlet and IBAction are not used by the compiler. They are used by Interface Builder. Interface Builder uses these keywords to parse out the outlets and actions available to it. Interface Builder can only see methods that are prefaced with IBAction and can only see variables or properties that are prefaced with IBOutlet. Also, the presence of these keywords tells other programmers, looking at your code in the future, that the variables and methods in question aren't dealt with entirely in code. They 'll need to delve into the relevant nib file to see how things are hooked up and used.

2010年9月10日

Spring事务配置的五种方式

前段时间对Spring的事务配置做了比较深入的研究,在此之间对Spring的事务配置虽说也配置过,但是一直没有一个清楚的认识。通过这次的学习发觉Spring的事务配置只要把思路理清,还是比较好掌握的。

总结如下:

Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分。

DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager。

具体如下图:

Spring事务配置 (2)

根据代理机制的不同,总结了五种Spring事务的配置方式,配置文件如下:

第一种方式:每个Bean都有一个代理

xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context
="http://www.springframework.org/schema/context"
xmlns:aop
="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
>

<bean id="sessionFactory"
class
="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
bean>


<bean id="transactionManager"
class
="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
bean>


<bean id="userDaoTarget" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
bean>

<bean id="userDao"
class
="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">

<property name="transactionManager" ref="transactionManager" />
<property name="target" ref="userDaoTarget" />
<property name="proxyInterfaces" value="com.bluesky.spring.dao.GeneratorDao" />

<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIREDprop>
props>
property>
bean>
beans>

第二种方式:所有Bean共享一个代理基类

xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context
="http://www.springframework.org/schema/context"
xmlns:aop
="http://www.springframework.org/schema/aop"
xsi:schemaLocation
="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
>

<bean id="sessionFactory"
class
="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
bean>


<bean id="transactionManager"
class
="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
bean>

<bean id="transactionBase"
class
="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
lazy-init
="true" abstract="true">

<property name="transactionManager" ref="transactionManager" />

<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIREDprop>
props>
property>
bean>


<bean id="userDaoTarget" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
bean>

<bean id="userDao" parent="transactionBase" >
<property name="target" ref="userDaoTarget" />
bean>
beans>

第三种方式:使用拦截器

xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context
="http://www.springframework.org/schema/context"
xmlns:aop
="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"
>

<bean id="sessionFactory"
class
="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
bean>


<bean id="transactionManager"
class
="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
bean>

<bean id="transactionInterceptor"
class
="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager" />

<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIREDprop>
props>
property>
bean>

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>*Daovalue>
list>
property>
<property name="interceptorNames">
<list>
<value>transactionInterceptorvalue>
list>
property>
bean>


<bean id="userDao" class="com.bluesky.spring.dao.UserDaoImpl">
<property name="sessionFactory" ref="sessionFactory" />
bean>
beans>

第四种方式:使用tx标签配置的拦截器

xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context
="http://www.springframework.org/schema/context"
xmlns:aop
="http://www.springframework.org/schema/aop"
xmlns:tx
="http://www.springframework.org/schema/tx"
xsi:schemaLocation
="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
>

<context:annotation-config />
<context:component-scan base-package="com.bluesky" />

<bean id="sessionFactory"
class
="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
bean>


<bean id="transactionManager"
class
="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
bean>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" />
tx:attributes>
tx:advice>

<aop:config>
<aop:pointcut id="interceptorPointCuts"
expression
="execution(* com.bluesky.spring.dao.*.*(..))" />
<aop:advisor advice-ref="txAdvice"
pointcut-ref
="interceptorPointCuts" />
aop:config>
beans>

第五种方式:全注解

xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi
="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context
="http://www.springframework.org/schema/context"
xmlns:aop
="http://www.springframework.org/schema/aop"
xmlns:tx
="http://www.springframework.org/schema/tx"
xsi:schemaLocation
="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
>

<context:annotation-config />
<context:component-scan base-package="com.bluesky" />

<tx:annotation-driven transaction-manager="transactionManager"/>

<bean id="sessionFactory"
class
="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="configLocation" value="classpath:hibernate.cfg.xml" />
<property name="configurationClass" value="org.hibernate.cfg.AnnotationConfiguration" />
bean>


<bean id="transactionManager"
class
="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
bean>

beans>

此时在DAO上需加上@Transactional注解,如下:

package com.bluesky.spring.dao;

import java.util.List;

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.stereotype.Component;

import com.bluesky.spring.domain.User;

@Transactional
@Component(
"userDao")
public class UserDaoImpl extends HibernateDaoSupport implements UserDao {

public List<User> listUsers() {
return this.getSession().createQuery("from User").list();
}


}

2010年9月6日

COPY-軟件版本Beta,RC,Demo,Build,GA等是什麼意思呢?

COPY-軟件版本Beta,RC,Demo,Build,GA等是什麼意思呢?
2009/02/23 10:40
Alpha:是內部測試版,一般不向外部發佈,會有很多Bug.一般只有測試人員使用。
Beta:也是測試版,這個階段的版本會一直加入新的功能。在Alpha版之後推出。
RC:(Release Candidate)顧名思義麼 ! 用在軟件上就是候選版本。系統平台上就是發行候選版本。RC版不會再加入新的功能了,主要著重於除錯。
RTM:(Release to Manufacture)是給工廠大量壓片的版本,內容跟正式版是一樣的,不過RTM版也有出限制、評估版的。但是和正式版本的主要程序代碼都是一樣的。
OEM:是給計算機廠商隨著計算機販賣的,也就是隨機版。只能隨機器出貨,不能零售。只能全新安裝,不能從舊有操作系統升級。包裝不像零售版精美,通常只有一面CD和說明書(授權書)。
RVL:號稱是正式版,其實RVL根本不是版本的名稱。它是中文版/英文版文檔破解出來的。
EVAL:而流通在網絡上的EVAL版,與「評估版」類似,功能上和零售版沒有區別。
RTL:Retail(零售版)是真正的正式版,正式上架零售版。在安裝盤的i386文件夾裡有一個eula.txt,最後有一行 EULAID,就是你的版本。比如簡體中文正式版是EULAID:WX.4_PRO_RTL_CN,繁體中文正式版是WX.4_PRO_RTL_TW。其中:如果是WX.開頭是正式版,WB.開頭是測試版。_PRE,代表家庭版;_PRO,代表專業版。

版本號:
V(Version):即版本,通常用數字表示版本號。(如:EVEREST Ultimate v4.20.1188 Beta )
Build:用數字或日期標示版本號的一種方式。(如:VeryCD eMule v0.48a Build 071112)
SP:Service Pack,升級包。(如:Windows XP SP 2/Vista SP 1)

授權和功能劃分:
Trial:試用版,通常都有時間限制,有些試用版軟件還在功能上做了一定的限制。可註冊或購買成為正式版
Unregistered:未註冊版,通常沒有時間限制,在功能上相對於正式版做了一定的限制。可註冊或購買成為正式版。
Demo:演示版,僅僅集成了正式版中的幾個功能,不能升級成正式版。
Lite:精簡版。
Full version:完整版,屬於正式版。

語言劃分:
SC:Simplified Chinese簡體中文版。
CN : 簡體中文版
GBK:簡體中文漢字內碼擴展規範版。
TC:Traditional Chinese繁體中文版。
CHT : 繁體中文版
BIG5:繁體中文大五碼版。
EN : 英文版
Multilanguage : 多語言版
UTF8:Unicode Transformation Format 8 bit,對現有的中文系統不是好的解決方案。

開發階段劃分:
α(Alpha)版:內測版,內部交流或者專業測試人員測試用。Bug較多,普通用戶最好不要安裝。
β(Beta)版:公測版,專業愛好者大規模測試用,存在一些缺陷,該版本也不適合一般用戶安裝。
γ(Gamma)版:相當成熟的測試版,與即將發行的正式版相差無幾。
RC版:Release Candidate。
RC 版。是 Release Candidate 的縮寫,意思是發佈倒計時,候選版本,處於Gamma階段,該版本已經完成全部功能並清除大部分的 BUG。到了這個階段只會除BUG,不會對軟件做任何大的更改。從Alpha到Beta再到Gamma是改進的先後關係,但RC1、RC2往往是取捨關係。
Final:正式版。

其他版本
Enhance :增強版或者加強版 屬於正式版1
Free :自由版
Release :發行版 有時間限制
Upgrade :升級版
Retail  :零售版
Cardware :屬共享軟件的一種,只要給作者回覆一封電郵或明信片即可。(有的作者並由此提供註冊碼等),目前這種形式已不多見。/ S
Plus :屬增強版,不過這種大部分是在程序界面及多媒體功能上增強。
Preview :預覽版
Corporation & Enterprise :企業版
Standard :標準版
Mini :迷你版也叫精簡版只有最基本的功能
Premium : 貴價版
Professional : 專業版
Express : 特別版
Deluxe : 豪華版
Regged : 已註冊版
Rip :是指從原版文件(一般是指光盤或光盤鏡像文件)直接將有用的內容(核心內容)分離出來,剔除無用的文檔,例如PDF說明文件啊,視頻演示啊之類的東西,也可以算做是精簡版吧…但主要內容功能是一點也不能缺少的!另:DVDrip是指將視頻和音頻直接從DVD光盤裡以文件方式分離出來。
RTM 版 :這基本就是最終的版本,英文是 Release To Manufactur,意思是發佈到生產商。


RC就是Release Candidate(候選版本)的簡稱
GA:General Availability,正式發佈的版本,在國外都是用GA來說明release版本的

2010年8月24日

技術對談-看Google怎麼用Java

席捲全球Java界的《Java Puzzlers》作者-Google首席Java架構師
Joshua Bloch與Google專任工程師兼Java講師Neal Gafter,應邀來臺擔任Java 2006的講師。iThome邀請多位Java專家與兩位Google大師對談。在輕鬆的氣氛中,Joshua與Neal針對Java的發展與Google的技術應用等議題,以幽默詼諧的方式提出他們的看法。

PC組成高度容錯的分散式架構
王建興:當我們知道可與兩位來自Google的Java大師座談時,心中浮現的自然是Google舉世聞名的分散式架構,Java向來有效能上的爭議,但Google為何會採用Java?又如何應用Java?

Neal:Google打造的許多應用程式都是大型的分散式系統,我們不會將應用程式放到一臺大型的單一主機上。我們喜歡使用現成的PC,來建構我們的系統,而不是大型而且可靠度佳的昂貴主機。單一PC隨時可能發生錯誤,我們試著用軟體的方式建立容錯的機制。

基本上,我們沒使用J2EE,這其中有許多原因,包括Google在J2EE之前就已經有了自己的分散式架構,甚至還是使用 Java 語言建構出來的。所以Google有自己的RPC(Remote Procedure Call)系統,而且廣泛地使用,它可以在不同語言之間達到良好的溝通,不論伺服器、用戶端、Java、Python 或C++,而且運作地很平順。

我們應用Interface Definition Language編譯程式,成為可以跨Java、Python及C++三種語言的系統。所以當我們設計一個分散式系統時,其中一件事就是看哪種通訊協定可以適合這些語言來開發,另外就是我們怎麼處理容錯、備份、如何確保我們不會遺失任何使用者的資料、如何恢復某臺當掉的機器。Google有特別的解決方式處理這方面的問題,我們有個通用的解決方法。

Joshua:沒錯,如Neal所言,失敗不是少見的情況,而是很常見的。當你要建立一個像Google這樣規模的搜尋服務,你可以想像會有多少問題,但是我們就是要持續的讓服務運作下去,盡量讓系統可以自動修復,不要造成延遲。

王文彬:Google的應用好像都是自製的,是否曾使用其他公司的產品?

Joshua:會的,我們會拿現成的來用,多數工具是我們自製的,不過如果有別人已開發好的軟體,剛好是我們需要的,而且價格不錯,我們會買下它,Google Web Toolkit(GWT)就是很好的例子。

Neal:我們就常用一些開放源碼的產品,用哪些可能不方便講,不過我們用很多Linux工具,有時候我們會用些其他人開發好的工具,其實Google沒有規定一定什麼都要自己開發,當你要把程式交給開放源碼社群時,你會傾向社群支持你的東西,但你不會要他們付錢給你。

彬:這就是JBoss要做的。

Java與C++人才的比重逐漸改變
王建興:我們聽早上的主題演講,你們說Google應用Java在中介層(Middle-Tier),例如GMail、Calendar。我們很有興趣知道什麼樣的應用適合使用Java?

Neal:沒錯,在中介層的開發,Java是很好的選擇。Google 有很多基礎的設施,Web Servlet 引擎,讓你很簡單就能以Java撰寫與部署中介層的程式。

Joshua:而且有許多現成的函式庫,不需要用到bit-level,就可以整合原有底層的C++程式,這可以廣泛地移植到用戶端的程式,像是一些AJAX程式。

Neal:這還是要看各個開發團隊決定用什麼,如果是要開發一些以Web為主的程式,可能就不考慮Java而選擇C++,有可能因為他們對C++很熟,所以,即使他們開發的東西和我做的很像,但他們還是決定選擇C++。

王建興:你們認為Java能提供更好的可攜性嗎?

Joshua:絕對是的。而且Java可以提供較好的效能表現。

Neal:一般來說,以Google使用Java的方式下,可攜性沒那麼重要。因為我們將Java應用程式部署在特定版本的Linux。

不過,可攜性的優點,是當我們移至下一個版本的Java,或下一個版本的Linux時,我們不想鎖在某個特定版本的Java、架構或作業系統,所以可攜性很重要。

Joshua:但我們不會要求寫程式時,要同時兼顧在任何平臺上都可以執行,這有點與寫程式的態度有關,你是要寫給自己用的,或是寫給全世界用的。

王建興:我想你們會採用 Java 有一個很重要的理由是想要降低開發的心力和時間。

Joshua:絕對如此。還有另一個理由,學生在學校裡學的都是Java,而且喜歡Java,他們甚至不懂C++,所以比找C++人才容易。

Neal:而且Java除錯容易多了。

王文彬:現在Google的C++與Java工程師的比例為何?各占一半嗎?

Joshua:我不太確定。可以肯定的是Google正在徵求更多的Java工程師。

Neal:應該不到這個比例,使用C++的應該多一點,可能是 6:4 左右。不過,使用Java的人正在成長中,5年前Google大概沒什麼人使用Java,所以我們可以預見,Java在Google裏越來越重要。

王建興:即使Java對開發企業應用程式是夠快的,可是你們怎麼兼顧延展性(Scalability)?

Joshua: Google解決Java的延展性,就像解決其他這類延展性問題。像是複製、大型分散式系統,或者用更多機器來解決這些問題。

王建興:所以你們會用分散式架構來解決?

Joshua:對。

葉秉哲:請問Google分散式基礎架構早在Google搜尋引擎剛草創的時代就已經存在了嗎?

Neal:許多Google的叢集和分散式基礎架構,的確可追溯到Google兩位創始者在史丹福大學研究所,以三五臺機器研究時代。不過早期的基礎架構都已被重新設計過,所以現在已經很難看到雜亂的程式碼了。單以搜尋引擎核心本身而言,大概就被改寫過三次。

Joshua:不過,當然,某些核心的演算法,例如PageRank仍然存在。

沒有修改JVM,調校是從程式著手
王建興:在你們早上的演講中,提到有許多在Java社群有所貢獻的人,現在都在Google,我們看到有很多人都熟悉JVM,我們猜你們有基於效率的考量修改JVM。我們有猜對嗎?

Joshua、Neal:猜錯了。

Joshua:你們可能會這麼想,但就我所知沒有。我們使用現成的JVM,不是我們為自己人說話,但昇陽的JVM效能一直在精進,我們把原來跑在JRE 1.4的程式放在Tiger上,發現,哇!效能這麼好。再放在Mustang,哇!效能更快,我們就不用花什麼功夫特別去調整我們的程式了。

Neal:我們的確是和昇陽保持聯繫,所以當我們遇到了特定的問題時,我們就會讓他們知道,這樣的VM對我們不夠用。

Joshua:而且我們提供解決方案。

王建興:所以最佳化是在應用程式的層級進行?

Joshua:是的,開發精簡而有效率的程式,Java是很好的工具。Java程式既短且清楚,而且提供夠多的函式庫,可以根據需求置換。要寫出一樣的C++程式,就得花上更多的力氣。

Neal:你可以找到許多用在Java的效能衡量與調校的工具,那可以很容易找出問題在哪。

王建興:程式效能的瓶頸。

Neal:是呀,我總是很驚訝瓶頸怎麼會在這兒,所以總是真的找到了才相信。

王建興:效能瓶頸所在,總是令人驚訝不已。

Joshua:沒有真的證明前,總是猜錯瓶頸究竟在哪裏。

開放原始碼,對Java的幫助有限
朱仲傑:我們知道這幾年Java將會開放原始碼,你們們覺得這會對提升Java的效能,或找出Java的效能瓶頸有幫助嗎?

Joshua:Neal和我並不同意這個觀點,基本上我們有研究許可(Research License)已經可以閱讀原始碼,調校或找出問題都很簡單,我不是要說這麼做沒用,而是我們不需要因為原始碼不夠開放而這麼做,從1995年到現在,已有很多進步,可以寫很高效能的Java程式。

Neal:Java SE今年年底就要釋出原始碼,你可能會覺得屆時開放源碼社群協助Java SE,找出Java SE的問題。不過,其實他們現在就已經可以這麼做了,Java SE已有公開的原始碼,你可以在上面加些警告,也可以告訴昇陽哪裏有問題。但是實際上,很少人這麼做,所以我不覺得完全開放源碼之後,會吸引大家貢獻些什麼,因為現在就可以了。

Joshua:不過學術單位在Java開放以後,就可以更自由的使用與修改Java了。

Neal:有些事是開放Java源碼以後可以做的。像是Java語法的實驗,現在我雖然可以改Java的東西,做些實驗性質的應用,加些新的功能,但因為我不是昇陽的員工,不被允許提供給別人試用。

Joshua:我要提出相反的看法,7年前Philip Wadler、Martin Odersky這兩個人,在Java上加了很多功能,然後取名叫「Pizza」,現在的Generic功能很多都是根源於Pizza的。

Neal:但如果他們在Java開放原始碼之後再做,就不需要自己重寫一個Java SE了!

Joshua:我要說的是,這類在Java之上的再開發,7、8年前就有。

Google力求提供在地化的服務
馮彥文:我很好奇Google的軟體,有沒有還未自動化的?需要人工處理的?

Neal:有,像在地化,因地區不同的翻譯之類。

Joshua:也是有些工具可以幫忙。

Neal:其實也有系統幫助不在Google工作的人,協助我們做在地化。

Joshua:基本上Google會盡量能自動化就自動化,Yahoo就不那麼自動化,Yahoo是有歷史原因的,因為從一開始的設計就是由人工登錄網址;而Google從一開始的索引就是讓網路自己搜尋,但也沒辦法做到樣樣自動化。

Neal:還是有些服務像登廣告,得由人工檢查填寫資料的是否正確,及是否侵犯他人的商標。

Joshua:不過我再強調,我們都盡量自動化,不能做到的,才用人工補強。例如Google News,就不是用人工挑選資料,是電腦篩選的。

Neal:Google設計服務時,還是以自動化為原則,這樣的服務才不會被限制住。如果有服務是我們無法靠自動化來解決的,可能我們就做錯了方向。

馮彥文:所以像 Google廣告,如果我想申請的是中文、日文,這些不同語言的廣告,你們需要不同地區的人協助嗎?

Neal:我想我們在全球不同國家,都有分公司照顧各種語言的用戶,我們的服務在全球都有,還是需要有人知道當地的語言、文化、法律的特色,才能服務當地的人。

葉秉哲:所以我們才能看到Google Map在日本是日文,在希臘是希臘文。

Neal:沒錯。

Joshua:Google了解這個世界只有一小部分人的母語是英文,我們也了解,在這個一直成長的網路世界人口中,大部分的人說著其他語言,Google想要為這部分的人服務。

衝擊可以激盪出更多興奮的點子
錢世豐:Google在全球有那麼多員工,分散在不同的地方,如何合作完成工作?

Joshua:我們公司使用電子郵件和即時傳訊系統溝通。有人說到Google Talk嗎?

昨晚我抵達台北的時候,寫電子郵件給老婆,一打開即時通訊,就有Google的人傳訊過來問我「Hey,你現在在哪裏?我有個技術問題想問你,有個API裏有個名字,我不認為那是個好名字,你覺得呢?」基本上,Google是個全球性的公司,合作的方法就是要有全球化的想法。

Neal:Google盡可能的維持小型團隊,而且讓小型團隊在同地點工作,理想的團隊規模可能是5個人,4個或5個,也許多到8個。大過這個數字,溝通就複雜多了。

Joshua:舉例像匹茲堡分公司,匹茲堡的團隊可能有些人會在某個計畫上分擔工作,和位於山景城(Mountain View)總部的員工合作。Google是從史丹福的研究生開始的,逐漸聚集許多工程師加入,這些工程師聚在一起就會有些東西蹦出來。

王文彬:你們現在有多少員工?一萬人?

Neal:還沒,大約7000~8000人。

王文彬:那現在一定也多了許多人是從事業務工作,從一個管理者的角度來看,如何和業務這類不同性質的人合作?

Joshua:這對公司而言的確是有點挑戰。可能有時候需要順從別人的意見,但Google還是盡量保有某些原則。當然還是有混亂衝擊(Chaos)的時候,優秀的科技公司,總是會有些「衝擊」,因為衝擊可以激盪出更多令人興奮的點子。

王文彬:我總很好奇,你們有很多東西都是自製的,那新進同仁要如何學習這些東西呢?學習方式為何?

Joshua:這是非常棒的問題,來到Google就像剛移民時,都要上一些課,像剛上大學會有新生訓練一樣,Google有兩個禮拜的新生訓練,教導Google現有的產品。例如我們是開發Java程式的,就會教導一些寫Java的方式。

Neal:新進員工會有指定的mentor(學長)帶他,如果有什麼問題,可以問學長,實作上的問題,所有辦公室都是可以分享的資源,Google人都很樂意幫忙。尤其新進來的工程師會有個「Starter Project」,會指導你怎麼做某些特別的事,讓你習慣程式管理機制,例如如何檢查程式、程式撰寫的原則、有問題該找誰等。

Neal:一般來說,Google不會把5、6個新員工放在一起,由他們自己做事,新員工都會安插在一些原有的團隊裡面。
Joshua:其實老員工都還會輪調到不同分公司,彼此交換經驗,像我這趟臺灣行結束後,我會馬上去賓州的匹茲堡分公司輪調一個星期。

Google的80/20工作法則
錢世豐:Google的員工可以利用20%的工作時間做任何想做的事,是真的嗎?

Neal:真的!我當初的20%時間是做一個行事曆的應用,後來發展成為今日的Google Calendar。它現在變成我的80%,而目前來臺灣就是用我的20%時間。

Joshua:很多年來,我的20%是更新《Effective Java》。



Google專任工程師Neal Gafter(左)與Google首席Java架構師Joshua Bloch(右),兩人利用Google的20%自由工作時間寫出《Java Puzzlers》暢銷書。攝影/賴基能

AJAX仍在演進,是未來的趨勢
朱仲傑:我們知道AJAX是從Google Map點燃的,而AJAX現在正火紅,Java Script有個大問題是相容性,有些語法只能在IE或Firefox上使用。Gmail可以在很多瀏覽器上使用,但Google Calendar就只能在IE和Firefox上用。

Neal:不,它現在可以在Mac OS上使用了。

使用AJAX撰寫應用程式,會比用Java用戶端技術撰寫應用程式花上更多力氣。真的,因為可攜性不會是免費奉送的。另一方面,並不是很多人的機器都裝有Java,但每個人都有瀏覽器,Firefox到處都有,而IE更佔有90%,只要有Windows就有IE。實務上來說,當你能夠在這兩個瀏覽器上執行,對大多數人來說,就具備了可攜性。

Joshua:我看AJAX是比以前的HTML更豐富,但還是沒有比傳統的Windows、Mac OS或Unix上的應用程式豐富。不過另方面,AJAX一直在改進,比以往更容易移植、更容易維護,這些背後技術的演進,讓使用者能自然地使用,不會感覺到技術端有什麼不同,這些都是好事。我有時候對AJAX做不到一些功能而感到挫折,但另一方面以一些商業的角度來看,選AJAX還是正確的決定。

Neal:AJAX的應用還是無法像一般例如Java開發的程式來得豐富,其中部分原因是技術太新,所以不那麼普及,沒有那麼多函式庫可以讓開發變得簡單,而且AJAX還是有效能上的問題,Java Script還是一個直譯式的語言,如果未來有更豐富複雜的以AJAX 開發的應用程式,我們還是需要有更好的Java Script解譯方法。我還是會很高興看到越來越多用戶端在使用Java。

Joshua:會的,大約60%的電腦出廠時就裝好Java 5的JRE(Java Runtime Environment)。

丁彬:Google以併購的方式取得GWT,Google還有其他的AJAX 工具集的計畫嗎?

Neal:Google很多專案採用AJAX技術,不過如果你的問題,是Google有沒有其他的AJAX Toolkit產品,就我所知是沒有。

Joshua:就我所知,也是沒有。就算有,我也不被允許談論這些計畫。

葉秉哲:所以你們會用嗎?(指GWT)

Neal:我們現在沒有,因Google Calendar在AJAX誕生之前就開始開發了,但如果開發時就有這些工具,我想我會採用。至於我現在是否會將Google Calendar轉由AJAX工具開發,這聽起來就有點費功夫了,可能不太值得。

葉秉哲:如果有新的應用,你們可能就會採用?

Neal:如果有新的計畫是以AJAX為目標,我想會採用的,因為有現成的可以利用。

Joshua:如果有時間我也會想用,不過有點困難。

葉秉哲:當Google在使用腳本語言時,為什麼會選擇Python,而不是Perl 之類的語言?

Joshua:當我們任職於Google時,Google已經採用Python,所以我並不知道最初的原因。在Google內部也有人使用Perl,不過只是用於個人的20%時間,屬於實驗性質,用來展示及說服個人的想法;當此計畫成長到有其他人加入時,還是得轉移到Google最常用的語言平臺上。

Google OS?只是揶揄微軟
朱仲傑:現在你們用的Java是哪一個版本?

Joshua:我很驕傲的說,現在我們已經用到Tiger,我是2004年7月4日加入Google,大約從2005年3月起,全公司就轉換到Tiger,這是在Tiger發表後沒多久的事。

Neal:我們已經完成JDK 1.6的測試,只要等JDK 1.6公佈後,我們也可以很快地轉換過去。

錢世豐:請問你們有自己的作業系統嗎?

Joshua:沒有,我們用Linux,我們在一些大規模的系統用Linux,但我們沒有自己的作業系統。

Neal:但我在自己的筆記型電腦上做了一個「Google OS」的螢幕保護程式,當我去演講開會的時候,大家都會看到我用「Google OS」。

王文彬:這是行銷?

Neal:是我們在揶揄微軟。

C#與Java各擅勝場
王建興:.NET是Java的主要競爭對手,現在也有開放源碼的.NET Framework可執行於Linux,它叫做Mono,你對Mono的看法?

Neal:嗯,它還不完整,是吧。有很多開放源碼的Java實作,也都不完整。很多Java的實作,都只有60%~70%的API支援,就算是90%都不夠好。那意謂著我可能會呼叫到一個不能起作用的方法。要支援全部才算有用。

Joshua:像我在Java的委員會,我還是覺得即使是.NET公開的API也不會很實際,Java有完整公開的計畫,主要就是要把所有函式庫都公開。

丁彬:那C#呢?C#的目的之一,就是要從Java社群挖走一些人,你們覺得呢?

Neal:我認為C#的確有一些好的想法。他們有自由選擇不需要保留向下相容性,舉例來說,當他們在語言中決定增加Generics語法時,他們建立了另一套全新的Collections API,身為程式設計員或是軟體廠商,必須在原本的和新的介面中,選擇一套來使用。以語言設計的角度來說,這樣做可能比Java設計者的選擇來得好,C#有這樣的自由可以讓設計者更大幅地改良語言的設計。

Joshua:我這樣說好了,「模仿」絕對是高度的讚美,何況是像C#如此公開的模仿。我不全然認為C#挖走了Java 的開發者。當你的目標是在Windows 上開發時,為了某些明顯的因素,使用C#或C++會面臨較少的整合性問題。

Neal:在Java的Generics中,有Wildcard的語法,我認為這是一個極為重要的部分,它讓程式設計人員更有威力、可以更靈活的表達設計理念。C#中就沒有這項功能,這也可能和當時的程式語言社群中對於 Wildcard 的需求討論還沒這麼熱烈有關,但我認為對C#來說,要增加這項功能已經太晚了。

Joshua:每一種程式語言都有它的優點和缺點,我或許有點偏頗,不過我的確很喜歡Java中這些新的功能。C# 中有太多過去Pascal及C++的影子。

王建興:以一個架構師的角度,可以區別一下Java和.NET的核心的不同嗎?

Joshua:我對.NET的核心沒有那麼熟,他們出現得比Java晚,所以可以針對Java的缺點改進,但如果你有看《Java Puzzlers》,你會發現還是有很多Puzzlers也同樣在C#出現。

朱仲傑:《Java Puzzlers》、《Effective Java》的第二版何時會問市呢?

Joshua:我在今年舊金山的JavaOne大會,於數千人面前保證,在下一屆JavaOne之前,會推出《Effective Java 2nd edition》,我會繼續努力朝著明年出版的目標努力。至於《Java Puzzlers》的第二版,希望不會再有那麼多難題了。因為越多難題與陷阱其實代表這個語言的缺點越多,只要我們不要再加入更多不好的語言特性在Java中,就不會有太多新的難題。


Java太肥了嗎?
王文彬:我對Java的未來方向有點好奇,Java已經很成熟,若我們再持續加入新功能,會不會讓語言太過龐大而失焦?

Joshua:會的,每個語言都有生命週期,有些語言持續增加新功能,導致該語言後來很難寫、很醜、難以使用,很多語言後來變成這樣,而我會盡力讓Java不變成這樣。我相信現在的Java已經是一個相當完整的語言,雖然還是有很多好的功能可以加入,但將這些東西一股腦全部加進來絕對是錯的。

Neal:我覺得在JDK 5就加入了太多功能。

王文彬:是的,像annotation 就是。

Joshua:但是使用者喜歡耶!我是annotation技術團隊的負責人,雖然這不是我心目中最想要的功能,但是我相信它對一般的Java工程師幫助很大。

Neal:有兩個我覺得後悔的功能:Static Import 和 Varargs。
Joshua:其實Static Import我覺得並沒有太傷害Java語言本身,因為根本就很少用。Varargs在特定的時候,確實會造成無法預期的結果。

錢世豐:你們最喜歡哪些Java新功能?

Joshua:我覺得enum這個小功能很重要,但當初沒有規劃好enum的比較,現在的規格允許我們可以比較兩個enum是否相等,但是卻不能使用像是大於、小於來比較兩個enum。另外像switch不能使用在String 上,很多人需要這個功能,沒理由Java無法做到。我覺得像這樣的小功能,是我很希望未來的Java再加入。

Neal:我自己認為有個功能是現在Java缺乏的:closures。

Web 2.0的定義始終是分歧的
朱仲傑:你們怎麼看Web 2.0呢?會覺得是泡沫嗎?

Joshua:我覺得這只是大家為現在的網路現象取個名字,就像從寫HTML開始,後來有Java Script,現在有公開的API,就可以做一些Mashups(混搭程式),我覺得一些Mashups很棒,像房屋網站Zillow.com就整合Google Map,這些東西都很棒,不過這比較像是行銷人員把這種現象取了個Web 2.0的名詞。

Neal:我不知道大家怎麼看待Web 2.0的意義,我認為這是把網路應用服務集合的意思,其中有些想法也是行之有年,像Amazon上可以寫書評。不過我想Web 2.0會是現在和未來一些網路服務的趨勢。

Joshua:那如果說Web 2.0是平臺呢?當我們說 Web 2.0是一些以網路服務為基礎的平臺,像Amazon、Google、eBay有提供 API,這樣一些網路服務就可以利用其他網站現有的平臺提供更多應用。

錢世豐:我覺得直到有租屋網站和Google Map整合後,大家才開始注意Web 2.0的現象。

Neal:我覺得大家對Web 2.0的定義都不大相同,幾年後如果Web 2.0成功了,這個詞就還會存在;如果沒有成功,就不會再叫Web 2.0了。

錢世豐:有些人說GMail與Google Map是 Web 2.0 的開始。
Joshua:Google Map無疑的是屬於Web 2.0服務,你看Zillow.com就是整合Google Map的應用。整理⊙李延華

Joshua Bloch
Google首席架構師,著有《Effective Java》、《Java Puzzlers》“當你要建立一個像Google這樣規模的搜尋服務,你可以想像會有多少問題,但是我們就是要持續的讓服務運作下去,盡量讓系統可以自動修復,不要造成延遲。”



Neal Gafter
Google專任工程師兼Java講師,著有《Java Puzzlers》“使用AJAX撰寫應用程式,會比用Java用戶端技術撰寫應用程式花上更多力氣,因為可攜性不會是免費奉送的。”

2010年8月19日

記錄:人月神話

曾經接過急迫且大型的網站建置專案,但是進行上卻還算順利,這是由於系統工程師十分有經驗,在分工、次序與掌控上,都做了很好的安排。
但在最近的一個朋友的經驗中,卻是遇到網站的機制改版,在時程與資源運用上卻不斷延遲,還好是內部機制,不用對客戶交代,否則所蒙受的將不是只有金錢上的損失。
我們知道時程延遲,但原因在哪裡?為何會發生?
人月神話的第二章,應該是這本書的精華,也是書名的由來,看完這個章節,發現作者道出很多軟體專案上的管理關鍵。而軟體專案是不可以完全套用傳統產業的專案管理方式。
第一個關鍵 樂觀
首先作者提出了一件常常發生的事情,這也是一個朋友身上發生的故事:『程式設計師都是樂觀的傢伙。』而且我發現越年輕越樂觀,但是無可避免的這是個年輕的產業,很少會遇到很有經驗的程式設計人員,這些年輕人,即使告訴他這樣是會有問題的,他也不改其樂觀。
所以『他們所做的第一個錯誤假設是一切都會進行的很順利』可是沒有想到一個bug,就可以花上一天時間也無法解決。然後,就延遲了後面本來假設可以順利進行的部分。
第二個關鍵 人月
人月,是我們預估和排定時程用的,但是作者提出一個前提:『使用人月必須要在人力與工時可以互換的狀況下。而且要當工作可以切割、投入工作的人不用溝通,人力與工時才能互換。』就是說要可以互換,才能使一個人做30天,與30個人做一天的結果一樣,不然單純用人月去估算時程,一定會有誤差。
為什麼呢?首先軟體專案有其連續性,作者舉了個例子,蠻傳神的,他說,生小孩就是要九個月,你叫多少個媽來生都是一樣。第二點是因為即使工作可切割,但是需要溝通,越多人投入就需要多的溝通與教育的時間,所以一個人做30天的工作不可能用30個人做一天就完成。
所以,Brooks定論說,在一個時程已經落後的軟體專案中增加人手,只會讓它更加落後。
因此,要讓一個專案順利進行,首先要有良好的時程規劃,考慮所有的因素,而且,程式人員要有勇氣不要妥協,堅持自己預估的時程。就像是廚師一樣,即使外面的客人在等著上菜,一隻雞要烤多久就要烤多久,不能因為妥協就用大火烤焦了。
外科手術團隊
這個篇章看完突然想到研究所的專案管理課程,三個學分,卻是我們一個學期的研究重心,四個實際運作的專案,大家分組進行,教授不斷的製造專案危機,像是公司被併購、人員流失、專案形式改變、客戶不斷施以壓力等等,但最後大家還是順利的結案。
這是個難以忘懷的經驗,組員們的默契在起初的確造成危機,但是,由於大家的素質還算整齊,很快就可以建立溝通的語言。加上大家熟悉協同科技概念與網路溝通,可以做到分時分地作業,也使得專案進行有效率。
但當我看完這篇外科手術團隊後,我發現其實還有個成功的關鍵在其中,就是同學們各有專長,所以,分工很容易,又因為有選出leader作為掌控者,所以,不會亂。大家不會做重複的事情。每個人按照時程交差,就可以順利完成。
作者認為一個外科手術中,有操刀的大夫,有護士、有麻醉師等等,各司其職。而不是像屠夫團隊,每個人都得拿刀。一個團隊中只有一個人操刀,其他人扮演支援的角色。這樣整件事情就會出自一個腦袋不會亂,也不需要不斷的溝通協調。
這兩個篇章是我比較有感覺的,寫出來跟大家分享。
最後,作者還有一個人月的概念我覺得很重要,這會影響到組織的公平性:一個好手,花一天可以做完,但同樣一件事,庸才卻需要三四天以上,甚至加班趕工,也許還領加班費。如果照這樣去計算人月,一定會差之千里。

2010年6月10日

[轉貼記錄] hibernate學習筆記

原创 hibernate學習筆記:hibernate中的Cache管理 收藏

Hibernate 中實現了良好的Cache 機制,我們可以借助Hibernate 內部的Cache迅速提高系統數據讀取性能。

需要注意的是:Hibernate做為一個應用級的數據訪問層封裝,只能在其作用範圍內保持 Cache中數據的的有效性,也就是說,在我們的系統與第三方系統共享數據庫的情況下,Hibernate的Cache機制可能失效。一個很簡單的例子, 如果你用access修改了庫中的值,那麼這就不會更新JVM中的緩衝池,這就導致了贓數據的產生。

Hibernate 在本地JVM 中維護了一個緩衝池,並將從數據庫獲得的數據保存到池中以供下次重複使用(如果在Hibernate中數據發生了變動,Hibernate同樣也會更新池 中的數據版本)。此時,如果有第三方系統對數據庫進行了更改,那麼,Hibernate並不知道數據庫中的數據已經發生了變化,也就是說,池中的數據還是 修改之前的版本,下次讀取時,Hibernate會將此數據返回給上層代碼,從而導致潛在的問題。外部系統的定義,並非限於本系統之外的第三方系統,即使 在本系統中,如果出現了繞過Hibernate數據存儲機制的其他數據存取手段,那麼Cache的有效性也必須細加考量。如,在同一套系統中,基於 Hibernate和基於JDBC的兩種數據訪問方式並存,那麼通過JDBC更新數據庫的時候,Hibernate同樣無法獲知數據更新的情況,從而導致 髒數據的出現。

基於Java 的Cache 實現,最簡單的莫過於HashTable,hibernate 提供了基於Hashtable 的Cache 實現機制,不過,由於其性能和功能上的侷限,僅供開發調試中使用。同時,Hibernate 還提供了面向第三方Cache 實現的接口,如JCS、EHCache、OSCache、JBoss Cache、SwarmCache等。

Hibernate中的 Cache大致分為兩層,第一層Cache在Session實現,屬於事務級數據緩衝,一旦事務結束,這個Cache 也就失效。此層Cache 為內置實現,無需我們進行干涉。第二層Cache,是Hibernate 中對其實例範圍內的數據進行緩存的管理容器。

我們主要學習第二層 Cache。

Hibernate早期版本中採用了JCS(Java Caching System -Apache Turbine項目中的一個子項目)作為默認的第二層Cache實現。由於JCS的發展停頓,以及其內在的一些問題(在某些情況下,可能導致內存洩漏以及 死鎖),新版本的Hibernate已經將JCS去除,並用EHCache作為其默認的第二級Cache實現。相對JCS,EHCache更加穩定,並具 備更好的緩存調度性能,缺陷是目前還無法做到分佈式緩存,如果我們的系統需要在多台設備上部署,並共享同一個數據庫,必須使用支持分佈式緩存的Cache 實現(如JCS、JBossCache)以避免出現不同系統實例之間緩存不一致而導致髒數據的情況。Hibernate對Cache進行了良好封裝,透明 化的Cache機制使得我們在上層結構的實現中無需面對繁瑣的Cache維護細節。

目前Hibernate支持的Cache實現有:

HashTable:net.sf.hibernate.cache.HashtableCacheProvider 支持查詢緩衝。

EHCache:net.sf.ehcache.hibernate.Provider 支持查詢緩衝。

OSCache:net.sf.hibernate.cache.OSCacheProvider 支持查詢緩衝。

SwarmCache:net.sf.hibernate.cache.SwarmCacheProvider 支持集群。

JBossCache:net.sf.hibernate.cache.TreeCacheProvider 支持集群。

其 中SwarmCache 提供的是invalidation 方式的分佈式緩存,即當集群中的某個節點更新了緩存中的數據,即通知集群中的其他節點將此數據廢除,之後各個節點需要用到這個數據的時候,會重新從數據庫 中讀入並填充到緩存中。而JBossCache提供的是Reapplication式的緩衝,即如果集群中某個節點的數據發生改變,此節點會將發生改變的 數據的最新版本複製到集群中的每個節點中以保持所有節點狀態一致。

使用第二層Cache,需要在hibernate的配置文件進行配置(省 略)主要介紹一下cache策略

cache策略可選值有以下幾種:

1. read-only 只讀。
2. read-write 可讀可寫。
3. nonstrict-read-write 如果程序對並發數據修改要求不是非常嚴格,只是偶爾需要更新數據,可以採用本選項,以減少無謂的檢查,獲得較好的性能。
4. transactional 事務性cache。在事務性Cache 中,Cache 的相關操作也被添加到事務之中,如果由於某種原因導致事務失敗,我們可以連同緩衝池中的數據一同回滾到事務開始之前的狀態。目前Hibernate 內置的Cache 中,只有JBossCache 支持事務性的Cache實現。

其他參數簡介:

maxElementsInMemory="10000" //Cache中最大允許保存的數據數量
eternal="false" //Cache中數據是否為常量
timeToIdleSeconds="120" //緩存數據鈍化時間
timeToLiveSeconds="120" //緩存數據的生存時間
overflowToDisk="true" //內存不足時,是否啟 用磁盤緩存

需要注意的是Hibernate 的數據庫查詢機制。我們從查詢結果中取出數據的時候,用的最多的是兩個方法:Query.list();Query.iterate();

對 於list方法而言,實際上Hibernate是通過一條Select SQL獲取所有的記錄。並將其讀出,填入到POJO中返回。

而iterate 方法,則是首先通過一條Select SQL 獲取所有符合查詢條件的記錄的id,再對這個id 集合進行循環操作,通過單獨的Select SQL 取出每個id 所對應的記錄,之後填入POJO中返回。

也就是說,對於list 操作,需要一條SQL 完成。而對於iterate 操作,需要n+1條SQL。

看上去iterate方法似乎有些多餘,但在不同的情況下確依然有其獨特的功效,如對海量數據的查詢,如果用 list方法將結果集一次取出,內存的開銷可能無法承受。另一方面,對於我們現在的Cache機制而言,list方法將不會從Cache中讀取數據,它總 是一次性從數據庫中直接讀出所有符合條件的記錄。而iterate 方法因為每次根據id獲取數據,這樣的實現機制也就為從Cache讀取數據提供了可能,hibernate首先會根據這個id 在本地Cache 內尋找對應的數據,如果沒找到,再去數據庫中檢索。

2010年6月8日

Servlet 3.0 新特性詳解

Servlet 3.0 新特性詳解

張 建平, 架構師, iSoftStone Co., Ltd
張建平,長期從事於 Java 開發的工作,對 Java EE 規範體系有深入研究,最近主要關注高並發的 Web 架構設計以及把既有項目 SOA 化的問題。

簡介: Servlet 是 Java EE 規範體系的重要組成部分,也是 Java 開發人員必須具備的基礎技能,Servlet 3.0 是 Servlet 規範的最新版本。本文主要介紹了 Servlet 3.0 引入的若干重要新特性,包括異步處理、新增的註解支持、可插性支持等等,為讀者順利向新版本過渡掃清障礙。

發佈日期: 2010 年 4 月 23 日
級別: 初級
1 star2 stars3 stars4 stars5 stars 平均分 (共 23 個評分 )

Servlet 3.0 新特性概述

Servlet 3.0 作為 Java EE 6 規範體系中一員,隨著 Java EE 6 規範一起發佈。該版本在前一版本(Servlet 2.5)的基礎上提供了若干新特性用於簡化 Web 應用的開發和部署。其中有幾項特性的引入讓開發者感到非常興奮,同時也獲得了 Java 社區的一片讚譽之聲:

1. 異步處理支持:有了該特性,Servlet 線程不再需要一直阻塞,直到業務處理完畢才能再輸出響應,最後才結束該 Servlet 線程。在接收到請求之後,Servlet 線程可以將耗時的操作委派給另一個線程來完成,自己在不生成響應的情況下返回至容器。針對業務處理較耗時的情況,這將大大減少服務器資源的佔用,並且提高並發處理速度。
2. 新增的註解支持:該版本新增了若干註解,用於簡化 Servlet、過濾器(Filter)和監聽器(Listener)的聲明,這使得 web.xml 部署描述文件從該版本開始不再是必選的了。
3. 可插性支持:熟悉 Struts2 的開發者一定會對其通過插件的方式與包括 Spring 在內的各種常用框架的整合特性記憶猶新。將相應的插件封裝成 JAR 包並放在類路徑下,Struts2 運行時便能自動加載這些插件。現在 Servlet 3.0 提供了類似的特性,開發者可以通過插件的方式很方便的擴充已有 Web 應用的功能,而不需要修改原有的應用。

下面我們將逐一講解這些新特性,通過下面的學習,讀者將能夠明晰瞭解 Servlet 3.0 的變化,並能夠順利使用它進行日常的開發工作。

回頁首

異步處理支持

Servlet 3.0 之前,一個普通 Servlet 的主要工作流程大致如下:首先,Servlet 接收到請求之後,可能需要對請求攜帶的數據進行一些預處理;接著,調用業務接口的某些方法,以完成業務處理;最後,根據處理的結果提交響應,Servlet 線程結束。其中第二步的業務處理通常是最耗時的,這主要體現在數據庫操作,以及其它的跨網絡調用等,在此過程中,Servlet 線程一直處於阻塞狀態,直到業務方法執行完畢。在處理業務的過程中,Servlet 資源一直被佔用而得不到釋放,對於並發較大的應用,這有可能造成性能的瓶頸。對此,在以前通常是採用私有解決方案來提前結束 Servlet 線程,並及時釋放資源。

Servlet 3.0 針對這個問題做了開創性的工作,現在通過使用 Servlet 3.0 的異步處理支持,之前的 Servlet 處理流程可以調整為如下的過程:首先,Servlet 接收到請求之後,可能首先需要對請求攜帶的數據進行一些預處理;接著,Servlet 線程將請求轉交給一個異步線程來執行業務處理,線程本身返回至容器,此時 Servlet 還沒有生成響應數據,異步線程處理完業務以後,可以直接生成響應數據(異步線程擁有 ServletRequest 和 ServletResponse 對象的引用),或者將請求繼續轉發給其它 Servlet。如此一來, Servlet 線程不再是一直處於阻塞狀態以等待業務邏輯的處理,而是啟動異步線程之後可以立即返回。

異步處理特性可以應用於 Servlet 和過濾器兩種組件,由於異步處理的工作模式和普通工作模式在實現上有著本質的區別,因此默認情況下,Servlet 和過濾器並沒有開啟異步處理特性,如果希望使用該特性,則必須按照如下的方式啟用:

1. 對於使用傳統的部署描述文件 (web.xml) 配置 Servlet 和過濾器的情況,Servlet 3.0 為 標籤增加了 子標籤,該標籤的默認取值為 false,要啟用異步處理支持,則將其設為 true 即可。以 Servlet 為例,其配置方式如下所示:


DemoServlet
footmark.servlet.Demo Servlet
true



2. 對於使用 Servlet 3.0 提供的 @WebServlet 和 @WebFilter 進行 Servlet 或過濾器配置的情況,這兩個註解都提供了 asyncSupported 屬性,默認該屬性的取值為 false,要啟用異步處理支持,只需將該屬性設置為 true 即可。以 @WebFilter 為例,其配置方式如下所示:

@WebFilter(urlPatterns = "/demo",asyncSupported = true)
public class DemoFilter implements Filter{...}


一個簡單的模擬異步處理的 Servlet 示例如下:

@WebServlet(urlPatterns = "/demo", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("進入Servlet的時間:" + new Date() + ".");
out.flush();

//在子線程中執行業務調用,並由其負責輸出響應,主線程退出
AsyncContext ctx = req.startAsync();
new Thread(new Executor(ctx)).start();

out.println("結束Servlet的時間:" + new Date() + ".");
out.flush();
}
}

public class Executor implements Runnable {
private AsyncContext ctx = null;
public Executor(AsyncContext ctx){
this.ctx = ctx;
}

public void run(){
try {
//等待十秒鐘,以模擬業務方法的執行
Thread.sleep(10000);
PrintWriter out = ctx.getResponse().getWriter();
out.println("業務處理完畢的時間:" + new Date() + ".");
out.flush();
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
}


除此之外,Servlet 3.0 還為異步處理提供了一個監聽器,使用 AsyncListener 接口表示。它可以監控如下四種事件:

1. 異步線程開始時,調用 AsyncListener 的 onStartAsync(AsyncEvent event) 方法;
2. 異步線程出錯時,調用 AsyncListener 的 onError(AsyncEvent event) 方法;
3. 異步線程執行超時,則調用 AsyncListener 的 onTimeout(AsyncEvent event) 方法;
4. 異步執行完畢時,調用 AsyncListener 的 onComplete(AsyncEvent event) 方法;

要註冊一個 AsyncListener,只需將準備好的 AsyncListener 對象傳遞給 AsyncContext 對象的 addListener() 方法即可,如下所示:

AsyncContext ctx = req.startAsync();
ctx.addListener(new AsyncListener() {
public void onComplete(AsyncEvent asyncEvent) throws IOException {
// 做一些清理工作或者其他
}
...
});


回頁首

新增的註解支持

Servlet 3.0 的部署描述文件 web.xml 的頂層標籤 有一個 metadata-complete 屬性,該屬性指定當前的部署描述文件是否是完全的。如果設置為 true,則容器在部署時將只依賴部署描述文件,忽略所有的註解(同時也會跳過 web-fragment.xml 的掃瞄,亦即禁用可插性支持,具體請看後文關於 可插性支持的講解);如果不配置該屬性,或者將其設置為 false,則表示啟用註解支持(和可插性支持)。

@WebServlet

@WebServlet 用於將一個類聲明為 Servlet,該註解將會在部署時被容器處理,容器將根據具體的屬性配置將相應的類部署為 Servlet。該註解具有下表給出的一些常用屬性(以下所有屬性均為可選屬性,但是 vlaue 或者 urlPatterns 通常是必需的,且二者不能共存,如果同時指定,通常是忽略 value 的取值):

表 1. @WebServlet 主要屬性列表
屬性名 類型 描述
name String 指定 Servlet 的 name 屬性,等價於 。如果沒有顯式指定,則該 Servlet 的取值即為類的全限定名。
value String[] 該屬性等價於 urlPatterns 屬性。兩個屬性不能同時使用。
urlPatterns String[] 指定一組 Servlet 的 URL 匹配模式。等價於 標籤。
loadOnStartup int 指定 Servlet 的加載順序,等價於 標籤。
initParams WebInitParam[] 指定一組 Servlet 初始化參數,等價於 標籤。
asyncSupported boolean 聲明 Servlet 是否支持異步操作模式,等價於 標籤。
description String 該 Servlet 的描述信息,等價於 標籤。
displayName String 該 Servlet 的顯示名,通常配合工具使用,等價於 標籤。

下面是一個簡單的示例:

@WebServlet(urlPatterns = {"/simple"}, asyncSupported = true,
loadOnStartup = -1, name = "SimpleServlet", displayName = "ss",
initParams = {@WebInitParam(name = "username", value = "tom")}
)
public class SimpleServlet extends HttpServlet{ … }


如此配置之後,就可以不必在 web.xml 中配置相應的 元素了,容器會在部署時根據指定的屬性將該類發佈為 Servlet。它的等價的 web.xml 配置形式如下:


ss
SimpleServlet
footmark.servlet.SimpleServlet
-1
true

username
tom



SimpleServlet
/simple



@WebInitParam

該註解通常不單獨使用,而是配合 @WebServlet 或者 @WebFilter 使用。它的作用是為 Servlet 或者過濾器指定初始化參數,這等價於 web.xml 中 子標籤。@WebInitParam 具有下表給出的一些常用屬性:

表 2. @WebInitParam 的常用屬性
屬性名 類型 是否可選 描述
name String 否 指定參數的名字,等價於
value String 否 指定參數的值,等價於
description String 是 關於參數的描述,等價於

@WebFilter

@WebFilter 用於將一個類聲明為過濾器,該註解將會在部署時被容器處理,容器將根據具體的屬性配置將相應的類部署為過濾器。該註解具有下表給出的一些常用屬性 ( 以下所有屬性均為可選屬性,但是 value、urlPatterns、servletNames 三者必需至少包含一個,且 value 和 urlPatterns 不能共存,如果同時指定,通常忽略 value 的取值 ):

表 3. @WebFilter 的常用屬性
屬性名 類型 描述
filterName String 指定過濾器的 name 屬性,等價於
value String[] 該屬性等價於 urlPatterns 屬性。但是兩者不應該同時使用。
urlPatterns String[] 指定一組過濾器的 URL 匹配模式。等價於 標籤。
servletNames String[] 指定過濾器將應用於哪些 Servlet。取值是 @WebServlet 中的 name 屬性的取值,或者是 web.xml 中 的取值。
dispatcherTypes DispatcherType 指定過濾器的轉發模式。具體取值包括:
ASYNC、ERROR、 FORWARD、INCLUDE、REQUEST。
initParams WebInitParam[] 指定一組過濾器初始化參數,等價於 標籤。
asyncSupported boolean 聲明過濾器是否支持異步操作模式,等價於 標籤。
description String 該過濾器的描述信息,等價於 標籤。
displayName String 該過濾器的顯示名,通常配合工具使用,等價於 標籤。

下面是一個簡單的示例:

@WebFilter(servletNames = {"SimpleServlet"},filterName="SimpleFilter")
public class LessThanSixFilter implements Filter{...}


如此配置之後,就可以不必在 web.xml 中配置相應的 元素了,容器會在部署時根據指定的屬性將該類發佈為過濾器。它等價的 web.xml 中的配置形式為:


SimpleFilter
xxx


SimpleFilter
SimpleServlet



@WebListener

該註解用於將類聲明為監聽器,被 @WebListener 標註的類必須實現以下至少一個接口:

* ServletContextListener
* ServletContextAttributeListener
* ServletRequestListener
* ServletRequestAttributeListener
* HttpSessionListener
* HttpSessionAttributeListener

該註解使用非常簡單,其屬性如下:

表 4. @WebListener 的常用屬性
屬性名 類型 是否可選 描述
value String 是 該監聽器的描述信息。

一個簡單示例如下:

@WebListener("This is only a demo listener")
public class SimpleListener implements ServletContextListener{...}


如此,則不需要在 web.xml 中配置 標籤了。它等價的 web.xml 中的配置形式如下:


footmark.servlet.SimpleListener



@MultipartConfig

該註解主要是為了輔助 Servlet 3.0 中 HttpServletRequest 提供的對上傳文件的支持。該註解標註在 Servlet 上面,以表示該 Servlet 希望處理的請求的 MIME 類型是 multipart/form-data。另外,它還提供了若干屬性用於簡化對上傳文件的處理。具體如下:

表 5. @MultipartConfig 的常用屬性
屬性名 類型 是否可選 描述
fileSizeThreshold int 是 當數據量大於該值時,內容將被寫入文件。
location String 是 存放生成的文件地址。
maxFileSize long 是 允許上傳的文件最大值。默認值為 -1,表示沒有限制。
maxRequestSize long 是 針對該 multipart/form-data 請求的最大數量,默認值為 -1,表示沒有限制。

回頁首

可插性支持

如果說 3.0 版本新增的註解支持是為了簡化 Servlet/ 過濾器 / 監聽器的聲明,從而使得 web.xml 變為可選配置, 那麼新增的可插性 (pluggability) 支持則將 Servlet 配置的靈活性提升到了新的高度。熟悉 Struts2 的開發者都知道,Struts2 通過插件的形式提供了對包括 Spring 在內的各種開發框架的支持,開發者甚至可以自己為 Struts2 開發插件,而 Servlet 的可插性支持正是基於這樣的理念而產生的。使用該特性,現在我們可以在不修改已有 Web 應用的前提下,只需將按照一定格式打成的 JAR 包放到 WEB-INF/lib 目錄下,即可實現新功能的擴充,不需要額外的配置。

Servlet 3.0 引入了稱之為「Web 模塊部署描述符片段」的 web-fragment.xml 部署描述文件,該文件必須存放在 JAR 文件的 META-INF 目錄下,該部署描述文件可以包含一切可以在 web.xml 中定義的內容。JAR 包通常放在 WEB-INF/lib 目錄下,除此之外,所有該模塊使用的資源,包括 class 文件、配置文件等,只需要能夠被容器的類加載器鏈加載的路徑上,比如 classes 目錄等。

現在,為一個 Web 應用增加一個 Servlet 配置有如下三種方式 ( 過濾器、監聽器與 Servlet 三者的配置都是等價的,故在此以 Servlet 配置為例進行講述,過濾器和監聽器具有與之非常類似的特性 ):

* 編寫一個類繼承自 HttpServlet,將該類放在 classes 目錄下的對應包結構中,修改 web.xml,在其中增加一個 Servlet 聲明。這是最原始的方式;
* 編寫一個類繼承自 HttpServlet,並且在該類上使用 @WebServlet 註解將該類聲明為 Servlet,將該類放在 classes 目錄下的對應包結構中,無需修改 web.xml 文件。
* 編寫一個類繼承自 HttpServlet,將該類打成 JAR 包,並且在 JAR 包的 META-INF 目錄下放置一個 web-fragment.xml 文件,該文件中聲明了相應的 Servlet 配置。web-fragment.xml 文件示例如下:




fragment
footmark.servlet.FragmentServlet


fragment
/fragment




從上面的示例可以看出,web-fragment.xml 與 web.xml 除了在頭部聲明的 XSD 引用不同之外,其主體配置與 web.xml 是完全一致的。

由於一個 Web 應用中可以出現多個 web-fragment.xml 聲明文件,加上一個 web.xml 文件,加載順序問題便成了不得不面對的問題。Servlet 規範的專家組在設計的時候已經考慮到了這個問題,並定義了加載順序的規則。

web-fragment.xml 包含了兩個可選的頂層標籤,,如果希望為當前的文件指定明確的加載順序,通常需要使用這兩個標籤, 主要用於標識當前的文件,而 則用於指定先後順序。一個簡單的示例如下:


FragmentA


FragmentB
FragmentC





...



如上所示, 標籤的取值通常是被其它 web-fragment.xml 文件在定義先後順序時引用的,在當前文件中一般用不著,它起著標識當前文件的作用。

標籤內部,我們可以定義當前 web-fragment.xml 文件與其他文件的相對位置關係,這主要通過 子標籤來實現的。在這兩個子標籤內部可以通過 標籤來指定相對應的文件。比如:


FragmentB
FragmentC



以上片段則表示當前文件必須在 FragmentB 和 FragmentC 之後解析。 的使用於此相同,它所表示的是當前文件必須早於 標籤裡所列出的 web-fragment.xml 文件。

除了將所比較的文件通過 中列出之外,Servlet 還提供了一個簡化的標籤 。它表示除了當前文件之外的其他所有的 web-fragment.xml 文件。該標籤的優先級要低於使用 明確指定的相對位置關係。

回頁首

ServletContext 的性能增強

除了以上的新特性之外,ServletContext 對象的功能在新版本中也得到了增強。現在,該對象支持在運行時動態部署 Servlet、過濾器、監聽器,以及為 Servlet 和過濾器增加 URL 映射等。以 Servlet 為例,過濾器與監聽器與之類似。ServletContext 為動態配置 Servlet 增加了如下方法:

* ServletRegistration.Dynamic addServlet(String servletName,Class servletClass)
* ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
* ServletRegistration.Dynamic addServlet(String servletName, String className)
* T createServlet(Class clazz)
* ServletRegistration getServletRegistration(String servletName)
* Map getServletRegistrations()

其中前三個方法的作用是相同的,只是參數類型不同而已;通過 createServlet() 方法創建的 Servlet,通常需要做一些自定義的配置,然後使用 addServlet() 方法來將其動態註冊為一個可以用於服務的 Servlet。兩個 getServletRegistration() 方法主要用於動態為 Servlet 增加映射信息,這等價於在 web.xml( 抑或 web-fragment.xml) 中使用 標籤為存在的 Servlet 增加映射信息。

以上 ServletContext 新增的方法要麼是在 ServletContextListener 的 contexInitialized 方法中調用,要麼是在 ServletContainerInitializer 的 onStartup() 方法中調用。

ServletContainerInitializer 也是 Servlet 3.0 新增的一個接口,容器在啟動時使用 JAR 服務 API(JAR Service API) 來發現 ServletContainerInitializer 的實現類,並且容器將 WEB-INF/lib 目錄下 JAR 包中的類都交給該類的 onStartup() 方法處理,我們通常需要在該實現類上使用 @HandlesTypes 註解來指定希望被處理的類,過濾掉不希望給 onStartup() 處理的類。

回頁首

HttpServletRequest 對文件上傳的支持

此前,對於處理上傳文件的操作一直是讓開發者頭疼的問題,因為 Servlet 本身沒有對此提供直接的支持,需要使用第三方框架來實現,而且使用起來也不夠簡單。如今這都成為了歷史,Servlet 3.0 已經提供了這個功能,而且使用也非常簡單。為此,HttpServletRequest 提供了兩個方法用於從請求中解析出上傳的文件:

* Part getPart(String name)
* Collection getParts()

前者用於獲取請求中給定 name 的文件,後者用於獲取所有的文件。每一個文件用一個 javax.servlet.http.Part 對象來表示。該接口提供了處理文件的簡易方法,比如 write()、delete() 等。至此,結合 HttpServletRequest 和 Part 來保存上傳的文件變得非常簡單,如下所示:

Part photo = request.getPart("photo");
photo.write("/tmp/photo.jpg");
// 可以將兩行代碼簡化為 request.getPart("photo").write("/tmp/photo.jpg") 一行。


另外,開發者可以配合前面提到的 @MultipartConfig 註解來對上傳操作進行一些自定義的配置,比如限制上傳文件的大小,以及保存文件的路徑等。其用法非常簡單,故不在此贅述了。

需要注意的是,如果請求的 MIME 類型不是 multipart/form-data,則不能使用上面的兩個方法,否則將拋異常。

回頁首

總結

Servlet 3.0 的眾多新特性使得 Servlet 開發變得更加簡單,尤其是異步處理特性和可插性支持的出現,必將對現有的 MVC 框架產生深遠影響。雖然我們通常不會自己去用 Servlet 編寫控制層代碼,但是也許在下一個版本的 Struts 中,您就能切實感受到這些新特性帶來的實質性改變。

參考資料

學習

* JSR-000315 Java Servlet 3.0 規範:這裡除了可以下載 Servlet 3.0 的規範文檔,還可以瞭解關於與該規範相關的一些其他信息。

* GlassFish 項目主頁:可以在這裡現在 GlassFish V3 版本,這是 SUN 提供的 Java EE 6 規範的參考實現。

* 「Servlet 2.2 的新特徵」(developerWorks,2000 年 12 月):討論 Servlet 2.2 一些新的比較重要的特徵,並給出了一些簡單的例子來演示這些特徵的用途及用法。

* 「Java Servlet 技術簡介」(developerWorks,2004 年 12 月):本教程包括一個說明 servlet 基本概念的簡單例子,以及一個涉及更多內容的例子,它說明如何在小型的合同管理程序中更複雜地使用 servlet。

* 技術書店:瀏覽關於這些和其他技術主題的圖書。

* developerWorks Java 技術專區:數百篇關於 Java 編程各個方面的文章。

討論

* 加入 developerWorks 社區。

* 查看 developerWorks 博客 的最新信息。