JavaWeb基础之Servlet全解析
Servlet 是在服务器上运行的小程序,其主要功能在于交互式地浏览和修改数据,生成动态Web内容。狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类。
本文内容:
1.Servlet开发步骤
学习Servlet,我们先看一下Servlet程序怎么开发,大体上可以分为五步,如下所示:
1)编写java类,继承HttpServlet类,重写doGet和doPost方法
public class FirstServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse response)
throws ServletException, IOException {
//向浏览器输出内容
response.getWriter().write("this is first servlet!");
}
}
2)在web.xml文件中进行配置
<!-- 配置一个servlet -->
<!-- servlet的配置 -->
<servlet>
<!-- servlet的内部名称,自定义。尽量有意义 -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的类全名: 包名+简单类名 -->
<servlet-class>cn.acamy.FirstServlet</servlet-class>
</servlet>
<!-- servlet的映射配置 -->
<servlet-mapping>
<!-- servlet的内部名称,一定要和上面的内部名称保持一致!! -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的映射路径(访问servlet的名称) -->
<url-pattern>/first</url-pattern>
</servlet-mapping>
3)部署,servlet程序的class码拷贝到WEB-INF/classes目录
4)启动tomcat服务器
5)通过URL访问
http://localhost:8080/demo/first
http:// :http协议
localhost: :到本地的hosts文件中查找是否存在该域名对应的IP地址(127.0.0.1)
8080 : 找到tomcat服务器
/demo : 在tomcat的webapps目录下找 demo的目录
/first: 资源名称。
1)在demo的web.xml中查找是否有匹配的url-pattern的内容(/first)
2)如果找到匹配的url-pattern,则使用当前servlet-name的名称到web.xml文件中查询是否相同名称的servlet配置
3)如果找到,则取出对应的servlet配置信息中的servlet-class内容:
字符串: cn.acamy.FirstServlet
通过反射:
a)构造FirstServlet的对象
b)然后调用FirstServlet里面的方法
2.本文涉及到的相关类
2.1 HttpServlet继承体系
我们写的Servlet程序都是要直接继续HttpServlet,由下面的类图可以看出该类有service方法的重写和重载,以及doGet,doPost...方法,继承于抽象类GenericServlet,GenericServlet实现了接口Servlet和ServletConfig,Servlet接口里面定义了init,service,destroy,getServletConfig,getServletInfo方法,ServletConfig里面定义了获取配置信息的相关方法。
2.2 HttpServletRequest与HttpServletResponse
HttpServletRequest继承于ServletRequest,封装了Servlet程序的请求信息,HttpServletResponse继承于ServletResponse,封装了Servlet程序的响应信息。
2.3 ServletContext
ServletContext是Servlet上下文对象,封装了当前的web应用环境。
3. Servlet的生命周期
Servlet程序的生命周期是由tomcat服务器控制的!
我们先来看一个Demo:
public class LifeDemo extends HttpServlet {
/**
* 1.构造方法
*/
public LifeDemo(){
System.out.println("1.servlet对象被创建了。");
}
/**
* 2.init方法
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("2.init方法被调用");
}
/**
* 3.service方法
*/
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("3.service方法被调用");
}
/**
* 4.destroy方法
*/
@Override
public void destroy() {
System.out.println("4.servlet对象销毁了");
}
}
效果如下:
1. 构造方法: 创建servlet对象的时候调用。默认情况下,第一次访问servlet的时候创建servlet对象。只调用1次。证明servlet对象在tomcat是单实例的。
2. init方法: 创建完servlet对象的时候调用。只调用1次。
3. service方法: 每次发出请求时调用。调用n次。
4. destroy方法: 销毁servlet对象的时候调用。停止服务器或者重新部署web应用时销毁servlet对象。 只调用1次。
Servlet生命周期如下图所示:
4.HttpServlet的service方法源码
从上面的生命周期可以看出,service方法在浏览器每发送一次请求就被调用一次。但我如果要调用doGet,doPost,doHead等方法时,又该怎么实现呢?先看一下HttpServlet里面与该方法相关的源码:
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";
//该方法是重写Servlet接口中的方法,可以看到先把ServletRequest和
//ServletResponse转化为其子类HttpServletRequest,HttpServletResponse
//然后调用重载的service(HttpServletRequest req, HttpServletResponse resp)
@Override
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);
}
//注意这个方法是上面service方法的重载,并且是protected面不是public修饰符修
//饰,说明该方法只能被自身或子类来调用。
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//首先得到方法名
String method = req.getMethod();
// 然后看该方法名与实现的哪个方法相对应,就执行哪个方法
// 如得到的方法名为POST,则对应到METHOD_POST,
// 执行doPost(req, resp);
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
//如果没有找到则说明没有该方法的实现,给出错误提示
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
结论:无论我们需要调用的是GET,POST还是其它方法,我们首先通过调用service(ServletRequest req, ServletResponse res),然后指向service(HttpServletRequest req, HttpServletResponse resp),最终通过方法名来进行匹配,实现转向,调用想doGET或POST或其它的方法。
5.Servlet的路径问题与自动加载
5.1 Servlet 的映射路径
<servlet-mapping>
<!-- servlet的内部名称,一定要和上面的内部名称保持一致!! -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的映射路径(访问servlet的名称) -->
<url-pattern>/first</url-pattern>
</servlet-mapping>
Servlet的映射路径分为精确匹配和模糊匹配
注意:
1)url-pattern要么以 / 开头,要么以*开头。 例如, itcast是非法路径。
2)不能同时使用两种模糊匹配,例如 /demo/*.do是非法路径
3)当有输入的URL有多个servlet同时被匹配的情况下:
3.1 精确匹配优先。(长的最像优先被匹配)
3.2 以后缀名结尾的模糊url-pattern优先级最低!!!
5.2 Servlet 的缺省路径
servlet的缺省路径(<url-pattern>/</url-pattern>)是在tomcat服务器内置的一个路径。该路径对应的是一个DefaultServlet(缺省Servlet)。这个缺省的Servlet的作用是用于解析web应用的静态资源文件。
问题: URL输入http://localhost:8080/demo/index.html 如何读取文件?
1)到当前demo应用下的web.xml文件查找是否有匹配的url-pattern。
2)如果没有匹配的url-pattern,则交给tomcat的内置的DefaultServlet处理
3)DefaultServlet程序到demo应用的根目录下查找是存在一个名称为index.html的静态文件。
4)如果找到该文件,则读取该文件内容,返回给浏览器。
5)如果找不到该文件,则返回404错误页面。
结论: 先找动态资源,再找静态资源。
5.3 Servlet 的自动加载
默认情况下,第一次访问servlet的时候创建servlet对象。如果servlet的构造方法或init方法中执行了比较多的逻辑代码,那么导致用户第一次访问sevrlet的时候比较慢。
改变servlet创建对象的时机: 提前到加载web应用的时候!
在servlet的配置信息中,加上一个<load-on-startup>即可!
<servlet>
<servlet-name>LifeDemo</servlet-name>
<servlet-class>gz.itcast.c_life.LifeDemo</servlet-class>
<!-- 让servlet对象自动加载 -->
<load-on-startup>1</load-on-startup> 注意: 整数值越大,创建优先级越低!!
</servlet>
自动加载效果如下图所示,可以看到Servlet在Tomcat服务器启动时就执行了构造方法和init方法:
6. Servlet的有参和无参init方法
有参数的init方法是servlet的生命周期方法,一定会被tomcat服务器调用。如果要编写初始代码,不需要覆盖。无参数的init方法是servlet的编写初始化代码的方法。是Sun公司设计出来专门给开发者进行覆盖,然后在里面编写servlet的初始逻辑代码的方法。
public class InitDemo extends HttpServlet {
/*
* @Override public void init(ServletConfig config) throws ServletException
* { System.out.println("有参数的init方法"); }
*/
@Override
public void init() throws ServletException {
// System.out.println("无参数的init方法");
}
}
7.Servlet的多线程并发问题
servlet对象在tomcat服务器是单实例多线程的。因为servlet是多线程的,所以当多个servlet的线程同时访问了servlet的共享数据,如成员变量,可能会引发线程安全问题。
解决办法:
1)把使用到共享数据的代码块进行同步(使用synchronized关键字进行同步)
2)建议在servlet类中尽量不要使用成员变量。如果确实要使用成员,必须同步。而且尽量缩小同步代码块的范围。(哪里使用到了成员变量,就同步哪里!!),以避免因为同步而导致并发效率降低。
public class TheradDemo extends HttpServlet {
int count = 1;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
synchronized (TheradDemo.class) {//锁对象必须唯一。建议使用类对象
response.getWriter().write("你现在是当前网站的第"+count+"个访客"); //线程1执行完 , 线程2执行
//线程1还没有执行count++
/*try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
count++;
}
}
}
8.ServletConfig对象
ServletConfig对象: 主要是用于加载servlet的初始化参数。在一个web应用可以存在多个ServletConfig对象(一个Servlet对应一个ServletConfig对象)
8.1 对象创建和得到
创建时机: 在创建完servlet对象之后,在调用init方法之前创建。
得到对象: 直接从有参数的init方法中得到!!!
8.2 servlet的初始化参数配置
<servlet>
<servlet-name>ConfigDemo</servlet-name>
<servlet-class>gz.itcast.f_config.ConfigDemo</servlet-class>
<!-- 初始参数: 这些参数会在加载web应用的时候,封装到ServletConfig对象中 -->
<init-param>
<param-name>path</param-name>
<param-value>e:/b.txt</param-value>
</init-param>
</servlet>
** Demo:**
public class ConfigDemo extends HttpServlet {
/**
* 以下两段代码GenericServlet已经写了,我们无需编写!!
*/
/*private ServletConfig config;*/
/**
* 1)tomcat服务器把这些参数会在加载web应用的时候,封装到ServletConfig对象中
* 2)tomcat服务器调用init方法传入ServletConfig对象
*/
/*@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
}*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/**
* 读取servlet的初始参数
*/
String path = this.getServletConfig().getInitParameter("path");
File file = new File(path);
//读取内容
BufferedReader br = new BufferedReader(new FileReader(file));
String str = null;
while( (str=br.readLine())!=null ){
System.out.println(str);
}
//查询当前servlet的所有初始化参数
Enumeration<String> enums = this.getServletConfig().getInitParameterNames();
while(enums.hasMoreElements()){
String paramName = enums.nextElement();
String paramValue = this.getServletConfig().getInitParameter(paramName);
System.out.println(paramName+"="+paramValue);
}
//得到servlet的名称
String servletName = this.getServletConfig().getServletName();
System.out.println(servletName);
}
}
9.ServletContext对象
Servlet的上下文对象。表示一个当前的web应用环境。一个web应用中只有一个ServletContext对象。
9.1 对象创建和得到
创建时机:加载web应用时创建ServletContext对象。
得到对象: 从当前Servlet或者ServletConfig对象的getServletContext方法得到
** Demo:**
public class ContextDemo extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.得到ServletContext对象
// ServletContext context = this.getServletConfig().getServletContext();
ServletContext context = this.getServletContext(); // (推荐使用)
// 2.得到web应用路径
/**
* web应用路径:部署到tomcat服务器上运行的web应用名称
*/
String contextPath = context.getContextPath();
System.out.println(contextPath);
System.out.println("参数" + context.getInitParameter("AAA"));
Enumeration<String> enums = context.getInitParameterNames();
while (enums.hasMoreElements()) {
String paramName = enums.nextElement();
String paramValue = context.getInitParameter(paramName);
System.out.println(paramName + "=" + paramValue);
}
// 尝试得到ConfigDemo中的servlet参数
String path = this.getServletConfig().getInitParameter("path");
System.out.println("path=" + path);
// 2.把数据保存到域对象中
context.setAttribute("name", "eric");
// 2.从域对象中取出数据
// String name = (String) context.getAttribute("name");
/**
* 案例:应用到请求重定向
*/
response.sendRedirect(contextPath + "/index.html");
}
}
9.2 转发与重定向
转发与重定向都能实现页面的跳转,但在实际应用中大不相同,比较如下;
注意: 如果要使用request域对象进行数据共享,只能用转发技术!!!