Web基础知识
Tomcat
简介
Tomcat服务器软件是一个免费的开源的web应用服务器。是Apache软件基金会的一个核心项目。由Apache,Sun和其他一些公司及个人共同开发而成。
由于Tomcat只支持Servlet/JSP少量JavaEE规范,所以是一个开源免费的轻量级Web服务器。
JavaEE规范: JavaEE => Java Enterprise Edition(Java企业版)
avaEE规范就是指Java企业级开发的技术规范总和。包含13项技术规范:JDBC、JNDI、EJB、RMI、JSP、Servlet、XML、JMS、Java IDL、JTS、JTA、JavaMail、JAF
因为Tomcat支持Servlet/JSP规范,所以Tomcat也被称为Web容器、Servlet容器。JavaWeb程序需要依赖Tomcat才能运行。
Tomcat的官网: https://tomcat.apache.org/
基本使用
下载
直接从官方网站下载:https://tomcat.apache.org/download-90.cgi
Tomcat软件类型说明:
- tar.gz文件,是linux和mac操作系统下的压缩版本
- zip文件,是window操作系统下压缩版本(我们选择zip文件)
大家可以自行下载,也可以直接使用资料中已经下载好的资源,
Tomcat的软件程序 :/资料/安装包/apache-tomcat-9.0.27-windows-x64.zip
安装与卸载
安装: Tomcat是绿色版,直接解压即安装
在E盘的develop目录下,将
apache-tomcat-9.0.27-windows-x64.zip
进行解压缩,会得到一个apache-tomcat-9.0.27
的目录,Tomcat就已经安装成功。
注意,Tomcat在解压缩的时候,解压所在的目录可以任意,但最好解压到一个不包含中文和空格的目录,因为后期在部署项目的时候,如果路径有中文或者空格可能会导致程序部署失败
打开apache-tomcat-9.0.27
目录就能看到如下目录结构,每个目录中包含的内容需要认识下
bin:目录下有两类文件,一种是以.bat
结尾的,是Windows系统的可执行文件,一种是以.sh
结尾的,是Linux系统的可执行文件。
webapps:就是以后项目部署的目录
卸载:卸载比较简单,可以直接删除目录即可
启动与关闭
启动Tomcat
- 双击tomcat解压目录/bin/startup.bat文件即可启动tomcat
注意: tomcat服务器启动后,黑窗口不会关闭,只要黑窗口不关闭,就证明tomcat服务器正在运行
Tomcat的默认端口为8080,所以在浏览器的地址栏输入:http://127.0.0.1:8080
即可访问tomcat服务器
127.0.0.1 也可以使用localhost代替。如:
http://localhost:8080
- 能看到以上图片中Apache Tomcat的内容就说明Tomcat已经启动成功
注意事项 :Tomcat启动的过程中,遇到控制台有中文乱码时,可以通常修改conf/logging.prooperties文件解决
关闭: 关闭有三种方式
1、强制关闭:直接x掉Tomcat窗口(不建议)
2、正常关闭:bin\shutdown.bat
3、正常关闭:在Tomcat启动窗口中按下 Ctrl+C
- 说明:如果按下Ctrl+C没有反映,可以多按几次
常见问题
问题1:Tomcat启动时,窗口一闪而过
- 检查JAVA_HOME环境变量是否正确配置
问题2:端口号冲突
-
发生问题的原因:Tomcat使用的端口被占用了。
-
解决方案:换Tomcat端口号
- 要想修改Tomcat启动的端口号,需要修改 conf/server.xml文件
注: HTTP协议默认端口号为80,如果将Tomcat端口号改为80,则将来访问Tomcat时,将不用输入端口号。
Servlet
快速入门
什么是Servlet
Servlet 是运行在Web服务器中的小型java程序,是Java提供的一门动态web资源开发技术。通常通过HTTP协议接收和响应来自于客户端的请求。
Servlet 是JavaEE规范之一,其实就是一个接口(定义Servlet需实现Servlet接口 或 继承HttpServlet) ,并由web服务器运行Servlet。
入门程序
需求:使用Servlet开发一个Web应用,浏览器发起请求 /hello 之后,给浏览器返回一个字符串 "Hello Xxx"。
步骤:
1). 准备:创建maven项目(设置打包方式为war),导入Servlet坐标(provided)
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
2). 定义一个类,实现Servlet接口(继承HttpServlet),并实现所有方法。在Servlet类上使用@WebServlet注解,配置该Servlet的访问路径。
@WebServlet(urlPatterns = "/hello")
public class HelloServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//接收请求参数
String name = request.getParameter("name");
//响应结果
String respMsg = "<h1>Hello, " + name + " ~</h1>";
response.getWriter().write(respMsg);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
3). 将开发好的Servlet部署在外部的Tomcat服务器中运行。
选择 Edit Configurations
。
选择 Tomcat Server
。
点击 Deployment
选择要部署的项目。
点击上方的小三角 ,启动Tomcat 服务。
启动完毕,会看到Tomcat输出的日志。
4). 打开浏览器,访问测试 http://localhost:9000/demo01/hello
注意事项
-
Servlet项目打包方式需要设置为 war。
<packaging>war</packaging>
-
servlet-api 依赖的作用范围
<scope>
必须设置为provided
。<scope>provided</scope>
开发第一个servlet接口
第一步:导入sevlet依赖
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
配置打包方式为war
<packaging>war</packaging>
第二步
编写一个类,继承HttpServlet类
第三步
重写do系列方法
第四步
类上添加@WebServlet注解,写接口访问路径
servlet方法
tomcat和servlet的关系
执行流程
Servlet的入门程序我们都已经完成了,并将其部署到了Tomcat服务器中进行测试,可以正常访问到Servlet程序。 那接下来,我们就来研究一下Servlet的执行流程。
研究Servlet的执行流程,主要就是研究,我们在浏览器地址栏访问 http://localhost:8080/servlet-demo/hello?name=Tomcat
,是如何访问到我们的服务中部署的servlet-demo
这个应用的。
1). 定位服务器
通过url地址中的ip/主机 与 端口号 http://localhost:8080
,首先会定位到我们要访问的Web服务器。
而在一个web服务器中,是可以部署多个应用程序的。那我们具体要访问到那个项目呢 ? 接下来,我们再来看如何定位到要访问的应用程序的。
2). 定位项目
通过端口号之后的 servlet-demo
,就可以定位到Web服务器中部署的应用。
而我们知道在一个应用中呢,是可以有多个Servlet程序的,那如何定位到要访问的是哪一个Servlet呢 ?
3). 定位Servlet
那么通过请求路径中的最后一个部分 /hello
就可以知道我们要访问的Servlet程序是 HelloServlet
,因为 HelloServlet
的请求路径是 /hello
。
4). 执行Servlet
定位到Servlet程序后,那接下来,就会指定这个Servlet程序,那具体调用Servlet程序中的哪个方法呢 (doGet
或 doPost
) ?
由于我们再浏览器地址栏发起的请求,请求方式都是Get,所以Web服务器会自动调用Servlet的 doGet
方法。
- Servlet 对象是由谁创建的,Servlet的doGet方法是由谁调用的?
- Servlet对象由Web服务器创建,Servlet方法由Web服务器调用。
- HttpServletRequest:用于封装所有的请求数据。
- HttpServletResponse:用于封装所有的响应数据。
HTTP协议
HTTP-概述
介绍
HTTP:Hyper Text Transfer Protocol(超文本传输协议),规定了浏览器与服务器之间数据传输的规则。
- http是互联网上应用最为广泛的一种网络协议
- http协议要求:浏览器在向服务器发送请求数据时,或是服务器在向浏览器发送响应数据时,都必须按照固定的格式进行数据传输
如果想知道http协议的数据传输格式有哪些,可以打开浏览器,点击F12
打开开发者工具,点击Network
来查看
浏览器向服务器进行请求时:
- 服务器按照固定的格式进行解析
服务器向浏览器进行响应时:
- 浏览器按照固定的格式进行解析
所以,我们学习HTTP主要就是学习请求和响应数据的具体格式内容。
特点
我们刚才初步认识了HTTP协议,那么我们在看看HTTP协议有哪些特点:
-
**基于TCP协议: ** 面向连接,安全
TCP是一种面向连接的(建立连接之前是需要经过三次握手)、可靠的、基于字节流的传输层通信协议,在数据传输方面更安全
-
基于请求-响应模型: 一次请求对应一次响应(先请求后响应)
请求和响应是一一对应关系,没有请求,就没有响应
-
HTTP协议是无状态协议: 对于数据没有记忆能力。每次请求-响应都是独立的
无状态指的是客户端发送HTTP请求给服务端之后,服务端根据请求响应数据,响应完后,不会记录任何信息。
- 缺点: 多次请求间不能共享数据
- 优点: 速度快
请求之间无法共享数据会引发的问题:
- 如:京东购物。加入购物车和去购物车结算是两次请求
- 由于HTTP协议的无状态特性,加入购物车请求响应结束后,并未记录加入购物车是何商品
- 发起去购物车结算的请求后,因为无法获取哪些商品加入了购物车,会导致此次请求无法正确展示数据
具体使用的时候,我们发现京东是可以正常展示数据的,原因是Java早已考虑到这个问题,并提出了使用会话技术(Cookie、Session)来解决这个问题。具体如何来做,我们后面课程中会讲到。
刚才提到HTTP协议是规定了请求和响应数据的格式,那具体的格式是什么呢?
***URL与URI
URL
统一资源定位系统(uniform resource locator;URL)是因特网的万维网服务程序上用于指定服务器信息位置的表示方法。
字符集
ASCLL编码
Http URL
http://
URI
统一资源标识符(Uniform Resource Identifier,URI)是一个用于标识某一互联网资源名称的字符串
相对URI
是指从浏览器中基本 URI 处指定的 URL,形如 /image/logo.gif。
HTTP-请求协议
浏览器和服务器是按照HTTP协议进行数据通信的。
HTTP协议又分为:请求协议和响应协议
- 请求协议:浏览器将数据以请求格式发送到服务器
- 包括:请求行、请求头 、请求体
- 响应协议:服务器将数据以响应格式返回给浏览器
- 包括:响应行 、响应头 、响应体
请求数据格式
在HTTP1.1版本中,浏览器访问服务器的几种方式:
请求方式 | 请求说明 |
---|---|
GET | 获取资源。 向特定的资源发出请求。例:http://www.baidu.com/s?wd=itheima |
POST | 传输实体主体。 向指定资源提交数据进行处理请求(例:上传文件),数据被包含在请求体中。 |
OPTIONS | 返回服务器针对特定资源所支持的HTTP请求方式。 因为并不是所有的服务器都支持规定的方法,为了安全有些服务器可能会禁止掉一些方法,例如:DELETE、PUT等。那么OPTIONS就是用来询问服务器支持的方法。 |
HEAD | 获得报文首部。 HEAD方法类似GET方法,但是不同的是HEAD方法不要求返回数据。通常用于确认URI的有效性及资源更新时间等。 |
PUT | 传输文件。 PUT方法用来传输文件。类似FTP协议,文件内容包含在请求报文的实体中,然后请求保存到URL指定的服务器位置。 |
DELETE | 删除文件。 请求服务器删除Request-URI所标识的资源 |
TRACE | 追踪路径。 回显服务器收到的请求,主要用于测试或诊断 |
CONNECT | 要求用隧道协议连接代理。 HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器 |
在我们实际应用中常用的也就是 :GET、POST
GET方式的请求协议
-
请求行 :HTTP请求中的第一行数据。由:
请求方式
、资源路径
、协议/版本
组成(之间使用空格分隔)- 请求方式:GET
- 资源路径:/brand/findAll?name=OPPO&status=1
- 请求路径:/brand/findAll
- 请求参数:name=OPPO&status=1
- 请求参数是以key=value形式出现
- 多个请求参数之间使用
&
连接
- 请求路径和请求参数之间使用
?
连接
- 协议/版本:HTTP/1.1
-
请求头 :第二行开始,上图黄色部分内容就是请求头。格式为key: value形式
- http是个无状态的协议,所以在请求头设置浏览器的一些自身信息和想要响应的形式。这样服务器在收到信息后,就可以知道是谁,想干什么了
常见的HTTP请求头有:
Host: 表示请求的主机名 User-Agent: 浏览器版本。 例如:Chrome浏览器的标识类似Mozilla/5.0 ...Chrome/79 ,IE浏览器的标识类似Mozilla/5.0 (Windows NT ...)like Gecko Accept:表示浏览器能接收的资源类型,如text/*,image/*或者*/*表示所有; Accept-Encoding:表示浏览器可以支持的压缩类型,例如gzip, deflate等。 Accept-Language:表示浏览器偏好的语言,服务器可以据此返回不同语言的网页; Connection:keep-alive一个tcp连接处理多个http请求 Content-Type:请求主体的数据类型 Content-Length:数据主体的大小(单位:字节)
举例说明:服务端可以根据请求头中的内容来获取客户端的相关信息,有了这些信息服务端就可以处理不同的业务需求。
比如:
- 不同浏览器解析HTML和CSS标签的结果会有不一致,所以就会导致相同的代码在不同的浏览器会出现不同的效果
- 服务端根据客户端请求头中的数据获取到客户端的浏览器类型,就可以根据不同的浏览器设置不同的代码来达到一致的效果(这就是我们常说的浏览器兼容问题)
- 请求体 :存储请求参数
- GET请求的请求参数在请求行中,故不需要设置请求体
POST方式的请求协议
- 请求行(以上图中红色部分):包含请求方式、资源路径、协议/版本
- 请求方式:POST
- 资源路径:/brand
- 协议/版本:HTTP/1.1
- 请求头(以上图中黄色部分)
- 请求体(以上图中绿色部分) :存储请求参数
- 请求体和请求头之间是有一个空行隔开(作用:用于标记请求头结束)
GET请求和POST请求的区别:
区别方式 | GET请求 | POST请求 |
---|---|---|
请求参数 | 请求参数在请求行中。 例:/brand/findAll?name=OPPO&status=1 |
请求参数在请求体中 |
请求参数长度 | 请求参数长度有限制(浏览器不同限制也不同) | 请求参数长度没有限制 |
安全性 | 安全性低。原因:请求参数暴露在浏览器地址栏中。 | 安全性相对高 |
请求数据获取
Web服务器对HTTP协议的请求数据进行解析,并进行了封装(HttpServletRequest),并在调用Servlet方法的时候传递给了Servlet。这样,就使得程序员不必直接对协议进行操作,让Web开发更加便捷。
代码演示:
@WebServlet(urlPatterns = "/req")
public class RequestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取请求方式 ;
String method = request.getMethod();
//2. 获取请求路径 ;
String uri = request.getRequestURI();
StringBuffer url = request.getRequestURL();
//3. 获取请求参数 ;
String name = request.getParameter("name");
//4. 获取请求头 ;
String ua = request.getHeader("User-Agent");
//5. 获取请求协议 ;
String scheme = request.getScheme();
//6. 获取查询字符串 ;
String queryString = request.getQueryString();
System.out.println("method: "+method);
System.out.println("uri: "+uri);
System.out.println("url: "+url);
System.out.println("name: "+name);
System.out.println("ua: "+ua);
System.out.println("scheme: "+scheme);
System.out.println();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
ServletInputStream inputStream = req.getInputStream();
System.out.println(IOUtils.toString(inputStream, "UTF-8"));
}
}
HTTP-响应协议
响应数据格式
格式介绍
与HTTP的请求一样,HTTP响应的数据也分为3部分:响应行、响应头 、响应体
-
响应行(以上图中红色部分):响应数据的第一行。响应行由
协议及版本
、响应状态码
、状态码描述
组成- 协议/版本:HTTP/1.1
- 响应状态码:200
- 状态码描述:OK
-
响应头(以上图中黄色部分):响应数据的第二行开始。格式为key:value形式
- http是个无状态的协议,所以可以在请求头和响应头中设置一些信息和想要执行的动作,这样,对方在收到信息后,就可以知道你是谁,你想干什么
常见的HTTP响应头有:
Content-Type:表示该响应内容的类型,例如text/html,image/jpeg ; Content-Length:表示该响应内容的长度(字节数); Content-Encoding:表示该响应压缩算法,例如gzip ; Cache-Control:指示客户端应如何缓存,例如max-age=300表示可以最多缓存300秒 ; Set-Cookie: 告诉浏览器为当前页面所在的域设置cookie ;
- 响应体(以上图中绿色部分): 响应数据的最后一部分。存储响应的数据
- 响应体和响应头之间有一个空行隔开(作用:用于标记响应头结束)
响应类型
响应状态码
状态码分类 | 说明 |
---|---|
1xx | 响应中 --- 临时状态码。表示请求已经接受,告诉客户端应该继续请求或者如果已经完成则忽略 |
2xx | 成功 --- 表示请求已经被成功接收,处理已完成 |
3xx | 重定向 --- 重定向到其它地方,让客户端再发起一个请求以完成整个处理 |
4xx | 客户端错误 --- 处理发生错误,责任在客户端,如:客户端的请求一个不存在的资源,客户端未被授权,禁止访问等 |
5xx | 服务器端错误 --- 处理发生错误,责任在服务端,如:服务端抛出异常,路由出错,HTTP版本不支持等 |
参考: 资料/响应状态码.md
关于响应状态码,我们先主要认识三个状态码,其余的等后期用到了再去掌握:
200 ok
客户端请求成功404 Not Found
请求资源不存在500 Internal Server Error
服务端发生不可预期的错误
常见的响应状态码
状态码 | 英文描述 | 解释 |
---|---|---|
200 | OK |
客户端请求成功,即处理成功,这是我们最想看到的状态码 |
302 | Found |
指示所请求的资源已移动到由Location 响应头给定的 URL,浏览器会自动重新访问到这个页面 |
304 | Not Modified |
告诉客户端,你请求的资源至上次取得后,服务端并未更改,你直接用你本地缓存吧。隐式重定向 |
400 | Bad Request |
客户端请求有语法错误,不能被服务器所理解 |
403 | Forbidden |
服务器收到请求,但是拒绝提供服务,比如:没有权限访问相关资源 |
404 | Not Found |
请求资源不存在,一般是URL输入有误,或者网站资源被删除了 |
405 | Method Not Allowed |
请求方式有误,比如应该用GET请求方式的资源,用了POST |
428 | Precondition Required |
服务器要求有条件的请求,告诉客户端要想访问该资源,必须携带特定的请求头 |
429 | Too Many Requests |
指示用户在给定时间内发送了太多请求(“限速”),配合 Retry-After(多长时间后可以请求)响应头一起使用 |
431 | Request Header Fields Too Large |
请求头太大,服务器不愿意处理请求,因为它的头部字段太大。请求可以在减少请求头域的大小后重新提交。 |
500 | Internal Server Error |
服务器发生不可预期的错误。服务器出异常了,赶紧看日志去吧 |
503 | Service Unavailable |
服务器尚未准备好处理请求,服务器刚刚启动,还未初始化好 |
状态码大全:https://cloud.tencent.com/developer/chapter/13553
响应数据设置
Web服务器对HTTP协议的响应数据进行了封装(HttpServletResponse),并在调用Servlet方法的时候传递给了Servlet。这样,就使得程序员不必直接对协议进行操作,让Web开发更加便捷。
代码演示:
@WebServlet(urlPatterns = "/resp")
public class ResponseServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 设置响应状态码
//resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
//2. 设置响应头
resp.setHeader("Content-Type", "text/html;charset=UTF-8");
//3. 设置响应体 - 获取字符输出流
resp.getWriter().write("<h1>Hello HTTP ~</h1>");
}
}
响应状态码 和 响应头如果没有特殊要求的话,通常不手动设定。服务器会根据请求处理的逻辑,自动设置响应状态码和响应头。
SpringBootWeb入门
刚才我们讲解了基于原始的Servlet程序开发Web应用,并基于此讲解了Http协议。 那接下来呢,我们就要来讲解现在企业开发的主流技术 SpringBoot,并基于SpringBoot进行Web程序的开发 。
概述
在没有正式的学习SpringBoot之前,我们要先来了解下什么是Spring。
我们可以打开Spring的官网(https://spring.io),去看一下Spring的简介:Spring makes Java simple。
Spring的官方提供很多开源的项目,我们可以点击上面的projects,看到spring家族旗下的项目,按照流行程度排序为:
Spring发展到今天已经形成了一种开发生态圈,Spring提供了若干个子项目,每个项目用于完成特定的功能。而我们在项目开发时,一般会偏向于选择这一套spring家族的技术,来解决对应领域的问题,那我们称这一套技术为spring全家桶。
而Spring家族旗下这么多的技术,最基础、最核心的是 SpringFramework。其他的spring家族的技术,都是基于SpringFramework的,SpringFramework中提供很多实用功能,如:依赖注入、事务管理、web开发支持、数据访问、消息服务等等。
而如果我们在项目中,直接基于SpringFramework进行开发,存在两个问题:配置繁琐、入门难度大。
所以基于此呢,spring官方推荐我们从另外一个项目开始学习,那就是目前最火爆的SpringBoot。
通过springboot就可以快速的帮我们构建应用程序,所以springboot呢,最大的特点有两个 :
- 简化配置
- 快速开发
Spring Boot 可以帮助我们非常快速的构建应用程序、简化开发、提高效率 。
接下来,我们就直接通过一个SpringBoot的web入门程序,让大家快速感受一下,基于SpringBoot进行Web开发的便捷性。
入门程序
需求
需求:基于SpringBoot的方式开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串 "Hello World ~"。
开发步骤
第1步:创建SpringBoot工程,并勾选Web开发相关依赖
第2步:定义HelloController类,添加方法hello,并添加注解
第3步:测试运行
1). 创建SpringBoot工程(需要联网)
基于Spring官方骨架,创建SpringBoot工程。
基本信息描述完毕之后,勾选web开发相关依赖。
点击Finish之后,就会联网创建这个SpringBoot工程,创建好之后,结构如下:
注意:在联网创建过程中,会下载相关资源(请耐心等待)
2). 定义请求处理类
在com.itheima这个包下新建一个类:HelloController
package com.itheima;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(String name){
System.out.println("HelloController .... hello : " + name);
return "Hello , " + name;
}
}
3). 运行测试
运行SpringBoot自动生成的引导类
打开浏览器,输入 http://localhost:8080/hello?name=itheima
入门程序解析
入门程序我们已经搞定了,接下来,我们就一起来解析一下我们所编写的入门程序。
4.3.1 main方法如何启动Tomcat ?
因为我们在创建springboot项目的时候,选择了web开发的起步依赖 spring-boot-starter-web。而spring-boot-starter-web依赖,又依赖了spring-boot-starter-tomcat,由于maven的依赖传递特性,那么在我们创建的springboot项目中也就已经有了tomcat的依赖,这个其实就是springboot中内嵌的tomcat。
而我们运行引导类中的main方法,其实启动的就是springboot中内嵌的Tomcat服务器。 而我们所开发的项目,也会自动的部署在该tomcat服务器中,并占用8080端口号 。
4.3.2 Tomcat是一个servlet容器,为什么可以运行我们编写的HelloController?
首先,大家需要知道,Tomcat是一个Servlet容器,而我们编写的Controller程序是不能被Tomcat服务器识别的。
那我们在浏览器地址栏访问 http://localhost:8080/hello?name=Tom
时是如何访问到HelloController程序的呢? 这是因为啊,虽然我们没有编写Servlet程序,但是呢,在Springboot框架的底层源码中,给我们提供了一个核心的Servlet程序,叫 DispatcherServlet
。
而DispatcherServlet
是一个Servlet程序,继承了HttpServlet。
前端发起的所有请求到达服务器之后,都会被DispatcherServlet接收并处理,而DispatcherServlet并不会直接对请求进行处理,而是将请求转发给后面我们自己编写的Controller程序,最终有Controller程序来进行请求的处理。
所以,我们基于Springboot进行Web程序的开发,是不需要直接编写原始的Servlet程序的,我们只需要编写Controller程序,来接收请求,响应数据即可。
***SpringBootWeb请求响应
前后端分离模式
基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢?如下图所示:
- 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
- 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
- 前后台并行开发:各自按照接口文档进行开发,实现需求
- 测试:前后台开发完了,各自按照接口文档进行测试
- 前后段联调测试:前段工程请求后端工程,测试功能
Restful风格
我们的案例是基于当前最为主流的前后端分离模式进行开发。
在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)
而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。
什么是REST风格呢?
- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
传统URL风格如下:
http://localhost:8080/user/getById?id=1 GET:查询id为1的用户
http://localhost:8080/user/saveUser POST:新增用户
http://localhost:8080/user/updateUser POST:修改用户
http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户
我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。
基于REST风格URL如下:
http://localhost:8080/users/1 GET:查询id为1的用户
http://localhost:8080/users POST:新增用户
http://localhost:8080/users PUT:修改用户
http://localhost:8080/users/1 DELETE:删除id为1的用户
其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
- GET : 查询
- POST :新增
- PUT :修改
- DELETE :删除
我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。
注意事项:
- REST是风格,是约定方式,约定不是规定,可以打破
- 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…
REST风格如何设计URL
如何设计URL
资源
主资源
/goods/1
子资源
/goods/1/pictures
参数放在路径中
/goods/{name}
合适的操作资源动词
post
delete
put
get
标准的状态码
http响应码 1xx-5xxx
自定义业务响应码
前言
在上一次的课程中,我们开发了springbootweb的入门程序。 基于SpringBoot的方式开发一个web应用,浏览器发起请求 /hello 后 ,给浏览器返回字符串 “Hello World ~”。
其实呢,是我们在浏览器发起请求,请求了我们的后端web服务器(也就是内置的Tomcat)。而我们在开发web程序时呢,定义了一个控制器类Controller,请求会被部署在Tomcat中的Controller接收,然后Controller再给浏览器一个响应,响应一个字符串 “Hello World”。 而在请求响应的过程中是遵循HTTP协议的。
但是呢,这里要告诉大家的时,其实在Tomcat这类Web服务器中,是不识别我们自己定义的Controller的。但是我们前面讲到过Tomcat是一个Servlet容器,是支持Serlvet规范的,所以呢,在tomcat中是可以识别 Servlet程序的。 那我们所编写的XxxController 是如何处理请求的,又与Servlet之间有什么联系呢?
其实呢,在SpringBoot进行web程序开发时,它内置了一个核心的Servlet程序 DispatcherServlet,称之为 核心控制器。 DispatcherServlet 负责接收页面发送的请求,然后根据执行的规则,将请求再转发给后面的请求处理器Controller,请求处理器处理完请求之后,最终再由DispatcherServlet给浏览器响应数据。
那将来浏览器发送请求,会携带请求数据,包括:请求行、请求头;请求到达tomcat之后,tomcat会负责解析这些请求数据,然后呢将解析后的请求数据会传递给Servlet程序的HttpServletRequest对象,那也就意味着 HttpServletRequest 对象就可以获取到请求数据。 而Tomcat,还给Servlet程序传递了一个参数 HttpServletResponse,通过这个对象,我们就可以给浏览器设置响应数据 。
那上述所描述的这种浏览器/服务器的架构模式呢,我们称之为:BS架构。
• BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。
那今天呢,我们的课程内容主要就围绕着:请求、响应进行。 今天课程内容,主要包含三个部分:
- 请求
- 响应
- 分层解耦
请求
在本章节呢,我们主要讲解,如何接收页面传递过来的请求数据。
Postman
之前我们课程中有提到当前最为主流的开发模式:前后端分离
在这种模式下,前端技术人员基于"接口文档",开发前端程序;后端技术人员也基于"接口文档",开发后端程序。
由于前后端分离,对我们后端技术人员来讲,在开发过程中,是没有前端页面的,那我们怎么测试自己所开发的程序呢?
方式1:像之前SpringBoot入门案例中一样,直接使用浏览器。在浏览器中输入地址,测试后端程序。
- 弊端:在浏览器地址栏中输入地址这种方式都是GET请求,如何我们要用到POST请求怎么办呢?
- 要解决POST请求,需要程序员自己编写前端代码(比较麻烦)
方式2:使用专业的接口测试工具(课程中我们使用Postman工具)
介绍
-
Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件。
Postman原是Chrome浏览器的插件,可以模拟浏览器向后端服务器发起任何形式(如:get、post)的HTTP请求
使用Postman还可以在发起请求时,携带一些请求参数、请求头等信息
-
作用:常用于进行接口测试
-
特征
- 简单
- 实用
- 美观
- 大方
安装
双击资料中提供的Postman-win64-8.3.1-Setup.exe
即可自动安装。
安装完成之后,进入页面中会提示有新版本可以升级(无需升级)
界面介绍:
如果我们需要将测试的请求信息保存下来,就需要创建一个postman的账号,然后登录之后才可以。
登录完成之后,可以创建工作空间:
创建请求:
点击"Save",保存当前请求
简单参数
简单参数:在向服务器发起请求时,向服务器传递的是一些普通的请求数据。
那么在后端程序中,如何接收传递过来的普通参数数据呢?
我们在这里讲解两种方式:
- 原始方式
- SpringBoot方式
原始方式
在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:
Tomcat接收到http请求时:把请求的相关信息封装到HttpServletRequest对象中
在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明 HttpServletRequest 对象。然后就可以通过该对象来获取请求信息:
//根据指定的参数名获取请求参数的数据值
String request.getParameter("参数名")
@RestController
public class RequestController {
//原始方式
@RequestMapping("/simpleParam")
public String simpleParam(HttpServletRequest request){
// http://localhost:8080/simpleParam?name=Tom&age=10
// 请求参数: name=Tom&age=10 (有2个请求参数)
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10
String name = request.getParameter("name");//name就是请求参数名
String ageStr = request.getParameter("age");//age就是请求参数名
int age = Integer.parseInt(ageStr);//需要手动进行类型转换
System.out.println(name+" : "+age);
return "OK";
}
}
以上这种方式,我们仅做了解。(在以后的开发中不会使用到)
SpringBoot方式
在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=10
// 第1个请求参数: name=Tom 参数名:name,参数值:Tom
// 第2个请求参数: age=10 参数名:age , 参数值:10
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
System.out.println(name+" : "+age);
return "OK";
}
}
postman测试( GET 请求):
postman测试( POST请求 ):
结论:不论是GET请求还是POST请求,对于简单参数来讲,只要保证请求参数名和Controller方法中的形参名保持一致,就可以获取到请求参数中的数据值。
参数名不一致
如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗?
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(String username , Integer age ){//请求参数名和形参名不相同
System.out.println(username+" : "+age);
return "OK";
}
}
答案:运行没有报错。 controller方法中的username值为:null,age值为20
- 结论:对于简单参数来讲,请求参数名和controller方法中的形参名不一致时,无法接收到请求数据
那么如果我们开发中,遇到了这种请求参数名和controller方法中的形参名不相同,怎么办?
解决方案:可以使用Spring提供的@RequestParam注解完成映射
在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下:
@RestController
public class RequestController {
// http://localhost:8080/simpleParam?name=Tom&age=20
// 请求参数名:name
//springboot方式
@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam("name") String username , Integer age ){
System.out.println(username+" : "+age);
return "OK";
}
}
注意事项:
@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错
如果该参数是可选的,可以将required属性设置为false
@RequestMapping("/simpleParam") public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){ System.out.println(username+ ":" + age); return "OK"; }
实体参数
在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。
此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:请求参数名与实体类的属性名相同
简单实体对象
定义POJO实体类:
public class User {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Controller方法:
@RestController
public class RequestController {
//实体参数:简单实体对象
@RequestMapping("/simplePojo")
public String simplePojo(User user){
System.out.println(user);
return "OK";
}
}
Postman测试:
- 参数名和实体类属性名一致时
- 参数名和实体类属性名不一致时
复杂实体对象
上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。
复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:
- User类中有一个Address类型的属性(Address是一个实体类)
复杂实体对象的封装,需要遵守如下规则:
- 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。
定义POJO实体类:
- Address实体类
public class Address {
private String province;
private String city;
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Address{" +
"province='" + province + '\'' +
", city='" + city + '\'' +
'}';
}
}
- User实体类
public class User {
private String name;
private Integer age;
private Address address; //地址对象
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", address=" + address +
'}';
}
}
Controller方法:
@RestController
public class RequestController {
//实体参数:复杂实体对象
@RequestMapping("/complexPojo")
public String complexPojo(User user){
System.out.println(user);
return "OK";
}
}
Postman测试:
数组集合参数
数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。
多个值是怎么提交的呢?其实多个值也是一个一个的提交。
后端程序接收上述多个值的方式有两种:
- 数组
- 集合
数组
数组参数:请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数
Controller方法:
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/arrayParam")
public String arrayParam(String[] hobby){
System.out.println(Arrays.toString(hobby));
return "OK";
}
}
Postman测试:
在前端请求时,有两种传递形式:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
集合
集合参数:请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系
默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系
Controller方法:
@RestController
public class RequestController {
//数组集合参数
@RequestMapping("/listParam")
public String listParam(@RequestParam List<String> hobby){
System.out.println(hobby);
return "OK";
}
}
Postman测试:
方式一: xxxxxxxxxx?hobby=game&hobby=java
方式二:xxxxxxxxxxxxx?hobby=game,java
日期参数
上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:
因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。
- @DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。
- 后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。
Controller方法:
@RestController
public class RequestController {
//日期时间参数
@RequestMapping("/dateParam")
public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
System.out.println(updateTime);
return "OK";
}
}
Postman测试:
JSON参数
在学习前端技术时,我们有讲到过JSON,而在前后端进行交互时,如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)
我们学习JSON格式参数,主要从以下两个方面着手:
- Postman在发送请求时,如何传递json格式的请求参数
- 在服务端的controller方法中,如何接收json格式的请求参数
Postman发送JSON格式数据:
服务端Controller方法接收JSON格式数据:
- 传递json格式的参数,在Controller中会使用实体类进行封装。
- 封装规则:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。
- @RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)
实体类:Address
public class Address {
private String province;
private String city;
//省略GET , SET 方法
}
实体类:User
public class User {
private String name;
private Integer age;
private Address address;
//省略GET , SET 方法
}
Controller方法:
@RestController
public class RequestController {
//JSON参数
@RequestMapping("/jsonParam")
public String jsonParam(@RequestBody User user){
System.out.println(user);
return "OK";
}
}
Postman测试:
路径参数
传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。
在现在的开发中,经常还会直接在请求的URL中传递参数。例如:
http://localhost:8080/user/1
http://localhost:880/user/1/0
上述的这种传递请求参数的形式呢,我们称之为:路径参数。
学习路径参数呢,主要掌握在后端的controller方法中,如何接收路径参数。
路径参数:
- 前端:通过请求URL直接传递参数
- 后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数
Controller方法:
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}")
public String pathParam(@PathVariable Integer id){
System.out.println(id);
return "OK";
}
}
Postman测试:
传递多个路径参数:
Postman:
Controller方法:
@RestController
public class RequestController {
//路径参数
@RequestMapping("/path/{id}/{name}")
public String pathParam2(@PathVariable Integer id, @PathVariable String name){
System.out.println(id+ " : " +name);
return "OK";
}
}
总结
后端Web实战(IOC+DI)
前言
Web开发的基础知识 ,包括 Tomcat、Servlet、HTTP协议等,我们都已经学习完毕了,那接下来,我们就要进入Web开发的实战篇。在实战篇中,我们将通过一个案例,来讲解Web开发的核心技术。
我们先来看一下,在这个实战篇中,我们都要完成哪些功能。
1). 部门管理
2). 员工管理
3). 员工信息统计
4). 日志信息统计
5). 班级管理
6). 学员管理
7). 学员信息统计
8). 登录认证
上述需求,都是在这个案例中,我们需要完成的功能 。
而我们今天主要完成如下功能:
开发规范
环境准备
查询部门
分层解耦(IOC+DI)
开发规范
前后端分离开发
在之前的课程中,我们介绍过,现在的企业项目开发有2种开发模式:前后台混合开发和前后台分离开发。
前后台混合开发,顾名思义就是前台后台代码混在一起开发,如下图所示:
这种开发模式有如下缺点:
- 沟通成本高:后台人员发现前端有问题,需要找前端人员修改,前端修改成功,再交给后台人员使用
- 分工不明确:后台开发人员需要开发后台代码,也需要开发部分前端代码。很难培养专业人才
- 不便管理:所有的代码都在一个工程中
- 难以维护:前端代码更新,和后台无关,但是需要整个工程包括后台一起重新打包部署。
所以我们目前基本都是采用的前后台分离开发方式,如下图所示:
我们将原先的工程分为前端工程和后端工程这2个工程,然后前端工程交给专业的前端人员开发,后端工程交给专业的后端人员开发。
前端页面需要数据,可以通过发送异步请求,从后台工程获取。但是,我们前后台是分开来开发的,那么前端人员怎么知道后台返回数据的格式呢?后端人员开发,怎么知道前端人员需要的数据格式呢?
所以针对这个问题,我们前后台统一制定一套规范!我们前后台开发人员都需要遵循这套规范开发,这就是我们的接口文档。接口文档有离线版和在线版本,接口文档示可以查询今天提供资料/接口文档里面的资料。
那么接口文档的内容怎么来的呢?是我们后台开发者根据产品经理提供的产品原型和需求文档所撰写出来的,产品原型示例可以参考今天提供资料/页面原型里面的资料。
那么基于前后台分离开发的模式下,我们后台开发者开发一个功能的具体流程如何呢?如下图所示:
- 需求分析:首先我们需要阅读需求文档,分析需求,理解需求。
- 接口定义:查询接口文档中关于需求的接口的定义,包括地址,参数,响应数据类型等等
- 前后台并行开发:各自按照接口文档进行开发,实现需求
- 测试:前后台开发完了,各自按照接口文档进行测试
- 前后段联调测试:前段工程请求后端工程,测试功能
Restful
我们的案例是基于当前最为主流的前后端分离模式进行开发。
在前后端分离的开发模式中,前后端开发人员都需要根据提前定义好的接口文档,来进行前后端功能的开发。
后端开发人员:必须严格遵守提供的接口文档进行后端功能开发(保障开发的功能可以和前端对接)
而在前后端进行交互的时候,我们需要基于当前主流的REST风格的API接口进行交互。
什么是REST风格呢?
- REST(Representational State Transfer),表述性状态转换,它是一种软件架构风格。
传统URL风格如下:
http://localhost:8080/user/getById?id=1 GET:查询id为1的用户
http://localhost:8080/user/saveUser POST:新增用户
http://localhost:8080/user/updateUser POST:修改用户
http://localhost:8080/user/deleteUser?id=1 GET:删除id为1的用户
我们看到,原始的传统URL呢,定义比较复杂,而且将资源的访问行为对外暴露出来了。
基于REST风格URL如下:
http://localhost:8080/users/1 GET:查询id为1的用户
http://localhost:8080/users POST:新增用户
http://localhost:8080/users PUT:修改用户
http://localhost:8080/users/1 DELETE:删除id为1的用户
其中总结起来,就一句话:通过URL定位要操作的资源,通过HTTP动词(请求方式)来描述具体的操作。
在REST风格的URL中,通过四种请求方式,来操作数据的增删改查。
- GET : 查询
- POST :新增
- PUT :修改
- DELETE :删除
我们看到如果是基于REST风格,定义URL,URL将会更加简洁、更加规范、更加优雅。
注意事项:
- REST是风格,是约定方式,约定不是规定,可以打破
- 描述模块的功能通常使用复数,也就是加s的格式来描述,表示此类资源,而非单个资源。如:users、emps、books…
环境准备
Apifox
我们上面讲到,在这个案例中,我们将会基于Restful风格的接口进行交互,那么其中就涉及到常见的4中请求方式,包括:POST、DELETE、PUT、GET。
因为在浏览器地中所发起的所有的请求,都是GET方式的请求。那大家就需要思考两个问题:
- 前后端都在并行开发,后端开发完对应的接口之后,如何对接口进行请求测试呢?
- 前后端都在并行开发,前端开发过程中,如何获取到数据,测试页面的渲染展示呢?
那这里我们就可以借助一些接口测试工具,比如项:Postman、ApiPost、ApiFox等。
那这些工具的使用基本类似,只不过Apifox工具的功能更强强大、更加完善,所以在课程中,我们会采用功能更为强大的ApiFox工具。
介绍
介绍:Apifox是一款集成了Api文档、Api调试、Api Mock、Api测试的一体化协作平台。
作用:接口文档管理、接口请求测试、Mock服务。
安装使用
下载Apifox的安装包,直接双击安装 。
安装完成之后,登录Apifox,就可以使用啦。。。
工程搭建
1). 创建SpringBoot工程,并引入Web开发的起步依赖、lombok的依赖。
2). 准备基础的包结构
查询部门
基本实现
需求
查询所有的部门数据:将 dept.txt
文件中存储的部门数据,查询出来展示在部门管理的页面中。
实现思路
-
加载并读取dept.txt文本中的数据
-
解析文本中的数据,并将其封装为集合
-
响应数据(json格式)
代码实现
具体的代码实现如下:
@RestController
public class DeptController {
@RequestMapping("/depts")
public List<Dept> list2(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IOUtils.readLines(in, "UTF-8");
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//2. 响应数据
return deptList;
}
}
打开Apifox,来测试当前接口:
新建请求,请求 http://localhost:8080/depts
@ResponseBody
前面我们学习过HTTL协议的交互方式:请求响应模式(有请求就有响应)。那么Controller程序呢,除了接收请求外,还可以进行响应。
在我们前面所编写的controller方法中,都已经设置了响应数据。
controller方法中的return的结果,怎么就可以响应给浏览器呢?
答案:使用@ResponseBody注解
@ResponseBody注解:
- 类型:方法注解、类注解
- 位置:书写在Controller方法上或类上
- 作用:将方法返回值直接响应给浏览器,如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器
但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?
这是因为,我们在类上加了@RestController注解,而这个注解是由两个注解组合起来的,分别是:@Controller 、@ResponseBody。 那也就意味着,我们在类上已经添加了@ResponseBody注解了,而一旦在类上加了@ResponseBody注解,就相当于该类所有的方法中都已经添加了@ResponseBody注解。
提示:前后端分离的项目中,一般直接在请求处理类上加@RestController注解,就无需在方法上加@ResponseBody注解了。
统一响应结果
分析
1). 刚才我们执行查询部门操作,查询返回的结果是一个List<Dept>
,原始代码及响应给前端的结果如下:
2). 如果我们还要实现一个需求,根据ID查询部门名称,原始代码及响应给前端的结果如下:
3). 如果我们还要实现一个需求,根据ID查询部门数据,原始代码及响应给前端的结果如下:
而大家会发现,上述的每一个需求,我们都实现了,但是呢,所有的Controller的方法的返回值是各式各样的,什么样的都有,响应的结果,也是各式各样。 如果做一个大型项目,要实现的需求、功能非常多,如果按照这种方案来,最终就会造成项目不便管理、难以维护。
而为了解决这个问题,我们就需要统一响应结果。 也就是说,无论什么实现什么功能,最终响应给前端的格式应该是统一的 。
统一响应结果
前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。
统一的返回结果使用类来描述,在这个结果中包含:
-
响应状态码:当前请求是成功,还是失败
-
状态码信息:给页面的提示信息
-
返回的数据:给前端响应的数据(字符串、对象、集合)
定义在一个实体类Result来包含以上信息。代码如下:
import lombok.Data;
import java.io.Serializable;
/**
* 后端统一返回结果
*/
@Data
public class Result {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private Object data; //数据
public static Result success() {
Result result = new Result();
result.code = 1;
return result;
}
public static Result success(Object object) {
Result result = new Result();
result.data = object;
result.code = 1;
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
功能优化
明确了为什么要统一响应结果,以及如何封装统一响应结果之后,接下来,我们就来将刚才完成的查询部门的功能完善一下。 分为如下两步操作:
1). 引入统一响应结果 Result (资料中提供)
2). 改造DeptController中的方法返回值
@RestController
public class DeptController {
@RequestMapping("/depts")
public Result list2(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
List<String> lines = IOUtils.readLines(in, "UTF-8");
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//2. 响应数据
return Result.success(deptList);
}
}
打开Apifox进行测试:
而我们在测试时候发现,即使我们将请求方式设置为 POST
PUT
DELETE
,也都是可以请求成功的。 如下所示:
这是因为,我们服务器端,也就是Controller程序中并没有限制该接口的请求方式,那么此时任何请求方式都是可以的。 如果要设置请求方式,可以通过如下两种方式来设置:
-
在controller的方法上,声明
@RequestMapping
注解的method属性,通过method属性指定请求方式。 如下:@RequestMapping(value = "/depts", method = RequestMethod.GET)
-
直接使用
@GetMapping
来替换 @RequestMapping 注解,@GetMapping其实就是对@RequestMapping的封装,并限定了请求方式为GET。@GetMapping("/depts")
前后端联调测试
联调测试
完成了查询部门的功能,我们也通过 Apifox 工具测试通过了,下面我们再基于前后端分离的方式进行接口联调。具体操作如下:
1、将资料中提供的 "前端环境" 文件夹中的压缩包,拷贝到一个没有中文不带空格的目录下。
2、拷贝到一个没有中文不带空格的目录后,进行解压 (解压到当前目录)
3、双击 nginx.exe
启动Nginx,一闪而过,就说明nginx以启动完成。
如果在任务管理器中,能看到上述两个进程,就说明nginx已经启动成功。
4、打开浏览器,访问:http://localhost:90
5、测试:系统信息管理 -> 查询部门列表
请求访问流程
前端工程请求服务器的地址为 http://localhost:90/api/depts
,是如何访问到后端的tomcat服务器的?
其实这里,是通过前端服务Nginx中提供的反向代理功能实现的。
1). 浏览器发起请求,请求的是localhost:90 ,那其实请求的是nginx服务器。
2). 在nginx服务器中呢,并没有对请求直接进行处理,而是将请求转发给了后端的tomcat服务器,最终由tomcat服务器来处理该请求。
这个过程就是通过nginx的反向代理实现的。 那为什么浏览器不直接请求后端的tomcat服务器,而是直接请求nginx服务器呢,主要有以下几点原因:
1). 安全:由于后端的tomcat服务器一般都会搭建集群,会有很多的服务器,把所有的tomcat暴露给前端,让前端直接请求tomcat,对于后端服务器是比较危险的。
2). 灵活:基于nginx的反向代理实现,更加灵活,后端想增加、减少服务器,对于前端来说是无感知的,只需要在nginx中配置即可。
3). 负载均衡:基于nginx的反向代理,可以很方便的实现后端tomcat的负载均衡操作。
具体的请求访问流程如下:
location:用于定义匹配特定uri请求的规则。
^~ /api/:表示精确匹配,即只匹配以/api/开头的路径。
rewrite:该指令用于重写匹配到的uri路径。
proxy_pass:该指令用于代理转发,它将匹配到的请求转发给位于后端的指令服务器。
分层解耦
问题分析
上述案例的功能,我们虽然已经实现,但是呢,我们会发现案例中:解析文本文件中的数据,处理数据的逻辑代码,给页面响应的代码全部都堆积在一起了,全部都写在controller方法中了。
当前程序的这个业务逻辑还是比较简单的,如果业务逻辑再稍微复杂一点,我们会看到Controller方法的代码量就很大了。
-
当我们要修改操作数据部分的代码,需要改动Controller
-
当我们要完善逻辑处理部分的代码,需要改动Controller
-
当我们需要修改数据响应的代码,还是需要改动Controller
这样呢,就会造成我们整个工程代码的复用性比较差,而且代码难以维护。 那如何解决这个问题呢?其实在现在的开发中,有非常成熟的解决思路,那就是分层开发。
三层架构
介绍
在我们进行程序设计以及程序开发时,尽可能让每一个接口、类、方法的职责更单一些(单一职责原则)。
单一职责原则:一个类或一个方法,就只做一件事情,只管一块功能。
这样就可以让类、接口、方法的复杂度更低,可读性更强,扩展性更好,也更利用后期的维护。
我们之前开发的程序呢,并不满足单一职责原则。下面我们来分析下之前的程序:
那其实我们上述案例的处理逻辑呢,从组成上看可以分为三个部分:
- 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作。
- 逻辑处理:负责业务逻辑处理的代码。
- 请求处理、响应数据:负责,接收页面的请求,给页面响应数据。
按照上述的三个组成部分,在我们项目开发中呢,可以将代码分为三层,如图所示:
- Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
- Service:业务逻辑层。处理具体的业务逻辑。
- Dao:数据访问层(Data Access Object),也称为持久层。负责数据访问操作,包括数据的增、删、改、查。
基于三层架构的程序执行流程,如图所示:
- 前端发起的请求,由Controller层接收(Controller响应数据给前端)
- Controller层调用Service层来进行逻辑处理(Service层处理完后,把处理结果返回给Controller层)
- Serivce层调用Dao层(逻辑处理过程中需要用到的一些数据要从Dao层获取)
- Dao层操作文件中的数据(Dao拿到的数据会返回给Service层)
思考:按照三层架构的思想,如何要对业务逻辑(Service层)进行变更,会影响到Controller层和Dao层吗?
答案:不会影响。 (程序的扩展性、维护性变得更好了)
***三层架构的具体结构
Dao(Data Access Object)数据访问层:访问数据
一般包含两块
对应操作实体类的mapper接口
数据库对应表实体类
domain
pojo(Plain Ordinary Java Object)
entity
只有属性
Service 业务层:面向接口开发
处理具体业务逻辑
一般包含三块
Service接口
定义抽象方法
Service接口实现类
写业务逻辑
业务层中所需数据实体类
VO(Value Object):值对象
DTO(Data Transfer Object):数据转换对象
Controller 控制层:访问控制
接收前端请求,进行参数校验
一般包含两块
(Servlet)接口
接收接口参数所需实体类
PO(Parameter Object )参数对象
请求中包含参数,不是必须,一般新增或者修改请求使用较多
书写方式一般按照 DAO -> Service ->Controller
***三层架构的执行过程
代码拆分
我们使用三层架构思想,来改造下之前的程序:
- 控制层包名:com.itheima.controller
- 业务逻辑层包名:com.itheima.service.impl
- 数据访问层包名:com.itheima.dao.impl
1). 控制层:接收前端发送的请求,对请求进行处理,并响应数据
@RestController
public class DeptController {
private DeptServiceImpl deptService = new DeptServiceImpl();
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
2). 业务逻辑层:处理具体的业务逻辑
public class DeptServiceImpl {
private DeptDaoImpl deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//......
return deptList;
}
}
3). 数据访问层:负责数据的访问操作,包含数据的增、删、改、查
public class DeptDaoImpl {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
return IOUtils.readLines(in, "UTF-8");
}
}
具体的请求调用流程:
三层架构的好处:
- 复用性强
- 便于维护
- 利用扩展
问题分析
Dao层在进行获取数据时,可能是从文件中获取 ,也可能有数据库中获取 ,那也就意味着Dao层的实现方式有多种 。
如果Dao层的实现方式有多种,如何增强程序的扩展性呢 ? 答案就是:接口、面相接口编程。
那么接下来,我们就需要为Dao层、Service层来设计对应的接口,并让实现类继承对应的接口 。
程序优化
1). Dao层
接口:
public interface DeptDao {
//查询全部部门数据
public List<String> queryDeptList();
}
实现:
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
return IOUtils.readLines(in, "UTF-8");
}
}
2). Service层
接口:
public interface DeptService {
//查询所有的部门数据
public List<Dept> queryDeptList();
}
实现:
public class DeptServiceImpl implements DeptService {
private DeptDao deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//......
return deptList;
}
}
3). Controller层
由于Controller层,就是接收请求,响应数据,不涉及到数据的访问、也不涉及逻辑处理,所以一般可以不要接口。
@RestController
public class DeptController {
private DeptService deptService = new DeptServiceImpl();
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
分层解耦
问题分析
由于我们现在在程序中,需要什么对象,直接new一个对象 new DeptServiceImpl()
。
如果说我们需要更换实现类,比如由于业务的变更,DeptServiceImpl 不能满足现有的业务需求,我们需要切换为 DeptServiceImpl2 这套实现,就需要修改Contorller的代码,需要创建 DeptServiceImpl2 的实现new DeptServiceImpl2()
。
Service中调用Dao,也是类似的问题。这种呢,我们就称之为层与层之间 耦合 了。 那什么是耦合呢 ?
首先需要了解软件开发涉及到的两个概念:内聚和耦合。
- 内聚:软件中各个功能模块内部的功能联系。
业务内聚:某一类业务的相关代码应该放在一起
- 耦合:衡量软件中各个层/模块之间的依赖、关联的程度。
组合原则:类与类之间优先使用组合关系,而不是继承
类与类之间的组合关系:
继承: is a
组合:属于关系,每个类还可以单独使用
软件设计原则:高内聚低耦合。
高内聚:指的是一个模块中各个元素之间的联系的紧密程度,如果各个元素(语句、程序段)之间的联系程度越高,则内聚性越高,即 "高内聚"。
低耦合:指的是软件中各个层、模块之间的依赖关联程序越低越好。
目前层与层之间是存在耦合的,Controller耦合了Service、Service耦合了Dao。而 高内聚、低耦合的目的是使程序模块的可重用性、移植性大大增强。
那最终我们的目标呢,就是做到层与层之间,尽可能的降低耦合,甚至解除耦合。
解耦思路
之前我们在编写代码时,需要什么对象,就直接new一个就可以了。 这种做法呢,层与层之间代码就耦合了,当service层的实现变了之后, 我们还需要修改controller层的代码。
那应该怎么解耦呢?
1). 首先不能在EmpController中使用new对象。代码如下:
此时,就存在另一个问题了,不能new,就意味着没有业务层对象(程序运行就报错),怎么办呢?
我们的解决思路是:
- 提供一个容器,容器中存储一些对象(例:DeptService对象)
- Controller程序从容器中获取DeptService类型的对象
2). 将要用到的对象交给一个容器管理。
3). 应用程序中用到这个对象,就直接从容器中获取
那问题来了,我们如何将对象交给容器管理呢? 程序运行时,容器如何为程序提供依赖的对象呢?
我们想要实现上述解耦操作,就涉及到Spring中的两个核心概念:
-
控制反转: Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
对象的创建权由程序员主动创建转移到容器(由容器创建、管理对象)。这个容器称为:IOC容器或Spring容器(其实就是map集合)
-
依赖注入: Dependency Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
程序运行时需要某个资源,此时容器就为其提供这个资源。
实际就是给属性赋值
例:EmpController程序运行时需要EmpService对象,Spring容器就为其提供并注入EmpService对象
IOC容器中创建、管理的对象,称之为:bean对象。
IOC&DI入门
1). 将Service及Dao层的实现类,交给IOC容器管理
在实现类加上 @Component
注解,就代表把当前类产生的对象交给IOC容器管理。
A. DeptDaoImpl
@Component
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
return IOUtils.readLines(in, "UTF-8");
}
}
B. DeptServiceImpl
@Component
public class DeptServiceImpl implements DeptService {
private DeptDao deptDao;
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//......
return deptList;
}
}
2). 为Controller 及 Service注入运行时所依赖的对象
通过 @Autowired
注解为应用程序提供运行时依赖的对象。
A. DeptServiceImpl
@Component
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//......
return deptList;
}
}
B. DeptController
@RestController
public class DeptController {
@Autowired
private DeptService deptService ;
@GetMapping("/depts")
public Result list(){
//1. 调用deptService
List<Dept> deptList = deptService.queryDeptList();
//2. 响应数据
return Result.success(deptList);
}
}
启动服务,运行测试。 打开浏览器,地址栏直接访问:http://localhost:90
***IOC详解
通过IOC和DI的入门程序呢,我们已经基本了解了IOC和DI的基础操作。接下来呢,我们学习下IOC控制反转和DI依赖注入的细节。
Bean的声明
前面我们提到IOC控制反转,就是将对象的控制权交给Spring的IOC容器,由IOC容器创建及管理对象。IOC容器创建的对象称为bean对象。
在之前的入门案例中,要把某个对象交给IOC容器管理,需要在类上添加一个注解:@Component
而Spring框架为了更好的标识web应用程序开发当中,bean对象到底归属于哪一层,又提供了@Component的衍生注解:
注解 | 说明 | 位置 |
---|---|---|
@Component | 声明bean的基础注解 | 不属于以下三类时,用此注解 |
@Controller | @Component的衍生注解 | 标注在控制层类上 |
@Service | @Component的衍生注解 | 标注在业务层类上 |
@Repository | @Component的衍生注解 | 标注在数据访问层类上(由于与mybatis整合,用的少) |
@Mapper |
那么此时,我们就可以使用 @Service
注解声明Service层的bean。 使用 @Repository
注解声明Dao层的bean。 代码实现如下:
Service层
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao= new DeptDaoImpl();
@Override
public List<Dept> queryDeptList() {
//1. 调用deptDao
List<String> lines = deptDao.queryDeptList();
//2. 对原始数据进行处理 , 组装部门数据
List<Dept> deptList = lines.stream().map(line -> {
String[] parts = line.split(",");
Integer id = Integer.parseInt(parts[0]);
String name = parts[1];
LocalDateTime updateTime = LocalDateTime.parse(parts[2],
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
return new Dept(id, name, updateTime);
}).toList();
//......
return deptList;
}
}
Dao层
@Repository
public class DeptDaoImpl implements DeptDao {
@Override
public List<String> queryDeptList(){
//1. 加载文件 , 获取原始数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("dept.txt");
return IOUtils.readLines(in, "UTF-8");
}
}
注意1:声明bean的时候,可以通过注解的value属性指定bean的名字,如果没有指定,默认为类名首字母小写。
注意2:使用以上四个注解都可以声明bean,但是在springboot集成web开发中,声明控制器bean只能用@Controller。
组件扫描
问题:使用前面学习的四个注解声明的bean,一定会生效吗?
答案:不一定。(原因:bean想要生效,还需要被组件扫描)
-
前面声明bean的四大注解,要想生效,还需要被组件扫描注解
@ComponentScan
扫描。 -
该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解
@SpringBootApplication
中,默认扫描的范围是启动类所在包及其子包。
DI详解
上一小节我们讲解了控制反转IOC的细节,接下来呢,我们学习依赖注解DI的细节。
依赖注入,是指IOC容器要为应用程序去提供运行时所依赖的资源,而资源指的就是对象。
在入门程序案例中,我们使用了@Autowired这个注解,完成了依赖注入的操作,而这个Autowired翻译过来叫:自动装配。
@Autowired注解,默认是按照类型进行自动装配的(去IOC容器中找某个类型的对象,然后完成注入操作)
入门程序举例:在EmpController运行的时候,就要到IOC容器当中去查找EmpService这个类型的对象,而我们的IOC容器中刚好有一个EmpService这个类型的对象,所以就找到了这个类型的对象完成注入操作。
那如果在IOC容器中,存在多个相同类型的bean对象,会出现什么情况呢?
此时,我们运行程序,看到控制台已经报错了。
如何解决上述问题呢?Spring提供了以下几种解决方案:
-
@Primary
-
@Qualifier
-
@Resource
方案一:使用@Primary注解
当存在多个相同类型的Bean注入时,加上@Primary注解,来确定默认的实现。
@Primary
@Service
public class DeptServiceImpl implements DeptService {
}
方案二:使用@Qualifier注解
指定当前要注入的bean对象。 在@Qualifier的value属性中,指定注入的bean的名称。 @Qualifier注解不能单独使用,必须配合@Autowired使用
@RestController
public class DeptController {
@Qualifier("deptServiceImpl")
@Autowired
private DeptService deptService;
方案三:使用@Resource注解
是按照bean的名称进行注入。通过name属性指定要注入的bean的名称。
@RestController
public class DeptController {
@Resource(name = "deptServiceImpl")
private DeptService deptService;
DI面试题
面试题 : @Autowird 与 @Resource的区别
- @Autowired 是spring框架提供的注解,而@Resource是JDK提供的注解
- @Autowired 默认是按照类型注入,而@Resource是按照名称注入
哪些类适合控制反转和依赖注入
-
控制器类(Controller)
Spring MVC是Spring框架的一部分,它提供了一个基于模型-视图-控制器(MVC)的Web应用程序开发架构。在Spring MVC中,控制器类负责处理客户端请求并返回响应。通过将控制器类交给Spring管理,可以方便地实现请求的映射、参数绑定、异常处理等功能。
-
服务类(Service)
在应用程序中,服务类负责处理业务逻辑。将服务类交给Spring管理可以实现依赖注入、事务管理等功能。使用Spring的依赖注入功能,可以方便地将依赖的类注入到服务类中,从而实现松耦合和可测试性。同时,Spring的事务管理功能可以确保服务类中的数据库操作具有一致性和完整性。
-
数据访问对象(DAO)
DAO类负责处理数据库的操作。通过将DAO类交给Spring管理,可以方便地使用Spring的事务管理功能和持久化框架(如Hibernate或MyBatis)来简化数据库操作。此外,Spring还提供了一些便捷的功能,如异常转换和批量更新,用于简化数据库访问的代码
-
工具类
工具类通常包含一些辅助方法,用于完成特定的功能。通过将工具类交给Spring管理,可以方便地在其他类中使用这些方法,避免手动创建对象。
-
AOP切面(Aspect)
DAO类负责处理数据库的操作。通过将DAO类交给Spring管理,可以方便地使用Spring的事务管理功能和持久化框架(如Hibernate或MyBatis)来简化数据库操作。此外,Spring还提供了一些便捷的功能,如异常转换和批量更新,用于简化数据库访问的代码
-
配置类(Configuration)
DAO类负责处理数据库的操作。通过将DAO类交给Spring管理,可以方便地使用Spring的事务管理功能和持久化框架(如Hibernate或MyBatis)来简化数据库操作。此外,Spring还提供了一些便捷的功能,如异常转换和批量更新,用于简化数据库访问的代码