Servlet概述
Servlet是Sun提供的动态web资源开发技术。其本质是一个java类,要求这个类必须实现Servlet接口,以便服务器能够调用。
开发Servlet的两个步骤:
(1) 写一个java程序实现Servlet接口(或继承其实现类GenericServlet或HttpServlet)
public class FirstServlet extends GenericServlet{
public void service(ServletRequest req, ServletResponse res) throws ServletException, java.io.IOException{
}
}
(2) 将编译好的带包的.class放到WEB-INF/classes下,配置web应用的 web.xml注册Servlet
<servlet>
<servlet-name>FirstServlet</servlet-name> <servlet-class>com.diysoul.FirstServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>FirstServlet</servlet-name> <url-pattern>/FirstServlet</url-pattern> </servlet-mapping>Servlet生命周期
servlet第一次被访问的时候在内存中创建对象,在创建后立即调用init()方法进行初始化。
对于每一次请求都会调用service(ServletRequest req, ServletResponse res)方法处理请求,此时Request对象封装请求信息,Response对象(最初是空的)代表响应消息,传入到service方法里供使用。
当service方法处理完成后,返回服务器,服务器根据Response中的信息组织成响应消息返回给浏览器。
响应结束后servlet并不销毁,一直驻留在内存中等待下一次请求。
直到服务器关闭或web应用被移除出虚拟主机,servlet对象销毁并在销毁前调用destroy()方法做一些善后的工作。
Servlet接口的继承结构
Servlet接口:定义了一个servlet应该具有的方法,所有的Servlet都应该直接或间接实现此接口。
GenericServlet:对Servlet接口的默认实现,通用Servlet,这是一个抽象类,其中的大部分方法都做了默认实现,只有service方法是一个抽象方法需要继承者自己实现。HttpServlet: 对HTTP协议进行了优化的Servlet,继承自GenericServlet类,并且实现了其中的service抽象方法,默认的实现中判断了请求的请求方式,并根据请求方式的不同分别调用不同的doXXX()方法。通常直接继承HttpServlet即可。Servlet配置与匹配规则
Servlet配置是通过在web.xml中添加servlet及servlet-mapping来实现的,其放置位置为: /WebRoot/WEB-INF/web.xml
在web.xml中利用<servlet><servlet-mapping>标签注册一个Servlet,一个<servlet>可以对应多个<servlet-mapping>。
<servlet>
<servlet-name>FirstServlet</servlet-name> <!--注意:此处要的是一个Servlet的完整类名,不是包含.java或.class扩展的文件路径--!> <servlet-class>com.diysoul.FirstServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>FirstServlet</servlet-name> <url-pattern>/FirstServlet</url-pattern> </servlet-mapping>1.Servlet的配置包括两部分
(1)<servlet>配置Servlet的名字和完整类路径:
servlet-name是自定义的,就是给Servlet取个名字。 servlet-class是Servlet完整的类,就是从一开始的包一直“.”到该Servlet。 (2)<servlet-mapping>是用来截获请求的,包括servlet-name和url-pattern。 servlet-name跟<servlet>中的servlet-name是对应的,两个servlet-name一定要一致,否则会找不到对应的Servlet。 url-pattern是截获请求的规则,当表单提交的时候,会根据特定的规则调用相应的Servlet。下面会具体阐述。2.url-pattern匹配规则
(1).完全匹配
如:<url-pattern>/servlet/MyServlet.do</url-pattern>(2).目录匹配 如:<url-pattern>/servlet/*</url-pattern>(3).扩展名匹配,包括两种形式:以”/"开头和以”/*”结尾,"*.扩展名" 如:<url-pattern>*.do</url-pattern>以”/"开头和以”/*”结尾的匹配形式是用来做路径映射的。
以”*.”开头的匹配是用来做扩展映射的。(4).缺省servlet:如果一个servlet的对外访问路径被设置为/,则该servlet就是一个缺省servlet,其他servlet不处理的请求都由它来处理。
注意:在conf/web.xml中配置了缺省servlet,对静态资源的访问和错误页面的输出都是由这个缺省servlet来处理。如果在web应用中配置了一个缺省servlet,此时conf/web.xml中的缺省servlet会被覆盖,会导致静态web资源无法访问。因此不推荐配置。
容器查找规则:
a、容器会首先查找完全匹配,如果找不到,再查找目录匹配,如果也找不到,就查找扩展名匹配。
b、如果一个请求匹配多个“目录匹配”,容器会选择最长的匹配。c、“*.”匹配级别最低。 如:servletA的url-pattern为/test/*,而servletB的url-pattern为/test/b/*,此 时访问http://localhost/test/b时,容器会选择路径最长的servlet来匹配,也就是这里的servletB。d、如果都不匹配,调用缺省Servlet。
对于如下的一些映射关系:
Servlet1 映射到 /abc/* Servlet2 映射到 /* Servlet3 映射到 /abc Servlet4 映射到 *.do 当请求URL为“/abc/a.html”,“/abc/*”和“/*”都匹配,Servlet引擎将调用Servlet1。 当请求URL为“/abc”时,“/abc/*”和“/abc”都匹配,Servlet引擎将调用Servlet3。 当请求URL为“/abc/a.do”时,“/abc/*”和“*.do”都匹配,Servlet引擎将调用Servlet1。 当请求URL为“/a.do”时,“/*”和“*.do”都匹配,Servlet引擎将调用Servlet2。 当请求URL为“/xxx/yyy/a.do”时,“/*”和“*.do”都匹配,Servlet引擎将调用Servlet2。注意:”/*.action”这样一个看起来很正常的匹配是错误的。因为这个匹配既属于路径映射,也属于扩展映射,会导致容器无法判断。
3.可以为<servlet>配置<load-on-startup>子标签,指定servlet随着服务器的启动而加载,其中配置的数值指定启动的顺序,从1开始执行。
<servlet>
<servlet-name>invoker</servlet-name> <servlet-class> org.apache.catalina.servlets.InvokerServlet </servlet-class> <load-on-startup>2</load-on-startup> </servlet>Servlet的线程安全问题
一个servlet在内存只有一个实例处理请求,当多个请求发送过来的时候就会有多个线程操作该servlet对象,此时可能导致线程安全问题。
解决方法:首先尽量不要在servlet中出现成员变量。
其次利用同步代码块解决问题,由于同一时间同步代码块只能处理一个请求,效率低下,因此同步代码块中尽量只包含核心的导致线程安全问题的代码。ServletConfig
FirstServlet com.diysoul.FirstServlet name gacl password 123 charset UTF-8
在Servlet的配置文件web.xml中,可以使用一个或多个<init-param>标签为servlet配置一些初始化参数。
当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,我们通过ServletConfig对象就可以得到当前servlet的初始化参数信息。
ServletConfig config = getServletConfig(); Enumerationenumration = config.getInitParameterNames(); while (enumration.hasMoreElements()) { String name = enumration.nextElement(); String value = config.getInitParameter(name); System.out.println(name + ":" + value); }
ServletContext
Web容器在启动时,它会为每个Web应用程序都创建一个对应的ServletContext对象,它代表当前Web应用。当服务器关闭或web应用被移除出容器时,ServletContext对象跟着销毁。
ServletConfig对象中维护了ServletContext对象的引用,在编写servlet时,可以通过ServletConfig.getServletContext方法获得ServletContext对象。 GenericServlet实现类中,也可以直接调用getServletContext取得。 ServletContext context = getServletConfig().getServletContext(); ServletContext context1 = getServletContext();// GenericServlet.getServletContext1.由于一个WEB应用中的所有Servlet共享同一个ServletContext对象,因此Servlet对象之间可以通过ServletContext对象来实现通讯。ServletContext对象通常也被称之为context域对象。
通常实现方式是在一个Servlet对象中调用setAttribute设置值,在其它的Servlet中getAttribute取得。ServletContext对象的实现数据共享的几个方法如下: public void setAttribute(String name, Object object); public Object getAttribute(String name); public Enumeration<String> getAttributeNames(); public void removeAttribute(String name);2.在web.xml可以配置整个web应用的初始化参数,利用ServletContext去获得
<context-param> <param-name>param1</param-name> <param-value>pvalue1</param-value> </context-param> ServletContext context = getServletConfig().getServletContext(); Enumeration<String> enumration = context.getInitParameterNames(); while (enumration.hasMoreElements()) { String name = enumration.nextElement(); String value = context.getInitParameter(name); System.out.println(name + ":" + value); }3.在不同servlet之间进行转发 getServletContext().getRequestDispatcher("/servlet/SecondServlet").forward(request, response); 方法执行结束,service就会返回到服务器,再由服务器去调用目标servlet,其中request会重新创建,并将之前的request的数据拷贝进去。4.读取资源文件 Tomcat中Servlet启动目录是tomcat/bin,因此相对于web应用的路径无法取得资源(当然,绝对路径是可以的,但并不推荐)。为了解决此问题需要使用如下方法: (1)在Servlet子类中可以使用ServletContext.getRealPath方法,资源文件放置到web应用根目录中。String path = getServletContext().getRealPath("/config.properties");
System.out.println("path:" + path); Properties prop = new Properties(); prop.load(new FileReader(path)); System.out.println(prop.getProperty("name")); System.out.println(prop.getProperty("value"));(2)在非Servlet子类中没有ServletContext,需要使用类加载器
URL url = getClass().getClassLoader().getResource("config.properties");
System.out.println("url:" + url); String path = url.getPath(); System.out.println("path:" + path); Properties prop = new Properties(); prop.load(new FileReader(path)); System.out.println(prop.getProperty("name")); System.out.println(prop.getProperty("value"));(3)类加载器位置引用
类加载器引用时相对于classes目录,如webapps/HelloWeb/WEB-INF/classes/ (HelloWeb为当前的web应用)
如果config.properties放在src目录下,可以直接引用。 其实际路径为:../webapps/HelloWeb/WEB-INF/classes/config.properties 如果放到包下,需要写包的全路径。如将其放置到com.john.web包下,引用时使用"com/john/web/config.properties"。 其实际路径为:../webapps/HelloWeb/WEB-INF/classes/com/john/web/config.properties 如果放到WEB-INF目录中,引用时使用"../config.properties" 其实际路径为:../webapps/HelloWeb/WEB-INF/config.properties 如果放置到webapps目录中,引用时使用"../../config.properties" (不推荐使用此方法,因为客户端可以直接访问webapps目录下的静态资源) 其实际路径为:../webapps/HelloWeb/config.properties(4)类加载器使用时注意事项
通过类装载器读取资源文件,不适合装载大文件,否则会导致jvm内存溢出。
资源文件放置到webapps时,利用类加载器直接将资源加载到内存中,有更新延迟的问题。HttpServletResponse
HttpServletResponse继承自ServletResponse,利用HttpServletResponse可以向客户端发送数据。
1.向客户端返回中文字符,两种方式:
String data = "中国";
response.setHeader("Content-type", "text/html;charset=utf-8");response.getOutputStream().write(data.getBytes("utf-8")); String data = "中国";response.setHeader("Content-type", "text/html;charset=utf-8");response.setCharacterEncoding("utf-8");response.getWriter().write(data);在HttpServletResponse对象中也可以直接调用setContentType设置编码解决乱码问题:
response.setContentType("text/html;charset=utf-8");
String data = "中国";response.getWriter().write(data);说明:Content-type 指定客户端解码的方式。setCharacterEncoding 方法指定了服务器端解码的方式。setContentType 方法指定了客户器端解码的方式,服务器端在调用此方法时也使用此方法指定的编码方式进行解码。
2.发送文件及文件名乱码问题
String encodeName = URLEncoder.encode("测试文件.txt", "utf-8"); response.setHeader("Content-Disposition", "attachment;filename=" + encodeName); String path = getServletContext().getRealPath("/WEB-INF/测试文件.txt"); System.out.println(new File(path).exists() + ", path:" + path); FileInputStream fis = new FileInputStream(path); OutputStream out = response.getOutputStream(); byte[] buffer = new byte[1024]; int len = 0; while((len = fis.read(buffer)) != -1) { out.write(buffer, 0, len); } fis.close();
Content-Disposition 响应头中设置了中文文件名,通过URLEncoder将"测试文件.txt"转换为URL格式的字符串(%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt),这里只能指定为utf-8,其它格式浏览器不支持。
URLEncoder 与 URLDecoder 字符串互转:
String encodeName = URLEncoder.encode("测试文件.txt", "utf-8");
System.out.println(encodeName); String decodeName = URLDecoder.decode("%E6%B5%8B%E8%AF%95%E6%96%87%E4%BB%B6.txt", "utf-8"); System.out.println(decodeName);
3.定时刷新跳转,设置Refresh,指定跳转的时间及url。如果不指定url,则刷新当前页面。
response.setHeader("Refresh", "3;url=/HelloWeb/index.jsp");
response.setContentType("text/html;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write("3秒后跳转到首页...");也可以直接在html中进行定时刷新跳转任务,当客户端访问此html时,5秒后会跳转到index.jsp。如下:
MyHtml.html 5秒后跳转到主页...
4.控制缓存, Expires 响应头指定缓存资源的时间,其值表示从1970年1月1日到目标的时间值,-1表示不缓存。
(1).不缓存资源
response.setIntHeader("Expires", -1);
response.setHeader("Cache-control", "no-cache"); response.setHeader("Pragma", "no-cache");(2).缓存资源,如下的代码设置缓存一天
response.setDateHeader("Exprires", System.currentTimeMillis() + 1000L * 3600 * 24);
5.重定向: 要求客户端跳转到另一个地址。会产生两次请求和两次响应。
response.setStatus(302);
response.setHeader("Location", "/HelloWeb/index.jsp");或者直接调用:
response.sendRedirect("/HelloWeb/index.jsp");
在大部分情况下请求重定向和转发的效果是差不多的,这时候应使用转发,以减少对服务器的访问。如果需要改变浏览器的地址,如跳转到主页等,这种情况下应使用重定向。
6.注意事项:
getOutputStream和getWriter这两个方法互相排斥,调用了其中的任何一个方法后,就不能再调用另一方法。
Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎从response里面获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。
Serlvet的service方法结束后,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎tomcat将调用close方法关闭该输出流对象。
HttpServletRequest
1.常用方法
String getRequestURL() 方法返回客户端发出请求完整URL
String getRequestURI() 方法返回请求行中的资源名部分
String getQueryString() 方法返回请求行中的参数部分
String getRemoteAddr() 方法返回发出请求的客户机的IP地址
String getMethod() 得到客户机请求方式
String getContextPath() 获得当前web应用虚拟目录名称
2.获得客户机的请求头
String getHeader(String name);
Enumeration<String> getHeaders(String name);
Enumeration<String> getHeaderNames();
int getIntHeader(String name);
long getDateHeader(String name);
3.利用请求头实现防盗链技术
String referer = request.getHeader("Referer"); if (referer == null || "".equals(referer) || !referer.startsWith("http://localhost/")) { response.setHeader("Content-Type", "text/html; charset=UTF-8"); response.sendRedirect("/HelloWeb/register.html"); return; }
4.获取请求参数
String getParameter(String name);
String[] getParameterValues(String name);
Enumeration<String> getParameterNames();
Map<String, String[]> getParameterMap();
5.请求参数中的乱码问题
String usrname = null;
String method = request.getMethod(); if (method.equalsIgnoreCase("POST")) { // 要求服务器以UTF-8解码实体内容 request.setCharacterEncoding("UTF-8"); usrname = request.getParameter("usrname"); } else if (method.equalsIgnoreCase("GET")) { usrname = request.getParameter("usrname"); if (usrname != null && !"".equals(usrname)) { // 表示很蛋疼,服务器可能并不是以ISO8859-1解,如果以UTF-8解,下面一行代码将得到一个乱码字符 usrname = new String(usrname.getBytes("ISO8859-1"), "UTF-8"); } }6.利用请求域传递对象。
Object getAttribute(String name);
void setAttribute(String name, Object o);
Enumeration<String> getAttributeNames();
void removeAttribute(String name);
7.request实现请求转发
getServletContext().getRequestDispatcher("").forward(request, response);
request.getRequestDispatcher("").forward(request,response);
在forward之前输入到response缓冲区中的数据,如果已经被发送到了客户端(如调用了response.getWriter().flush()),forward将失败,将抛出异常。
在forward之前输入到response缓冲区中的数据,但是还没有发送到客户端,forward可以执行,但是缓冲区将被清空,之前的数据丢失。但丢失的只是请求体中的内容,头内容仍然有效。 在一个Servlet中进行多次forward也是不行的,因为第一次forward结束,response已经被提交了,没有机会再forward了。 总之,一次请求只能有一次响应,响应提交走后,就不能再向客户端输出数据。8.请求包含
请求包含用来进行页面布局,以便将两个资源的输出进行合并后输出,但是被包含的对象不能改变状态码和响应头,如果有这样的语句,将被忽略。
getServletContext().getRequestDispatcher("").include(request, response);
request.getRequestDispatcher("").include(request, response);
请求重定向和请求转发的区别
1.转发只能将请求转发给同一个WEB应用中的组件;重定向还可以转到同一个站点上的其他应用程序中的资源,甚至是使用绝对URL重定向到其他站点的资源。
2.转发指定的相对URL以“/”开头,它是相对于当前WEB应用程序的根目录;重定向相对URL以“/”开头,它是相对于服务器的根目录。
3.转发时浏览器显示的URL一直保持初始的URL;重定向访问时浏览器地址栏显示的URL会发生变化,由初始的URL变为重定向的URL。
4.转发由服务器将另一个资源发给客户端,客户端并不知道服务器内部的行为;重定向要求客户端重新访问一个新的URL,会产生两次请求两次响应。
5.转发时调用者和被调用者之间共享相同的request和response对象,它们属于同一个访问请求和响应;而重定向是两次请求和两次响应,将产生两组不同的request和response对象。
使用规则:
尽量使用转发,以减少对服务器的访问压力。
需要在资源跳转时利用request域传递域属性则必须使用请求转发。
需要更新客户端地址栏显示时使用重定向,如防盗链。
地址的写法
虚拟路径
如果路径是给浏览器用的,这个路径相对于虚拟主机,所以需要写上web应用的名称。
如果路径是个服务器用的,这个路径相对于web应用,所以可以省写web应用的名称。 如:浏览器:
<a href="/WebApp/....."> <form action="/WebApp/..."> <img src="/WebApp/...."> response.setHeader("Location","/WebApp/...."); response.setHeader("refresh","3;url=/WebApp/..."); response.sendRedirect("/WebApp/..."); 服务器: request.getRequestDispathce("/index.jsp").forward(); request.getRequestDispathce("/index.jsp").include(); 真实路径具体问题具体分析,如下:
servletContext.getRealPath("config.properties");// 相对于web应用目录的路径 classLoader.getResource("../config.properties");// 给一个相对于类加载目录的路径
URL编码
1.由于HTTP协议规定URL路径中只能存在ASCII码中的字符,所以如果URL中存在中文或特殊字符需要进行URL编码。
2.编码原理: 将空格转换为加号(+) 对0-9,a-z,A-Z之间的字符保持不变 对于所有其他的字符,用这个字符的当前字符集编码在内存中的十六进制格式表示,并在每个字节前加上一个百分号(%)。 如字符“+”用%2B表示,字符“=”用%3D表示,字符“&”用%26表示, 每个中文字符在内存中占两个字节,字符“中”用%D6%D0表示,字符“国”用%B9%FA表示, 对于空格也可以直接使用其十六进制编码方式,即用%20表示,而不是将它转换成加号(+) 。 3.在java中进行URL编码和解码 URLencoder.encode("xxxx","utf-8"); URLDecoder.decode(str,"utf-8");