1. Servlet接口:
其中init、Service和destroy是生命周期方法
init:当该Servlet第一次被请求时,Servlet容器会调用这个方法(这个方法在后续请求中不会再被调用)(通过配置可以使得该方法在容器装载该servlet时就被调用,避免了第一次请求时因为需要执行init方法而耗时过长)。Servlet容器会传入一个ServletConfig对象。
service:每当该Servlet被请求时,Servlet容器就会调用这个方法。ServletRequest对象封装了http请求报文,使得我们可以不用自己去解析http报文。ServletResponse对象用来方便我们编写http回复报文,使得我们不用自己管http协议中的许多细节。
destroy:当要销毁Servlet时,Servlet容器就会调用这个方法。
getServletInfo:返回对当前Servlet的描述。
getServletConfig:返回当前Servlet的ServletConfig对象。
2. 一个Servlet的例子(所有Servlet相关接口都位于包java.servlet下):
web应用程序需按照如下目录结构部署:
——WEB-INF下的classes文件夹放编译后的Servlet类。lib文件夹放该web应用程序需要的外部jar包(本例中不需要外部jar包,lib为空,因为代码中引用的javax.servlet包在Tomcat中有。但是在编译该Servlet时,则需要将javax.servlet包放在当前目录下去完成编译)
——JSP,HTML,图片等资源放在WEB-INF外。
注:放在WEB-INF文件夹外的资源,用户只要输入资源URL就可以直接访问到。如果想让某一个资源只能被Servlet访问,而不可以被用户访问,那么就要把它放在WEB-INF目录下。
将上述目录放置在Tomcat的webapps目录下即可。启动Tomcat后,在浏览器中输入localhost:8080/ch02/my,就会访问到我们上面写的Servlet程序。
下图中,Servlet/JSP容器对应于如Tomcat这类程序,里面的三个框框对应于各种Servlet类的实例。当容器收到请求时,则会根据请求的url去调用相应的servlet类的实例的方法去处理该请求(如本例中,容器通过url中的ch02定位到相应的web应用程序目录,通过my定位到MyServlet这个Servlet的实例,因为我们在程序中加了WebServlet注解,表示该Servlet用于处理/my这种urlPatterns。除了通过注解的方式也可以在web.xml中进行配置),并返回相应的响应。
注:在一个应用程序中,容器只会为每个Servlet类生成一个实例。
3. ServletRequest接口:
getParameter是用来获得客户端传来的参数的,而getAttribute则是用来在服务端获得另一个服务端通过setAttribute传进去的参数的。即Parameter是用于客户端和服务端之间的信息传递,而Attribute是用于服务端之间的信息传递。
4. ServletResponse接口:
getWriter方法获得ServletResponse对象的writer,通过调用writer的print方法向response中写入内容。
setContentType设置response的内容类型。使用方法如上面例子中所示。
5. ServletConfig接口:
getInitParameter,getInitParameterNames获取该servlet的初始参数。初始参数以键值对的形式在注解中或者在部署描述符web.xml中设置。
getServletContext获得该Servlet的上下文信息,返回一个ServletContext对象
6. ServletContext接口:
ServletContext代表的是整个当前的web应用程序的上下文。
ServletContext接口也有相应的对InitParameter的操作方法,但与ServletConfig不同的是,ServletContext中的InitParameter是全局范围的,该应用程序中的所有servlet都能访问到,而ServletConfig中的InitParameter只是当前这个servlet的,只有当前servlet能访问。
同理,ServletContext的Attribute的作用范围也比ServletRequest的Attribute的作用范围大。
7. HttpServlet
GenericServlet:实现Servlet接口中的全部五个方法较麻烦。因此GenericServlet实现了Servlet接口,给我们提供了默认的实现。当我们要创建自己的Servlet时只需要继承GenericServlet类,覆盖需要改写的方法即可。
HttpServlet:继承GenericServlet,重写了其中的service方法,并提供了一个重载的service方法 service(HttpServletRequest req, HttpServletResponse resp)。HttpServletRequest接口和HttpServletResponse接口为ServletRequest接口和ServletResponse接口的子接口。
重写的service方法中就是将传进来的ServletRequest对象和ServletResponse对象强制转换为HttpServletRequest对象和HttpServletResponse对象,并调用重载的service方法
重载的service方法通过HttpServletRequest对象的getMethod方法获取请求的http方法,根据请求的http方法去调用doGet、doPost、doHead、doPut、doTrace、doOptions、doDelete这七个方法中对应的那个。
因此,使用HttpServlet时,我们只需要覆盖上述的七种方法即可,不需要覆盖service方法。
8. 四种会话跟踪技术:URL重写,隐藏域,Cookie,Session。
这四种方式都是用来保存键值对参数的。
URL重写:将键值对参数加在url地址后面,url?key1=value1&key2=value2...
以该url访问服务端时,服务端可以通过request.getParameter(key)方法获得相应的参数值
隐藏域:将键值对参数放在表单中的隐藏域中,<input type='hidden' name='id' value='1'/>
当浏览器提交该表单时,服务端可以通过request.getParameter("id")方法获得相应的参数值
Cookie:
服务端通过,
Cookie cookie = new Cookie(String key, String value);
httpServletResponse.addCookie(cookie);
将存有键值对的cookie添加到response中回复给浏览器。
在之后,每次当浏览器请求该服务端时都会将该cookie放在request中,服务端可以通过getCookies方法找到相应的cookie,并通过Cookie类的getName, getValue方法获取其中的键值对参数
Session:
服务端会为每一个客户端在服务端的内存中保存一个HttpSession对象,该对象可以通过HttpServletRequest对象的getSession方法获得(若该客户端在服务端中没有对应的HttpSession对象,则调用该方法会为其创建一个;若该客户端的HttpSession对象已经存在于服务端中了,则该方法会直接返回该对象)。当服务端为该客户端在服务端内存中创建HttpSession对象时,会为该客户端生成给一个独一无二的id,并通过cookie的方式回复给该客户端(该过程无需我们管),则在之后该客户端对服务端的访问中,都会带上这个写有id的cookie,从而使得服务端可以通过该id在服务端的内存中找到与该客户端对应的session对象。
通过HttpSession对象的setAttribute(String name, Object value)和getAttribute(String name)方法来更新和获取键值对参数。
Cookie和Session的不同就在于,
Cookie:键值对参数是保存在客户端上的,服务端将键值对参数写进cookie后,回复给客户端,让客户端保存存有键值对参数的cookie。当服务端需要获取键值对参数时,需要从客户端发来的cookie中获取。
Session:键值对参数是保存在服务端上的,服务端将键值对参数写进HttpSession对象中存在自己的内存里。当服务端需要获取键值对参数时,只需要从客户端获取其id,就可以从自己的内存中获取到对应的键值对参数。
如开头所说,这四种都是用来保存键值对参数的方法,但是对于前两种方法,其只适合于在较少的几个页面的跳转中保存住键值对参数(通过把键值对参数写在url上或者表单中)。虽然其也能够在更多个页面的跳转中保存住这些键值对参数,但是较为麻烦。
而后两种方法则通过将键值对保存在客户端或者保存在服务端的方式,使得键值对参数可以被较持久的保存。
1. JSP
当jsp页面被第一次访问时,Servlet/JSP容器会将其翻译为一个JSP页面的实现类并编译,该类实现了Servlet接口。因此,所有JSP都是Servlet。该jsp页面后续被访问时,容器会检查jsp有没有被改动,如果改动了则会重新翻译编译,如果没有则继续按照之前编译好的servlet的生命周期执行。
JSP中的内容由语法元素和模板数据两部分构成。语法元素由如<% %>这种类型的符号包裹。其余的内容都为模板数据。模板数据在转换成servlet时,会全部原封不动的被writer.print到response中,而语法元素部分则会被转为相应的Java代码。
2. JSP隐式对象
上面的对象可以在JSP中的Java代码中直接使用。
其中out对象相当于从HttpServletResponse中获得的PrintWriter对象。
pageContext对象的getAttribute(String name, int scope)和setAttribute(String name, Object value, int scope)方法可以指定Attribute的作用范围(PAGE_SCOPE、REQUEST_ SCOPE、SESSION_SCOPE和APPLICATION_SCOPE)。
当scope被设置为REQUEST/SESSION/APPLICATION_SCOPE时,就相当于调用了HttpServletRequest/HttpSession/ServletContext对象中的get/setAttribute方法。Attribute是和这些对象绑定的,这些对象的存活时间和作用范围决定了Attribute的存活时间和作用范围。
PAGE_SCOPE的Attribute则只是在当前页面有效。
3.语法元素包含三种类型:指令,脚本元素,动作。
——指令:
page指令,语法格式 <%@ page attr1="value1" attr2="value2"...%>
如<%@ page import="java.util.ArrayList, java.util.Date" errorPage="errorHandler.jsp"%>引入了ArrayList和Date包以及指定了当前页面的errorPage(当前页面发生异常后会跳转去该errorPage,并且那个errorPage的jsp也要用page指令表明isErrorPage="True")
include指令,语法格式<%@ include file="url"%>
在翻译前,该指令会被"url"所指向的文件中的内容所替换,然后只被翻译为一个Servlet类(url所指向的文件不会被翻译成Servlet)。
如有,content.jspf:<p>Hello</p>
main.jsp:<html><body><%@ include file="content.jspf"%></body></html>
则main.jsp相当于:<html><body><p>Hello</p></body></html>
——脚本元素:
<% ... %>:用于在其中写Java代码。在JSP被翻译成Servlet时,被该符号括起来的内容将会作为Java代码直接放在__jspService(相当于service)方法中。
<%! ... %>:用于在其中声明方法以及变量。在JSP被翻译成Servlet时,声明在里面的方法将会被翻译成Servlet类的成员方法,声明在里面的变量将会被翻译成Servlet类的成员变量(而在<% ... %>中声明的变量将会被翻译为__jspService方法中的局部变量)。
<%= ... %>:Today is <%=java.util.Calendar.getInstance().getTime()%>等价于
Today is <% out.print(java.util.Calendar.getInstance().getTime()); %>
——动作:
include动作:
在翻译成Servlet时,main.jsp中的<jsp:include ..>部分会被翻译成一个include方法,然后得到main.jsp对应的Servlet类。同时,jspf/menu.jsp也会通过翻译得到其对用的Servlet类。
在main.jsp被访问,执行到include方法时,服务端会用参数text="How are you?"访问menu.jsp,并且将从menu.jsp得到的回复作为main.jsp的模板数据。
forward动作:
<jsp:forward page="jspf/login.jsp">
<jsp:param name="text" value="Please login"/>
</jsp:forward>
在翻译成Servlet时,该动作会被翻译成一个forward方法,执行到这个方法时,服务端会用参数text="Please login"去访问login.jsp,并且当前jsp的余下部分将不会被执行。
forward动作(转发)和HttpServletResponse的sendRedirect(String url)(重定向)都可以用来跳转去另一个servlet。它们的不同在于,forward是服务端在执行servlet执行到这个动作时发生的跳转,而sendRedirect则是服务端通过HttpServletResponse去回复给客户端一个url,让客户端再去发请求给这个新的url
现代的Web应用层程序规范推荐在JSP页面中使用EL和JSTL来全面替代脚本元素中scriplet(<% ... %>)的使用的使用,使得JSP中不包含Java后端代码,使得前后端能更好的解耦。
1. EL(Expression Language)表达式语言
语法:${...}
EL表达式返回的内容将直接作为JSP页面的模板数据
${a[b]} or ${a.b}(前者为规范的形式)形式的表达式的取值规则:
(i). 如果a是Map,则返回key为b的value
(ii). 如果a是List或者数组,则b需要为int,用来进行索引取值
(iii). 如果a不是上述的情况,则它必需是一个对象(被setAttribute方法存在某个范围中的对象)。这种情况下,b会被当作是对象a的属性,并且会调用b的getter方法,即a.getB或者a["getB"]。所以要求a对象一定要有名字为getB的方法。
EL隐式对象:
和JSP隐式对象类似,表达式语言也有隐式对象用于访问request,response这类的对象。
--pageContext:这是一个PageContext对象,它可以获得所有的JSP隐式对象。如,${pageContext.request.method}相当于<%request.getMethod()%>
--initParam:这是一个Map,里面存了当前ServletContext的初始参数。如,${initParam.key}相当于<%application.getInitParameter("key")%>
--param:这是一个Map,里面存了当前请求的参数。如,${param.key}相当于<%request.getParameter("key")%>
--paramValues:这是一个Map,与param类似,只不过value都是数组,用于获取值是数组的参数。如,${paramValues.selectedOptions[0]}表示取出selectedOptions参数的第一个值。
--header:这是一个Map,里面存了请求的头。
--cookie:这是一个Map,里面存了当前请求中的所有cookie。cookie名称就是key名称,并且每个key都映射到一个cookie对象。
--applicationScope、sessionScope、 requestScope、pageScope:
这些都是Map,里面存了不同范围的Attribute。如,${applicationScope.myVar}相当于<%application.getAttribute("myVar")%>
2.JSTL(JSP标准标签库,JSP Standard Tag Library)
JSTL可以支持很多不同的库,要使用这两个库需要在JSP中写如下指令:
<%@ taglib uri="uri" prefix="prefix" %>
这里主要介绍核心库,它的uri为http://java.sun.com/jsp/jstl/core,prefix可以任意指定(这里设为了c)。
set标签:
<c:set var="job" scope="request" value="${...}" or "..."/>,在当前request中set一个名字为job的attribute,并将value赋给这个attribute。
(相当于<%request.setAttribute("job", value)%>)
<c:set target="${address}" property="city" value="${...}" or "..."/>,调用address对象的setCity方法将value的值set进去。
remove标签:<c:remove var="job" scope="request"/>,删除request中job这个attribute。
if标签:
<c:if test="${param.user=='usr' && param.password=='psw'} ">
JSP segment
</c:if>
如果请求参数中的user为'usr',password为'psw',则中间的JSP片段会被执行,反之被忽略。
forEach标签:
<c:forEach var="phone" items="${address.phones}" varStatus="status">
<p> status.index <\p>
<p> ${phone} <\p>
</c:forEach>
遍历address.phones。其中varStatus是一个LoopTagStatus类型的对象,它有一个index属性可以获得当前遍历到的索引。
综上,EL更倾向用于访问或者获取某些东西和对它们进行一些算术或逻辑运算,而JSTL则用于完成其余部分。两者共同配合使用来完成对scriplet的替代。
1. 监听器Listener:
对监听器接口进行实现并注册,便可以对当前web应用程序的某些行为进行监听。
@WebListener
public class ListenerClass implements ListenerInterface {
public xxx methodA(...){...}
public xxx methodB(...){...}
...
}
上面ListenerClass类实现了监听器接口ListenerInterface并通过标注WebListener进行了注册(也可通过web.xml注册)。注册后,当前web应用程序将被监听,当某事件发生时,接口中的对应方法将会被调用。
监听器接口:
ServletContextListener:当ServletContext被创建和销毁时,接口中对应的那个方法会被调用。
ServletContextAttributeListener:当ServletContext中的Attributes被添加、删除、修改时,接口中对应的那个方法会被调用。
HttpSessionListener:当HttpSession被创建和销毁时,接口中对应的那个方法会被调用。
HttpSessionAttributeListener:当HttpSession中的Attributes被添加、删除、修改时,接口中对应的那个方法会被调用。
ServletRequestListener:当ServletRequest被创建和销毁时,接口中对应的那个方法会被调用。
ServletRequestAttributeListener:当ServletRequest中的Attributes被添加、删除、修改时,接口中对应的那个方法会被调用。
这些接口中的方法都会接收一个xxxEvent类型的对象,通过该对象可以拿到触发当前方法的对象。如ServletContextListener中的方法都会接收一个ServletContextEvent对象,通过该对象的getServletContext方法可以拿到触发该方法的ServletContext;ServletRequestListener中的方法都会接收一个ServletRequestEvent对象,通过该对象的getServletRequest方法可以拿到触发该方法的ServletRequest。
2. 拦截器Filter:
拦截器可以对客户端的请求进行拦截,从而使得该请求在被其对应的Servlet的service方法处理前,先被拦截器中的方法处理。拦截器可以形成一个链条,使得请求先被Filter_A处理,然后再交给Filter_B处理,等所有Filter处理完后才会交给Servlet处理。
拦截器就是实现了Filter接口的类,Filter接口的方法有:
init(FilterConfig filterConfig),
doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain),
destroy()
Filter接口的三个方法在执行时机等各方面与Servlet接口的init,service,destroy完全类似。
唯一的不同在于doFilter方法比service方法多了个FilterChain。在Filter接口的doFilter方法最后必须要调用FilterChain接口的doFilter(request, response)方法,用于将request继续向后传递给链条中的下一个(可能是Filter或者Servlet)进行处理。
FilterConfig接口与ServletConfig接口完全类似。
(Filter同样可以通过注解或部署描述符web.xml进行部署,但如果要指定多个Filter拦截的先后顺序需要使用web.xml进行配置)
1. 异步处理:
每次有请求进来时,容器会从线程池中为该请求分配一个线程来处理该请求。然而,容器中的线程池能分配的线程数是有上限的。当处理请求的程序(如service或doFilter方法)较耗时时,会导致线程无法短时间内完成并归还池中,线程池将很快耗尽,便无法接受新进来的请求。
为了解决这个问题,可以使用异步的Servlet或Filter,使得处理请求的过程不在当前的方法(如service或doFilter方法)中进行,而是只在当前方法中新建一个线程去处理该请求,然后就结束当前方法,使得该条从容器线程池中取出的线程能很快被还回。
可以认为只是换了个线程(不使用容器的线程池中的线程)去处理这个请求,其它完全没有区别。当新建的线程结束后,便会发回response给客户端(同非异步的过程)。
使用方法:
1、在Servlet或Filter上注解为异步的(也可以通过web.xml部署):
@WebServlet(... asyncSupported=true ...) or @WebFilter(... asyncSupported=true ...)
2、在如service、doFilter、doGet、doPost这类处理请求的方法中新建线程去处理该请求:
AsyncContext asyncContext = request.startAsync();
asyncContext.start(new MyRunnable(asyncContext));
调用ServletRequest的startAsync方法获得一个AsyncContext 的实例。该实例包含了当前的ServletRequest和ServletResponse,可通过getter方法取出。调用AsyncContext实例的start方法并传一个Runnable实例来开启一个新线程去执行Runnable中的run方法。如果run方法中需要用到request,则可以把AsyncContext实例传递过去(也可以使用匿名内部类,就可以直接访问AsyncContext实例了)。
在run方法的最后需要调用AsyncContext实例的complete()方法(用以结束当前线程)或者dispatch(String url)方法(用以将请求转发给url指定的servlet并结束当前线程)。
2. 异步监听器:
AsyncListener接口:当异步操作启动完毕、执行完成、执行出错、执行超时时,接口中对应的那个方法会被调用。接口中的每个方法都会接收一个AsyncEvent对象,可以从中获取到AsyncContex和当执行出错时抛出的异常。
异步监听器的注册:在调用AsyncContext对象的start方法前,通过调用AsyncContext对象的addListener(a AsyncListener instance)方法进行注册。
1. 文件上传
前端代码编写:
--在form标签中添加enctype="multipart/form-data"。
--添加一个type为file的input标签(该input会显示为一个选择要上传的文件的按钮)(增加multiple属性则可以用此按钮选择多个要上传的文件)。
服务端代码编写:
--为Servlet增加MultipartConfig注解
--调用request的getPart(String name)方法获取表单中文件的那Part。
--调用Part对象的write方法将文件写入硬盘。
2.文件下载
两种方式:
(i). 客户端直接访问要下载的文件的url(文件位于web应用程序目录下)
(ii). 在servlet中将文件放进response中回复回去:
--设置response的contentType为"application/file-type",若设置file-type为octet-stream,则浏览器会提示用户进行下载;若设置file-type为pdf或者图片之类的,则浏览器会直接解析显示出来。
--将文件通过输入流读取成二进制放进byte数组中
--将byte数组中的内容通过输出流写进response中。