[关闭]
@cxm-2016 2016-12-23T17:20:56.000000Z 字数 7815 阅读 1557

Java Web (二)——Servlet

Web

版本:1
作者:陈小默
声明:禁止商业,禁止转载


Servlet

Servlet 是 sun公司提供的一种动态 web 资源开发技术。在这里需要明确的两个概念:
- Servlet容器:能够运行Servlet的环境。
- web容器:能够运行web应用的环境。

使用Servlet的一般方式为:使用一个类实现Servlet接口,并将信息配置到web应用的web.xml文件中。

Servlet的类继承结构

Servlet
|->GenericServlet
        |->HttpServlet

Servlet接口与生命周期

Servlet接口中定义了Servlet声明其周期方法。

  1. public void init(ServletConfig config) throws ServletException;
  2. public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
  3. public void destroy();

初始化
init方法是Servlet的初始化声明周期方法,该方法会在第一次被访问时调用。当WEB容器启动时,并不会创建Servlet对象,而是在此Servlet被访问时创建,同时调用其init方法并传入初始化参数。

接收请求
service方法是在每一次访问时调用。同一个Servlet在web容器中只会存在一个实例,所以我们必须保证属性的安全访问,或者不使用全局的非常量属性。当浏览器进行请求时,容器就会将请求信息封装成为一个ServletRequest对象,并且生成一个与之对应的ServletResponse响应对象。

销毁
destroy当前Web应用被移除时会调用此方法。

GenericServlet抽象类

该抽象类实现了Servlet接口,并且在其中新增了一系列获取信息的方法。

HttpServlet

这个类已经是实现了基本功能的类了。这个类对于声明周期方法service进行了基本的封装。可以让请求根据其类型分发到其他具体的方法。其中实现的具体方法有如下:

  • doGet
  • doHead
  • doPost
  • doPut
  • doDelete
  • doOptions
  • doTrace

Servlet映射

为了让网络请求能够访问到目标Servlet,就需要让某些链接地址和具体的Servlet类绑定起来,解决这个问题的方式叫做映射

多映射关联

一个<servlet>可以对应多个<servlet-mapping>,从而可以使得多个访问路径能够访问同一个Servlet对象。

以上一篇的HelloServlet为例,我们在web.xml中增加一个映射:

  1. <servlet-mapping>
  2. <servlet-name>HelloServlet</servlet-name>
  3. <url-pattern>/hello2</url-pattern>
  4. </servlet-mapping>

这样,我们在浏览器输入http://127.0.0.1:8080/smart/hello2http://127.0.0.1:8080/smart/hello都会得到一样的内容。

通配符

当我们需要让一系列符合规则的网络地址指向同一个Servlet时,我们就不能采用多映射的方式进行关联了。而必须采用通配符的形式指定符合规则的地址。

问题:有如下映射关系

  • Servlet1 -> /abc/*
  • Servlet2 -> /*
  • Servlet3 -> /abc
  • Servlet4 -> *.do

当请求url为:

  1. /abc/a.html
  2. /abc
  3. /abc/a.do
  4. /a.do

将会访问哪些Servlet?

接下来,我们使用程序来演示这个过程。

首先,创建Servlet:

  1. class Servlet1 : HttpServlet() {
  2. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  3. val writer = resp.writer
  4. writer.write(this.javaClass.simpleName)
  5. writer.flush()
  6. }
  7. }
  8. class Servlet2 : HttpServlet() {
  9. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  10. val writer = resp.writer
  11. writer.write(this.javaClass.simpleName)
  12. writer.flush()
  13. }
  14. }
  15. class Servlet3 : HttpServlet() {
  16. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  17. val writer = resp.writer
  18. writer.write(this.javaClass.simpleName)
  19. writer.flush()
  20. }
  21. }
  22. class Servlet4 : HttpServlet() {
  23. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  24. val writer = resp.writer
  25. writer.write(this.javaClass.simpleName)
  26. writer.flush()
  27. }
  28. }

接下来,在web.xml中添加映射

  1. <servlet>
  2. <servlet-name>Servlet1</servlet-name>
  3. <servlet-class>com.github.cccxm.smart.Servlet1</servlet-class>
  4. </servlet>
  5. <servlet>
  6. <servlet-name>Servlet2</servlet-name>
  7. <servlet-class>com.github.cccxm.smart.Servlet2</servlet-class>
  8. </servlet>
  9. <servlet>
  10. <servlet-name>Servlet3</servlet-name>
  11. <servlet-class>com.github.cccxm.smart.Servlet3</servlet-class>
  12. </servlet>
  13. <servlet>
  14. <servlet-name>Servlet4</servlet-name>
  15. <servlet-class>com.github.cccxm.smart.Servlet4</servlet-class>
  16. </servlet>
  17. <servlet-mapping>
  18. <servlet-name>Servlet1</servlet-name>
  19. <url-pattern>/abc/*</url-pattern>
  20. </servlet-mapping>
  21. <servlet-mapping>
  22. <servlet-name>Servlet2</servlet-name>
  23. <url-pattern>/*</url-pattern>
  24. </servlet-mapping>
  25. <servlet-mapping>
  26. <servlet-name>Servlet3</servlet-name>
  27. <url-pattern>/abc</url-pattern>
  28. </servlet-mapping>
  29. <servlet-mapping>
  30. <servlet-name>Servlet4</servlet-name>
  31. <url-pattern>*.do</url-pattern>
  32. </servlet-mapping>

运行后看到结果
1. /abc/a.html -> Servlet1
2. /abc -> Servlet3
3. /abc/a.do -> Servlet1
4. /a.do -> Servlet2

Servlet地址匹配标准是:

  1. 精度优先,所以第一题和第三题都匹配了Servlet1,而不是Servlet2
  2. 在精度相同的情况下,也就是有多个匹配存在的情况下,包含.的通配符优先级最低,所以第四题按照精度优先的情况下满足Servlet2和Servlet4,但由于.的优先级最低,所以直接匹配了Servlet4

缺省Servlet

如果一个Servlet的url被指定为/,那么当有一个请求不被其他Servlet所处理时,就会交给这个Servlet处理。

举个栗子:

我们删除上面测试的四个Servlet并删除映射关系。接下来创建一个默认的Servlet

  1. class DefaultServlet : HttpServlet() {
  2. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  3. resp.characterEncoding = "utf-8"
  4. val writer = resp.writer
  5. writer.write("当前为缺省Servlet")
  6. writer.flush()
  7. }
  8. }

创建一个缺省映射

  1. <servlet>
  2. <servlet-name>Default</servlet-name>
  3. <servlet-class>com.github.cccxm.smart.DefaultServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>Default</servlet-name>
  7. <url-pattern>/</url-pattern>
  8. </servlet-mapping>

此时,我们如果访问/hello就是之前的打印hello Servlet的页面,其他任何访问都会跳转到缺省应用。

自启动Servlet

在上面的介绍中我们知道,Servlet会在第一次被访问时创建。可是这样对开发可能造成不便,比如我们需要在web启动的时候就激活一个框架,或者发送一些消息等等。那么有没有一种方式能够让Servlet在web应用启动时就创建而不是被访问的时候呢。

如果我们要让一个Servlet能够随Web应用的启动而启动,可以给Servlet配置<load-on-startup>标签。

现在让HelloServlet实现一个方法

  1. override fun init() {
  2. println("${javaClass.simpleName}启动了")
  3. super.init()
  4. }

然后创建一个新的Servlet,也实现这个方法

  1. class StartupServlet : HttpServlet() {
  2. override fun init() {
  3. println("${javaClass.simpleName}启动了")
  4. super.init()
  5. }
  6. }

然后配置

  1. <servlet>
  2. <servlet-name>Startup</servlet-name>
  3. <servlet-class>com.github.cccxm.smart.StartupServlet</servlet-class>
  4. <load-on-startup>1</load-on-startup>
  5. </servlet>
  6. <servlet-mapping>
  7. <servlet-name>Startup</servlet-name>
  8. <url-pattern>/</url-pattern>
  9. </servlet-mapping>

<load-on-startup>中的数字为启动顺序。

Servlet中的线程安全

我们模拟一个场景

  1. class MessageServlet : HttpServlet() {
  2. lateinit var message: String
  3. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  4. message = req.getParameter("message")
  5. Thread.sleep(5000)
  6. val writer = resp.writer
  7. writer.write(message)
  8. writer.flush()
  9. }
  10. }

当客户端发送一条消息时,服务端将消息暂存在对象中,然后经过一段时间进行其他处理。最后将暂存的数据返回给客户端。

  1. <servlet>
  2. <servlet-name>message</servlet-name>
  3. <servlet-class>com.github.cccxm.smart.MessageServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>message</servlet-name>
  7. <url-pattern>/message</url-pattern>
  8. </servlet-mapping>

接下来,我们在相隔不超过5秒的时间内访问
/message?message=hello
/message?message=world
这时我们会发现,服务端返回的数据永远是后访问的那个。也就是说这里发生了线程安全问题。所以我们需要在必要的地方加锁。但是更建议尽量避免使用全局变量。

ServletConfig

ServletConfig代表当前Servlet在web.xml文件中的配置信息对象。

  1. <servlet>
  2. <servlet-name>getConfig</servlet-name>\
  3. <servlet-class>com.github.cccxm.smart.ConfigServlet</servlet-class>
  4. <init-param>
  5. <param-name>charset</param-name>
  6. <param-value>utf-8</param-value>
  7. </init-param>
  8. <init-param>
  9. <param-name>encoding</param-name>
  10. <param-value>gzip</param-value>
  11. </init-param>
  12. </servlet>
  13. <servlet-mapping>
  14. <servlet-name>getConfig</servlet-name>
  15. <url-pattern>/config</url-pattern>
  16. </servlet-mapping>

比如,我们可以使用上面的方式,指定当前Servlet的初识参数,然后在运行时根据这些初始参数执行相应的操作。

接下来,我们将全部的初始化参数打印出来

  1. class ConfigServlet : HttpServlet() {
  2. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  3. val config = servletConfig
  4. val builder = StringBuilder()
  5. val servletName = config.servletName
  6. builder.append("servletName:$servletName").append("\n")
  7. val names = config.initParameterNames
  8. for (name in names) {
  9. val param = config.getInitParameter(name)
  10. builder.append("$name:$param").append("\n")
  11. }
  12. val writer = resp.writer
  13. writer.write(builder.toString())
  14. writer.flush()
  15. }
  16. }

ServletContext

作为域

该对象可以在整个web应用范围内共享数据

操作域的一般方法为

  1. public void setAttribute(String name, Object object);
  2. public void removeAttribute(String name);
  3. public Object getAttribute(String name);
  4. public Enumeration<String> getAttributeNames();

web应用的初始化参数

  1. public Enumeration<String> getInitParameterNames();
  2. public boolean setInitParameter(String name, String value);
  3. public String getInitParameter(String name);

实现请求转发

在web应用中,如果需要从一个资源跳转到另一个资源,有两种方式,分别是 请求转发和请求重定向。

请求转发:特点是一次请求,一次响应,内部流转。
请求重定向:特点是多次请求,多次相应,外部流转。

  1. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  2. val context = servletContext
  3. context.getRequestDispatcher("/hello").forward(req, resp)
  4. }

加载应用资源

使用context.getRealPath方法可以获取到webapp目录下的文件

  1. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  2. val context = servletContext
  3. val path = context.getRealPath("/WEB-INF/resources/pi.txt")
  4. val file = File(path)
  5. val bytes = file.readBytes()
  6. val output = resp.outputStream
  7. output.write(bytes)
  8. output.flush()
  9. }

对于某些情况,我们可能需要用到的数据并非存储在webapp文件夹下,那么我们可以使用类加载器来加载某些文件。

放在默认的resources文件夹下的资源可以通过context.classLoader.getResource方式得到

  1. override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
  2. val context = servletContext
  3. val path = context.classLoader.getResource("number.txt").path
  4. val file = File(path)
  5. val bytes = file.readBytes()
  6. val output = resp.outputStream
  7. output.write(bytes)
  8. output.flush()
  9. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注