前言:

我为什么要去学这些东西呢?

以前你问我MVC是什么,相信我肯定可以回答出(Model---View---Controller)然后,然后我就什么就不知道了....这其中涉及到哪些思想,哪些具体的东西,我是一概不知的。

然后最开始接触javaWeb的时候撸了一些jsp后,写项目直接用了springboot,正所谓springboot一时爽,一直springboot一直爽。但是当我后面接触一个struts2的项目时我突然发现写Filter和interceptor后,完全不明白为什么要这么写,为什么这样就可以通过呢?那样就不可以呢?它究竟是通过什么来实现的呢?然后还有写完这些还要去配置web.xml(或许连xml文件都不是很看得懂)真的感觉很懵逼。突然就觉得自己好像什么都不会。然后恰逢看书时看到一句话:javaee的核心设计模式就是MVC,框架只是一种简单的应用。我觉得很有道理,于是我就开始了我的弥补之路。

MVC

Java EE中标准的MVC设计模式如下图
1.jpg

在标准的MVC设计模式中,用户一旦发出请求后会将所有的请求交给控制层处理,然后再由控制层调用模型层中的模型组件,并通过这些组件进行持久层的访问,再将所有结果保存在JavaBean中,最终由JSP和JavaBean一起完成页面的显示。虽然在不同的开发架构中会存在一些差别,但是只要掌握了这个标准,就可以很容易的上手其他的框架。

初阶Servlet

Servlet

由上图可以看出控制层主要是Servlet,那么什么是Servlet呢?

狭义的Servlet是指Java语言实现的一个接口(javax.servlet.Servlet),广义的Servlet是指任何实现了这个Servlet接口的类(javax.servlet.GenericServlet , javax.servlet.http.HttpServlet , 还有其他继承了这些类的类),一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

Servlet带给人们最大的好处就是它可以处理客户端传来的HTTP请求,并返回一个响应。

Servlet处理的基本流程:

  1. 客户端(很可能是web浏览器)通过HTTP提出请求.
  2. web服务器接收该请求并将其发送给Servlet,如果这个Servlet尚未被加载,web服务器将把他加载到Java虚拟机并执行它
  3. Servlet程序将接收该Http请求并执行某种处理。
  4. Servlet会将处理后的结果向web服务器返回应答
  5. web服务器将从Servlet收到的应答发回给客户端

在整个Servlet中最重要的就是Servlet接口,在此接口下定义了一个GenericServlet子类,但是一般都不会继承此类,而是根据使用的协议选择GenericServlet的子类继承。例如现在采用的HTTP协议处理,所以一般而言当使用HTTP协议操作时用户自定义的Servlet都要继承HttpServlet类。

关于 HttpServlet、GenericServlet 和 Servlet 的关系

Servlet

公共接口Servlet定义了所有Servlet必须实现的方法。

Servlet(javax.servlet.Servlet) 接口提供了五个方法,其中三个生命周期方法和两个普通方法

/*
 * 1.实例化(使用构造方法创建对象)
 * 2.初始化  执行init方法
 * 3.服务     执行service方法
 * 4.销毁    执行destroy方法
 */

public interface Servlet {
    
    //生命周期方法:当Servlet第一次被创建对象时执行该方法,该方法在整个生命周期中只执行一次
    void init(ServletConfig servletConfig) throws ServletException;

 
    ServletConfig getServletConfig();

    //生命周期方法:对客户端响应的方法,该方法会被执行多次,每次请求该servlet都会执行该方法
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

    String getServletInfo();

     //生命周期方法:当Servlet被销毁时执行该方法
    void destroy();
}


GenericServlet

GenericServlet (javax.servlet.GenericServlet)是一个抽象类,实现了 Servlet 接口,并且对其中的 init() 和 destroy() 和 service() 提供了默认实现。在 GenericServlet 中,主要完成了以下任务:

  • 将 init() 中的 ServletConfig 赋给一个类级变量,可以由 getServletConfig 获得;
  • 为 Servlet 所有方法提供默认实现;
  • 可以直接调用 ServletConfig 中的方法;
abstract class GenericServlet implements Servlet,ServletConfig{
 
   //GenericServlet通过将ServletConfig赋给类级变量
   private trServletConfig servletConfig;
 
   public void init(ServletConfig servletConfig) throws ServletException {

      this.servletConfig=servletConfig;

      /*
       自定义init()的原因是:如果子类要初始化必须覆盖父类的init() 而使它无效 这样
       this.servletConfig=servletConfig不起作用 ,此时servletConfig=null
       这样就会导致在其他方法中使用servletConfig进行一系列操作的代码发生空指针异常
       这样如果子类要初始化,可以直接覆盖不带参数的init()方法 
       */
      this.init();
   }
   
   //自定义的init()方法,可以由子类覆盖  
   //init()不是生命周期方法
   public void init(){
  
   }
 
   //实现service()空方法,并且声明为抽象方法,强制子类必须实现service()方法 
   public abstract void service(ServletRequest request,ServletResponse response) 
     throws ServletException,java.io.IOException{
   }
 
   //实现空的destroy方法
   public void destroy(){ }
}

以上就是 GenericServlet 的大致实现思想,可以看到如果继承这个类的话,我们必须重写 service() 方法来对处理请求。

注:在GenericServlet中有两个init方法(),一个为没带参数(非生命周期方法),一个为带参数(生命周期方法),在我们自定义Servlet继承于它的时候,尽量不要去覆写生命周期init()方法。而是覆写非生命周期init()方法进行初始化。因为覆盖父类的生命周期方法init() 而使它无效 这样
this.servletConfig=servletConfig不起作用 ,此时servletConfig=null
这样就会导致在其他方法中使用servletConfig进行一系列操作的代码发生空指针异常

HttpServlet

HttpServlet ( javax.servlet.http.HttpServlet)也是一个抽象类,它进一步继承并封装了 GenericServlet,使得使用更加简单方便,由于是扩展了 Http 的内容,所以还需要使用 HttpServletRequest 和 HttpServletResponse,这两个类分别是 ServletRequest 和 ServletResponse 的子类。代码如下:

abstract class HttpServlet extends GenericServlet{
 
   //HttpServlet中的service()
   protected void service(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse){
        //该方法通过httpServletRequest.getMethod()判断请求类型调用doGet() doPost()
   }
 
   //必须实现父类的service()方法
   public void service(ServletRequest servletRequest,ServletResponse servletResponse){
      HttpServletRequest request;
      HttpServletResponse response;
      try{
         request=(HttpServletRequest)servletRequest;
         response=(HttpServletResponse)servletResponse;
      }catch(ClassCastException){
         throw new ServletException("non-http request or response");
      }
      //调用service()方法
      this.service(request,response);
   }
}

我们可以看到,HttpServlet 中对原始的 Servlet 中的方法都进行了默认的操作,不需要显式的销毁初始化以及 service(),在 HttpServlet 中,自定义了一个新的 service() 方法,其中通过 getMethod() 方法判断请求的类型,从而调用 doGet() 或者 doPost() 处理 get,post 请求,使用者只需要继承 HttpServlet,然后重写 doPost() 或者 doGet() 方法处理请求即可。

我们一般都使用继承 HttpServlet 的方式来定义一个 servlet。

Servlet的运行环境

:为了更好的了解Servlet,以下所有的Servlet都是通过实现javax.servlet.Servlet来展示,因为公共接口Servlet定义了所有Servlet必须实现的方法。所以了解了这个接口对后面的所有实现这个接口的Servlet理解将会变得十分容易

示例

Servlet就像其他任何的Java程序,只需要通过JAVAC编译Servlet,在编译之后在经过web容器的一些配置就可以访问了。

注:在编译的时候可能会出现的错误提示,软件包javax.servlet不存在,软件包java.servlet.http不存在,这是因为Servlet的开发包保存在web容器的\lib下面,而使用javac的时候属于javase的环境编译,但是Servlet本身已经属于了Javaee的应用范畴,所以此时可以通过系统环境变量classpath将此开发包添加进来。当然如果你是使用IDE的话那么肯定会用更简洁的方式。

下面就来看一个简单的Servlet

ServletDemo.java

import javax.servlet.*;
import java.io.IOException;
import java.io.PrintWriter;

public class ServletDemo implements Servlet {

    private transient ServletConfig servletConfig;

    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;

    }

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        ServletConfig servletConfig = getServletConfig();

        response.setContentType("text/html");//设置响应内容类型,告诉浏览器内容类型为HTML

        /*
         * 调用ServletResponse的getWriter方法,他返回了一个可以向客户端发送文本的java.io.PrintWriter。默认使用ISO-8859-1编码
         * */
        PrintWriter writer = response.getWriter();

        writer.print("<html><head></head><body>" + "hello Servlet" + "</body></html>");
    }

    @Override
    public String getServletInfo() {
        return "555555555";
    }

    @Override
    public void destroy() {

    }
}

现在写好了一个Servlet程序,并对它编译之后,我们该如何去访问呢?我们可以发现无法对这个程序进行直接的访问,因为所有的Servlet程序都是以.class的形式存在的,所以必须在web.xml(具体哪个目录的xml后面有解释)文件中进行servlet程序的映射配置

配置如下

  <servlet><!-- 定义servlet-->
    <servlet-name>hello</servlet-name><!-- 与servlet-mapping相对应-->
    <servlet-class><!-- 定义包.类名称-->
        ServletDemo<!-- 我这个Servlet没有包所以直接写类名-->
    </servlet-class>
  </servlet>
  <servlet-mapping><!-- 映射路径-->
    <servlet-name>hello</servlet-name><!-- 与servlet相对应-->
    <url-pattern>/hello</url-pattern><!-- 页面的映射路径-->
  </servlet-mapping>

上面的配置是,用过/helloServlet路径即可找到对应的节点,并找到所指定的Servlet程序的“包.类”名称,然后启动服务器以后输入“ http:localhost:8080/hello”即可访问到该页面,你会发现此时浏览器显示如下

2.png

到此为止我们可以大概的了解到一个Servlet程序运行需要进行的步骤

一般运行步骤
  • 导入Servlet需要依赖的其他库
  • 编写servlet,编译生成相应的.class文件
  • servlet的部署默认情况下,Servlet 应用程序位于路径<Tomcat-installation-directory>/webapps/ROOT 下,且类文件放在<Tomcat-installation-directory>/webapps/ROOT/WEB-INF/classes 中。这时我们就可以根据这个把自己的servlet程序放到上诉位置,然后修改<Tomcat-installation-directory>/webapps/ROOT/WEB-INF/ 的 web.xml即可。web.xml编写的时候一定要注意把相应的配置写到<web-app>...</web-app>标签内。
  • 然后通过相应的映射就可以进行访问了
IDE运行步骤

当然现在大多数人都是使用IDE直接进行Servlet的编写,通过IDE我们就不需要再去向classpath里面添加servlet的软件包了,我们可以直接在创建项目的时候将相应的包加载进来就可以了。但是我们发现似乎只是这样的话那么工作量其实并没有减少好多呀,还是要进行xml的配置,感觉依旧麻烦,其实我们可以通过注解的方式来替代xml的配置,具体看下面的代码。

ServletTest.java

import javax.servlet.*;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.io.PrintWriter;



/**
 * Servlet3.0 默认不在有web.xml
 * 3.0中采用@webServlet注解的方式来配置一些相应的参数
 *
 *WebServlet注解用来声明一个Servle。命名Servlet时,还可以暗示容器,是哪个URL调用这个Servlet
 *name属性是可选的,如果写的话,一般是该Servlet的名称
 *重要的是urlPatterns属性,他也是可选的但是一般都要有,在ServletTest中urlPatterns告诉容器,/Demo样式表示应该调用该servlet
 * initParams指定一组 Servlet 初始化参数
 * @WebInitParam 用来配置 initParams name为InitParameter名字,value为值,之间用特殊符号隔开,这里用逗号。
 * 这个就相当于在xml中写入:
 * <servlet><!-- 定义servlet-->
 *     <servlet-name>hello</servlet-name><!-- 与servlet-mapping相对应-->
 *     <servlet-class><!-- 定义包.类名称-->
 *         ServletTest<!-- 我这个Servlet没有包所以直接写类名-->
 *     </servlet-class>
 *     <init-param><!-- 定义初始化参数-->
 *       <param-name>admin</param-name><!-- 表明初始化参数的名字-->
 *       <param-value>yqh</param-value><!-- 参数对应的值-->
 *     </init-param>
 *      <init-param>
 *        <param-name>email</param-name>
 *        <param-value>1290341092@qq.com</param-value>
 *       </init-param>
 *   </servlet>
 *   <servlet-mapping><!-- 映射路径-->
 *     <servlet-name>hello</servlet-name><!-- 与servlet相对应-->
 *     <url-pattern>/Demo</url-pattern><!-- 页面的映射路径-->
 *   </servlet-mapping>
 *
 */
@WebServlet(name = "ServletTest", urlPatterns = "/Demo",
        initParams = {@WebInitParam(name = "admin", value = "yqh"),
                @WebInitParam(name = "email", value = "1290341092@qq.com")
        }
)
public class ServletTest implements Servlet {
    private transient ServletConfig servletConfig;

    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        this.servletConfig = servletConfig;

    }

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        ServletConfig servletConfig = getServletConfig();
        String admin = servletConfig.getInitParameter("admin");
        String email = servletConfig.getInitParameter("email");
        response.setContentType("text/html");
        PrintWriter writer = response.getWriter();
        writer.print("<html><head></head><body>" + "admin" + admin + "<br>Email" + email + "</body></html>");
    }

    @Override
    public String getServletInfo() {
        return "555555555";
    }

    @Override
    public void destroy() {

    }
}

通过以上写注解的形式就在功能上相当于了在xml文件写如下代码

<servlet><!-- 定义servlet-->
    <servlet-name>hello</servlet-name><!-- 与servlet-mapping相对应-->
    <servlet-class><!-- 定义包.类名称-->
        ServletTest<!-- 我这个Servlet没有包所以直接写类名-->
    </servlet-class>
      <init-param><!-- 定义初始化参数-->
        <param-name>admin</param-name><!-- 表明初始化参数的名字-->
        <param-value>yqh</param-value><!-- 参数对应的值-->
      </init-param>
      <init-param>
         <param-name>email</param-name>
         <param-value>1290341092@qq.com</param-value>
        </init-param>
   </servlet>
  <servlet-mapping><!-- 映射路径-->
    <servlet-name>hello</servlet-name><!-- 与servlet相对应-->
    <url-pattern>/Demo</url-pattern><!-- 页面的映射路径-->
  </servlet-mapping>

当你在IDE中配置好了该项目的容器时,就可以直接运行这个项目,然后在浏览器中输入 “http://localhost:8080/Demo”就可以访问了。

3.png

Servlet的生命周期

什么是Servlet的生命周期呢?

Servlet生命周期,即阐述Servlet从产生到毁灭的整个过程。前面介绍Servlet接口的时候说过Servlet提供了三个生命周期方法 初始化方法init(),处理客户请求的方法service(),终止方法destroy()。 他们将在Servlet的生命周期中发挥自己作用。下面先看一个图解

4.jpg

图中展示的就是一个Servlet的生命周期的过程,生命周期包括加载程序初始化服务销毁卸载5个部分。

上面的各个生命周期都可以的Servlet中找到相对应的方法,如下表所示。

No方法类型描述
1public void init() throws ServletException普通Servlet初始化时候调用(非生命周期方法)
2public void init(ServletConfig servletConfig) throws ServletException普通Servlet初始化时候调用,可以通过ServletConfig读取配置信息
3public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException普通Servlet服务,一般不会直接覆写此方法,而是使用doGet()或doPost()
4public void destroy()普通Servlet销毁时使用

各个生命周期的作用

1. 加载Servlet

Web容器负责加载Servlet,当Web容器启动的时候或者是在第一次使用这个Servlet的时候,容器会负责创建Servlet实列,但是用户必须通过部署描述符指定Servlet的位置(Servlet所在的包.类名称)(也可以使用注解的方式),成功加载后,Web容器会通过反射的方式对Servlet进行实列化。

2.初始化

当一个Servlet被实例化后,容器将调用init()方法初始化这个对象,初始化的目的是为让Servlet对象在处理客户端请求前完成一些初始化的工作,如建立数据库的连接,读取资源文件信息等,如果初始化失败,这个Servlet将被直接卸载。

3.处理服务

当有请求提交的时候。Servlet将调用service()方法(常用的是doGet或者doPost())进行处理,在service()方法中,Servlet可以通过ServletRequest接收客户的请求,也可以利用ServletResponse设置响应消息。

4.销毁

当Web容器关闭或者检测到一个Servlet要从容器中删除的时候,会自动调用destroy()方法,以便使该实例释放掉所占用的资源

5.卸载

当一个Servlet调用玩destroy()方法后,此实例将等待被垃圾收集器回收,如果需要再次使用此Servlet的时候,会重新调用init()方法初始化。

注:在正常情况下,Servlet只会被初始化一次,而处理服务会被调用多次,销毁也只会调用一次,但是yigeServlet长时间不使用的话,也会被容器自动销毁,而如果需要再次使用时会重新进行初始化的操作,记载特殊情况下初始化可能会进行多次,销毁也会被调用多次。

下面通过一个例子分析这个过程

6.示例

LifeCycleServlet.java

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

/*
*用来观察一个Servlet程序的生命周期的运行过程
* */
@WebServlet(name="LifeCycleServlet",urlPatterns="/life")

public class LifeCycleServlet implements Servlet {
    private transient ServletConfig servletConfig;


    /**
     * 当容器启动的时候或者第一次使用这个Servlet的时候会调用此方法进行初始化信息,重复请求时也只会调用一次(第一次)
     */
    @Override
    public  void    init(ServletConfig servletConfig)  throws ServletException{
        this.servletConfig = servletConfig;
        System.out.println("Servlet---------->初始化");
    }

    /*
    * 当客户端请求服务的时候会调用此方法,重复请求时重复调用此方法
    * */
    @Override
    public  void service(ServletRequest request, ServletResponse response)throws ServletException,IOException{
        System.out.println("Servlet---------->调用服务");
    }

    /*
    * 当容器关闭的时候或者web应用程序从容器中删除的时候调用此方法
    * */
    @Override
    public  void destroy(){
        System.out.println("Servlet---------->销毁");
    }

    @Override
    public String getServletInfo() {
        return "这是一个Servlet";
    }

    @Override
    public ServletConfig getServletConfig() {
        return servletConfig;
    }
}

运行结果:

5.png

由运行结果可以看出分成了3个部分

  • 标号为1的部分就是当我们第一次调用,在浏览器输入http://localhost:8080/life此Servlet输出的信息。因为我们使用了此Servlet,所以服务器会去查询是否有此Servlet的实列,由于这里是第一次使用,所以服务器肯定不会有此Servlet的实列,所以服务器就创建一个此Servlet的实列对象,然后调用该对象的**init()**方法进行初始化,所以这里可以看到第一句的 “Servlet------------>初始化” 这样的输出,然后由于我们调用了Servlet,即请求了他的服务所以下面输出了 “Servlet------------>调用服务” 这条语句,即调用了service()方法.
  • 标号为2的部分则是在第一次通过 http://localhost:8080/life 请求Servlet后,再次请求了两次该Servlet,根据输出可以看出,这时只有服务的调用,则不会再次对Servlet进行初始化。
  • 标号为3的部分则是我关闭了Web容器,此时之前创建的Servlet就会随着容器的关闭而被销毁,所以可以看到 “Servlet------------>销毁” 这条语句的输出,即此时调用了destroy()方法。

在前面代码init()方法我们介绍过除第一次调用此Servlet的时候会调用此方法,还有一种就是在Web容器启动的时候自动为Servlet进行初始化,那么什么时候会出现第二种情况呢?其实在默认情况下,初始化方法是在第一次调用的时候使用,那么要想出现第二种情况就肯定少不了我们的配置文件了。所有我们可以通过配置web.xml 文件来完成,配置如下

<servlet>
    <servlet-name>LifeCycleServlet</servlet-name>
    <servlet-class>LifeCycleServlet</servlet-class>
    <load-on-startup>1</load-on-startup><!-- 自定加载 -->
</servlet>
<!--  <load-on-startup>元素配置的数必须为正整数,数字越小,Servlet越优先创建。   -->

​ 通过这种方法人们就可以在web容器启动的时候装载Servlet。

​ 当然除了通过配置xml的方法还可以通过注解的方法,代码如下


import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;

/*
*用来观察一个Servlet程序的生命周期的运行过程,并配置自动加载
* */
@WebServlet(name="LifeCycleServlet",
            urlPatterns="/life",
            loadOnStartup = 1 //配置了自动加载
           )

public class LifeCycleServlet implements Servlet {
   ......................
   ......................
}

运行结果

6.png

将这个与上面的进行对比可以看出init()方法可以两种不同的情况下调用

用途:为web应用写一个InitServlet,这个Servlet配置为启动时装载,为整个web应用创建必要的数据库表和数据。

通过上面的过程可以可以总结出的运行过程

Servlet的运行过程

过程

Servlet程序是由web服务器调用,web服务器收到客户端的Servlet访问求后:

①web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
装载并创建该Servlet的一个实例对象
③调用Servlet实例对象init()方法
④创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法并将请求和响应对象作为参数传递进去。

⑤web应用程序被停止或重新启动之前,Servlet引擎将卸载Servlet,并在卸载之前调用Servlet的destroy()方法

destory()方法会在web容器移除servlet时执行,客户机第一次访问服务器时,服务器会创建servlet实例对象,它就永远驻留内存里面了,等待客户机第二次访问,这时有一个用户访问完servlet之后,此servlet对象并不会被摧毁,destory()方法就不会被执行。

一道面试题:请说出servlet的生命周期

答:servlet对象是用户第一次访问时创建,对象创建之后就驻留在内存里面了,响应后续的请求。servlet对象一旦被创建,init()就会被执行,客户端的每次请求导致service()方法被执行,servlet对象被摧毁时(web服务器停止后或者web应用从服务器里删除时),destory()方法就会被执行。

在Servlet的整个生命周期内,Servlet的init方法只被调用一次。而对一个Servlet的每次访问请求都导致Servlet引擎调用一次servlet的service方法。对于每次访问请求,Servlet引擎都会创建一个新的HttpServletRequest请求对象和一个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调用的Servlet的service()方法,service方法再根据请求方式分别调用doXXX方法。

注:

不知道你是否注意到了上面的④的过程提到了两个对象 HttpServletRequest对象和HttpServletResponse 这两个对象将会被传入到service()方法中,但是我们现在来看一下service的签名 service(ServletRequest request, ServletResponse response) 我们可以发现service()方法需要的参数并不是一致的,那么它们之间存在什么关系呢?前面我们提到过HttpServlet的时候有介绍过 HttpServletRequest 和 HttpServletResponse,这两个类分别是 ServletRequest 和 ServletResponse 的子类 ,下面我们先分别看一下官方对这四个类的描述,再来阐释为什么时是这样。

运行过程的补充

ServletRequest

公共接口ServletRequest定义一个对象,以便向servlet提供客户机请求信息。servlet容器创建一ServletRequest对象,并将其作为参数传递给servlet的服务方法。ServletRequest对象提供包括参数名称和值、属性和输入流在内的数据。扩展ServletRequest的接口可以提供额外的特定于协议的数据(例如,HTTP数据由HttpServletRequest提供)。

ServletResponse

公共接口ServletResponse定义一个对象,以帮助servlet向客户机发送响应。servlet容器创建一ServletResponse对象,并将其作为参数传递给servlet的服务方法。要在MIME主体响应中发送二进制数据,请使用 getOutputStream()返回的ServletOutputStream。要发送字符数据,请使用getWriter()返回的PrintWriter对象。

HttpServletRequest

公共接口HttpServletRequest扩展了ServletRequest接口,为HTTP servlet提供请求信息。servlet容器创建一个HttpServletRequest对象,并将其作为参数传递给servlet的服务方法(doGet、doPost等)。

HttpServletResponse

公共接口HttpServletResponse 扩展了ServletResponse接口,以在发送响应时提供http特定的功能。例如,它有访问HTTP头文件和cookie的方法。servlet容器创建一个HttpServletResponse对象,并将其作为参数传递给servlet的服务方法(doGet、doPost等)。

通过上面四个类的描述我们可以发现一个重要的东西 HTTP ,这个就涉及到了Servlet的处理协议的问题,从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。之前提到过 HttpServlet————>GenericServlet————>Servlet ,以上我们的所有的代码都是实现的javax.servlet.Servlet来展示的,而这个接口定义了所有Servlet必须实现的方法。所以上面才会出现创建HttpServletRequest对象和HttpServletResponse 这两个对象将会被传入到service()方法中,当然如果以后出现了其他的协议的时候传进的参数也就可能是XXXServletRequest对象和XXXServletResponse 这两个对象,但是不管是什么对象,它一定会是ServletRequest,ServletResponse的子类。了解这一点是很有必要的,因为有时候我们编写的Servlet是实现的javax.servlet.Servlet 的时候,而我们又需要对cookie或者session进行操作的时候,一定要将service(ServletRequest request, ServletResponse response)的参数进行向下转型为HttpServletRequest 和 HttpServletResponse,因为,不管是cookie还是session都是属于http协议的范畴,如果不进行转型的话,你将不能通过相应的方法得到相应的数据 。

当然这个向下转型的应用其实用的最多的是在Filter中,通过doFilter()方法将参数向下转型过后对session进行判断

通过以上的描述应该对一个普通的Servlet(javax.servlet.Servlet)有了一个很好的理解。下面我们来看一下GenericServlet这个类

GenericServlet

abstract class GenericServlet implements Servlet,ServletConfig{
 
   //GenericServlet通过将ServletConfig赋给类级变量
   private trServletConfig servletConfig;
 
   public void init(ServletConfig servletConfig) throws ServletException {

      this.servletConfig=servletConfig;

      /*
       自定义init()的原因是:如果子类要初始化必须覆盖父类的init() 而使它无效 这样
       this.servletConfig=servletConfig不起作用 ,此时servletConfig=null
       这样就会导致在其他方法中使用servletConfig进行一系列操作的代码发生空指针异常
       这样如果子类要初始化,可以直接覆盖不带参数的init()方法 
       */
      this.init();
   }
   
   //自定义的init()方法,可以由子类覆盖  
   //init()不是生命周期方法
   public void init(){
  
   }
 
   //实现service()空方法,并且声明为抽象方法,强制子类必须实现service()方法 
   public abstract void service(ServletRequest request,ServletResponse response) 
     throws ServletException,java.io.IOException{
   }
 
   //实现空的destroy方法
   public void destroy(){ }
}

如果试着去实现一个普通的Servlet,你会发现其实是一件麻烦的事情,因为你不得不去实现他所有的abstract方法,而很多的时候我们只需要去实现他的service()方法,并且这样的一个servlet而干的事情是非常的有限的,要实现更多的功能,就不得不自己每次去编写,但其实很多的功能在大多数的情况下是通用的,所以就有了GenericServlet。

公共抽象类GenericServlet extends java.lang.Object implements Servlet, ServletConfig, java.io.Serializable。

该类定义一个普通的、依赖于协议的servlet,如果要写一个用于Web的HTTP servlet,扩展HpptServlet。一个servlet可以直接扩展GenericServlet类,然而扩展一个指定协议的子类(如HttpServlet)显得更为普遍。GenericServlet类使编写servlets变得更容易。它提供了一般版本的生命周期方法:inin(),destroy()和来自ServletConfig接口的方法。GenericServlet类也实现了log()方法,这是一个在ServletContext类中定义的方法。

总之,GenericServlet正如他的名字通用Servlet一样,它可以解决大多数的情形,但是我们一般不会用它,而是选择HttpServlet,因为Servlet只用来响应居于HTTP协议的Web服务器,那么它保留的意义在哪?这是为了让以后出现其他协议的时候可以更好的让Servlet对这个协议进行扩展的。

注:在GenericServlet中有两个init方法(),一个为没带参数(非生命周期方法),一个为带参数(生命周期方法),在我们自定义Servlet继承于它的时候,尽量不要去覆写生命周期init()方法。而是覆写非生命周期init()方法进行初始化。因为覆盖父类的生命周期方法init() 而使它无效 这样
this.servletConfig=servletConfig不起作用 ,此时servletConfig=null
这样就会导致在其他方法中使用servletConfig进行一系列操作的代码发生空指针异常

详细介绍:GenericServlet

HttpServlet

HttpServlet extends GenericServlet implements java.io.Serializable

httpServlet 提供要子类化的抽象类,以创建适合于Web站点的HTTP servlet。HttpServlet的子类必须覆盖至少一个方法,通常是以下方法之一:

  • doGet,如果servlet支持HTTP GET请求
  • doPost,用于HTTP POST请求
  • doPut,用于HTTP PUT请求
  • doDelete,用于HTTP删除请求
  • init and destroy,以管理servlet生命周期中保存的资源
  • getServletInfo, servlet使用它来提供关于自身的信息

几乎没有理由重写service方法。service通过将标准HTTP请求分派给每种HTTP请求类型的处理程序方法(上面列出的doXXX方法)来处理标准HTTP请求。同样,几乎没有理由重写doOptions和doTrace方法。如果重写了service方法,则对应的doXXXX方法就不再起作用,而是直接调用service方法进行处理 。从实际开发来看,doGet()和doPost()两个方法的使用几率最高,所以一般主要覆写这两个方法。

Servlet的应用

取得初始化配置信息

通过前面的生面周期的介绍,我们可以看到一个Init(ServletConfig config)方法,通过此方法我们可以获得一个ervletConfig 实例对象,我们可以通过这个对象来获取到一些东西(具体查阅此接口的方法),这里我们通过他的

getInitParameter()方法,来获得初始化配置信息。

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 用来取得配置的初始化信息
 * @method getInitParameter 
 * */

@WebServlet(name = "InitParamServlet", urlPatterns = "/param",
        initParams = {@WebInitParam(name = "name", value = "yqh"),
                @WebInitParam(name = "phone", value = "13145201314")
        }
)//通过注解的形式来代替xml文件的配置

public class InitParamServlet extends HttpServlet {

        private String name=null;
        private String phone=null;

        public  void    init(ServletConfig config) throws ServletException{
        name = config.getInitParameter("name");
        phone = config.getInitParameter("phone");
        }

        public void  doGet(HttpServletRequest request, HttpServletResponse response) throws  ServletException, IOException{
                System.out.println("doGet获取初始化参数 :name :"+name+" phone : "+phone );
        }
        public void  doPost(HttpServletRequest request, HttpServletResponse response) throws  ServletException, IOException{
                System.out.println("doPost获取初始化参数 :name :"+name+" phone : "+phone );
        }
}

在浏览器输入URL请求此Servlet我们可以看到这样的输出

doGet获取初始化参数 :name :yqh phone : 13145201314

说明通过ServletConfig对象的getInitParameter方法的确获取到了他的初始话配置信息。这里可以看到是调用的doGet方法,这是因为在浏览器输入URL的方式默认为get请求。

上面的getInitParameter只能根据初始化参数的名字来获取相应的值,如果有多个参数的时候还用此方法,将会使得代码变得臃肿,所以这时候就可以通过另外一个方法 getInitParameterNames()取得全部初始化参数名称。然后通过getInitParameter获得所有的值。

代码示例如下:

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;


/**
 * 用来取得配置的初始化信息
 *
 * @method getInitParameterNames
 */

@WebServlet(name = "InitParamServlet", urlPatterns = "/params",
        initParams = {@WebInitParam(name = "name", value = "yqh"),
                @WebInitParam(name = "phone", value = "13145201314"),
                @WebInitParam(name = "class", value = "03011703"),
                @WebInitParam(name = "sex", value = "男")
        }
)
public class InitParameterNamesServlet extends HttpServlet {
    private Enumeration enums = null;
    private transient ServletConfig config = null;//用来接收config对象,不然后面获取不到每一个参数的具体值

    public void init(ServletConfig config) throws ServletException {
        this.config = config;
        enums = config.getInitParameterNames();

    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws
            ServletException, IOException {
        String param = null;
        while (enums.hasMoreElements()) {
            param = (String) enums.nextElement();
            System.out.println("doGet获取的参数:" + param + " : " + config.getInitParameter(param));
        }

    }

    public void doPost(HttpServletRequest request, HttpServletResponse response) throws
            ServletException, IOException {

    }
}

运行结果

doGet获取的参数:phone : 13145201314
doGet获取的参数:sex : 男
doGet获取的参数:name : yqh
doGet获取的参数:class : 03011703

以上代码就是通过getInitParameterNames获取全部配置参数的演示,需要注意的是此方法返回的是Enumeration的数据,然后通过nextElement()返回的是一个Object对象。然后再通过ServletConfig对象调用getInitParameter()获取它的具体的值时要将它转换成String后再作为参数。

还有需要注意的是,我们一个要定义一个ServletConfig对象,然后在init()方法中将自定义的ServletConfig对象用来接收由Web容器创立的ServletConfig对象。不然在调用getInitParameter()的时候会出现空指针异常。

取得其他的内置对象

在上面介绍过可以通过Servlet获取到config对象,实际上通过Servlet程序也可以取得session以及application的内置对象。

在Servlet程序中要想获得一个session对象,则可以通过HttpServletRequest接口完成。 取得HttpSession的接口实例:

No.方法类型描述
1public HttpSession getSession()普通返回当前的session
2public HttpSession getSession(boolean create)普通返回当前的session,*如果没有则创建一个新的session对象返回

看一段代码:

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/*
* 通过HttpServletSession获得session
* */
@WebServlet(name = "GetHttpSessionServlet",urlPatterns = "/session")
public class GetHttpSessionServlet extends HttpServlet {
    public void init() throws ServletException {
        System.out.println("sesssion的获取");
    }
    public void  doGet(HttpServletRequest request, HttpServletResponse response) throws
            ServletException, IOException {
        HttpSession session = request.getSession();//HttpServletRequest接口调用getSession可以返回一个HttpSession
        session.setAttribute("name","yqh");
        System.out.println("获取存到session的name:"+session.getAttribute("name"));
    }
}

通过实现了HttpServlet的类直接通过HttpServletSession对象可以返回Httpsession对象,十分方便,下面看另外一段代码

import javax.servlet.GenericServlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;


/*
* 通过ServletRequest获得HttpSession
* */
@WebServlet(name = "GetHttpSessionServletDemo",urlPatterns = "/resession")
public class ReGetHttpSessionServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException{

        HttpServletRequest req=(HttpServletRequest)request;//将ServletRequest对象转换成HttpServletRequest
        HttpServletResponse rep=(HttpServletResponse)response;//将ServletResponse对象转换成HttpServletResponse
        HttpSession session=req.getSession();

        session.setAttribute("name", "yqh");
        System.out.println("通过将ServletRequest向下转型获取存到session的name:" + session.getAttribute("name"));
        
    }
}

这段程序同样获取到了session,但是可以看到这段代码与上面代码的不同之处就在于这两句

HttpServletRequest req=(HttpServletRequest)request
HttpServletResponse rep=(HttpServletResponse)response;

在这里我们对service方法的参数进行了向下转型的操作,然后通过向下转型对象调用getSession方法。从而获得HttpSession对象。

注: 在获取session对象的时候一定要注意你的Servlet实现的是HttpServlet,还是SerVlet和GenericServlet。如果是前者,那么他相应的服务方法的参数就是HttpServletRequest和HttpServletResponse,此时可以直接调用getSession方法取得session。如果是后者那么他的service方法的参数就是ServletRequest和ServletResponse,此时如果需要获得session,则一定要把Servlet对象向下转型为HttpServletRequest对象,此时才能调用getsession()获取到session。

取得ServletContext实例

ServletContext官方叫servlet上下文。服务器启动时会为每一个工程创建一个对象,这个对象就是ServletContext对象。这个对象全局唯一,而且工程内部的所有servlet都共享这个对象。所以叫全局应用程序共享对象。如果要在一个Servlet中使用此对象,直接通过GenericServlet类提供的方法即可(HttpServlet继承了GenericServlet类的所有方法,只是自己根据请求方式的不同重写了service方法())。

N0.方法类型描述
1public ServletContext getServletContext()普通取得ServletContext对象

代码示例:

import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
;


/**
 * 获取ServletContext对象
 * @method 通过重GenericServlet继承来的getServletContext()
 * */

@WebServlet(name = "GetServletContextServlet", urlPatterns = "/application")
public class GetServletContextServlet extends HttpServlet {


    /*
    * 注释一:覆写了生命周期init()方法,注意覆写非生命周期init()方法的区别
    * */
    public void init(ServletConfig config) throws ServletException{
       super.init(config);//注释二:调用父类的生命周期方法init(),初始化ServletConfig对象
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext app = getServletContext();
        System.out.println("真实路径 : " + app.getRealPath("/6666666"));
        //getRealPath()的参数代表在真实路径后面添加的字符串内容
    }
}

通过直接调用父类的getServletContext()方法就可以获取到ServletContext对象,特别注意 方面注释一二的地方。

思考一下为什么?思考前我们先看一下getServletContext()方法的实现。

public ServletContext getServletContext() {
        return this.getServletConfig().getServletContext();
    }

 public ServletConfig getServletConfig() {
        return this.config;
    }

可以看到一个东西:ServletConfig实列对象 config 。相信看到这里你肯定已经明白了为什么。因为生命周期init方法,除了初始化,还干了一件特别重要的事情,那就是获取服务器创建的config对象将它赋值给我们自己创建的config对象。如果将生命周期方法init()覆写了,而又没有对自己创建的config对象赋初值,那么它默认为null,也就会导致在后面所有的通过config调用的方法报空指针异常。

所以在我们写Servlet时不要去覆写生命周期方法init()方法,如果非要去覆盖,那么一定要调用父类的生命周期init()方法对config对象进行接收。

Servlet跳转

从一个JSP或者HTML页面可以通过表单或超链接跳转到Servlet,那么从Servlet也可以跳转到其他的Servlet,JSP或其他页面。而跳转又分为客户端跳转(重定向)和服务器端跳转(分发)。

客户端跳转

在Servlet中如果要想进行客户端跳转,直接使用HttpServletResponse接口的sendRedirect()方法就可以了,需要注意的是,此跳转只能传递session及application范围的属性,而无法传递request范围的属性。

代码示例:

ClientRedirectServlet.java

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@WebServlet(name="ClientRedirectServlet",value = "/send")
public class ClientRedirectServlet extends HttpServlet{
    public  void    doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException{
       request.getSession().setAttribute("session","这是一个客户端跳转呀传递的session");
       request.setAttribute("request","这是一个客户端跳转呀传递的request");
       getServletContext().setAttribute("application","这是一个客户端跳转呀传递的application");
       response.sendRedirect("Revice_info.jsp");
    }
}

Revice_info.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/23
  Time: 17:10
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>接收客户端跳转传来的信息</title>
    <% request.setCharacterEncoding("GBK");%>
</head>
<body>
<h2>session属性:<%=session.getAttribute("session")%></h2>
<h2>request属性:<%=request.getAttribute("request")%></h2>
<h2>application属性:<%=application.getAttribute("application")%></h2>
</body>
</html>

这里需要注意的是jsp文件与Servlet的位置关系,如果jsp文件不是在web根目录下面的话,servlet的sendRedirect()参数要做相应改变。

启动web容器后,我们在浏览器输入localhost:8080/send后的结果如下。

7.png

这里需要注意的就是浏览器的URL,我们可以很显然的看见URL是变化了的,因为是客户端跳转。。。。然后还有就是JSP页面的输出关于request属性的值为null,这很好的说明了之前所说的客户端的跳转范围,request属性范围的内容无法在客户端跳转中接收到,因为request属性范围只有在服务器端跳转中才可以使用。

服务器跳转

在Servlet中没有像JSP中的<jsp:fowad>指令,所以要想执行服务器端跳转,就必须依靠RequestDispatcher接口来完成,此接口提供了下表所示的两种方法。

No.方法类型描述
1public void forward(ServletRequest request,ServletResponse response) throws ServletException,IOException普通页面跳转
2public void include(ServletRequest request,ServletResponse response) throws ServletException,IOException普通页面包含

使用RequestDispatcher接口的forward()方法即可完成跳转功能的实现,但是如果要使用此接口还需要使用HttpRequest接口提供的如下表的方法进行初始化。

No.方法类型描述
1public RequestDispatcher getRequestDispatcher(String path)普通取得RequestDispatcher接口的实列

下面通过具体的代码来看以下这个的实现

ServerRedirectServlet.java

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "ServerRedircetServlet",value = "/foward")
public class ServerRedircetServlet extends HttpServlet {
    public  void    doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.getSession().setAttribute("session","这是一个服务器跳转呀传递的session");
        request.setAttribute("request","这是一个服务器跳转呀传递的request");
        getServletContext().setAttribute("application","这是一个服务器跳转呀传递的application");
        RequestDispatcher rq=request.getRequestDispatcher("Foward_info.jsp");
        rq.forward(request,response);
    }
}

Foward_info.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/23
  Time: 17:51
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>接收服务器端跳转的内容</title>
    <% request.setCharacterEncoding("GBK");%>
</head>
<body>
<h2>session属性:<%=session.getAttribute("session")%></h2>
<h2>request属性:<%=request.getAttribute("request")%></h2>
<h2>application属性:<%=application.getAttribute("application")%></h2>
</body>
</html>

启动web容器后,我们在浏览器输入localhost:8080/foward后的结果如下。

8.png

通过运行结果可以看到URL并没有变化,但是输出的内容变成了JSP页面的内容,与此同时接收到了request属性的内容。

以上就是通过Servlet实现跳转的两种方式。

进阶Servlet

JSP能完成的功能都可以用Servlet来完成,但是Servlet具备的很多功能是JSP所不具备的,从使用上来看Servlet是可以分为简单Servlet(之前所讲解的都属于简单Servlet),过滤Servlet(过滤器)和监听Servlet(监听器)3种。JSP所能完成的也只是简单的Servlet的功能,下面看一下过滤器的使用。

Filter

Filter的基本概念

Filter是在Servlet2.3之后添加的新功能,当需要限制用户访问某些资源或者在处理请求的时候提前处理某些资源的时候,即可使用过滤器来完成。

过滤器是以一种组件的形式绑定到Web应用程序当中的,与其他的Web应用组件不同的是,过滤器是采用 “ 链 ” 的方式进行处理的,如下图所示

9.png

在没有过滤器之前,客户端都是直接请求Web资源的,但是一旦加入了了过滤器,所有的请求都会先交给过滤器来处理,然后再访问相应的Web资源,这样就可以达到对某些资源的限制。

Filter如何实现拦截

要想了解到Filter是如何实现拦截的那么首先应该去看一下他的源码

package javax.servlet;

import java.io.IOException;

public interface Filter {
    void init(FilterConfig var1) throws ServletException;

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    void destroy();
}

从以上Filter的源码我们可以看到他只有三个方法。很显然init()和destroy()是生命周期方法,那么就可以知道Filter是通过doFilter()方法来实现拦截的。

具体解释:
  • 当客户端发生请求后,在HttpServletRequest 到达Servlet 之前,过滤器拦截客户的HttpServletRequest 。
  • 根据需要检查HttpServletRequest ,也可以修改HttpServletRequest 头和数据。
  • 在过滤器中调用doFilter方法,对请求放行。请求到达Servlet后,对请求进行处理并产生HttpServletResponse发送给客户端。
  • 在HttpServletResponse 到达客户端之前,过滤器拦截HttpServletResponse 。
  • 根据需要检查HttpServletResponse ,可以修改HttpServletResponse 头和数据。
  • 最后,HttpServletResponse到达客户端。

当我们编写好Filter后,并配置对哪个web资源进行拦截以后(在web.xml文件中配置或者使用注解的方式),Web服务器每次在调用Web资源的service方法之前,都会先调用一下Filter的doFilter方法,因此在该方法内编写代码可以达到如下目的:

  • 调用目标资源之前,让一段代码执行
  • 觉得是否调用目标资源(即是否让用户访问Web资源)
  • 调用目标资源之后让一段代码执行。

web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对 象,它也提供了一个doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方 法,即web资源就会被访问,否则web资源不会被访问。

Filter开发步骤

了解了Filter的拦截原理过后,就可以自己动手写一个Filter了

Filter开发分为二个步骤:

  1. 编写java类实现Filter接口,并实现其doFilter方法。
  2. 在 web.xml 文件中使用<filter><filter-mapping>元素对编写的filter类进行注册,并设置它所能拦截的资源。

示例如下

第一步

FilterTest.java

import javax.servlet.*;
import java.io.IOException;


/*
* 编写一个Filter用来拦截访问
* */
public class FilterTest implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {//初始化过滤器
        System.out.println("初始化一个过滤器");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {//执行过滤
        System.out.println(" filterChain调用doFilter之前");
        filterChain.doFilter(servletRequest ,servletResponse);//将请求继续传递
        System.out.println(" filterChain调用doFilter之后");
    }

    @Override
    public void destroy() {//销毁过滤
        System.out.println("过滤器销毁");
    }
}

第二步

现在可以看到已经编写好了一个Filter,但是由于我们还没有对它进行配置,所以这个Filter是不起任何作用的,下面我们在web.xml文件进行配置 (当然也可以用注解的形式直接在Filter上进行配置)

<filter><!-- 注册filter-->
<filter-name>filtertest</filter-name><!-- filter的名字与filter-mapping相对应 不能为空-->
	<filter-class><!--元素用于指定过滤器的完整的限定类名。 -->
	FilterTest
	</filter-class>
    <!-- 也可以在这个地方配置初始化参数-->
</filter>
<filter-mapping> <!--映射filter-->
    <filter-name>filtertest</filter-name>
    <!-- 用于设置filter的注册名称。该值必须是在<filter>元素中声明过的过滤器的名字-->
    <url-pattern>/foward</url-pattern>
    <!-- 设置 filter 所拦截的请求路径(过滤器关联的URL样式)                   -->
    <!-- 除了通过url方式拦截之外,还可以配置servlet的拦截                     -->
    <!-- <servlet-name></servlet-name> 指定过滤器所拦截的Servlet名称       -->
    
    <!-- 还可以用<dispatcher>指定过滤器所拦截的资源被 Servlet                -->
    <!-- 容器调用的方式可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST -->
     

    <!-- REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过         -->
    <!-- RequestDispatcher的include()或forward()方法访问时,那么该过滤器就不会被调用。 -->
  
    <!-- INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤 -->
    <!-- 器将被调用。除此之外,该过滤器不会被调用。                                    -->
  
    <!-- FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过   -->
    <!-- 滤器将被调用,除此之外,该过滤器不会被调用。                                  -->
  
    <!-- ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外, -->
    <!-- 过滤器不会被调用。                                                        -->
    
</filter-mapping>

如果是写注解的话就是:

@WebFilter(filterName="filtertest",urlPatterns="/foward")

通过以上两步过后,当我们在浏览器输入要拦截的URL过后,就会调用该过滤器。后台的输出信息如下

10.png

根据输出我们可以看到过滤器中的初始化方法不用我们配置自动加载,它也会在Web容器启动的时候自动调用。

后面的输出可以看到doFilter会调用两次,一次是在FilterChain操作之前,一次是FilterChain操作之后。

即请求到达服务器之前以及响应返回客户端之前)

Filter链

实现

在一个Web应用中,可以开发多个Filter,这些Filter经过配置就可以变成一个Filter链。

示例如下

FilterFirst.java

import javax.servlet.*;
import java.io.IOException;


/*
*跟FilterSecond组合使用形成Filter链
* */
public class FilterFirst  implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {//初始化过滤器
        System.out.println("初始化一个过滤器-----FilterFirst");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {//执行过滤
        System.out.println(" filterChain调用doFilter之前-----FilterFirst");
        filterChain.doFilter(servletRequest ,servletResponse);//将请求继续传递
        System.out.println(" filterChain调用doFilter之后-----FilterFirst");
    }

    @Override
    public void destroy() {//销毁过滤
        System.out.println("过滤器销毁-----FilterFirst");
    }
}

FilterSecond.java

import javax.servlet.*;
import java.io.IOException;



/*
 *跟FilterFirst组合使用形成Filter链
 * */
public class FilterSecond  implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {//初始化过滤器
        System.out.println("初始化一个过滤器-----FilterFSecond");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {//执行过滤
        System.out.println(" filterChain调用doFilter之前-----FilterFSecond");
        filterChain.doFilter(servletRequest ,servletResponse);//将请求继续传递
        System.out.println(" filterChain调用doFilter之后-----FilterFSecond");
    }

    @Override
    public void destroy() {//销毁过滤
        System.out.println("过滤器销毁-----FilterFSecond");
    }
}


web.xml的配置如下

<filter>
<filter-name>filterfirst</filter-name>
	<filter-class>
	FilterFirst
	</filter-class>
</filter>
<filter-mapping> 
    <filter-name>filterfirst</filter-name>
    <url-pattern>/foward</url-pattern>
</filter-mapping>
<filter>
<filter-name>filtersecond</filter-name>
	<filter-class>
	FilterSecond
	</filter-class>
</filter>
<filter-mapping> 
    <filter-name>filtersecond</filter-name>
    <url-pattern>/foward</url-pattern>
</filter-mapping>

在配置时,配置Filter的顺序就是该过滤链的执行顺序,这里需要注意的是两个Filter的<url-pattern>的标签内必须是同一个URL,这样这两个Filter才算组成了一个Filter链,否则他们将没多大关系。

通过如此配置之后,运行结果如下。

12.png

注意

从上面可以看出过滤链的初始化顺序不是按照web.xml文件对应的声明顺序进行的。这里的初始化顺序是根据<filter-name>的长度来决定的,谁的<filter-name>长,谁就先初始化。然后一样长的时候就开始比较<filter-name>的字母顺序,字母越靠前越先执行,有相同的就延后比较下一个字母。(我也不是很明白为什么要这样设计来决定他的初始化顺序)这个需要特别的注意一下,网上的博客现在大多数说的都是根据注册顺序来声明的。

然后虽然初始化顺序与声明顺序无关,但是他的执行顺序却是跟初始化顺序一样的,即先初始化先执行。但是需要注意的是由于他是一个链式,所以可以看到 它的doFilter的执行,先会把所有的到达服务器前的请求依次过滤,然后再将服务器返回客户端的响应依次过滤返回。整个过程就很像一个函数的递归调用一样,详情如下图:

Filter的生命周期

Filter与Servlet一样也具有自己的生命周期,之前看Filter的源码的时候知道他有三个方法。

  • init()方法:初始化参数,在创建Filter的时候(Web容器启动的时候)自动调用,当我们需要设置初始化参数的时候,可以写到该方法中。
  • doFilter()方法:拦截到要执行的请求的时候,doFilter就会执行,这里写我们对请求和响应的预处理。
  • destroy()方法:在销毁Filter时自动调用。

很显然它的生命周期方法是init()方法和destory()方法。Filter的创建和销毁由web服务器控制。

  • 服务器启动的时候,web容器创建Filter的实列对象,并调用init()方法,完成对象的初始化功能。Filter对象只会创建一次,init()方法也只会执行一次。
  • 拦截到请求的时候执行doFilter()方法,可以执行多次。
  • 服务器关闭的时候,web服务器销毁Filter的实列对象。

一个Filter的完整的生命周期就是如此。

FilterConfig

在生命周期的介绍中提到了init()方法可以初始化参数,那么初始参数的话肯定需要一个对象。我们看一下init方法的签名init(FilterConfig var1)可以看到它有一个FilterConfig类型的对象,这个对象有什么用呢?在前面介绍Servlet的时候,它的init()方法由ServletConfig,我们可以通过此参数我们可以获得一些该Servlet在web.xml的配置信息以及Servlet 容器的一些信息。同理FilterConfig对象也是用来获取一些Filter的配置信息和Servlet 容器的一些信息。

FilterConfig接口定义的各个方法:

  • getFilterName 方法,返回 <filter-name>元素的设置值。
  • getServletContext 方法,返回 FilterConfig 对象中所包装的 ServletContext 对象的引用
  • getInitParameter 方法,用于返回在 web.xml 文件中为 Filter 所设置的某个名称的初始化的参数值。
  • getInitParameterNames 方法,返回一个 Enumeration 集合对象。

Filter的应用

Filter在实际的Web开发中发挥着巨大的用处,下面我们通过一些具体的代码来看看它可以干什么

统一全站的字符编码

学习jsp的时候如果需要统一编码的话,需要在每一个jsp的页面加入相应打代码,这样就会照成大量的代码重复。于是我们我就可以通过一个Filter来解决这种问题,通过配置参数charset来指明使用哪一种字符编码,以处理乱码问题。

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


/*
 * 用来统一全站编码的Filter
 * */
public class CharacterEncondingFilter implements Filter {
    private FilterConfig filterConfig = null;
    private String defaultCharset = "UTF-8";

    @Override
    public void init(FilterConfig config) throws ServletException {
      filterConfig=config;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {//执行过滤
        HttpServletRequest request =(HttpServletRequest)servletRequest;
        HttpServletResponse response=(HttpServletResponse)servletResponse;

        String charset=filterConfig.getInitParameter("charset");
        if (charset==null){
            charset=defaultCharset;
        }
        request.setCharacterEncoding(charset);
        response.setCharacterEncoding(charset);

        filterChain.doFilter(request, response);//将请求继续传递
    }

    @Override
    public void destroy() {
    }
}



登录验证

登陆验证在Web开发中是必不可少的一个环节,最早用样是在每个页面进行session的判断,于是也造成了大量的代码重复,而用过滤器来实现这些操作这可以避免这种重复的操作。

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;


/*
 *用于登录验证
 * 其中userid表示登录过后存入session的属性,login.jsp表示登录页面
 * */
public class LoginFilter implements Filter {
    @Override
    public void init(FilterConfig config) throws ServletException { }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {//执行过滤
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpSession session = request.getSession();
        if (session.getAttribute("userid") != null) {
            filterChain.doFilter(request, response);//将请求继续传递
        }else {
            request.getRequestDispatcher("login.jsp").forward(request,response);
        }

    }
    @Override
    public void destroy() { }
}

通过Filter不止可以做上面的事情,事实上只要在开发中有需要自动完成的操作的时候,都可以通过Filter来处理。

XXXListener

简介

上面介绍了第二种Servlet(Filter),现在介绍第三种Servlet,监听Servlet,它的主要功能是负责监听Web的各种操作,当相关的事件触发后将产生事件,并对此事件进行处理,在Web中可以对applicatin,session和request3种操作进行监听。事件源和监听器绑定的过程:通过配置web.xml来完成。

Servlet中提供了8个监听器,可以将它们分成3类。

一类:监听域对象的创建和销毁的监听器。

对象类型对应的监听器
ServletContextServletContextListener
HttpSessionHttpSessionListener
HttpServletRequestServletRequestListener

二类: 监听三个域对象的属性变更的监听器。(属性添加,属性移除,属性替换)

对象类型对应的监听器
ServletContextServletContextAttributeListener
HttpServletRequestServletRequestAttributeListener
HttpSessionHttpSessionAttributeListener

三类: 监听HttpSession对象中的JavaBean的状态的改变.(绑定,解除绑定,钝化和活化)

对象类型对应的监听器
HttpSessionHttpSessionBindingListener(绑定,解除绑定)
HttpSessionHttpSessionActivationListener(钝化和活化)

为了更好的展示,我们根据操作对象的不同,来分别介绍以上的8种监听器

对application进行监听

对application监听,实际上就是对Servletcontext(Servlet 上下文)监听。主要使用ServletContextListener和ServletContextAttributeListener两个接口

ServletContextListener

对Servlet上下文状态监听可以使用javax.servlet.ServletContextListener接口,此接口定义的方法如下表所示:

No.方法类型描述
1public void contextInitialized(ServletContextEvent sce)普通容器启动时触发
2public void contextDestroyed(ServletContextEvent sce)普通容器销毁时触发

可以看到,它的两个方法参数都是ServletContextEvent实列,因此,在上下文状态监听操作中,一旦触发了ServletContextListener接口中定义的方法后,我们就可以通过ServletContextEvent进行事件的处理,此事件定义的方法如下所示。

No.方法类型描述
1public ServletContext getServletContext()普通取得ServletContext对象

可以看到,我们通过ServletContextEvent的 getServletContext方法就可以取得 ServletContext对象的实列。

下面通过代码来看一下对Servlet上下文状态的监听

ListenerServletContextTest.java

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/*
* 对ServletContext进行监听
* */
@WebListener
public class ListenerServletContextTest implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent event) {
        System.out.println("容器初始化" + event.getServletContext().getContextPath());
    }

    @Override
    public void contextDestroyed(ServletContextEvent event) {
        System.out.println("容器销毁" + event.getServletContext().getContextPath());
    }
}

以上程序需要注意的是在类的上面有个注解@WebListener这个就相当于在web.xml中写入了

<listener>
    <listener-class>
        ListenerServletContextTest
    </listener-class>
</listener>

在本程序的容器初始化和销毁的过程中,分别通过ServletContextEvent事件对象取得ServletContext实列,然后调用getContextPath()方法。所以在容器启动时,后台会输出以下内容。

13.png

这里注意一下,当我们的程序中既有Servlet,FIlter,Listener的时候,我们的配置文件web.xml怎么写。

首先配置过滤器:<filter>,<filter-mapping>

再配置监听器: <listener>

最后配置简单Servlet:<servlet>,<servlet-mapping>

ServletContextAttributeListener

对Servlet上下文的属性操作监听,可以使用javax.servlet.ServletContextAttributeListener接口,此接口定义的方法,如下表所示:

No.方法类型描述
1public void attributeAdded(ServletContextAttributeEvent scab)普通添加属性时触发
2public void attributeRemoved(ServletContextAttributeEvent scab)普通删除属性时触发
3public void attributeReplaced(ServletContextAttributeEvent scab)普通替换属性时(重复设置)触发

同样可以看到,它的三个方法参数都是ServletContextAttributeEvent 实列,因此,在上下文属性监听操作中,一旦触发了ServletContextAttributeListener接口中定义的方法后,我们就可以通过ServletContextAttributeEvent进行事件的处理,此事件定义的方法如下所示。

No.方法类型描述
1public String getName()普通取得设置的属性的名称
2public Object getValue()普通取得设置的属性的内容

对Servlet上下文属性监听代码示例

ListenerServletContextAttributeTest.java

import javax.servlet.ServletContextAttributeEvent;
import javax.servlet.ServletContextAttributeListener;
import javax.servlet.annotation.WebListener;

/*
* 与add.jsp,remove.jsp,replace.jsp配合演示对Servlet Context上下文属性的监听
* */
@WebListener
public class ListenerServletContextAttributeTest implements ServletContextAttributeListener {
    @Override
    public void attributeAdded(ServletContextAttributeEvent event){
        System.out.println("增加属性:"+event.getName()+"属性内容:"+event.getValue());
    }
    @Override
    public void attributeRemoved(ServletContextAttributeEvent event){
        System.out.println("删除属性:"+event.getName()+"属性内容:"+event.getValue());
    }
    @Override
    public void attributeReplaced(ServletContextAttributeEvent event){
        System.out.println("替换属性:"+event.getName()+"属性内容:"+event.getValue());
    }
}

add.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/27
  Time: 21:01
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>增加属性</title>
</head>
<body>
<%
application.setAttribute("add","新增属性");
%>
</body>
</html>

remove.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/27
  Time: 21:13
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>删除属性</title>
</head>
<body>
<%
application.removeAttribute("add");
%>
</body>
</html>

replace.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/27
  Time: 21:14
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>替换属性</title>
</head>
<body>
<%
application.setAttribute("add","再次设置add属性");
%>
</body>
</html>

启动Web容器过后,在浏览器依次访问上面三个jsp页面,输出如下

14.png

可以看到,我们每次对application对象属性进行操作的事件的确是被监听到的。

对session进行监听

在监听器中,针对于session的监听操作主要使用HttpSessionListener,HttpSessionAttributeListener和HttpSessionBindingListener接口。

HttpSessionListener

通过此接口可以对session的状态进行监听,当需要创建或者销毁的时候可以实现 javax.servlet.http.HttpSessionListener接口,此接口定义的方法如下表

No.方法类型描述
1public void sessionCreated(HttpsessionEvent se)普通sessio创建时调用
2public void sessionDestroyed(HttpsessionEvent se)普通sessio销毁时调用

可以看出当session创建或者销毁的时候将产生HttpsessionEvent事件,此接口的方法为

No.方法类型描述
1public Httpsession getSession()普通取得当前的session对象

对session的监听的具体代码与application差不多,将前面的理解过后,对session的监听的代码就很写了,这里就不举例了。

这里需要注意的是:每当一个新用户打开一个动态网页过后,服务器就会自动为用户分配session,并且触发HttpSessionListener中的sessionCreated事件,但是在销毁的时候却存在两种可能。

  • 调用HttpSession的invalidate()方法,让一个session失效

  • 通过配置的session超时时间,session超时时间可以直接在项目的web.xml中配置

    <session-config>
        <session-timeout>5</session-timeout>
    </session-config>
    

    以上将一个session的超时事件配置成了5分钟,如果一个用户在5分钟后没有与服务器进行交互的话,那么服务器会默认此用户已经离开,会自动注销。如果没有配置超时时间,那么tomcat的默认的超时时间是30分钟

HttpsessionAttributeListener

同样在session监听中,也可以对他的属性进行监听,这一点与监听上下文属性的道理是一样的,要对session的属性进行监听,则要使用javax.servlet.http.HttpsessionAttributeListener接口完成,此接口定义的方法为:

No.方法类型描述
1public void attributeAdded(HttpSessionBindingEvent se)普通增加属性时触发
2public void attributeRemoved(HttpSessionBindingEvent se)普通删除属性时触发
3public void attributeReplaced(HttpSessionBindingEvent se)普通替换属性时触发

同样可以看出此接口的的三个方法都需要一个HttpSessionBindingEvent 对象参数,所以我们可以通过HttpSessionBindingEvent 接口来进行事件的处理,此接口的方法如下表

No.方法类型描述
1public HttpSession getSession()普通取得session
2public String getName()普通取得属性的名称
3public Object getValue()普通取得属性的内容

同样这里与监听上下文属性的代码差不多,就不在写代码进行实列。

HttpSessionBindingListener

之前所有介绍的session监听接口都需要在web.xml中配置,或者以注解的形式标明后才可以起作用,但是在web中也提供了一个javax.servlet.http.HttpSessionBindingListener接口,通过此接口实现的监听程序可以不用配置而直接使用,此接口定义的方法如下表:

No.方法类型描述
1public void valueBound(HttpSessionBindingEvent event)普通绑定对象到session时触发
2public void valueUnbound(HttpSessionBindingEvent event)普通session中移除对象时触发

下面看一段代码

LoginUser.java

package Login;

import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

/*
* 利用HttpSessionBindingListener对登录用户一对一的监听
* */
public class LoginUser implements HttpSessionBindingListener {
    private String username;

    public LoginUser(String username){
        this.username=username;
    }

    public String getUsername() {
        return username;
    }

    @Override
    public void valueBound(HttpSessionBindingEvent event){
        System.out.println("在session中保存LoginUser对象(name ="+this.getUsername()+" ),session id ="+event.getSession().getId());

    }
    @Override
    public void valueUnbound(HttpSessionBindingEvent event){
        System.out.println("在session中移除LoginUser对象(name ="+this.getUsername()+" ),session id ="+event.getSession().getId());
    }
}

session.jsp

<%--
  Created by IntelliJ IDEA.
  User: yqh
  Date: 2019/3/27
  Time: 23:09
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="Login.LoginUser" %>
<html>
<head>
    <title>seeion</title>
</head>
<body>

<%
LoginUser user=new LoginUser("yqh");
session.setAttribute("login",user);
%>
</body>
</html>

在浏览器输入:http://localhost:8080/session.jsp会在控制台看见 输出如下

在session中保存LoginUser对象(name =yqh ),session id =74ECCECACD71DE300A3601A51CFE0AAA

这里需要注意的一个问题就是,虽然HttpSessionBindingListener不需要配置,但是HttpSessionBindingListener他必须需要实例化放入一个session中,才可以监听。从监听范围上比较,HttpSessionListener设置一次就可以监听所有session,HttpSessionBindingListener通常都是一对一的。

正是这种区别成就了HttpSessionBindingListener的优势,我们可以让每个listener对应一个username,这样就不需要每次再去session中读取username,进一步可以将所有操作在线列表的代码都移入listener,更容易维护。

对request进行监听

对requset操作进行监听,主要使用ServletRequestListener和ServletRequestAttributeListener两个接口。

ServletRequestListener

ServletRequestListener对请求状态监听,当需要对用户的每一次请求进行监听的时候,可以使用 Javax.servlet.ServletRequestListener接口,该接口定义的方法如下表

No.方法类型描述
1public void requestDestroyed(ServletRequestEvent sre)普通请求开始时调用
2public void requestInitialized(ServletRequestEvent sre)普通请求结束时调用

同样这个接口的方法需要一个ServletRequestEvent类型参数,这个接口的方法如下表:

No.方法类型描述
1public ServletRequest getServletRequest()普通取得ServletRequest对象
2public ServletContext getServletContext()普通取得ServletContext对象

通过这个接口就取得相应的对象过后,就可以开始相应的操作了。

ServletRequestAttributeListener

对request范围的属性可以使用javax.servlet.ServletRequestAttributeListener接口进行监听,它的方法如下表

No.方法类型描述
1public void attributeAdded(ServletRequestAttributeEvent srae)普通属性增加时调用
2public void attributeRemoved(ServletRequestAttributeEvent srae)普通属性删除时调用
3public void attributeReplaced(ServletRequestAttributeEvent srae)普通属性替换时调用

ServletRequestAttributeEvent接口的方法如下

No.方法类型描述
1public java.lang.String getName()普通取得设置属性的名称
2public java.lang.Object getValue()普通取得设置属性的内容

通过这两个接口的配合就可以实现对request操作属性的监听了。


标题:Servlet学习记录
作者:geektomya
地址:HTTPS://blog.zhqy.xyz/articles/2019/12/13/1576226249913.html
彧语:乾坤未定,你我皆是黑马!!!