Servlet 详解
1、什么是 Servlet?
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。使用 Servlet,可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。
2、Servlet 入门实例
第一步:创建一个JavaWeb项目,并创建一个servlet类-----HelloServlet,实现接口 Servlet
package com.ys.servlet; import java.io.IOException; import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloServlet implements Servlet{ //只被调用一次,第一次请求Servlet时,创建Servlet的实例,调用构造器 public HelloServlet() { System.out.println("构造器 HelloServelt()..."); } //该方法用于初始化Servlet,就是把该Servlet装载入内存 //只被调用一次,在创建好实例后立即被调用 @Override public void init(ServletConfig config) throws ServletException { System.out.println("初始化方法 init()..."); } //被多次调用,每次请求都会调用service方法。实际用于响应请求的 @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { System.out.println("执行方法主体 service()..."); } //只被调用一次,在当前Servlet所在的WEB应用被卸载前调用,用于释放当前Servlet所占用的资源 @Override public void destroy() { System.out.println("servlet 销毁时调用方法 destroy()..."); } @Override public ServletConfig getServletConfig() { return null; } @Override public String getServletInfo() { return null; } }
第二步:在 web.xml 文件中配置上面创建的 HelloServlet 映射关系
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <!--在tomcat 服务器中运行时,如果不指名访问文件名,默认的根据项目名访问文件顺序如下配置 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <!--给创建的 Servlet 配置映射关系 --> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.ys.servlet.HelloServlet</servlet-class> <!--servlet的完整名称--> </servlet> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <!-- 与上面配置的 servlet-name 名字要对应,一个servlet可以有多个 servlet-mapping --> <url-pattern>/hello</url-pattern> <!--访问路径--> </servlet-mapping> </web-app>
第三步:将项目部署在 tomcat 服务器,如何部署请看这篇文章:http://www.cnblogs.com/ysocean/p/6893446.html,然后启动服务器
这里我们项目的结构为:
①、我们直接通过项目名来访问,由于我们在 web.xml 文件中配置了 <welcome-file-list>,那么会依次找下面配置的文件,我们只创建了一个 index.jsp,那么就会访问这个JSP 文件
②、通过在 web.xml 文件中配置的<url-pattern>/hello</url-pattern> 来访问
我们可以看控制台打印内容如下:
如果我们不断的刷新 http://localhost:8080/ServletImprove/hello 这个访问链接,那么控制台如下:
3、Servlet 的生命周期
我们通过上面的实例,可以看到也就是只有第一次才会执行 构造器和 init() 方法,后面每次点击都只调用 service() 方法。那这是为什么呢?
上面这幅图可以这样理解:
1、客户端向 Web 服务器发送请求,服务器查询 web.xml 文件配置。根据请求信息找到对应的 Servlet。
2、Servlet 引擎检查是否已经装载并创建了该 Servlet 的实例对象,如果有,则直接执行第4步,否则执行第3步,
3、Web 服务器加载 Servlet,并调用 Servlet 构造器(只会调用一次),创建 Servlet 的实例对象。并调用 init() 方法,完成 Servlet 实例对象的初始化(只会调用一次)。
4、Web 服务器把接收到的 http 请求封装成 ServletRequest 对象,并创建一个 响应消息的 ServletResponse 对象,作为 service() 方法的参数传入。(每一次访问都会调用一次该方法)
5、执行 service() 方法,并将处理信息封装到 ServletResponse 对象中返回
6、浏览器拆除 ServletResponse 对象,形成 http 响应格式,返回给客户端。
7、Web 应用程序停止或者重新启动之前,Servlet 引擎将卸载 Servlet实例,并在卸载之前调用 destory() 方法
4、创建 Servlet 的三种方法
第一种:就是我们上面写的 实现接口 Servlet
第二种:由于实现接口我们需要实现里面所有的方法,里面有一些方法我们可能并不想实现,那么我们就继承 GenericServlet 类
package com.ys.servlet; import java.io.IOException; import javax.servlet.GenericServlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloServlet extends GenericServlet{ //只被调用一次,第一次请求Servlet时,创建Servlet的实例,调用构造器 public HelloServlet() { System.out.println("构造器 HelloServelt()..."); } //该方法用于初始化Servlet,就是把该Servlet装载入内存 //只被调用一次,在创建好实例后立即被调用 @Override public void init(ServletConfig config) throws ServletException { System.out.println("初始化方法 init()..."); } //被多次调用,每次请求都会调用service方法。实际用于响应请求的 @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { System.out.println("执行方法主体 service()..."); } //只被调用一次,在当前Servlet所在的WEB应用被卸载前调用,用于释放当前Servlet所占用的资源 @Override public void destroy() { System.out.println("servlet 销毁时调用方法 destroy()..."); } }
第三种:通常我们浏览器发出的请求都是 http 请求,那么请求方式可能有多种,比如 get,post,而我们在处理请求的时候都是在 service() 方法中,这种方式显然不够明确。那么我们通常是 继承 HttpServlet 类
package com.ys.servlet; import java.io.IOException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class HelloServlet extends HttpServlet{ //只被调用一次,第一次请求Servlet时,创建Servlet的实例,调用构造器 public HelloServlet() { System.out.println("构造器 HelloServelt()..."); } //该方法用于初始化Servlet,就是把该Servlet装载入内存 //只被调用一次,在创建好实例后立即被调用 @Override public void init(ServletConfig config) throws ServletException { System.out.println("初始化方法 init()..."); } //处理 post 请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } //处理get请求 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { } //只被调用一次,在当前Servlet所在的WEB应用被卸载前调用,用于释放当前Servlet所占用的资源 @Override public void destroy() { System.out.println("servlet 销毁时调用方法 destroy()..."); } }
其实上面三种方法,后面两种都是对 Servlet 类的封装,我们可以看 API,其实 HttpServlet 是继承 GenericServlet的。
而 GenericServlet 又是实现 Servlet 接口的
5、Servlet 的多线程问题
我们通过 Servlet 的生命周期可以知道,Servlet 类的构造器只会在第一次访问的时候调用,后面的请求都不会再重新创建 Servlet 实例。即 Servlet 是单例,那么既然是单例的,那就要注意多线程访问所造成的安全问题。如下:
package com.ys.servlet; import java.io.IOException; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloServlet extends GenericServlet{ //多线程共享资源 private int i = 0; public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { i++; //为了使多线程访问安全问题更加突出,我们增加一个延时程序 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } }
我们用两个浏览器,输入 http://localhost:8080/ServletImprove/hello,然后一起访问,不断刷新,结果如下:
结果分析:显然,我们用两个浏览器访问,便相当于两个线程,第一个访问,已经执行了 i++,但是还没来得及打印 i 的值,就马上就睡眠了;接着第二个浏览也来访问,执行 i++,那么i的值相当于增加加了两次1,然后这两个浏览器输出最终结果。这便造成了多线程访问共享资源造成冲突。那么如何解决多线程冲突呢?
可以参考这篇文章:如何解决多线程同步问题 http://www.cnblogs.com/ysocean/p/6883729.html
那么在 Servlet 中如何处理呢?
第一种方法:使用同步代码块
package com.ys.servlet; import java.io.IOException; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloServlet extends GenericServlet{ //多线程共享资源 private int i = 0; public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { synchronized (this) { i++; //为了使多线程访问安全问题更加突出,我们增加一个延时程序 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } } }
结果:
分析:这种办法虽然能解决多线程同步问题,但是如果 延时程序特别长,那么会造成访问假死的现象。即第一个线程访问结果没有出来,第二个线程就会一直卡死,出不来结果
第二种办法:实现接口 SingleThreadModel
package com.ys.servlet; import java.io.IOException; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.SingleThreadModel; public class HelloServlet extends GenericServlet implements SingleThreadModel{ //多线程共享资源 private int i = 0; public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { i++; //为了使多线程访问安全问题更加突出,我们增加一个延时程序 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } }
结果:
分析:SingleThreadModel 接口指定了系统如何处理对同一个Servlet的调用。如果一个Servlet被这个接口指定,那么在这个Servlet中的service方法将不会有两个线程被同时执行,当然也就不存在线程安全的问题。但是,如果一个Servlet实现了SingleThreadModel接口,Servlet引擎将为每个新的请求创建一个单独的Servlet实例,这将引起大量的系统开销,在现在的Servlet开发中基本看不到SingleThreadModel的使用,这种方式了解即可,尽量避免使用。
第三种办法:避免使用实例变量
线程安全问题很大一部分是由于实例变量造成的,那么我们只要在 Servlet 里面不定义任何的实例变量,那么就不会有线程安全的问题。因为在 Java 内存模型中,方法中的临时变量是在栈上分配空间,而且每个线程都有自己的私有栈空间,不会造成线程安全问题。
package com.ys.servlet; import java.io.IOException; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloServlet extends GenericServlet{ public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { int i = 0; i++; //为了使多线程访问安全问题更加突出,我们增加一个延时程序 try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(i); } }
结果:
6、Servlet 和 JSP 的区别
①、JSP 的本质就是 Servlet,JSP 经过编译后就会变为一个类似 Servlet 的Java文件
②、Servlet 基本是JAVA程序代码构成,擅长于流程控制和事务处理,当然也可以用来生成html代码,但是通过Servlet来生成动态网页很不直观.
③、JSP由HTML代码和JSP标签构成,可以方便地编写动态网页,当然里面也可以编写 Java代码,但是整体看上去不够优雅。而且比较麻烦
所以:JSP侧重于视图,Servlet主要用于控制逻辑。
我们可以看一个 JSP 文件,index.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Insert title here</title> </head> <body> index.jsp </body> </html>
经过编译后:很显然下面的代码结构和 Servlet 是差不多的
package org.apache.jsp; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase implements org.apache.jasper.runtime.JspSourceDependent { private static final javax.servlet.jsp.JspFactory _jspxFactory = javax.servlet.jsp.JspFactory.getDefaultFactory(); private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants; private javax.el.ExpressionFactory _el_expressionfactory; private org.apache.tomcat.InstanceManager _jsp_instancemanager; public java.util.Map<java.lang.String,java.lang.Long> getDependants() { return _jspx_dependants; } public void _jspInit() { _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory(); _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig()); } public void _jspDestroy() { } public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response) throws java.io.IOException, javax.servlet.ServletException { final javax.servlet.jsp.PageContext pageContext; javax.servlet.http.HttpSession session = null; final javax.servlet.ServletContext application; final javax.servlet.ServletConfig config; javax.servlet.jsp.JspWriter out = null; final java.lang.Object page = this; javax.servlet.jsp.JspWriter _jspx_out = null; javax.servlet.jsp.PageContext _jspx_page_context = null; try { response.setContentType("text/html; charset=ISO-8859-1"); pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true); _jspx_page_context = pageContext; application = pageContext.getServletContext(); config = pageContext.getServletConfig(); session = pageContext.getSession(); out = pageContext.getOut(); _jspx_out = out; out.write("\r\n"); out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\r\n"); out.write("<html>\r\n"); out.write("<head>\r\n"); out.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=ISO-8859-1\">\r\n"); out.write("<title>Insert title here</title>\r\n"); out.write("</head>\r\n"); out.write("<body>\r\n"); out.write("\tindex.jsp\r\n"); out.write("</body>\r\n"); out.write("</html>"); } catch (java.lang.Throwable t) { if (!(t instanceof javax.servlet.jsp.SkipPageException)){ out = _jspx_out; if (out != null && out.getBufferSize() != 0) try { out.clearBuffer(); } catch (java.io.IOException e) {} if (_jspx_page_context != null) _jspx_page_context.handlePageException(t); else throw new ServletException(t); } } finally { _jspxFactory.releasePageContext(_jspx_page_context); } } }
JSP 页面的九个隐含对象:
①、request:HttpServletRequest的一个对象,封装请求信息
②、pageContext:页面的上下文,是PageContext的一个对象,可以从该对象中获取其它8个隐含对象。
③、session:代表浏览器和服务器的一次会话,是HttpSession 的一个对象
④、application:代表当前WEB应用,是ServletContext对象
⑤、config:当前JSP对应Servlet的ServletConfig对象
⑥、out:JspWriter对象,调用out.prinln()可以直接把字符串打印到浏览器上
⑦、page:指向当前JSP对应的Servlet对象的应用,但为Object类型,只能调用 Object 类的方法
⑧、exception:在声明了page指令的isErrorPage="true"时,才可以使用
7、Servlet 的转发和重定向
重定向:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; response.sendRedirect("index.jsp");//重定向 }
转发:
HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //response.sendRedirect("index.jsp"); request.getRequestDispatcher("/index.jsp").forward(request, response);//转发
我们再看看浏览器访问:同时输入 http://localhost:8080/ServletImprove/hello
重定向变为:
转发为:
本质区别:转发只发出了一次请求,而重定向发出了两次请求
①.转发:地址栏是初次发出请求的地址
重定向:地址栏不再是初次发出的请求地址,地址栏为最后响应的那个地址
②.转发:在最终的Servlet中,request对象和中转的那个request是同一个对象
重定向:在最终的Servlet中,request对象和中转的那个request不是同一个对象
③.转发:只能转发给当前WEB应用的资源
重定向:可以重定向到任何资源
response.sendRedirect("http://www.baidu.com");是可以的
转发就不行
④.转发:/ 代表的是当前WEB应用的根目录(http://localhost:8080/项目名称/)
重定向: / 代表的是当前WEB站点的根目录(http://localhost:8080/)
注意:这两条跳转语句不能同时出现在一个页面中,否则会报IllegalStateException - if the response was already committed
8、Servlet 的过滤器
①、什么是 过滤器?
JavaWEB 的一个重要组件,可以对发送到 Servlet 的请求进行拦截,并对响应也进行拦截
②、如何实现一个过滤器?
第一步:创建一个过滤器类,实现 Filter 接口
package com.ys.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class HelloFilter implements Filter{ public HelloFilter() { System.out.println("构造器 HelloFilter()..."); } @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init()..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("doFilter()..."); } @Override public void destroy() { System.out.println("destroy()..."); } }
第二步:在 web.xml 文件中配置过滤器
<!--给创建的过滤器配置关系 --> <filter> <filter-name>helloFilter</filter-name> <filter-class>com.ys.filter.HelloFilter</filter-class> </filter> <filter-mapping> <filter-name>helloFilter</filter-name> <url-pattern>/*</url-pattern><!-- 这表示可以拦截任何请求 --> </filter-mapping>
启动服务器:我们发现还没发送请求,过滤器的 构造方法和 init() 方法就已经开始运行了
服务器启动成功之后,我们输入任意连接,比如
每刷新一次,控制台都会打印 doFilter()...
总结:生命周期和 Servlet 的类似。只不过其构造方法和初始化方法是在容器启动时就调用了,而其 doFilter() 方法则是在每次请求的时候调用。故过滤器可以对请求进行拦截过滤。可以用来进行权限设置,对传输数据进行加密等等操作。