@cxm-2016
2016-12-23T17:20:56.000000Z
字数 7815
阅读 1584
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.writer
writer.write(this.javaClass.simpleName)
writer.flush()
}
}
class Servlet2 : HttpServlet() {
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val writer = resp.writer
writer.write(this.javaClass.simpleName)
writer.flush()
}
}
class Servlet3 : HttpServlet() {
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val writer = resp.writer
writer.write(this.javaClass.simpleName)
writer.flush()
}
}
class Servlet4 : HttpServlet() {
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val writer = resp.writer
writer.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.writer
writer.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: String
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
message = req.getParameter("message")
Thread.sleep(5000)
val writer = resp.writer
writer.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 = servletConfig
val builder = StringBuilder()
val servletName = config.servletName
builder.append("servletName:$servletName").append("\n")
val names = config.initParameterNames
for (name in names) {
val param = config.getInitParameter(name)
builder.append("$name:$param").append("\n")
}
val writer = resp.writer
writer.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 = servletContext
context.getRequestDispatcher("/hello").forward(req, resp)
}
使用context.getRealPath
方法可以获取到webapp目录下的文件
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val context = servletContext
val path = context.getRealPath("/WEB-INF/resources/pi.txt")
val file = File(path)
val bytes = file.readBytes()
val output = resp.outputStream
output.write(bytes)
output.flush()
}
对于某些情况,我们可能需要用到的数据并非存储在webapp文件夹下,那么我们可以使用类加载器来加载某些文件。
放在默认的resources文件夹下的资源可以通过context.classLoader.getResource
方式得到
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val context = servletContext
val path = context.classLoader.getResource("number.txt").path
val file = File(path)
val bytes = file.readBytes()
val output = resp.outputStream
output.write(bytes)
output.flush()
}