JavaWab(二)
本文摘要:
- 了解JavaWeb开发的技术栈
- 理解HTTP协议和HTTP请求与响应数据的格式
- 掌握Tomcat的使用
- 掌握在IDEA中使用Tomcat插件
- 理解Servlet的执行流程和生命周期
- 掌握Servlet的使用和相关配置
Web概述
Web和JavaWeb的概念
Web是全球广域网,也称为万维网(www),能够通过浏览器访问的网站。
在我们日常的生活中,经常会使用浏览器去访问百度
、知乎
、推特
等这些网站,这些网站统称为Web网站。
我们知道了什么是Web,那么JavaWeb又是什么呢?顾名思义JavaWeb就是用Java技术来解决相关Web互联网领域的技术栈。
JavaWeb技术栈
了解JavaWeb技术栈之前,有一个很重要的概念要介绍。
B/S架构
什么是B/S架构?
B/S 架构:Browser/Server,浏览器/服务器 架构模式,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取Web资源,服务器把Web资源发送给浏览器即可。
- 打开浏览器访问百度首页,输入要搜索的内容,点击回车或百度一下,就可以获取和搜索相关的内容
- 思考下搜索的内容并不在我们自己的点上,那么这些内容从何而来?答案很明显是从百度服务器返回给我们的
- 日常百度的小细节,逢年过节百度的logo会更换不同的图片,服务端发生变化,客户端不需做任务事情就能获取最新内容
- 所以说B/S架构的好处:易于维护升级:服务器端升级后,客户端无需任何部署就可以使用到新的版本。
了解了什么是B/S架构后,作为后台开发工程师的我们将来主要关注的是服务端的开发和维护工作。在服务端将来会放很多资源,都有哪些资源呢?
静态资源
- 静态资源主要包含HTML、CSS、JavaScript、图片等,主要负责页面的展示。
- 我们之前已经学过前端网页制作
三剑客
(HTML+CSS+JavaScript),使用这些技术我们就可以制作出效果比较丰富的网页,将来展现给用户。但是由于做出来的这些内容都是静态的,这就会导致所有的人看到的内容将是一模一样。 - 在日常上网的过程中,我们除了看到这些好看的页面以外,还会碰到很多动态内容,比如我们常见的百度登录效果:
张三
登录以后在网页的右上角看到的是张三
,而李四
登录以后看到的则是李四
。所以不同的用户访问相同的资源看到的内容大多数是不一样的,要想实现这样的效果,光靠静态资源是无法实现的。
动态资源
- 动态资源主要包含Servlet、JSP等,主要用来负责逻辑处理。
- 动态资源处理完逻辑后会把得到的结果交给静态资源来进行展示,动态资源和静态资源要结合一起使用。
- 动态资源虽然可以处理逻辑,但是当用户来登录百度的时候,就需要输入
用户名
和密码
,这个时候我们就又需要解决的一个问题是,用户在注册的时候填入的用户名和密码、以及我们经常会访问到一些数据列表的内容展示(如下图所示),这些数据都存储在哪里?我们需要的时候又是从哪里来取呢?
数据库
- 浏览器发送一个请求到服务端,去请求所需要的相关资源;
- 资源分为动态资源和静态资源,动态资源可以是使用Java代码按照Servlet和JSP的规范编写的内容;
- 在Java代码可以进行业务处理也可以从数据库中读取数据;
- 拿到数据后,把数据交给HTML页面进行展示,再结合CSS和JavaScript使展示效果更好;
- 服务端将静态资源响应给浏览器;
- 浏览器将这些资源进行解析;
- 解析后将效果展示在浏览器,用户就可以看到最终的结果。
HTTP协议
- HTTP协议:主要定义通信规则
- 浏览器发送请求给服务器,服务器响应数据给浏览器,这整个过程都需要遵守一定的规则,之前学习过TCP、UDP,这些都属于规则,这里我们需要使用的是HTTP协议,这也是一种规则。
Web服务器
- Web服务器:负责解析 HTTP 协议,解析请求数据,并发送响应数据
- 浏览器按照HTTP协议发送请求和数据,后台就需要一个Web服务器软件来根据HTTP协议解析请求和数据,然后把处理结果再按照HTTP协议发送给浏览器
- Web服务器软件有很多,本文使用的是最为常用的
Tomcat
服务器
HTTP
简介
HTTP概念
HyperText Transfer Protocol,超文本传输协议,规定了浏览器和服务器之间数据传输的规则。
数据传输的规则指的是请求数据和响应数据需要按照指定的格式进行传输。
如果想知道具体的格式,可以打开浏览器,点击
1
F12
打开开发者工具,点击
1
Network
来查看某一次请求的请求数据和响应数据具体的格式内容
- 请求头
- 响应头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20PLAINTEXT
HTTP/1.1 200 OK
Bdpagetype: 2
Bdqid: 0xc9b693f200183fa5
Cache-Control: private
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Tue, 16 Aug 2023 10:39:20 GMT
Expires: Tue, 16 Aug 2023 10:39:20 GMT
Isprivate: 1
Server: BWS/1.1
Set-Cookie: BDSVRTM=221; path=/
Set-Cookie: BD_HOME=1; path=/
Set-Cookie: H_PS_PSSID=36560_36981_36955_36917_36776_37126_37135_26350_36863; path=/; domain=.baidu.com
Strict-Transport-Security: max-age=172800
Traceid: 1660646360018991617014534967514977812389
X-Frame-Options: sameorigin
X-Ua-Compatible: IE=Edge,chrome=1
Transfer-Encoding: chunked
学习HTTP主要就是学习请求和响应数据的具体格式内容。
HTTP协议特点
HTTP协议有它自己的一些特点,分别是:
基于TCP协议: 面向连接,安全
TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全。
基于请求-响应模型的:一次请求对应一次响应
请求和响应是一一对应关系
HTTP协议是无状态协议:对于事物处理没有记忆能力。每次请求-响应都是独立的
无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。这种特性有优点也有缺点,
- 缺点:多次请求间不能共享数据
- 优点:速度快
请求之间无法共享数据会引发的问题,如:
- 京东购物,
加入购物车
和去购物车结算
是两次请求, - HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
- 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据
但实际使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用
会话技术(Cookie、Session)
来解决这个问题。具体如何来做,后面会详细讲到。刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?
请求格式数据
格式介绍
请求数据总共分为三部分内容,分别是请求行
、请求头
、请求体
请求行: HTTP请求中的第一行数据,请求行包含三块内容,分别是 GET[请求方式] /[请求URL路径] HTTP/1.1[HTTP协议及版本]
请求方式有七种,最常用的是GET和POST
请求头: 第二行开始,格式为key: value形式
请求头中会包含若干个属性,常见的HTTP请求头有:
1
2
3
4
5
6PLAINTEXT
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等。这些数据有什么用处?
举例说明:服务端可以根据请求头中的内容来获取客户端的相关信息,有了这些信息服务端就可以处理不同的业务需求,比如:- 不同浏览器解析HTML和CSS标签的结果会有不一致,所以就会导致相同的代码在不同的浏览器会出现不同的效果
- 服务端根据客户端请求头中的数据获取到客户端的浏览器类型,就可以根据不同的浏览器设置不同的代码来达到一致的效果
- 这就是我们常说的浏览器兼容问题
请求体: POST请求的最后一部分,存储请求参数
请求体和请求头之间是有一个空行隔开(不过我现在用Chrome浏览器和Edge浏览器查看的时候,请求体和请求头都是分开的,具体看下面的实例演示)。此时浏览器发送的是POST请求,为什么不能使用GET呢?这时就需要回顾GET和POST两个请求之间的区别了:GET请求请求参数在请求行中,没有请求体,POST请求请求参数在请求体中
GET请求请求参数大小有限制,POST没有
实例演示
简单写了两个表单,分别来测试GET请求和POST请求的参数携带方式。
- get请求
- post请求
1 |
|
随便输入用户名和密码,在点击注册之前,用F12
打开开发者工具,点击Network
来查看请求的请求数据和响应数据具体的格式,得到如下结果
- get请求
- post请求
1 | PLAINTEXT |
GET请求请求参数在请求行(注意看第一行)中,没有请求体,POST请求请求参数在请求体中(注意看最后一行,其实这行是在payload中找到的,Chrome浏览器把请求参数和请求体分开了)
响应格式数据
格式介绍
响应数据总共分为三部分内容,分别是响应行
、响应头
、响应体
1 | PLAINTEXT |
响应行:响应数据的第一行,响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
响应头:第二行开始,格式为key:value形式
响应头中会包含若干个属性,常见的HTTP响应头有:
1
2
3
4
5PLAINTEXT
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg;
Content-Length:表示该响应内容的长度(字节数);
Content-Encoding:表示该响应压缩算法,例如gzip;
Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒响应体: 最后一部分。存放响应数据
上面的代码中<html>...</html>
这部分内容就是响应体,它和响应头之间有一个空行隔开(在Chrome浏览器中响应头和响应体也是分开的)。
响应状态码
状态码大类
状态码分类 | 说明 |
---|---|
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 |
客户端需要进行身份验证才能获得网络访问权限 |
Tomcat
简介
什么是Web服务器?
Web服务器是一个应用程序(软件),对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是”提供网上信息浏览服务”。
我们可以把自己写的Web项目部署到Web Tomcat服务器软件中,当Web服务器软件启动后,部署在Web服务器软件中的页面就可以直接通过浏览器来访问了。
Web服务器软件使用步骤
- 准备静态资源
- 下载安装Web服务器软件
- 将静态资源部署到Web服务器上
- 启动Web服务器使用浏览器访问对应的资源
Tomcat的相关概念:
Tomcat是Apache软件基金会一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范。
概念中提到了JavaEE规范,那什么又是JavaEE规范呢?
JavaEE: Java Enterprise Edition,Java企业版。指Java企业级开发的技术规范总和。包含13项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF。
因为Tomcat支持Servlet/JSP规范,所以Tomcat也被称为Web容器、Servlet容器。Servlet需要依赖Tomcat才能运行。
Tomcat的官网: https://tomcat.apache.org/ 从官网上可以下载对应的版本进行使用。
关于IDEA创建Maven Web项目和在Tomcat的使用这里就不过多介绍了。
Servlet
简介
- Servlet是JavaWeb最为核心的内容,它是Java提供的一门动态web资源开发技术。
- 使用Servlet就可以实现,根据不同的登录用户在页面上动态显示不同内容。
- Servlet是JavaEE规范之一,其实就是一个接口,将来我们需要定义Servlet类实现Servlet接口,并由web服务器运行Servlet
快速入门
需求分析: 编写一个Servlet类,并使用IDEA中Tomcat插件进行部署,最终通过浏览器访问所编写的Servlet程序。
具体的实现步骤为:
创建Web项目
web-demo
,导入Servlet依赖坐标1
2
3
4
5
6
7
8
9
10
11
12
<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>创建:定义一个类,实现Servlet接口,并重写接口中所有方法,并在service方法中输入一句话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
public class ServletDemo implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("hello servlet");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}配置:在类上使用@WebServlet注解,配置该Servlet的访问路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
//加上这个东东
public class ServletDemo implements Servlet {
public void init(ServletConfig servletConfig) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("hello servlet");
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}访问:启动Tomcat,浏览器中输入URL地址访问该Servlet
http://localhost:8080/web-demo/demo1
访问后,在控制台会打印
hello servlet
说明servlet程序已经成功运行。
至此,Servlet的入门案例就已经完成
执行流程
Servlet程序已经能正常运行,但是我们需要思考个问题: 我们并没有创建ServletDemo类的对象,也没有调用service中的方法,为什么控制台就输出了hello servlet
这句话呢?
要解答上述问题,我们就需要对Servlet的执行流程进行一个学习。
浏览器发出
1
http://localhost:8080/web-demo/demo1
请求,从请求中可以解析出三部分内容,分别是
1
localhost:8080
、
1
web-demo
、
1
dem
- 根据
localhost:8080
可以找到要访问的Tomcat Web服务器 - 根据
web-demo
可以找到部署在Tomcat服务器上的web-demo项目 - 根据
demo1
可以找到要访问的是项目中的哪个Servlet类,根据@WebServlet后面的值进行匹配
- 根据
找到ServletDemo这个类后,Tomcat Web服务器就会为ServletDemo这个类创建一个对象,然后调用对象中的service方法
- ServletDemo实现了Servlet接口,所以类中必然会重写service方法供Tomcat Web服务器进行调用
- service方法中有ServletRequest和ServletResponse两个参数,ServletRequest封装的是请求数据,ServletResponse封装的是响应数据,后期我们可以通过这两个参数实现前后端的数据交互
小结
介绍完Servlet的执行流程,需要大家掌握两个问题:
Servlet由谁创建?Servlet方法由谁调用?
Servlet由web服务器创建,Servlet方法由web服务器调用
服务器怎么知道Servlet中一定有service方法?
因为我们自定义的Servlet,必须实现Servlet接口并复写其方法,而Servlet接口中有service方法
生命周期
介绍完Servlet的执行流程后,我们知道Servlet是由Tomcat Web服务器帮我们创建的。
接下来咱们再来思考一个问题:Tomcat什么时候创建的Servlet对象?
要想回答上述问题,我们就需要对Servlet的生命周期进行一个学习。
生命周期: 对象的生命周期指一个对象从被创建到被销毁的整个过程。
Servlet运行在Servlet容器(web服务器)中,其生命周期由容器来管理,分为4个阶段:
加载和实例化:默认情况下,当Servlet第一次被访问时,由容器创建Servlet对象
默认情况,Servlet会在第一次访问被容器创建,但是如果创建Servlet比较耗时的话,那么第一个访问的人等待的时间就比较长,用户的体验就比较差,那么我们能不能把Servlet的创建放到服务器启动的时候来创建,具体如何来配置?
1
2loadOnstartup的取值有两类情况
(1)负整数:第一次访问时创建Servlet对象
(2)0或正整数:服务器启动时创建Servlet对象,数字越小优先级越高
初始化:在Servlet实例化之后,容器将调用Servlet的init()方法初始化这个对象,完成一些如加载配置文件、创建连接等初始化的工作。该方法只调用一次
请求处理:每次请求Servlet时,Servlet容器都会调用Servlet的service()方法对请求进行处理
服务终止:当需要释放内存或者容器关闭时,容器就会调用Servlet实例的destroy()方法完成资源的释放。在destroy()方法调用之后,容器会释放这个Servlet实例,该实例随后会被Java的垃圾收集器所回收
通过案例演示下上述的生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
//设置为1之后,在我们访问http://localhost:8080/web-demo/demo之前,就会进行初始化操作了,第一个人访问的时候就不会等那么久了
public class ServletDemo implements Servlet {
/**
* 初始化方法
* 1.调用时机:默认情况下,Servlet被第一次访问时,调用
* * loadOnStartup: 默认为-1,修改为0或者正整数,则会在服务器启动的时候,调用
* 2.调用次数: 1次
*
* @param servletConfig
* @throws ServletException
*/
public void init(ServletConfig servletConfig) throws ServletException {
System.out.println("init..");
}
/**
* 提供服务
* 1.调用时机:每一次Servlet被访问时,调用
* 2.调用次数: 多次
*
* @param servletRequest
* @param servletResponse
* @throws ServletException
* @throws IOException
*/
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("hello servlet");
}
/**
* 销毁方法
* 1.调用时机:内存释放或者服务器关闭的时候,Servlet对象会被销毁,调用
* 2.调用次数: 1次
*/
public void destroy() {
System.out.println("destroy...");
}
public String getServletInfo() {
return null;
}
public ServletConfig getServletConfig() {
return null;
}
}
小结
Servlet对象在什么时候被创建的?
默认是第一次访问的时候被创建,可以使用
@WebServlet(urlPatterns = "/demo2",loadOnStartup = 1)
修改成在服务器启动的时候创建。Servlet生命周期中涉及到的三个方法,这三个方法是什么?什么时候被调用?调用几次?
涉及到三个方法,分别是 init()、service()、destroy()
init方法在Servlet对象被创建的时候执行,只执行1次
service方法在Servlet被访问的时候调用,每访问1次就调用1次(每刷新一次界面,控制台就会输出一次)
destroy方法在Servlet对象被销毁的时候调用,只执行1次
方法介绍
Servlet中总共有5个方法,我们已经介绍过其中的三个,剩下的两个方法是:
获取Servlet信息
1
2
3
4
5
6
String getServletInfo()
//该方法用来返回Servlet的相关信息,没有什么太大的用处,一般我们返回一个空字符串即可
public String getServletInfo() {
return "";
}获取ServletConfig对象
1
2
ServletConfig getServletConfig()
ServletConfig对象,在init方法的参数中有,而Tomcat Web服务器在创建Servlet对象的时候会调用init方法,必定会传入一个ServletConfig对象,我们只需要将服务器传过来的ServletConfig进行返回即可。具体如何操作?
创建一个私有的ServletConfig对象,当我们调用init方法的时候,将传入的ServletConfig对象赋给私有的ServletConfig对象,代码如下
1 |
|
getServletInfo()和getServletConfig()这两个方法使用的不是很多,了解一下就行。
体系结构
通过上面的学习,我们知道要想编写一个Servlet就必须要实现Servlet接口,重写接口中的5个方法,虽然已经能完成要求,但是编写起来还是比较麻烦的,因为我们更关注的其实只有service方法,那有没有更简单方式来创建Servlet呢?
要想解决上面的问题,我们需要先对Servlet的体系结构进行下了解:
因为我们将来开发B/S架构的web项目,都是针对HTTP协议,所以我们自定义Servlet,会通过继承HttpServlet
1 |
|
要想发送一个GET请求,请求该Servlet,只需要通过浏览器发送
http://localhost:8080/web-demo/demo4
,就能看到doGet方法被执行了要想发送一个POST请求,请求该Servlet,单单通过浏览器是无法实现的,这个时候就需要编写一个form表单来发送请求,在webapp下创建一个
1
index.html
页面,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/web_demo_war_exploded/demo" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交">
</form>
</body>
</html>启动测试,在控制台可看到doPost方法被执行了。
Servlet的简化编写就介绍完了,接着需要思考两个问题:
- HttpServlet中为什么要根据请求方式的不同,调用不同的方法?
- 如何调用?
针对问题一,我们需要回顾之前的知识点前端发送GET和POST请求的时候,参数的位置不一致,GET请求参数在请求行中,POST请求参数在请求体中,为了能处理不同的请求方式,我们得在service方法中进行判断,然后写不同的业务处理,这样能实现,但是每个Servlet类中都将有相似的代码,针对这个问题,有什么可以优化的策略吗?
可以把这些相似的代码写入一个类当中,HttpServlet已经帮我们解决了,下面来看看HttpServlet中的service方法,首先先获取请求方法,随后根据不同的请求方法来进行不同的操作。
前面说过一共有7种请求方法,HttpServlet类中已经包含了这7种,不仅可以处理GET和POST还可以处理其他五种请求方式。
1 |
|
小结
HttpServlet的使用步骤
继承HttpServlet
重写doGet和doPost方法HttpServlet原理
获取请求方式,并根据不同的请求方式,调用不同的doXxx方法
urlPattern配置
Servlet类编写好后,要想被访问到,就需要配置其访问路径(==urlPattern==)
一个Servlet,可以配置多个urlPattern,例如
1
2JAVA
在浏览器上输入
http://localhost:8080/web-demo/demo1
,http://localhost:8080/web-demo/demo2
这两个地址都能访问到ServletDemo的doGet方法。urlPattern配置规则
精确匹配
1
2访问路径
1
http://localhost:8080/web-demo/user/select
目录匹配
1
2访问路径
1
http://localhost:8080/web-demo/user/任意
扩展名匹配
1
2
//注意这里没加斜杠访问路径
1
http://localhost:8080/web-demo/任意.do
任意匹配
1
2
3
4
//或访问路径
1
http://localhost:8080/demo-web/任意
注意:/
和/*
的区别?
- 当我们的项目中的Servlet配置了 “/“,会覆盖掉tomcat中的DefaultServlet,当其他的url-pattern都匹配不上时都会走这个Servlet
- 当我们的项目中配置了”/*”,意味着匹配任意访问路径
- DefaultServlet是用来处理静态资源,如果配置了”/“会把默认的覆盖掉,就会引发请求静态资源的时候没有走默认的而是走了自定义的Servlet类,最终导致静态资源不能被访问(不必太过担心这种问题,因为我们几乎不会配置这种匹配方式)
小结
- urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配
- 五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > / ,无需记,以最终运行结果为准。
XML配置
前面对应Servlet的配置,我们都使用的是@WebServlet,这个是Servlet从3.0版本后开始支持注解配置,3.0版本前只支持XML配置文件的配置方法。
对于XML的配置步骤有两步:
编写Servlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.web;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
public class ServletDemo extends MyHttpServlet {
protected void doGet(ServletRequest req, ServletResponse res) {
System.out.println("demo get...");
}
protected void doPost(ServletRequest req, ServletResponse res) {
}
}在web.xml中配置该Servlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<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_4_0.xsd"
version="4.0">
<!--
Servlet 全类名
-->
<servlet>
<!-- servlet的名称,名字任意-->
<servlet-name>demo13</servlet-name>
<!--servlet的类全名-->
<servlet-class>com.itheima.web.ServletDemo13</servlet-class>
</servlet>
<!--
Servlet 访问路径
-->
<servlet-mapping>
<!-- servlet的名称,要和上面的名称一致-->
<servlet-name>demo13</servlet-name>
<!-- servlet的访问路径-->
<url-pattern>/demo13</url-pattern>
</servlet-mapping>
</web-app>
这种配置方式和注解比起来,确认麻烦很多,所以建议使用注解来开发。但是还是要认识上面这种配置方式,因为并不是所有的项目都是基于注解开发的。
本文摘要:
- 掌握Request对象的概念与使用
- 掌握Response对象的概念与使用
- 能够完成用户登录注册案例的实现
- 能够完成SqlSessionFactory工具类的抽取
Request和Response的概述
Request是请求对象,Response是响应对象。这两个对象在我们使用Servlet的时候有看到:
1 |
|
那么request和response这两个参数的作用是什么?
- request:获取请求数据
- 浏览器会发送HTTP请求到后台服务器(Tomcat)
- HTTP的请求中会包含很多请求数据(请求行+请求头+请求体)
- 后台服务器(Tomcat)会对HTTP请求的数据进行解析,并把解析解惑存入到一个对象中
- 所存入的对象即为request对象,我们可以从request对象中获取请求的相关参数
- 获取到数据后就可以继续后续的业务,比如获取用户名和密码就可以实现登录操作的相关业务
- response:设置响应数据
- 业务处理完后,后台就需要给前端返回业务处理的结果,即响应数据
- 把响应数据封装到response对象中
- 后台服务器(Tomcat)会解析response对象,按照
响应行+响应头+响应体
的格式拼接结果 - 浏览器最终解析结果,把内容展示在浏览器给用户浏览
通过一个案例来初步体验下request和response对象的使用。
写一个表单,请求方式为GET,当我们输入不同的username,并点击提交时,界面上就会出现username,欢迎访问
1 |
|
启动成功后就可以通过浏览器来访问,并且根据传入参数的不同就可以在页面上展示不同的内容:
1 |
|
小结
在这节中,主要认识了一下request对象和reponse对象:
- request对象是用来封装请求数据的对象
- response对象是用来封装响应数据的对象
目前我们只知道这两个对象是用来干什么的,那么它们具体是如何实现的,就需要我们继续深入的学习。接下来,就先从Request对象来学习,主要学习下面这些内容:
- request继承体系
- request获取请求参数
- request请求转发
Request对象
Request继承体系
ServletRequest和HttpServletRequest是继承关系,并且两个都是接口,接口是无法创建对象,那么方法里的HttpServletRequest参数是从哪儿来的呢?
1 |
|
这个时候,我们就需要用到Request继承体系中的RequestFacade
:
- 该类实现了HttpServletRequest接口,也间接实现了ServletRequest接口。
- Servlet类中的service方法、doGet方法或者是doPost方法最终都是由Web服务器(Tomcat)来调用的,所以Tomcat提供了方法参数接口的具体实现类,并完成了对象的创建
- 要想了解RequestFacade中都提供了哪些方法,我们可以直接查看JavaEE的API文档中关于ServletRequest和HttpServletRequest的接口文档,因为RequestFacade实现了其接口就需要重写接口中的方法
对于上述结论,要想验证,可以编写一个Servlet,在方法中把request对象打印下,就能看到最终的对象是不是RequestFacade,代码如下:
1 |
|
启动服务器访问页面,控制台输出org.apache.catalina.connector.RequestFacade@36bf693c
小结
- Request的继承体系为ServletRequest—>HttpServletRequest—>RequestFacade
- Tomcat需要解析请求数据,封装为request对象,并且创建request对象传递到service方法
- 使用request对象,可以查阅JavaEE API文档的HttpServletRequest接口中方法说明
Request获取请求数据
HTTP请求数据总共分为三部分内容,分别是请求行、请求头、请求体,对于这三部分内容的数据,分别该如何获取,首先我们先来学习请求行数据如何获取?
获取请求行数据
请求行包含三块内容,分别是请求方式
、请求资源路径
、HTTP协议及版本
,例如GET /tomcat_demo_war/index.html?username=suger1201&password=dsaasd HTTP/1.1
对于这三部分内容,request对象都提供了对应的API方法来获取,具体如下:
获取请求方式:
GET
1
2
String getMethod()获取虚拟目录(项目访问路径):
/request-demo
1
2
String getContextPath()获取URL(统一资源定位符):
http://localhost:8080/request-demo/req1
1
2
StringBuffer getRequestURL()获取URI(统一资源标识符):
/request-demo/req1
1
2
String getRequestURI()获取请求参数(GET方式):
username=zhangsan&password=123
1
2
String getQueryString()
介绍完上述方法后,咱们通过代码把上述方法都使用下:
- Servlet
- HTML表单
1 |
|
启动服务器,并向表单中随便输入一个username,然后点击提交,控制台输出如下
请求方式:GET
项目访问路径:/web_demo_war_exploded
URL:http://localhost:8080/web_demo_war_exploded/demo
URI:/web_demo_war_exploded/demo
请求参数:username=Cyderpunk2077%40gmail.com
获取请求头数据
对于请求头的数据,格式为key: value
所以根据请求头名称获取对应值的方法为
1 |
|
接下来,在代码中如果想要获取客户端浏览器的版本信息,则可以使用
1 |
|
启动服务器,随便输入数据然后提交,控制台输出如下
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
获取请求体数据
浏览器在发送GET请求的时候是没有请求体的,所以需要把请求方式变更为POST
1 |
|
对于请求体中的数据,Request对象提供了如下两种方式来获取其中的数据,分别是:
获取字节输入流,如果前端发送的是字节数据,比如传递的是文件数据,则使用该方法
1
2
ServletInputStream getInputStream()获取字符输入流,如果前端发送的是纯文本数据,则使用该方法
1
2
BufferedReader getReader()下面我们在Servlet的doPost方法中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
public class ServletDemo extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("get??");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//由于获取的是纯文本数据,所以这里用的getReader()
BufferedReader bufferedReader = req.getReader();
String line = bufferedReader.readLine();
System.out.println(line);
}
}BufferedReader流是通过request对象来获取的,当请求完成后request对象就会被销毁,request对象被销毁后,BufferedReader流就会自动关闭,所以此处就不需要手动关闭流了。
启动服务器,访问 http://localhost:8080/web_demo_war_exploded/index.html ,填入username并提交,在控制台就可以看到前端发送的请求数据了
小结
HTTP请求数据中包含了请求行
、请求头
和请求体
,针对这三部分内容,Request对象都提供了对应的API方法来获取对应的值:
- 请求行
- getMethod()获取请求方式
- getContextPath()获取项目访问路径
- getRequestURL()获取请求URL
- getRequestURI()获取请求URI
- getQueryString()获取GET请求方式的请求参数
- 请求头
- getHeader(String name)根据请求头名称获取其对应的值
- 请求体
- 注意: 浏览器发送的POST请求才有请求体
- 如果是纯文本数据:getReader()
- 如果是字节数据如文件数据:getInputStream()
获取请求参数的通用方式
请求参数获取方式
GET方式:
1
2
String getQueryString()POST方式:
1
2
BufferedReader getReader();
思考:
GET请求方式和POST请求方式区别主要在于获取请求参数的方式不一样,是否可以提供一种统一获取请求参数的方式,从而统一doGet和doPost方法内的代码?
解决方式一:
1 |
|
使用request的getMethod()来获取请求方式,根据请求方式的不同分别获取请求参数值,这样就可以解决上述问题,但是以后每个Servlet都需要这样写代码,实现起来比较麻烦,这种方案我们不采用
解决方式二:
request对象已经将上述获取请求参数的方法进行了封装,并且request提供的方法实现的功能更强大,以后只需要调用request提供的方法即可,在request的方法中都实现了哪些操作呢?
- 根据不同的请求方式获取请求参数,例如:
username=zhangsan&password=asd123&hobby=1&hobby=2
- 把获取到的内容进行分割,
username=zhangsan&password=asd123&hobby=1
->username=zhangsan password=asd123 hobby=1 hobby=2
->username zhangsan password asd123 hobby 1 hobby 2
- 把分割后端数据,存入到一个Map集合中,其中Map集合的泛型为
<String,String[]>
,因为参数的值可能是一个,也可能有多个,所以value的值的类型为String数组。
基于上述理论,request对象为我们提供了如下方法:
获取所有参数Map集合
1
2
Map<String,String[]> getParameterMap()根据名称(Key)获取参数值(Value)(数组)
1
2
String[] getParameterValues(String name)根据名称(Key)获取参数值(Value)(单个值)
1
2
String getParameter(String name)
接下来,我们通过案例来把上述的三个方法进行实例演示:
随便写一个表单,加上一个复选框,爱好可以多选,所以到时候的参数值就不止一个了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/web_demo_war_exploded/demo" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
爱好:<input type="checkbox" name="hobby" value="1">Apex
<input type="checkbox" name="hobby" value="2">Terraria<br>
<input type="submit" value="提交">
</form>
</body>
</html>在Servlet代码中获取页面传递请求的参数值
- getParameterMap()
- getParameterValues(String name)
- getParameter(String name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import javax.servlet.*;
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.Map;
public class ServletDemo extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取所有参数的Map集合
Map<String, String[]> parameterMap = req.getParameterMap();
//遍历Map
for (String key : parameterMap.keySet()) {
System.out.print(key+":");
String[] values = parameterMap.get(key);
for (String v : values) {
System.out.print(v+" ");
}
System.out.println();
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
/*
获取的结果如下
username:Cyderpunk2077@gmail.com
password:dsaasd
hobby:1 2
*/
如果你在post请求方式下输入了中文username并提交,控制台会输出乱码
解决方案:通过req.setCharacterEncoding("UTF-8");
设置编码,UTF-8也可以写成小写
Request请求转发
请求转发(forward):一种在服务器内部的资源跳转方式。
- 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
- 资源A处理完请求后将请求发给资源B
- 资源B处理完后将结果响应给浏览器
- 请求从资源A到资源B的过程就叫请求转发
测试步骤
创建一个RequestDemo1类,接收/req5的请求,在doGet方法中打印
1
这里是RequestDem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo1");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}创建一个RequestDemo2类,接收/req6的请求,在doGet方法中打印
1
这里是RequestDem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
public class RequestDemo2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo2");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}在RequestDemo1的方法中使用
1
req.getRequestDispatcher("/RequestDemo2").forward(req,resp)
进行请求转发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo1");
//加上这行
request.getRequestDispatcher("/RequestDemo2").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}启动测试
当我们访问
1
RequestDem
时,控制台会得到如下输出
这里是RequestDemo1
这里是RequestDemo2
说明请求已经转发到了/RequestDemo2
在转发的同时我们还可以传递数据给/RequestDemo2
request对象提供了三个方法:
存储数据到request域[范围,数据是存储在request对象]中
1
2
void setAttribute(String name,Object o);根据key获取值
1
2
Object getAttribute(String name);根据key删除该键值对
1
2
void removeAttribute(String name);
接着上个需求来:
在RequestDemo1的doGet方法中转发请求之前,将数据存入request域对象中.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo1");
//存储数据
request.setAttribute("msg","HELLO~");
//请求转发
request.getRequestDispatcher("/RequestDemo2").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}在RequestDemo2的doGet方法从request域对象中获取数据,并将数据打印到控制台.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RequestDemo2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是RequestDemo2");
Object msg = request.getAttribute("msg");
System.out.println(msg);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}启动访问测试,得到的输出结果如下
这里是RequestDemo1
这里是RequestDemo2
HELLO~
此时就可以实现在转发多个资源之间共享数据。
请求转发的特点
- 浏览器地址栏路径不发生变化
虽然后台从/这里是RequestDemo1
转发到/这里是RequestDemo2
,但是浏览器的地址一直是/这里是RequestDemo1
,未发生变化 - 只能转发到当前服务器的内部资源
不能从一个服务器通过转发访问另一台服务器 - 一次请求,可以在转发资源间使用request共享数据
虽然后台从/RequestDemo1
转发到/RequestDemo2
,但是这个只有一次请求
Response对象
Reponse的继承体系和Request的继承体系也非常相似:
Response设置响应数据功能介绍
HTTP响应数据总共分为三部分内容,分别是==响应行、响应头、响应体==,对于这三部分内容的数据,respone对象都提供了哪些方法来进行设置?
响应行
响应行包含三块内容,分别是 HTTP/1.1[HTTP协议及版本] 200[响应状态码] ok[状态码的描述]
对于响应头,比较常用的就是设置响应状态码:
1
2
void setStatus(int sc);响应头
响应头的格式为key:value形式
设置响应头键值对:1
2
void setHeader(String name,String value);响应体
对于响应体,是通过字符、字节输出流的方式往浏览器写,
获取字符输出流:1
2
PrintWriter getWriter();获取字节输出流
1
2
ServletOutputStream getOutputStream();
Response请求重定向
- Response重定向(redirect):一种资源跳转方式。
- 浏览器发送请求给服务器,服务器中对应的资源A接收到请求
- 资源A现在无法处理该请求,就会给浏览器响应一个302的状态码+location的一个访问资源B的路径(要加上资源B的虚拟目录)
- 浏览器接收到响应状态码为302就会重新发送请求到location对应的访问地址去访问资源B
- 资源B接收到请求后进行处理并最终给浏览器响应结果,这整个过程就叫重定向
重定向的实现方式
1
2
3
4
5
response.setStatus(302);
response.setHeader("location","资源B的访问路径");
//或
resposne.sendRedirect("资源B的访问路径");具体的实现方式
创建一个ResponseDemo1类,接收/ResponseDemo1的请求,在doGet方法中打印
1
这里是ResponseDemo1
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ResponseDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo1");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}创建一个ResponseDemo2类,接收/ResponseDemo1的请求,在doGet方法中打印
1
这里是ResponseDemo2
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ResponseDemo2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo2");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}在ResponseDemo1的方法中使用
1
response.sendRedirect("/web_demo_war_exploded/RequestDemo2");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ResponseDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("这里是ResponseDemo1");
//重定向
response.sendRedirect("/web_demo_war_exploded/RequestDemo2");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}启动测试,访问
1
/ResponseDemo1
,控制台得到如下输出,同时地址栏的地址也变更为
1
/RequestDem
这里是RequestDemo1
这里是RequestDemo2
重定向的特点
- 浏览器地址栏路径发送变化
当进行重定向访问的时候,由于是由浏览器发送的两次请求,所以地址会发生变化 - 可以重定向到任何位置的资源(服务内容、外部均可)
因为第一次响应结果中包含了浏览器下次要跳转的路径,所以这个路径是可以任意位置资源。 - 两次请求,不能在多个资源使用request共享数据
因为浏览器发送了两次请求,是两个不同的request对象,就无法通过request对象进行共享数据
介绍完请求重定向
和请求转发
以后,接下来把这两个放在一块对比下:
重定向特点 | 请求转发特点 |
---|---|
浏览器地址栏路径发生变化 | 浏览器地址栏路径不发生变化 |
可以重定向到任意位置的资源(服务器内部、外部均可) | 只能转发到当前服务器的内部资源 |
两次请求,不能在多个资源使用request共享数据 | 一次请求,可以在转发的资源间使用request共享数据 |
路径问题
问题1:转发的时候路径上没有加虚拟目录
web_demo_war_exploded
,而重定向加了,那么到底什么时候需要加,什么时候不需要加呢?其实判断的依据很简单,只需要记住下面的规则即可:
浏览器使用:需要加虚拟目录(项目访问路径)
服务端使用:不需要加虚拟目录
对于转发来说,因为是在服务端进行的,所以不需要加虚拟目录
对于重定向来说,路径最终是由浏览器来发送请求,就需要添加虚拟目录。
掌握了这个规则,接下来就通过一些练习来强化下知识的学习:
1 | PLAINTEXT |
- 问题2:在重定向的代码中,
`web_demo_war_exploded
是固定编码的,如果后期通过Tomcat插件配置了项目的访问路径,那么所有需要重定向的地方都需要重新修改,该如何优化?
我们可以去pom.xml配置文件中配置项目的访问地址,然后在代码中动态去获取项目访问的虚拟目录,request对象中提供了getContextPath()方法
- XML配置文件
- ResponseDemo1
配置项目的访问地址,无视掉那个tomcat的插件
1 |
|
重新启动访问测试,功能依然能够实现,此时就可以动态获取项目访问的虚拟路径,从而降低代码的耦合度。
Response响应字符数据
要想将字符数据写回到浏览器,我们需要两个步骤:
- 通过Response对象获取字符输出流: PrintWriter writer = resp.getWriter();
- 通过字符输出流写数据: writer.write(“aaa”);
接下来,我们实现通过些案例把响应字符数据给实际应用下:
返回一个简单的字符串
hello world
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
PrintWriter printWriter = response.getWriter();
printWriter.write("hello world");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}返回一串html字符串,并且能被浏览器解析
返返回一串html字符串,并且能被浏览器解析,需要注意设置响应数据的编码为
utf-8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应的数据格式及数据的编码
response.setContentType("text/html;charset=utf-8");
PrintWriter printWriter = response.getWriter();
printWriter.write("<h1>你好<h1>");
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
Response响应字节数据
要想将字节数据写回到浏览器,我们需要两个步骤:
- 通过Response对象获取字节输出流:ServletOutputStream outputStream = resp.getOutputStream();
- 通过字节输出流写数据: outputStream.write(字节数据);
接下来,我们实现通过些案例把响应字符数据给实际应用下:
返回一个图片文件到浏览器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.FileInputStream;
import java.io.IOException;
public class RequestDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
FileInputStream fis = new FileInputStream("D:\\background.jpg");
ServletOutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = fis.read(buffer)) != -1){
os.write(buffer,0,len);
}
fis.close();
//不需要关闭ServletOutputStream,response会帮我们关闭
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
用户注册登录案例
用户登录
需求分析
- 用户在登录页面输入用户名和密码,提交请求给LoginServlet
- 在LoginServlet中接收请求和数据[用户名和密码]
- 在LoginServlt中通过Mybatis实现调用UserMapper来根据用户名和密码查询数据库表
- 将查询的结果封装到User对象中进行返回
- 在LoginServlet中判断返回的User对象是否为null
- 如果为nul,说明根据用户名和密码没有查询到用户,则登录失败,返回”登录失败”数据给前端
- 如果不为null,则说明用户存在并且密码正确,则登录成功,返回”登录成功”数据给前端
环境准备
写一个简单的表单提交
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--注意将请求提交给loginServlet-->
<form action="/web_demo_war_exploded/loginServlet" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>创建db1数据库,创建tb_user表,创建User实体类
- tb_user表
- User实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 创建数据库
CREATE DATABASE db1;
USE db1;
-- 创建用户表
CREATE TABLE tb_user(
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(20) UNIQUE,
PASSWORD VARCHAR(32)
);
-- 添加数据
INSERT INTO tb_user(username,PASSWORD) VALUES('zhangsan','123'),('lisi','234');
SELECT * FROM tb_user;
导入MyBatis坐标,MySQL驱动坐标
1
2
3
4
5
6
7
8
9
10
11
<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.34</version>
</dependency>创建mybatis-config.xml核心配置文件,UserMapper.xml映射文件,UserMaper接口
- mybatis-config.xml
- UserMapper.xml
- UserMaper接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:13306/db1?useSSL=false&useServerPrepStmts=true"/>
<property name="username" value="root"/>
<property name="password" value="PASSWORD."/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.blog.mapper"/>
</mappers>
</configuration>
代码实现
在UserMapper接口中提供一个根据用户名和密码查询用户对象的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
//由于逻辑比较简单,所以这里用的注解
public interface UserMapper {
/**
* 根据用户名和密码查询是否有此用户
* @param username 用户名
* @param password 密码
* @return 用户
*/
User select(; String username, String password)
}编写LoginServlet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class LoginServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应格式和字符编码
response.setContentType("text/html;charset=utf-8");
//1. 接收用户名和密码
String password = request.getParameter("password");
String username = request.getParameter("username");
//2. 调用MyBatis完成查询
//2.1 获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2.2 获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//2.3 获取mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//2.4 调用方法
User user = mapper.select(username, password);
//2.5 释放资源
sqlSession.close();
//3. 获取字符输出流,并判断user是否为null
PrintWriter writer = response.getWriter();
if (user != null) {
writer.write("登陆成功");
} else {
writer.write("登陆失败");
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}启动服务器测试
如果输入用户名或密码错误,则显示登陆失败
用户名和密码正确,则登录成功
可能遇到的问题:java.lang.NoClassDefFoundError: org/apache/ibatis/io/Resources
去看看Tomcat的lib目录下有没有mybatis的jar包,如果没有则导入一个
至此,一个极其简易的用户登录的功能就完成了
用户注册
需求分析
- 用户在注册页面输入用户名和密码,提交请求给RegisterServlet
- 在RegisterServlet中接收请求和数据[用户名和密码]
- 在RegisterServlet中通过Mybatis实现调用UserMapper来根据用户名查询数据库表
- 将查询的结果封装到User对象中进行返回
- 在RegisterServlet中判断返回的User对象是否为null
- 如果为nul,说明根据用户名可用,则调用UserMapper来实现添加用户
- 如果不为null,则说明用户不可以,返回”用户名已存在”数据给前端
代码编写
编写一个注册页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>欢迎注册</h1>
<a href="login.html">已有账号?点击登录</a>
<form action="/web_demo_war_exploded/registerServlet" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" id="username"></td>
<br>
<span id="username_arr" style="display: none">用户名已被占用</span>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password" id="password"></td>
</tr>
</table>
<input type="submit" value="注册">
</form>
</body>
</html>编写UserMapper提供根据用户名查询用户数据方法和添加用户方法
1
2
3
4
5
6
User selectByUserName(; String username)
void add(User user);创建RegisterServlet类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class RegisterServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置响应格式和字符编码
response.setContentType("text/html;charset=utf-8");
//获取注册信息
String password = request.getParameter("password");
String username = request.getParameter("username");
//封装用户对象
User user = new User();
user.setUsername(username);
user.setPassword(password);
//获取SqlSessionFactory对象
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//获取SqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//获取mapper
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//调用方法
User tmp = mapper.selectByUserName(username);
//获取字符输出流
PrintWriter writer = response.getWriter();
//判断用户名是否已经存在
if (tmp == null) {
mapper.add(user);
//提交事务
sqlSession.commit();
//释放资源
sqlSession.close();
//提示信息
writer.write("注册成功");
} else {
//提示信息
writer.write("用户名已存在");
}
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}启动服务器进行测试
如果测试成功,则在数据库中就能查看到新注册的数据
如果用户已经存在,则在页面上展示用户名已存在
的提示信息
SqlSessionFactory工具类抽取
上面两个功能已经实现,但是在写Servlet的时候,因为需要使用Mybatis来完成数据库的操作,所以对于Mybatis的基础操作就出现了些重复代码,如下
1 |
|
有了这些重复代码就会造成一些问题:
- 重复代码不利于后期的维护
- SqlSessionFactory工厂类进行重复创建
- 就相当于每次买手机都需要重新创建一个手机生产工厂来给你制造一个手机一样,资源消耗非常大但性能却非常低。所以这么做是不允许的。
那如何来优化呢?
- 代码重复可以抽取工具类
- 对指定代码只需要执行一次可以使用静态代码块
1 |
|
工具类抽取以后,以后在对Mybatis的SqlSession进行操作的时候,就可以直接使用
1 |
|
这样就可以很好的解决上面所说的代码重复和重复创建工厂导致性能低的问题了。
1 |
|
本文摘要:
- 理解 JSP 及 JSP 原理
- 能在 JSP中使用
EL表达式
和JSTL标签
- 理解
MVC模式
和三层架构
- 能完成品牌数据的增删改查功能
JSP概述
JSP(全称:Java Server Pages):Java 服务端页面。是一种动态的网页技术,其中既可以定义 HTML、JS、CSS等静态内容,还可以定义 Java代码的动态内容,也就是 JSP = HTML + Java
。如下就是jsp代码
1 |
|
上面代码 h1
标签内容是展示在页面上,而 Java 的输出语句是输出在 idea 的控制台。
JSP快速入门
搭建环境
创建一个maven的web项目
导入JSP依赖
在dependencies标签中导入JSP依赖
1 |
|
注意将该依赖的 scope
设置为 provided
,因为 tomcat 中有这个jar包了,所以在打包时我们是不希望将该依赖打进到我们工程的war包中。
创建JSP页面
在项目的 webapp
下创建一个名为hello.jsp的页面
编写代码
在 hello.jsp
页面中书写 HTML
标签和 Java
代码,如下
1 |
|
测试
启动服务器并访问该页面,我们可以在页面上看到一号标题的 hello jsp
,同时在IDEA的控制台可以看到输出的HELLO JSP
内容
JSP原理
之前说 JSP 就是一个页面,那么在 JSP 中写 html
标签,这很容易理解,但是为什么还可以写 Java
代码呢?
因为JSP 本质上就是一个 Servlet。下面我们来说说访问jsp时的流程
- 浏览器第一次访问
hello.jsp
页面,服务器会解析请求消息,找是否有index.jsp资源 - 如果有,
tomcat
会将hello.jsp
转换为名为hello_jsp.java
的一个Servlet
,但.java文件不能直接被访问 - 所以
tomcat
再将转换的Servlet
编译成字节码文件hello_jsp.class
tomcat
会执行该字节码文件,向外提供服务
如果使用的是IDEA,那么可以在C:\Users\username\AppData\Local\JetBrains\IntelliJIdea2023.2\tomcat\虚拟路径\org\apache\jsp
目录下找到转换后的.java文件和.class文件
打开 hello_jsp.java
文件,来查看里面的代码
1 |
|
第15行,hello_jsp类继承了HttpJspBase
这个类,通过查看HttpJspBase
类的继承关系,发现HttpJspBase
类继承了 HttpServlet
;那么 hello_jsp
这个类就间接的继承了 HttpServlet
,也就说明 hello_jsp
是一个 servlet
。
继续阅读 hello_jsp
类的代码,可以看到有一个名为 _jspService()
的方法,该方法就是每次访问 jsp
时自动执行的方法,和 servlet
中的 service
方法一样 。
而在 _jspService()
方法中可以看到往浏览器写标签的代码:
1 |
|
以前我们自己写 servlet
时,这部分代码是由我们自己来写,现在有了 jsp
后,由tomcat完成这部分功能。
JSP脚本
SP脚本用于在 JSP页面内定义 Java代码。在之前的入门案例中我们就在 JSP 页面定义的 Java 代码就是 JSP 脚本。
JSP脚本分类
JSP 脚本有如下三个分类:
- <%…%>:内容会直接放到_jspService()方法之中
- <%=…%>:内容会放到out.print()中,作为out.print()的参数,此方法可以将Java代码中获取的数据动态的展示到页面中
- <%!…%>:内容会放到_jspService()方法之外,被类直接包含
案例
需求
使用JSP脚本展示品牌数据
- 效果
- 源码
在该案例中数据不从数据库中查询,而是在 JSP 页面上写死
序号 | 品牌名称 | 企业名称 | 排序 | 品牌介绍 | 状态 |
---|---|---|---|---|---|
1 | 三只松鼠 | 三只松鼠 | 100 | 三只松鼠,好吃不上火 | 启用 |
2 | 优衣库 | 优衣库 | 10 | 优衣库,服适人生 | 禁用 |
3 | 小米 | 小米科技有限公司 | 1000 | 为发烧而生 | 启用 |
实现
在项目的
webapp
中创建brand.jsp
,并将刚刚写死的页面中的内容拷贝过来。在
1 | brand.jsp |
中准备一些数据,假装是从数据库来的数据
1 |
|
- 将
1 | brand.jsp |
页面中的
1 | table |
标签中的数据改为动态的
1 |
|
成品代码
1 |
|
JSP缺点
通过上面的案例,我们可以看到 JSP 的很多缺点。
由于 JSP页面内,既可以定义 HTML 标签,又可以定义 Java代码,造成了以下问题:
- 书写麻烦:特别是复杂的页面,既要写 HTML 标签,还要写 Java 代码
- 阅读麻烦
- 复杂度高:运行需要依赖于各种环境,JRE,JSP容器,JavaEE…
- 占内存和磁盘:JSP会自动生成.java和.class文件占磁盘,运行的是.class文件占内存
- 调试困难:出错后,需要找到自动生成的.java文件进行调试
- 不利于团队协作:前端人员不会 Java,后端人员不精 HTML
- 如果页面布局发生变化,前端工程师对静态页面进行修改,然后再交给后端工程师,由后端工程师再将该页面改为 JSP 页面
基于上述的问题, JSP 已逐渐退出历史舞台,以后开发更多的是使用 HTML
+ Ajax
来替代。Ajax
是后续重点学习的技术。有个这个技术后,前端工程师负责前端页面开发,而后端工程师只负责前端代码开发。下来对技术的发展进行简单的说明
- 第一阶段:使用
servlet
即实现逻辑代码编写,也对页面进行拼接。这种模式我们之前也接触过 - 第二阶段:随着技术的发展,出现了
JSP
,人们发现JSP
使用起来比Servlet
方便很多,但是还是要在JSP
中嵌套Java
代码,也不利于后期的维护 - 第三阶段:使用
Servlet
进行逻辑代码开发,而使用JSP
进行数据展示 - 第四阶段:使用
servlet
进行后端逻辑代码开发,而使用HTML
进行数据展示。而这里面就存在问题,HTML
是静态页面,怎么进行动态数据展示呢?这就是ajax
的作用了。
那既然 JSP 已经逐渐的退出历史舞台,那我们为什么还要学习 JSP
呢?原因有两点:
- 一些公司可能有些老项目还在用
JSP
,所以要求我们必须动JSP
- 我们如果不经历这些复杂的过程,就不能体现后面阶段开发的简单
接下来我们来学习第三阶段,使用 EL表达式
和 JSTL
标签库替换 JSP
中的 Java
代码。
EL表达式
概述
EL(全称Expression Language )表达式语言,用于简化 JSP 页面内的 Java 代码。
EL 表达式的主要作用是 获取数据。其实就是从域对象中获取数据,然后将数据展示在页面上。
而 EL 表达式的语法也比较简单,${expression}
。例如:${brands}
就是获取域中存储的 key 为 brands 的数据。
代码演示
定义servlet,在 servlet 中封装一些数据并存储到 request 域对象中并转发到 el-demo.jsp
页面。
1 |
|
注意:此处需要用转发,因为转发才可以使用 request 对象作为域对象进行数据共享
在 el-demo.jsp
中通过 EL表达式 获取数据
1 |
|
可能遇到的问题:页面上只输出了${brands},而不是具体的数据
解决方案:在JSP页面头加上<%@ page isELIgnored="false" %>
在浏览器访问,得到以下数据
1 | PLAINTEXT |
域对象
JavaWeb中有四大域对象,分别是:
- page:当前页面有效
- request:当前请求有效
- session:当前会话有效
- application:当前应用有效
el 表达式获取数据,会依次从这4个域中寻找(域范围依次增大,层层包裹),直到找到为止。
例如: ${brands},el 表达式获取数据,会先从page域对象中获取数据,如果没有再到 requet 域对象中获取数据,如果再没有再到 session 域对象中获取,如果还没有才会到 application 中获取数据。
JSTL标签
概述
JSP标准标签库(Jsp Standarded Tag Library) ,使用标签取代JSP页面上的Java代码。如下代码就是JSTL标签
1 |
|
上面代码看起来比 JSP 中嵌套 Java 代码看起来舒服好了。
这里只对两个最常用的标签进行讲解,<c:forEach>
标签和 <c:if>
标签。
JSTL 使用也是比较简单的,分为如下步骤:
导入坐标
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>在JSP页面上引入JSTL标签库
1
2
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>使用标签
if标签
<c:if>
:相当于 if 判断
- 属性:test,用于定义条件表达式
1 |
|
代码演示:
定义一个 servlet
,在该 servlet
中向 request 域对象中添加 键是 status
,值为 1
的数据
1 |
|
定义 jstl-if.jsp
页面,在该页面使用 <c:if>
标签
1 |
|
注意要在该页面已经要引入 JSTL核心标签库<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
forEach标签
<c:forEach>
:相当于 for 循环。java中有增强for循环和普通for循环,JSTL 中的 <c:forEach>
也有两种用法
用法一
类似于 Java 中的增强for循环。涉及到的 <c:forEach>
中的属性如下
- items:被遍历的容器
- var:遍历产生的临时变量
- varStatus:遍历状态对象
如下代码,是从域对象中获取名为 brands 数据,该数据是一个集合;遍历遍历,并给该集合中的每一个元素起名为 brand
,是 Brand对象。在循环里面使用 EL表达式获取每一个Brand对象的属性值
1 |
|
代码演示:
创建一个servlet,将数据存储到域当中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ServletDemo1 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//准备数据
List<Brand> brands = new ArrayList<Brand>();
brands.add(new Brand(1, "三只松鼠", "三只松鼠", 100, "三只松鼠,好吃不上火", 1));
brands.add(new Brand(2, "优衣库", "优衣库", 200, "优衣库,服适人生", 0));
brands.add(new Brand(3, "小米", "小米科技有限公司", 1000, "为发烧而生", 1));
//存储到request域中
request.setAttribute("brands", brands);
//转发到jstl-foreach.jsp
request.getRequestDispatcher("/jstl-foreach.jsp").forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}定义名为
jstl-foreach.jsp
页面,内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page isELIgnored="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<hr>
<table border="1" cellspacing="0" width="800">
<tr>
<th>序号</th>
<th>品牌名称</th>
<th>企业名称</th>
<th>排序</th>
<th>品牌介绍</th>
<th>状态</th>
</tr>
<c:forEach items="${brands}" var="brand">
<tr align="center">
<td>${brand.id}</td>
<td>${brand.brandName}</td>
<td>${brand.companyName}</td>
<td>${brand.ordered}</td>
<td>${brand.description}</td>
<td>${brand.status == 1 ? "启用" : "禁用"}</td>
</tr>
</c:forEach>
</table>
</body>
</html>这里的
${brand.id}
,并不是直接获取的属性的id,而是调用的brand的getId()方法,底层实现是先将首字母大写,由id
变成Id
,然后在前面拼接一个get,从而变成getId
,随后调用brand的getId()
用法二
类似于 Java 中的普通for循环。涉及到的 <c:forEach>
中的属性如下
- begin:开始数
- end:结束数
- step:步长
实例代码:
从0循环到10,变量名是 i
,每次自增1
1 |
|
MVC格式与三层架构
MVC 模式和三层架构是一些理论的知识,将来我们使用了它们进行代码开发会让我们代码维护性和扩展性更好。
MVC模式
MVC 是一种分层开发的模式,其中:
- M:Model,业务模型,处理业务
- V:View,视图,界面展示
- C:Controller,控制器,处理请求,调用模型和视图
控制器(serlvlet)用来接收浏览器发送过来的请求,控制器调用模型(JavaBean)来获取数据,比如从数据库查询数据;控制器获取到数据后再交由视图(JSP)进行数据展示。
MVC 好处:
- 职责单一,互不影响。每个角色做它自己的事,各司其职。
- 有利于分工协作。
- 有利于组件重用
三层架构
三层架构是将我们的项目分成了三个层面,分别是 表现层
、业务逻辑层
、数据访问层
。
- 数据访问层:对数据库的CRUD基本操作
- 业务逻辑层:对业务逻辑进行封装,组合数据访问层层中基本功能,形成复杂的业务逻辑功能。例如
注册业务功能
,我们会先调用数据访问层
的selectByName()
方法判断该用户名是否存在,如果不存在再调用数据访问层
的insert()
方法进行数据的添加操作 - 表现层:接收请求,封装数据,调用业务逻辑层,响应数据
而整个流程是,浏览器发送请求,表现层的Servlet接收请求并调用业务逻辑层的方法进行业务逻辑处理,而业务逻辑层方法调用数据访问层方法进行数据的操作,依次返回到serlvet,然后servlet将数据交由 JSP 进行展示。
MVC格式和三层架构
通过 MVC 和 三层架构 有什么区别和联系?
如上图上半部分是 MVC 模式,上图下半部分是三层架构。 MVC 模式
中的 C(控制器)和 V(视图)就是 三层架构
中的表现层,而 MVC 模式
中的 M(模型)就是 三层架构
中的 业务逻辑层 和 数据访问层。
可以将 MVC 模式
理解成是一个大的概念,而 三层架构
是对 MVC 模式
实现架构的思想。 那么我们以后按照要求将不同层的代码写在不同的包下,每一层里功能职责做到单一,将来如果将表现层的技术换掉,而业务逻辑层和数据访问层的代码不需要发生变化。
案例
需求:完成品牌数据的增删改查操作
这个案例是将今天学习的所有的内容(包含 MVC模式 和 三层架构)进行应用,并将整个流程贯穿起来
环境准备
环境准备工作,我们分以下步骤实现:
创建新的模块 brand_demo,引入坐标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>brand-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--jsp-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!--jstl-->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
</plugins>
</build>
</project>创建三层架构的包结构
- 分别创建
mapper
,pojo
,service
,util
,web
五个包
- 分别创建
数据库表 tb_brand
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 创建数据库
CREATE DATABASE db1;
-- 使用数据库
USE db1;
-- 创建tb_brand表
CREATE TABLE tb_brand
(
-- id 主键
id INT PRIMARY KEY AUTO_INCREMENT,
-- 品牌名称
brand_name VARCHAR(20),
-- 企业名称
company_name VARCHAR(20),
-- 排序字段
ordered INT,
-- 描述信息
description VARCHAR(100),
-- 状态:0:禁用 1:启用
STATUS INT
);
-- 添加数据
INSERT INTO tb_brand (brand_name, company_name, ordered, description, STATUS)
VALUES ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),
('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1),
('小米', '小米科技有限公司', 50, 'are you ok', 1);
SELECT * FROM tb_brand;实体类 Brand
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.blog.pojo;
public class Brand {
private Integer id;
private String brandName;
private String companyName;
private Integer ordered;
private String description;
private Integer status;
public Brand() {
}
public Brand(Integer id, String brandName, String companyName, Integer ordered, String description, Integer status) {
this.id = id;
this.brandName = brandName;
this.companyName = companyName;
this.ordered = ordered;
this.description = description;
this.status = status;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public Integer getOrdered() {
return ordered;
}
public void setOrdered(Integer ordered) {
this.ordered = ordered;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String toString() {
return "Brand{" +
"id=" + id +
", brandName='" + brandName + '\'' +
", companyName='" + companyName + '\'' +
", ordered=" + ordered +
", description='" + description + '\'' +
", status=" + status +
'}';
}
}MyBatis 基础环境
Mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<configuration>
<typeAliases>
<package name="com.blog.pojo"></package>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:13306/db1?useSSL=false&useServerPrepStmts=true"/>
<property name="username" value="root"/>
<property name="password" value="PASSWORD."/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.blog.mapper"/>
</mappers>
</configuration>BrandMapper.xml
由于数据库中的命名和Brand类的命名不一致,所以这里要用resultMap来解决
1
2
3
4
5
6
7
8
9
10
11
12
<mapper namespace="com.blog.mapper.BrandMapper">
<!--用resultMap来定义字段和属性的映射关系-->
<resultMap id="brandResultMap" type="brand">
<result column="brand_name" property="brandName"></result>
<result column="company_name" property="companyName"></result>
</resultMap>
</mapper>BrandMapper接口
1
2
3
4
5
package com.blog.mapper;
public interface BrandMapper {
}
查询所有
当我们点击 index.html
页面中的 查询所有
这个超链接时,就能查询到产品的数据。
对于上述的功能,点击 查询所有
超链接是需要先请后端的 servlet
,由 servlet
跳转到对应的页面进行数据的动态展示。
编写index.html页面
1 |
|
编写BrandMapper
在 mapper
包下创建创建 BrandMapper
接口,在接口中定义 selectAll()
方法
1 |
|
编写工具类
这个工具类的编写在上篇文章讲过
1 |
|
编写BrandService
1 |
|
编写Servlet
在 web
包下创建名为 SelectAllServlet
的 servlet
,该 servlet
的逻辑如下:
- 调用
BrandService
的selectAll()
方法进行业务逻辑处理,并接收返回的结果 - 将上一步返回的结果存储到
request
域对象中 - 跳转到
brand.jsp
页面进行数据的展示
1 |
|
编写brand.jsp页面
在表格中使用 JSTL
和 EL表达式
从request域对象中获取名为 brands
的集合数据并展示出来。
1 |
|
测试
启动服务器,并在浏览器输入 http://localhost:8080/brand-demo/index.html
,看到如下 查询所有
的超链接,点击该链接就可以查询出所有的品牌数据
添加
编写BrandMapper方法
在 BrandMapper
接口,在接口中定义 add(Brand brand)
方法
1 |
|
编写BrandService方法
在 BrandService
类中定义添加品牌数据方法 add(Brand brand)
1 |
|
改进brand.jsp页面
在该页面表格的上面添加 新增
按钮
1 |
|
并给该按钮绑定单击事件,当点击了该按钮需要跳转到 brand.jsp
添加品牌数据的页面
1 |
|
编写addBrand.jsp页面
1 |
|
编写servlet
在 web
包下创建 AddServlet
的 servlet
,该 servlet
的逻辑如下:
- 设置处理post请求乱码的字符集
- 接收客户端提交的数据
- 将接收到的数据封装到
Brand
对象中 - 调用
BrandService
的add()
方法进行添加的业务逻辑处理 - 跳转到
selectAllServlet
资源重新查询数据
具体的代码如下:
1 |
|
测试
点击 brand.jsp
页面的 新增
按钮,会跳转到 addBrand.jsp
页面,输入数据点击提交,可以看到刚添加的数据
修改
修改
功能需要从两方面进行实现,数据回显和修改操作。当我们点击修改时,页面上会显示原有的数据,而不是空白的,我们可以选择我们需要的来修改。
回显数据
编写BrandMapper方法
在 BrandMapper
接口,在接口中定义 selectById(int id)
方法
1 |
|
编写BrandService方法
在 BrandService
类中定义根据id查询数据方法 selectById(int id)
1 |
|
编写servlet
在 web
包下创建 SelectByIdServlet
的 servlet
,该 servlet
的逻辑如下:
- 获取请求数据
id
- 调用
BrandService
的selectById()
方法进行数据查询的业务逻辑 - 将查询到的数据存储到 request 域对象中
- 跳转到
update.jsp
页面进行数据真实
具体代码如下:
1 |
|
编写update.jsp页面
拷贝 addBrand.jsp
页面,改名为 update.jsp
并做出以下修改:
title
标签内容改为修改品牌
form
标签的action
属性值改为/brand_demo/updateServlet
```
input1
2
3
4
5
6
7
标签要进行数据回显,需要设置
value
1
2
3
4
5
6
7
8
9
10
属性
```JSP
品牌名称:<input name="brandName" value="${brand.brandName}"><br>
企业名称:<input name="companyName" value="${brand.companyName}"><br>
排序:<input name="ordered" value="${brand.ordered}"><br>```
textarea1
2
3
4
5
6
7
标签要进行数据回显,需要在标签体中使用
EL表达式
1
2
3
4
```JSP
描述信息:<textarea rows="5" cols="20" name="description">${brand.description} </textarea><br>单选框使用
1 | if |
标签需要判断
1 | brand.status |
的值是 1 还是 0 在指定的单选框上使用
1 | checked |
属性,表示被选中状态
1 |
|
综上,update.jsp
代码如下:
1 |
|
修改数据
在修改页面进行数据修改,点击 提交
按钮,会将数据提交到后端程序,后端程序会对表中的数据进行修改操作,然后重新进行数据的查询操作。整体流程如下:
编写BrandMapper方法
在 BrandMapper
接口,在接口中定义 update(Brand brand)
方法
1 |
|
编写BrandService方法
在 BrandService
类中定义根据id查询数据方法 update(Brand brand)
1 |
|
编写servlet
在 web
包下创建 AddServlet
的 servlet
,该 servlet
的逻辑如下:
- 设置处理post请求乱码的字符集
- 接收客户端提交的数据
- 将接收到的数据封装到
Brand
对象中 - 调用
BrandService
的update()
方法进行添加的业务逻辑处理 - 跳转到
selectAllServlet
资源重新查询数据
具体的代码如下:
1 |
|
之前的update.jsp 页面提交数据时是没有携带主键数据的,而后台修改数据需要根据主键进行修改。
针对这个问题,我们不希望页面将主键id展示给用户看,但是又希望在提交数据时能将主键id提交到后端。
我们可以使用隐藏域解决这个问题
1 |
|
最终的update.jsp代码如下
1 |
|
测试
访问页面,当我们点击修改时,数据会回显,修改完我们需要的之后,自动返回查询所有页面
删除
经过上面的学习,删除操作对我们来说已经非常简单了
修改brand.jsp
设置一下删除的超链接
1 |
|
编写BrandMapper方法
在 BrandService
类中定义根据id查询数据方法 deleteById(int id)
1 |
|
编写BrandService方法
在 BrandService
类中定义根据id查询数据方法 selectById(int id)
1 |
|
编写servlet
在 web
包下创建 SelectByIdServlet
的 servlet
,该 servlet
的逻辑如下:
- 获取请求数据
id
- 调用
BrandService
的deleteById()
方法进行数据查询的业务逻辑 - 跳转到
selectAllServlet
资源重新查询数据
具体代码如下:
1 |
|