Servlet

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

    <servlet>
        <servlet-name>FirstServlet</servlet-name>
        <servlet-class>com.diysoul.FirstServlet</servlet-class>
        <!--配置ServletConfigDemo1的初始化参数 -->
        <init-param>
            <param-name>name</param-name>
            <param-value>gacl</param-value>
        </init-param>
        <init-param>
            <param-name>password</param-name>
            <param-value>123</param-value>
        </init-param>
        <init-param>
            <param-name>charset</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </servlet>

在Servlet的配置文件web.xml中,可以使用一个或多个<init-param>标签为servlet配置一些初始化参数。

当servlet配置了初始化参数后,web容器在创建servlet实例对象时,会自动将这些初始化参数封装到ServletConfig对象中,并在调用servlet的init方法时,将ServletConfig对象传递给servlet。进而,我们通过ServletConfig对象就可以得到当前servlet的初始化参数信息。

        ServletConfig config = getServletConfig();
        Enumeration<String> enumration = 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.getServletContext

1.由于一个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。如下:

<!DOCTYPE html>
<html>
  <head>
    <title>MyHtml.html</title>
    <meta http-equiv="Refresh" content="5;url=/HelloWeb/index.jsp">
  </head>
  
  <body>
    5秒后跳转到主页...  <br>
  </body>
</html>

 

 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");

原文地址:https://www.cnblogs.com/diysoul/p/5561617.html