1.编写一个简单的Servlet程序
1.1 创建项目
Maven是JAVA中一个常用的"构建工具".
一个程序,编写过程中,往往需要涉及到一些第三方库的依赖,另外还需要对这个写好的程序进行打包部署.
Maven存在的意义,就是为了能够方便的进行依赖管理和打包.
1.2 引入依赖
当前的代码要使用Servlet开发,而Servlet并不是java标准库自带的,就需要让maven能够把Servlet的依赖给获取出来.
在maven中央仓库中找到依赖.
java
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
手动创建一个<dependencies>标签,把刚才的坐标复制到这个标签里.
此时IDEA就会自动的通过maven从中央仓库来下载这里的依赖.
1.3 手动创建一些必要的目录文件
此处的目录结构,目录名字,都是固定的.
web.xml就是告诉tomcat,现在这个目录里的东西就是一个webapp,要把加载起来.
当然,web.xml里,需要写一些固定的内容.
java
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
1.4 编写代码
这里简单写一个Hello World
- 这个doGet方法不需要手动调用,doGet本质上也是一个"回调函数".
把这个方法写好之后,就会交给Tomcat,Tomcat在收到一个合适的GET请求的时候,就会自动调用doGet.
调用doGet的时候,tomcat就会解析这次的HTTP请求.生成一个HTTPServletRequest对象.(这个对象里的属性什么的都是和HTTP协议格式匹配的)
同时,Tomcat也会构造出一个空的HTTPServletResponse对象(这个空不是null,而是一个new好的,但是没有初始化属性的对象,把这个resp对象也会传递到doGet里面)
doGet要做的事情,就是根据这次请求,计算出响应.
doGet里的代码,就是根据rep里不同的参数的细节,生成一个具体的resp对象(往空对象里设置属性)
tomcat就会根据这个响应对象,转换成符合HTTP协议的响应报文,返回给浏览器了.
总之,doGet做的事,就是根据请求计算出响应.
一个Servlet程序里,可能有多个Servlet类的.
这些Servlet类,就需要在不同的情况下被执行到了.
当请求的路径中带有hello的时候,才能执行到这个HelloServlet的代码.
不同的Servlet类,就可以关联到不同的路径.
问:
这个代码写完了吗,不需要一个main方法嘛?
- Servlet程序,不需要main方法!!!
- 一个程序,是需要main方法,作为入口.
- 实际上,我们写的这个代码,并不是独立的程序,而是放到Tomcat上执行的.
- main方法其实是在tomcat里的.
- 我们写的这些doGet之类的,都是让Tomcat来调用的.
1.5 打包程序
我们的程序是放在Tomcat上执行的,就需要对程序进行打包.
打成一个Tomcat能识别的包的格式,此时代码才会被Tomcat给加载起来.
打包,也是借助maven完成的.
看到BUILD SUCCESS即为打包成功.
此时,就会出现下述内容.
在maven中,默认打包生成的是jar包,但是tomcat需要的是war包!
此时就需要修改pom.xml,添加一个<packaging>
当然,也可以给打包命名
下面重新打包,就可以把这个war包放到tomcat里了.
1.6 部署
把写好的war包,放到Tomcat上.
具体就是,把这个war包拷贝到Tomcat的webapps的目录中.
启动Tomcat即可.
1.7 验证程序,是否能够正常工作
通过浏览器发起HTTP Get请求,触发刚才的Servle代码.
通过第一级路径,确定一个webapp
通过第二级路径,确定哪个Servlet
通过方法,确定执行Servlet中的哪个方法
2. Smart Tomcat插件
观察上述程序,如果要修改代码,就得重复步骤5,6,7.比较麻烦
这个时候就可以使用Smart Tomcat插件,让IDEA和Tomcat集成起来.
第一次使用Smart Tomcat需要简单配置
点击这里,就可以运行了.
- 这是怎么回事呢?
因为我们前面启动了Tomcat,之前的Tomcat已经占用了8080,一个端口号只能被一个进程绑定.
把之前的Tomcat关闭,再启动:
此时,启动成功.
3. 一些访问出错
3.1 404
浏览器要访问的资源,在服务器上不存在!!!
- ①检查你请求的路径,和你服务器这边的配置,是否一致.
- ②确认你的webapp是否被正确加载.
Smart Tomcat由于只是加载这一个webapp,如果加载失败,就会直接启动失败.
拷贝war的方式,Tomcat要加载多个webapp,如果加载失败,只会有日志.(观察是否有"部署成功")
如果web.xml没有/目录错了/内容错了/名字拼写错了,都可能引起加载失败!!!
3.2 405
①写的doXX方法,和请求发起的方法,是不匹配的.
浏览器发起GET请求,服务器代码写的是doPost
在浏览器地址栏输入URL,发起的是get请求,但是服务器写的是doPost.
②发的是Get请求,服务器写的也是doGet,但是没有把supe.doGet给删了.
3.3 500
服务器内部错误,代码中抛出异常了.
3.4 空白页面
往往是没有执行getWriter.write方法.
3.5 无法访问此网站
这种情况,要么就是Tomcat服务器没有正确运行.
要么就是ip或端口号编写的不对.
4.Servlet API
API就是一组类和方法.
Servlet中的类很多,这里详解其中三个
4.1 HTTPServlet
我们写 Servlet 代码的时候, 首先第一步就是先创建类, 继承自 HttpServlet, 并重写其中的某些方法,让Tomcat去调用到这里的逻辑.
4.1.1 核心方法
|------------------------------|---------------------------------|
| 方法名称 | 方法名称 |
| init | 在 HttpServlet 实例化之后被调用一次 |
| destory | 在 HttpServlet 实例不再使用的时候调用一次 |
| service | 收到 HTTP 请求的时候调用 |
| doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
| doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
| doPut/doDelete/doOptions/... | 收到其他请求的时候调用(由 service 方法调用) |
①init方法:
webapp被加载的时候,执行init,使用这个方法,进行一些初始化操作.
②destory方法:
webapp在被销毁的时候(Tomcat结束)执行destory,使用这个方法进行一些收尾工作.
当然,这个方法不保证能够被调用到.
1.通过8005端口,给Tomcat发起特殊的请求,Tomcat就关闭了.(能够执行destory)
2.直接杀死Tomcat进程.(无法执行到destory了)
③service方法:
每次收到请求,都会执行service,处理每个请求.
往往使用doXX方法替代service.
[面试题]谈谈Servlet的生命周期:
生命周期,更严格的理解"什么阶段,做什么事情".
就像人的幼儿,少年,青年,中年,老年,每个时期要做的事情是不一样的.
1)webapp刚被加载的时候,调用Servlet的init方法.
2)每次收到请求的时候,调用service方法.
3)webapp要结束的时候,调用destroy方法.
4.1.2 示例一:处理请求
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/method")
public class MethodServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doGet");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doPut");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("doDelete");
}
}
针对上述的这三个方法,浏览器只能比较方便的构get请求,不太方便构造其它的.使用ajax或postman.
4.2 HTTPServletRequest
HTTPServletRequest是和HTTP请求数据,是匹配的.
4.2.1 核心方法
|--------------------------------------------|----------------------------------------------|
| 方法 | 描述 |
| String getProtocol() | 返回请求协议的名称和版本。 |
| String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT。 |
| String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请 求的 URL 的一部分。 |
| String getContextPath() | 返回指示请求上下文的请求 URI 部分。 |
| String getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串。 |
| Enumeration getParameterNames() | 返回一个 String 对象的枚举,包含在该请求中包含的参数的名 称。 |
| String getParameter(String name) | 以字符串形式返回请求参数的值,或者如果参数不存在则返回 null。 |
| String[] getParameterValues(String name) | 返回一个字符串对象的数组,包含所有给定的请求参数的值,如 果参数不存在则返回 null。 |
| Enumeration getHeaderNames() | 返回一个枚举,包含在该请求中包含的所有的头名。 |
| String getHeader(String name) | 以字符串形式返回指定的请求头的值。 |
| String getCharacterEncoding() | 返回请求主体中使用的字符编码的名称。 |
| String getContentType() | 返回请求主体的 MIME 类型,如果不知道类型则返回 null。 |
| int getContentLength() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长 度未知则返回 -1。 |
| InputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象. |
①String getRequestURI()方法:
URL唯一资源定位符,描述了网络上的一个资源.
URI唯一资源标识符.
URL也可以理解成URI的一种实现方式.
②Enumeration getParameterNames()和String getParameter(String name)方法:
可以通过一些方式,给服务器传递自定义数据.
1.query string
2.body(通过post form表单的形式提交的请求的话,此时body也是键值对格式)
query string本身就是键值对结构的数据,Tomcat收到这个请求之后,就会把这个query string解析成Map,使用getParameter就可以根据key获取到value.
③Enumeration getHeaderNames()和String getHeader(String name)方法:
获取到请求头里的键值对.
Tomcat收到请求之后也会把请求头解析长Map.
④InputStream getInputStream()方法:
读取这个流对象就能得到body的内容.
Ajax也可以把提交的数据放到query string中,使用getParameter获取.
如果使用ajax POST提交json格式的数据(或者其它非form表单的格式),就需要getInputStream来获取了.
4.2.2 示例一:打印请求信息
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
@WebServlet("/request")
public class RequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//这个操作是必要的,显示告诉浏览器,你拿到的数据是html
resp.setContentType("text/html");
//调用req的各个方法,把得到的结果汇总到一个字符串中,统一返回到页面上.
StringBuilder respBody = new StringBuilder();
//下列内容是在浏览器上按照html的方式来展示的,此时\n在html中并不是换行.
//而使用<br>标签来表示换行
respBody.append(req.getProtocol());
respBody.append("<br>");
respBody.append(req.getMethod());
respBody.append("<br>");
respBody.append(req.getRequestURI());
respBody.append("<br>");
respBody.append(req.getQueryString());
respBody.append("<br>");
//拼接header
Enumeration<String> headers = req.getHeaderNames();//枚举
//迭代器遍历枚举里面的每个部分
while (headers.hasMoreElements()){
String header = headers.nextElement();//取回
respBody.append(header);
respBody.append(header + ": " + req.getHeader(header));//header的key和value
respBody.append("<br>");
}
//返回结果
resp.getWriter().write(respBody.toString());
}
}
如何获取到query string和body的数据?
4.2.3 示例二:获取 GET 请求中的参数
获取query string:
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/parameter")
public class ParameterServlet extends HttpServlet {
//约定,客户端使用query string传递数据
//query string形如: username=zhangshan&password=123
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("username= "+ username);
System.out.println("password= "+ password);
resp.getWriter().write("ok");
}
}
4.2.4 示例三: 获取 POST 请求中的参数
4.2.4.1 获取body(考虑form表单的格式)
form表单:和query string格式意义,也是键值对.
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/parameter2")
public class Parameter2Servlet extends HttpServlet {
//预期让客户端发送一个POST请求,同时使用form格式的数据,在body中把数据传送过来
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("username= "+ username);
System.out.println("password= "+ password);
resp.getWriter().write("ok");
}
}
4.2.4.2 获取body(考虑json格式)
这种格式在开发中非常常见.
Servlet自身不能对json格式的数据进行解析,需要引入第三方库:
java
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
- 那么如何使用ObjectMapper?
Map也叫做映射表.
把一个对象映射到JSON字符串,也可以把JSON字符串映射到对象.
java
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
class User{
public String username;
public String password;
}
@WebServlet("/json")
public class JsonServlet extends HttpServlet {
/**
此处约定客户端body按照json格式来进行传输
* {
* username:"zhangshan"
* password:"123"
* }
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
User user = objectMapper.readValue(req.getInputStream(),User.class);//从字符串到对象
System.out.println("username= " + user.username +", password= "+user.password);
//也可以java对象转成json字符串
String userString = objectMapper.writeValueAsString(user);
System.out.println("userString: "+userString);
resp.getWriter().write("ok");
}
}
这个方法有很多版本,作用就是把json字符串给解析成JAVA对象.
其中这里第一个参数,是一个流对象,也就表示json从哪里来读.
请求的body是通过getlnputStream得到流对象,进一步读出来的.
请求的第二个参数,则是指定的类型.当前你这边得到的json字符串,需要转成什么样的一个java对象,需要指定一下对象的类型.
就可以定义一个类,让这个类里的属性的名字和类型都和json字符串匹配.
这里必须先知道json的结构是什么,才能够根据json结构,构造出User对象.
readValue就把req的body中的字符串读取并解析了,然后构造成了User对象,User对象中的属性,就是前面json中所体现的内容.
- 在这里写json格式的时候,key也加上了",但是之前在注释里没写?
在ison格式中,key一定是字符串类型,所以原则上,key不写"也是完全可以的.
但是有些库/程序,检查更加严格,就必须强制写".
js的ajax方式构造json,此时key是没有引号的.
但是使用postman构造的json,此时key就需要引号.
- 此处的public能不能写成private?
不能,会出现500.
本身,jackson会通过反射的方式,把User类里包含的public的属性给获取到.
此时,就可以根据反射这里得到的"属性名字",去json解析出来的键值对中进行匹配.
如果匹配到了,就把value设置到刚才得到的属性中.
由于把username改成了private,而Jackson并不会直接针对private属性进行扫描,username就不认识了.
当然,提供对应的getter和setter方法就可以写成private
[重点理解]jackson的readValue工作过程:
- 先把json字符串解析成键值对,放到Map中.
- 再根据参数填入的类对象,通过反射API就可以知道,这个类里面有什么属性,每个属性的名字和类型.
- 一次把这里的每个属性都取出来,通过属性名字查询上述的Map,把得到的值,赋给这个类的属性.
4.3 HTTPServletResponse
HTTPServletResponse同样也是和HTTP响应数据,是匹配的.
针对状态码,各种header,body这些属性,就可以进行"设置".
- 请求对象,我们拿到之后的目的,是为了获取里面的属性(读).
- 响应对象, 我们拿到之后的目的,是为了设置里面的属性(写).
对于doXX这样的方法来说,本身要做的事情就是"根据请求计算响应".
- 请求对象,是Tomcat收到请求之后,对HTTP协议解析得到的对象.
- 响应对象,是Tomcat创建的空的对象,我们在代码中把响应对象的属性设置好.(响应对象,相当于是一个输出型参数)
4.3.1 核心方法
|-------------------------------------------|--------------------------------------------------------|
| 方法 | 描述 |
| void setStatus(int sc) | 为该响应设置状态码。 |
| void setHeader(String name, String value) | 设置一个带有给定的名称和值的 header. 如果 name 已经存在, 则覆盖旧的值. |
| void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header. 如果 name 已经存在, 不覆盖旧的值, 并列添加新的键值对 |
| void setContentType(String type) | 设置被发送到客户端的响应的内容类型。 |
| void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如, UTF-8。 |
| void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端。 |
| PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
| OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据. |
①void setCharacterEncoding(String charset)方法:
这里是告诉浏览器,要按照什么样的字符集来解析响应的body.如果不去描述清楚,可能浏览器展示的内容就会乱码.
②void addHeader(String name, String value)方法:
使用addHeader,header中可能出现,key相同的两个键值对.
一般来说,约定键值对中的key是唯一的,但实践中,有些情况,确实也会需要key不唯一.
③void sendRedirect(String location)方法:
这个是特殊的方法,用来设置"重定向"响应.
4.3.2 示例一:设置状态码
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/status")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(200);
//resp.sendError(404);
}
}
其实可以在返回状态码的同时,给body写入数据,就可以得到一些"个性化的错误页面".
resp.sendError(404):
4.3.3 示例二:重定向
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/redirect")
public class RedirectServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//让页面被重定向到 搜狗主页
//resp.setStatus(302);
//重定向响应,一定腰带有Location属性,
//resp.setHeader("Location","http://www.sogou.com");
resp.sendRedirect("http://www.sogou.com");
}
}
4.3.4 示例三:自动刷新
可以使用setHeader设置任意的响应报头.
通过refresh属性,设置浏览器自动刷新.
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/refresh")
public class RefreshServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setHeader("refresh","2");
//返回系统时间,方便再次观察
resp.getWriter().write("time: "+ System.currentTimeMillis());
}
}
设置这个属性之后,浏览器就会每隔两秒自动刷新一次!!!
但这里的刷新的间隔也不是精准的2000ms,会比2000稍微多点.
必将,浏览器发起请求,服务器响应,知道页面被解析出来,都是需要消耗一定的时间的.
4.3.5 示例四:编码方式匹配
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/body")
public class BodyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//让服务器返回一个html数据
resp.getWriter().write("<div>你好<div>");
}
}
- 这是怎么回事?
浏览器,默认会跟随系统的编码.
windows简体中文版,默认的编码是gbk.
一般在IDEA里面直接写一个中文字符串,就是utf8的编码.
拿着utf8的数据,浏览器按照gbk的方式来解析,势必就会出现乱码!!!
解决乱码的原则,就是编码方式匹配.
java
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/body")
public class BodyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//设置响应的字符集
resp.setContentType("text/html;charset=utf8");
//让服务器返回一个html数据
resp.getWriter().write("<div>你好<div>");
}
}
字符集,其实是ContentType的一部分.
这里要注意,给resp设置属性的时候,必须要注意顺序!!
先设置header,后设置body.
一旦开始设置body,就相当于header和status都定型了,已经来不及修改了.