Servlet简介

  • 网络请求即是进行Http连接,http本质上也是通过tcp进行数据传输。使用原生的http``tcp接口十分复杂,所以我们可以借助servlet等web容器,把连接等底层步骤交给servlet和web服务器去做
  • 我们使用Servlet API编写自己的Servlet来处理HTTP请求,Web服务器实现Servlet API接口。Servlet帮助我们完成大量的底层工作

  • 每一次请求,servlet容器都会对请求生成HttpServletResponseHttpServletRequest2个数据,这两个数据包含了一些请求的信息,如请求头,请求方式,cookie,请求参数和返回参数等等
    • Response还可以设置返回状态,默认成功状态是200
  • 请求体和响应体无关请求方式,post和get都能用

  • 每次请求servlet都会根据请求路径发送到对应的servlet

    HttpServlet…

    HttpServletRequest

    HttpServletRequest封装了一个HTTP请求,它实际上是从ServletRequest继承而来。最早设计Servlet时,设计者希望Servlet不仅能处理HTTP,也能处理类似SMTP等其他协议,因此,单独抽出了ServletRequest接口,但实际上除了HTTP外,并没有其他协议会用Servlet处理,所以这是一个过度设计。
    我们通过HttpServletRequest提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:

  • getMethod():返回请求方法,例如,”GET”,”POST”;

  • getRequestURI():返回请求路径,但不包括请求参数,例如,”/hello”;
  • getQueryString():返回请求参数,例如,”name=Bob&a=1&b=2”;
  • getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
  • getContentType():获取请求Body的类型,例如,”application/x-www-form-urlencoded”;
  • getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串””;
  • getCookies():返回请求携带的所有Cookie;
  • getHeader(name):获取指定的Header,对Header名称不区分大小写;
  • getHeaderNames():返回所有Header名称;
  • getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
  • getReader():和getInputStream()类似,但打开的是Reader;
  • getRemoteAddr():返回客户端的IP地址;
  • getScheme():返回协议类型,例如,”http”,”https”;

此外,HttpServletRequest还有两个方法:setAttribute()和getAttribute(),可以给当前HttpServletRequest对象附加多个Key-Value,相当于给HttpServletRequest添加了一个临时的数据存放map
调用HttpServletRequest的方法时,注意务必阅读接口方法的文档说明,因为有的方法会返回null,例如getQueryString()的文档就写了:
This method returns null if the URL does not have a query string…

HttpServletResponse

HttpServletResponse封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse对象时,必须先调用设置Header的方法,最后调用发送Body的方法。
常用的设置Header的方法有:

  • setStatus(sc):设置响应代码,默认是200;
  • setContentType(type):设置Body的类型,例如,”text/html”;
  • setCharacterEncoding(charset):设置字符编码,例如,”UTF-8”;
  • setHeader(name, value):设置一个Header的值;
  • addCookie(cookie):给响应添加一个Cookie;
  • addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;

写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。
写入响应前,无需设置setContentLength(),因为底层服务器会根据写入的字节数自动设置,如果写入的数据量很小,实际上会先写入缓冲区,如果写入的数据量很大,服务器会自动采用Chunked编码让浏览器能识别数据结束符而不需要设置Content-Length头。
但是,写入完毕后调用flush()却是必须的,因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。此外,写入完毕后千万不要调用close(),原因同样是因为会复用TCP连接,如果关闭写入流,将关闭TCP连接,使得Web服务器无法复用此TCP连接。
写入完毕后对输出流调用flush()而不是close()方法!
有了HttpServletRequest和HttpServletResponse这两个高级接口,我们就不需要直接处理HTTP协议。注意到具体的实现类是由各服务器提供的,而我们编写的Web应用程序只关心接口方法,并不需要关心具体实现的子类。

准备:

新建javaweb项目

  • 在maven项目的基础上增加这些文件:webapp,WEB-INF,web.xml这些是必须的web应用文件及文件夹

    1. web-servlet-hello
    2. ├── pom.xml
    3. └── src
    4. └── main
    5. ├── java
    6. ├── resources
    7. └── webapp
    8. └── WEB-INF
    9. └── web.xml

    导入Servlet依赖

  • Servlet API是一个jar包,我们需要通过Maven来引入它,才能正常编译。

    • <scope>指定为provided,表示编译时使用,但不会打包到.war文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
  • 注意到这个pom.xml与前面我们讲到的普通Java程序有个区别,打包类型不是jar,而是war,表示Java Web Application Archive(java网络应用包):<packaging>war</packaging>
    1. <project xmlns="http://maven.apache.org/POM/4.0.0"
    2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    4. <modelVersion>4.0.0</modelVersion>
    5. <groupId>com.itranswarp.learnjava</groupId>
    6. <artifactId>web-servlet-hello</artifactId>
    7. <packaging>war</packaging>
    8. <version>1.0-SNAPSHOT</version>
    9. <properties>
    10. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    11. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    12. <maven.compiler.source>11</maven.compiler.source>
    13. <maven.compiler.target>11</maven.compiler.target>
    14. <java.version>11</java.version>
    15. </properties>
    16. <dependencies>
    17. <dependency>
    18. <groupId>javax.servlet</groupId>
    19. <artifactId>javax.servlet-api</artifactId>
    20. <version>4.0.0</version>
    21. <scope>provided</scope>
    22. </dependency>
    23. </dependencies>
    24. <build>
    25. <finalName>hello</finalName>
    26. </build>
    27. </project>

配置web.xml

  • 我们还需要在工程目录下创建一个web.xml描述文件,放到src/main/webapp/WEB-INF目录下(固定目录结构,不要修改路径,注意大小写)。文件内容可以固定如下:

    <!DOCTYPE web-app PUBLIC
    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
    <display-name>Archetype Created Web Application</display-name>
    </web-app>
    

    整个工程结构如下:

    web-servlet-hello
    ├── pom.xml
    └── src
      └── main
          ├── java
          │   └── com
          │       └── itranswarp
          │           └── learnjava
          │               └── servlet
          │                   └── HelloServlet.java
          ├── resources
          └── webapp
              └── WEB-INF
                  └── web.xml
    

    第一个Servlet应用

  • 一个Servlet应用类总是继承自**HttpServlet**,然后覆写**doGet()****doPost()**方法。一个项目可以有很多个Servlet

    • doGet()方法可以传入HttpServletRequestHttpServletResponse两个对象,分别代表HTTP请求和响应。
    • 我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest和HttpServletResponse就已经封装好了请求和响应
  • 输出中文需要修改编码resp.setCharacterEncoding("UTF-8");
    // WebServlet注解表示这是一个Servlet,并映射到地址/:
    @WebServlet(urlPatterns = "/") 
    public class HelloServlet extends HttpServlet {
    //获取HTTP请求与响应
      protected void doGet(HttpServletRequest req, HttpServletResponse resp)
          throws ServletException, IOException {
          // 设置响应类型:
          resp.setContentType("text/html");
          // 获取输出流:
          PrintWriter pw = resp.getWriter();
          // 写入响应:
          pw.write("<h1>Hello, world!</h1>");
          // 最后不要忘记flush强制输出:
          pw.flush();
      }
    }
    

打包部署web程序

打包:运行Maven命令mvn clean package,在target目录下得到一个hello.war文件,这个文件就是我们编译打包后的Web应用程序。(可以在idea结构图的war文件那,右键 copy->copy Path知道war文件路径)

  • 需要配置maven环境变量才能使用maven的命令
    • 如果环境变量配置成功,idea里还是不能使用mvn命令,就管理员身份运行idea)


  • 运行:先启动Web服务器,再由Web服务器加载我们编写的HelloServlet,这样就可以让HelloServlet处理浏览器发送的请求。
    • 把hello.war复制到Tomcat的webapps目录下,然后切换到bin目录,执行**.\startup.sh****.\startup.bat**启动Tomcat服务器:(win10里面./和.\一样的)

错误之一
执行startup.bat后报文件提前结束。是因为web.xml文件是空文件。 写入<xml version="1.0” encoding=”utf-8" />即可。正确启动服务器并加载项目文件,startup.bat命令末尾会提示?.war项目的部署已在?ms内完成
在浏览器输入http://localhost:8080/hello/即可看到HelloServlet的输出

浏览器运行

  • 输入[http://localhost:8080/?/](http://localhost:8080/hello/) ?为war文件名

    • 可以把webapp下所有文件删除。然后将war文件名改为ROOT.war就可以将项目页面改为localhost:8080的默认页面
    • tomcat启动时命令行的**http-nio-xxxx**即可快速看到端口号

      Servlet和Tomcat原理及注意

      image.png
      实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()方法,然后由Tomcat负责加载我们的.war文件,并创建一个HelloServlet实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/(假定部署文件为ROOT.war),就转发到HelloServlet并传入HttpServletRequestHttpServletResponse两个对象。
      因为我们编写的Servlet并不是直接运行,而是由Web服务器加载后创建实例运行,所以,类似Tomcat这样的Web服务器也称为Servlet容器。
  • 在Servlet容器中运行的Servlet具有如下特点:

    • 无法在代码中直接通过new创建Servlet实例,必须由Servlet容器自动创建Servlet实例;
    • Servlet容器只会给每个Servlet类创建唯一实例;
    • Servlet容器会使用多线程执行doGet()doPost()方法。

Servlet多线程模型

一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的doGet()、doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题:
public class HelloServlet extends HttpServlet { private Map map = new ConcurrentHashMap<>(); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 注意读写map字段是多线程并发的: this.map.put(key, value); } }
对于每个请求,Web服务器会创建唯一的HttpServletRequest和HttpServletResponse实例,因此,HttpServletRequest和HttpServletResponse实例只有在当前处理线程中有效,它们总是局部变量,不存在多线程共享的问题。


  • 在Servlet中定义的实例变量会被多个线程同时访问,要注意线程安全;
  • **HttpServletRequest****HttpServletResponse**实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
  • doGet()doPost()方法中,如果使用了ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为Servlet容器很可能用线程池实现线程复用。

因此,正确编写Servlet,要清晰理解Java的多线程模型,需要同步访问的必须同步。

内部启动Tomcat

  • tomcat可以实现在idea里启动项目和tomcat,无需再打包然后复制到webapp中。

    导入扩展依赖

  • 在常规web项目的基础上增加如下依赖,这2个依赖都是用于内嵌tomcat的

    • 这两个依赖也相当于引入了一个tomcat服务器,通过api来设置与启动服务器
  • 这两个依赖的版本信息必须设置为tomcat服务器的版本

    • tomcat-embed-jasper
    • tomcat-embed-core
      <dependency>
             <groupId>org.apache.tomcat.embed</groupId>
             <artifactId>tomcat-embed-core</artifactId>
             <version>8.5.59</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.tomcat.embed</groupId>
             <artifactId>tomcat-embed-jasper</artifactId>
             <version>8.5.59</version>
             <scope>provided</scope>
         </dependency>
      

      在main()中启动tomcat

  • tomcat启动代码貌似必须写在**main**方法中

    • 启动代码如下,照搬即可,无需修改
      public class Main {
      public static void main(String[] args) throws Exception {
         // 启动Tomcat:
         Tomcat tomcat = new Tomcat();
         //设置服务器端口
         tomcat.setPort(Integer.getInteger("port", 8888));
         //建立连接
         tomcat.getConnector();
         // 创建webapp:
         Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
         WebResourceRoot resources = new StandardRoot(ctx);
         resources.addPreResources(
                 new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
         ctx.setResources(resources);
         tomcat.start();
         tomcat.getServer().await();
      }
      }
      
      下面即成功运行的截图:
      image.png