JavaWeb——Tomcat介绍、Servlet技术

146 阅读12分钟

这里主要参考黑马程序员课程进行学习,网址www.bilibili.com/video/BV1Qf…

Apache Tomcat

简介

Tomcat是Apache软件基金会的一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/jsp少量JavaEE规范

JavaEE: Java Enterprise Edition,Java企业版,指的是Java企业级开发的技术规范总和。包含13项技术规范:JDBC、XML、Servlet等等

基本使用

Snipaste_2024-11-20_09-43-26.png

Tomcat配置

  • 配置

    • 修改启动的端口号,在路径conf/server.xml

Snipaste_2024-11-20_09-49-16.png

Snipaste_2024-11-20_14-15-30.png

Snipaste_2024-11-20_14-21-50.png

编译后的Java字节码文件和resources的资源文件,放到WEB-INF下的classes目录下

pom.xml中依赖坐标对应的jar包,放入WEB-INF下的lib目录下

IDEA中部署tomcat

集成本地tomcat

在idea中集成本地tomcat

使用Tomcat的Maven插件

Snipaste_2024-11-20_15-01-38.png

Servlet

Servlet快速入门

Servlet是Java提供的一门动态web资源开发技术

Servlet是JavaEE规范之一,实际上就是一个接口,将来我们需要定义Servlet类实现Servlet接口,并且由web服务器运行Servlet

Snipaste_2024-11-20_15-32-11.png

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

这里实际上 scope 标签里面必须将其生命周期定义为 provided

provided的生命周期,其作用范围实际上就只在 编译和测试 而在运行时没有,这是由于tomcat之中本身也有servlet协议,有可能冲突

Servlet执行流程

@WebServlet("/demo1")
public class ServletDemo implements Servlet {

}

http://localhost:8080/tomcat_demo1/demo1

这里,我们来解析这个url

其中 http://localhost:8080指向的是web服务器

/tomcat_demo1 指向的是我们的web项目本体

/demo1 指代的是我们分配的这个servlet的地址

  1. Servlet由谁来创建呢?Servlet的方法由谁来调用呢?

    • Servlet由web服务器创建,Servlet方法由web服务器调用
  2. 服务器怎么知道Servlet中一定有service方法?

    • 因为我们自定义的Servlet,必须实现Servlet接口并且复写其方法,而Servlet接口中有service方法

Servlet生命周期

package com.itheima.web;
​
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
@SuppressWarnings("all")
@WebServlet("/demo1")
public class ServletDemo implements Servlet {
​
    /**
     * 初始化方法
     * 1、调用时机:默认情况下,当servlet被第一次调用时,调用该方法
     * 2、调用次数:only一次
     * @param servletConfig
     * @throws ServletException
     */
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("service init……");
    }
​
    /**
     * 1、调用时机:在每一次请求Servlet时
     * 2、调用次数:每次请求Servlet时,都会调用一次
     * @param servletRequest
     * @param servletResponse
     * @throws ServletException
     * @throws IOException
     */
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("servlet hello world");
    }
​
    /**
     * 服务终止,当需要释放内存或者容器关闭。
     * 只会调用一次
     */
    public void destroy() {
        System.out.println("Servlet destroy……");
    }
    public ServletConfig getServletConfig() {
        return null;
    }
​
​
    public String getServletInfo() {
        return null;
    }
​
​
}

这里有一个很重要的点,需要理解:

就是对于Servlet本身而言,我们实际上需要调用的基本上只有 service方法,但是Servlet是一个接口,其中又有以上五个抽象方法,那么也就意味着,只要我们使用Servlet,都需要实现着5个方法。这是相当麻烦的!!!

所以这里后来人对其进行了实现,书写了很多实现类,这时候我们只需要来实现对应的实现类,进而来简化操作。

Servlet体系结构

Snipaste_2024-11-20_17-00-48.png

这里我们直接来继承 HttpServlet

package com.itheima.web;
​
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("/demo4")
public class HttpServletDemo extends HttpServlet {
    /**
     * 捕获web页面的get请求,然后针对get请求来进行操作
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doget……");
    }
​
    /**
     * 捕获web页面的post请求,然后针对post请求进行操作
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("do post……");
    }
}
​

这里我们直接来对于源码进行解答:

实际上这里有点玄学:

先把玄学的部分给说了:

  1. 首先明确 HttpServlet是一个抽象类,public abstract class HttpServlet extends GenericServlet
  2. 无论是创建Servlet——调用init()方法,还是监听请求,调用service()方法 ,都是由Web容器(如Tomcat)自动管理的,而不需要编程人员主动编码实现。

HttpServlet抽象类中的service()方法,实际上它是来识别请求的方法,以此来进行分类是进行get方法处理还是进行post方法处理

接下来看看HttpServlet源码部分

web容器(比如Tomcat)自动识别调用这个service()方法,进入下面HttpServletservice()方法

public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
        //对传入的请求 req 和res进行强制类型转换
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        //调用另一个重写的service方法
        this.service(request, response);
    } else {
        throw new ServletException("non-HTTP request or response");
    }
}

另一个重写的service方法如下:

看下面的这个源码,能够看到,实际上就是根据获得的请求方法 req.getMethod()来判别究竟调用自己写的哪个方法。

protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String method = req.getMethod();
    long lastModified;
    if (method.equals("GET")) {
        lastModified = this.getLastModified(req);
        if (lastModified == -1L) {
            this.doGet(req, resp);
        } else {
            long ifModifiedSince = req.getDateHeader("If-Modified-Since");
            if (ifModifiedSince < lastModified) {
                this.maybeSetLastModified(resp, lastModified);
                this.doGet(req, resp);
            } else {
                resp.setStatus(304);
            }
        }
    } else if (method.equals("HEAD")) {
        lastModified = this.getLastModified(req);
        this.maybeSetLastModified(resp, lastModified);
        this.doHead(req, resp);
    } else if (method.equals("POST")) {
        this.doPost(req, resp);
    } else if (method.equals("PUT")) {
        this.doPut(req, resp);
    } else if (method.equals("DELETE")) {
        this.doDelete(req, resp);
    } else if (method.equals("OPTIONS")) {
        this.doOptions(req, resp);
    } else if (method.equals("TRACE")) {
        this.doTrace(req, resp);
    } else {
        String errMsg = lStrings.getString("http.method_not_implemented");
        Object[] errArgs = new Object[]{method};
        errMsg = MessageFormat.format(errMsg, errArgs);
        resp.sendError(501, errMsg);
    }
​
}

再回到最初始我们自己写的代码,集成抽象类HttpServlet并重载其doGetdoPost方法。这样就能够根据我们自己需要的逻辑来进行处理。

urlPattern配置

这里我们直接来看@WebServlet注解的源码即可

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package javax.servlet.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
    String name() default "";

    String[] value() default {};

    String[] urlPatterns() default {};

    int loadOnStartup() default -1;

    WebInitParam[] initParams() default {};

    boolean asyncSupported() default false;

    String smallIcon() default "";

    String largeIcon() default "";

    String description() default "";

    String displayName() default "";
}

针对上面注解的源码,如果看不太懂注解,可以参考我原来另一篇文章 ,Java注解

这里

  • @Target({ElementType.TYPE})表明这个注解是可以应用于类的任何元素上的
  • @Retention(RetentionPolicy.RUNTIME) 这个元注解表明 被修饰的这个注解是运行时有效
  • 在这里着重介绍 String[] urlPatterns() default {};

实际上我们在使用@WebServlet注解的时候,有时候会这样写 @WebServlet("/demo1") 这里实际上就是使用的是 String[] value() default {}; 它是可以省略的。

如果我们要同步使用别的这里面注解中的“成员变量”最好是使用urlPatterns

这里我们注意到 urlPatterns实际上是一个数组,那么也就意味着可以配置多个路径

比如: @WebServlet(urlPatterns={"/demo4","/demo1"})

另外再了解一下urlPattern配置规则

  • 精确匹配 —— 配置路径 @WebServlet("/user/select")

  • 目录匹配—— 配置路径 @WebServlet("/user/*")

    • 这时候访问的url可以是 localhost:8080/web_demo/user/aaa 或者是把aaa改成其他任何内容
  • 扩展名匹配—— 配置路径 @WebServlet("*.do")

    • 这时候访问的url可以是

      • localhost:8080/web_demo/aaa.do
      • localhost:8080/web_demo/bbb.do
  • 任意匹配—— 配置路径 @WebServlet("/") 或者是 @WebServlet("/*")

Request&Response

Snipaste_2024-11-20_20-26-40.png

上面这个图实在是画的太好了,我们直接引用过来,然后进行解释说明。

这里首先客户端(浏览器)会向服务器发送请求数据,实际上这些都是字符串,然后请求发送过来,就会被Web容器(比如Tomcat)所解析,解析之后,Tomcat就会把这些请求数据,放到一个对象里面——这个对象正好就是我们要说的request对象,故而request对象之中就会存储一大堆请求数据,故而我们后面编程就会使用request对象来获取对应的请求数据。

通过response对象来设置响应数据,Tomcat在响应给浏览器之前,会先把response对象中的响应数据给取出来,然后拼成符合HTTP响应规则的字符串,然后响应返回给浏览器。

Request获取请求数据

Snipaste_2024-11-20_20-54-14.png

注:只有请求方法为post的时候,请求体中才有内容

package com.itheima.web;
​
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.BufferedReader;
import java.io.IOException;
​
@WebServlet("/requestDemo")
public class RequestDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String method = req.getMethod();
        System.out.println(method);
        String contextPath = req.getContextPath();
        System.out.println(contextPath);
        StringBuffer requestURL = req.getRequestURL();
        System.out.println(requestURL.toString());
        String requestURI = req.getRequestURI();
        System.out.println(requestURI);
        String queryString = req.getQueryString();
        System.out.println(queryString);
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BufferedReader reader = req.getReader();
        String s = reader.readLine();
        System.out.println(s);
    }
}

使用postman通过get方法调用一下 http://localhost:8080/tomcat_demo1/requestDemo?name=zhangsan

输出结果为:

GET /tomcat_demo1 http://localhost:8080/tomcat_demo1/requestDemo /tomcat_demo1/requestDemo name=zhangsan

Request通用方法获取请求参数

通过上面的学习,我们实际上可以看到,如果请求是get方法,那么是通过getQueryString()方法来获取请求参数的。如果是post请求,那么就是通过req.getReader() 相关方法来获取参数的。

但是想象一下,如果我现在就传递两个参数比如 name = zhangsan 以及 age =18

无非是通过get请求还是post请求发送的问题,实际上我接收到对应的参数数据之后,进行处理的逻辑是完完全全一样的,但是我却要对于这个玩意儿写两套代码,这是完全不合理的。

▲所以Java贴心的给我们准备了这么一套通用的方法,无论是get请求还是post请求,都可以调用一套通用的方法来进行参数的获取

  • String getParameter(String var1); 根据名称获取参数值,单个值

  • Map<String, String[]> getParameterMap(); 获取所有参数的map集合

    • 比方说通过get方法传入,是这么传的 xxxxxx?name=zhangsan&money=1000&money=500
    • 那么获取到的话就是 key=name ,value=zhangsan 以及 key=money ,value = 1000,500
    • 注意到上面值的类型是 String[]哦~
  • String[] getParameterValues(String var1); 根据名称获取参数值,数组

实际上,对于以上的方法,我们可以这么简单理解。就是进行了一定程度的封装,然后在底层封装判断是get还是post请求,进而在底层来调用req.getQueryStrint()还是req.getReader()这种东西

那么我们的servlet代码就可以这么写:

package com.itheima.web;
​
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.BufferedReader;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
​
@WebServlet("/requestDemo")
public class RequestDemo extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Map<String, String[]> parameterMap = req.getParameterMap();
        Set<Map.Entry<String, String[]>> entries = parameterMap.entrySet();
        for(Map.Entry<String, String[]> entry:entries){
            System.out.println(entry.getKey());
            String[] value = entry.getValue();
            for(String s:value){
                System.out.println(s);
            }
        }
​
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
​
        this.doGet(req,resp);
    }
}
​

这里只是一个示例哈。

然后!有一个很奇怪的事儿,我就纳了闷了,我们直接进入req.getParameter的源码,进入的是HttpServletRequest的接口中的抽象方法(毕竟传入的参数req就是HttpServletRequest类型的嘛)

这时候尝试输出: req.getClass()

输出的结果为:class org.apache.catalina.connector.RequestFacade

OK,原来实际上是实现类RequestFacade给我们办的好事儿,做好事儿不留名是这样的。

Request参数中文乱码

详见下面文章:

请求参数中文乱码

Request请求转发

请求转发:是一种在服务器内部的资源跳转方式。

请求转发的特点:

  • 浏览器地址栏路径不发生变化
  • 只能转发到当前服务器的内部资源
  • 一次请求可以在转发的资源间使用request共享数据。

Snipaste_2024-11-21_14-17-27.png

这里直接写个小demo来进行演示:

RequestDemo1:

package com.itheima.web;
​
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("/requestDemo1")
public class RequestDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is demo1 do get");
        //把一个数据存储进去  键是name 对应的值为zhangsan
        req.setAttribute("name","zhangsan");
        //将请求转发给当前服务器的另一个地址 
        req.getRequestDispatcher("/requestDemo2").forward(req,resp);
​
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is demo1 do post");
        this.doGet(req,resp);
    }
}

RequestDemo2的代码:

package com.itheima.web;
​
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("/requestDemo2")
public class RequestDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is demo2 do get");
        Object name = req.getAttribute("name");
        System.out.println(name.toString());
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is demo2 do post");
        this.doGet(req,resp);
    }
}

Response设置相应数据

Snipaste_2024-11-21_14-30-41.png

Response完成重定向

关于重定向的路径问题说明:

需要明确路径是谁来使用

  • 浏览器使用:需要加虚拟目录(项目访问路径)
  • 服务端使用:不需要加虚拟目录

写一个小demo

ResponseDemo1代码如下

package com.itheima.web.response;
​
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("/resDemo1")
public class ResponseDemo1 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is ResponseDemo1 do get");
        //获取项目访问路径
        String contextPath = req.getContextPath();
        //设置重定向
        resp.setStatus(302);
        //因为response设置重定向,路径是给客户端用,所以要加上项目访问路径
        resp.setHeader("location",contextPath+"/resDemo2");
​
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is ResponseDemo1 do post");
        this.doGet(req,resp);
    }
}

Snipaste_2024-11-21_14-32-04.png ResponseDemo2代码如下

package com.itheima.web.response;
​
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("/resDemo2")
public class ResponseDemo2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is ResponseDemo2 do get");
​
​
    }
​
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("this is ResponseDemo2 do post");
        this.doGet(req,resp);
    }
}

个人总结

这里我最后还是想写一段自己的感悟,对于自己的理解落实到文字上。

  1. 首先还是明确,Tomcat是符合JavaEE中的部分规范的,其中这部分规范中就包括了Servlet规范

    • 所以这里一定要知道,Tomcat是能够自动监听请求,并且每次有请求过来的时候,就会执行一次service()方法。(至于怎么监听,怎么“自动的”,这个别管,总之可以)
  2. Java自己提供了一个Servlet接口(package javax.servlet;),这里就相当于是Java自动与Tomcat底层代码接上,调用的实际上是这个Servlet接口的service()方法

  3. 但是呢,这个Servlet接口里面,有五个方法,也就意味着,我们每次写这个接口的实现类,就必须要实现5个方法。这是没必要的。

    • 这里顺带提一嘴这个“父子关系”。 Servlet接口<—实现—GenericServlet(抽象类)<—继承—HttpServlet抽象类
  4. 结合上面父子关系,因为我们很多时候请求都是基于Http协议的,所以Java就最后给了我们一个HttpServlet抽象类。这个HttpServlet抽象类里面最最主要的是两个方法,doGet和doPost

这里我们再回过头来,看一下:

只要请求过来,就会自动调用service()方法;然后再结合Java的多态机制,调用的就会是HttpServlet中的service()方法,然后呢!Tomcat针对HttpServlet抽象类,又有一个实现类RequestFacade。那进而调用的我相信一定是RequestFacadeservice()方法。然后我们自个儿再编写自定义类来实现HttpServlet类并重载doGetdoPost方法,就能实现自己的逻辑。至此,完事儿,真厉害啊Java老兄。