很多小伙伴对Java Web后台开发感兴趣,但是又苦于入门,一是书上和网络上的其他资料大多是千篇一律,要么就是偏知识,大量篇幅介绍Servlet相关内容,看不进去,要么太浅,看完又觉得好像并没有学到什么。所以从大家的需求出发,内容主要包括以下四个部分:
- 第一部分从javax.servlet.http.HttpServlet类源码开始,了解一次http请求过来后,服务器的处理逻辑是什么样儿的。
- 第二部分是动手实践部分,如何在Eclipse中创建Web工程,并对web工程的目录结构进行一个分析。
- 第三部分结合Tomcat中的examples了解Servlet相关类的应用。
- 第四部分设计模式篇来看下Template Pattern在Servlet中的应用,不要觉得设计模式很高深,是无法理解的知识,我们平时在写代码的过程中已经不知不觉在应用它们了。
需要的小伙伴可以下载FirstJavaWeb工程来对照着看。
HttpServlet源码
Servlet是运行在服务器端的程序,用于处理及响应客户端的请求,它是个特殊的Java类,这个Java类必须继承javax.servlet.http.HttpServlet,HttpServlet类提供了不同的方法用于处理客户端的请求。
方法
描述
doDelete
用于处理DELETE请求
doGet
用于处理GET请求
doHead
用于处理HEAD请求
doOptions
用于处理OPTIONS请求
doPost
用于处理POST请求
doPut
用于处理PUT请求
doTrace
用于处理TRACE请求
getLastModified
返回一个long整数,值为所请求数据的最后修改时间相对于GMT时间1970年1月1号0时0分0秒的毫秒数
service
用于映射请求,根据请求的HTTP方法,调用do Method
根据HttpServlet service方法的处理逻辑,HttpServlet目前只可响应客户端的GET,HEAD,POST,PUT,DELETE,OPTIONS,TRACE请求。
http请求
描述
GET
获取服务器上某一资源
HEAD
HEAD和GET本质是一样的,区别在于HEAD请求的响应不包含响应实体,而仅仅包含响应消息头
POST
向服务器提交数据
PUT
PUT和POST极为相似,PUT通常指定了资源的存放位置,向指定资源位置上传其最新内容
DELETE
删除服务器上某一个资源
OPTIONS
获取服务器针对特定资源所支持的HTTP请求方法,请求头等
TRACE
回显服务器收到的请求,主要用于测试或诊断
PUT,DELETE,OPTIONS,TRACE请求并不常使用,OPTIONS请求作者只在处理非简单CORS(Cross-origin resource sharing)的时候遇见过。
通常我们的请求只有GET和POST两种,为了响应这两种请求,一般需要重写doGet和doPost两个方法,也可以通过重写service方法的方式。但是注意如果你重写的service方法没有调用do method 方法,即使你在Servlet中又重写了其他do method 方法也是不会被调用的,原因我们看了HttpServlet类的source code就会明白。
public abstract class HttpServlet extends GenericServlet
{
public HttpServlet()
{
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if(protocol.endsWith("1.1"))
resp.sendError(405, msg);
else
resp.sendError(400, msg);
}
protected long getLastModified(HttpServletRequest req)
{
return -1L;
}
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
if(DispatcherType.INCLUDE.equals(req.getDispatcherType()))
{
doGet(req, resp);
} else
{
NoBodyResponse response = new NoBodyResponse(resp);
doGet(req, response);
response.setContentLength();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if(protocol.endsWith("1.1"))
resp.sendError(405, msg);
else
resp.sendError(400, msg);
}
protected void doPut(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_put_not_supported");
if(protocol.endsWith("1.1"))
resp.sendError(405, msg);
else
resp.sendError(400, msg);
}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_delete_not_supported");
if(protocol.endsWith("1.1"))
resp.sendError(405, msg);
else
resp.sendError(400, msg);
}
private static Method[] getAllDeclaredMethods(Class c)
{
if(c.equals(javax/servlet/http/HttpServlet))
return null;
Method parentMethods[] = getAllDeclaredMethods(c.getSuperclass());
Method thisMethods[] = c.getDeclaredMethods();
if(parentMethods != null && parentMethods.length > 0)
{
Method allMethods[] = new Method[parentMethods.length + thisMethods.length];
System.arraycopy(parentMethods, 0, allMethods, 0, parentMethods.length);
System.arraycopy(thisMethods, 0, allMethods, parentMethods.length, thisMethods.length);
thisMethods = allMethods;
}
return thisMethods;
}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
Method methods[] = getAllDeclaredMethods(getClass());
boolean ALLOW_GET = false;
boolean ALLOW_HEAD = false;
boolean ALLOW_POST = false;
boolean ALLOW_PUT = false;
boolean ALLOW_DELETE = false;
boolean ALLOW_TRACE = true;
boolean ALLOW_OPTIONS = true;
for(int i = 0; i < methods.length; i++)
{
Method m = methods[i];
if(m.getName().equals("doGet"))
{
ALLOW_GET = true;
ALLOW_HEAD = true;
}
if(m.getName().equals("doPost"))
ALLOW_POST = true;
if(m.getName().equals("doPut"))
ALLOW_PUT = true;
if(m.getName().equals("doDelete"))
ALLOW_DELETE = true;
}
String allow = null;
if(ALLOW_GET)
allow = "GET";
if(ALLOW_HEAD)
if(allow == null)
allow = "HEAD";
else
allow = (new StringBuilder()).append(allow).append(", HEAD").toString();
if(ALLOW_POST)
if(allow == null)
allow = "POST";
else
allow = (new StringBuilder()).append(allow).append(", POST").toString();
if(ALLOW_PUT)
if(allow == null)
allow = "PUT";
else
allow = (new StringBuilder()).append(allow).append(", PUT").toString();
if(ALLOW_DELETE)
if(allow == null)
allow = "DELETE";
else
allow = (new StringBuilder()).append(allow).append(", DELETE").toString();
if(ALLOW_TRACE)
if(allow == null)
allow = "TRACE";
else
allow = (new StringBuilder()).append(allow).append(", TRACE").toString();
if(ALLOW_OPTIONS)
if(allow == null)
allow = "OPTIONS";
else
allow = (new StringBuilder()).append(allow).append(", OPTIONS").toString();
resp.setHeader("Allow", allow);
}
protected void doTrace(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String CRLF = "\r\n";
StringBuilder buffer = (new StringBuilder("TRACE ")).append(req.getRequestURI()).append(" ").append(req.getProtocol());
String headerName;
for(Enumeration reqHeaderEnum = req.getHeaderNames(); reqHeaderEnum.hasMoreElements(); buffer.append(CRLF).append(headerName).append(": ").append(req.getHeader(headerName)))
headerName = (String)reqHeaderEnum.nextElement();
buffer.append(CRLF);
int responseLength = buffer.length();
resp.setContentType("message/http");
resp.setContentLength(responseLength);
ServletOutputStream out = resp.getOutputStream();
out.print(buffer.toString());
out.close();
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if(method.equals("GET"))
{
long lastModified = getLastModified(req);
if(lastModified == -1L)
{
doGet(req, resp);
} else
{
long ifModifiedSince;
try
{
ifModifiedSince = req.getDateHeader("If-Modified-Since");
}
catch(IllegalArgumentException iae)
{
ifModifiedSince = -1L;
}
if(ifModifiedSince < (lastModified / 1000L) * 1000L)
{
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else
{
resp.setStatus(304);
}
}
} else
if(method.equals("HEAD"))
{
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else
if(method.equals("POST"))
doPost(req, resp);
else
if(method.equals("PUT"))
doPut(req, resp);
else
if(method.equals("DELETE"))
doDelete(req, resp);
else
if(method.equals("OPTIONS"))
doOptions(req, resp);
else
if(method.equals("TRACE"))
{
doTrace(req, resp);
} else
{
String errMsg = lStrings.getString("http.method_not_implemented");
Object errArgs[] = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
private void maybeSetLastModified(HttpServletResponse resp, long lastModified)
{
if(resp.containsHeader("Last-Modified"))
return;
if(lastModified >= 0L)
resp.setDateHeader("Last-Modified", lastModified);
}
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
try
{
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
}
catch(ClassCastException e)
{
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
private static final long serialVersionUID = 1L;
private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
private static final String HEADER_LASTMOD = "Last-Modified";
private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
}
HttpServlet是一个不包含任何抽象方法的抽象类,继承GenericServlet类,很巧妙的编码,既不能够直接创建它的实例,也不强迫子类去实现任何方法,就是说我们自己的Servlet类可以通过继承它,不重写任何方法就能够直接对外提供服务。
HttpServlet类中有两个的service方法,public void service(ServletRequest req, ServletResponse res)方法是其父类GenericServlet类的抽象方法实现,作用是接受客户端的请求并将其传递给重载的service方法,protected void service(HttpServletRequest req, HttpServletResponse resp)。容器会针对每个客户端请求创建一个处理线程,准确来说应该是使用线程池中空闲的线程,并创建Request和Response对象传递给处理线程,就是说浏览器的一次http请求的所有信息都封装在HttpServletRequest中,而HttpServletResponse对象代表服务器对客户端的响应,可以通过操作这两个对象来交换数据。
protected void service(HttpServletRequest req, HttpServletResponse resp),是HttpServlet类定义的重载方法,访问权限是protected,可用于子类继承。方法首先获取本次请求的http方法,并根据方法的类型去进入相应的if else分支处理,除了GET请求处理相对复杂一些,其他处理很简单,直接调用相应请求的do method 方法,当用户的请求不是上面列出的请求方法时,会向客户端返回501状态码,Method is not implemented by this servlet for this URI。在service中的GET请求逻辑部分首先调用getLastModified(HttpServletRequest req)方法得到一个long整数lastModified,如果为-1L则调用doGet方法,否则获取本次请求的“If-Modified-Since”头部得值,“If-Modified-Since”带一个时间值代表请求数据的上次修改时间,它会与lastModified值比较,如果lastModified值比较新,容器调用maybeSetLastModified(resp, lastModified)方法,这个方法的作用是在响应头中加一个“Last-Modified”头,告诉浏览器你应该更新本地缓存了。除此来之外的任何结果,则返回304状态码,告诉浏览器从你上次访问我之后,请求的网页未修改,你可以使用本地缓存直接加载页面,节省带宽和开销。我们看到子类如果不重写getLastModified方法的话,这个方法将永远返回-1L,每次GET请求都只是调用doGet方法而已。
当浏览器发现响应中有“Last-Modified”头部,那么它在下一次请求同一数据时会加上“If-Modified-Since”请求头,值为上一次响应的“Last-Modified”时间值。304状态码告诉客户端自从上次请求后,请求的网页未修改,本地cache的页面是最新的。服务器返回此响应时,不会返回网页内容,也就是说浏览器只在每次启动后第一次访问这个页面时,才向服务器发出请求,对于后续的访问都是直接加载本地缓存的页面,这在提高网站性能方面特别有用。
带大家演示一下,我们先写一个servlet,代码贴上,重写了父类的doGet方法,逻辑也很简单,就是打印系统当前时间相对GMT1970年1月1日0点0时0分的毫秒数。
@WebServlet(urlPatterns = ("/example/one"))
public class ExampleOneServlet extends HttpServlet {
/**
* serialVersionUID:TODO.
*/
private static final long serialVersionUID = 6686766899053457864L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("Frist Servlet " + System.currentTimeMillis());
}
// @Override
// protected long getLastModified(HttpServletRequest req) {
// long now = System.currentTimeMillis();
// System.out.println("getLastModified: " + now);
// return now;
// }
// @Override
// protected long getLastModified(HttpServletRequest req) {
// return req.getDateHeader("If-Modified-Since");
// }
}
Tomcat中运行,浏览器查看,此时请求头和响应头中并没有任何特殊的头部。
解注释第一个重写的getLastModified方法并运行,这个getLastModified方法每次返回一个新的时间值,并且总是大于请求头”If-Modified-Since”的时间值。这次我们看到响应头中多了“Last-Modified”字段,刷新下浏览器,此时请求头中多了”If-Modified-Since”,并且响应头中依然包含“Last-Modified”头,响应状态码为200,浏览器窗口中显示的字符串的时间值也一直在随着时间推进。
注释这个getLastModified方法,并解注释第二个重写的getLastModified方法,这个方法每次返回的值为请求头”If-Modified-Since”的值。刷新下浏览器,我们看到此时的响应头中并没有”Last-Modified”头,并且响应的状态码为304。再刷新下浏览器,我们看到浏览器窗口中显示的字符串中的时间值没变,证明了此时浏览器加载的是缓存的数据,并不是服务器端传回来的新数据。还有”If-Modified-Since”的时间值也没有改变。
Eclipse中创建Java Web工程
打开Eclipse,注意安装用于Web开发的Java EE版本的Eclipse,将下载好的Apache Tomcat® [Tomcat是一个轻量级免费开源的Servlet容器,能够运行我们的Java Web应用] 配置进来。File->New->Other。点击Server->next。相关工具大家也可以直接去我的百度网盘下载,Eclipse两个版本都可以用,zip是luna,exe是最新的neon,自己安装。
选择相应的Tomcat版本->next,找到Tomcat安装路径,Finish,一个server就给配好了。
接下来我们创建一个Java Dynamic Web Project。File->New->Dynamic Web Project,如若没找到Dynamic Web Project菜单,则File->New->other,在Wizards输入要搜索的项。在弹出的New Dynamic Web Project窗口输入你的Project name,一路next,在点击最后Finish按钮之前勾选Generate web.xml deployment discriptor。Servlet 3.0之后支持注解的配置方式,所以生成web.xml成为可选项,这里勾选上,我们一会儿会演示使用注解和xml文件两种方式配置Servlet。
看下这个project的Web工程目录结构,就是下面这个WebContent文件夹,这个文件夹下对应着project部署到容器中时的上下文根路径,根目录,就是我们在浏览器输入根URL所访问的服务器地址,其他资源地址都是相对于这个地址的相对路径。
再来看下这个project部署到容器中时,它的目录结构。在这个project上右键->Run As->Run on Server。根据这个路径找到eclipse中运行的tomcat的发布文件夹%you eclipse workplace%\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps
找到我们的project目录,进去后发现其下内容与eclipse中project的WebContent下的内容完全一样,验证了我们之前的说法,小伙伴想问why?回到Eclipse中,project上右键->Properties,找到Deployment Assembly项,Deployment Assembly项为web工程指定在发布的时候要部署到容器中的部件。我们看到WebContent文件夹对应的就是部署后的项目的上下文根路径,就这么个原因。
我们可以更改这个目录为我们想要的目录,将Deployment Assembly中的WebContent Remove,新创建一个webapp目录,并照着WebContent目录下的文件,文件夹新建一份,之后把它配置为Deploy Path。此时我们的项目结构是这样的:
我又新加了一个index.jsp,内容很简单,显示”hello world!”,运行一下,成功,完全是自己掌控。
对于使用Eclipse的初学者来说,知道这个相当重要,比如当使用maven spring的时候,我们的项目第一次运行后可能会起不起来,出现第三方库的ClassNotFoundException,这个时候一般去检查Deployment Assembly中是否将Maven Dependencies库加到了WEB-INF/lib下面,很快就解决了。
回过头看下WebContent目录下的内容,META-INF,看名字就能明白,存放一些meta information,这个目录下的文件应该都是build工具来生成的。WEB-INF是一个特殊的文件夹,容器会**保护此文件下的内容不被浏览器访问**,就是说我们直接在浏览器通过URL的方式是访问不到的,所以我们一般会将图片文件夹,JS,CSS文件夹,以及我们的JSP页面放到WEB项目的根目录下,而将配置文件放到WEB-INF下面。WEB-INF的lib文件夹存放本应用所依赖的第三方包。
web.xml文件用来配置Web应用的组件。我们自己的Servlet要在文件中配置后才能访问,不过Servlet 3.0之后也可以不用配置文件而直接使用注解的方式配置servlet,两种方式各有利弊,注解散落在个各类中,不好管理,而使用配置文件都集中在一处配置,一目了然,但是随着内容的增加会略显臃肿。
Servlet创建
在src文件加上右键->New->Class->输入Class Name->点击Browse->搜索HttpServlet->点击OK->点击Finish,一个HttpServlet类就创建完成了。
使用注解的方式配置Servlet,只需要在我们创建的类上加上注解@WebServlet(urlPatterns = ("you path"))
即可,直接运行,页面显示405状态码,HttpServlet中doGet方法的默认实现,这就OK了,你也可以重写doGet方法,加入自己的处理逻辑。
使用文件配置的方式,打开web.xml,加入如下配置即可:
<servlet>
<servlet-name>exampleOne</servlet-name>
<servlet-class>example.ExampleTwoServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>exampleOne</servlet-name>
<url-pattern>/example/two</url-pattern>
</servlet-mapping>
Tomcat自带Examples
这一节的目的是通过Tomcat中的Examples了解相关类的应用。点击Examples出现404,是因为安装Tomcat时没有勾选examples的选项,examples和host-manager一样,是Tomcat自己写的web应用而已,同样部署在Tomcat中,我们在%CATALINA_HOME%\webapps
下都能找到,同样的也可以从别的途径获得examples代码,放到webapps下面就可以访问了。
Servlets examples、JSP Examples、WebSocket Examples,我们只过了Servlets examples,例子都很适合初学者阅读。我把源码中从上到下涉及到的所有类及方法总结出来,自己对照着去看代码。
ResourceBundle(能够方便读取.properties配置文件)
方法
描述
ResourceBundle.getBundle(String baseName, Locale locale)
返回具有给定基本名称和语言环境的ResourceBundle对象
String getString(String key)
从此ResourceBundle资源包中获取给定键的字符串
HttpServletRequest(获取请求头和请求参数)
方法
描述
Locale getLocale()
获取客户端所使用的本地语言
String getMethod()
返回http请求的方法名称
String getRequestURI()
a String containing the part of the URL from the protocol name up to the query string ()
String getProtocol()
返回协议名称和协议版本号
String getMethod()
返回http请求的方法名称
String getPathInfo()
返回请求URL中Servlet路径之后查询字符串之前的额外路径信息。如果没有额外路径信息,返回null
String getRemoteAddr()
获取发送请求的客户端的IP地址
Object getAttribute(String name)
获取指定属性的属性值
Enumeration
获取所有请求参数的名称
String getHeader(String name)
获取指定请求头的值
String getParameter(String name)
获取请求参数的值
Cookie[] getCookies()
获取本次请求携带地所有cookies
HttpSession getSession(boolean create)
返回与当前请求相关联的HttpSession对象,create为true时,如果当前会话无效,则创建,否则返回null
Servlet 3.1 API文档对String getRequestURI()方法的描述是a String containing the part of the URL from the protocol name up to the query string (),翻译过来是URL中的协议之后到查询字符串之前的部分,但实际上得到是域名之后到查询字符串的部分,理解有出入,故不敢随便翻译。看下它的底层实现return request.getServletContext();
,实际是就是获取了ServletContextPath。
String getPathInfo()方法返回请求URL中Servlet路径之后查询字符串之前的额外路径信息。如果没有额外路径信息,返回null。查询字符串,客户端发起的GET请求如果携带参数,如http://localhost:8088/examples/servlets/servlet/RequestInfoExample?param1=value1¶m2=value2
,的形式?号后面的字符串会被解析为查询字符串,格式一般是?param1=value1¶m2=value2。而String getPathInfo()返回servlet path和查询字符串之间的路径,如http://localhost:8088/examples/servlets/servlet/RequestInfoExample/user/xiaoming?param1=value1¶m2=value2
,我们配置的servlet path是/servlets/servlet/RequestInfoExample,那么extra path就是/user/xiaoming,不要搞混了。
HttpServletResponse(代表服务器对客户端的响应)
方法
描述
void setContentType(String type)
设置发送到客户端的响应的内容类型
void setCharacterEncoding(String charset)
设置发送到客户端的响应的字符编码(MIME字符集),例如,设置为UTF-8
PrintWriter getWriter()
获取页面输出流
void addCookie(Cookie cookie)
设置cookie
Cookie(浏览器端跟踪用户会话状态)
方法
描述
String getName()
获取cookie的名字
String getValue()
获取cookie的值
HttpSession(服务器端跟踪用户会话状态)
方法
描述
long getCreationTime()
获取session的创建时间
long getLastAccessedTime()
获取上一次访问当前会话session的时间
String getId()
获取当前会话ID
void setAttribute(String name, Object value)
设置session范围内属性
Enumeration
获取所有属性的名字
Object getAttribute(String name)
获取指定属性名的value
Session和cookie区别(摘自网络):
- 都是用来跟踪用户的会话状态,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案
- cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session
- session是Java对象,会在一定时间内保存在服务器上,当访问增多,会比较占用服务器内存
- 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
Template Pattern
In Template pattern, an abstract class exposes defined way(s)/template(s) to execute its methods. Its subclasses can override the method implementation as per need but the invocation is to be in the same way as defined by an abstract class. This pattern comes under behavior pattern category.[在模板模式中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。]
在Servlet中,模板方法由service()方法担任,子类按需重写do method 方法,不过需要注意一点,更多时候我们会将Template Method定义为final方法,防止子类重写父类模板。