Javaweb(一) servlet学习笔记

1 Servlet

1.1 Servlet概述

1.1.1 Applet

首先,先了解一下Applet (来源于百度百科https://baike.baidu.com/item/applet)
Applet是采用Java编程语言编写的小应用程序,该程序可以包含在 HTML(标准通用标记语言的一个应用)页中,与在页中包含图像的方式大致相同。
含有Applet的网页的HTML文件代码中部带有 和这样一对标记,当支持Java的网络浏览器遇到这对标记时,就将下载相应的小应用程序代码并在本地计算机上执行该Applet。

1.1.2 Servlet概述

Servlet是SUN公司提供的一套规范,名称就叫Servlet规范,它也是JavaEE规范之一。我们可以像学习Java基础一样,通过API来学习Servlet。这里需要注意的是,在我们之前JDK的API中是没有Servlet规范的相关内容,需要使用JavaEE的API。目前在Oracle官网中的最新版本是JavaEE8,该网址中介绍了JavaEE8的一些新特性。当然,我们可以通过访问官方API,学习和查阅里面的内容。打开官方API网址,在左上部分找到javax.servlet包,在左下部分找到Servlet,如下图显示:
在这里插入图片描述
通过阅读API,可以得到如下信息:
第一:Servlet是一个运行在web服务端的java小程序
第二:它可以用于接收和响应客户端的请求
第三:要想实现Servlet功能,可以实现Servlet接口,继承GenericServlet或者HttpServlet
第四:每次请求都会执行service方法
第五:Servlet还支持配置
具体请看下图:
在这里插入图片描述

1.2 Servlet入门

1.2.1Servlet执行过程分析

我们通过浏览器发送请求,请求首先到达Tomcat服务器,由服务器解析请求URL,然后在部署的应用列表中找到我们的应用。接下来,在我们的应用中找应用里的web.xml配置文件,在web.xml中找到FirstServlet的配置,找到后执行service方法,最后由FirstServlet响应客户浏览器。整个过程如下图所示:
servlet执行过程
浏览器——>Tomcat服务器——>我们的应用——>应用中的web.xml——>FirstServlet——>响应浏览器

1.2.2 Servlet类视图

Servlet类视图

1.2.3 Servlet编写方式

1)编写方式说明

我们在实现Servlet功能时,可以选择以下三种方式:
第一种:实现Servlet接口,接口中的方法必须全部实现。
​ 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。
第二种:继承GenericServlet,service方法必须重写,其他方可根据需求,选择性重写。
​ 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发Servlet变得简单。但是,此种方式是和HTTP协议无关的。
第三种:继承HttpServlet,它是javax.servlet.http包下的一个抽象类,是GenericServlet的子类。如果我们选择继承HttpServlet时,只需要重写doGet和doPost方法,不要覆盖service方法。
​ 使用此种方式,表示请求和响应需要和HTTP协议相关。也就是说,是通过HTTP协议来访问的。那么每次请求和响应都符合HTTP协议的规范。请求的方式就是HTTP协议所支持的方式(目前只知道GET和POST,而实际HTTP协议支持7种请求方式,GET POST PUT DELETE TRACE OPTIONS HEAD )。

2)HttpServlet的使用细节

第一步:在入门案例的工程中创建一个Servlet继承HttpServlet

注意:不要重写任何方法,如下图所示:
在这里插入图片描述在这里插入图片描述
第二步:部署项目并测试访问

当在地址栏输入ServletDemo2的访问URL时,出现了访问错误,状态码是405。提示信息是:方法不允许。

第三步:分析原因
得出HttpServlet的使用结论:
继承了HttpServlet,需要重写里面的doGet和doPost方法来接收get方式和post方式的请求。
为了实现代码的可重用性,只需要在doGet或者doPost方法中一个里面提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。
实现doGet或doPost请求原理介绍
  1、浏览器发送请求到HttpSevr类调用HttpServ的service(servletRequest, servletReponse)方法
  2、由于没有找到这个方法,去调用父类(HttpServlet) 的同名方法。
  3、父类的service方法将ServletRequest req请求转换成HttpServletRequest请求,再去调用service(request, response) 方法。
 将ServletRequest req请求转换成HttpServletRequest请求再调用service(request, response) 方法源码如下:

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

4、 调用的service(request, response) 方法功能是判断用户发出是什么请求,如果是get则调用子类(HttpSevrlet)的doGet方法,如果是post则调用子类(HttpSevrlet)的doPost方法。
service(request, response) 方法源码如下:

protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
 
        String method = req.getMethod();
 
        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);
        }
    }

5、调用关系图
在这里插入图片描述

1.3 Servlet使用细节

1.3.1 Servlet的生命周期

对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向 于开发的官方说法就是对象创建到销毁的过程。

出生:请求第一次到达Servlet时,对象就创建出来,并且初始化成功。只出生一次,就放到内存中。

活着:服务器提供服务的整个过程中,该对象一直存在,每次只是执行service方法。

死亡:当服务停止时,或者服务器宕机时,对象消亡。

通过分析Servlet的生命周期我们发现,它的实例化和初始化只会在请求第一次到达Servlet时执行,而销毁只会在Tomcat服务器停止时执行,由此我们得出一个结论,Servlet对象只会创建一次,销毁一次。所以,Servlet对象只有一个实例。如果一个对象实例在应用中是唯一的存在,那么就说它是单实例的,即运用了单例模式。

1.3.2 Servlet的线程安全

由于Servlet运用了单例模式,即整个应用中只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。接下来,看下面的的示例:

/**
 * 演示Servlet的线程安全问题:
 *    示例需求:
 *       模拟网上看书的翻页功能。
 *       (类似的有浏览商品的翻页,浏览论坛帖子的翻页)
 * 
 */
public class ServletDemo4 extends HttpServlet {

    /**
     * 我们讨论的是类成员的线程安全问题,所以要定义一个类成员
     */
    //定义浏览书籍的页码,都是从第一页开始的
    private int currentPage = 1;

    /**
     * 真正翻页看书的功能
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取当前要看的书名
        String bookName = req.getParameter("bookName");
        //2.输出书名和当前页码
        System.out.println("您看的是:"+bookName+",当前页码是:"+currentPage);
        //3.执行翻页
        currentPage++;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

这是一个模拟在网上看书的示例,在Servlet中记录了当前要看的页码,理想状态下,用户每次请求都来看自己该看的页码。启动服务,测试一下:
不同的用户共享了页码。

通过上面的测试发现,在Servlet中定义了类成员之后,多个浏览器都会共享类成员的数据。其实每一个浏览器端发送请求,就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享Servlet类成员中的数据,其中任何一个线程修改了数据,都会影响其他线程。因此,可以认为Servlet它不是线程安全的。

分析产生这个问题的根本原因,其实就是因为Servlet是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都是改变,而不会重新初始化。

解决这个问题也非常简单,就是在Servlet中定义类成员要慎重。如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题。如果类成员并非共用,或者每次使用都有可能对其赋值,那么就要考虑线程安全问题了,把它定义到doGet或者doPost方法里面去就可以了。

1.3.3 Servlet是单例还是多例

servlet是单例的,严格地说是一个ServletMapping对应一个单例实例(如果一个Servlet被映射了两个URL地址,会生成两个实例)后续另起一片学习记录,此处只做学习扩展

1.3.4Servlet的注意事项

1)映射Servlet的细节

Servlet支持三种映射方式,以达到灵活配置的目的。
首先编写一个Servlet,代码如下:

/**
 * 演示Servlet的映射方式
 */
public class ServletDemo5 extends HttpServlet {

    /**
     * doGet方法输出一句话
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("ServletDemo5接收到了请求");
    }

    /**
     * 调用doGet方法
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

第一种:指名道姓的方式

​ 此种方式,只有和映射配置一模一样时,Servlet才会接收和响应来自客户端的请求。
​ 例如:映射为:/servletDemo5
​ 访问URL:http://localhost:8585/servlet_demo/servletDemo5

在这里插入图片描述
第二种:/开头+通配符的方式

​ 此种方式,只要符合目录结构即可,不用考虑结尾是什么。
​ 例如:映射为:/servlet/*
​ 访问URL:http://localhost:8585/servlet/itheima
http://localhost:8585/servlet/itcast.do
​ 这两个URL都可以。因为用的*,表示/servlet/后面的内容是什么都可以。
在这里插入图片描述
第三种:通配符+固定格式结尾

​此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确)
​ 例如:映射为:.do
​ 访问URL:http://localhost:8585/servlet/itcast.do
http://localhost:8585/itheima.do
这两个URL都可以方法。因为都是以.do作为结尾,而前面用
号通配符配置的映射,所有无须关心。

在这里插入图片描述
通过测试发现,Servlet支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的Servlet映射都符合请求URL时,由谁来响应呢?注意:HTTP协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,来明确一下,多种映射规则的优先级。
先说结论:指名道姓的方式优先级最高,带有通配符的映射方式,有/的比没/的优先级高
所以,前面的三种映射方式的优先级为:第一种>第二种>第三种。
演示代码如下:

/**
 * 它和ServletDemo5组合演示Servlet的访问优先级问题
 *
 */
public class ServletDemo6 extends HttpServlet {

    /**
     * doGet方法输出一句话
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("ServletDemo6接收到了请求");
    }

    /**
     * 调用doGet方法
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}
<!--配置ServletDemo6-->
<servlet>
    <servlet-name>servletDemo6</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo6</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo6</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

运行结果如下:
在这里插入图片描述

2)多路径映射Servlet

它其实就是给一个Servlet配置多个访问映射,从而可以根据不同请求URL实现不同的功能。
首先,创建一个Servlet:

/**
 * 演示Servlet的多路径映射
 *
 */
public class ServletDemo7 extends HttpServlet {

    /**
     * 根据不同的请求URL,做不同的处理规则
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //1.获取当前请求的URI
        String uri = req.getRequestURI();
        uri = uri.substring(uri.lastIndexOf("/"),uri.length());
        //2.判断是1号请求还是2号请求
        if("/servletDemo7".equals(uri)){
            System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示");
        }else if("/demo7".equals(uri)){
            System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示");
        }else {
            System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示");
        }
    }

    /**
     * 调用doGet方法
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req,resp);
    }
}

接下来,在web.xml配置Servlet:

<!--配置ServletDemo7-->
<servlet>
    <servlet-name>servletDemo7</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class>
</servlet>
<!--映射路径1-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/demo7</url-pattern>
</servlet-mapping>
<!--映射路径2-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/servletDemo7</url-pattern>
</servlet-mapping>
<!--映射路径3-->
<servlet-mapping>
    <servlet-name>servletDemo7</servlet-name>
    <url-pattern>/servlet/*</url-pattern>
</servlet-mapping>

最后,启动服务测试运行结果:
在这里插入图片描述

3)启动时创建Servlet

我们前面讲解了Servlet的生命周期,Servlet的创建默认情况下是请求第一次到达Servlet时创建的。但是我们都知道,Servlet是单例的,也就是说在应用中只有唯一的一个实例,所以在Tomcat启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?

  • 第一种:应用加载时创建Servlet,它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。它的弊端也同样明显,因为在应用加载时就创建了Servlet对象,因此,导致内存中充斥着大量用不上的Servlet对象,造成了内存的浪费。
  • 第二种:请求第一次访问是创建Servlet,它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的Servlet对象都没有创建,因此也提高了服务器的启动时间。而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,它都没法完成,从而要考虑其他技术实现。

通过上面的描述,相信同学们都能分析得出何时采用第一种方式,何时采用第二种方式。就是当需要在应用加载就要完成一些工作时,就需要选择第一种方式。当有很多Servlet的使用时机并不确定是,就选择第二种方式。

在web.xml中是支持对Servlet的创建时机进行配置的,配置的方式如下:我们就以ServletDemo3为例。

<!--配置ServletDemo3-->
<servlet>
    <servlet-name>servletDemo3</servlet-name>
    <servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>
    <!--配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建
        配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高
    -->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>servletDemo3</servlet-name>
    <url-pattern>/servletDemo3</url-pattern>
</servlet-mapping>

在这里插入图片描述

4)默认Servlet

默认Servlet是由服务器提供的一个Servlet,它配置在Tomcat的conf目录下的web.xml中。如下图所示:
在这里插入图片描述
它的映射路径是<url-pattern>/<url-pattern>,我们在发送请求时,首先会在我们应用中的web.xml中查找映射配置,找到就执行,这块没有问题。但是当找不到对应的Servlet路径时,就去找默认的Servlet,由默认Servlet处理。所以,一切都是Servlet。

1.4 Servlet关系总图

在这里插入图片描述
参考:https://www.cnblogs.com/myseries/p/10695141.html
在此感谢黑马程序员的大数据课程!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值