第 16 章 Never End...

学习到此为止告一段落

对教程内容和jsp有什么建议的话,可以跟叮咚姐姐联系,随着技术的进步,我们也会同步更新教程的内容。

16.1. 对session何时生成的无用讨论

提出问题:“session是在什么时候生成的?”

现象:

  1. 如果写一个servlet,在里边调用getSession(false),得到的是null。(这里的参数表示当session不存在时是否新建,返回null表示还没为当前用户生成对应的session)。

  2. 如果写一个jsp,在里边打印session,可以看到打印的信息,说明session已经创建了。

对现象的感性认识:jsp会自动创建HttpSession对象,而servlet则不会,只有在servlet中显式调用getSession()方法时才创建session。

对现象的理性说明

  1. 只有执行了getSession()才会生成session。(此处无参数,默认与getSession(true)功能相同)。

  2. jsp转换成servlet后,生成的servlet代码中包含getSession()。

    例子中见tomcat目录下,work\Catalina\localhost\99-01\org\apache\jsp\test1.jsp,其中包含了pageContext.getSession()。

  3. 我们可以在jsp中使用<%@ page session="false"%>禁止生成session的代码,这样即使访问此jsp页面,也不会创建session。

    例子中见tomcat目录下,work\Catalina\localhost\99-01\org\apache\jsp\test2.jsp,这里已经看不到getSession()的代码了。

因为<%@ page session="false"%>不会生成默认的session,在这个jsp页面里就无法直接使用session变量了。下面的代码会导致jsp无法编译的错误。

<%@ page session="false"%>
<%
    out.println(session);
%>
<hr />
        

访问test3.jsp会出现500服务器内部错误

在使用过<%@ page session="false"%>的页面里只能使用getSession()手工获得session才能使用了。

演示用代码放在99-01目录下,TestServlet映射到/请求路径,test1.jsp会自动创建session,test2.jsp禁用了session,test3.jsp直接使用session会出现错误。

这个问题是典型的无用研究之一,实际工作中会有人使用getSession(false)吗?

16.2. 对手工将jsp转换为servlet的无用讨论

有同学提到了一个问题:让用户自己编写jsp上传到服务器,服务器接收后保存入数据库,在用户访问的时候从数据库中取出对应的jsp,运行后返回响应。

这个效果从理论上讲是可以实现的,我们已经对jsp的运行机制非常熟悉了,第 6.2 节 “jsp与servlet的关系”,我们只需要将jsp转换为对应servlet的java文件,再编译为class,最后使用ClassLoader加载生成的class执行即可。

好消息是tomcat为我们提供了批量转换jsp的工具,使用以下的ant脚本就可以将指定目录下的jsp转换成对应servlet的java代码。(ant的使用方法已经超出了本文的讨论范围,如果有兴趣请自行参阅官方网站。http://ant.apache.org/

<project name="jspc" default="jspc" basedir=".">
    <property name="CATALINA_HOME" location="../../"/>

    <path id="build.lib">
        <pathelement location="WEB-INF/classes"/>
        <fileset dir="WEB-INF/lib">
            <include name="*.jar"/>
        </fileset>
    </path>

    <target name="jspc">
        <taskdef classname="org.apache.jasper.JspC" name="jasper2">
            <classpath>
                <pathelement location="${java.home}/../lib/tools.jar"/>
                <fileset dir="${CATALINA_HOME}/server/lib">
                    <include name="*.jar"/>
                </fileset>
                <fileset dir="${CATALINA_HOME}/common/lib">
                    <include name="*.jar"/>
                </fileset>
                <path refid="build.lib"/>
            </classpath>
        </taskdef>

        <jasper2 verbose="1"
            package="org.apache.jsp"
            uriroot="."
            webXmlFragment="WEB-INF/generated_web.xml"
            outputDir="./WEB-INF/src/" />
    </target>
</project>


        

为了顺利完成转换工作,我们需要把一系列jar文件放入classpath中:

  1. WEB-INF/lib/*.jar(星号代表所有)

  2. ${java.home}/../lib/tools.jar(${java.home}表示jdk安装目录)

  3. ${CATALINA_HOME}/server/lib/*.jar(${CATALINA_HOME}表示tomcat的安装目录)

  4. ${CATALINA_HOME}/common/lib/*.jar

简要介绍一下jasper2的配置参数。

  1. verbose="1"表示打印进度信息,如果verbose="0"则不打印信息。

  2. package="org.apache.jsp"表示生成servlet中的包名(package)。

  3. uriroot="."表示会把当前目录下的所有jsp都转换成servlet。

  4. webXmlFragment="WEB-INF/generated_web.xml"会在WEB-INF目录下生成servlet的默认配置。

    比如我们转换的test.jsp将生成如下部分。

    <servlet>
        <servlet-name>org.apache.jsp.test_jsp</servlet-name>
        <servlet-class>org.apache.jsp.test_jsp</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>org.apache.jsp.test_jsp</servlet-name>
        <url-pattern>/test.jsp</url-pattern>
    </servlet-mapping>
                    

    将这部分复制到你项目中的web.xml中就可以通过/test.jsp请求来访问生成的servlet了。

  5. outputDir="./WEB-INF/src/"表示生成的servlet文件放在WEB-INF/src目录下,tomcat会自动生成package对应的目录,比如test.jsp生成的最终路径是WEB-INF/src/org/apache/jsp/test_jsp.java。

接着把生成的servlet编译成class,这次除了common/lib/servlet-api.jar以外我们还需要common/lib/jasper-runtime.jar加入classpath中,编译脚本参考WEB-INF/src/compile.bat。

现在可以删除test.jsp了,web.xml中已经将/test.jsp请求转发至对应的servlet处理,我们甚至不需要修改任何链接。

jspc的主要功能在于预先编译jsp发现其中的语法错误,有些公司也使用这种方式进行加密(毫无意义的做法,把jsp唯一的灵活性都浪费了)。

演示程序在99-02目录下,需要安装ant之后才可能执行run.bat将jsp转换成servlet。

在了解如何手工转换jsp之后,我们可以来讨论在数据库中保存jsp的问题了。

将jsp从数据库中提取出来,将这些数据保存成本地文件,使用jspc转换成servlet再编译为class,最后使用自定义的ClassLoader读取到jvm中执行。

难点在于生成文件要保证互不影响和自定义ClassLoader如何加载管理这些生成的class。

这个问题是典型的无用研究之一,任何想获得灵活模板功能的同学都应该亦然决然的抛弃jsp,jsp这种先解释编译再执行的机制不仅没有帮助我们提升效率,反而大大增加技术难度并降低响应效率。需要自定义模板的同志务必考虑velocity, freemarker此类模板引擎或者groovy一类脚本语言,这时使用jsp无异于给自己带上一副沉重的枷锁。

16.3. 胡乱解释一下session

会话都保存在服务器端。

每个用户打开浏览器就服务器就会给它生成一个sessionId,浏览器或者把这个sessionId放到cookie里,或者每次请求都带在url后边(自动的),然后服务器就拿到这个sessionid,在内存里翻啊翻啊,翻出对应的session来,就这么对应上的。

ServletContext和session就没多大关系了,虽然ServletContext也是在服务器端,每个web应用发布的时候,就要创建这么一块空间放置ServletContext,这个web应用中的所有的servlet, jsp, filter, listener都可以访问这块空间。你可以把他看作是一个全局变量,所有共享数据都放到里边。

16.4. 下载文件乱码

参考 如何在PHP下载文件名中解决乱码

基本代码如下:

<%@page contentType="text/html;charset=UTF-8"%><%request.setCharacterEncoding("utf-8");%><%
String client = request.getHeader("User-Agent");
String fileName = "中文";
if (client.indexOf("MSIE") > 0) {
    fileName = "attachment; filename=\"" + java.net.URLEncoder.encode(fileName, "utf-8") + ".txt\"";
} else {
    fileName = "attachment; filename*=\"utf8''" + java.net.URLEncoder.encode(fileName, "utf-8") + ".txt\"";
}
String content = "xml格式内容." ;
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", fileName);
out.print(content);
%>
        

按照RFC2231的定义, 多语言编码的Content-Disposition应该这么定义:

Content-Disposition: attachment; filename*="utf8''%E4%B8%AD%E6%96%87%20%E6%96%87%E4%BB%B6%E5%90%8D.txt"
        
  • filename后面的等号之前要加 *

  • filename的值用单引号分成三段,分别是字符集(utf8)、语言(空)和urlencode过的文件名。

  • 最好加上双引号,否则文件名中空格后面的部分在Firefox中显示不出来。

  • 使用urlencode对非iso-8859-1字符进行编码。

经过试验,发现几种主流浏览器的支持情况如下:

  • IE6

    attachment; filename="<URL编码之后的UTF-8文件名>"
                    
  • FF3

    attachment; filename="UTF-8文件名"
    attachment; filename*="utf8''<URL编码之后的UTF-8文件名>"
                    
  • O9

    attachment; filename="UTF-8文件名"
                    
  • Safari3(Win)

    貌似不支持?上述方法都不行