http://localhost:9090/xxx.html?userName=xxc
问:为何html静态页面也能接收参数?
答:xxc.html并不是一个真正的html页面,而是在web.xml文件中经过了映射而已。
<servlet>
<servlet-name>servletday1</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletday1</servlet-name>
<url-pattern>/xxc.com</url-pattern>
</servlet-mapping>
问:servlet在容器中的实例对象可以被创建多次么?
答:一般情况下当服务器收到要访问这个servlet的请求后,这个servlet实例对象就会被创建,而且只创建一次,后面如果有同样的请求,那么就会访问已经创建好的实例对象(这也说明了,为什么第一次访问servlet的时候会比后面访问的速度要慢)。但是如果在web.xml中多次注册了这个servlet,那么servlet在容器中的实例对象就可以被创建多次(注册几次就创建几次)。
<servlet>
<servlet-name>servletday1</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
</servlet>
<!-- 再次注册同一个servlet,但是servletName必须不一样 -->
<servlet>
<servlet-name>servletday2</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletday1</servlet-name>
<url-pattern>/pkq.com</url-pattern>
</servlet-mapping>
问:一个servlet可以被映射多次么?
答:可以。如下创建就可以用两种url来访问。
(1)http://localhost:9090/项目名/xxc.com
(2)http://localhost:9090/项目名/pkq.com
<servlet>
<servlet-name>servletday1</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletday1</servlet-name>
<url-pattern>/xxc.com</url-pattern>
<url-pattern>/pkq.com</url-pattern>
</servlet-mapping>
servlet的*通配符:
在Servlet映射到的URL中也可以用*通配符,但是只能有两种固定的格式:
(1)“*.扩展名”
(2)以正斜杠(/)开头并以“/*”结尾。
注意:正斜杠(/)后的*不能带扩展名,即不能设置为“/action/*.xx”形式
Servlet映射的最具体匹配规则:
对于如下的一些映射关系:
●/abc/*映射到Servlet1
●/*映射到Servlet2
●/abc映射到Servlet3
●*.do映射到Servlet4
将发生如下一些行为:
●当请求URL为“/abc/a.html”,“/abc/*”和“/*”都可以匹配这个URL,
Servlet引擎将调用Servlet1
●当请求URL为“/abc”,“/*”和“/abc”都可以匹配这个URL,
Servlet引擎将调用Servlet3
●当请求URL为“/a.do”,“/*”和“*.do”都可以匹配这个URL,
Servlet引擎将调用Servlet2
●当请求URL为“/xxx/yyy/a.do”时,“/*”和“*.do”都可以匹配这个URL
Servlet引擎将调用Servlet2
匹配原则总结:
目录格式优先级大于后缀名格式。如果同是目录格式或者后缀名格式,哪个更像哪个匹配。
问:为何在访问http://localhost:9090/项目名/a.html时,就能访问到a.html这个页面?
答:因为在tomcat下的conf目录下有一个web.xml文件,这个文件中有如下一段配置信息。这个默认的servlet会去硬盘上找a.html网页返回给浏览器。
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
Servlet的生命周期:
1.web容器检查是否已经装载并创建了该Servlet的实例对象。如果是,则执行第四步,否则,这行第二步。
2.装载并创建该Servlet的一个实例对象。
3.调用Servlet实例对象的inint()方法。
4.创建一个用于封装HTTP请求消息的HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse对象,然后调用Servlet的service()方法,并将请求和响应对象作为参数传递进去。
5.WEB应用程序被停止或重新启动之前,WEB容器将卸载Servlet,并在卸载之前调用Servlet的destroy()方法。
特赦情况:
为了防止第一个访问Servlet的用户看到生成后的网页所需要的时间比第二个慢的情况。我们可以在WEB容器启动这个WEB程序的时候就进行加载并创建实例对象(不需要发出请求)。
<servlet>
<servlet-name>servletday1</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
<load-on-startup>1</load-on-startup><!--启动时加载,数字是1至5,越小越优先,如果相同则按顺序加载-->
</servlet>
Servlet的构造方法和init方法的区别:
1.构造方法是JVM调用,init方法是由tomcat调用。
2.构造方法要优先于init方法调用,原因也很好理解:因为要先有Servlet这个实例对象,才能使WEB容器去调用。
3.init方法中有HttpServletRequest和HttpServletResponse两个参数。但是构造方法里却没有。所以不能说将init()里的步骤可以放到构造方法里去执行。
Tomcat将Servlet的一些配置信息都封装在ServletConfig对象中
java.lang.String | getInitParameter(java.lang.String name) Returns a String containing the value of the named initialization parameter, ornull if the parameter does not exist. |
java.util.Enumeration | getInitParameterNames() Returns the names of the servlet's initialization parameters as an Enumeration of String objects, or an empty Enumeration if the servlet has no initialization parameters. |
ServletContext | getServletContext() Returns a reference to the ServletContext in which the caller is executing. 获取上下文对象 |
java.lang.String | getServletName() Returns the name of this servlet instance. |
Web.xml中的全局和局部配置信息
<context-param><!--全局初始化参数 -->
<param-name>userName</param-name>
<param-value>术士</param-value>
</context-param>
<servlet>
<servlet-name>servletday1</servlet-name>
<servlet-class>cn.xxc.servletday1.FirstServlet</servlet-class>
<init-param><!-- 注意这个标签必须紧跟servlet-class标签 -->
<param-name>name1</param-name>
<param-value>萨满</param-value>
</init-param>
<init-param>
<param-name>name2</param-name>
<param-value>法师</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
获取Web.xml中的配置信息
private ServletConfig config;
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
}
@Override
public void service(ServletRequest request, ServletResponse response)
throws ServletException, IOException {
System.out.println("ServletName--->"+config.getServletName());//获取Servlet的名字
Enumeration<String> configs = config.getInitParameterNames();//获取Servlet的初始化信息 name值,是枚举类型
while(configs.hasMoreElements()){
String name = configs.nextElement();
String value = config.getInitParameter(name);//根据name值,获取value
System.out.println(name+" -------------- "+value);
}
ServletContext context = config.getServletContext();
String value = context.getInitParameter("userName");//获取全局初始化参数
System.out.println(value);
}
以下是一个实现了Servlet接口的自定义的Servlet 需要实现5个方法
package cn.xxc.servletday1;
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 FirstServlet implements Servlet {
@Override
public void init(ServletConfig paramServletConfig) throws ServletException {//tomcat在调用Servlet时,初始化会自动调用此方法,只会调用一次
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest paramServletRequest,
ServletResponse paramServletResponse) throws ServletException,
IOException {//当用户发送一次请求到这个Servlet时,会调用这个方法
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {//Web应用程序在卸载或重启前会调用此方法
}
}
我们发现每次写一个Servlet都需要实现Servlet接口,并且要覆写5个方法,而且这5个方法并不是全部会用到,这样就有些不足。
这时我们可以让自己写的Servlet继承GenericServlet类
package cn.xxc.servletday1;
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 FirstServlet extends GenericServlet {
@Override
public void service(ServletRequest paramServletRequest,
ServletResponse paramServletResponse) throws ServletException,
IOException {
/*
* 为什么这样就能取到ServletConfig?
* 请看GenericServlet源码。
*/
ServletConfig config = this.getServletConfig();
}
}
GenericServlet类源码
package javax.servlet;
import java.io.IOException;
import java.io.Serializable;
import java.util.Enumeration;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public abstract class GenericServlet
implements Servlet, ServletConfig, Serializable{
private transient ServletConfig config;
public void destroy(){//覆写Servlet中的销毁方法
}
public String getInitParameter(String name){
return getServletConfig().getInitParameter(name);
}
public Enumeration getInitParameterNames(){
return getServletConfig().getInitParameterNames();
}
public ServletConfig getServletConfig(){
//这里就解释了为何实现了GenericServlet的子类可直接ServletConfig config = this.getServletConfig();
return this.config;
}
public ServletContext getServletContext(){
return getServletConfig().getServletContext();
}
public String getServletInfo(){
return "";
}
public void init(ServletConfig config)//覆写Servlet接口的初始化方法
throws ServletException{
this.config = config;
init();
/*
* 这种函数称为钩子函数
* 既然继承了GenericServlet,它给我们提供了什么样的简便呢?
* GenericServlet覆写了init(ServletConfig config)然后在其中又调用一个自定函数init()
* 在子类中只需要覆写init()方法即可,即不需要做像this.config = config赋值操作【最主要的方便就是省略了这句话】
* 为何在destroy函数中不定义这种钩子函数
* 没必要,因为destroy函数是无参的,即不需要做像this.config = config赋值操作
* 在GenericServlet中init()方法为空
* 当一个类继承了GenericServlet,如果需要在初始化函数中加入一些操作,那么就可以覆写GenericServlet中的init()方法
* 这样做的原因是因为tomcat调用初始化方法必定是init(ServletConfig config)而不会去调用自定义的init()
*/
}
public void init() throws ServletException{
}
public void log(String msg){
getServletContext().log(getServletName() + ": " + msg);
}
public void log(String message, Throwable t){
getServletContext().log(getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest paramServletRequest, ServletResponse paramServletResponse)
throws ServletException, IOException;
public String getServletName(){
return this.config.getServletName();
}
}
GenericServlet还不足以满足我们的需求,比如获取用户的IP地址,是在HttpServletRequest类中才有的方法,需要把ServletRequest强转。
但是每次强转又显得不足,这时我们可以继承HttpServlet来解决这个问题。(HttpServlet继承了GenericServlet)
而且Http请求方式又有好几种,每种方式都对应不同的方法。如果使用ServletRequest,那么就需要我们自己去判断到底是get,post,put,delete,options,trace。这样也很麻烦。
所以此时,我们可以用HttpServletRequest。
摘取HttpServletRequest源码中的两个方法就能明白它是怎么运行的。
public void service(ServletRequest req, ServletResponse res) //强转,并且返回强转后的结果
throws ServletException, IOException{
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 service(HttpServletRequest req, HttpServletResponse resp)//判断请求方式是什么,并且进行相应的方法调用
throws ServletException, IOException{
long lastModified;
String method = req.getMethod();
if (method.equals("GET")) {
lastModified = getLastModified(req);
if (lastModified == -1L){
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if (ifModifiedSince < lastModified / 1000L * 1000L){
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(304);
}
}
}
else if (method.equals("HEAD")) {
lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
}
else if (method.equals("POST")) {
doPost(req, resp);
}
else if (method.equals("PUT")) {
doPut(req, resp);
}
else if (method.equals("DELETE")) {
doDelete(req, resp);
}
else if (method.equals("OPTIONS")) {
doOptions(req, resp);
}
else if (method.equals("TRACE")) {
doTrace(req, resp);
}
else{
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
这也就是我们平时在开发时用的最多的。
package cn.xxc.servletday1;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// TODO Auto-generated method stub
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// TODO Auto-generated method stub
super.doPost(req, resp);
}
}
Response详解:
1.首先要明确一点,确切的来说并不是response将服务器的响应送给了客户端,而是web容器将response里的内容取出后,生成响应的网页返回给客户端。
2.状态码详解:
void | setStatus(int sc) Sets the status code for this response. 设置状态码,但是有可能数字记不住,这时可以用静态常量来表示 例如HttpServletResponse.SC_BAD_REQUEST 表示404的意思 |
void | setStatus(int sc, java.lang.String sm) Deprecated. As of version 2.1, due to ambiguous meaning of the message parameter. To set a status code use setStatus(int) , to send an error with a description usesendError(int, String) . Sets the status code and message for this response.过时设置状态码,同时生成状态码的message信息,设置message信息的时候不要忘记在前面加response.setCharacterEncoding("UTF-8");否则乱码如下图: |
void | sendError(int sc) Sends an error response to the client using the specified status code and clearing the buffer.//设置状态码,而且清空下面写在response里的内容,返回给浏览器与状态码对应的结果页面,无论这个方法写在setStatus前还是后,都是此方法生效 |
以下3种方式都是设置服务器相应数据的长度。
response.setHeader("Content-Length", "6");
response.setIntHeader("Content-Type", 6);
response.setContentLength(6);
例如:总共返回3个字节,但是消息头却说有6个字节,这时浏览器的进度条就一直会处于等待状态。但最后进度条还是会读完,原因是服务器和浏览器建立的时间超过限定时间就和浏览器断开连接了。
response.setHeader("Content-Length", "6");
response.getOutputStream().write("aaa".getBytes());
问:为何消息头写的是5个字节,送回的数据是3个字节,进度条还是和正常一样无需等待就能显示?
答:因为aaa后还有2个字节:\r\n。
response.setContentLength(5);
response.getOutputStream().write("aaa".getBytes());
页面编码:
默认:
response.setHeader("Content-Type", "text/html");//告诉浏览器服务器响应的数据要以字符显示(默认为上次在浏览器设置的编码形式)
response.getOutputStream().write("哈哈哈".getBytes());//响应的数据,以gbk(默认)编码成二进制数据送给浏览器
设置:
response.setHeader("Content-Type", "text/html;charset=utf-8");//设置浏览器用UTF-8编码来显示接收到的二进制数据
response.getOutputStream().write("哈哈哈".getBytes("utf-8"));//送给浏览器的二进制数据以UTF-8来进行编码
geiWriter:
response.setHeader("Content-Type", "text/html");//告诉浏览器服务器响应的数据要以字符显示(编码默认为gbk)
response.getWriter().println("好久不见");//这个操作省略了字符编码操作,根据上面设置的编码来确定用什么码来进行二进制编码,如果上面没写,则为ISO-8859-1
问:为何getWriter()不是默认GBK编码?
答:getWriter()是tomcat的方法,它是按照tomcat默认编码iso-8859-1进行编码的,而getBytes()是JVM的方法,他默认是按照操作系统的编码。
response.setHeader("Content-Type", "text/html;charset=utf-8");//下面这两句和第一句是同一个意思
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
设置网页不缓存的3个消息头:
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
由于HTML页面是静态页面不能动态生成,所以HTML页面的编码采用如下格式定义:
http-equiv表示等同于哪个消息头的意思 content表示消息头的值
<meta http-equiv="Content-Type" content="text/html;charset=gbk">
如下例子可以证明:html加载页面的时候是先加载所有meta标签后,然后再加载正文的。
<!DOCTYPE html>
<html>
<head>
<title>gbk.html</title>
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
<meta http-equiv="description" content="this is my page">
</head>
<body>
哈哈哈哈<br> <!--依旧不显示乱码-->
</body>
<meta http-equiv="Content-Type" content="text/html;charset=gbk"><!--meta标签在下面也是有效的,meta会先于被加载-->
</html>
模拟自动刷新的消息头:
<meta http-equiv="Refresh" content="3">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Pragma" content="no-cache">
用于文件下载的两个消息头:
response.setContentType("application/octet-stream");//告诉浏览器服务器返回的是二进制数据
response.setHeader("Content-Disposition", "attachment;filename="+URLEncoder.encode("皮卡丘.txt","UTF-8"));//如果URL里有中文必须进行URL的编码
注意:
往respons里写数据的时候,response并不是马上把写入的数据交给服务器处理的,而是先写到response的缓冲区里的。
Request:
注意:
jsp中代码
<form action="servlet/ThirdServlet?userName=1&passWord=2" method="post">
姓名<input type="text" name="uesrName"><br>
密码<input type="text" name="passWord"><br>
<input type="submit" value="提交">
</form>
servlet中代码
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String userName = request.getParameter("userName");
String passWord = request.getParameter("passWord");
response.getWriter().println("userName --->"+userName);
response.getWriter().println("passWord---->"+passWord);
}
经过测试发现,get请求时候,组件里的值会覆盖url后的参数,而post既可以获取url后面的参数的值也可以获取组件里的值
关于浏览器发送给服务器乱码问题:
在servlet中一直有这么一个疑问:
request.setCharacterEncoding("UTF-8");这个方法里的参数到底是GBK还是UTF-8或者是别的什么编码。
经过HTTP协议的学习后,势必有这么一个疑问,response送给浏览器显示网页的时候,会告诉浏览器要用什么码来显示网页,那么浏览器传递数据给服务器的时候,是否也有这么一个消息头来告诉服务器应该以什么码来解码浏览器送给服务器的中文数据呢?事实上,浏览器没有这么一个消息头送给服务器。也就是说
System.out.println(request.getCharacterEncoding());
这句话的打印结果是Null。
那么,我们该如何去设置request.setCharacterEncoding("UTF-8")方法里的参数呢?
既然,我们不能获取浏览器是用什么码编码,那就规定好浏览器在进行中文数据编码的时候用什么码来编码的。
(浏览器将中文数据是按什么码进行编码的,主要看浏览器当前显示网页的编码是什么码。例如IE可以右键--->编码进行查看和设置。)
这样我们就可以把request.setCharacterEncoding("UTF-8");方法里的参数写成规定的编码。来解决问题了。
request.setCharacterEncoding("UTF-8");只针对post提交有效,因为这句话针对的是请求体里的内容解码。
对URL后面的中文参数是起不到解决乱码的作用,即get方式提交时,此方法无效。
那么改怎么办呢?
1.在tomcat的conf文件夹下的server.xml文件的改tomcat端口的标签里增加如下属性,表示经过URL编码的的中文都用UTF-8来解码。
<Connector port="9090" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" URIEncoding="UTF-8"/>
2.在tomcat的conf文件夹下的server.xml文件的改tomcat端口的标签里增加如下属性,表示是否用post处理中文的方式来处理get的中文乱码问题。这样改还不够,需要在post方法里写出用什么码解码。也就是说需要两步!
<Connector port="9090" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" useBodyEncodingForURI="true"/>
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
}
3.但是以上两种解决get请求的中文乱码问题的方式仅限于tomcat服务器,如果是别的web服务器该怎么解决呢?
String userName = request.getParameter("userName");
userName = new String(userName.getBytes("iso-8859-1"),"UTF-8");
request.getRequestDispatcher().include(request, response)和request.getRequestDispatcher().forward(request, response)方法的区别:
这个方法是跳转到一个页面上,并且把request和response都带去下个页面。
request.getRequestDispatcher("/WEB-INF/success.jsp").forward(request, response);
但是如下例子:
servlet代码
response.setCharacterEncoding("UTF-8");
response.getWriter().println("跳转前...");
request.getRequestDispatcher("/index.jsp").forward(request, response);
response.getWriter().println("跳转后...");
如果用request.getRequestDispatcher().forward(request, response);只能显示index.jsp页面的内容,不能显示response用write方法往缓冲区里写的内容。
原因:在进行forward跳转的时候,会将writer往缓冲区里写的内容全部清空。
response.setCharacterEncoding("UTF-8");
response.getWriter().println("跳转前...");
request.getRequestDispatcher("/index.jsp").include(request, response);
response.getWriter().println("跳转后...");
如果用
request.getRequestDispatcher("/index.jsp").include(request, response);则不会清空write往缓冲区里写的内容。即,可以显示 跳转前和跳转后字样。
但是经过测试发现,如果用request.getRequestDispatcher("/index.jsp").include(request, response);进行页面的跳转,Chrome和Firefox会把index.jsp页面的源码原封不懂的显示给浏览器,而IE可以正确显示index,jso里body里的部分。
当使用request.getRequestDispatcher("/index.jsp").include(request, response);被引用的index.jsp页面中有中文字符,这时index.jsp页面里的page指令都是无效的。所以会出现乱码。
因为这时是引用的,servlet只引用了jsp中的内容,那么用什么编码显示被引用的页面是由引用方来决定的。所以,为了防止出现乱码,需要在servlet里加一句:response.setCharacterEncoding("UTF-8");