软件架构方式
- C/S:Client/Server,客户端/服务器
- B/S:Browser/Server,浏览器/服务器
C/S架构 | 说明 |
---|---|
特点 | 必须在Client客户端安装特定软件 |
优点 | 图形显示效果较好 |
缺点 | 服务器软件升级后客户端必须也升级,不利于维护。 |
示例 | QQ、微信 |
B/S架构 | 说明 |
---|---|
特点 | 无需安装客户端,任何浏览器均可直接访问。 |
优点 | 只需升级服务器端 |
缺点 | 图形显示效果不如C/S,需通过HTTP协议访问。 |
HTTP协议
URL
服务器资源需通过浏览器URL地址访问获取,浏览器给出的请求地址会被解析为符合HTTP协议格式并发出。URL是一种特殊类型的URI,包含了用于查找某个资源的足够信息。
http://host[:port]/[path]
HTTP全称Hypertext Transfer Protocol超文本传输协议,是一个客户端请求与响应的标准协议,协议规定了浏览器和WWW万维网服务器之间相互通信的规则,用户输入地址和端口后即可从服务器上获取所需网页信息。HTTP是互联网上应用最为广泛地一种网络协议,它是一个基于请求与响应模式的、无状态的、应用层协议,运行在TCP协议基础之上。
HTTP特点
- 支持B/S模式
- 简单快速:客户端只需向服务器发送请求方法和路径,服务器即可响应数据,因此通信速度块。
- 灵活:HTTP允许传输任意类型的数据,传输的数据类型会由Content-Type标识。
- 无连接:每次TCP连接只处理一个或多个请求,服务器处理完客户端请求后会断开连接以节省传输时间。
- 无状态:协议对事务处理没有记录能力
HTTP版本
- HTTP1.0 一次请求响应后连接直接断开,因此又称为短链接。
- HTTP1.1 一次请求响应后不会立即断开连接,会等待一段时间,若等待期间有新的请求则通过之前建立的连接通道来收发消息。若等待期间没有新的请求则断开连接。因此又称为长连接。
HTTP通信流程
- 客户端与服务器通过三次握手建立连接
- 客户端向服务器发送请求
- 服务器接收请求后根据请求返回相应地数据作为应答
- 客户端与服务器通过四次挥手断开连接
请求报文与响应报文
通信规则规定了客户端发送给服务器的内容格式,也规定了服务器发送给客户端的内容格式。客户端发送给服务器的格式称为请求协议,服务器发送给客户端的格式称为响应协议。
HTTP请求报文
当浏览器向HTTP服务器发出请求时,会向服务器传递一个数据块,也就是请求信息(请求报文)。
HTTP请求信息分为4部分组成
HTTP请求报文 | 说明 |
---|---|
请求行 | 请求方法 地址 URI协议/版本 |
请求头 | Request Header |
空行 | 空白行 |
请求正文 | 请求体 |
例如:GET请求没有请求体
GET http://127.0.0.1:8080/test/my HTTP/1.1
Host: 127.0.0.1:8080
Proxy-Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: JSESSIONID=2525F50364A5038B034720AEBCBAD210
HTTP响应报文与请求报文类似,也由4部分组成:
HTTP/1.1 200
Content-Type: text/html;charset=ISO-8859-1
Content-Length: 53
Date: Tue, 11 Oct 2022 01:42:16 GMT
Keep-Alive: timeout=20
Connection: keep-alive
响应报文 | 说明 |
---|---|
状态行 | HTTP/1.1 200 |
响应头 | Response Header |
空行 | - |
响应正文 | 相应体 |
常见响应状态码
响应状态码 | 描述 | 说明 |
---|---|---|
200 | OK | 客户端请求成功 |
302 | Found | 临时重定向 |
403 | Forbidden | 服务器接收到请求但拒绝提供服务,响应体给出原因。 |
404 | NotFound | 请求资源不存在 |
500 | Internal Server Error | 服务器发生不可预期错误导致无法完成请求 |
Web服务器
WWW(World Wide Web)万维网,用来表示Internet主机上供外界访问的资源。
Intetnet上供外界访问的资源可分为两类
网络资源 | 说明 |
---|---|
静态资源 | Web页面中供浏览的数据,始终是不变的,比如HTML/CSS/JS... |
动态资源 | Web中供人浏览的数据是由程序产生,不同时点不同设备访问时内容各异。比如JSP/Sevlet... |
通常的动态网页技术
动态网页技术 | 描述 |
---|---|
CGI | Common GateWay Interface |
API | 常用的有NSAPI、ISAPI |
ASP | Active Server Page以进程方式运行,效率较低,现在演化为ASP.net。 |
PHP | Personal Home Page |
JSP/Servlet | 以线程方式运行 |
Java中动态Web资源开发技术统称为Java Web。
Web服务器是运行以及发布Web应用的容器,只有将开发好的Web项目放置到Web服务器中,才能使网络中的用户通过浏览器进行访问。
常用的Web服务器分为收费和开源两种
开源 | 说明 |
---|---|
Tomcat | Apache,Java语言编写。 |
Jetty | 淘宝,运行效率比Tomcat高。 |
Resin | 新浪,开源Web服务器中运行效率最高 |
收费 | 说明 |
---|---|
WebLogic | Oracle |
WebSphere | IBM |
Tomcat
Tomcat是Apache软件基金会Jakarta项目中的一个核心项目,它免费开源且支持Servlet和JSP规范。
安装注意
- 下载地址 https://tomcat.apache.org
- 将Tomcat源码包解压到一个没有特殊符号的目录中,建议是使用纯英文名称的文件夹名称。
- 避免将Tomcat放到磁盘层级较深的目录下
- 避免将Tomcat安装到含有中文名称的路径下
环境配置
将Tomcat的安装路径作为CATALINA_HOME
的环境变量,同时为到path
环境添加%CATALINA_HOME%\bin
记录。
$ echo %CATALINA_HOME%
D:\tomcat\apache-tomcat-8.5.82
目录结构
文件夹 | 说明 |
---|---|
bin | 存放二进制可执行文件 |
conf | 存放配置文件 |
lib | Tomcat类库,存放Tomcat运行所需的jar包。 |
logs | 存放日志文件,记录开启和关信息。 |
temp | 临时文件,当Tomcat停止运行后会自动删除。 |
webapp | 存放Web项目,每个文件夹代表一个项目。ROOT为默认项目。 |
work | 运行时生成的文件,最终运行的文件都将这里。 |
默认8080端口被占用时,修改Tomcat配置文件,重启生效。
$ vim conf/server.xml
如果Tomcat启动时闪退,可能是由于JAVA_HOME
环境变量配置出错。
若Tomcat启动失败,需要检查下
-
JAVA_HOME
环境变量配置 - 系统防火墙端口设置
- 查看启动日志
- 以catalina的debug模式启动
- 检查默认端口是否被占用
项目部署
Tomcat是比较有名的Servlet容器,Jetty、WebLogic等都是Servlet容器。容器是指符合相应的规范,提供组件运行环境的程序。组件是指符合相应规范且具有部分功能,同时需要部署到相应地容易内才能运行的软件模块。
Java Web项目部署时需存放到webapp
目录下,然后通过特定的URL地址来进行访问。
创建项目
$ cd webapp
$ mkdir test && cd test
文件结构 | 说明 |
---|---|
META-INF | 存放Web应用上下文信息,符合J2EE标准。 |
WEB-INF | 存放项目核心内容 |
WEB-INF/classes | 存放字节码文件,存放编译后的Servlet。 |
WEB-INF/lib | 存放jar包 |
WEB-INF/web.xml | 项目配置文件,部署描述文件,用于描述Servlet。 |
pages | 存放项目页面 |
WEB-INF是不能被外部直接访问的,因此项目页面文件是不能存放在WEB-INF目录下的。
URL访问资源,浏览器输入 http://127.0.0.1:8080/test/index.html
URL组成 | 示例 |
---|---|
协议 | http:// |
域名或主机地址 | 127.0.0.1 |
端口 | 8080 |
资源路径 | test/index.html |
Tomcat核心服务模块:Connector连接模块、Container容器
Tomcat服务器核心是一个Servlet/JSP Container,对每个HTTP请求的处理流程:获取客户端连接 -> Servlet分析请求(HttpServletRequest)-> 调用其service方法进行业务处理 -> 产生相应的响应(HttpServletResponse)-> 关闭连接
Servlet
- 服务器小应用程序(Server let)
- 完成B/S架构下客户端请求的响应处理
- 平台独立性能良好,以线程方式运行
- Servlet API为Servlet提供了统一的编程接口
- Servlet在容器中运行,常见Servlet容器Tomcat。
Servlet是Server Applet的组合简称,表示用Java编写的服务器端应用程序(代码、功能实现),具有独立于平台和协议的特性,可交互式的处理客户端发送服务器端的请求,并完成操作响应,生成动态Web内容。
狭义的Servlet指的是Jave实现的一个接口,广义的Servlet指的是任何实现了Servlet接口的类。
Servlet是Java Web程序开发的基础,是JavaEE接口规范的一个组成部分。
Servlet的作用是接收客户端请求并完成操作、动态生成网页、将包含操作结果的动态网页响应给客户端
Servlet规范
- Servlet规范来自于JavaEE规范
- Servlet规范中指定了【动态资源文件】的开发步骤
- Servlet规范中指定了HTTP服务器调用动态资源文件的规则
- Servlet规范中指定了HTTP服务器管理动态资源文件实例对象的规则
Servlet是SUN公司制订的一种用于扩展Web服务器端功能的组件规范,由于Web服务器自身只能处理静态请求而无法处理动态请求,因此处理动态请求就必须对原有的Web服务器进行扩展。
早期为了实现对Web服务器对动态请求的支持,最常见的方式是采用CGI(Common Gateway Interface,通用网关接口)程序来扩展,但CGI程序开发复杂且可移植性差。
传统CGI中每个请求都要启动一个全新的进程,如果CGI程序本身的执行时间较短,启动进程所需的开销很可能反而会超过实际执行时间。Servlet中每个请求由一个轻量级的Java线程处理,而非重量级的操作系统进程。因此Servlet执行速度更快,每个客户端请求会被激活成单个程序中的一个线程,进而创建单独的程序。
相交于CGI,由于Servlet本身的有Java编写的,因此Java类库的全部功能对其都是可用的,同样也是独立于平台的。重点是Servlet会在Web服务器的地址空间内执行,因此没有必要再创建一个单独的线程来处理每个客户端请求。
快速入门
- 将Servlet的servlet-api.jar包(位于Tomcat的lib目录下)添加到环境变量
classpath
中
- 编写Servlet类实现
javax.servlet.Servlet
接口并重写5个方法
$ vim MyServlet.java
import javax.servlet.*;
import java.io.IOException;
public class MyServlet implements Servlet{
public void init(ServletConfig config) throws ServletException{
System.out.println("MyServlet.init");
}
public void service(ServletRequest req, ServletResponse resp) throws ServletException, IOException{
System.out.println("MyServlet.service");
}
public void destroy(){
System.out.println("MyServlet.destroy");
}
public ServletConfig getServletConfig(){
return null;
}
public String getServletInfo(){
return null;
}
}
执行编译生成字节码文件
$ javac MyServlet.java
- 将编译好的Servlet的字节码文件存放到Tomcat项目目录的
WEB-INF/classes
文件夹下,然后再编写WEB-INF
项目配置文件web.xml
。
$ cd webapps
$ mkdir test && cd test
$ mkdir WEB-INF && cd WEB-INF
$ mkdir lib classes
将编译生成的字节码文件放到classes
目录下
$ mv MyServlet.class test/WEB-INF/classes/
修改Web应用配置文件,设置Servlet访问参数。
$ vim test/WEB-INF/web.xml
配置servlet和servlet-mapping节点
MyServlet
MyServlet
MyServlet
/my
重启Tomcat服务后,浏览器输入 http://127.0.0.1:8080/test/my后查看命令行打印信息。
IDEA创建Servlet项目
- 新建Java Enterprise项目
Servlet接口
Servlet接口来自于Servlet规范,Servlet接口由HTTP服务器提供,如Tomcat服务器的lib
目录下的server-api.jar
包。由于Servlet不是Java平台标准版的组成部分,因此须为编译器指定Servlet类的路径。使用前需将server-api.jar
包添加到本机上的环境变量classpath
类路径中,即可通过JDK的编译器来编译Servlet程序。
$ echo %classpath%
D:\tomcat\apache-tomcat-8.5.82\lib\servlet-api.jar
Servlet API中最重要的是Servlet接口,所有Servlet都直接或间接的与该接口发生联系,或是直接实现该接口,或是间接继承实现了该接口的类。
Servlet框架核心是javax.servlet.Servlet
接口,所有Servlet都必须实现这一接口。
Servlet规范规定HTTP服务器能调用的动态资源文件必须是Servlet接口的实现类,Servlet是运行在支持Java Servlet规范的解释器的Web服务器上的Java类。
Servlet接口中定义了5个方法,Servlet被设计成请求驱动的,其中3个为Servlet生命周期方法。
接口方法 | 说明 |
---|---|
ServletConfig getServletConfig() | 获取Servlet配置 |
String getServletInfo() | 获取Servlet信息 |
生命周期方法 | 描述 |
---|---|
init(ServletConfig config) | 负责初始化Servlet对象 |
service(ServletRequest req, ServletResponse resp) | 负责响应客户端请求 |
destroy() | 当Servlet对象退出生命周期时负责释放所占用的资源 |
Servlet生命周期
Web服务器启动后,当配置的URL访问时会自动调用Servlet的生命周期方法。Servlet对象会由Servlet容器创建,生命周期方法也由Servlet容器调用。
Servlet生命周期可被定义为从创建到销毁的整个过程,分为4个阶段:实例化->初始化->服务->销毁
阶段 | 生命周期 | 操作 | 执行 | 说明 |
---|---|---|---|---|
1 | 实例化 | new | 一次 | Servlet对象的创建 |
2 | 初始化 | init() | 一次 | Servlet对象初始化 |
3 | 服务 | service() | 多次 | Servlet对象调用service() 方法来响应(处理)客户端请求 |
4 | 销毁 | destroy() | 一次 | Web服务器关闭时调用destroy() 方法来销毁Servlet对象 |
- Servlet对象的创建
默认情况下Servlet容器第一次接受到HTTP请求时会创建对应的Servlet对象。Servlet容器之所以能够做到是由于在注册Servlet时提供了全类名,Servlet容器会使用反射技术创建了Servlet对象。
Servlet第一次请求加载时,Web服务器会创建一个Servlet对象,Servlet对象会调用init()
方法来完成必要的初始化工作。
- Servlet对象初始化
Servlet容器创建Servlet对象后会调用init(ServletConfig config)
方法对其进行初始化。javax.servlet.Servlet
接口中init()
方法要求容器将ServletConfig
实例传入,这也是获取ServletConfig
实例对象的根本方法。
public void init(ServletConfig c) throws ServletException {}
当执行初始化时,Web服务器会把一个ServletConfig
类型的对象传递给init()
方法,这个对象会被保存Servlet中,直到Servlet被销毁。ServletConfig
对象负责向Servlet传递服务设置信息,若传递失败则会发生ServletException
异常,此时Servlet就无法正常工作了。
init()
方法只会被调用一次,即在Servlet第一次被请求加载时。当后续客户端请求Servlet服务时,Web服务器会启动一个新的线程,该线程中Servlet会调用service()
方法来响应客户端请求。也就是说,每个客户端请求都将导致service()
方法被调用执行,执行过程会分别运行在不同的线程中。
- 新建的Servlet调用
service()
方法来响应(处理)客户端请求
public void service(ServletRequest req, ServletResponse res) throws ServletException ,IOException{}
第一个到达Web服务器的HTTP请求被委派到Servlet容器,Servlet容器在调用service()
方法前会加载Servlet。然后Servlet容器会处理由多个线程产生的多个请求,每个线程执行一个单一的Servlet实例的service()
方法。
Servlet的请求可能包含多个数据项,当Web服务器接收到某个Servlet请求时Web服务器会把请求封装成一个HttpServletRequest
请求,然后把它传递给Servlet对应的服务方法。
- 当Web服务器关闭时调用
destroy()
方法来销毁Servlet
Servlet销毁前会调用destroy()
方法, 由JVM的垃圾回收器进行处理。
Servlet线程安全
Servlet在访问后会执行实例化操作来创建一个Servlet对象,注意只会创建一个同时初始化一次。然而Tomcat容器可同时开多个线程,并发访问同一个Servlet,也就是多线程并发访问同一个对象(临界资源),此时如果在某方法中对成员变量做修改操作,就会导致数据不一致从而产生线程安全问题。那么应如何保证线程安全的呢?
解决方案 | 说明 |
---|---|
synchronized | 将存在线程安全问题的代码放到同步代码块中去执行,原子操作上锁。并发访问时只能串行处理,效率太低。 |
实现SingleThreadModel接口 | 实现后每个线程都会创建Servlet实例,每个客户端请求就不存在共享资源的问题,效率太低已淘汰。 |
局部变量 | 尽可能使用局部变量 |
例如:使用synchronized将原子操作上锁以保证线程安全,问题是并发访问时只能串行处理,效率太低。
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
synchronized (this) {
String username = request.getParameter("username");
String password = request.getParameter("password");
String msg = String.format("username = %s, password = %s", username, password);
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("");
out.println("" + message + "
");
out.println("");
}
}
例如:Servlet实现SingleThreadModel接口,问题是并发时每个线程访问都需要创建对象并初始化,浪费时间。每个对象都独立占用内存,浪费空间。响应客户端效率极低,已经被淘汰。
public class IndexServlet extends HttpServlet implements SingleThreadModel {
}
最佳方案:禁止使用成员变量,尽量使用局部变量。
之所以产生线程安全问题,最终原因是因为多线程并发访问或修改同一对象的成员变量而引发的。
public class IndexServlet extends HttpServlet{
//实例变量:多线程并发访问时会引发线程安全问题,禁止使用。
//private String message;
}
Servlet实现类
Servlet体系中除了实现Servlet接口外,还可通过继承GenericServlet或HttpServlet类实现自定义Servlet类。
Servlet可使用javax.servlet
和javax.servlet.http
包创建,它是JavaEE标准组成部分。Servlet的框架是由两个jar组成的:
Servlet包 | 描述 |
---|---|
javax.servlet | 定义了所有Servlet类必须实现或扩展的通用接口和类 |
javax.servlet.http | 定义了采用HTTP通信协议的HttpServlet类 |
GenericServlet
GenericServlet抽象类是所有Servlet类的鼻祖,提供了Servlet生命周期方法简单实现,同时提供了抽象的service()
方法(子类必须重写父类的抽象方法)。因此使编写Servlet变得更加容易,子类只需重写抽象的service()
方法即可。
$ vim GenServlet.java
@WebServlet(name = "genServlet", value = "/gen")
public class GenServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("generic servlet children...");
}
}
HttpServlet
HttpServlet抽象类继承自GenericServlet,在其基础上做了进一步的扩展,提供了要被子类化以创建适用于Web站点的方法。
GenericServlet、HttpServlet是Servlet接口的抽象实现类,为什么实现类要使用抽象类呢?因为抽象类本身的作用就是降低接口实现类对接口实现过程的难度,将接口中无需使用的抽象方法交给抽象类来完成,接口实现类只需要对接口中所需使用的方法重写即可。
HttpService类源自servlet.http
包,HttpServlet是能够处理HTTP请求的Servlet,它在GenericServlet基础上又添加了一些与HTTP处理方法。
用于HTTP的Servlet编程都通过继承HttpServlet实现,开发编写Servlet时通常是继承HttpServlet而不是直接实现Servlet接口。
HttpServlet子类至少必须重写一个请求处理方法,请求处理方法分别对应HTTP协议的7种请求方式:
编程接口 | 协议版本 | 说明 |
---|---|---|
doGet() | ALL | 响应GET请求 |
doPost() | ALL | 响应POST请求 |
doHead() | ALL | 仅响应GET请求的头部 |
doPut() | HTTP1.1 | - |
doDelete() | HTTP1.1 | - |
doOptions() | HTTP1.1 | - |
doTrace() | HTTP1.1 | - |
例如:Servlet接口实现类开发
- 创建一个Java类继承HttpServlet父类使其成为一个Servlet接口实现类
- 重写HttpServlet父类两个方法
doGet()
和doPost()
$ vim IndexServlet.java
package com.jc.sl;
import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
@WebServlet(name = "indexServlet", value = "/index")
public class IndexServlet extends HttpServlet {
private String message;
@Override
public void init() {
message = "success";
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");
PrintWriter pw = response.getWriter();
pw.println("");
pw.println("" + message + "
");
pw.println("");
}
@Override
public void destroy() {
}
}
Servlet配置
Servlet2.5之前使用的web.xml
配置的方式
$ vim webapp/WEB-INF/web.xml
HelloServlet
com.jc.svlt.HelloServlet
1
HelloServlet
/hello
标签用于定义匹配规则,取值说明
匹配方式 | 示例 | 说明 |
---|---|---|
精确匹配 | /xxx | 只有URL路径为具体名称是才触发 |
后缀匹配 | *.xxx | 只有以xxx结尾的会才触发 |
通配符匹配 | /* | 匹配所有请求,包含服务器的所有资源。 |
通配符匹配 | / | 匹配所有请求,包含服务器所有资源,但不包含.jsp 。 |
标签
-
标记容器是否应该在Web应用程序启动时就加载此Servlet -
值必须是一个整数,表示Servlet被加载的先后顺序。 - 若值为负数或没有设置,容器会在Servlet被请求时才会加载。
- 若为正整数或0,表示容器在应用启动时就加载并初始化此Servlet。其值越小Servlet加载的优先级越高越先被加载。值相同时容器会根据自己选择的顺序来加载。
Servlet3.0后支持注解方式
@WebServlet(name = "indexServlet", value = "/index")
@WebServlet
注解属性
属性 | 说明 |
---|---|
name | Servlet名字,可选。 |
value | URL路径,支持多个。 |
urlPattern | URL路径,和value作用相同但不能同时使用。 |
loadOnStartup | Servlet创建时机 |
HttpServletRequest
Servlet API中定义了HttpServletRequest接口,继承自ServletRequest接口,专门用来封装HTTP请求消息。
- HttpServletRequest接口来自Servlet规范,即Tomcat提供的servlet-api.jar包。
- HttpServletRequest接口实现类由HTTP服务器负责提供
- HttpServletRequest接口负责在doGet/doPost方法运行时读取HTTP请求协议包中的信息
- 开发人员习惯将HttpServletRequest接口修饰的对象称为请求对象
HttpServletRequest是一个接口,其对象由Tomcat负责创建。当客户端发送HTTP请求时,Tomcat会将HTTP协议中的信息和数据全部解析出来,封装到HttpServletRequest对象中。一次请求会对应一个HttpServletRequest对象,只在当前请求中有效(请求域),请求结束请求域就会被销毁。
HttpServletRequest接口的作用
- 读取HTTP请求协议包中请求行信息
- 读取保存在HTTP请求协议报中请求头或请求体中请求参数信息
- 代替浏览器向HTTP服务器申请资源文件调用
HttpServletRequest接口方法
接口方法 | 说明 |
---|---|
String getParameter(String name) | 根据表单组件名称获取提交数据 |
void setCharacterEncoding(String charset) | 设置请求字符编码方式 |
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String msg = String.format("username = %s, password = %s", username, password);
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println(msg);
}
中文乱码
使用GET和POST发送中文时,有时候POST接收的内容中出现了乱码,GET却正常,为什么呢?这跟Tomcat版本有关,Tomcat7以下版本会出现。
浏览器以GET方式发送请求时,请求参数保存在请求头中,当HTTP请求协议包到达HTTP服务器之后,HTTP服务器首先会对其解码,此时请求头中的二进制数据内容由Tomcat负责解码,在Tomcat9.0中默认使用的是UTF-8字符集,因此可以解析所有国家文字。
浏览器以POST方式发送请求时,请求参数保存在请求体中,当HTTP请求协议包到达HTTP服务器之后,解码请求体二进制内存会由当前请求对象request
负责,请求对象默认采用的是ISO8859-1字符集(东欧语系字符集),请求体参数内容如果是中文将无法解码只能是乱码。
可手工转码处理
String username = request.getParameter("username");
username = new String(username.getBytes("ISO8859-1"), "UTF-8");
解决方案是使用POST请求方式时,在读取请求体内容前,应通知请求对象使用UTF-8字符集对请求体进行解码。
request.setCharacterEncoding("UTF-8");
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String msg = String.format("username = %s, password = %s", username, password);
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println(msg);
}
数据共享
同一网站中如果两个Servlet之间通过【请求转发】方式进行调用,彼此之间共享同一个请求协议包。而一个请求协议包只对应一个请求对象,因此Servlet之间共享同一个请求对象,此时可利用这个请求对象在两个Servlet之间实现数据共享。
当请求对象实现Servlet之间数据共享时,开发人员将请求对象又称为【请求作用域对象】。
例如:设置共享数据后发送给其他Servlet
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
//共享数据
String name = "token";
String value = "k9fYYQMuMaepzYty";
request.setAttribute(name, value);
//向Tomcat申请调用其他Servlet
request.getRequestDispatcher("/log").forward(request, response);
}
HttpServletResponse
HttpServlet中的doGet/doPost方法的目的是根据请求计算最终得到响应,再将响应数据设置到HttpServletResponse对象中。最后Tomcat会把HttpServletResponse对象按照HTTP协议格式转换为字符串并通过Socket写回给浏览器。
- HttpServletResponse接口来自于Servlet规范
servlet-api.jar
- HttpServletResponse接口实现类由HTTP服务器负责提供
- HttpServletResponse接口负责将
doGet/doPost
方法执行结果写入到【响应体】后交给浏览器 - 开发习惯将HttpServletResponse接口修改的对象称为【响应对象】
HttpServletResponse对象作用
- 将执行结果以二进制形式写入到【响应体】中
- 支持设置响应头中【Content-Type】属性来控制浏览器使用对应编译器将二进制数据转化为目标格式。
- 支持设置响应头中【Location】属性来控制浏览器项指定地址发送请求
核心方法 | 说明 |
---|---|
setStatus(int sc) | 设置响应状态码 |
setHeader(String name, String value) | 设置一个带有给定名称和值的Header,若name已存在则会覆盖。 |
addHeader(int sc) | 添加一个带有给定名称和值的Header,若name已存在则不会覆盖依然会添加新值。 |
setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME字符集) |
sendRedirect(String location) | 使用指定的重定向位置URL发送临时重定向响应到客户端 |
PrintWriter getWriter() | 用于向响应体中写入文本格式数据 |
OutputStream getOutputStream() | 用于向响应体中写入二进制数据 |
例如:使用响应对象向浏览器输出HTML内容
中文乱码
响应对象缓冲区默认编码采用的是ISO8859-1,不支持中文因此会出现乱码。解决输出中文乱码:
- 设置服务器端响应的编码格式
response.setCharacterEncoding("UTF-8"); // 设置服务器端字符编码格式
- 设置客户端响应内容的头内容文件类型以及编码格式
response.setHeader("Content-Type","text/html;charset=UTF-8");
注意:设置操作需在获取输出流之前才会生效
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String password = request.getParameter("password");
String msg = String.format("username = %s, password = %s", username, password);
response.setCharacterEncoding("UTF-8"); // 设置服务器端字符编码格式
response.setHeader("Content-Type","text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println(msg);
}
推荐同时设置服务端的编码格式和客户端响应的文件类型以及响应时的编码格式,替代上述两步。
response.setContentType("text/html; charset=UTF-8");
请求和响应对象的生命周期
- 当HTTP服务器接收到浏览器发送的HTTP请求协议包之后,会自动为当前HTTP请求协议包生成一个请求对象和响应对象。
- 请求对象和响应对象的生命周期会贯穿一次请求的处理过程
- 请求对象和响应对象相当于用户在服务端的代言人
- 当HTTP服务器调用doGet/doPost方法时,会将请求对象和响应对象作为实参传递给方法,以确保doGet/doPost正确执行。
- 当HTTP服务器准备推送HTTP响应协议包之前,会将本次请求关联的请求对象和响应对象销毁。
Servlet & JDBC
添加Maven工程依赖
$ vim pom.xml
org.apache.commons
commons-lang3
3.12.0
commons-codec
commons-codec
1.15
org.projectlombok
lombok
1.18.24
provided
commons-dbutils
commons-dbutils
1.6
com.alibaba
druid
1.2.8
mysql
mysql-connector-java
8.0.28
javax.servlet
javax.servlet-api
4.0.1
provided
创建数据源配置项
$ vim resources/druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/fw
username=root
password=root
initialSize=10
maxActive=20
maxWait=10000
配置数据源工具类并处理事务
$ vim util/DruidUtil.java
package com.jc.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class DruidUtil {
private static DataSource ds = null;
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
static {
//导入配置文件
Properties p = new Properties();
InputStream is = DruidUtil.class.getClassLoader().getResourceAsStream("druid.properties");
try {
p.load(is);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
//获取数据源
try {
ds = DruidDataSourceFactory.createDataSource(p);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
//获取数据源
static DataSource getDataSource(){
return ds;
}
//获取连接
public static Connection getConnection(){
Connection conn = THREAD_LOCAL.get();
if(conn != null){
return conn;
}
try {
conn = ds.getConnection();
THREAD_LOCAL.set(conn);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return conn;
}
//开启事务
public static void beginTransaction(){
Connection conn = getConnection();
try {
conn.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
//提交事务
public static void commit(){
Connection conn = getConnection();
try {
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally {
release(conn);
}
}
//事务回滚
public static void rollback(){
Connection conn = getConnection();
try {
conn.rollback();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}finally {
release(conn);
}
}
//释放连接返还连接池
public static void release(Connection conn, Statement stmt, ResultSet rs){
if(rs != null){
try{
rs.close();
}catch (Exception e) {
e.printStackTrace();
}
rs = null;
}
if(stmt != null){
try{
stmt.close();
}catch (Exception e) {
e.printStackTrace();
}
stmt = null;
}
if(conn != null){
try{
conn.close();
THREAD_LOCAL.remove();
}catch (Exception e) {
e.printStackTrace();
}
conn = null;
}
}
public static void release(Connection conn, Statement stmt){
release(conn, stmt, null);
}
public static void release(Connection conn){
release(conn, null, null);
}
}
使用数据源工具类创建数据操作类
$ vim util/DbUtil.java
package com.jc.util;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.*;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
public class DbUtil {
public static QueryRunner getQueryRunner(){
return new QueryRunner(DruidUtil.getDataSource());
}
public static Long count(String sql, Object... param){
try {
Object v = getQueryRunner().query(sql, new ScalarHandler<>(), param);
return v==null ? 0L : (Long)v;
} catch (Exception e) {
e.printStackTrace();
}
return 0L;
}
public static Object[] getArray(String sql, Object... param){
try {
return getQueryRunner().query(sql, new ArrayHandler(), param);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static Map getMap(String sql, Object... param){
try {
return getQueryRunner().query(sql, new MapHandler(), param);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static T getBean(Class type, String sql, Object... param){
try {
return getQueryRunner().query(sql, new BeanHandler<>(type), param);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static List
创建数据表
CREATE TABLE `sys_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`code` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '唯一编号',
`created_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`created_by` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
`updated_by` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '更新人',
`username` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '账户',
`password` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码',
`salt` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码SALT',
`balance` bigint(20) DEFAULT '0' COMMENT '账户金额',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户';
创建实体类
$ vim entity/SysUser.java
package com.jc.entity;
import lombok.Data;
import java.util.Date;
@Data
public class SysUser {
private Long id;
private String code;
private String createdBy;
private Date createdAt;
private String updatedBy;
private Date updatedAt;
private String username;
private String password;
private String salt;
private Long balance;
}
创建DAO层
$ vim dao/SysUserDao.java
package com.jc.dao;
import com.jc.entity.SysUser;
public interface SysUserDao {
Boolean insert(SysUser e);
Long cntByName(String username);
SysUser getByName(String username);
}
$ vim dao/impl/SysUserImpl.java
package com.jc.dao.impl;
import com.alibaba.druid.util.StringUtils;
import com.jc.dao.SysUserDao;
import com.jc.entity.SysUser;
import com.jc.util.DbUtil;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.RandomStringUtils;
public class SysUserDaoImpl implements SysUserDao {
@Override
public Boolean insert(SysUser e){
String username = e.getUsername();
String password = e.getPassword();
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
return false;
}
username = username.trim();
password = password.trim();
String salt = RandomStringUtils.random(16, true, true);
password = DigestUtils.md5Hex(DigestUtils.md5Hex(password) + salt);
String code = RandomStringUtils.randomNumeric(11);
String sql = "INSERT INTO sys_user(created_at, username, password, salt, code) VALUES(NOW(), ?, ?, ?, ?)";
int n = DbUtil.insert(sql, username, password, salt, code);
return n > 0;
}
@Override
public Long cntByName(String username) {
if(StringUtils.isEmpty(username)){
return 0L;
}
username = username.trim();
String sql = "SELECT COUNT(1) FROM sys_user WHERE 1=1 AND username = ?";
return DbUtil.count(sql, username);
}
@Override
public SysUser getByName(String username) {
if(username == null){
return null;
}
username = username.trim();
String sql = "SELECT * FROM sys_user WHERE 1=1 AND username = ?";
return DbUtil.getBean(SysUser.class, sql, username);
}
}
创建Service服务层
$ vim service/SysUserService.java
package com.jc.service;
import com.jc.entity.SysUser;
public interface SysUserService {
Boolean create(String username, String password);
SysUser login(String username, String password);
}
$ vim service/impl/SysUserServiceImpl.java
package com.jc.service.impl;
import com.jc.dao.SysUserDao;
import com.jc.dao.impl.SysUserDaoImpl;
import com.jc.entity.SysUser;
import com.jc.service.SysUserService;
import com.jc.util.DbUtil;
import org.apache.commons.codec.digest.DigestUtils;
public class SysUserServiceImpl implements SysUserService {
private SysUserDao userDao = new SysUserDaoImpl();
@Override
public Boolean create(String username, String password) {
SysUser d = new SysUser();
d.setUsername(username.trim());
d.setPassword(password.trim());
try {
DbUtil.beginTransaction();
if(userDao.cntByName(username) > 1){
return false;
}
if(!userDao.insert(d)){
return false;
}
DbUtil.commit();
}catch (Exception e) {
e.printStackTrace();
DbUtil.rollback();
}
return true;
}
@Override
public SysUser login(String username, String password) {
SysUser d = null;
try {
DbUtil.beginTransaction();
//根据账户查询记录
d = userDao.getByName(username);
if(d == null){
return null;
}
//验证密码
String salt = d.getSalt();
password = DigestUtils.md5Hex(DigestUtils.md5Hex(password.trim()) + salt);
if(!d.getPassword().equals(password)){
return null;
}
DbUtil.commit();
}catch (Exception e) {
e.printStackTrace();
DbUtil.rollback();
}
return d;
}
}
创建Servlet
$ vim servlet/LoginServlet.java
package com.jc.servlet;
import com.jc.entity.SysUser;
import com.jc.service.SysUserService;
import com.jc.service.impl.SysUserServiceImpl;
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.io.PrintWriter;
@WebServlet(name = "loginServlet", value = "/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
//接收参数
String username = req.getParameter("username");
String password = req.getParameter("password");
//调用业务逻辑
SysUserService userService = new SysUserServiceImpl();
SysUser d = userService.login(username, password);
//处理结果
PrintWriter pw = resp.getWriter();
if(d == null){
pw.println("操作失败");
}else{
pw.println("操作成功");
}
pw.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
转发 Foward
项目 | 名称 | 说明 |
---|---|---|
Forward | 转发 | 服务器端行为,浏览器地址无反应。 |
Redirect | 重定向 | 客户端行为,浏览器地址栏发生变化,服务器返回301/302状态码告诉客户端去请求新的URL地址。 |
早期调用业务逻辑和显示结果页面都在同一个Servlet内,这就产生了设计上的问题:首先不符合单一职责原理、各司其职的思想,其次不利于后期维护。因此应该将业务逻辑和页面显示相分离。
业务逻辑和页面显示分离后需要解决的问题:业务逻辑Servlet如何跳转到页面显示Servlet?业务逻辑Servlet得到的结果数据如何传递给页面显示Servlet?
request.getRequestDispatcher(url-pattern).forward(request, response);
在业务逻辑Servlet中使用forward()
实现服务器转发行为,转发作用在服务器端,是将请求发送给服务器上的其他资源,以共同完成一次请求的处理。转发发生在服务器内部跳转,因此浏览器地址栏不会发生变化,仍属于同一次请求。
数据传递
转发是在服务器内部跳转,表示一次请求,因此可以共享一次请求作用域中的数据。请求作用域拥有存储数据的空间,作用范围对一次请求有效,一次请求可经多次转发。因此可将数据存入请求对象后,在一次请求过程中的任何位置获取。请求作用域可以存储任何类型的数据。数据以键值对形式存在请求作用域中,其中键名为String类型,键值为Object类型。
// 存入数据
request.setAttribute(String k, Object v);
// 取出数据
request.getAtttribute(String k);
转发特点
- 转发是服务器行为
- 转发时浏览器只做了一次访问请求
- 转发时浏览器地址栏不会产生变化,用户无感知。
- 转发两次跳转之间传输的信息不会丢失,可通过请求对象实现数据传递。
- 转发只能将请求转给同一个Web应用中的组件
例如:将LoginServlet拆分为两个Servlet,一个用于控制请求处理业务数据,一个用于视图显示逻辑。
$ vim controller/LoginController.java
package com.jc.controller;
import com.alibaba.druid.util.StringUtils;
import com.jc.entity.SysUser;
import com.jc.service.SysUserService;
import com.jc.service.impl.SysUserServiceImpl;
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(name = "loginControler", value = "/login")
public class LoginController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
//接收参数
String username = req.getParameter("username");
String password = req.getParameter("password");
//调用业务逻辑
SysUserService userService = new SysUserServiceImpl();
SysUser d = userService.login(username, password);
//数据传递
req.setAttribute("data", d);
//转发
req.getRequestDispatcher("/loginView").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
$ vim view/LoginView.java
package com.jc.view;
import com.jc.entity.SysUser;
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.io.PrintWriter;
@WebServlet(name = "loginView", value = "/loginView")
public class LoginView extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
//获取数据
SysUser d = (SysUser)req.getAttribute("data");
//处理结果
PrintWriter pw = resp.getWriter();
if(d == null){
pw.println("操作失败");
}else{
pw.println("操作成功");
}
pw.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
重定向 Redirect
重定向作用在客户端,客户端将请求发送给服务器后,服务器会响应给客户端一个全新的请求地址,客户端根据此地址重新发送新请求。
request.sendRedirect(URI)
URI全称Uniform Resource Identifier统一资源标识符,用于在服务器中定位一个资源,资源是在Web项目中的路径(/project/source)。
数据传递
重定向跳转时浏览器地址栏发生了改变,代表客户端重新发送的请求,属于两次请求。由于响应对象没有作用域,因此两次请求中的数据无法共享。
传递数据可通过URI拼接查询字符串实现,获取数据仍使用request.getParameter()
方法。
重定向特点
- 重定向是客户端浏览器的行为
- 重定向发生时浏览器做了至少两次访问请求
- 重定向发生时浏览器地址栏会发生改变
- 重定向两次跳转之间传递的数据会丢失
- 重定向可以指向任何资源,包括:当前应用中的其他资源、同一站点上其他应用中的资源、其他站点的资源。
例如:使用JSP技术替代视图显示的Servlet
创建首页
$ vim webapp/index.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
home
Homepage
创建登录页
$ vim webapp/login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Login
Login
<%=request.getAttribute("message")%>
使用JSP后重新修改登录控制逻辑
$ vim controller/LoginController.java
package com.jc.controller;
import com.alibaba.druid.util.StringUtils;
import com.jc.entity.SysUser;
import com.jc.service.SysUserService;
import com.jc.service.impl.SysUserServiceImpl;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
@WebServlet(name = "loginControler", value = "/login")
public class LoginController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
SysUser d = null;
//Session判断是否登录过
HttpSession hs = req.getSession();
d = (SysUser) hs.getAttribute("user");
if(d != null){
resp.sendRedirect("index.jsp");
return;
}
//接收参数
String username = req.getParameter("username");
String password = req.getParameter("password");
if(StringUtils.isEmpty(username)){
req.setAttribute("message", "账户禁止为空");
req.getRequestDispatcher("login.jsp").forward(req, resp);
return;
}
if(StringUtils.isEmpty(password)){
req.setAttribute("message", "密码禁止为空");
req.getRequestDispatcher("login.jsp").forward(req, resp);
return;
}
//调用业务逻辑
SysUserService userService = new SysUserServiceImpl();
d = userService.login(username, password);
if(d == null){
req.setAttribute("message", "操作失败");
req.getRequestDispatcher("login.jsp").forward(req, resp);
return;
}
//存入Session
hs.setAttribute("user", d);
//重定向
resp.sendRedirect("index.jsp");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}