拥有多年互联网和银行系统性能测试开发经验,对性能瓶颈诊断定位和优化领域有较多研究。 重回互联网行业,性能测试开发、自动化测试开发、Java开发

发布新日志

  • session怎样创建和销毁

    2013-01-06 17:11:06

    session怎样创建和销毁 ? how

    session是保存在内存中的,所以会有一些性能上的影响。因此本着这个原则,session是只有在使用到的时候才会被创建,如果始终没有用到session,这个session是永远不会被创建的。

    比如: 访问servlet ,只要你代码中没有 request.getSession()或request.getSession(true);这两行是等价的,那session是不会创建。又 当你访问静态页面时,根本不会生成servlet,所以也不会创建session。

    下面解释一些疑惑: session是第一次请求时创建的?

    大家都知道 jsp是被编译成servlet才执行的,问题就在jsp编译的过程。

    jsp中有个<%@ page session="true/false"%> 这个开关表示是否创建session,当你不写这行时,它会默认给你加上这句。所以会造成上面的疑惑。当然还有一些标签中可能有getSession()操作,会产生一些不必要的session。

    session只能在服务端销毁,有三种途径: 1,到达session的最大请求间隔时间时,2,session。invalidate()

  • java的synchronized关键字

    2013-01-06 16:36:12

    1.yield()/sleep()/join() java.lang.Thread


    join方法等待线程运行结束
    sleep方法使线程阻塞一定的时间
    yield方法使线程放弃一次调度机会


    2.wait()/notifyAll()/notify() java.lang.Object


    wait方法使线程阻塞直至其它线程调用notify或notifAll
    notifyAll方法唤醒所有等在对象锁的线程
    notify方法唤醒一个等在对象锁的线程


    3.sleep() 与 wait()


    两者都使线程阻塞,但sleep时当前线程不释放对象锁,wait时则释放对象锁

    4.中断

    java.lang.Thread:

    void interrupt(),改变中断状态
    static boolean interrupted(),检查并清除中断状态
    boolean isInterrupted(),检查中断状态

    Object.wait、Thread.sleep、Thread.join这些方法内部会不断检查中断状态的值,并抛出InterruptedException异常

    1. public class SynMutex {
    2. public static void main(String[] args) {
    3. Integer i = 0;
    4. synchronized (i) {
    5. i = 1;
    6. /*拥有对象锁的不是i
    7. *而是i所指向的对象
    8. *这就是产生异常的原因
    9. */
    10. i.notify();//IllegalMonitorStateException
    11. i.notifyAll();//IllegalMonitorStateException
    12. try {
    13. i.wait();//IllegalMonitorStateException
    14. } catch (Exception ex) {
    15. ex.printStackTrace();
    16. }
    17. }
    18. }
    19. /*
    20. * method1与method4效果是一样的:
    21. * 当存在一个线程执行到SynMutex类的任一个对象实例的synchronized域时,其它线程不能执行该类的synchronized域
    22. *
    23. * method2与method3效果是一样的:
    24. * 当存在一个线程执行到SynMutex类的某一个对象实例的synchronized域时,其它线程不能执行该对象实例的synchronized域
    25. * (其它对象实例的synchronized域可以执行)
    26. *
    27. * method1与method2用在方法上
    28. * method3与method4用在区块上
    29. */
    30. static public synchronized void method1() {
    31. }
    32. public synchronized void method2() {
    33. }
    34. public void method3() {
    35. synchronized (this) {
    36. }
    37. }
    38. public void method4() {
    39. synchronized (SynMutex.class) {
    40. }
    41. }
    42. }
  • Thread 多线程同步锁

    2013-01-06 15:47:44

    Java Thread 多线程同步、锁、通信

    线程同步、同步锁、死锁

    线程通信

    线程组和未处理异常

    Callable和Future

    12、线程同步
        当多个线程访问同一个数据时,非常容易出现线程安全问题。这时候就需要用线程同步
        Case:银行取钱问题,有以下步骤:
        A、用户输入账户、密码,系统判断是否登录成功
        B、用户输入取款金额
        C、系统判断取款金额是否大于现有金额
        D、如果金额大于取款金额,就成功,否则提示小于余额
     
        现在模拟2个人同时对一个账户取款,多线程操作就会出现问题。这时候需要同步才行;
        同步代码块:
        synchronized (object) {
            //同步代码
        }
        Java多线程支持方法同步,方法同步只需用用synchronized来修饰方法即可,那么这个方法就是同步方法了。
        对于同步方法而言,无需显示指定同步监视器,同步方法监视器就是本身this
        同步方法:
        public synchronized void editByThread() {
            //doSomething
        }
     
        需要用同步方法的类具有以下特征:
        A、该类的对象可以被多个线程访问
        B、每个线程调用对象的任意都可以正常的结束,返回正常结果
        C、每个线程调用对象的任意方法后,该对象状态保持合理状态
        不可变类总是线程安全的,因为它的对象状态是不可改变的,但可变类对象需要额外的方法来保证线程安全。
        例如Account就是一个可变类,它的money就是可变的,当2个线程同时修改money时,程序就会出现异常或错误。
        所以要对Account设置为线程安全的,那么就需要用到同步synchronized关键字。
        
        
        下面的方法用synchronized同步关键字修饰,那么这个方法就是一个同步的方法。这样就只能有一个线程可以访问这个方法,
        在当前线程调用这个方法时,此方法是被锁状态,同步监视器是this。只有当此方法修改完毕后其他线程才能调用此方法。
        这样就可以保证线程的安全,处理多线程并发取钱的的安全问题。
        public synchronized void drawMoney(double money) {
            //取钱操作
        }
        注意:synchronized可以修饰方法、代码块,但不能修饰属性、构造方法
        
        可变类的线程安全是以降低程序的运行效率为代价,为了减少线程安全所带来的负面影响,可以采用以下策略:
        A、不要对线程安全类的所有方法都采用同步模式,只对那些会改变竞争资源(共享资源)的方法进行同步。
        B、如果可变类有2中运行环境:单线程环境和多线程环境,则应该为该可变提供2种版本;线程安全的和非线程安全的版本。
        在单线程下采用非线程安全的提高运行效率保证性能,在多线程环境下采用线程安全的控制安全性问题。
        
        释放同步监视器的锁定
        任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器锁定?
        程序无法显示的释放对同步监视器的锁定,线程可以通过以下方式释放锁定:
        A、当线程的同步方法、同步代码库执行结束,就可以释放同步监视器
        B、当线程在同步代码库、方法中遇到break、return终止代码的运行,也可释放
        C、当线程在同步代码库、同步方法中遇到未处理的Error、Exception,导致该代码结束也可释放同步监视器
        D、当线程在同步代码库、同步方法中,程序执行了同步监视器对象的wait方法,导致方法暂停,释放同步监视器
     
        下面情况不会释放同步监视器:
        A、当线程在执行同步代码库、同步方法时,程序调用了Thread.sleep()/Thread.yield()方法来暂停当前程序,当前程序不会释放同步监视器
        B、当线程在执行同步代码库、同步方法时,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。注意尽量避免使用suspend、resume
        
        同步锁(Lock)
        通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock更灵活的结构,有很大的差别,并且可以支持多个Condition对象
        Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,
        线程开始访问共享资源之前应先获得Lock对象。不过某些锁支持共享资源的并发访问,如:ReadWriteLock(读写锁),在线程安全控制中,
        通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁。
         
        class C {
            //锁对象
            private final ReentrantLock lock = new ReentrantLock();
            ......
            //保证线程安全方法
            public void method() {
                //上锁
                lock.lock();
                try {
                    //保证线程安全操作代码
                } catch() {
                
                } finally {
                    lock.unlock();//释放锁
                }
            }
        }
        使用Lock对象进行同步时,锁定和释放锁时注意把释放锁放在finally中保证一定能够执行。
        
        使用锁和使用同步很类似,只是使用Lock时显示的调用lock方法来同步。而使用同步方法synchronized时系统会隐式使用当前对象作为同步监视器,
        同样都是“加锁->访问->释放锁”的操作模式,都可以保证只能有一个线程操作资源。
        同步方法和同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在一个块结构中,而且获得多个锁时,
        它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有资源。
        Lock提供了同步方法和同步代码库没有的其他功能,包括用于非块结构的tryLock方法,已经试图获取可中断锁lockInterruptibly()方法,
        还有获取超时失效锁的tryLock(long, timeUnit)方法。
        ReentrantLock具有重入性,也就是说线程可以对它已经加锁的ReentrantLock再次加锁,ReentrantLock对象会维持一个计数器来追踪lock方法的嵌套调用,
        线程在每次调用lock()加锁后,必须显示的调用unlock()来释放锁,所以一段被保护的代码可以调用另一个被相同锁保护的方法。
        
        死锁
        当2个线程相互等待对方是否同步监视器时就会发生死锁,JVM没有采取处理死锁的措施,这需要我们自己处理或避免死锁。
        一旦死锁,整个程序既不会出现异常,也不会出现错误和提示,只是线程将处于阻塞状态,无法继续。
        主线程保持对Foo的锁定,等待对Bar对象加锁,而副线程却对Bar对象保持锁定,等待对Foo加锁2条线程相互等待对方先释放锁,进入死锁状态。
        由于Thread类的suspend也很容易导致死锁,所以Java不推荐使用此方法暂停线程。
     
    13、线程通信
        (1)、线程的协调运行
            场景:用2个线程,这2个线程分别代表存款和取款。——现在系统要求存款者和取款者不断重复的存款和取款的动作,
            而且每当存款者将钱存入账户后,取款者立即取出这笔钱。不允许2次连续存款、2次连续取款。
            实现上述场景需要用到Object类,提供的wait、notify和notifyAll三个方法,这3个方法并不属于Thread类。但这3个方法必须由同步监视器调用,可分为2种情况:
            A、对于使用synchronized修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步中直接调用这3个方法。
            B、对于使用synchronized修改的同步代码块,同步监视器是synchronized后可括号中的对象,所以必须使用括号中的对象调用这3个方法
            方法概述:
            一、wait方法:导致当前线程进入等待,直到其他线程调用该同步监视器的notify方法或notifyAll方法来唤醒该线程。
                    wait方法有3中形式:无参数的wait方法,会一直等待,直到其他线程通知;带毫秒参数的wait和微妙参数的wait,
                    这2种形式都是等待时间到达后苏醒。调用wait方法的当前线程会释放对该对象同步监视器的锁定。
            二、notify:唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。
                只有当前线程放弃对该同步监视器的锁定后(用wait方法),才可以执行被唤醒的线程。
            三、notifyAll:唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才能执行唤醒的线程。
        (2)、条件变量控制协调
            如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,
            也不能使用wait、notify、notifyAll方法来协调进程的运行。
            当使用Lock对象同步,Java提供一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法组合使用,
            为每个对象提供了多个等待集(wait-set),这种情况下,Lock替代了同步方法和同步代码块,Condition替代同步监视器的功能。
            Condition实例实质上被绑定在一个Lock对象上,要获得特定的Lock实例的Condition实例,调用Lock对象的newCondition即可。
            Condition类方法介绍:
            一、await:类似于隐式同步监视器上的wait方法,导致当前程序等待,直到其他线程调用Condition的signal方法和signalAll方法来唤醒该线程。
                该await方法有跟多获取变体:long awaitNanos(long nanosTimeout),void awaitUninterruptibly()、awaitUntil(Date daadline)
            二、signal:唤醒在此Lock对象上等待的单个线程,如果所有的线程都在该Lock对象上等待,则会选择随机唤醒其中一个线程。
                只有当前线程放弃对该Lock对象的锁定后,使用await方法,才可以唤醒在执行的线程。
            三、signalAll:唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后,才可以执行被唤醒的线程。
         
        (3)、使用管道流
            线程通信使用管道流,管道流有3种形式:
            PipedInputStream、PipedOutputStream、PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,
            它们分别是管道流的字节流、管道字符流和新IO的管道Channel。
            管道流通信基本步骤:
            A、使用new操作法来创建管道输入、输出流
            B、使用管道输入流、输出流的connect方法把2个输入、输出流连接起来
            C、将管道输入、输出流分别传入2个线程
            D、2个线程可以分别依赖各自的管道输入流、管道输出流进行通信
        
    14、线程组和未处理异常
        ThreadGroup表示线程组,它可以表示一批线程进行分类管理,Java允许程序对
        Java允许直接对线程组控制,对线程组控制相对于同时控制这批线程。用户创建的所有线程都属于指定的线程组。
        如果程序没有值得线程属于哪个组,那这个线程就属于默认线程组。在默认情况下,子线程和创建它父线程属于同一组。
        一旦某个线程加入了指定线程组之后,该线程将属于该线程组,直到该线程死亡,线程运行中途不能改变它所属的线程组。
        Thread类提供一些构造设置线程所属的哪个组,具有以下方法:
        A、Thread(ThreadGroup group, Runnable target):target的run方法作为线程执行体创建新线程,属于group线程组
        B、Thread(ThreadGroup group, Runnalbe target, String name):target的run方法作为线程执行体创建的新线程,该线程属于group线程组,且线程名为name
        C、Thread(ThreadGroup group, String name):创建新线程,新线程名为name,属于group组
     
        因为中途不能改变线程所属的组,所以Thread提供ThreadGroup的setter方法,但提供了getThreadGroup方法来返回该线程所属的线程组,
        getThreadGroup方法的返回值是ThreadGroup对象的表示,表示一个线程组。
        ThreadGroup有2个构造形式:
        A、ThreadGroup(String name):name线程组的名称
        B、ThreadGroup(ThreadGroup parent, String name):指定名称、指定父线程组创建的一个新线程组
     
        上面的构造都指定线程名称,也就是线程组都必须有自己的一个名称,可以通过调用ThreadGroup的getName方法得到,
        但不允许中途改变名称。ThreadGroup有以下常用的方法:
        A、activeCount:返回线程组活动线程数目
        B、interrupt:中断此线程组中的所有线程
        C、isDeamon:判断该线程是否在后台运行
        D、setDeamon:把该线程组设置为后台线程组,后台线程具有一个特征,当后台线程的最后一个线程执行结束或最后一个线程被销毁,后台线程组自动销毁。
        E、setMaxPriority:设置线程组最高优先级
        uncaughtException(Thread t, Throwable e)该方法可以处理该线程组内的线程所抛出的未处理的异常,
        Thread.UncaughtExceptionHandler是Thread类的一个内部公共静态接口,
        该接口内只有一个方法:void uncaughtException(Thread t, Throwable e) 该方法中的t代表出现异常的线程,而e代表该线程抛出的异常
         
        Thread类中提供2个方法来设置异常处理器:
        A、staticsetDefaultUnaughtExceptionHandler(Thread.UncaughtExceptionHandler eh):为该线程类的所有线程实例设置默认的异常处理器
        B、setUncaughtExceptionHandler(Thread.UncaughtExceptionHander eh):为指导线程实例设置异常处理器
     
        ThreadGroup实现了Thread.UncaughtExceptionHandler接口,所以每个线程所属的线程组将会作为默认的异常处理器。当一个线程抛出未处理异常时,
        JVM会首先查找该异常对应的异常处理器,(setUncaughtExceptionHandler设置异常处理器),如果找到该异常处理器,将调用该异常处理器处理异常。
        否则,JVM将会调用该线程的所属线程组的uncaughtException处理异常,线程组处理异常流程如下:
        A、如果该线程有父线程组,则调用父线程组的uncaughtException方法来处理异常
        B、如果该线程实例所属的线程类有默认的异常处理器(setDefaultUnaughtExceptionHandler方法设置异常处理器),那就调用该异常处理器来处理异常信息
    <PRE style="TEXT-ALIGN: left; PADDING-BOTTOM: 0px; LINE-HEIGHT: 12pt; BACKGROUND-COLOR: rgb(244,244,244); MARGIN: 0em; PADDING-LEFT: 0px; WIDTH: 100%; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New',
  • 如何开始学习JSP

    2013-01-05 17:20:32

    一个普通的错误是把JSP当作简化的 Java。它不是,(事实上, JSP 是简化的 servlets 。)程序员通常试着没有学习要求的支持技巧而直接学习JSP 。JSP 是一个衔接技术,并且成功地连接你需要理解的另外的技术。如果你已经知道 Java , HTML 和 Javascript,这意味着JSP 将确实是简单的。

    需要成为一个成功的 JSP 程序员可以参考这个时间表。请注意下列:

    *忽略你已经熟悉的步骤。
    *训练的时间只是代表学习好足够的基础时间,这样才能转移到下一步。

    1、建立并且理解你的Web Server。
    因为Apache 是免费的并且在大多数平台上工作,为训练目的推荐 Apache。
    安装时间:2 天。


    2、 保证你理解 HTML / XHTML 。
    你将需要了解html基础, 特别是 HTML 布局中的table的使用。XHTML 不久将代替 HTML ,学习 XHTML 的基础是一个好主意。许多程序员

    通过 HTML IDE 学习 HTML ( 集成开发环境 ) 。因为大多数 HTML IDE产生混乱的HTMl语法,所以花时间学习手工写作html是很有必要的。因

    为你将会使用 JSP 和 HTML 混合编程,精通HTML语法是重要的。所以,你必须能流利地写 HTML 。
    训练时间:2 ~ 4 个星期。


    3、开始学习 Java 。
    开始学习 Java 1.3 理解 Java 基础是很重要的。不用担心学习Swing或 Java 的图形方面,因为在JSP 中你不会使用这些特征。集中精力

    在 Java 工作的细节,学习 Java 的逻辑,也在 Java Bean上花时间。学习Applet是好的, 但是就象Swing, JSP 的大多数应用将不使用小程

    序。
    训练时间:3 ~ 6 个星期。


    3、学习 JavaScript.
    学习怎么将 JavaScript在HTML中验证输入的Form元素。也学习 JavaScript怎么能在一 HTML 页以内修改Form的元素。最后要求你能从一

    HTML 页内的事件中触发 JavaScript. Function。
    训练时间:一~ 2 个星期。


    4、学习并且理解你的Web Server的更好的细节。
    熟悉Web Server的特征,这是很重要的。
    训练时间:2 天。


    5、建立你的 JSP Server
    我推荐以Tomcat开始。它可以很好地运行JSP程序。当你不能在生产使用Tomcat时,学习尽可能多的知识以便于更好的运行程序。另外, 许

    多 JSP 程序员使用Tomcat。因此当你遇到一个问题时,你将容易发现帮助。
    安装时间:一~ 2 天。


    6、开始学习 JSP 。
    基本的 JSP 学习通过的步骤 1到步骤6可以完成, 然后使用 JSP 对象和脚本写 JSP 程序来联系。学习 JSP 的另外一个方面可以学习怎么创

    建一个分布式的应用程序。
    训练时间:4 ~ 6 个星期。


    7、学习更多的 JSP server。
    没有关于更多的 JSP Server当然也可以运行jsp程序。然而, 许多 JSP server都由自己特殊的特征,可以让你更好的理解你的JSP 工程。

    学习更多的Jsp server如何处理jsp程序是有必要的。同样也可以优化你的 JSP 应用程序,并且使之运行得更快而不出任何问题。
    训练时间:2 ~ 7 天。


    8、 学习 JDBC 。
    JSP 大多数应用将使用数据库,JDBC 被用于数据库连接。经常忽略的一个事实就是,每个 JDBC Driver 所支持的东西是相当不同的。了

    解并熟悉在jsp工程上被使用的 JDBC driver的细节是很重要的。
    (有时这部分的学习被包含在前面 Java 或JSP的学习中了 。)
    训练时间:1~ 2 个星期。

    到现在,你已经成为了熟练的 JSP 程序员。仍然有很多需要学习,你可以考虑扩展你的知识比如 DHTML , XML ,java证书, JSP Tag Libraries 或 Servlets , 看你想要造什么类型的网站而决定了。

    这些训练是JSP 的核心。你不必都学习上面所有的, 取决于你在工程中分配到什么任务和你已经有什么知识。但是这是我成功地训练程序员的时间表。关键的单元是时间。平均的说, 5 个月时间确实能够训练一个人 ( 从开始到完成 ) 成为一个对jsp熟悉程序员。5 个月时间似乎很长,但要成为一个资深的WEB程序员所学的东西远远不止这一些。

    也许你认为这样学习一种语言花费的时间太长了,因为学 ASP 会更快、时间会更短。 但是学习 ASP 不需要学习java的。
  • JSP与Servlets的区别

    2013-01-05 17:18:33

    JSP和SERVLET到底在应用上有什么区别,很多人搞不清楚。我来胡扯几句吧。简单的说,SUN首先发展出SERVLET,其功能比较强劲,体系设计也很先进,只是,它输出HTML语句还是采用了老的CGI方式,是一句一句输出,所以,编写和修改HTML非常不方便。

    后来SUN推出了类似于ASP的镶嵌型的JSP,把JSP TAG镶嵌到HTML语句中,这样,就大大简化和方便了网页的设计和修改。新型的网络语言如ASP,PHP,JSP都是镶嵌型的SCRIPT语言。

    从网络三层结构的角度看,一个网络项目最少分三层:data layer,business layer, presentation layer。当然也可以更复杂。SERVLET用来写business layer是很强大的,但是对于写presentation layer就很不方便。JSP则主要是为了方便写presentation layer而设计的。当然也可以写business layer。写惯了ASP,PHP,CGI的朋友,经常会不自觉的把presentation layer和business layer混在一起。就象前面那个朋友,把数据库处理信息放到JSP中,其实,它应该放在business layer中。

    根据SUN自己的推荐,JSP中应该仅仅存放与presentation layer有关的东东,也就是说,只放输出HTML网页的部份。而所有的数据计算,数据分析,数据库联结处理,统统是属于business layer,应该放在JAVA BEANS中。通过JSP调用JAVA BEANS,实现两层的整合。

    实际上,微软推出的DNA技术,简单说,就是ASP+COM/DCOM技术。与JSP+BEANS完全类似,所有的presentation layer由ASP完成,所有的business layer由COM/DCOM完成。通过调用,实现整合。

    为什么要采用这些组件技术呢?因为单纯的ASP/JSP语言是非常低效率执行的,如果出现大量用户点击,纯SCRIPT语言很快就到达了他的功能上限,而组件技术就能大幅度提高功能上限,加快执行速度。

    另外一方面,纯SCRIPT语言将presentation layer和business layer混在一起,造成修改不方便,并且代码不能重复利用。如果想修改一个地方,经常会牵涉到十几页CODE,采用组件技术就只改组件就可以了。

    综上所述,SERVLET是一个早期的不完善的产品,写business layer很好,写presentation layer就很臭,并且两层混杂。

    所以,推出JSP+BAEN,用JSP写presentation layer,用BAEN写business layer。SUN自己的意思也是将来用JSP替代SERVLET。

    可是,这不是说,学了SERVLET没用,实际上,你还是应该从SERVLET入门,再上JSP,再上JSP+BEAN。

    强调的是:学了JSP,不会用JAVA BEAN并进行整合,等于没学。大家多花点力气在JSP+BEAN上。

    再补充几句:

    我们可以看到,当ASP+COM和JSP+BEAN都采用组件技术后,所有的组件都是先进行编译,并驻留内存,然后快速执行。所以,大家经常吹的SERVLET/JSP先编译驻内存后执行的速度优势就没有了。

    反之,ASP+COM+IIS+NT紧密整合,应该会有较大的速度优势呈现。而且,ASP+COM+IIS+NT开发效率非常高,虽然BUG很多。
  • Servlet教程

    2013-01-05 16:54:12

    Servlet教程


    一、 Servlet简介Servlet是对支持Java的服务器的一般扩充。它最常见的用途是扩展Web服务器,提供非常安全的、可移植的、易于使用的CGI替代品。它是一种动态加载的模块,为来自Web服务器的请求提供服务。它完全运行在Java虚拟机上。由于它在服务器端运行,因此它不依赖于浏览器的兼容性。
    servlet容器:
    负责处理客户请求、把请求传送给servlet并把结果返回给客户。不同程序的容器实际实现可能有所变化,但容器与servlet之间的接口是由servlet API定义好的,这个接口定义了servlet容器在servlet上要调用的方法及传递给servlet的对象类。
    servlet的生命周期:
    1、servlet容器创建servlet的一个实例
    2、容器调用该实例的init()方法
    3、如果容器对该servlet有请求,则调用此实例的service()方法
    4、容器在销毁本实例前调用它的destroy()方法
    5、销毁并标记该实例以供作为垃圾收集
    一旦请求了一个servlet,就没有办法阻止容器执行一个完整的生命周期。
    容器在servlet首次被调用时创建它的一个实例,并保持该实例在内存中,让它对所有的请求进行处理。容器可以决定在任何时候把这个实例从内存中移走。在典型的模型中,容器为每个servlet创建一个单独的实例,容器并不会每接到一个请求就创建一个新线程,而是使用一个线程池来动态的将线程分配给到来的请求,但是这从servlet的观点来看,效果和为每个请求创建一个新线程的效果相同。
    servlet API
    servlet接口:
    public interface Servlet
    它的生命周期由javax.servlet.servlet接口定义。当你在写servlet的时候必须直接或间接的实现这个接口。一般趋向于间接实现:通过从javax.servlet.GenericServlet或javax.servlet.http.HttpServlet派生。在实现servlet接口时必须实现它的五个方法:
    init():
    public void init(ServletConfig config) throws ServletException
    一旦对servlet实例化后,容器就调用此方法。容器把一个ServletConfig对象传统给此方法,这样servlet的实例就可以把与容器相关的配置数据保存起来供以后使用。如果此方法没有正常结束就会抛出一个ServletException。一旦抛出该异常,servlet就不再执行,而随后对它的调用会导致容器对它重新载入并再次运行此方法。接口规定对任何servlet实例,此方法只能被调用一次,在任何请求传递给servlet之前,此方法可以在不抛出异常的情况下运行完毕。
    service():
    public void service(ServletRequest req,ServletResponse res) throws ServletException,IOException
    只有成功初始化后此方法才能被调用处理用户请求。前一个参数提供访问初始请求数据的方法和字段,后一个提供servlet构造响应的方法。
    destroy():
    public void destroy()
    容器可以在任何时候终止servlet服务。容器调用此方法前必须给service()线程足够时间来结束执行,因此接口规定当service()正在执行时destroy()不被执行。
    getServletConfig():
    public ServletConfig getServletConfig()
    在servlet初始化时,容器传递进来一个ServletConfig对象并保存在servlet实例中,该对象允许访问两项内容:初始化参数和ServletContext对象,前者通常由容器在文件中指定,允许在运行时向sevrlet传递有关调度信息,后者为servlet提供有关容器的信息。此方法可以让servlet在任何时候获得该对象及配置信息。
    getServletInfo():
    public String getServletInfo()
    此方法返回一个String对象,该对象包含servlet的信息,例如开发者、创建日期、描述信息等。该方法也可用于容器。
    GenericServlet类
    Public abstract class GenericServlet implants Servlet,ServletConfig,Serializable
    此类提供了servlet接口的基本实现部分,其service()方法被申明为abstract,因此需要被派生。init(ServletConfig conf)方法把servletConfig对象存储在一个private transient(私有临时)实例变量里,getServletConfig()方法返回指向本对象的指针,如果你重载此方法,将不能使用getServletConfig来获得ServletConfig对象,如果确实想重载,记住要包含对super.config的调用。2.1版的API提供一个重载的没有参数的init()方法。现在在init(ServletConfig)方法结束时有一个对init()的调用,尽管目前它是空的。2.1版API里面,此类实现了ServletConfig接口,这使得开发者不用获得ServletConfig对象情况下直接调用ServletConfig的方法,这些方法是:getInitParameter(),getInitParameterNames(),getServletContext。此类还包含两个写日志的方法,它们实际上调用的是ServletContext上的对应方法。log(String msg)方法将servlet的名称和msg参数写到容器的日志中,log(String msg,Throwable cause)除了包含servlet外还包含一个异常。
    HttpServlet类
    该类扩展了GenericServlet类并对servlet接口提供了与HTTP更相关的实现。
    service():
    protected void service(HttpServletRequest req,HttpServletResponse res) throws ServletException,IOException
    public void service(HttpServletRequest req,HttpServletResponse res)throws ServletException,IOException
    该方法作为HTTP请求的分发器,这个方法在任何时候都不能被重载。当请求到来时,service()方法决定请求的类型(GET,POST,HEAD,OPTIONS,DELETE,PUT,TRACE),并把请求分发给相应的处理方法(doGet(),doPost(),doHead(),doOptions(),doDelete(),doPut(),doTrace())每个do方法具有和第一个service()相同的形式。为了响应特定类型的HTTP请求,我们必须重载相应的do方法。如果servlet收到一个HTTP请求而你没有重载相应的do方法,它就返回一个说明此方法对本资源不可用的标准HTTP错误。
    getLatModified():
    protected long getLastModified(HttpServletRequest req)
    该方法返回以毫秒为单位的的自GMT时间1970年1月1日0时0分0秒依赖的最近一次修改servlet的时间,缺省是返回一个负数表示时间未知。当处理GET请求时,调用此方法可以知道servlet的最近修改时间,服务器就可决定是否把结果从缓存中去掉。
    HttpServletRequest接口
    public interface HttpServletRequest extends ServletRequest
    所有实现此接口的对象(例如从servlet容器传递的HTTP请求对象)都能让servlet通过自己的方法访问所有请求的数据。下面是一些用来获取表单数据的基本方法。
    getParameter()
    public String getParameter(String key)
    此方法试图将根据查询串中的关键字定位对应的参数并返回其值。如果有多个值则返回列表中的第一个值。如果请求信息中没有指定参数,则返回null。
    getParameterValues():
    public String[] getParameterValues(String key)
    如果一个参数可以返回多个值,比如复选框集合,则可以用此方法获得对应参数的所有值。如果请求信息中没有指定参数,则返回null。
    GetParameterNames():
    Public Enumeration getParameterNames()
    此方法返回一个Enumeration对象,包含对应请求的所有参数名字列表。
    HttpServletResponse接口
    public interface HttpServletResponse extends servletResponse
    servlet容器提供一个实现该接口的对象并通过service()方法将它传递给servlet。通过此对象及其方法,servlet可以修改响应头并返回结果。
    setContentType():
    public void setContentType(String type)
    在给调用者发回响应前,必须用此方法来设置HTTP响应的MIME类型。可以是任何有效的MIME类型,当给浏览器返回HTML是就是”text/html”类型。
    getWriter():
    public PrintWriter getWriter()throws IOException
    此方法将返回PrintWriter对象,把servlet的结果作为文本返回给调用者。PrintWriter对象自动把Java内部的UniCode编码字符转换成正确的编码以使客户端能够阅读。
    getOutputStream():
    public ServletOutputStream getOutputStream() throws IOException
    此方法返回ServletOutputStream对象,它是java.io.OutputStream的一个子类。此对象向客户发送二进制数据。
    setHeader():
    public void setHeader(String name,String value)
    此方法用来设置送回给客户的HTTP响应头。有一些快捷的方法用来改变某些常用的响应头,但有时也需要直接调用此方法。
    编译条件
    需要从http://java.sun.com/products/servlet/ 获得一份JSDK的拷贝,并把servlet.jar移动到JDK安装目录下的/jre/lib/ext目录下。如果是JDK1.1,则移动到/lib下,并在CLASSPATH中加入servlet.jar的绝对路径。
    运行条件
    需要Apache Jserv,Jrun Servlet Exec,Java Web Server,Weblogic,WebSphere,Tomcat,Resin等servlet服务器端程序。
    简单范例

    import java.io.*;
    import javax.servlet.*;
    import javax.servlet.http.*;

    public class HelloWorld extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException
    {
    response.setContentType("text/html");
    PrintWriter ut = response.getWriter();
    out.println("<html>");
    out.println("<body>");
    out.println("<head>");
    out.println("<title>Hello World!</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>Hello World!</h1>");
    out.println("</body>");
    out.println("</html>");
    }
    }

    servlet的性能和效率
    一个servlet仅被初始化一次而执行多次,因此极小的低效性也会随着时间的增加而产生很很大的影响。在代码中需要考虑String对象的使用,如果产生HTML响应需要用到很多字符串时,不应该为每一个字符串生成一个String对象,因为这会产生大量的String和StringBuffer对象,造成大量的对象构造消耗和垃圾收集负担,解决的办法是一行一行的把所有需要写入的直接写入PrintWriter中,或者创建一个StringBuffer对象,并使用append()方法将文本加入。
    及时回送
    有时,程序需要花费很长时间执行,在这种情况下应该回送给客户端一些信息,而不是长时间显示白屏,这可以在执行到一定程度就回送一些东西,可以使用PrintWriter的flush()方法强制将现有的内容回送给浏览器。

    Servlet会话
    由于Web服务器使用的协议HTTP是一种无状态的协议,因此要维护单个客户机一系列请求的当前状态就需要使用其它的附加手段,在以前,一般的方法是使用:
    l 隐藏的表格字段:在浏览器中,这种类型的字段是不可见的,然而,它在请求中被传送,服务器端程序可以读取它的值。它的优点是实现容易,且大多浏览器都支持;缺点是字段必须按照特定的顺序建立,客户端可以通过查看源代码得到其值,在浏览器中单击“后退”按钮会丢失加到当前页中的附加字段,同时也限制了用户动态的生成文档。
    l Cookie:是一些关键的数据片断,由服务器建立并由客户机的浏览器存放。浏览器维护一个它自己的Cookie表,这使得它可以作为一种会话跟踪的解决方案。使用Cookie的好处是它比在URL或表单中储存数据更直观。它的缺点是它可以用于在比一次短会话更长时间内跟踪用户,甚至可以用来跟踪某个用户向站点发送的每一个请求,因此有人担心自己的隐私问题而关闭了Cookie,一些老的浏览器也不支持cookie。Servlet API提供一个Cookie类支持cookie,使用HttpServletResponse.addCookie()和HttpServletResponse.getCookies()方法添加和读取cookie。
    l URL重写:修改请求的url,使之包含会话ID。这种方法的缺点是:对于大量的数据,URL会变得很长而失去控制;在某些环境下,URL的字符串长度有一定的限制;数据保密问题,你可能不想让旁边的人或者可以使用同一个计算机的看到你的会话数据。Servlet提供HttpServletRequest类可以获得参数。
    Servlet API有自己内置的会话跟踪支持,使用HttpSession对象既可。它的setAttribute()方法绑定一对名字/值数据,把它存到当前会话中,如果会话中已经存在该名字责替换它,语法为:public void setAttribute(String name,Object value)。getAttribute()方法读取存储在会话中的对象,语法为:public Object getAttribute(String name)。getAttributeNames()方法返回存储在会话中的所有名字,语法为:public String[] getAttributeNames()。最后一个方法是removeAttribute()方法,它从会话中删除指定的信息,语法为:public void removeAttribute(String name)。HttpSession对象可以使用HttpServletRequest对象request的getSession(true)方法获得。参数为true意味着如果不存在该对象则创建一个。

    二、 servlet规范定义的Servlet 生命周期servlet有良好的生存期的定义,包括如何加载、实例化、初始化、处理客户端请求以及如何被移除。这个生存期由javax.servlet.Servlet接口的init,service和destroy方法表达。
    1、加载和实例化
    容器负责加载和实例化一个servlet。实例化和加载可以发生在引擎启动的时候,也可以推迟到容器需要该servlet为客户请求服务的时候。
    首先容器必须先定位servlet类,在必要的情况下,容器使用通常的Java类加载工具加载该servlet,可能是从本机文件系统,也可以是从远程文件系统甚至其它的网络服务。容器加载servlet类以后,它会实例化该类的一个实例。需要注意的是可能会实例化多个实例,例如一个servlet类因为有不同的初始参数而有多个定义,或者servlet实现SingleThreadModel而导致容器为之生成一个实例池。

    2、初始化
    servlet加载并实例化后,容器必须在它能够处理客户端请求前初始化它。初始化的过程主要是读取永久的配置信息,昂贵资源(例如JDBC连接)以及其它仅仅需要执行一次的任务。通过调用它的init方法并给它传递唯一的一个(每个servlet定义一个)ServletConfig对象完成这个过程。给它传递的这个配置对象允许servlet访问容器的配置信息中的名称-值对(name-value)初始化参数。这个配置对象同时给servlet提供了访问实现了ServletContext接口的具体对象的方法,该对象描述了servlet的运行环境。
    2.1初始化的错误处理
    在初始化期间,servlet实例可能通过抛出UnavailableException 或者 ServletException异常表明它不能进行有效服务。如果一个servlet抛出一个这样的异常,它将不会被置入有效服务并且应该被容器立即释放。在此情况下destroy方法不会被调用因为初始化没有成功完成。在失败的实例被释放后,容器可能在任何时候实例化一个新的实例,对这个规则的唯一例外是如果失败的servlet抛出的异常是UnavailableException并且该异常指出了最小的无效时间,那么容器就会至少等待该时间指明的时限才会重新试图创建一个新的实例。
    2.2、工具因素
    当工具(注:根据笔者的理解,这个工具可能是应用服务器的某些检查工具,通常是验证应用的合法性和完整性)加载和内省(introspect)一个web应用时,它可能加载和内省该应用中的类,这个行为将触发那些类的静态初始方法被执行,因此,开发者不能假定只要当servlet的init方法被调用后它才处于活动容器运行状态(active container runtime)。作为一个例子,这意味着servlet不能在它的静态(类)初始化方法被调用时试图建立数据库连接或者连接EJB容器。

    3、处理请求
    在servlet被适当地初始化后,容器就可以使用它去处理请求了。每一个请求由ServletRequest类型的对象代表,而servlet使用ServletResponse回应该请求。这些对象被作为service方法的参数传递给servlet。在HTTP请求的情况下,容器必须提供代表请求和回应的HttpServletRequest和HttpServletResponse的具体实现。需要注意的是容器可能会创建一个servlet实例并将之放入等待服务的状态,但是这个实例在它的生存期中可能根本没有处理过任何请求。
    3.1、多线程问题
    容器可能同时将多个客户端的请求发送给一个实例的service方法,这也就意味着开发者必须确保编写的servlet可以处理并发问题。如果开发者想防止这种缺省的行为,那么他可以让他编写的servlet实现SingleThreadModel。实现这个类可以保证一次只会有一个线程在执行service方法并且一次性执行完。容器可以通过将请求排队或者维护一个servlet实例池满足这一点。如果servlet是分布式应用的一部分,那么,那么容器可能在该应用分布的每个JVM中都维护一个实例池。如果开发者使用synchronized关键字定义service方法(或者是doGet和doPost),容器将排队处理请求,这是由底层的java运行时系统要求的。我们强烈推荐开发者不要同步service方法或者HTTPServlet的诸如doGet和doPost这样的服务方法。
    3.2、处理请求中的异常
    servlet在对请求进行服务的时候有可能抛出ServletException或者UnavailableException异常。ServletException表明在处理请求的过程中发生了错误容器应该使用合适的方法清除该请求。UnavailableException表明servlet不能对请求进行处理,可能是暂时的,也可能是永久的。如果UnavailableException指明是永久性的,那么容器必须将servlet从服务中移除,调用它的destroy方法并释放它的实例。如果指明是暂时的,那么容器可以选择在异常信息里面指明的这个暂时无法服务的时间段里面不向它发送任何请求。在这个时间段里面被被拒绝的请求必须使用SERVICE_UNAVAILABLE (503)返回状态进行响应并且应该携带稍后重试(Retry-After)的响应头表明不能服务只是暂时的。容器也可以选择不对暂时性和永久性的不可用进行区分而全部当作永久性的并移除抛出异常的servlet。
    3.3线程安全
    开发者应该注意容器实现的请求和响应对象(注:即容器实现的HttpServletRequest和HttpServletResponese)没有被保证是线程安全的,这就意味着他们只能在请求处理线程的范围内被使用,这些对象不能被其它执行线程所引用,因为引用的行为是不确定的。

    4、服务结束
    容器没有被要求将一个加载的servlet保存多长时间,因此一个servlet实例可能只在容器中存活了几毫秒,当然也可能是其它更长的任意时间(但是肯定会短于容器的生存期)
    当容器决定将之移除时(原因可能是保存内存资源或者自己被关闭),那么它必须允许servlet释放它正在使用的任何资源并保存任何永久状态(这个过程通过调用destroy方法达到)。容器在能够调用destroy方法前,它必须允许那些正在service方法中执行的线程执行完或者在服务器定义的一段时间内执行(这个时间段在容器调用destroy之前)。一旦destroy方法被调用,容器就不会再向该实例发送任何请求。如果容器需要再使用该servlet,它必须创建新的实例。destroy方法完成后,容器必须释放servlet实例以便它能够被垃圾回收。

    三、 serlvet为什么只需要实现doGet和doPostSerlvet接口只定义了一个服务方法就是service,而HttpServlet类实现了该方法并且要求调用下列的方法之一:
    doGet:处理GET请求
    doPost:处理POST请求
    doPut:处理PUT请求
    doDelete:处理DELETE请求
    doHead:处理HEAD请求
    doOptions:处理OPTIONS请求
    doTrace:处理TRACE请求
    通常情况下,在开发基于HTTP的servlet时,开发者只需要关心doGet和doPost方法,其它的方法需要开发者非常的熟悉HTTP编程,因此这些方法被认为是高级方法。
    而通常情况下,我们实现的servlet都是从HttpServlet扩展而来。

    doPut和doDelete方法允许开发者支持HTTP/1.1的对应特性;
    doHead是一个已经实现的方法,它将执行doGet但是仅仅向客户端返回doGet应该向客户端返回的头部的内容;
    doOptions方法自动的返回servlet所直接支持的HTTP方法信息;
    doTrace方法返回TRACE请求中的所有头部信息。

    对于那些仅仅支持HTTP/1.0的容器而言,只有doGet, doHead 和 doPost方法被使用,因为HTTP/1.0协议没有定义PUT, DELETE, OPTIONS,或者TRACE请求。

    另外,HttpServlet定义了getLastModified方法以支持有条件的(conditional)get操作。有条件的get操作是指使用GET方式请求资源并且在头部指定只有在资源内容在指定时间后被修改的情况下服务器才有必要回应请求并发送请求的内容。对于那些实现doGet方法并且在不同请求之间内容相同的servlet而言,它应该实现这个方法以提高网络资源的利用率。

    另外要提及的是,按照规范的要求,servlet容器至少要实现HTTP/1.0协议规范,推荐实现HTTP/1.1规范,在此基础上可以实现其它的基于请求回应模式(based request response model)的协议(例如HTTPS)。

    四、 servlet实例的个数及因此引发的问题在缺省情况下,一个容器中只为每个servlet定义生成一个servlet类实例。在servlet实现SingleThreadModel接口的情况下,容器可以生成多个实例以应付沉重的请求,也可以将请求排队发送给同一个实例(对于一个高性能的容器,也可能是这两种方式的结合,因为实例的个数是有限制的,因此在线程安全方式下一个实例会有多个请求排队等待服务同时容器中多个实例可以对请求进行服务)。对于为可分布式(distributable)应用开发的servlet而言,在每个JVM中对每个SERVLET定义都会有一个实例,如果在这样的应用中servlet也实现了SingleThreadModel接口,那么在每个JVM中每个servlet定义也可能有多个实例。

    使用SingleThreadModel接口可以保证一个线程一次性执行完给定实例的service方法,需要注意的是这个保证只能应用于servlet实例,那些可以被多个servlet实例访问的对象(例如HttpSession实例)依然对多个servlet有效,即使他们实现了SingleThreadModel。

    根据规范中的这些说明,我们在实现自己的serlvet时需要考虑多线程的问题,一般而言,不要在servlet中定义可变的成员,只能定义一些常量(使用final定义,如果没有使用,应该注意在程序中不应该修改其值),笔者见过一个定义很差的servlet:
    public class SomeHttpServlet extends HttpServlet {

    HttpSession session;
    ...
    }

    这样的servlet在使用中一定会出现问题,所有的用户都会共用一个session(这样很节约系统资源,不是吗?:)),因此一个用户请求的信息突然跑到另一个用户的ie窗口豪不奇怪。
    而且,即使你的servlet实现了SingleThreadModel接口也不要定义可变的成员,因为该成员的信息会保留下来,而这对于其它的用户而言在绝大部分情况下是毫无意义的。(你确定会有意义的情况例外,例如某种计数)

    另外需要说明的是上面说明中都是针对servlet定义而言的,而servlet定义定义不等价servlet类定义,即一个servlet类可能会有多个servlet定义,但是笔者还没有找到“servlet定义”的定义,规范中提到实例化一个servlet时可能会有不同的初始参数,但是这个也不同于带参数的多个构造方法。一般情况下我们可以认为一个servlet类对应一个servlet定义。

    五、 servlet会话HTTP协议是一种无状态的协议,而对于现在的web应用而言,我们往往需要记录从特定客户端的一系列请求间的联系。现在已经有很多会话跟踪的技术,但是对于程序员而言都不是很方便直接使用。servlet规范定义了一个简单的HttpSession接口以方便servlet容器进行会话跟踪而不需要开发者注意实现的细节。

    一般而言,有两种最常用的会话跟踪机制,一种就是URL重写。在客户端不接受cookie的情况下可以使用URL重写进行会话跟踪。URL重写包括向URL路径添加一些容器可以解释的数据。规范要求会话ID必须编码在URL路径中,参数名称必须是jsessionid,例如:
    http://www.myserver.com/catalog/index.html;jsessionid=1234

    另一种就是现在最常用的cookie了,规范要求所有的servlet都必须支持cookie。容器向客户端发送一个cookie,客户端在后续的处于同一个会话的请求中向服务器返回该cookie。会话跟踪cookie的名字必须是JSESSIONID。

    新出现的一种会话功能是SSL会话,SSL(Secure Sockets Layer,安全套接字层)是HTTPS协议使用的一种加密技术,内建了会话跟踪功能,servlet容器可以非常容易的使用这些数据建立会话跟踪。(但是HTTPS不是规范要求servlet必须支持的协议)

    因为HTTP是一种基于请求响应的协议,因此会话只有在客户端加入它以后才被新建立。当会话跟踪信息被成功的返回给服务器以指示会话给建立时客户端才算加入了一个会话。如果客户端没有加入会话,那么下一次请求不会被认为是会话的一部分。如何客户端还不知道会话或者客户端选择不加入一个会话,那么会话被认为是新的。开发者必须自己设计自己的应用中的会话处理状态,在什么地方没有加入会话,什么地方不能加入会话以及什么地方不需要加入会话。
    规范要求HttpSession在应用或者servlet上下文级别有效,诸如cookie这样的建立会话的底层机制可以在上下文中共享,但是对于那些外露的对象,以及更重要的是对象的那些属性是不能在上下文中共享的。

    对于会话的属性的绑定而言,任何对象都可以绑定到某个命名属性。被绑定的属性对象对于其它处于相同ServletContext并且处于同一个会话处理中的其它servlet也是可见的。
    某些对象在被加入会话或者被从会话中移除时要求得到通知,这样的信息可以通过让该对象实现HttpSessionBindingListener接口得到。该接口定义了两个方法用以标记被绑定到会话或者从会话中被移除。
    valueBound方法在对象通过getAttribute之前就被调用,而valueUnbound方法在对象已经不能通过getAttribute得到后才被调用。

    由于HTTP是无状态协议,因此客户端不再活动时没有什么明显的信号,这也就意味着只有一种机制可以用于表明客户端不再活动:超时。会话的缺省的时限由servlet容器定义并且可以通过HttpSession的getMaxInactiveInterval得到,开发者也可以通过使用setMaxInactiveInterval方法进行设置,这些方法返回的单位是秒,如果时限被设置为-1,那么意味着永远不会超时。

    通过调用HttpSession的getLastAccessedTime方法,我们可以得到在当前请求之前的访问时间。当会话中的一个请求被servlet上下文处理时会话就被认为被访问了。

    另外需要注意的就是一些很重要的会话的语义问题。
    多线程问题:多个请求线程可能会同时访问同一个会话,开发者有责任以适当的方式同步访问会话中的资源。
    分布式环境:对于被标记为可分布的应用而言,同一会话中的所有请求只能被单一的VM处理。同时,放入HttpSession中的所有对象都必须实现Serializable接口,否则容器可能会抛出IllegalArgumentException(在jboss_tomcat下没有抛出这个异常,但是如果在关闭服务器时还有未完成的会话,那么服务器在试图存储会话时会出现串行化异常,在重新启动的时候会试图回复会话,也会出现异常)。这个限制意味着开发者不会遇到非可分布容器中的那些并发问题。另外容器提供者可以通过将一个会话对象以及它的内容从分布式系统的一个活动节点移动到系统的其它不同节点的能力来保证可伸缩性。
    客户端的语义:基于cookie或者SSL证书通常是被web浏览器控制并且不联系到特定浏览器窗口的事实,从客户端应用的所有窗口发送到容器的请求都可能是同一个会话。为了达到最大的可移植性,开发者不能总假设特定客户端的所有窗口的请求都处于同一个会话中。六、 Bean和Servlet的企业应用J2EE是一个企业应用程序的开发平台,包括了对EJB、Servlet、JavaServer Page、JNDI、XML等的支持。在这个平台上可以开发瘦客户端的多层体系结构的企业应用程序。

      Enterprise JavaBean技术是J2EE的主要基础。EJB技术对在分布式的计算环境中执行应用逻辑提供了一个可伸缩的框架结构。J2EE通过将EJB组件结构和其它的企业技术相结合,解决了在Java平台上进行开发和配置服务端应用程序的无缝结合。

      要使用J2EE开发您的企业应用,您必须要在您的机器上安装一个Web服务器,还要支持XML。为了在浏览器中运行Java 2的API,还要给您的浏览器安装一个支持Java2的插件。

      下面就介绍怎样用J2EE SDK写一个包括了HTML页面,Servlet和Session Bean的一个简单的瘦客户端的多层体系结构的企业应用程序。听起来是不是心动了呢?下面就开始吧。

    还要提醒一点的就是:在编程的时候,适当的增加catch子句是一个很好编程风格。如果例子代码抛出了一个正确的异常,代码就被 try/catch这样的程序结构包围。Catch子句应该中应该加入处理异常的代码,千万不要留成空白。至少,应该加入语句:e.printStackTrace()来在控制台显示异常信息。

      J2EE SDK是一个J2EE平台上用于演示、教育等用途的非商业的东东。可以从javasoft的网站上免费下载。很适合用来学习。如果你没有出国权限,还可以从国内各高校的FTP服务器上去下载,速度比较快,但可能版本不是最新的。


    瘦客户端的多层体系结构的应用程序的例子:

      本例子通过一个HTML页面的输入来调用一个Servlet,Servlet再用Java的名字目录服务接口(JNDI)APIs来寻找一个会话Session Bean,用这个Session Bean来执行一个计算。当Servlet得到了计算的结果的之后,Servlet把计算结果返回给HTML页面的用户。

      之所以说这是一个瘦客户端的应用程序,是因为Servlet本身并没有执行任何的应用逻辑。这个简单的计算是由一个Session Bean在J2EE的应用服务器上执行的。客户没有参与过程的任何操作,所有的计算都是由Session Bean完成的。

      所谓的多层体系结果实际上是由三或者四层组成的。我们的例子实际上是四层的一个结构。三层的体系结构是在标准的两层的客户/服务器结构基础上,将一个多线程的应用服务器加到了非浏览器的客户端和后台数据库之间。而四层的体系结构是通过Servlet和JavaServer Pages技术将客户端的应用程序由浏览器和HTML页面来取代。这个例子我们暂时只用其中的三层,在下一个例子中。我们再去访问数据库。这样,就扩展到四层了。再以后,我们会涉及到JavaServer Pages技术和XML技术。


    J2EE软件的安装:

      为了使我们的例子能够运行起来,首先要下载一个Java2 SDK Enterprise Edition(J2EE)的1.2.1的版本和一个J2SE(Java 2 Standard Edition)的1.2以上的版本。在Windows 2000系统中,假设我们把J2EE和J2SE都装到了C:/J2EE目录下。安装详细目录如下:

    J2EE:C:/J2EE/j2sdkee1.2.1

    J2SE:C:/J2EE/jdk1.2.2


    Path和ClassPath的设置:

      下载的东西包括了J2EE的应用服务器、Cloudscape数据库、使用了加密套接字协议层的Web服务器、开发和配置的工具、企业级的Java APIs。其Path和ClassPath的设置如下:

    Path的设置:在Windows系统中,需要把Path的目录包含下面两个目录:

    C:/J2EE/j2sdkee1.2.1/bin

    C:/J2EE/jdk1.2.2/bin

    Classpath的设置:在Windows系统中,需要把Classpath参数包含下面的文件:

    C:/J2EE/j2sdkee.1.2.1/lib/j2ee.jar

    另外,还要配置环境变量:

    J2EE_HOME=C:/J2EE/j2sdkee1.2.1

    JAVA_HOME=C:/J2EE/jdk1.2.2

      这样,就可以执行C:/J2EE/j2sdkee1.2.1/bin目录下面的批处理命令了。仔细看看里面的批处理,你会发现不少的东西的。


    J2EE应用程序组件:

      J2EE程序员编写J2EE组件。J2EE组件是一个功能齐全的软件单元。将其它的应用程序组件组装到J2EE的应用程序和接口中。J2EE规范中定义如下的应用程序组件:


    应用程序客户组件


    Enterprise JavaBean组件


    Servlet和JavaServer Pages组件(也叫做Web组件)


    Applet

      在本例子中,我们创建了一个J2EE的应用程序和两个J2EE的组件:一个Servlet和一个Session Bean。Servlet和HTML文件是捆绑在一个WAR(WEB Archive)文件中。Session Bean的类和接口捆绑到了一个JAR文件中。然后再把WAR文件和JAR文件加到J2EE的应用程序,捆绑到一个EAR(Enterprise Archive)文件中。并验证测试产品环境的配置。

      在这所有的步骤中。实际上执行了很多的不用的角色的功能。编写Session Bean和Servlet是开发工作。而创建一个J2EE的应用程序,将J2EE组件组装到应用程序中是应用程序的组装工作。实际上,这些工作可以在不同的地方由不用的人员来做。

    创建一个HTML页面:

    这个页面名字为bonus.html。HTML代码如下:

      代码中,让人感兴趣的是用别名来调用BonusServlet.class。因为在后面提到的应用程序的组装的时候,将它映射到了这个别名BonusServlet上

    <HTML>

    <BODY BGCOLOR = "WHITE">

    <BLOCKQUOTE>

    <H3>Bonus Calculation</H3>

    <FORM. METHOD="GET" ACTION="BonusAlias">

    <P>

    Enter social security Number:

    <P>

    <INPUT TYPE="TEXT" NAME="SOCSEC"></INPUT>

    <P>

    Enter Multiplier:

    <P>

    <INPUT TYPE="TEXT" NAME="MULTIPLIER"></INPUT>

    <P>

    <INPUT TYPE="SUBMIT" VALUE="Submit">

    <INPUT TYPE="RESET">

    </FORM>

    </BLOCKQUOTE>

    </BODY>

    </HTML>

      这个HTML文件有两个数据域,用户可以输入社会保险号和一个乘数。当用户单击了Submit按纽。BonusServlet就得到了终端用户的数据。然后寻找Session Bean。将用户数据传递给Session Bean。Session Bean计算出奖金,把结果返回给Servlet。Servlet再通过另一个HTML页面将奖金结果返回给用户。

    创建Servlet:

    例子假定BonusServlet.java文件是在C:/J2EE/Client-Code目录下面。在运行的时候,Servlet代码执行如下操作:


    获得用户数据


    查找Session Bean


    将用户数据传递给Session Bean


    在得到Session Bean的返回结果以后,创建一个HTML页面将结果返回给客户。


    Servlet代码如下:

    import javax.servlet.*;

    import javax.servlet.http.*;

    import java.io.*;

    import javax.naming.*;

    import javax.rmi.PortableRemoteObject;

    import Beans.*;

    public class BonusServlet extends HttpServlet {

    CalcHome homecalc;

    public void init(ServletConfig config)

    throws ServletException{

    //Look up home interface

    try{

    InitialContext ctx = new InitialContext();

    Object bjref = ctx.lookup("calcs");

    homecalc =

    (CalcHome)PortableRemoteObject.narrow(

    objref,

    CalcHome.class);

    } catch (Exception NamingException) {

    NamingException.printStackTrace();

    }

    }

    public void doGet (HttpServletRequest request,

    HttpServletResponse response)

    throws ServletException, IOException {

    String socsec = null;

    int multiplier = 0;

    double calc = 0.0;

    PrintWriter out;

    response.setContentType("text/html");

    String title = "EJB Example";

    out = response.getWriter();

    out.println("<HTML><HEAD><TITLE>");

    out.println(title);

    out.println("</TITLE></HEAD><BODY>");

    try{

    Calc theCalculation;

    //Get Multiplier and Social Security Information

    String strMult =

    request.getParameter("MULTIPLIER");

    Integer integerMult = new Integer(strMult);

    multiplier = integerMult.intValue();

    socsec = request.getParameter("SOCSEC");

    //Calculate bonus.10 AUGUST 28, 2000

    double bonus = 100.00;

    theCalculation = homecalc.create();

    calc =

    theCalculation.calcBonus(multiplier, bonus);

    } catch(Exception CreateException){

    CreateException.printStackTrace();

    }

    //Display Data

    out.println("<H1>Bonus Calculation</H1>");

    out.println("<P>Soc Sec: " + socsec + "<P>");

    out.println("<P>Multiplier: " +

    multiplier + "<P>");

    out.println("<P>Bonus Amount: " + calc + "<P>");

    out.println("</BODY></HTML>");

    out.close();

    }

    public void destroy() {

    System.out.println("Destroy");

    }

    }


      在import子句中,javax.servlet包括了Servlet Class的协议。Java.io是系统输入输出包。Javax.naming里面包含了Java名字目录服务APIs。Javax.rmi是用来Session Bean的home接口和Remote对象的通信使用的。

      在BonusServlet.init方法中,查找Session Bean的home接口。并且产生它的实例。方法使用了JNDI在组件的组装中的指定的名字calcs。用它来得到home接口的reference。然后就把这个reference和home接口类传递给PortableRemoteObject.narrow方法。来保证把reference转化为CalcHome类型。

      DoGet()方法有两个参数。一个是request对象,另一个是reponse对象。浏览器发送一个request对象给Servlet。而Servlet返回一个response对象给浏览器。方法访问request对象里面的信息,可以发现是谁在发出的请求、请求的数据在什么表单里面、是哪个HTTP头被发送。并使用reponse对象产生一个HTML页面来响应浏览器的请求。

      当方法处理请求的时候,如果产生输入输出错误,就抛出一个IOException异常。如果不能处理请求,就会抛出一个ServletException异常。为了计算奖金值,doGet()创建了一个home接口,调用它的calcBonus。


    创建Session Bean:

      Session Bean代表了与客户的一个短暂的会话。如果服务或者客户有一方崩溃了。数据就消失了。相反,Entity Bean代表了数据库中一段持久的数据。如果服务或者客户又一方崩溃了,底层的服务保证数据能被保存下来。

      因为这个Enterprise Bean只是应BonusServlet的请求,执行了一个简单的计算。如果发生崩溃,可以重新初始化计算。这样,我们在本例子中就选择Session Bean来实现这个计算。

     在组装配置好以后,Servlet组件和Session Bean组件如何在一个J2EE应用程序中协同工作。容器是Session Bean和支持Session Bean的底层平台之间的接口。容器是在配置期间产生的。

      本例子假定CalcBean.java、Calc.java和CalcHome.java文件都放在C:/J2EE/Beans目录下面。CalcHome.java文件前面的Package名字 Beans和目录Beans的名字应该是一样的。当这些文件被编译的时候,是从Beans目录中编译,其名字是包的名字后面加一个斜线在加上类或者接口的名字。


     

    CalcHome.java文件:

    package Beans;

    import java.rmi.RemoteException;

    import javax.ejb.CreateException;

    import javax.ejb.EJBHome;

    public interface CalcHome extends EJBHome {

    Calc create() throws CreateException, RemoteException;

    }

      BonusServlet并不直接同Session Bean通信。而是通过产生一个CalcHome的实例。这个Home接口扩展了EJBHome接口。有一个Create()方法,用来在容器中产生一个Session Bean。如果无法产生Session Bean,将会抛出一个CreateException异常。如果不能与Session Bean的远程方法通信,就会抛出一个RemoteException异常。


    Calc.java文件:

    package Beans;

    import javax.ejb.EJBObject;

    import java.rmi.RemoteException;

    public interface Calc extends EJBObject {

    public double calcBonus(int multiplier,

    double bonus)

    throws RemoteException;

    }

      产生一个Home接口以后,J2EE应用程序就创建一个Remote接口和一个Session Bean。Remote接口扩展了EJBObject接口。并且声明了一个calcBonus()方法来计算奖金值。方法需要抛出javax.rmi.RemoteException异常。方法的实现在CalcBean类里面。


    CalcBean.java文件:

    package Beans;

    import java.rmi.RemoteException;

    import javax.ejb.SessionBean;

    import javax.ejb.SessionContext;

    public class CalcBean implements SessionBean {

    public double calcBonus(int multiplier,

    double bonus) {

    double calc = (multiplier*bonus);

    return calc;

    }

    public void ejbCreate() { }

    public void setSessionContext(

    SessionContext ctx) { }

    public void ejbRemove() { }

    public void ejbActivate() { }

    public void ejbPassivate() { }

    public void ejbLoad() { }

    public void ejbStore() { }

    }

      本Session Bean类实现了SessionBean接口,提供了CalcBonus()方法的行为。在BonusServlet调用CalcHome的Create()方法以后,依次调用setSessionContext()方法和ejbCreate()方法。

      这些空的方法是从SessionBean中来的。由容器负责调用。除非在Bean的创建或者删除里面,你需要附加一些你自己的操作。否者,你并不需要提供这些方法的行为。七、


    本文来自CSDN博客:http://blog.csdn.net/apicescn/archive/2005/03/15/320154.aspx

     

  • 深入分析 Java I/O 的工作机制(网络 I/O 优化)

    2013-01-05 16:38:32

    网络 I/O 优化

      网络 I/O 优化通常有一些基本处理原则:

      1、一个是减少网络交互的次数:要减少网络交互的次数通常我们在需要网络交互的两端会设置缓存,比如 Oracle 的 JDBC 驱动程序,就提供了对查询的 SQL 结果的缓存,在客户端和数据库端都有,可以有效的减少对数据库的访问。关于 Oracle JDBC 的内存管理可以参考《 Oracle JDBC 内存管理》。除了设置缓存还有一个办法是,合并访问请求:如在查询数据库时,我们要查 10 个 id,我可以每次查一个 id,也可以一次查 10 个 id。再比如在访问一个页面时通过会有多个 js 或 css 的文件,我们可以将多个 js 文件合并在一个 HTTP 链接中,每个文件用逗号隔开,然后发送到后端 Web 服务器根据这个 URL 链接,再拆分出各个文件,然后打包再一并发回给前端浏览器。这些都是常用的减少网络 I/O 的办法。

      2、减少网络传输数据量的大小:减少网络数据量的办法通常是将数据压缩后再传输,如 HTTP 请求中,通常 Web 服务器将请求的 Web 页面 gzip 压缩后在传输给浏览器。还有就是通过设计简单的协议,尽量通过读取协议头来获取有用的价值信息。比如在代理程序设计时,有 4 层代理和 7 层代理都是来尽量避免要读取整个通信数据来取得需要的信息。

      3、尽量减少编码:通常在网络 I/O 中数据传输都是以字节形式的,也就是通常要序列化。但是我们发送要传输的数据都是字符形式的,从字符到字节必须编码。但是这个编码过程是比较耗时的,所以在要经过网络 I/O 传输时,尽量直接以字节形式发送。也就是尽量提前将字符转化为字节,或者减少字符到字节的转化过程。

      4、根据应用场景设计合适的交互方式:所谓的交互场景主要包括同步与异步阻塞与非阻塞方式,下面将详细介绍。

      同步与异步

      所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

      在设计到 IO 处理时通常都会遇到一个是同步还是异步的处理方式的选择问题。因为同步与异步的 I/O 处理方式对调用者的影响很大,在数据库产品中都会遇到这个问题。因为 I/O 操作通常是一个非常耗时的操作,在一个任务序列中 I/O 通常都是性能瓶颈。但是同步与异步的处理方式对程序的可靠性影响非常大,同步能够保证程序的可靠性,而异步可以提升程序的性能,必须在可靠性和性能之间做个平衡,没有完美的解决办法。

      阻塞与非阻塞

      阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

      两种的方式的组合

      组合的方式可以由四种,分别是:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞,这四种方式都对 I/O 性能有影响。下面给出分析,并有一些常用的设计用例参考。

      表 3. 四种组合方式

    组合方式

    性能分析

    同步阻塞

    最常用的一种用法,使用也是最简单的,但是 I/O 性能一般很差,CPU 大部分在空闲状态。

    同步非阻塞

    提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。
    这种方式通常能提升 I/O 性能,但是会增加 CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗,也就是系统的瓶颈是在 I/O 还是在 CPU 上。

    异步阻塞

    这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些备份记录通常都是采用异步阻塞的方式写 I/O。
    异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。

    异步非阻塞

    这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的Gossip 通信机制就是采用异步非阻塞的方式。
    它适合同时要传多份相同的数据到集群中不同的机器,同时数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。

      虽然异步和非阻塞能够提升 I/O 的性能,但是也会带来一些额外的性能成本,例如会增加线程数量从而增加 CPU 的消耗,同时也会导致程序设计的复杂度上升。如果设计的不合理的话反而会导致性能下降。在实际设计时要根据应用场景综合评估一下。

    下面举一些异步和阻塞的操作实例:

      在 Cassandra 中要查询数据通常会往多个数据节点发送查询命令,但是要检查每个节点返回数据的完整性,所以需要一个异步查询同步结果的应用场景,部分代码如下:

      清单 3.异步查询同步结果

    class AsyncResult implements IAsyncResult{
    private byte[] result_;
    private AtomicBoolean done_ = new AtomicBoolean(false);
    private Lock lock_ = new ReentrantLock();
    private Condition condition_;
    private long startTime_;
    public AsyncResult(){
    condition_ = lock_.newCondition();// 创建一个锁
    startTime_ = System.currentTimeMillis();
    }
    /*** 检查需要的数据是否已经返回,如果没有返回阻塞 */
    public byte[] get(){
    lock_.lock();
    try{
    if (!done_.get()){condition_.await();}
    }catch (InterruptedException ex){
    throw new AssertionError(ex);
    }finally{lock_.unlock();}
    return result_;
    }
    /*** 检查需要的数据是否已经返回 */
    public boolean isDone(){return done_.get();}
    /*** 检查在指定的时间内需要的数据是否已经返回,如果没有返回抛出超时异常 */
    public byte[] get(long timeout, TimeUnit tu) throws TimeoutException{
    lock_.lock();
    try{ boolean bVal = true;
    try{
    if ( !done_.get() ){
    long overall_timeout = timeout - (System.currentTimeMillis() - startTime_);
    if(overall_timeout > 0)// 设置等待超时的时间
    bVal = condition_.await(overall_timeout, TimeUnit.MILLISECONDS);
    else bVal = false;
    }
    }catch (InterruptedException ex){
    throw new AssertionError(ex);
    }
    if ( !bVal && !done_.get() ){// 抛出超时异常
    throw new TimeoutException("Operation timed out.");
    }
    }finally{lock_.unlock(); }
    return result_;
    }
    /*** 该函数拱另外一个线程设置要返回的数据,并唤醒在阻塞的线程 */
    public void result(Message response){
    try{
    lock_.lock();
    if ( !done_.get() ){
    result_ = response.getMessageBody();// 设置返回的数据
    done_.set(true);
    condition_.signal();// 唤醒阻塞的线程
    }
    }finally{lock_.unlock();}
    }
    }

      总结

      本文阐述的内容较多,从 Java 基本 I/O 类库结构开始说起,主要介绍了磁盘 I/O 和网络 I/O 的基本工作方式,最后介绍了关于 I/O 调优的一些方法。

Open Toolbar