Servlet
学习摘要:
理解为何要学习Servlet? 什么是Sevlet?如何编写一个Servlet程序。
Servlet的生命周期是什么。继承体系又是什么。访问流程是怎样的。掌握Servlet的细节上的处理,注解方式的使用,理解Servlet是线程不安全的,如何解决。
为何要学习Servlet
课前引导为何要学习Servlet?
我们前面学习的都是从服务器访问的静态资源,访问的流程是怎么样的呢?客户端向服务器请求什么静态资源,服务器就去找对应的静态资源,找到了以后就直接返回给浏览器。在这个流程中是服务器是没有对资源进行任何加工处理的。那么现在有这样一个需求。
需求:
完成一个登陆案例,登陆成功返回成功页面,如果登陆失败返回失败页面。如果还是使用访问静态资源的方式,显然不能满足我们的需求。如图:
在登陆案例中,我们通过发送请求把账号和密码传给了服务器。在服务器端需要做什么呢?
在服务器端需要根据账号和密码调用DAO去查询数据库,校验数据库中是否存在该用户。然后根据校验的结果生成动态的页面给浏览器显示。这里”动态“两字的理解,其实指的是如果校验成功动态生成成功页面返回给浏览器,如果校验失败动态生成失败页面返回浏览器。
那么这些在服务器端做的工作具体由谁来完成呢?
就是我们今天学习的Servlet技术。Servlet可以帮我们完成账号和密码的校验,并动态生成页面返回给浏览器进行显示。如:
那么Servlet到底是什么呢? 我看下面的Servelt的概述。
Servlet的概述
在百度百科中是这样解释的:
Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,主要功能在于交互式地浏览和修改数据,生成动态Web内容。
狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。
在这里我们需要摘出来一些重点。需要大家理解的。
第一个: Servlet 英文全称 Java Servlet。这是常识。
第二个: Servelt是Java编写的服务器端程序。主要是和浏览器进行交互的,可以生成动态web内容。
第三个: Servelt的本质是一个类,并且是实现Servlet接口的类。运行在Java的应用服务器中
第四个: Servelt可以处理任何类型的请求,但是我们一般用来处理基于Http协议的请求。
最后我们再通过一个图来区分一下访问静态资源和动态资源的区别。如:
在图中,我们可以看到,在服务器端有个web项目,如果通过浏览器访问web项目的静态资源,方式很简单,通过发送静态资源的地址到服务器,服务器根据资源地址找到相应的资源,返回给浏览器。如果通过浏览器访问we项目的动态资源,服务器交给Servlet处理,处理过程中,可能需要访问数据库。 Servlet根据需求生成动态的web内容,然后把内容响应给浏览器。
第一个Servlet程序
查看J2EE文档,在左边的菜单中选择索引选项。在搜索框中输入Servlet。
首先我们看到文档中关于Servlet的介绍。如:
public interface Servlet
看到这句话我们知道Servelt是一个接口。
A servlet is a small Java program that runs within a Web server. Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol.
看到这一段话,我们知道Servlet是一个java小程序,运行在web服务器中。 通常是通过Http(超文本传输协议)接收和响应web客户端发送来的请求。
上面这些介绍和我们前面讲概述中的概念是吻合的。
再往下看文档。看到一些抽象方法。如:
文档中有这样一句话 如:
Defines methods that all servlets must implement.
这句话的意思是,所有的Servlet必须实现接口中定义的方法。
所以得出结论,我们如果定义一个Servlet,需要实现接口中的方法.
我们现在知道了定义一个Servlet的流程,接下来我们创建第一个Servelt的程序。
首先我们先写一下开发步骤:
A.搭建JavaWeb项目
1.创建一个Java项目:servlet;
2.在servlet中创建一个文件夹webapp,表示Web项目的根;
3.在webapp中创建WEB-INF文件夹;
4.在WEB-IN中创建文件夹:lib,classes;
5.去Tomcat根/conf拷贝web.xml文件放在WEB-IN中,修改编码为UTF-8,只需要保留根元素.
6.把当前项目的classpath路径改成webapp/WEB-IN下的classes中.
B.编写Servlet
1.为该项目增加Servlet的支持.
1.1:把Tomcat根/lib中servlet-api.jar文件拷贝到项目下WEB-INF下的lib中
1.2:在项目中选择servlet-api.jar,鼠标右键,build path-->add to build path
2.开发Servlet程序:
2.1:定义一个类HelloServlet,并让该类去实现javax.servlet.Servlet接口;
2.2:实现Servlet接口中的init,service,destory等方法.
注意:若生成方法中的参数是arg0或者arg1等格式的,原因是还没有关联源代码的问题:
关联上:apache-tomcat-8.0.53.zip,并重新实现/复写方法就OK;
C.配置Servlet
HelloServlet,仅仅是一个普通的实现类而已,而我最终要运行在Tomcat服务器中,所以得告诉Tomcat,来帮我管理HelloServlet类;
1.找到项目根下的WEB-INF下的web.xml文件:
2.在根元素web-app中创建一个新的元素节点:servlet
3.在根元素web-app中创建一个新的元素节点:servlet-mapping
D.部署项目
修改tomcat根/conf中的server.xml 文件,通过Context标签进行部署。
<Context docBase="C:\stsworkspace\servlet\webapp" path=""/>
代码如下:
HelloServlet:
public class HelloServlet implements Servlet {
@Override
public void destroy() {
}
@Override
public void init(ServletConfig arg0) throws ServletException {
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
System.out.println("hello servlet");
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
}
web.xml:
<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>cn.wolfcode._01_.hello.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
项目目录结构图:
通过浏览器访问的Servlet的简单流程图:
Servlet的生命周期
什么是Servlet的生命周期:
百度百科中解释到:生命周期就是指一个对象的生老病死。
那顾名思义Servlet的生命周期,就是指的是Servlet对象的创建到销毁的整个过程。
为何要研究Servlet的生命周期呢?
我们前面通过Servlet的入门程序,编写一个Servlet其实质就是一个类,那么我们创建一个类的目的何在呢?无非就是在合适的时机创建对象,合适的时机调用其里面的方法,最终达到为我所用的目的。
那么现在我们定义的Servlet这个类,创建对象交给了tomcat,连方法的调用时机也是由tomcat来决定的,我只有研究它的生命周期,这样我可以根据方法的调用时机不同,做出相应的业务逻辑处理,最终也达到了为我所用的目的。
Servlet的生命周期方法
通过查看API,我们知道Servlet接口中有五个抽象方法,这五个方法并非都是生命周期的方法。
生命周期的方法有三个:
public void init(ServletConfig config)
public void service(ServletRequest req, ServletResponse res)
public void destroy()
我们可以直观的看到这三个方法,都是非静态的方法。所以调用此方法必须要先创建对象。所以我再增加一个构造方法,通过实验来看看生命周期方法的调用执行顺序是怎么样的。
通过实验结果我们发现:
方法的执行流程: 构造器----->init方法------>循环[service方法]------>destory方法
构造器方法执行时机 : 只会在第一次请求的时候执行一次,后面不再创建,说明多次请求,Servlet对象只创建一个。因此得出结论,Servlet是单例的。
如果我把构造器方法改成私有的,发现程序报错。 如:
说明底层通过反射创建对象的方式是访问的公共的构造器方法,如果改成私有,Java虚拟机就不会分配公共的无参构造器了。
那么只要有公共的构造器 就可以了吗?我们再做一个实验,在公共构造器中添加参数,发现也报错。如:
更进一步说明底层通过反射创建对象是访问的公共的无参数的构造器方法,如果在公共构造器中添加参数,Java虚拟机还是不会分配公共的无参构造器。
那么我们也可以猜想到底层可能是通过Class.forName(类的全限定名).newInstance();这种方式来创建的Servlet对象。
init方法执行时机:
该方法在第一次请求的时候执行一次, 后面的请求中不会再执行,所以我们可以在这个方法中处理初始化的操作
service方法执行时机:
每次请求,都会执行该方法,所以在这个方法中处理客户端发送来的请求和响应信息给客户端。
destroy方法执行时机:
通过实验发现,当服务器正常关闭的时候,执行一次,服务器非正常关闭的时候不会执行。所以不要期望该方法一定会执行,因此也不要在该方法中处理资源销毁的操作。
Servlet的请求流程
接下来我们详细的分析一下Servlet的请求流程是怎么样的?
如图:
看图分析:
1:浏览器发出请求:http://localhost:80/servlet/hello
2:解析请求信息:
http:协议
localhost:找互联网上的哪一台主机.
80: 从主机中找到对应80端口的程序--->Tomcat服务器.
/servlet: 当期项目的上下文路径
/hello: 当期请求的资源名
3:找到Tomcat根/config/server.xml文件.
解析server.xml文件:
判断获取哪一个<Context/>元素的path属性为servlet
若找不到:404错误.
若 找到:解析该<Context/>元素,得到docBase属性,获取当期访问Web项目的根的绝对路径:
C:\stsworkspace\servlet\webapp
4:从C:\stsworkspace\servlet\webapp下的WEB-INF下找到web.xml文件.
判断web.xml中是否有<url-pattern>的文本内容为/hello.
若找不到:404错误.
若 找到:继而可以获取该资源对应Servlet类的全限定名称:cn.wolfcode._01_.hello.HelloServlet.
5:判断Servlet实例缓存池中是否有cn.wolfcode.01.hello.HelloServlet的对象.
Map<String,Servlet> cache = ......(Tomcat提供的);
key:存Servlet类的全限定名称
value:该Servlet类的对象.
Servlet obj = cache.get("cn.wolfcode._01_.hello.HelloServlet");
if(obj==null){
//Servlet实例缓存中没有该类的对象,第一次.
GOTO 6:
}else{
//有对象,非第一次.
GOTO 8:
}
6:使用反射调用构造器,创建对象.
obj = Class.forName("cn.wolfcode._01_.hello.HelloServlet").newInstance();
把当前创建的Servlet对象,存放在缓存之中,供下次使用.
cache.put("cn.wolfcode._01_.hello.HelloServlet",obj);
7:创建ServletConfig对象,并调用init方法.
obj.init(config);
8:创建ServletRequest对象和ServletResponse对象,并调用service方法.
obj.service(req,resp);
9:在service方法中对浏览器做出响应操作.
Servlet的继承体系
为何研究继承体系
前面我们在学习Servlet的时候,说过如何定义一个Servlet,只要定义一个类然后实现Servlet接口即可,但是必须要实现里面的五个抽象方法,而在开发中这五个方法并不是都是常用的。对于不常用的方法,我们是没有必要也覆写过来的。
比如: 我们只需要处理请求和响应,所以只关心service方法,像destroy,getServletInfo 等方法就不需要覆写。
我们如果不想覆写所有的方法,该如何解决呢?
解决思路就是进行抽取处理。
也就是说,我们可以定义一个抽象类比如(MyServlet),然后让MyServlet去实现Servelt接口。在该类中,覆写那些不常用的方法。这样我们自己再定义Servlet的时候,只需要继承MyServlet类即可。如:
MyServlet:
public abstract class MyServlet implements Servlet {
@Override
public void init(ServletConfig config) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
ExtendServlet:
public class ExtendServlet extends MyServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
// 处理请求和响应
}
}
我们这种做法是很好的。但是如果我们研究了Servlet的继承体系,我们就不会这样做,为何呢?
因为在Servlet中提供了一个实现类(GenericServlet),已经完成了这样的操作,所以我们是没有必要自己再定义一个类的。这样是多此一举的,所以我们直接用就可以了。
通过查看API文档,得知GenericServlet是一个抽象类。并且实现了Servlet和 ServletConfig和Serializable 接口
public abstract class GenericServlet
extends Object
implements Servlet, ServletConfig, Serializable
在文档中也介绍说 GenericServlet 是一个通用的Servlet。继承它只需要覆写service方法即可 如:
To write a generic servlet, you need only override the abstract service method.
//在通用的Servlet中,只需要覆写service方法即可。
我们在讲解Servlet概述的时候,说过我们学习Servlet的主要目的是处理基于HTTP协议的请求。也就是说,我么也可以采取抽取的方式对ServletRequest 和 ServletResponse 进行强制类型转换。但是通过查看文档,这种抽取的做法,我们还是不用做。文档中是这样介绍的。如:
Defines a generic, protocol-independent servlet. To write an HTTP servlet for use on the Web, extend HttpServlet instead.
我们如果要定义一个独立协议的通用的Servlet,编写用于Web的HTTP servlet,只需要继承HttpServlet就可以了。
可见:Servlet的继承体系中给我们提供了一个HttpServlet这个类,HttpServlet 帮我们处理了强转的问题。我们只需要继承该类即可。
接下来我们研究一下: HttpServlet是什么呢?和 GenericServlet又有和关系呢?
查看API文档,发现 HttpServlet是GenericServlet的一个子类。并且还是一个抽象类。通过查看源码发现它覆写了service方法,并在里面做了request,response 两个对象的强转操作。再次确认了类型转换的做法。如:
@Override
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);
}
}
然而它在service方法中做了强转以后,然后调用了自己提供的service方法。如:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
//省略
}
我们学过Servlet的生命周期,我们知道,Servlet的方法调用顺序:
构造方法->init方法 ->service(ServletRequest req,ServletResponse resp)
如果我们定义一个子类(比如ExtendServlet),然后继承HttpServlet,覆写里面protected修饰的service方法,这样最终执行的顺序就是
构造方法->init方法 ->service(ServletRequest req,ServletResponse resp)->service(HttpServletRequest req,HttpServletResponse resp)
如:
这里要注意,覆写service方法的时候,不能再写super.service(req,res);方法,这样会调用父类中的service方法,最终调用doGet方法,报错405的错误如:
导致报错的原因,我们通过一个流程图来进行说明如:
到这里我们已经把Servlet的继承体系讲完了,最后我们再通过一个图来进行梳理一下,servlet的继承结构。
通过我们前面的学习和查看API文档,我们知道GenericServlet 是一个通用的servlet,帮助我们覆写了一些不常用的方法。它实现了两个接口 Servlet ,servletConfig 。servletConfig 这个接口,我们放到下个知识点中详细讲解。我们最终编写Servlet是通过继承 HttpServlet,HttpServlet是 GenericServlet的一个子类。主要是帮我们解决了对象的强转问题。这样我们可以更专注于处理基于Http协议的请求。
在实际开发中,我们编写很多个Servlet,比如 AServlet,BServlet,CServlet 如果我们发现这三个Servlet中还是存在有重复的代码,我们进行上上抽取,再抽取一个父类,比如BaseServlet,专门处理公共的代码部分。
ServletConfig接口
编写一个案例,根据编码不同在控制台上打印不同的内容。代码如下:
InitServlet:
public class InitServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String encoding = "utf-8";
if ("utf-8".equals(encoding)) {
System.out.println("切换成中文内容输出");
} else {
System.out.println("切换成英文内容输出");
}
}
}
web.xml:
<servlet>
<servlet-name>InitServlet</servlet-name>
<servlet-class>cn.wolfcode._04_.init.InitServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>InitServlet</servlet-name>
<url-pattern>/init</url-pattern>
</servlet-mapping>
在案例中,我们发现,encoding的值是写死在里面了,如果修改编码需要修改代码,很显然存在硬编码,不好维护。
那么如何解决存在的硬编码呢?
这种类似的硬编码问题,我们前面讲过,所以很容易想到处理方案,就是抽取, 抽取到配置文件中进行维护。配置文件的选择我们知道目前学习了两种,一种是properties文件,一种是xml文件。那么对于当前情况,我们应该选择xml文件。为何呢?因为存在一个现成的web.xml 文件,我们不需要额外的再定义一个配置文件。
那么编码的相关信息配置在哪里呢? 思考一下,我们是给谁配置,是给某一个Servlet,所以编码的信息应该配置对应的Servlet标签中才为合理。 在这里我们通过 init-param 标签来配置编码信息 这个标签中有两个子标签,一个是 param-name 用来存入参数的名称。另一个是 param-value 用来存入参数的值。 有点像我们的map结构 key-value 形式。
好了配置编码信息的问题解决了,那么我们如何在代码中获取配置信息呢?
关于获取配置信息的问题 ,我们就要学习一个新的知识点才能解决。那就是 ServletConfig。
API文档中给出的解释是:
A servlet configuration object used by a servlet container to pass information to a servlet during initialization.
含义就是说:它是servlet容器用于在初始化期间向servlet传递信息的servlet配置对象。
通过文档我们还得知,ServletConfig 是一个接口,提供了一些方法。如:
String getInitParameter(String name):根据指定的初始化参数名称,获取对应的参数值.
Enumeration<String> getInitParameterNames():获取当期Servlet所有初始化参数的名字,返回Enumeration对象(古老Iterator对象).
ServletContext getServletContext():获取应用上下文对象.
String getServletName():获取Servlet配置的名字<servlet-name>的文本内容.
那么我们就可以通过 调用 getInitParameter 方法,传入我们之前定义参数名称,来获取对应的内容。
修改web.xml 文件,把编码信息配置到对应的Servlet上面。 如:
web.xml:
<servlet>
<servlet-name>InitServlet</servlet-name>
<servlet-class>cn.wolfcode._04_.init.InitServlet</servlet-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-9</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>InitServlet</servlet-name>
<url-pattern>/init</url-pattern>
</servlet-mapping>
然后通过 ServletConfig 接口中提供的 getInitParameter 方法来获取配置的信息。如:
InitServlet:
public class InitServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String encoding = getServletConfig().getInitParameter("encoding");
if ("utf-8".equals(encoding)) {
System.out.println("切换成中文内容输出");
} else {
System.out.println("切换成英文内容输出");
}
}
}
通过查看源码发现:直接使用 getInitParameter 方法和 getServletConfig().getInitParameter 是一样的。 直接调用 getInitParameter 方法,其实内部还是通过 ServletConfig 对象调用。 如:
HttpServletRequest 中常用的API
用户发送请求到服务器。我们都知道可以通过浏览器抓包,查看发送的信息。发送的信息有三部分组成(请求行,请求头,请求体)。通过观察控制台,组成的三部分中都是key-value的形式存在,那以我们java中面向对象思想的角度分析,这些键值对会被封装到对象中,使用对象来操作它们。这个对象就是我们今天要学习的 HttpServletRequest 对象。
HttpServletRequest 在文档中是这样介绍的:
Extends the ServletRequest interface to provide request information for HTTP servlets.
扩展ServletRequest接口,为HTTP servlet提供请求信息。
The servlet container creates an HttpServletRequest object and passes it as an argument to the servlet's service methods
servlet容器创建一个HttpServletRequest对象,并将其作为参数传递给servlet的service方法
也就是说, Servlet 创建 HttpServletRequest 对象,使用该 HttpServletRequest 对象封装请求信息,然后作为参数传给了service方法。
在该接口中提供了很多方法,都是用来获取请求信息的,我们可以看一些常用的方法。如:
String getContextPath():获取上下文路径,<Context path="上下文" ../>
String getHeader(String headName):根据指定的请求头获取对应的请求头的值.
String getRequestURI():返回当期请求的资源名称. 上下文路径/资源名
StringBuffer getRequestURL():返回浏览器地址栏的内容
String getRemoteAddr():返回请求服务器的客户端的IP
我们通过工具,给大家演示一下,编写的代码和输出的结果:
在一个请求中,我们最关心的是用户发送的请求的参数。 所以我们今天重点掌握的方法,就是获取参数的方法。如:
String getParameter(String name) : 获取指定名称的请求参数的值
例如:
http://localhost:8080/servlet/request?name=xxx&age=10
System.out.println(req.getParameter("name"));
System.out.println(req.getParameter("age"));
Map getParameterMap() :所有的数据就在这一个Map集合中,那么如何快速的封装到JavaBean对象中呢????
Enumeration getParameterNames() : 获取所有的请求参数的名称
String[] getParameterValues(String name) : 获取指定名称的请求参数对应的多个值
演示代码如下:
发送请求的地址:
处理的Servlet:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String age = req.getParameter("age");
System.out.println("方式一:(单个参数获取方式)" + name + "/" + age);
System.out.println("==============================");
Map<String, String[]> map = req.getParameterMap();
System.out.println("方式二:(map获取方式)" + Arrays.toString(map.get("name")) + "/" + Arrays.toString(map.get("age"))
+ "/" + Arrays.toString(map.get("hobby")));
System.out.println("==============================");
String[] hobby = req.getParameterValues("hobby");
System.out.println("方式三:(一个参数对应多个值的获取方式)" + Arrays.toString(hobby));
System.out.println("==============================");
Enumeration<String> names = req.getParameterNames();
while (names.hasMoreElements()) {
System.out.println("方式四:(迭代器方式)" + names.nextElement());
}
}
控制台打印结果:
HttpServletRequest的应用--注册案例
我们使用刚才学的接收参数方法,来完成一个注册案例,注册的页面如下:
注册案例分析:
需要定义一个表单,使用post提交方式,action属性里面设置要访问的Servlet的映射地址。
然后再定义Servlet用于接收表单中提交过来的注册信息。
注册案例流程分析图:
案例代码如下:
register.html:
<body>
<h2>用户注册页面</h2>
<form action="/servlet/register" method="post">
用户名:<input type="text" name="username" /> <br/>
密 码:<input type="password" name="password" /><br/>
性 别:<input type="radio" name="sex" checked="checked" value="1"/>男
<input type="radio" name="sex" value="0">女<br/>
爱 好:<input type="checkbox" name="hobby" value="c" />C
<input type="checkbox" name="hobby" value="android" />Android
<input type="checkbox" name="hobby" value="java" />Java
<input type="checkbox" name="hobby" value="h5" />H5 <br/>
常住地:<select name="place">
<option value="gz">广州</option>
<option value="zz">郑州</option>
<option value="fz">福州</option>
<option value="cz">潮州</option>
</select> <br/>
<p/>
<input type="submit" value="注册">
</form>
</body>
RegisterServlet:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("username");
String password = req.getParameter("password");
String sex = req.getParameter("sex");
String[] hobby = req.getParameterValues("hobby");
String place = req.getParameter("place");
System.out.println("姓名:" + name);
System.out.println("密码:" + password);
System.out.println("性别:" + sex);
System.out.println("爱好:" + Arrays.toString(hobby));
System.out.println("常住地:" + place);
}
web.xml:
<servlet>
<servlet-name>RegisterDemoServlet</servlet-name>
<servlet-class>cn.wolfcode._05_.request.RegisterDemoServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RegisterDemoServlet</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>
通过抓包工具,查看数据提交情况:
解决请求参数为中文导致的乱码问题
在上述案例中,我们发现如果表单中出现中文的情况,在后台打印的结果是乱码的情况。这是因为Tomcat服务器是使用的ISO-8859-1编码的,该编码只占一个字节,不支持中文(两个字节).
解决方案:
方式一: 对乱码使用ISO-8859-1解码,然后在重新使用UTF-8 进行编码. 如:
String name = req.getParameter("username");
// 使用ISO-8859-1解码,恢复为二进制形式
byte[] bytes = name.getBytes("ISO-8859-1");
// 重新使用UTF-8 编码
name = new String(bytes, "UTF-8");
上述方式可以解决乱码的问题,但是如果需要处理的参数过多呢?如果还是使用上面的方式,肯定造成很多重复的代码。开发的工作量加大。
方式二:针对POST提交方式,Tomcat提供了一种更为简便的方式。通过请求对象调用setCharacterEncoding方法设置请求的编码方式即可。如:
request.setCharacterEncoding("UTF-8");//设置请求的编码方式.
注意: 这种方式使用必须满足一个条件,就是必须放置在获取第一个参数之前设置。并且这种方式只对POST请求有效。
如:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
String name = req.getParameter("username");
}
那么针对GET的请求方式怎么处理呢?
如果我们是使用的Tomcat 8.0以上,GET请求的乱码,Tomcat已经帮我们处理了,我们不用理会。
如果是Tomcat 8.0 以下,我们可以 重新设置Tomcat的编码方式,修改Tomcat的配置文件.
文件的所在位置: Tomcat根/conf/server.xml.
修改的内容如图:
HttpServletResponse对象的常用API
响应数据到客户端,其实就是在服务端把数据通过流的方式写会到客户端。流的方式无非就两种,一个是字符流,一个字节流。要通过流的方式响应数据,首先要先获取流,获取流,应该是get开头的方法。查看API 在 HttpServletResponse 接口中没有get开头的方法。那就继续去它的父接口中寻找,最终发现get方法有两个。如:
PrintWriter getWriter();
这个方法可以 返回一个PrintWriter对象,该对象可以向客户端发送字符文本
ServletOutputStream getOutputStream();
这个方法可以返回一个ServletOutputStream对象,该对象适合在响应中写入二进制数据。
这种方式通常用来实现文件下载的功能。
我们获取到PrintWriter对象,就可以调用它里面的方法,往浏览器上输出内容。如:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().println("<html><head><title>这是代码生成的html</title></head><body>您好</body></html>");
}
但是这样写,返回给浏览器的只是一个字符串。浏览器也不知道返回的字符串是什么格式的,什么编码。浏览器不知道调用什么解析引擎进行解析,不知道使用什么编码解析。在浏览器上显示会出现乱码。 如:
通过查看API,寻找解决方案。
我们要设置编码:
应该寻找方法的名字中以set开头,方法的名字中有Encoding关键字的。不难发现 setCharacterEncoding(String charset) 符合我们的要求。 这个方法就是设置响应给客户端的字符的编码方式。
我们要设置响应数据的格式:
应该寻找方法的名字中以set开头,方法的名字中有ContentType关键字的。不难发现 setContentType(String type) 符合我们的要求,这个方法就是在提交响应之前,设置响应数据的内容类型。
代码如下:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 设置响应数据的编码方式
resp.setCharacterEncoding("utf-8");
// 设置响应数据的格式
resp.setContentType("text/html");
resp.getWriter().println("<html><head><title>这是代码生成的html</title></head><body>您好</body></html>");
}
通过两句代码完成设置,过于麻烦。可以简写成一句。如:
resp.setContentType("text/html;charset=UTF-8");
处理结果图:
HttpServletResponse的应用--计算器案例
我们使用刚才学的API方法,来完成一个简单版的计算器案例,计算器效果图如下:
计算器案例分析:
需要定义一个表单,使用post提交方式,action属性里面设置要访问的Servlet的映射地址。登陆按钮设置为提交按钮,计算方式设置为下拉选,当点击登陆按钮的时候,提交表单,把输入框中的数据提交到Servlet,然后再定义CalServlet用于接收表单中提交过来的参数,计算出相应的结果,然后再动态的生成一个html页面,返回给浏览器。
注册案例流程分析图:
案例代码如下:
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 接收请求的参数
String first = req.getParameter("first");
String symbols = req.getParameter("symbols");
String second = req.getParameter("second");
// 执行计算
int result = 0;
int firstInt = 0;
int secondInt = 0;
if (hasLength(first) && hasLength(second)) {
firstInt = Integer.valueOf(first);
secondInt = Integer.valueOf(second);
switch (symbols) {
case "+":
result = firstInt + secondInt;
break;
case "-":
result = firstInt - secondInt;
break;
case "/":
result = firstInt / secondInt;
break;
default:
result = firstInt * secondInt;
break;
}
}
// 响应结果给浏览器
resp.setContentType("text/html;charset=UTF-8");
PrintWriter printWriter = resp.getWriter();
printWriter.println("<h1>简易的网页版计算器</h1>");
printWriter.println("<form action='/webapp/cal' method='post'>");
printWriter.println("<input name='first' value='" + firstInt + "'/>");
printWriter.println("<select name='symbols'>");
printWriter.println("<option>+</option>");
printWriter.println("<option>-</option>");
printWriter.println("<option>/</option>");
printWriter.println("<option>*</option>");
printWriter.println("</select>");
printWriter.println("<input name='second' value='" + secondInt + "'/>");
printWriter.println("<input type='submit' value='='/>");
printWriter.println("<input name='result' value='" + result + "'/> ");
printWriter.println("</form>");
}
public boolean hasLength(String str) {
return str != null && str.trim().length() != 0;
}
注意:在案例中,我们要考虑传入的数据,需要先判断有没有值,再进行转换,如果不判断会导致类型转换异常,如:
在案例中,我们不考虑输入的数据的内容为0和为字符串的情况,
Servlet的细节
1.在配置<servlet-name>元素的时候 ,其中的内容可以随便写,但是请不要使用default,为何呢? 因为在tomcat服务器内部,default这个名字已经被使用了,如果再使用,会覆盖之前的。如:
如果在我们自己的项目中servlet-name为default,那么将会把DefaultServlet覆盖掉,
所以,所有的静态资源的访问都是404
2.映射的路径
url-parttern: /a----->可以使用/a来访问到当前的Servlet
url-parttern: /a/b/c----->可以使用 /a/b/c来访问到当前的Servlet
通配符(*)来设值映射路径
url-parttern: /*----->可以使用任意个数的任意字符来访问到当前的Servlet
url-parttern: /system/*----->可以使用/system/任意个数的任意字符来访问到当前的Servlet
url-parttern: *.xxx----->可以使用以xxx为后缀的资源名来访问到当前的Servlet
3.添加欢迎页面
在tomcat/conf/web.xml中有如下的配置:
在我们请求url中如果没有指定具体的资源名称,那么会使用上面的欢迎页面
我们可以在我们自己的项目中添加对应的配置来修改欢迎页面
4.Servlet的初始化时机
Servlet默认是在第一次请求的时候,会执行创建并初始化
如果我们需要在当前Servlet中完成一个比较复杂(耗时)的初始化操作
那么这样会造成第一个访问该Servlet的用户体验非常差!!!!!
解决思路:不应该将初始化放在第一个访问的时候完成,而应该在服务器启动的时候
注解的使用
为何要学习注解呢?
问题:先看一下我们之前定义一个Servlet,需要在配置文件中配置,需要编写八行代码,完成servlet的配置。如图:
如上图我们开发一个Servlet,都需要在web.xml配置文件中添加八行配置信息,比较麻烦,还会造成一个xml文件非常的臃肿,不好维护。
如何来解决这个问题呢? 可以使用注解的方式来代替xml配置的方式。
直接使用Servlet规范中定义好的一个注解来实现Servlet的配置:
1.可以直接为当前注解的value属性设置,等同于为urlPatterns属性设置
@WebServlet("/cal")
public class CalResponseServlet extends HttpServlet {
}
2.注解的使用,必须需要第三方程序来赋予其功能
在web.xml中修改下面的配置即可
metadata-complete:true,不扫描类上的@WebServlet注解
metadata-complete:false,扫描类上的@WebServlet注解,默认值,可以不写
所以,在后面的开发中,都可以直接使用注解来完成Servlet的配置(将Servlet交给Tomcat管理)
注解虽然使用方便但是也存在弊端,就是映射资源路径写死在了代码中,xml方式虽然配置麻烦,但是解决了硬编码的问题。所以最后是使用xml还是注解,根据开发的实际情况而定。
Servlet 线程安全问题
我们先通过一个案例来看一个问题:
案例很简单,就是在Servlet中,接收到浏览器发来的内容,然后通过调用sleep方法让程序休眠五秒,休眠完成以后再将接收的参数输出到浏览器上显示。 在休眠期间,再通过浏览器请求一次。
代码如下:
@WebServlet("/thread")
public class ThreadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private String name;
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
name = req.getParameter("name");
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
resp.getWriter().println(name);
}
}
这个实验的结论会导致,输出结果错乱。这个问题被称为线程不安全问题。
造成这个问题的原因是什么呢?
servlet是单例的。对象只有一份,那么里面的成员变量也是共享一个。
如何解决呢?
有两种解决方案:
1.Servlet实现一个接口SingleThreadModel
此时只能一个线程来访问当前的Servlet,当前线程访问完之后,再交给下一个线程继续访问
这种访问,会造成我们的Servlet在处理请求和响应的时候效率低下的问题,不建议使用
2.建议不要使用成员变量,而使用局部变量(推荐)
如果当前的数据是需要根据请求参数来改变的话,这种使用局部变量
其他的都可以使用成员变量