4. HTTP
4.1 HTTP简介
HTTP:HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则
- 数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。
- 打开浏览器,点击
F12
打开开发者工具,点击Network
,然后搜索baidu.com,就可以看到本次的请求数据的格式
HTTP协议特点
-
基于TCP协议: 面向连接,安全
-
基于请求-响应模型的: 一次请求对应一次响应
-
HTTP协议是无状态协议: 无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会导致一些问题,java将使用
会话技术(Cookie、Session)
来解决这个问题
4.2 请求数据格式
请求数据总共分为3部分内容,分别是请求行、请求头、请求体
-
请求行
HTTP请求中的第一行数据,包含3块内容,分别是
请求方式有8种,最常用的是:GET, POST, PUT, DELETE -
请求头
第二行开始,格式为key: value形式
请求头中会包含若干个属性,常见的HTTP请求头有- Host: 表示请求的主机名 - User-Agent: 浏览器版本,例如Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...) like Gecko; - Accept:表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有; - Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页; - Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。
-
请求体
POST请求
的最后一部分,存储请求参数请求体和请求头之间是有一个空行隔开
- GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中
- GET请求请求参数大小有限制,POST没有
4.3 响应数据格式
响应数据总共分为3部分内容,分别是响应行、响应头、响应体
-
响应行
响应数据的第一行,响应行包含3块内容,分别是HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
-
响应头
第二行开始,格式为key:value形式
响应头中会包含若干个属性,常见的HTTP响应头有:Content-Type:表示该响应内容的类型,例如text/html,image/jpeg; Content-Length:表示该响应内容的长度(字节数); Content-Encoding:表示该响应压缩算法,例如gzip; Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存3
-
响应体
最后一部分。存放响应数据
上图中<html>...</html>
这部分内容就是响应体,它和响应头之间有一个空行隔开。
4.4 状态码
状态码分类 | 说明 |
---|---|
1xx | 响应中——临时状态码,表示请求已经接受,告诉客户端应该继续请求或者如果它已经完成则忽略它 |
2xx | 成功——表示请求已经被成功接收,处理已完成 |
3xx | 重定向——重定向到其它地方:它让客户端再发起一个请求以完成整个处理。 |
4xx | 客户端错误——处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
5xx | 服务器端错误——处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 |
状态码大全:https://cloud.tencent.com/developer/chapter/13553
常见响应码
状态码 | 英文描述 | 解释 |
---|---|---|
200 | OK | 客户端请求成功,即处理成功,这是我们最想看到的状态码 |
302 | Found | 指示所请求的资源已移动到由Location 响应头给定的 URL,浏览器会自动重新访问到这个页面 |
304 | Not Modified | 告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 |
400 | Bad Request | 客户端请求有语法错误,不能被服务器所理解 |
403 | Forbidden | 服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 |
404 | Not Found | 请求资源不存在,一般是URL输入有误,或者网站资源被删除了 |
428 | Precondition Required | 服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
429 | Too Many Requests | 太多请求,可以限制客户端请求某个资源的数量,配合 Retry-After(多长时间后可以请求)响应头一起使用 |
431 | Request Header Fields Too Large | 请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。 |
405 | Method Not Allowed | 请求方式有误,比如应该用GET请求方式的资源,用了POST |
500 | Internal Server Error | 服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 |
503 | Service Unavailable | 服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
511 | Network Authentication Required | 客户端需要进行身份验证才能获得网络访问权限 |
5. Tomcat
Tomcat:是Apache软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范
Web服务器:安装在服务器端的一款软件,把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了;
Web服务器的作用:(1)封装HTTP协议操作,简化开发;(2)可以将web项目部署到服务器中,对外提供网上浏览服务
5.1 安装和使用
-
下载
官网地址:https://tomcat.apache.org/,在Download下选择8.5版本
-
安装:直接解压即可(注意解压地址不要有中文或空格)
-
卸载:直接删除目录即可
-
启动
双击bin目录下的start.bat文件即可(注意启动后不要关闭命令行窗口)
启动后在浏览器输入:http://localhost:8080 即可看到tomcat页面- 修改日志编码
cmd窗口启动界面有很多乱码,这是因为tomcat默认输出日志编码是utf-8,而cmd默认编码是gbk
可以修改conf目录下的logging.properties
- 修改日志编码
-
关闭
关闭有三种方式- 直接x掉运行窗口:强制关闭(不建议)
- bin\shutdown.bat:正常关闭
ctrl+c
: 正常关闭
-
配置
-
修改启动端口号
Tomcat默认的端口是8080,要想修改Tomcat启动的端口号,需要修改 conf/server.xml
注: HTTP协议默认端口号为80,如果将Tomcat端口号改为80,则将来访问Tomcat时,将不用输入端口号。
-
端口号冲突
java报错:Address already in use: bind
-
-
部署
-
Tomcat部署项目: 将项目放置到
webapps
目录下,即部署完成
将 资料/2. Tomcat/hello 目录拷贝到Tomcat的webapps目录下
通过浏览器访问 http://localhost/hello/a.html,能看到内容就说明项目已经部署成功随着项目的增大,项目中的资源也会越来越多,项目在拷贝的过程中也会越来越费时间,该如何解决呢?
-
一般JavaWeb项目会被打包称
war包
,然后将war包放到webapps目录下,Tomcat会自动解压缩war文件
将 资料/2. Tomcat/haha.war 目录拷贝到Tomcat的webapps目录下
Tomcat检测到war包后会自动完成解压缩,在webapps目录下就会多一个haha目录
通过浏览器访问 http://localhost/haha/a.html,能看到内容就说明项目已经部署成功war包可以通过IDEA生成
-
5.2 创建MavenWeb项目
MavenWeb项目结构
-
MavenWeb项目结构:开发中的项目
-
MavenWeb项目结构:开发完成,可以部署的项目
- 开发项目通过执行Maven打包命令
package
,可以获取到部署的Web项目目录 - 编译后的Java字节码文件和resources的资源文件,会被放到WEB-INF下的classes目录下
- pom.xml中依赖坐标对应的jar包,会被放入WEB-INF下的lib目录下
- 开发项目通过执行Maven打包命令
IDEA创建MavenWeb项目
-
使用骨架创建(不推荐)
生成的目录如下
其中
pom.xml
将自动生成packaging
标签其中
web.xml
文件将自动生成
其中
main
目录下如果缺失java
文件夹和resources
文件夹,自行创建
注意,如果创建时没有提示的话,创建完普通文件夹后需要右键,选择Mark Directory as
-
不使用骨架创建(推荐)
先创建普通的maven项目然后添加web相关文件
在pom.xml中添加packaging标签,并刷新(一定要刷新)
<packaging>war</packaging>
自动生成webapp目录
注意webapp路径是在src/main之下
生成web.xml文件
注意这里web.xml路径需要设置在src\main\webapp下
5.3 IDEA使用Tomcat
部署项目需要使用 mvn package将项目打包成war包,然后放到tomcat的webapps目录下,但是这样对于不利于代码修改
可以将tomcat集成到IDEA中,这样就不用每次修改都需要打包一次了
集成本地Tomcat
此方式比较麻烦,可以用后面的插件方式
到这里就部署成功了,下面通过a.html验证
Tomcat Maven插件
-
在pom.xml中添加Tomcat插件
<build> <plugins> <!--Tomcat插件 --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> </plugin> </plugins> </build>
-
使用Maven Helper插件快速启动项目,选中项目,右键–>Run Maven --> tomcat7:run
-
修改端口号和路径
<build> <plugins> <!--Tomcat插件 --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <port>8080</port> <path>/</path> </configuration> </plugin> </plugins> </build>
6. Servlet
Servlet是JavaWeb最为核心的内容
6.1 Servlet快速入门
-
(1)创建Web项目,导入Servlet依赖坐标
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <!-- provided指的是在编译和测试过程中有效,最后生成的war包时不会加入 因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时候生效就会和Tomcat中的jar包冲突,导致报错 --> <scope>provided</scope> </dependency>
-
(2)创建: 定义一个类,实现Servlet接口,并重写接口中所有方法
重点是
service()
方法package org.example.web; @WebServlet("/demo1")//设置访问路径 public class ServletDemo1 implements Servlet { //需要重写这5个方法 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { //访问时service方法会被自动执行 System.out.println("servlet hello world"); } public void init(ServletConfig servletConfig) throws ServletException { } public ServletConfig getServletConfig() { return null;} public String getServletInfo() { return null;} public void destroy() { } }
@WebServlet注解,配置该Servlet的访问路径
-
(3)访问: 启动Tomcat,浏览器中输入URL地址访问该Servlet
可以看到控制台输出了service()方法里的内容
6.2 Servlet执行流程
- 浏览器发出
http://localhost:8080/tomcat-demo2/demo1
请求,从请求中可以解析出三部分内容,分别是localhost:8080
、tomcat-demo2
、demo1
- 根据
localhost:8080
可以找到要访问的Tomcat Web服务器 - 根据
tomcat-demo2
可以找到部署在Tomcat服务器上的web-demo项目 - 根据
demo1
可以找到要访问的是项目中的哪个Servlet类,根据@WebServlet后面的值进行匹配
- 根据
- 找到ServletDemo1这个类后,Tomcat Web服务器就会为ServletDemo1这个类创建一个对象,然后调用对象中的service方法
- ServletDemo1实现了Servlet接口,所以类中必然会重写service方法供Tomcat Web服务器进行调用
- service方法中有ServletRequest和ServletResponse两个参数,ServletRequest封装的是请求数据,ServletResponse封装的是响应数据,后期可以通过这两个参数实现前后端的数据交互
Servlet由web服务器创建,Servlet方法由web服务器调用
6.3 Servlet生命周期
思考:Tomcat什么时候创建的Servlet对象?
Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段
-
加载和实例化
默认情况,Servlet会在第一次访问被容器创建,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,因此可以设置将Servlet的创建放到服务器启动的时候来创建,使用
loadOnStartup
来配置@WebServlet(urlPatterns = "/demo1", loadOnStartup = 1)
loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高 -
初始化
在Servlet实例化之后,容器将调用Servlet的
init()
方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次
-
请求处理
每次请求Servlet时,Servlet容器都会调用Servlet的
service()
方法对请求进行处理 -
服务终止
当需要释放内存或者容器关闭时,容器就会调用Servlet实例的
destroy()
方法完成资源的释放
在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收
演示
-
init()
@WebServlet("/demo2") public class ServletDemo2 implements Servlet { /** * init:初始化方法 * 调用实际:默认情况下,在Servlet第一次被访问时调用 * 调用次数:1次 */ public void init(ServletConfig servletConfig) throws ServletException { System.out.println("init ..."); } public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { System.out.println("servlet hello world"); } ... }
多次访问
http://localhost:8080/tomcat-demo2/demo2
,最终结果如下
-
loadOnStartup
@WebServlet(urlPatterns = "/demo2", loadOnStartup = 1)
此时,不访问地址,只要启动项目即可看到 init() 方法已被执行
-
-
destroy()
@WebServlet(urlPatterns = "/demo2", loadOnStartup = 1) public class ServletDemo2 implements Servlet { ... /** * destroy:销毁方法 * 调用时机:内存释放或服务器关闭的时候,Servlet对象会被销毁,destroy()被调用 * 调用次数:1次 */ public void destroy() { System.out.println("destroy ..."); } ... }
这里测试时,发现如果设置了loadOnStartup,上面强制关闭也可以看到destroy()被执行
正常关闭可以通过IDEA下方的Terminal窗口
mvn tomcat7:run //启动 ctrl+c //正常关闭
6.4 Servlet方法介绍
- init
void init(ServletConfig config)
- service
void service(ServletRequest req, ServletResponse res)
- destroy
void destroy()
- getServletInfo:获取Servlet信息
String getServletInfo()
//该方法用来返回Servlet的相关信息,没有什么太大的用处,一般返回一个空字符串即可
public String getServletInfo() {
return "";
}
- 获取ServletConfig对象
ServletConfig getServletConfig()
ServletConfig对象,在init方法的参数中有,而Tomcat Web服务器在创建Servlet对象的时候会调用init方法,必定会传入一个ServletConfig对象,只需要将服务器传过来的ServletConfig进行返回即可。具体操作如下
@WebServlet(urlPatterns = "/demo3",loadOnStartup = 1)
public class ServletDemo3 implements Servlet {
private ServletConfig servletConfig;
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
System.out.println("init...");
}
public ServletConfig getServletConfig() {
return servletConfig;
}
...
}
6.5 Servlet体系结构
-
HttpServlet的使用步骤
- 继承HttpServlet
- 重写doGet和doPost方法
-
HttpServlet原理
- 获取请求方式,并根据不同的请求方式,调用不同的doXxx方法
HttpServlet
-
编写Servlet.java
package org.example.web; @WebServlet("/demo4") public class ServletDemo4 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ System.out.println("get ..."); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ System.out.println("post ..."); } }
-
Get方式演示
默认方法是get方法,只需要启动,然后进入http://localhost:8080/tomcat-demo2/demo4
即可看到控制台输出了 get … -
Post方式演示
在webapp新建a.html,创建表单<form action="/tomcat-demo2/demo4" method="post"> <input name="username"><input type="submit"> </form>
在输入地址,即可看到控制台输出了 post …
模拟HttpServlet
前面讲过,get方式和post方式的请求体存放位置不同,如果使用原始的Servlet方法,需要对请求方式进行判断,然后分别进行处理
-
原始Servlet处理get请求和post请求
需要重写service方法
package org.example.web; public class ServletDemo5 implements Servlet { @Override public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { //根据请求方式,进行分别的处理 HttpServletRequest request = (HttpServletRequest) req; //1. 获取请求方式 String method = request.getMethod(); //2. 判断 if("GET".equals(method)){ //get方式的处理逻辑 }else if("POST".equals(method)){ //post方式处理逻辑 } } ... }
这样每次使用Servlet都需要复写这段代码,于是将其抽象出一个类MyHttpServlet
package org.example.web; public class MyHttpServlet implements Servlet { @Override public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException { //根据请求方式,进行分别的处理 HttpServletRequest request = (HttpServletRequest) req; //1. 获取请求方式 String method = request.getMethod(); //2. 判断 if("GET".equals(method)){ //get方式的处理逻辑 doGet(req, resp); }else if("POST".equals(method)){ //post方式处理逻辑 doPost(req, resp); } } //protected表示子类可见 protected void doGet(ServletRequest req, ServletResponse resp) { } protected void doPost(ServletRequest req, ServletResponse resp) { } ... }
此后写的Servlet只需要extends MyHttpServlet即可
package org.example.web; @WebServlet("/demo5") public class ServletDemo5 extends MyHttpServlet { @Override protected void doGet(ServletRequest req, ServletResponse resp){ System.out.println("get ..."); } @Override protected void doPost(ServletRequest req, ServletResponse resp){ System.out.println("post ..."); } }
访问
http://localhost:8080/tomcat-demo2/demo5
可以看到能正常输出 get …HttpServlet就是实现了Servlet的各种方法,如封装了对各种请求方式判断的service方法
6.6 Servlet urlPatterns配置
-
配置多个访问路径:urlPatterns
@WebServlet(urlPatterns = { "/demo5_1", "/demo5_2"})
-
urlPatterns配置规则
-
精确匹配
配置路径:@WebServlet("/user/select") 访问路径:http://localhost:8080/tomcat-demo2/user/select
-
目录匹配
配置路径:@WebServlet("/user/*") 访问路径:http://localhost:8080/tomcat-demo2/user/abb 访问路径:http://localhost:8080/tomcat-demo2/user/bbb 只要是 http://localhost:8080/tomcat-demo2/user 下的路径都可以访问
当一个路径同时满足精确匹配和目录匹配时,精确匹配优先级更高
-
扩展名匹配
配置路径:@WebServlet("*.do") 访问路径:http://localhost:8080/tomcat-demo2/aaa.do 访问路径:http://localhost:8080/tomcat-demo2/user/bbb.do 只要以 http://localhost:8080/tomcat-demo2/.../.do 的路径名访问都可以
-
任意匹配
配置路径:@WebServlet("/") 或 @WebServlet("/*") 只要以 http://localhost:8080/tomcat-demo2/... 的路径名访问都可以
/
和/*
的区别- 当项目中的Servlet配置了 “/”,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet
- 当我们的项目中配置了"/*",意味着匹配任意访问路径
在
tomcat的conf
中有一个web.xml
配置文件,里面配置了DefaultServlet
DefaultServlet是用来处理静态资源(如a.html),如果配置了"/"会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问
-
6.7 XML配置Servlet
3.0版本后开始支持注解配置,3.0版本前只支持XML配置文件的配置方法
步骤:
-
编写Servlet类
public class ServletDemo6 extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println("get ..."); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{ } }
-
在web.xml中配置该Servlet
<!-- 1. 配置Servlet 全类名 --> <servlet> <servlet-name>demo6</servlet-name> <servlet-class>org.example.web.ServletDemo6</servlet-class> </servlet> <!-- 2. 配置Servlet 访问路径 --> <servlet-mapping> <servlet-name>demo6</servlet-name> <url-pattern>/demo6</url-pattern> </servlet-mapping>
7. Request 和 Response
- request: 获取请求数据
- 浏览器会发送HTTP请求到后台服务器[Tomcat]
- HTTP的请求中会包含很多请求数据[请求行+请求头+请求体]
- 后台服务器[Tomcat]会对HTTP请求中的数据进行解析并把解析结果存入到一个对象中
- 所存入的对象即为request对象,所以我们可以从request对象中获取请求的相关参数
- 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
- response: 设置响应数据
- 业务处理完后,后台就需要给前端返回业务处理的结果即响应数据
- 把响应数据封装到response对象中
- 后台服务器[Tomcat]会解析response对象,按照[响应行+响应头+响应体]格式拼接结果
- 浏览器最终解析结果,把内容展示在浏览器给用户浏览
示例:
package org.example.web;
@WebServlet("/demo")
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
//使用request对象获取请求数据
String name = request.getParameter("name");//url?name=zhangsan
//使用request对象设置响应数据
response.setHeader("content-type", "text/html;charset=utf-8");
response.getWriter().write("<h11>"+name+",欢迎您!</h1>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
}
}
7.1 Request
Request继承体系
- 当Servlet类实现的是Servlet接口的时候,service方法中的参数是ServletRequest和ServletResponse
- 当Servlet类继承的是HttpServlet类的时候,doGet和doPost方法中的参数就变成HttpServletRequest和HttpServletReponse
ServletRequest和HttpServletRequest都是接口,实现类RequestFacade由Tomcat提供,并进行对象的创建
-
Tomcat需要解析请求数据,封装为request对象,并且创建request对象传递到service方法中
System.out.println(request); //输出: org.apache.catalina.connector.RequestFacade@c4a2f23
Request请求数据
-
获取请求行数据
请求行包含三块内容,分别是
请求方式
、请求资源路径
、HTTP协议及版本
对于请求行:GET /request-demo/req1?username=zhangsan HTTP/1.1
//获取请求方式: GET String getMethod() //获取虚拟目录(项目访问路径):/request-demo String getContextPath() //获取URL(统一资源定位符): http://localhost:8080/request-demo/req1 StringBuffer getRequestURL() //获取URI(统一资源标识符): /request-demo/req1 String getRequestURI() //获取请求参数(GET方式): username=zhangsan&password=123 String getQueryString()
-
获取请求头数据
对于请求头的数据,格式为
key: value
如下:User-Agent: Mozilla/5.0 Chrome/91.0.4472.106
//获取请求头: user-agent: 浏览器的版本信息 String getHeader(String name)
示例:
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ String agent = request.getHeader("user-agent"); System.out.println(agent); }
-
获取请求体数据
只有
POST
有请求体对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:-
获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
ServletInputStream getInputStream()
-
获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
BufferedReader getReader()
示例:
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //1. 获取字符输入流 BufferedReader br = request.getReader(); //2. 读取数据 String line = br.readLine(); System.out.println(line); } //例如,在a.html中输入admin,就可以获取到:username=admin
-
Request通用方式获取请求参数
-
GET方式
String getQueryString()
-
POST方式
BufferedReader getReader();
仅仅由于请求方式不同,doGet()
和doPost()
中将有大量重复代码
解决方法如下:在doPost()中只写一行代码,即调用doGet()
@WebServlet("/demo")
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
//代码
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
doGet(request, response);
}
}
GET请求方式和POST请求方式,获取请求参数的方式不一样,解决方法:
-
方法一(不推荐)
@WebServlet("/demo") public class ServletDemo extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //获取请求方式 String method = request.getMethod(); //获取请求参数 String params = ""; if("GET".equals(method)){ params = request.getQueryString(); }else if("POST".equals(method)){ BufferedReader reader = request.getReader(); params = reader.readLine(); } //将请求参数进行打印控制台 System.out.println(params); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ this.doGet(request, response); } }
每个Servlet中都要写这样的逻辑,不简便
-
方法二(常用)
Request已经封装好了一个Map<String, String[]>集合,用于存储所有参数
//获取所有参数Map集合 Map<String,String[]> getParameterMap() //根据名称获取参数值(数组) String[] getParameterValues(String name) //根据名称获取参数值(单个值) String getParameter(String name)
示例:
-
a.html
<form action="/tomcat-demo2/demo" method="post"> <input type="text" name="username"><br> <input type="password" name="password"><br> <input type="checkbox" name="hobby" value="1"> 游泳 <br> <input type="checkbox" name="hobby" value="2"> 爬山 <br> <input type="submit"> </form>
-
ServletDemo.java
@WebServlet("/demo") public class ServletDemo extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ //1. 获取所有参数的Map集合 Map<String, String[]> map = request.getParameterMap(); for(String key : map.keySet()){ System.out.print(key+":"); //获取值 String[] values = map.get(key); for (String value : values) { System.out.print(value + " "); } System.out.println(); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ this.doGet(request, response); } }
可以看到用get和post请求方式(通过a.html)都是可以获取到的
-
IDEA快速创建Servlet
(1)右键文件夹web --> New —> Servlet
(2)模板中doPost()和doGet都为空,可以修改模板:File --> Settings --> Editor --> File and code template --> Other --> Web --> Java code templates --> Servlet Annotated Class.java,即可修改默认模板
请求参数中文乱码问题
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
System.out.println(username);
}
如果访问http://localhost:8080/tomcat-demo2/servlet?username=张三
,无论是POST还是GET方式,中文都将乱码
GET方式下乱码原因
乱码原因:浏览器需要将"张三"发送给Tomcat服务器,但浏览器不支持中文,因此将"张三"变成URL编码"%E5%BC%A0%E4%B8%89"进行发送
发送时是将UTF-8编码转换为URL编码,但Tomcat在接收时会将URL编码默认解码为ISO-8859-1编码,因此出现乱码
注意这里的ISO-8859-1编码是写死的,目前没有提供修改的方式
URL编码
1. 将字符串按照编码方式转为二进制
2. 每个字节转为2个16进制并在前边加上%
3. 例如:UTF-8下一个汉字占三个字节,"张三"的二进制为 1110 0101 1011 1100 1010 0000 1110 0100 1011 1000 1000 1001,转为URL编码就是"%E5%BC%A0%E4%B8%89"
String username = "张三";
//1. URL编码
String encode = URLEncoder.encode(username, "utf-8");//将utf-8转为URL
System.out.println(encode);//输出:%E5%BC%A0%E4%B8%89
//2. URL解码
String decode = URLDecoder.decode(encode, "ISO-8859-1");//将URL转为ISO-8859-1
System.out.println(decode);//输出:乱码
//3. 转换为字节数组
byte[] bytes = decode.getBytes("ISO-8859-1");
//4. 将字节数组转为字符串
String s = new String(bytes, "utf-8");
System.out.println(s);//输出:张三
! 不管是utf-8还是URL或者是ISO-8859-1编码,它们的二进制是一样的
POST和GET乱码解决方法
-
POST:设置输入流的编码
//1. POST解决乱码:getReader() request.setCharacterEncoding("UTF-8");//只能解决POST方式下的乱码 //GET使用getQueryString(),而不通过getReader()流的方式来设置编码,因此上述设置对GET无效
-
通用方式(GET/POST):先编码,再解码
username = new String(username.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8);
Tomcat 8.0之后,已经将GET请求乱码问题解决,设置默认的解码方式为UTF-8
Request请求转发
请求转发(forward): 一种在服务器内部的资源跳转方式
实现方式
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//将请求转发给req2
request.getRequestDispatcher("/req2").forward(request, response);
}
请求转发资源间共享数据:使用Request对象
//存储数据到request域中
void setAttribute(String name,Object o);
//根据key获取值
Object getAttribute(String name);
//根据key删除该键值对
void removeAttribute(String name);
示例:在请求转发过程中添加参数
req1转发给req2
//在req1中
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setAttribute("msg", "hello");
request.getRequestDispatcher("/req2").forward(request, response);
}
//在req2中
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object msg = request.getAttribute("msg");
System.out.println(msg);
}
请求转发的特点
- 浏览器地址栏路径不发生变化
- 只能转发到当前服务器的内部资源
- 一次请求,可以在转发资源间使用request共享数据。即 请求 --> req1 --> req2 --> 响应 是一次请求;
7.2 Response
Response设置响应数据
-
响应行
HTTP/1.1 200 OK 分别表示HTTP协议及版本 响应状态码 状态码的描述
//设置响应状态码 void setStatus(int sc);
-
响应头
Content-Type:text/html 键值对
//设置响应头键值对 void setHeader(String name,String value);
-
响应体
即html等,对于响应体,是通过字符、字节输出流的方式往浏览器写
-
获取字符输出流
PrintWriter getWriter();
-
获取字节输出流
ServletOutputStream getOutputStream();
-
Response重定向
重定向(Redirect):一种资源跳转方式
实现方式
resp.setStatus(302);
resp.setHeader("location", "资源B的路径");
response.sendRedirect("资源B的路径");
示例:从resp1重定向到resp2
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1");
//1. 设置响应状态码:302
response.setStatus(302);
//2. 设置响应头:location
response.setHeader("Location", "/tomcat-demo2/resp2");//需要加虚拟路径
}
//最终resp1和resp2都被访问了
简化方式(常用)
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("resp1");
response.sendRedirect("/tomcat-demo2/resp2");
}
重定向的特点
- 浏览器地址栏路径发生变化
- 可以重定向到任何位置的资源(服务器内部、外部均可)
- 两次请求,不能在多个资源使用request共享数据
动态获取虚拟机路径
String contextPath = request.getContextPath();
response.sendRedirect(contextPath+"/resp2");
Response响应字符数据
使用
- 通过Response对象获取字符输出流:
PrintWriter writer = resp.getWriter();
- 通过字符输出流写数据:
writer.write("aaa");
示例
PrintWriter writer = response.getWriter();
response.setHeader("content-type", "text/html");//告诉浏览器,响应的格式是html格式(新版本即使不加这句,也能成功打印出html格式)
writer.write("<h1>aaaa</h1>");
注意事项
- 一次请求响应结束后,writer流对象会随着response对象一起被销毁掉,所以不用手动关闭流
- 中文会乱码
Response响应字节数据
基础写法
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件,如a.jpg
FileInputStream fis = new FileInputStream("e://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf)) != -1)
os.write(buf, 0, len);
fis.close();
}
工具类写法
导入io相关依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
使用commons-io
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 读取文件,如a.jpg
FileInputStream fis = new FileInputStream("e://a.jpg");
//2. 获取response字节输出流
ServletOutputStream os = response.getOutputStream();
//3. 完成流的copy
IOUtils.copy(fis, os);
fis.close();
}
中文乱码问题
默认编码是ISO-8859-1,所以乱码;修改方式如下:
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("<h1>a是吗a</h1>");
7.3 路径问题
判断依据
- 浏览器使用:需要加虚拟目录(项目访问路径)
- 服务端使用:不需要加虚拟目录
request请求转发是在服务端内部,因此不用加虚拟路径
response重定向的路径实际是第二次请求时的访问路径,是给浏览器使用的,因此需要加虚拟路径
常见场景
- <a href='路径'> 需要加虚拟目录
- <form action='路径'> 需要加虚拟目录
- req.getRequestDispatcher("路径") 不需要加虚拟目录
- resp.sendRedirect("路径") 需要加虚拟目录
7.4 案例:注册和登录
准备工作
前端和数据库
- 前端页面:login.html, register.html
- 数据库中创建:tb_user表
后端
-
创建User类
package org.example.pojo; public class User { private Integer id; private String username; private String password; //这里省略getter, setter, toString方法 }
-
导入MyBatis和MySQL驱动坐标
<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency>
注意,如果是mysql 8.0版本,那么这里的mysql-connector-java版本不能太低,否则会连接不上
-
创建mybatis-config.xml核心配置文件
这里建议增加一个mybatis-config.xml的模板
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <typeAliases> <package name="org.example.pojo"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <!-- 在xml中不识别&,因此需要使用转义字符& --> <property name="url" value="jdbc:mysql:///mybatis?useSSL=false&useServerPrepStmts=true"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <package name="org.example.mapper"/> </mappers> </configuration>
-
创建UserMapper.xml配置文件
注意路径是在resource下,和之后的UserMapper路径要相对应,这里是resources/org/example/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.example.mapper.UserMapper"> </mapper>
-
创建UserMapper接口
package org.example.mapper; public interface UserMapper { }
代码实现
- 登录功能
-
UserMapper接口
package org.example.mapper; public interface UserMapper { /** * 根据用户名和密码查询用户 * @param username * @param password * @return */ @Select("select * from tb_user where username = #{username} and password = #{password}") User select(@Param("username") String username, @Param("password") String password); }
-
LoginServlet.java
注意这里要修改login.html里面的表单
action
和method
属性,即
<form action="/tomcat-demo2/loginServlet" method="post" id="form">
package org.example.web; @WebServlet("/loginServlet") public class LoginServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); //1. 接收用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); //2. 调用MyBatis完成查询 //2.1 获取SqlSessionFactory对象 String resource = "mybatis-config.xml";//在resource根目录下,这样写就可以了 InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.2 获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //2.3 获取Mapper UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //2.4 调用方法 User user = userMapper.select(username, password); //2.5 释放资源 sqlSession.close(); //获取字符输出流,并设置content-type response.setContentType("text/html;charset=utf-8"); PrintWriter writer = response.getWriter(); //3. 判断是否登录成功 if(user != null){ writer.write("登录成功"); }else{ writer.write("登录失败"); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
在浏览器输入:
http://localhost:8080/tomcat-demo2/login.html
,即可正常登录
-
注册功能
要求:用户名不能重复
-
UserMapper接口
package org.example.mapper; public interface UserMapper { /** * 根据用户名和密码查询用户 * @param username * @param password * @return */ @Select("select * from tb_user where username = #{username} and password = #{password}") User select(@Param("username") String username, @Param("password") String password); /** * 根据用户名查询用户对象 * @param username * @return */ @Select("select * from tb_user where username = #{username}") User selectByUsername(String username); /** * 增加用户对象 * @param user */ @Insert("insert into tb_user values(null, #{username}, #{password})") void add(User user); }
-
registerServlet.java
同样需要先修改register.html表单提交位置和方法
package org.example.web; @WebServlet("/registerServlet") public class RegisterServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); //1. 接收用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); //封装对象 User user = new User(); user.setUsername(username); user.setPassword(password); //2. 调用mapper,根据用户名查询对象 //2.1 获取SqlSessionFactory对象 String resource = "mybatis-config.xml";//在resource根目录下,这样写就可以了 InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.2 获取SqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //2.3 获取Mapper UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //2.4 调用方法 User u = userMapper.selectByUsername(username); //3. 判断用户对象是否为null if(u == null){ //用户名不存在,添加用户 userMapper.add(user); //提交事务 sqlSession.commit(); //释放资源 sqlSession.close(); }else{ response.setContentType("text/html;charset=utf-8"); response.getWriter().write("用户名已存在"); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.doGet(request, response); } }
7.5 代码优化:SqlSessionFactory工具类抽取
问题
- LoginServlet 和 RegisterServlet 创建 SqlSession 时代码重复,重复代码不利于后期的维护
解决:工具类 - SqlSessionFactory工厂类进行重复创建,每次都会占用一个数据库池,资源浪费
解决:静态代码块
SqlSessionFactory工具类抽取
package org.example.util;
public class SqlSessionFactoryUtils {
private static SqlSessionFactory sqlSessionFactory;
static {
//静态代码块随着类的加载自动执行,并且只执行一次
try {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSessionFactory getSqlSessionFactory(){
return sqlSessionFactory;
}
}
然后只需要将 LoginServlet 和 RegisterServlet 中对应的代码改为
SqlSessionFactory sqlSessionFactory = SqlSessionFactoryUtils.getSqlSessionFactory();
静态代码块只会执行一次,这样无论获取多少次sqlSessionFactory,都只会创建一个工厂,一个数据库池
注意:不推荐将获取SqlSession对象的那行代码一起抽取,因为 openSession() 是开启连接,如果使所有请求都共用一个连接,效率低