阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Servlet进阶

29次阅读
没有评论

共计 4349 个字符,预计需要花费 11 分钟才能阅读完成。

一个 Web App 就是由一个或多个 Servlet 组成的,每个 Servlet 通过注解说明自己能处理的路径。例如:

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

上述 HelloServlet 能处理 /hello 这个路径的请求。

提示

早期的 Servlet 需要在 web.xml 中配置映射路径,但最新 Servlet 版本只需要通过注解就可以完成映射。

因为浏览器发送请求的时候,还会有请求方法(HTTP Method):即 GETPOSTPUT 等不同类型的请求。因此,要处理 GET 请求,我们要覆写 doGet() 方法:

@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {@Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {...}
}

类似的,要处理 POST 请求,就需要覆写 doPost() 方法。

如果没有覆写 doPost() 方法,那么 HelloServlet 能不能处理 POST /hello 请求呢?

我们查看一下 HttpServletdoPost()方法就一目了然了:它会直接返回 405 或 400 错误。因此,一个 Servlet 如果映射到/hello,那么所有请求方法都会由这个 Servlet 处理,至于能不能返回 200 成功响应,要看有没有覆写对应的请求方法。

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

@WebServlet(urlPatterns = "/hello")
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:

               ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐

               │            /hello    ┌───────────────┐│
                          ┌──────────▶│ HelloServlet  │
               │          │           └───────────────┘│
┌───────┐    ┌──────────┐ │ /signin   ┌───────────────┐
│Browser│───▶│Dispatcher│─┼──────────▶│ SignInServlet ││
└───────┘    └──────────┘ │           └───────────────┘
               │          │ /         ┌───────────────┐│
                          └──────────▶│ IndexServlet  │
               │                      └───────────────┘│
                              Web Server
               └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘

这种根据路径转发的功能我们一般称为 dispatch。映射到 /IndexServlet比较特殊,它实际上会接收所有未匹配的路径,相当于/*,因为 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 继承而来。最早设计 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<String, Object> 使用。

调用 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()方法!

有了 HttpServletRequestHttpServletResponse这两个高级接口,我们就不需要直接处理 HTTP 协议。注意到具体的实现类是由各服务器提供的,而我们编写的 Web 应用程序只关心接口方法,并不需要关心具体实现的子类。

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 实例只有在当前处理线程中有效,它们总是局部变量,不存在多线程共享的问题。

小结

一个 Webapp 中的多个 Servlet 依靠路径映射来处理不同的请求;

映射为 / 的 Servlet 可处理所有“未匹配”的请求;

如何处理请求取决于 Servlet 覆写的对应方法;

Web 服务器通过多线程处理 HTTP 请求,一个 Servlet 的处理方法可以由多线程并发执行。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计4349字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中