@cxm-2016
2016-12-23T09:20:56.000000Z
字数 7815
阅读 2061
Web
版本:1
作者:陈小默
声明:禁止商业,禁止转载
Servlet 是 sun公司提供的一种动态 web 资源开发技术。在这里需要明确的两个概念:
- Servlet容器:能够运行Servlet的环境。
- web容器:能够运行web应用的环境。
使用Servlet的一般方式为:使用一个类实现Servlet接口,并将信息配置到web应用的web.xml文件中。
Servlet的类继承结构
Servlet
|->GenericServlet
|->HttpServlet
Servlet接口中定义了Servlet声明其周期方法。
public void init(ServletConfig config) throws ServletException;public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;public void destroy();
初始化
init方法是Servlet的初始化声明周期方法,该方法会在第一次被访问时调用。当WEB容器启动时,并不会创建Servlet对象,而是在此Servlet被访问时创建,同时调用其init方法并传入初始化参数。
接收请求
service方法是在每一次访问时调用。同一个Servlet在web容器中只会存在一个实例,所以我们必须保证属性的安全访问,或者不使用全局的非常量属性。当浏览器进行请求时,容器就会将请求信息封装成为一个ServletRequest对象,并且生成一个与之对应的ServletResponse响应对象。
销毁
destroy当前Web应用被移除时会调用此方法。
该抽象类实现了Servlet接口,并且在其中新增了一系列获取信息的方法。
这个类已经是实现了基本功能的类了。这个类对于声明周期方法service进行了基本的封装。可以让请求根据其类型分发到其他具体的方法。其中实现的具体方法有如下:
- doGet
- doHead
- doPost
- doPut
- doDelete
- doOptions
- doTrace
为了让网络请求能够访问到目标Servlet,就需要让某些链接地址和具体的Servlet类绑定起来,解决这个问题的方式叫做映射。
一个<servlet>可以对应多个<servlet-mapping>,从而可以使得多个访问路径能够访问同一个Servlet对象。
以上一篇的HelloServlet为例,我们在web.xml中增加一个映射:
<servlet-mapping><servlet-name>HelloServlet</servlet-name><url-pattern>/hello2</url-pattern></servlet-mapping>
这样,我们在浏览器输入http://127.0.0.1:8080/smart/hello2和http://127.0.0.1:8080/smart/hello都会得到一样的内容。
当我们需要让一系列符合规则的网络地址指向同一个Servlet时,我们就不能采用多映射的方式进行关联了。而必须采用通配符的形式指定符合规则的地址。
问题:有如下映射关系
- Servlet1 -> /abc/*
- Servlet2 -> /*
- Servlet3 -> /abc
- Servlet4 -> *.do
当请求url为:
/abc/a.html/abc/abc/a.do/a.do将会访问哪些Servlet?
接下来,我们使用程序来演示这个过程。
首先,创建Servlet:
class Servlet1 : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val writer = resp.writerwriter.write(this.javaClass.simpleName)writer.flush()}}class Servlet2 : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val writer = resp.writerwriter.write(this.javaClass.simpleName)writer.flush()}}class Servlet3 : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val writer = resp.writerwriter.write(this.javaClass.simpleName)writer.flush()}}class Servlet4 : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val writer = resp.writerwriter.write(this.javaClass.simpleName)writer.flush()}}
接下来,在web.xml中添加映射
<servlet><servlet-name>Servlet1</servlet-name><servlet-class>com.github.cccxm.smart.Servlet1</servlet-class></servlet><servlet><servlet-name>Servlet2</servlet-name><servlet-class>com.github.cccxm.smart.Servlet2</servlet-class></servlet><servlet><servlet-name>Servlet3</servlet-name><servlet-class>com.github.cccxm.smart.Servlet3</servlet-class></servlet><servlet><servlet-name>Servlet4</servlet-name><servlet-class>com.github.cccxm.smart.Servlet4</servlet-class></servlet><servlet-mapping><servlet-name>Servlet1</servlet-name><url-pattern>/abc/*</url-pattern></servlet-mapping><servlet-mapping><servlet-name>Servlet2</servlet-name><url-pattern>/*</url-pattern></servlet-mapping><servlet-mapping><servlet-name>Servlet3</servlet-name><url-pattern>/abc</url-pattern></servlet-mapping><servlet-mapping><servlet-name>Servlet4</servlet-name><url-pattern>*.do</url-pattern></servlet-mapping>
运行后看到结果
1. /abc/a.html -> Servlet1
2. /abc -> Servlet3
3. /abc/a.do -> Servlet1
4. /a.do -> Servlet2
Servlet地址匹配标准是:
.的通配符优先级最低,所以第四题按照精度优先的情况下满足Servlet2和Servlet4,但由于.的优先级最低,所以直接匹配了Servlet4如果一个Servlet的url被指定为/,那么当有一个请求不被其他Servlet所处理时,就会交给这个Servlet处理。
举个栗子:
我们删除上面测试的四个Servlet并删除映射关系。接下来创建一个默认的Servlet
class DefaultServlet : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {resp.characterEncoding = "utf-8"val writer = resp.writerwriter.write("当前为缺省Servlet")writer.flush()}}
创建一个缺省映射
<servlet><servlet-name>Default</servlet-name><servlet-class>com.github.cccxm.smart.DefaultServlet</servlet-class></servlet><servlet-mapping><servlet-name>Default</servlet-name><url-pattern>/</url-pattern></servlet-mapping>
此时,我们如果访问/hello就是之前的打印hello Servlet的页面,其他任何访问都会跳转到缺省应用。
在上面的介绍中我们知道,Servlet会在第一次被访问时创建。可是这样对开发可能造成不便,比如我们需要在web启动的时候就激活一个框架,或者发送一些消息等等。那么有没有一种方式能够让Servlet在web应用启动时就创建而不是被访问的时候呢。
如果我们要让一个Servlet能够随Web应用的启动而启动,可以给Servlet配置<load-on-startup>标签。
现在让HelloServlet实现一个方法
override fun init() {println("${javaClass.simpleName}启动了")super.init()}
然后创建一个新的Servlet,也实现这个方法
class StartupServlet : HttpServlet() {override fun init() {println("${javaClass.simpleName}启动了")super.init()}}
然后配置
<servlet><servlet-name>Startup</servlet-name><servlet-class>com.github.cccxm.smart.StartupServlet</servlet-class><load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>Startup</servlet-name><url-pattern>/</url-pattern></servlet-mapping>
<load-on-startup>中的数字为启动顺序。
我们模拟一个场景
class MessageServlet : HttpServlet() {lateinit var message: Stringoverride fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {message = req.getParameter("message")Thread.sleep(5000)val writer = resp.writerwriter.write(message)writer.flush()}}
当客户端发送一条消息时,服务端将消息暂存在对象中,然后经过一段时间进行其他处理。最后将暂存的数据返回给客户端。
<servlet><servlet-name>message</servlet-name><servlet-class>com.github.cccxm.smart.MessageServlet</servlet-class></servlet><servlet-mapping><servlet-name>message</servlet-name><url-pattern>/message</url-pattern></servlet-mapping>
接下来,我们在相隔不超过5秒的时间内访问
/message?message=hello
/message?message=world
这时我们会发现,服务端返回的数据永远是后访问的那个。也就是说这里发生了线程安全问题。所以我们需要在必要的地方加锁。但是更建议尽量避免使用全局变量。
ServletConfig代表当前Servlet在web.xml文件中的配置信息对象。
<servlet><servlet-name>getConfig</servlet-name>\<servlet-class>com.github.cccxm.smart.ConfigServlet</servlet-class><init-param><param-name>charset</param-name><param-value>utf-8</param-value></init-param><init-param><param-name>encoding</param-name><param-value>gzip</param-value></init-param></servlet><servlet-mapping><servlet-name>getConfig</servlet-name><url-pattern>/config</url-pattern></servlet-mapping>
比如,我们可以使用上面的方式,指定当前Servlet的初识参数,然后在运行时根据这些初始参数执行相应的操作。
接下来,我们将全部的初始化参数打印出来
class ConfigServlet : HttpServlet() {override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val config = servletConfigval builder = StringBuilder()val servletName = config.servletNamebuilder.append("servletName:$servletName").append("\n")val names = config.initParameterNamesfor (name in names) {val param = config.getInitParameter(name)builder.append("$name:$param").append("\n")}val writer = resp.writerwriter.write(builder.toString())writer.flush()}}
该对象可以在整个web应用范围内共享数据
操作域的一般方法为
public void setAttribute(String name, Object object);public void removeAttribute(String name);public Object getAttribute(String name);public Enumeration<String> getAttributeNames();
public Enumeration<String> getInitParameterNames();public boolean setInitParameter(String name, String value);public String getInitParameter(String name);
在web应用中,如果需要从一个资源跳转到另一个资源,有两种方式,分别是 请求转发和请求重定向。
请求转发:特点是一次请求,一次响应,内部流转。
请求重定向:特点是多次请求,多次相应,外部流转。
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val context = servletContextcontext.getRequestDispatcher("/hello").forward(req, resp)}
使用context.getRealPath方法可以获取到webapp目录下的文件
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val context = servletContextval path = context.getRealPath("/WEB-INF/resources/pi.txt")val file = File(path)val bytes = file.readBytes()val output = resp.outputStreamoutput.write(bytes)output.flush()}
对于某些情况,我们可能需要用到的数据并非存储在webapp文件夹下,那么我们可以使用类加载器来加载某些文件。
放在默认的resources文件夹下的资源可以通过context.classLoader.getResource方式得到
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {val context = servletContextval path = context.classLoader.getResource("number.txt").pathval file = File(path)val bytes = file.readBytes()val output = resp.outputStreamoutput.write(bytes)output.flush()}