二、Servlet核心
2.1 GenericServlet
2.1.1 GenericServlet 源码
//缺省适配器设计模式
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
private transient ServletConfig config;
@Override
public void destroy() {
// NOOP by default
}
@Override
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
@Override
public ServletConfig getServletConfig() {
return config;
}
@Override
public ServletContext getServletContext() {
return getServletConfig().getServletContext();
}
@Override
public String getServletInfo() {
return "";
}
//模板设计模式
public void init(ServletConfig config) throws ServletException {
this.config = config;
//调用无参的init()方法
this.init();
}
//该无参的init()方法,就是要让子类来重写的
public void init() throws ServletException {
// NOOP by default
}
//抽象化service方法,让子类去实现
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
@Override
public String getServletName() {
return config.getServletName();
}
}
实现了servlet接口,并抽象化了service方法,让子类去实现。
实现了servletconfig接口,子类可以直接调用servletconfig的方法。
2.1.2 获取请求的提交方式
public class genericSevletDemo extends GenericServlet {
private static final long serialVersionUID = 1L;
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
String method = req.getMethod();
if("POSt".equals(method)){
doPost(req,res);
}else if("GET".equals(method)){
doGet(req,res);
}
}
}
继续封装doPost和doGet为HttpServlet类
2.2 HttpServlet
HttpServlet源码
public abstract class HttpServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res){
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
...
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
...
}
}
//servlet继承httpservlet类
public class HttpservDemo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().append("Served at: ").append(request.getContextPath());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
2.3 HttpServletRequest
2.3.1 请求的生命周期
当客户端浏览器将请求发送到服务器后,服务器会根据HTTP请求协议的格式对请求进行解析。同时,服务器会创建HttpServletRequest的实现类RequestFacade的对象,即请求对象。然后再调用相应的set方法,将解析出的数据封装到请示对象中。此时HttpServletRequest实例就创建并初始化完成了。也就是说,请求对象是由服务器创建的的。
当服务器向客户端发送响应结束后,HttpServletRequest实例对象就被服务器销毁。
一次请求对应一个请求对象,别外一次请求对应另一个请求对象,与之前的请求对象没有任何关系。HttpServletRequest实例的生命周期很短暂。
2.3.2 请求参数
HttpServletRequest对于请求中所携带的参数是以Map的形式接收的,并且该Map的key为String,value为String数组(一个请求参数可能会有多个值的情况出现)。
public class HttpservDemo extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//getParameter()方法,获取指定key对应的value,等同于getParameterValues()[0]
String name = request.getParameter("name");
String ageStr = request.getParameter("age");
Integer age = Integer.valueOf(ageStr);
System.out.println("name = "+name);
System.out.println("age = "+age);
//getParameterName()方法,获取所有keys
Enumeration<String> names = request.getParameterNames();
while(names.hasMoreElements()) {
String eleName = names.nextElement();
String eleValue = request.getParameter(eleName);
System.out.println(eleName + "-----" + eleValue);
}
//getParameterValues()方法,获取指定key对应的所有values,
//如<checkbox name = "hobby">running</checkbox>
//如<checkbox name = "hobby">reading</checkbox>
String[] hobby = request.getParameterValues("hobby");
for(String h : hobby) {
System.out.println(h);
}
//getParameterMap()方法,获取存放请求参数的Map
Map<String, String[]> map = request.getParameterMap();
for(String key : map.keySet()) {
System.out.println(key+"-----"+request.getParameter(key));
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
2.3.3 域属性
Request中也存在域属性空间,用于存放有名称的数据。该数据只在当前Request请求中可以访问。
对于Request中的域属性操作的常用方法:
设置
void setAttribute(String name, Object object);
获取
Object getAttribute(String name);
在FirstServlet中设置,跳转到otherServlet中获取
public class FirstServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("address", "zhuhaijinwan");
request.setAttribute("phone", "15011112222");
request.getRequestDispatcher("/other").forward(request, response);
}
}
public class OtherServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String address = (String) request.getAttribute("address");
String phone = (String) request.getAttribute("phone");
Enumeration<String> names = request.getAttributeNames();
while(names.hasMoreElements()) {
String name = names.nextElement();
String value = (String) request.getAttribute(name);
}
}
}
2.3.4 服务器相关信息
1、获取请求的URL,如:“http://localhost:8080/httpServlet/some”
StringBuffer getRequestURL();
2、获取请求的URI,如:”/httpServlet/some”
String getRequestURI();
3、获取当前应用在Web容器中的名称,如:”httpServlet”
String getContext();
4、获取路径中与web.xml中注册servlet时的<url-pattern/>
相匹配的路径信息
//servletPath指的是与<url-pattern>精确部分相匹配的路径
String getServletPath()
//pathInfo指的是与非精确部分相匹配的路径
String getPathInfo()
a、例如:<url-pattern/>
的值为/xxx/ooo/*,用户提交的请求为:http://localhost:8080/httpServlet/xxx/ooo/abc/def。
servletPath就是:/xxx/ooo
pathInfo就是:/abc/def
b、若用户提交的请求为后辍方式,如:http://localhost:8080/httpServlet/aaa/bbb/ccc.do。
servletpath就是:/aaa/bbb/ccc.do
pathInfo: null
c、URI = ServletContext + ServletPath + PathInfo
5、获取请求的方式是post还是get等
String getMethod()
6、获取客户端浏览器的IP
//对于服务器端来说,客户端就是远程端
String getRemoteAddr()
2.4 中文乱码问题
当页面中提交一个包含中文的请求时,在服务器端有可能会出现中文乱码问题。
2.4.1 乱码产生的原因
Http协议中规定,数据传输采用字节码编码方式,即无论浏览器提交的数据包含的中文是什么字符编码格式,一旦由浏览器经过Http传输协议传输,则这些数据均将以字节码的形式上传给服务器。
因为Http协议的底层使用的TCP(Transmission Contrl Protocol)传输协议。传输控制协议是一种面向连接的、可靠的、基于字节流的、端对端的通信协议。在请求中,这些字节均以%开头,并以十六进制形式出现。如%5A%3D等。
那么乱码是如何产生的呢?
当用户通过浏览器提交一个包含 UTF-8编码格式的两个字的中文请求时,浏览器会将这两个中文字符变为六个字节,即形成六个类似%8E的字节表示形式,并将这六个字节上传到tomcat服务器。
tomcat服务器接收到这六个字节后,并不知道它们原来采用的是什么字符编码。而tomcat默认的编码格式为ISO-8859-1。所以会将这个六个字符按照这种格式进行编码,编码后在控制台显示,所以在控制台会显示乱码。
2.4.3 乱码的解决方法
1、tomcat9已经解决了GET提交时的中文乱码问题。
2、request.setCharacterEncoding(“UTF-8”);解决了POST提交中文时的乱码问题。无法解决GET提交时的中文乱码问题。
3、Get提交时中文乱码解决方案有两种:
(1)、修改tomcat/conf/server.xml,添加URIEncoding
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeOut="20000" redirectPort="8443" URIEncoding="UTF-8"
/>
(2)、无论Post还是get都可以
String name = request.getParaeter("name");
byte[] bytes = name.getBytes("ISO8859-1");
name = new String(bytes,"UTF-8");
2.5 HttpServletResponse
web服务器收到一个Http请求后,会针对每个请求创建一个HttpServletRequest对象和HttpServletResponse对象。若需要获取客户端提交请求的相关信息,刚需要HttpServletRequest对象来完成。若需要向客户端发送数据,则需要通过HttpServletResponse对象来完成。
2.5.1 向客户端发送数据
ServletResonse接口有一个方法getWriter(),用于获取到一个输出流对象PrintWriter,该输出流对象是专门用于向客户端浏览器中输出字符数据的,称为标准输出流。
response.getWriter().append("Served at: ").append(request.getContextPath());
PrintWriter out = response.getWriter();
out.append("dddd");
out.print("aaa");
out.pirntln("bbbb");
out.Write("ccc");
2.5.2 响应乱码的产生
两种方法:
1、设置MIME类型,设置字符编码格式
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
2、同时设置MIME类型和字符编码格式
response.setContentType("text/html;charset=UTF-8");
2.6 请求转发与重定向
通过HttpServletRequest获取到的RequestDispatcher对象的forward()方法,可以完成请求转发功能。
而通过HttpServletResponse的sendRedirect()方法,可以完成重定向功能。
2.6.1什么是请求转发与重定向
请求转发:一次请求,一次响应。
重定向:多次请求,多次响应。
2.6.2 请求转发程序举例
public class SomServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String age = req.getParameter("age");
req.setAttribute("ageStr", age);
//请求转发
req.getRequestDispatcher("other").forward(req, resp);
}
}
public class OtherServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//可以继续使用request,response,也可以获取someServlet中设置的域属性
String name = request.getParameter("name");
request.getAttribute("ageStr");
PrintWriter out = response.getWriter();
out.print("this is otherServlet");
}
}
2.6.3 重定向举例
public class SomServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String age = req.getParameter("age");
req.setAttribute("ageStr", age);
req.getRequestDispatcher("other").forward(req, resp);
//重定向
resp.sendRedirect("other");
}
}
public class OtherServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//不能再继续使用request,response,也不能获取someServlet中设置的域属性
PrintWriter out = response.getWriter();
out.print("this is otherServlet");
}
}
2.6.4 重定向时的数据传递
//重定向数据传递,及乱码解决方案
public class SomServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
//解决中文乱码
name = new String(name.getBytes("ISO8859-1"),"UTF-8");
String age = req.getParameter("age");
req.setAttribute("ageStr", age);
req.getRequestDispatcher("other").forward(req, resp);
//编码,解决重定向传递时乱码问题
name = URLEncoder.encode(name,"UTF-8");
//重定向
resp.sendRedirect("other?pname="+name+"&page="+age);
}
}
public class OtherServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String pname = request.getParameter("pname");
//解码
pname = URLDecoder.decode(pname,"UTF-8");
//打散组装
pname = new String(pname.getBytes("ISO8859-1"),"UTF-8");
String page = request.getParameter("page");
System.out.println("pname====" +pname);
System.out.println("page====" +page);
PrintWriter out = response.getWriter();
out.print("this is otherServlet");
}
}
2.6.5 重定向到其它应用
重定向与请求转发还有一点很重要的不同点是,重定向可以跳转到其它应用中,而请求转发只能在当前应用中跳转。也正因为如此,所以sendRedirect()的参数中,必须要添加request.getContext(),即当前应用的根目录,指定要跳转到哪个应用的哪个资源。
//重定向到另一个应用的名称为other的servlet
response.sendRedirect("/redirect-two/other");
2.6.6 请求转发与重定向对比
(1)、请求转发
A、浏览器只发出一次请求,收到一次响应
B、请求所转发到的资源中可以直接获取到请求中所携带的数据
C、浏览器地址栏显示的是用户所提交的路径
D、只能跳转到当前应用中的资源
(2)、
A、浏览器发出两次请求,接收到两次响应
B、重定向到的资源不能直接获取到用户提交请求中所携带的数据
C、浏览器地址栏显示的是重定向的请求路径,而非用户提交请求的路径。也正因为如此,重定向有一个很重要的作用:防止表单重复提交
D、重定向不仅可以跳转到当前应用的其它资源,也可以跳转到其它应用中的资源
2.6.7 请求转发与重定向的选择
(1)、若需跳转到其它应用,只能选择重定向
(2)、若是处理表单数据的servlet,要跳转到其它servlet,则需要选择重定向。为了防止表单重复提交。
(3)、若对某一请求进行处理的servlet的执行需要消耗大量的服务器资源(cpu,内存),此时这个servlet执行完毕后,也需要重定向。
(4)、其它情况,一般使用请求转发。
2.7 RequestDispatcher
RequestDispatcher是javax.servlet包下的一个接口,通过HttpServletRequest可以获取到其接口对象,该对象就是用于完成转发功能的。
2.7.1 forward()与include()
SomeServlet
public class SomServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("some request = " + req);
System.out.println("some resp = " + resp);
req.getRequestDispatcher("other").forward(req, resp);
}
}
OtherServlet
public class OtherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("other response = " + response);
System.out.println("other request = " + request);
}
}
输出结果中的request:
some request = org.apache.catalina.connector.RequestFacade@289113c7
other request = org.apache.catalina.core.ApplicationHttpRequest@4d0d88e6
1、other中的HttpServletRequest request是对some中的HttpServletRequest req的增强,实际上是将用户请求和转发请求这两次请求进行了合并。
2、将req.getRequestDispatcher(“other”).forward(req, resp);改为req.getRequestDispatcher(“other”).include(req, resp);后,输出结果和上面的相同。说明对于request,forward与include没有区别。
输出结果中的response:
//forward()方法跳转时
some resp = org.apache.catalina.connector.ResponseFacade@6451e20e
other response = org.apache.catalina.connector.ResponseFacade@6451e20e
//include()方法跳转时
some resp = org.apache.catalina.connector.ResponseFacade@48202e11
other response = org.apache.catalina.core.ApplicationHttpResponse@5f53ab27
1、forward()方法跳转时,some中的resp与other中的response一样,都是ResponseFacade。
2、include()方法跳转时,some中的resp为ResponseFacade,other中的response为ApplicationHttpResponse。
3、说明requestDispatcher的forward()与include()的区别,主要集中在响应对象上。
2.7.2 代码测试
SomeServlet
public class SomServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter out = resp.getWriter();
out.print("someServlet:forward() before ");
req.getRequestDispatcher("other").forward(req, resp);
out.print("someServlet:forward() after ");
}
}
OtherServlet
public class OtherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter out = response.getWriter();
out.print("other servlet data");
}
}
客户端浏览器输出结果:
forward()方法跳转的输出结果:
other servlet data
incluce()方法跳转的输出结果:
someServlet:forward() before
other servlet data
someServlet:forward() after
1、forward()与include()的区别,主要表现在标准输出流的开启时间不同。
2、说明在SomeServlet中,用forward()方法向前跳转时,请求此时还并没有结束,所以reqp还没有开启,因此两个输出语句没有执行。
3、include()方法跳转时,some和other中的输出语句全部执行。说明,include不仅将other的数据写入到了标准输出流中,也将some中的数据包含到了自己的输出流中,
4、forward的转发示意:
浏览器—>发送请求—>服务器中的SomeServlet—->发送请求—->服务器中的OtherServlet—>OtherServlet对浏览器做出响应。
向浏览器给出响应的是OtherServlet,输出流是在OtherServlet中开启的。
5、include的转发示意:
浏览器—>发送请求—>服务器中的SomeServlet—->发送请求—>服务器中的OtherServlet—->OtherServlet将SomeServlet进行了包含,并向SomeServlet做出响应 —> SomeServle对浏览器做出响应。
向浏览器给出响应的是SomeServlet,输出流在SomeServlet中就开启了。
包含的过程就是由ApplicationHttpResponse这个增强的response来完成的。
2.9 Servlet的线程安全问题
Servlet是单例多线程环境下运行的,其运行可能会产生线程安全问题。
2.9.1 线程安全问题
1、同时满足以下两个条件,则会出现线程安全问题。
(1)、存在多线程并发访问
(2)、存在可修改的共享数据
2、JVM中可能存在线程安全问题的数据分析
(1)、栈内存数据分析
栈内存是是多例的,即JVM会为每个线程创建一个栈,所以其中的数据是不共享的。另外,栈里面放的是方法栈帧,一个方法在栈里面,以一个栈帧的形式出现,方法的栈帧中放的是方法的签名(头部信息)、局部变量、返回的信息等,方法中的局部变量存放在Stack的栈帧中,方法执行完毕,栈帧弹栈,局部变量消失。局部变量是局部的,不是共享的。所以栈内存中的数据不存在线程安全问题。
(2)、堆内存数据分析
一个JVM中只存放一个堆内存,堆内存是共享的。被创建出的对象存放在堆内存中,而存放在堆内存中的对象,实际就是对象成员变量的集合。即成员变量是存放在堆内存的。堆内存中的数据是多线程共享的,也就是说,堆内存中的数据是存在线程安全隐患的。
(3)、方法区数据分析
一个JVM中只存在一个方法区。方法的代码片段,静态变量与常量都存放在方法区,方法区是多线程共享的。常量是不能修改的,所以常量不存在安全问题。静态变量是多线程共享的,所以静态变量存在安全隐患。
3、线程安全问题的解决方案
(1)、对于一般性的类,不要定义为单例的。除非项目有特殊需求,或该类对象属于重量级对象。所谓重量级对象是指,创建该类对象是会占用较大的系统资源。
(2)、无论类是否为单例类,尽量不要使用静态变量。
(3)、若需要定义为单例类,则单例类尽量不使用成员变量(例如Servlet类)。
(4)、若单例类中必须要使用成员变量,则对成员变量的操作添加串行化锁synchronized,实现线程同步。不过,最好不要使用线程同步机制,因为一旦操作进入串行化的排队状态,将大大降低程序的执行效率。
2.9.2 Servlet的线程安全问题
Servlet是单例多线程并发访问的,所以存在线程安全问题。
为了避免问题产生,对用Servlet类的使用,一般是不声明成员变量的。
若项目中必须要求声明成员变量,则只能通过同步机制synchronize避免。
2.9.3 对线程安全问题的合理利用
Servlet中的成员变量是每一个线程均可修改和访问的,所以可以利用这一点,实现计数器功能,用于统计当前Servlet的被访问的次数。
public class SomeServlet extends HttpServle{
//定义成员变量 - 计数器
private int count;
public void doGet(...){
count++;
response.setContentType("text/html;charset=UTF-8");
PrinterWriter out = resp.getWriter();
out.print("当前网页的访问次数:" + count);
}
}