Web开发基础|青训营笔记(一)

118 阅读8分钟

Web基础


BS架构

访问网站、使用App时,都是基于Web的Browser/Server模式,简称BS架构

  • 客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端
  • 浏览器只需要请求服务器,获取Web页面并展示给用户即可
  • BS架构下,服务器端升级后,客户端无需任何部署就可以使用到新的版本

Servlet接口

Servlet基础


  1. 编写Web应用程序就是编写Servlet处理HTTP请求;
  2. Servlet API提供HttpServletRequestHttpServletResponse两个高级接口封装HTTP请求和响应;
  3. Web应用程序必须按固定结构组织并打包为.war文件;
  4. 需要启动Web服务器来加载webapps中的war包并运行Servlet

完整web开发流程

  1. 编写Servlet;
  2. 打包为war文件;
  3. 复制到Tomcat的webapps目录下;
  4. 启动Tomcat

使用web服务器提供的Servlet API编写Servlet来处理HTTP请求,从而将处理TCP连接、解析HTTP协议等底层工作都交给现成的Web服务器去实现,以实现高效、可靠的开发。因此,编写Web应用程序就是编写Servlet处理HTTP请求。

如下图,浏览器与web server通过HTTP协议建立连接,web server提供Servlet API,web应用程序利用这些接口来处理HTTP请求!

image.png

简单的Servlet

// WebServlet注解:这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    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();
    }
}
  • Servlet类总是继承自HttpServlet,并覆写doGet()doPost()方法
  • doGet()方法传入HttpServletRequestHttpServletResponse两个对象代表HTTP请求和响应
  • 使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequestHttpServletResponse已封装好了请求和响应。以发送响应为例,只需要设置正确的响应类型,然后获取PrintWriter输出流,写入响应即可。

Servlet API获取

Servlet API是一个jar包,需要通过Maven引入才能正常编译。jar包的引入方式编写在pom.xml文件中:

<packaging>war</packaging>	//打包类型:war包(Java Web Application Archive)

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0</version>
    <scope>provided</scope> //provided表示编译时使用,不会打包到.war文件中
</dependency>				//因为运行期Web服务器已提供Servlet API相关的jar包

执行war包

  1. 运行maven命令得到war文件,即打包后的web应用程序
  2. 将war包放入web serverwebapps目录下
  3. 启动web服务器并加载自编Servlet类
  4. Servlet类处理浏览器请求并响应

支持Servlet API的常用Web服务器有:

  • Tomcat:由Apache开发的开源免费服务器;
  • Jetty:由Eclipse开发的开源免费服务器;
  • GlassFish:一个开源的全功能JavaEE服务器。

Servlet容器

实际上,类似Tomcat这样的服务器也是Java编写的,启动Tomcat服务器实际上是启动Java虚拟机,执行Tomcat的main()方法,然后由Tomcat负责加载我们的.war文件,并创建一个HelloServlet实例,最后以多线程的模式来处理HTTP请求。如果Tomcat服务器收到的请求路径是/(假定部署文件为ROOT.war),就转发到HelloServlet并传入HttpServletRequestHttpServletResponse两个对象。

编写的Servlet并不直接运行,而是由Web服务器加载后创建实例运行,所以Web服务器也称Servlet容器。

Servlet容器中运行的Servlet具有如下特点:

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

复习Java多线程的内容,可以得出结论:

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

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

Servlet开发


Servlet开发时通过main()方法启动Tomcat服务器并加载webapp有如下好处:

  1. 启动简单,无需下载Tomcat或安装任何IDE插件;
  2. 调试方便,可在IDE中使用断点调试;
  3. 使用Maven创建war包后,也可以正常部署到独立的Tomcat服务器中

引入嵌入式Tomcat服务器

开发Servlet时,可使用main()方法启动嵌入式Tomcat服务器并加载当前工程的webapp,便于开发调试,且不影响打包部署,能极大地提升开发效率。

pom.xml中仍定义<packaging>类型仍然为war,需要引入依赖tomcat-embed-coretomcat-embed-jasper,并定义引入的Tomcat版本<tomcat.version>9.0.26。不必引入Servlet API,因为引入Tomcat依赖后自动引入了Servlet API。

编写main()方法,启动Tomcat服务器

public class Main {
    public static void main(String[] args) throws Exception {
        // 启动Tomcat:
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(Integer.getInteger("port", 8080));
        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();
    }
}

直接运行main()方法,无需用mvn打包成war包,即可启动嵌入式Tomcat服务器,然后,通过预设的tomcat.addWebapp("", new File("src/main/webapp"),Tomcat会自动加载当前工程作为根webapp,可直接在浏览器访问http://localhost:8080/

Servlet进阶

1. 请求响应处理

  • 一个Webapp可以有多个Servlet,分别映射不同的路径,从而处理不同的请求;
  • 映射为/的Servlet可处理所有“未匹配”的请求;
  • 如何处理请求取决于Servlet覆写的对应方法;
  • Web服务器通过多线程处理HTTP请求,一个Servlet的处理方法可以由多线程并发执行。

路径转发Dispatcher

一个Webapp完全可以有多个Servlet,分别映射不同的路径:

@WebServlet(urlPatterns = "/hello")		//路径hello转发到HelloServlet进行处理
public class HelloServlet extends HttpServlet {
    ...
}

@WebServlet(urlPatterns = "/signin")	
public class SignInServlet extends HttpServlet {
    ...
}

@WebServlet(urlPatterns = "/")		
public class IndexServlet extends HttpServlet {
    ...
}

浏览器发出的HTTP请求总是由Web Server先接收,然后根据Servlet配置的映射,不同的路径转发到不同的Servlet

Dispatcher的映射逻辑描述

String path = ...
if (path.equals("/hello")) {
    dispatchTo(helloServlet);
} else if (path.equals("/signin")) {
    dispatchTo(signinServlet);
} else {
    // 所有未匹配的路径均转发到"/"
    dispatchTo(indexServlet);
}

所以在浏览器输入http://localhost:8080/abc也会看到IndexServlet生成的页面

HttpServletRequest

HttpServletRequest封装了一个HTTP请求。它从ServletRequest继承而来。从其提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:

HttpServletRequest还有两个方法:setAttribute()getAttribute(),可以给当前HttpServletRequest对象附加多个Key-Value,相当于把HttpServletRequest当作一个Map<String, Object>使用。

HttpServletResponse

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

写入响应前,无需设置setContentLength(),因为底层服务器会根据写入的字节数自动设置

写入响应时,需要通过getOutputStream()获取写入流,或者通过getWriter()获取字符流,二者只能获取其中一个。

写入完毕后,对输出流调用flush()而不是close()方法

  • 必须调用flush(),因为大部分Web服务器都基于HTTP/1.1协议,会复用TCP连接。如果没有调用flush(),将导致缓冲区的内容无法及时发送到客户端。
  • 不能调用close(),因为大部分Web服务器都基于HTTP/1.1协议,如果关闭写入流,TCP将被关闭,导致无法复用

HttpServletRequestHttpServletResponse两个高级接口使得编程时无需直接处理HTTP协议,只需关心接口方法,具体实现类由各服务器提供,从而降低编程难度、增加效率。

Servlet多线程模型

一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求。因此,一个Servlet的doGet()doPost()等处理请求的方法是多线程并发执行的。如果Servlet中定义了字段,要注意多线程并发访问的问题:

public class HelloServlet extends HttpServlet {
    private Map<String, String> map = new ConcurrentHashMap<>();

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 注意读写map字段是多线程并发的:
        this.map.put(key, value);
    }
}

对于每个请求,Web服务器会创建唯一的HttpServletRequestHttpServletResponse实例,因此,HttpServletRequestHttpServletResponse实例只有在当前处理线程中有效,它们总是局部变量,不存在多线程共享的问题。