总体技术体系
单一架构
一个项目,一个工程,导出为一个war包,在一个Tomcat上运行。也叫all in one。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PSWh4Dcw-1679163182412)(SSM.assets/img002.03307860.png)]
分布式架构
一个项目(对应 IDEA 中的一个 project),拆分成很多个模块,每个模块是一个 IDEA 中的一个 module。每一个工程都是运行在自己的 Tomcat 上。模块之间可以互相调用。每一个模块内部可以看成是一个单一架构的应用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gz9xQ61H-1679163182412)(SSM.assets/img003.88bee938.png)]
框架的概念
框架=jar包+配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vxKYp3Wo-1679163182413)(SSM.assets/img004.37fe39f1.png)]
一、原生Web应用中的组件
1、请求处理流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0wnziuVs-1679163182413)(SSM.assets/image-1678705096156-1.png)]
表述层: ●视图层:Thymeleaf负责服务器端页面渲染 ●控制层:XxxServlet负责处理请求、跳转页面
2、组件
基本上我们可以说,整个项目就是由各种组件搭建而成的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bSk8y3Z8-1679163182414)(SSM.assets/image-1678705128515-4.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uk3gBlnS-1679163182414)(SSM.assets/image-1678705157766-7.png)]
不用框架:private BookService bookService = new BookServiceImpl();
框架不仅仅是创建对象,还会增强这个对象的功能,其中最重要的就是给Service方法附加事务操作。 另一个方面,使用框架之后,实现了解耦。
SSM整合
总体来说,就是两件事情,两个层次:
- 顶层:开发功能
- 底层:搭建环境
- Spring和Mybatis整合
- Spring和SpringMVC整合
第一部分:Spring & MyBatis整合
1.1.1整体思路
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IPEPh2gg-1679163182414)(SSM.assets/image-1679054257983-1.png)]
1.1.2 MyBatis运行机制及设计
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PtimvW8d-1679163182415)(SSM.assets/3f307380c0fb62bf446bc6321fbef06b.png)]
Mybatis的功能架构
分为三层:
(1) API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
(3) 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
Spring整合Mybatis要解决的问题
-
将 SqlSessionFactory 对象交由 Spring 来管理。
将 SqlSessionFactory 的对象生成器 SqlSessionFactoryBean 注册在 Spring 容器中,再将其注入给 Dao 的实现类即可完成整合。
-
对于mybatis执行sql语句,需要用到的对象有:
- SqlSessionFactory对象,只有创建了SqlSessionFactory对象,才能调用openSession()方法得到SqlSession对象。
- dao接口的代理对象,例如StudentDao接口,需要的代理对象为:SqlSeesion.getMapper(StudentDao.class)。
- 数据源DataSource对象,使用一个更强大、功能更多的连接池对象代替mybatis自己的PooledDataSource。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FgYGv05e-1679163182415)(SSM.assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzgyMzgwOA==,size_16,color_FFFFFF,t_70.png)]
第二部分:Spring & SpringMVC整合
第一部分: MyBatis
概述
1.1 特性
-
MyBatis支持定制化SQL、存储过程以及高级映射
-
MyBatis避免了几乎所有的JDBC代码和手动设置参数以及结果集解析操作
-
MyBatis可以使用简单的XML或注解实现配置和原始映射;将接口和Java的POJO(Plain Ordinary Java Object,普通的Java对象)映射成数据库中的记录
-
Mybatis是一个半自动的ORM(Object Relation Mapping)框架
1.2 和其它持久化层技术对比
- JDBC
- SQL 夹杂在Java代码中耦合度高,导致硬编码内伤
- 维护不易且实际开发需求中 SQL 有变化,频繁修改的情况多见
- 代码冗长,开发效率低
- Hibernate 和 JPA
- 操作简便,开发效率高
- 程序中的长难复杂 SQL 需要绕过框架
- 内部自动生成的 SQL,不容易做特殊优化
- 基于全映射的全自动框架,大量字段的 POJO 进行部分映射时比较困难。
- 反射操作太多,导致数据库性能下降
- MyBatis
- 轻量级,性能出色
- SQL 和 Java 编码分开,功能边界清晰。Java代码专注业务、SQL语句专注数据
- 开发效率稍逊于 HIbernate,但是完全能够接收
开发效率:Hibernate>Mybatis>JDBC
运行效率:JDBC>Mybatis>Hibernate
1.3 相关概念
- ORM(Object Relationship Mapping)对象关系映射。
-
对象:Java的实体类对象
-
关系:关系型数据库
-
映射:二者之间的对应关系
-
下表列举的是最简单的单表映射(一个表和一个类):
Java概念 | 数据库概念 |
---|---|
类 | 表 |
属性 | 字段/列 |
对象 | 记录/行 |
-
实体类的多个不同的叫法:
-
domain:领域模型
-
entity:实体
-
POJO:Plain Old Java Object
-
Java bean:一个Java类)
-
![](SSM.assets/2023-01-03_214536.png)
-
映射文件命名规则
一个数据库表<->一个pojo实体类<->一个mapper接口<->一个映射文件
-
表所对应的实体类的类名+Mapper.xml
例如: 表Luser,映射的实体类为Uuser,所对应的映射文件为UserMapper.xml
-
MyBatis映射文件用于编写SQL,访问以及操作表中的数据
-
MyBatis映射文件存放的位置是src/main/resources/mappers目录下
-
-
MyBatis中可以面向接口操作数据,要保证两个一致:
- mapper接口的全类名和映射文件的命名空间(namespace)保持一致
- mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致
-
MyBatis的使用
使用Idea创建核心配置文件和映射文件的模板
- 位置:File | Settings | 编辑器 | 文件和代码模板
2.1 Mybatis 全局
配置文件(mybatis-config.xml)解读
习惯上命名为 mybatis-config.xml,这个文件名仅仅只是建议,并非强制要求。将来整合 Spring 之后,这个配置文件可以省略
1.properties标签
–引入配置文件
<properties resource="jdbc.properties" />
2.typeAliases标签
–设置类型别名
-
用alias标注别名
<typeAlias type="com.yujie.mybatis.pojo.User" alias="abc"></typeAlias>
-
当不写alias时,会设置默认别名为类名,此处为User,不区分大小写
<typeAlias type="com.yujie.mybatis.pojo.User" ></typeAlias>
-
直接放入包名,则此包下所有默认实体类均按默认类名=别名,且不区分大 小写
<package name="com.yujie.mybatis.pojo"/>
3.environments标签
配置Mybatis的开发环境,可以配置多个环境。
在众多具体环境中,使用default属性指定实际运行时使用的环境。
default属性的取值是environment标签的id属性的值。
a.transactionManager标签
Mybatis的内置的事务管理器
type:设置事务管理的方式:
1.JDBC(使用原生的事务管理方式)
2.MANAGED(被管理,eg:Spring)
b.dataSource标签
配置数据源
type:设置数据源的类型type="POOLED|UNPOOLED|JNDI"
POOLED:表示使用数据库连接池
UNPOOLED:表示不使用数据库连接池
JNDI:表示使用上下文中的数据源
c.property标签
建立数据库连接的具体信息
-
直接配置
<property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis-test?serverTimezone=UTC"/> <property name="username" value="root"/> <property name="password" value="751688"/>
-
利用properties文件进行配置
格式:包名.选项名:jdbc.driver
<property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/>
4.mappers标签
Mapper注册:指定Mybatis映射文件的具体位置
a.mapper标签
-
直接导入映射文件
resource属性:指定Mapper映射文件的实际存储位置,这里需要使用一个以类路径根目录为基准的相对路径
对Maven工程的目录结构来说,resources目录下的内容会直接放入类路径,所以这里我们可以以resources目录为基准
<mapper resource="mappers/UserMapper.xml"/>
-
以包的形式导入映射文件
需要满足:
1.mapper接口和映射文件所在的包必须保持一致
2.mapper接口的名字和映射文件的名字必须一致之所以需要包名完全一致,因为程序包java和资源包resources编译后在同一个文件夹
2.2 MyBatis的映射文件(mybatis-mapper.xml)解读
1.基本标签
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
2.mapper标签
mapper是根标签,namespace属性:在Mybatis全局范围内找到一个具体的Mapper配置
引入接口后,为了方便通过接口全类名来找到Mapper配置文件,所以通常将namespace属性设置为接口全类名
<mapper namespace="com.yujie.mybatis.mapper.UserMapper">
3.具体SQL语句标签
-
编写具体的SQL语句,使用id属性唯一的标记一条SQL语句
mapper接口和映射文件要保证两个一致:
1、mapper接口的全类名和映射文件的namespace一致
2、mapper接口中的方法的方法名要和映射文件中的sql的id保持一致 -
增删改标签
中间填写具体的SQL语句
<insert id="insertUser"> </insert> <update id="updateUser"> </update> <delete id="deleteUser"> </delete>
-
查询标签
- resultType属性:指定封装查询结果的Java实体类的全
- resultMap属性: 自定义映射,处理多对一或者一对多的映射关系-
<select id="getAllUser" resultType="com.yujie.mybatis.pojo.User"> select * from user </select>
2.3 MyBatis数据输入
(1)概述
1. Mybatis总体机制概括
![img](SSM.assets/img005.ebd8c6a3.png)
2.概念说明
-
简单类型:只包含一个值的数据类型
-
基本数据类型:int、byte、short、double、……
-
基本数据类型的包装类型:Integer、Character、Double、……
-
字符串类型:String
-
-
复杂类型:包含多个值的数据类型
- 实体类类型:Employee、Department、……
- 集合类型:List、Set、Map、……
- 数组类型:int[]、String[]、……
- 复合类型:List、实体类中包含集合……
在设计接口时,增删改方法的返回值类型只能是void和int(只能用来返回处理的行数时用)
(2)两种方式
1. #{}形式
Mybatis会将SQL语句中的#{}转换问号占位符。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hBYhGHIq-1679163182415)(SSM.assets/image.png)]
2. ${}形式
${}形式传参,底层Mybatis做的是字符串拼接操作。
** 注意 + 单引号的问题, e g : U s e r N a m e = ′ {} + 单引号的问题,eg: UserName = ' +单引号的问题,eg:UserName=′{param1}'**
![img](SSM.assets/image-1673467548772-3.png)
(3)分类讨论
1. 参数为多个的简单类型数据
此时MyBatis会将参数放在map集合中,以两种方式存储数据
-
以arg0,arg1…为键,以参数为值
-
以param1,param2…为键,以参数为值
因此,只需要通过#{}和${}访问map集合的键,就可以获取相对应的值
- 以@Param注解的value属性值为键,以参数为值(推荐)
①Mapper接口中抽象方法的声明
int updateEmployee(@Param("empId") Integer empId,@Param("empSalary") Double empSalary);
②SQL语句
<update id="updateEmployee">
update t_emp set emp_salary=#{empSalary} where emp_id=#{empId}
</update>
③对应关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OI1bd1aj-1679163182416)(SSM.assets/img007.976da128.png)]
2.实体类类型参数
①Mapper接口中抽象方法的声明
int insertEmployee(Employee employee);
②SQL语句
<insert id="insertEmployee">
insert into t_emp(emp_name,emp_salary) values(#{empName},#{empSalary})
</insert>
③对应关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1pQkdEN-1679163182416)(SSM.assets/img006.f9958c52.png)]
④结论
Mybatis会根据#{}中传入的数据,加工成getXxx()方法,通过反射在实体类对象中调用这个方法,从而获取到对应的数据。填充到#{}解析后的问号占位符这个位置。
3. 参数为Map类型
①Mapper接口中抽象方法的声明
int updateEmployeeByMap(Map<String, Object> paramMap);
②SQL语句
<update id="updateEmployeeByMap">
update t_emp set emp_salary=#{empSalaryKey} where emp_id=#{empIdKey}
</update>
③junit测试
@Test
public void testUpdateEmpNameByMap() {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("empSalaryKey", 999.99);
paramMap.put("empIdKey", 5);
int result = mapper.updateEmployeeByMap(paramMap);
log.info("result = " + result);
}
有很多零散的参数需要传递,但是没有对应的实体类类型可以使用。使用@Param注解一个一个传入又太麻烦了。所以都封装到Map中。
2.5 数据输出
(1)返回实体类对象
-
单个对象
注意设置 resultType 即可,可以使用别名.
当查询的数据为多条时,不能使用实体类作为返回值
当SQL语句返回多条记录的时候,会报:TooManyResultException 的错误.
User getUserById(@Param("id") String id);
<select id="getUserById" resultType="user"> SELECT * FROM t_user WHERE id = #{id} </select>
-
List集合多个返回值
注意设置 resultType 依然为 POJO 的类型
List<User> getAllUser();
<select id="getAllUser" resultType="user">
SELECT * FROM t_user;
</select>
(2)返回单个数据
MyBatis 为 java中常用的类型设置了类型别名.
int → java.lang.Integer
string → java.lang.String
_int → int
map → java.utils.Map;
故而resultType中填写的是别名,不用在意大小写
Integer getCount();
<select id="getCount" resultType="int">
SELECT count(1) FROM t_user;
</select>
(3)返回 Map集合
查询的结果没有对应的实体类的时候,就可以使用Map集合。
无论是一条数据还是多条数据都可以使用
-
方法一:List
多条数据会返回多条map,所以用list方式查询
List<Map<String, Object>> getAllUser();
<select id="getUserById" resultType="map"> SELECT * FROM t_user; </select>
-
方法二:@MapKey 注解
@MapKey("id") //将id设为map的键,其余设置为值 Map<String, Object> getAllUserToMap();
<select id="getAllUserToMap" resultType="map">
SELECT * FROM t_user;
</select>
// Map<id, User>
{
3={password=123, gender=男, id=3, age=23, email=12345@qq.com, username=root},
4={password=123456, gender=男, id=4, age=23, email=12345@qq.com, username=admin},
5={password=123456, gender=男, id=5, age=23, email=12345@qq.com, username=admin},
6={password=123456, gender=男, id=6, age=20, email=geek_zh@163.com, username=zs}
}
2.6 特殊SQL
(1)模糊查询
List<User> getUserByLike(@Param("mohu") String mohu);
<select id="getUserByLike" resultType="user">
SELECT * FROM t_user WHERE username like '%${mohu}%'
</select>
<select id="getUserByLike" resultType="user">
SELECT * FROM t_user WHERE username like CONCAt('%', #{mohu}, '%');
</select>
<!-- 第三种方式:用的最多的方式的 -->
<select id="getUserByLike" resultType="user">
SELECT * FROM t_user WHERE username like "%"#{mohu}"%"
</select>
(2)批量删除 in
即传入的参数数量不确定 这里直接先构成一个字符串,再使用拼接方式传入即可。
int deleteMore(@Param("ids") String ids);
<!-- ids = "1,2,3" -->
<delete id="deleteMore" resultType="int">
DELETE FROM t_user WHERE id in (${ids})
</delete>
(3) 动态表名
表查询的字段相同,但是表名称不相同
表名不能加‘’,所以只能用${},
List<User> getUserList(@Param("tableName") String tableName);
<select id="getUserList" resultType="user">
SELECT * FROM ${tableName};
</select>
(4)获取自增主键
场景:比如一个班级表和一个学生表,在一个事务当中先创建一个班级(使用的是自增的主键)然后在这个班级里面添加若干的学生。
Integer insertUser(User user);
<insert id="insertUser" resultType="int" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user VALUES(NULL, #{username}, #{password}, #{age}, #{gender}, #{email});
</insert>
-
useGeneratedKeys属性:
jdbc本身不支持获取自增主键
此选项为“true”时表示当前添加功能使用自增的主键
-
keyProperty属性:
因为增删改有统一的返回值是受影响的行数
则利用实体类的属性将此添加数据的自增主键带回
2.7 自定义映射ResultMap
(1)字段名和属性名不一致的情况
1.方式一:查询的时候使用别名
2.方式二:开启下划线 → 小驼峰的配置,在 mybatis-config.xml
文件当中添加如下配置。
<settings>
<!-- 下划线 自动映射 驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
3.方式三:使用 ResltMap , 注意 resultMap 和 resultType 是二选一的。
Emp getEmpByEmpId(@Param("empId") Integer empId);
<mapper namespace="com.atguigu.mybatis.mapper.EmpMapper">
<!-- 相同的属性和名称不用写也行的 -->
<resultMap id="empResultMap" type="Emp">
<!-- id 处理主键和属性字段的映射关系 -->
<id column="emp_id" property="empId"></id>
<!-- result 处理普通字段和属性的映射关系 -->
<result column="emp_name" property="empName"></result>
</resultMap>
<!-- Emp getEmpByEmpId(@Param("empId") Integer empId); -->
<select id="getEmpByEmpId" resultMap="empResultMap">
SELECT * FROM t_emp WHERE emp_id = #{empId};
</select>
</mapper>
(2)resultMap处理字段和属性的映射关系
若字段名和实体类中的属性名不一致,则可以通过resultMap设置自定义映射
-
属性:
-
id:表示自定义映射的唯一标识
-
type:查询的数据要映射的实体类的类型
-
-
子标签:
- id:设置主键的映射关系
- result:设置普通字段的映射关系
- association:设置多对一的映射关系
- property:设置映射关系中实体类中的属性名
- javaType:设置需要处理的属性类型
- column:设置映射关系中表中的字段名
- collection:设置一对多的映射关系
(3)多对一关系
1.方式一:级联方式处理 ,即一次连接出所有字段。
Emp getEmpAndDeptByEmpId(@Param("empId") Integer empId);
<resultMap id="getEmpAndDeptByEmpId" type="Emp">
<id column="emp_id" property="empId"></id>
<result column="emp_name" property="empName"></result>
<!-- 级联方式:多对一的映射关系 -->
<result column="dept_id" property="dept.deptId"></result>
<result column="dept_name" property="dept.deptName"></result>
</resultMap>
<select id="getEmpAndDeptByEmpId" resultMap="getEmpAndDeptByEmpId">
SELECT *
FROM t_emp
LEFT JOIN t_dept
ON t_emp.dept_id = t_dept.dept_id
WHERE emp_id = #{empId};
</select>
![2023-01-14_025812](SSM.assets/2023-01-14_025812.png)
2. 方式二:association 标签
相当于嵌套书写方法一,
所以内部需要有独立的id标签,
且properties可以直接写属性名
<resultMap id="getEmpAndDeptByEmpId" type="Emp">
<id column="emp_id" property="empId"></id>
<result column="emp_name" property="empName"></result>
<!-- association : 专门处理多对一、一对一 的映射关系 -->
<association property="dept" javaType="Dept">
<id column="dept_id" property="deptId"></id>
<result column="dept_name" property="deptName"></result>
</association>
</resultMap>
<select id="getEmpAndDeptByEmpId" resultMap="getEmpAndDeptByEmpId">
SELECT *
FROM t_emp
LEFT JOIN t_dept
ON t_emp.dept_id = t_dept.dept_id
WHERE emp_id = #{empId};
</select>
方法一二对比:
![2023-01-14_030552](SSM.assets/2023-01-14_030552.png)
3. 方式三:分布查询
先查询出Emp,再根据Emp中deptId 查询 Dept
优点:可以延迟加载,但必须在核心配置文件设置全局配置
- EmpMapper.java
Emp getEmpAndDeptByStepOne(@Param("empId") Integer empId);
- DeptMapper.java
Dept getEmpAndDeptByStepTwo(@Param("deptId") Integer deptId);
- EmpMapper.xml
- property: 关联的类型
- fetchType: eager 表示全局配置了懒加载,但是这里我还是想立即加载
- select: 第二步查询的唯一标识
- column: 第一步查询出来的外键
<resultMap id="getEmpAndDeptByStepResultMap" type="Emp">
<id column="emp_id" property="empId"></id>
<result column="emp_name" property="empName"></result>
<association property="dept" fetchType="eager" select="com.atguigu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo"
column="dept_id" >
</association>
</resultMap>
<!-- Emp getEmpAndDeptByStepOne(@Param("empId") Integer empId); -->
<select id="getEmpAndDeptByStepOne" resultMap="getEmpAndDeptByStepResultMap">
SELECT * FROM t_emp WHERE emp_id = #{empId};
</select>
- DeptMapper.xml
<!-- Dept getEmpAndDeptByStepTwo(@Param("deptId") Integer deptId);
这里没有使用 ResultMap, 因为开启了 下划线 转 驼峰的配置
-->
<select id="getEmpAndDeptByStepTwo" resultType="dept">
SELECT * FROM t_dept WHERE dept_id = #{dept_id};
</select>
分步查询的优点:
可以实现延迟加载
但是必须在核心配置文件中设置全局配置信息:
lazyLoadingEnabled:延迟加载的全局开关。当开启时,所有关联对象都会延迟加载
aggressiveLazyLoading:当开启时,任何方法的调用都会加载该对象的所有属性。否则,每个属性会按需加载
此时就可以实现按需加载,获取的数据是什么,就只会执行相应的sql。
此时可通过association和collection中的fetchType属性设置当前的分步查询是否使用延迟加载,
fetchType=“lazy(延迟加载)|eager(立即加载)”
(4)一对多关系
POJO:一个 Dept 对应多个 Emp
public class Dept {
private Integer deptId;
private String deptName;
private List<Emp> emps;
// getter/setter/toString
}
1. 方式一:级联 + collection
Dept getDeptAndEmpsByDeptId(@Param("deptId") Integer deptId);
<resultMap id="getDeptAndEmpsByDeptIdResultMap" type="dept">
<id column="dept_id" property="deptId"></id>
<result column="dept_name" property="deptName"></result>
<!--
property: 关联的属性.
ofType: 集合内部元素的类型.
-->
<collection property="emps" ofType="emp">
<id column="emp_id" property="empId"></id>
<result column="emp_name" property="empName"></result>
</collection>
</resultMap>
<!-- Dept getDeptAndEmpsByDeptId(@Param("deptId") Integer deptId); -->
<select id="getDeptAndEmpsByDeptId" resultMap="getDeptAndEmpsByDeptIdResultMap">
SELECT *
FROM t_dept
LEFT JOIN t_emp
ON t_dept.dept_id = t_emp.dept_id
WHERE t_dept.dept_id = #{deptId};
</select>
2. 方式二:分布查询
先查询部门,再根据部门的id查询部门下的员工
- DeptMapper.java
//分步查询 查询部门以及部门中的员工的信息
Dept getDeptAndEmpdsStepOne(@Param("deptId") Integer deptId);
- EmpMapper.java
//分布查询的第二步:查询出一个部门下的员工
List<Emp> getDeptAndEmpdsStepTwo(@Param("deptId") Integer deptId);
- DeptMapper.xml
<resultMap id="getDeptAndEmpdsStepOneResultMap" type="dept">
<id column="dept_id" property="deptId"></id>
<result column="dept_name" property="deptName"></result>
<!-- fetchType="eager" 关闭懒加载 -->
<collection property="emps" fetchType="eager" ofType="emp"
select="com.atguigu.mybatis.mapper.EmpMapper.getDeptAndEmpdsStepTwo"
column="dept_id">
</collection>
</resultMap>
<!-- Dept getDeptAndEmpdsStepOne(@Param("deptId") Integer deptId); -->
<select id="getDeptAndEmpdsStepOne" resultMap="getDeptAndEmpdsStepOneResultMap">
SELECT *
FROM t_dept
WHERE dept_id = #{deptId};
</select>
- EmpMapper.xml
<!--List<Emp> getDeptAndEmpdsStepTwo(@Param("empId") Integer empId);-->
<select id="getDeptAndEmpdsStepTwo" resultType="emp">
SELECT *
FROM emp
WHERE dept_id = #{deptId};
</select>
(5)多对多关系
2.8 动态SQL
根据特定条件动态拼装SQL语句的功能
为了解决 拼接SQL语句字符串时的痛点问题。
(1)if
<!-- List<Emp> getEmpByCondition(Emp emp); -->
<select id="getEmpByCondition" resultType="emp">
SELECT *
FROM t_emp
<trim prefix="WHERE" suffixOverrides="AND">
<if test="empName != null and empName != ''">
emp_name LIKE "%"#{empName}"%" AND
</if>
<if test="age != null and age != ''">
age = #{age} AND
</if>
<if test="gender != null and gender != ''">
gender = #{gender} AND
</if>
</trim>
</select>
(2) choose, when, otherwise (switch)
<!-- List<Emp> getEmpByChoose(Emp emp); -->
<select id="getEmpByChoose" resultType="emp">
SELECT *
FROM t_emp
<where>
<choose>
<when test="empName != null and empName != ''">
emp_name = #{empName}
</when>
<when test="age != null and age != ''">
age = #{age}
</when>
</choose>
</where>
</select>
(3) foreach(批量插删,IN)
经常用于批量添加 和 批量删除
- 批量插入:
<!-- void insertMoreEmp(@Params("emps") List<Emp> emps); -->
<insert id="insertMoreEmp">
INSERT INTO t_emp
VALUES
<foreach collection="emps" item="emp" separator=",">
(NULL, #{emp.empName}, #{emp.age}, #{emp.gender}, null)
</foreach>
</insert>
- 批量删除:
<!-- void deleteMoreEmp(@Param("empIds") Integer[] empIds); -->
<delete id="deleteMoreEmp">
DELETE FROM t_emp
WHERE emp_id
IN
<foreach collection="empIds" item="empId" separator="," open="(" close=")">
#{empId}
</foreach>
</delete>
2.9 SQL片段
将重复的SQL代码抽取出来被多个语句重复使用
<sql id="empColumns">
emp_id, emp_name, age, gender, dept_id
</sql>
<!-- List<Emp> getEmpByCondition(Emp emp); -->
<select id="getEmpByCondition" resultType="emp">
SELECT <include refid="empColumns"></include>
FROM t_emp
<trim prefix="WHERE" suffixOverrides="AND">
<if test="empName != null and empName != ''">
emp_name LIKE "%"#{empName}"%" AND
</if>
<if test="age != null and age != ''">
age = #{age} AND
</if>
<if test="gender != null and gender != ''">
gender = #{gender} AND
</if>
</trim>
</select>
第二部分:Spring框架(Spring5)
概述
- 轻量级,开源的,JavaEE框架
- 可以解决企业应用开发的复杂性
- 两个核心部分:IOC(控制反转)&AOP(面向切面)
- 面向切面编程:AOP,在不修改源代码的基础上增强代码功能。
- IOC:控制反转,把创建对象过程交给Spring进行管理
1 特点
- 方便解耦,简化开发
- Aop.编程支持。
- 方便程序测试。
- 方便和其他框架进行整合
- 方便进行事务操作。
- 降低API开发难度。
Spring Framework五大功能模块
功能模块 | 功能介绍 |
---|---|
Core Container | 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。 |
AOP&Aspects | 面向切面编程 |
Testing | 提供了对 junit 或 TestNG 测试框架的整合。 |
Data Access/Integration | 提供了对数据访问/集成的功能。 |
Spring MVC | 提供了面向Web应用程序的集成功能。 |
2 IOC(Inversion of Control,反转控制)
2.1 IOC思想及底层原理
-
IOC目的:降低耦合
-
发展历程:
-
原始模式
(问题:耦合度太高,关联度过于紧密)
-
纯工厂模式
问题:依旧存在耦合度,需要进一步解耦
-
IOC
-
-
底层原理
- xml解析
- 工厂模式
- 反射
-
DI
DI:Dependency Injection,翻译过来是依赖注入。
DI 是 IOC 的另一种表述方式:即组件以一些预先定义好的方式(例如:setter 方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。
IOC 就是一种反转控制的思想, 而 DI 是对 IOC 的一种具体实现。
2.2 IOC接口
-
IOC思想基于IOC容器完成,IOC容器底层就是对象工厂。
-
Spring提供IOC容器实现两种方式:(两个接口)。
-
Beanfactory: IOC容器基本实现,是Spring 内部的使用接口,不提供开发人员进行使用。
加载配置文件时候不会创建对象,在获取对象(使用)才去创建对象。
-
ApplicationContext: Beanfactory接口的子接口,提供更多更强大的功能,一般由开发人员进行使用。
加载配置文件时候就会把在配置文件对象进行创建。 (在服务器启动时就完成,节省操作时的时间和资源)
-
2.3 IOC操作-Bean管理-基于xml方式
2.3.1 创建bean
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-96nc7rdQ-1679163182416)(SSM.assets/img006.c8bae859.png)]
-
配置组件
`<!-- 实验一 [重要]创建bean -->` `<bean id="happyComponent" class="com.atguigu.ioc.component.HappyComponent"/>`
- bean标签:通过配置bean标签告诉IOC容器需要创建对象的组件是什么
- id属性:bean的唯一标识
- class属性:组件类的全类名
IOC容器如何创建对象
通过class里面设置的类型,来获取类型里的class对象
通过new instance通过反射,来调用无参构造创建的对象。
*Spring* 底层默认通过反射技术调用组件类的无参构造器来创建组件对象
三种不同的方式获取ApplicationCongtest
- 第一种采用类路径的加载方式获取:
ClassPathXmlApplicationContext
此处的文件必须位于classpath路径中
-
第二种采用系统文件路径加载的方式获取:
FileSystemXmlApplicationContext
此处的application.xml必须位于系统中一个具体的位置
(以后的工程会变成jar包,其他电脑不一定有相同的配置文件,一般用第一种)
- 第三种使用beanfactory加载配置信息:
通过反射和工厂模式创建对象
Spring 底层默认通过反射技术调用组件类的无参构造器来创建组件对象,这一点需要注意。如果在需要无参构造器时,没有无参构造器,则会抛出下面的异常:
NoSuchMethodException(找不到此方法)
2.3.2 获取bean的三种方式
-
方式一:根据id获取
由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。
-
方式二:根据类型获取
-
①指定类型的 bean 唯一
能够正常获取到
@Test public void testExperiment02() { HappyComponent component = iocContainer.getBean(HappyComponent.class); component.doWork(); }
-
②指定类型的 bean 不唯一
相同类型的 bean 在IOC容器中一共配置了两个:
<!-- 实验一 [重要]创建bean --> <bean id="happyComponent" class="com.atguigu.ioc.component.HappyComponent"/> <!-- 实验二 [重要]获取bean --> <bean id="happyComponent2" class="com.atguigu.ioc.component.HappyComponent"/>
-
根据类型获取时会抛出异常:NoUniqueBeanDefinitionException
没找到对应类型的bean:NoUniqueBeanDefinitionException
-
方式三:id和类型
-
如果组件类实现了接口,根据接口类型可以获取 bean 吗?
可以,前提是bean唯一
-
如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?
不行,因为bean不唯一
根据类型来获取bean时,在满足bean唯一性的前提下,
其实只是看:『对象 instanceof 指定的类型』的返回结果,
只要返回的是true就可以认定为和类型匹配,能够获取到。
通过bean的类型,bean所继承的类的类型,bean所实现的接口的类型都可以
2.3.3 给bean的属性赋值:
- setter注入
通过property标签配置的属性值会通过setXxx()方法注入
<!-- 实验三 [重要]给bean的属性赋值:setter注入 -->
<bean id="happyComponent3" class="com.atguigu.ioc.component.HappyComponent">
<!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
<!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
<!-- value属性:指定属性值 -->
<property name="componentName" value="veryHappy"/>
</bean>
- 引用外部已声明的bean
- 配置新组件的 bean
<bean id="happyMachine" class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="makeHappy"/>
</bean>
- 在原组件的 bean 中引用新组件的 bean
<bean id="happyComponent4" class="com.atguigu.ioc.component.HappyComponent">
<!-- ref 属性:通过 bean 的 id 引用另一个 bean -->
<property name="happyMachine" ref="happyMachine"/>
</bean>
这个操作在 IDEA 中有提示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6Lu9JDTD-1679163182417)(SSM.assets/img010.ae0329dd.png)]
-
易错点
如果错把ref属性写成了value属性,会抛出异常:
Caused by: java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘com.atguigu.ioc.component.HappyMachine’ for property ‘happyMachine’: no matching editors or conversion strategy found
意思是不能把String类型转换成我们要的HappyMachine类型
说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值
ref:类;value: 字面量
- 内部bean
在bean里面配置的bean就是内部bean,内部bean只能在当前bean内部使用,在其他地方不能使用。
<!-- 实验五 [重要]给bean的属性赋值:内部bean -->
<bean id="happyComponent5" class="com.atguigu.ioc.component.HappyComponent">
<property name="happyMachine">
<!-- 在一个 bean 中再声明一个 bean 就是内部 bean -->
<!-- 内部 bean 可以直接用于给属性赋值,可以省略 id 属性 -->
<bean class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="makeHappy"/>
</bean>
</property>
</bean>
- 构造器注入
利用类的构造函数赋值,如果有多个构造函数。可以用一个或多个name来指定赋值项,帮助选择
constructor-arg标签还有两个属性可以进一步描述构造器参数:
- index属性:指定参数所在位置的索引(从0开始)
- name属性:指定参数名
<!-- 实验八 给bean的属性赋值:构造器注入 -->
<bean id="happyTeam" class="com.atguigu.ioc.component.HappyTeam">
<constructor-arg value="happyCorps"/>
<constructor-arg value="10"/>
<constructor-arg value="1000.55"/>
</bean>
- 级联属性赋值
装配关联对象并赋值级联属性
关联对象:happyMachine
级联属性:happyMachine.machineName
<!-- 实验七 给bean的属性赋值:级联属性赋值 -->
<bean id="happyComponent6" class="com.atguigu.ioc.component.HappyComponent">
<!-- 装配关联对象 -->
<property name="happyMachine" ref="happyMachine2"/>
<!-- 对HappyComponent来说,happyMachine的machineName属性就是级联属性 -->
<property name="happyMachine.machineName" value="cascadeValue"/>
</bean>
6.特殊值处理
-
CDATA节
<!-- 实验九 给bean的属性赋值:特殊值处理 --> <bean id="propValue" class="com.atguigu.ioc.component.PropValue"> <property name="expression"> <!-- 解决方案二:使用CDATA节 --> <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 --> <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 --> <!-- 所以CDATA节中写什么符号都随意 --> <value><![CDATA[a < b]]></value> </property> </bean>
-
XML实体
<!-- 实验九 给bean的属性赋值:特殊值处理 --> <bean id="propValue" class="com.atguigu.ioc.component.PropValue"> <!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 --> <!-- 解决方案一:使用XML实体来代替 --> <property name="expression" value="a < b"/> </bean>
- 集合属性
<!-- 实验十一 给bean的属性赋值:集合属性 -->
<bean id="happyTeam2" class="com.atguigu.ioc.component.HappyTeam">
<property name="memberNameList">
<!-- list标签:准备一组集合类型的数据,给集合属性赋值 -->
<!--<list>
<value>member01</value>
<value>member02</value>
<value>member03</value>
</list>-->
<!-- 使用set标签也能实现相同效果,只是附带了去重功能 -->
<!--<set>
<value>member01</value>
<value>member02</value>
<value>member02</value>
</set>-->
<!-- array也同样兼容 -->
<array>
<value>member01</value>
<value>member02</value>
<value>member02</value>
</array>
</property>
<property name="managerList">
<!-- 给Map类型的属性赋值 -->
<!--<map>
<entry key="财务部" value="张三"/>
<entry key="行政部" value="李四"/>
<entry key="销售部" value="王五"/>
</map>-->
<!-- 也可以使用props标签 -->
<props>
<prop key="财务部">张三2</prop>
<prop key="行政部">李四2</prop>
<prop key="销售部">王五2</prop>
</props>
</property>
</bean>
2.3.4 配置
不用bean标签,用util标签
<!-- 实验十一 给bean的属性赋值:集合属性 -->
<util:list id="machineList">
<bean class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="machineOne"/>
</bean>
<bean class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="machineTwo"/>
</bean>
<bean class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="machineThree"/>
</bean>
</util:list>
2.3.5 引入外部属性文件
- 加入依赖
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
- 使用
- 一个项目中不需要创建多个数据源,数据源是帮助我们管理连接的
<!-- 实验六 [重要]给bean的属性赋值:引入外部属性文件 -->
因为数据源并没有实现类,所以不可以用ref
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis-test?serverTimezone=UTC"></property>
<property name="username" value="root"></property>
<property name="password" value="751688"></property>
</bean>
-
也可以专门设置jdbc.properties 保存引入
Spring容器仅允许最多定义一个PropertyPlaceholderConfigurer 或
<content:property-placeholder>
其余的会被Spring忽略。
在 IDEA 中引入 Spring 配置文件中名称空间的两种操作方式:
●在打字标签名的过程中根据提示选择一个正确的名称空间
●对于直接复制过来的完整标签,可以在名称空间上点击,然后根据提示引入:
引入之后,可以用“${来设置路径}”
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
2.3.6 bean的作用域
- 概念
在 Spring 中可以通过配置 bean 标签的 scope 属性来指定 bean 的作用域范围,各取值含义参加下表:
取值 | 含义 | 创建对象的时机 | 默认值 |
---|---|---|---|
singleton | 在 IOC 容器中,这个 bean 的对象始终为单实例 | IOC 容器初始化时 | 是 |
prototype | 这个 bean 在 IOC 容器中有多个实例 | 获取 bean 时 | 否 |
说明:这里单实例、多实例指的是一个bean标签配置之后,对应一个对象还是多个对象。
如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):
取值 | 含义 |
---|---|
request | 在一个请求范围内有效 |
session | 在一个会话范围内有效 |
<!-- 实验十五 bean的作用域 -->
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="happyMachine4" scope="prototype" class="com.atguigu.ioc.component.HappyMachine">
<property name="machineName" value="iceCreamMachine"/>
</bean>
2.3.7 bean的生命周期清单
- bean 对象创建(调用无参构造器)–实例化
- 给 bean 对象设置属性(调用属性对应的 setter 方法)–依赖注入
- bean 对象初始化之前操作(由 bean 的后置处理器负责)–初始化
- 需要通过init-method属性指定初始化的方法(类中的函数名)
- bean 对象初始化(需在配置 bean 时指定初始化方法)
- bean 对象初始化之后操作(由 bean 的后置处理器负责)
以上三步再获取ioc容器的时候,就已经完成了–前提bean的scoup属性是单例(singleton)(创建一次用很多次)
如果是多例(prototype),每次需要的是新的对象,所以不需要提前创建,在获取bean时完成
-
bean 对象就绪可以使用
-
bean 对象销毁(需在配置 bean 时指定销毁方法)
- ioc容器关闭之后才会销毁(ioc.close)
- 而常使用的ApplicationContext是没有关闭容器的方法的
- ApplicationContext的子接口ConfigurableApplicationContext拓展了刷新和关闭容器的方法
- 需要通过bean的destroy-method属性制定销毁的方法。(类中的函数名)
-
IOC 容器关闭
2.3.8 自动装配autowire
-
byType表示根据类型进行装配,
-
根据制定的策略,自动为bean中的类类型的属性或接口类型的属性赋值
-
类的beanID需要和属性名相一致
-
此时如果没有一个类型匹配的bean,则使用属性的默认值
-
此时如果类型匹配的bean不止一个,那么会抛NoUniqueBeanDefinitionException -->
-
-
byName表示根据bean的id进行匹配。(不常用)
而bean的id是根据需要装配组件的属性的属性名来确定的 -->
- 当类型匹配了多个时用byname
<!-- 实验十二 自动装配 -->
<bean id="happyService3" class="com.atguigu.ioc.component.HappyService"/>
<bean id="happyService2" class="com.atguigu.ioc.component.HappyService"/>
<!-- 使用bean标签的autowire属性设置自动装配效果 -->
<!-- byType表示根据类型进行装配,此时如果类型匹配的bean不止一个,那么会抛NoUniqueBeanDefinitionException -->
<!-- byName表示根据bean的id进行匹配。而bean的id是根据需要装配组件的属性的属性名来确定的 -->
<bean id="happyController"
class="com.atguigu.ioc.component.HappyController"
autowire="byName"
>
<!-- 手动装配:在property标签中使用ref属性明确指定要装配的bean -->
<!--<property name="happyService" ref="happyService"/>-->
</bean>
```
2.4 IOC操作-Bean管理-基于注解
- 扫描
Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解 进行后续操作。
2.4.1 四个典型注解
- @Component
注解标记的普通组件
-
@Controller
注解标记的控制器组件,这个组件就是我们在三层架构中表述层里面,使用的控制器。以前是Servlet,以后我们将会使用Controller来代替Servlet。
-
@Service
注解标记的业务逻辑组件
这个组件就是我们在三层架构中使用的业务逻辑组件。
-
@Repository注解标记的持久化层组件
这个组件就是我们以前用的Dao类,但是以后我们整合了Mybatis,这里就变成了Mapper接口,而Mapper接口是由Mybatis和Spring的整合包负责扫描的。
由于Mybatis整合包想要把Mapper接口背后的代理类加入Spring的IOC容器需要结合Mybatis对Mapper配置文件的解析,所以这个事情是Mybatis和Spring的整合包来完成,将来由Mybatis负责扫描,也不使用@Repository注解。
@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。
这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。
注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记
2.4.2 扫描方式
-
指定要排除的组件
或者也可以说指定不扫描的组件
-
context:exclude-filter标签:指定排除规则
-
type属性:annotation|asssignable
-
annotation取值表示根据注解的全类名来进行排除,形如:“org.springframework.stereotype.Repository”
-
asssignable取值表示根据类的全类名来进行排除
形如:com.atguigu.ioc.component
-
expression属性:指定排除规则的表达式,对于注解来说指定全类名即可
<!-- 指定不扫描的组件 --> <context:component-scan base-package="com.atguigu.ioc.component"> <!-- context:exclude-filter标签:指定排除规则 --> <!-- type属性:指定根据什么来进行排除,annotation取值表示根据注解来排除 --> <!-- expression属性:指定排除规则的表达式,对于注解来说指定全类名即可 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
-
-
仅扫描指定组件
-
context:include-filter标签:指定在原有扫描规则的基础上追加的规则
-
use-default-filters属性:取值false表示关闭默认扫描规则
(否则,默认base-package下所有都扫描)
-
context:include-filter标签:指定在原有扫描规则的基础上追加的规则
-
<!-- 情况四:仅扫描指定的组件 -->
<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.atguigu.ioc.component" use-default-filters="false">
<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
-
指定匹配模式
<!-- 情况二:在指定扫描包的基础上指定匹配模式 --> <context:component-scan base-package="com.atguigu.ioc.component" resource-pattern="Soldier*.class"/>
-
最基本的扫描方式[常用]
<!-- 配置自动扫描的包 -->
<!-- 最基本的扫描方式 -->
<context:component-scan base-package="com.atguigu.ioc.component"/>
2.4.5 组件的BeanName
- 默认情况
==简单类名首字母小写就是 bean 的 id。(小驼峰)==例如:SoldierController 类对应的 bean 的 id 就是 soldierController。
简单类名:不带包名的类名本身 EmpController
全类名:带包名的完整的类名 com.atguigu.ioc.component.EmpController
-
使用value属性指定
@Controller(value = "tianDog") public class SoldierController { }
当注解中只设置一个属性时,value属性的属性名可以省略:
@Service("smallDog") public class SoldierService { }
2.4.6 自动装配(注解方式)
-
@Autowired
- 表示在成员变量上,不需要设置成员变量的set方法
- 也可以表示在set方法上,也可以实现功能
- 为当前成员变量的复制的有参构造上
-
注解的原理
-
默认通过byType的方式,在IOC容器中通过类型匹配某个bean为属性赋值
-
如果有多个类型匹配的Bean,转化为byName的方式实现自动装配
用要赋值的属性名作为bean的ID匹配
-
如果都不行,可以添加注解“@Qualifier
通过指定的注解的value属性值,指定某个bean的ID,将这个bean为属性赋值
-
-
@Autowired 工作流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4VLdjnu-1679163182417)(SSM.assets/image-20230314222129892.png)]
- 首先根据所需要的组件类型到 IOC 容器中查找
- 能够找到唯一的 bean:直接执行装配
- 如果完全找不到匹配这个类型的 bean:装配失败
- 和所需类型匹配的 bean 不止一个
- 没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配
- 能够找到:执行装配
- 找不到:装配失败
- 使用 @Qualifier 注解:根据 @Qualifier 注解中指定的名称作为 bean 的id进行匹配
- 能够找到:执行装配
- 找不到:装配失败
- 没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配
3 AOP(Aspect Oriented Programming 面向切面编程)
3.1 代理模式
![](SSM.assets/img004.e76b3080.png)
![](SSM.assets/img005.74dd7746.png)
-
相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 动词:指做代理这个动作,或这项工作
- 名词:扮演代理这个角色的类、对象、方法
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
理解代理模式、AOP的核心关键词就一个字:套
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
3.1.1 静态代理
- 创建代理对象继承实际对象接口
- 将被代理的目标对象声明为成员变量
- 附加功能由代理类中的代理方法来实现
- 通过目标对象来实现核心业务逻辑
@Slf4j
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
log.debug("[日志] add 方法开始了,参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
log.debug("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
……
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
3.1.2 动态代理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNA3T7PV-1679163182417)(SSM.assets/img003.2fe524a2.png)]
①生产代理对象的工厂类
JDK本身就支持动态代理,这是反射技术的一部分。下面我们还是创建一个代理类(生产代理对象的工厂类):
@Slf4j
// 泛型T要求是目标对象实现的接口类型,本代理类根据这个接口来进行代理
public class LogDynamicProxyFactory<T> {
// 将被代理的目标对象声明为成员变量
private T target;
public LogDynamicProxyFactory(T target) {
this.target = target;
}
public T getProxy() {
// 创建代理对象所需参数一:加载目标对象的类的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
// 创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
Class<?>[] interfaces = target.getClass().getInterfaces();
// 创建代理对象所需参数三:InvocationHandler对象
// Lambda表达式口诀:
// 1、复制小括号
// 2、写死右箭头
// 3、落地大括号
InvocationHandler handler = (
// 代理对象,当前方法用不上这个对象
Object proxy,
// method就是代表目标方法的Method对象
Method method,
// 外部调用目标方法时传入的实际参数
Object[] args)->{
// 我们对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
// 围绕目标方法的调用,就可以添加我们的附加功能
// 声明一个局部变量,用来存储目标方法的返回值
Object targetMethodReturnValue = null;
// 通过method对象获取方法名
String methodName = method.getName();
// 为了便于在打印时看到数组中的数据,把参数数组转换为List
List<Object> argumentList = Arrays.asList(args);
try {
// 在目标方法执行前:打印方法开始的日志
log.debug("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);
// 调用目标方法:需要传入两个参数
// 参数1:调用目标方法的目标对象
// 参数2:外部调用目标方法时传入的实际参数
// 调用后会返回目标方法的返回值
targetMethodReturnValue = method.invoke(target, args);
// 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
log.debug("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);
}catch (Exception e){
// 通过e对象获取异常类型的全类名
String exceptionName = e.getClass().getName();
// 通过e对象获取异常消息
String message = e.getMessage();
// 在目标方法失败后:打印方法抛出异常的日志【死于非命】
log.debug("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
}finally {
// 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
log.debug("[动态代理][日志] " + methodName + " 方法最终结束了");
}
// 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
return targetMethodReturnValue;
};
// 创建代理对象
T proxy = (T) Proxy.newProxyInstance(classLoader, interfaces, handler);
// 返回代理对象
return proxy;
}
}
②测试
@Test
public void testDynamicProxy() {
// 1.创建被代理的目标对象
Calculator target = new CalculatorPureImpl();
// 2.创建能够生产代理对象的工厂对象
LogDynamicProxyFactory<Calculator> factory = new LogDynamicProxyFactory<>(target);
// 3.通过工厂对象生产目标对象的代理对象
Calculator proxy = factory.getProxy();
// 4.通过代理对象间接调用目标对象
int addResult = proxy.add(10, 2);
log.debug("方法外部 addResult = " + addResult + "\n");
int subResult = proxy.sub(10, 2);
log.debug("方法外部 subResult = " + subResult + "\n");
int mulResult = proxy.mul(10, 2);
log.debug("方法外部 mulResult = " + mulResult + "\n");
int divResult = proxy.div(10, 2);
log.debug("方法外部 divResult = " + divResult + "\n");
}
3.2 AOP 概念&术语
-
作用
下面两点是同一件事的两面,一枚硬币的两面:
- 代码简化:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
-
横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
-
通知[记住]
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
-
前置通知:在被代理的目标方法前执行
-
返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
-
异常通知:在被代理的目标方法异常结束后执行(死于非命)
-
后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
-
环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
-
切面[记住]
封装通知方法的类。根据不同的非核心业务逻辑,我们可以创建不同的切面类:
-
连接点
和横切关注点一样,这又是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p6WCojTU-1679163182418)(SSM.assets/img010.5af189f7.png)]
- 切入点[记住]
定位连接点的方式。
我们通过切入点,可以将通知方法精准的植入到被代理目标方法的指定位置。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
封装了代理逻辑的通知方法就像一颗制导导弹,在切入点这个引导系统的指引下精确命中连接点这个打击目标:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SsclhKEY-1679163182418)(SSM.assets/image-1678841725113-30.png)]
3.3 AOP实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OnpJucJ2-1679163182418)(SSM.assets/img015.9c921baf.png)]
3.3.1 加入依赖
在IOC所需依赖基础上再加入下面依赖即可:
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
3.3.2 创建配置文件(通过注解实现AOP)
-
< aop:aspectj-autoproxy/>
开启基于注解的aop。
-
<context:component-scan base-package=“com.yujie.spring.aop”/>
配置自动扫描的包
3.3.3 创建切面类
-
切面类和目标类都需要交给IOC容器管理
-
@Component
不是控制类,服务类,所以用普通标签
确保该切面类可以放入IOC容器
-
@Aspect
将当前组件标识为切面组件
-
切面方法注解详解
- @Before:声明当前方法是前置通知方法
-
在目标方法执行之前执行
@Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
- @AfterReturning:AOP返回通知,方法成功返回
-
在目标对象返回值之后执行(通常用来返回执行成功的信息)
-
在返回通知中若要获取目标对象方法的返回值,只需要通过该注解的returning属性returning=”X“
-
则代理方法的同名参数Object X可作为接受返回值的对象
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wjGKS2UO-1679163182419)(SSM.assets/img026.a3718a15.png)]
- @AfterThrowing:AOP异常通知
-
在异常通知中若要获取目标对象方法的异常类型,只需要通过该注解的throwing属性throwing=”X“
-
则代理方法的同名参数Throwable X 可作为接受返回值的对象
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzRG3ivk-1679163182419)(SSM.assets/img027.85db0441.png)]
@AfterThrowing(value = "pointCut()",throwing = "ex") public void throwingAdviceMethod(JoinPoint joinPoint, Throwable ex){ Signature signature = joinPoint.getSignature(); System.out.println("LoggerAspect,[AOP异常通知] "+"方法"+signature.getName()+" 异常为"+ex); } 输出结果: LoggerAspect,[AOP前置通知] 方法div开始了 LoggerAspect,[AOP异常通知] 方法div 异常为java.lang.ArithmeticException: / by zero LoggerAspect,[AOP后置通知] 方法div最终结束了
- @After:AOP后置通知,方法最终结束
在目标对象方法的finally字句中执行
- @Around:标明环绕通知方法
环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。
所以之后要不单独用以上四种的任意一种,要不直接用环绕通知
-
此处也说明了上述四种通知的作用位置
try { System.out.println("环绕通知-->前置通知"); //表示目标对象方法的执行 joinPoint.proceed(); System.out.println("环绕通知-->返回通知"); } catch (Throwable e) { System.out.println("环绕通知-->异常通知"); e.printStackTrace(); }finally { System.out.println("环绕通知-->后置通知"); }
输出结果: 环绕通知-->前置通知 LoggerAspect,[AOP前置通知] 方法div开始了 div方法内部 result = 1 LoggerAspect,[AOP后置通知] 方法div最终结束了 环绕通知-->返回通知 环绕通知-->结束通知undefined
ps:环绕通知的方法的返回值一定要和目标对象方法的返回值一致
-
value属性:
指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
-
execution():固定用法。
-
public int:目标对象方法的访问修饰符和返回值
-
com.atguigu.aop.api.Calculator.add()
不能直接写add,需要将方法所在的详细包名列出
-
add(int,int)
方法所需的输入类型
3.3.4 各个通知获取细节信息
-
JoinPoint接口
org.aspectj.lang.JoinPoint
在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
public void printLogBeforeCore(JoinPoint joinPoint) {
-
要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名
(方法声明时的完整信息)
Signature signature = joinPoint.getSignature();
-
要点2:通过目标方法签名对象获取方法名
String methodName = signature.getName();
-
要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
Object[] args = joinPoint.getArgs();
由于数组直接打印看不到具体数据,所以转换为List集合
List<Object> argList = Arrays.asList(args);
-
要点四:@Pointcut,用于声明一个公共的切入点表达式
@Pointcut("execution(* com.yujie.spring.aop.annotation.Calculator.*(..))")
public void pointCut(){}
则before标注中可以改为
@Before("pointCut()")
-
-
切入点表达式重用
为了使同一个代理方法在目标类中通用,可以写成
@Before("execution(* com.yujie.spring.aop.annotation.CalculatorImpl.*(..))")
-
add(int,int) —> *(…)
代表当前类下所有的方法(任意数量类型的参数列表)
-
public int ----> *
代表任意的访问修饰符和返回值类型
-
类的地方也可以使用*,表示包下所有的类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GoFGHVWy-1679163182420)(SSM.assets/img011.dde1a79a.png)]
-
3.3.5 切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
![](SSM.assets/img012.b353bc56.png)
使用 @Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
![](SSM.assets/img013.53c41dc7-1678860677943-13.png)
此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。
![](SSM.assets/img014.ee4ed40a.png)
3.4 声明式事务
-
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
-
TransactionManager
接口中什么都没有,但是它还是有存在的意义——定义一个技术体系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aoJ8b5W1-1679163182420)(SSM.assets/image-20230315161844653.png)]
我们现在要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 Mybatis 用的也是这个类。
DataSourceTransactionManager类中的主要方法:
- doBegin():开启事务
- doSuspend():挂起事务
- doResume():恢复挂起的事务
- doCommit():提交事务
- doRollback():回滚事务
3.4.1 事务管理器
- 配置事务管理器
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>
-
开启基于注解的声明式事务功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xYRaq3FN-1679163182421)(https://secure2.wostatic.cn/static/hZixzpHSx8biUK3FUkXTRn/image.png?auth_key=1678868417-fUbNfiHY2PhqhfM4hnEz1u-0-321a82858755ba83ec97dccd5f4c628b)]
<!-- 开启基于注解的声明式事务功能 --> <!-- 使用transaction-manager属性指定当前使用是事务管理器的bean --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager"/>
注意:导入名称空间时有好几个重复的,我们需要的是 tx 结尾的那个。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YGr0G67-1679163182421)(SSM.assets/image-1678869687321-20.png)]
- 在需要事务的方法上使用注解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3quZ3oT-1679163182421)(SSM.assets/image-1678869687322-21.png)]
@Transactional
public void updateTwice(
// 修改员工姓名的一组参数
Integer empId4EditName, String newName,
// 修改员工工资的一组参数
Integer empId4EditSalary, Double newSalary
) {
// 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
empDao.updateEmpNameById(empId4EditName, newName);
empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
}
@Transactional注解标识的位置:
-
标识在方法上
-
标识在类上,这类中所有方法,都会被事务管理
3.4.2 事物属性
1.只读
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
- 设置方式
// readOnly = true把当前事务设置为只读
@Transactional(readOnly = true)
public String getEmpName(Integer empId) {
return empDao.selectEmpNameById(empId);
}
@Transactional 注解在类级别标记,会影响到类中的每一个方法。
对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。
- 针对增删改操作设置只读
会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
2. 超时
超时回滚,释放资源。
- @Transactional注解中的设置
@Transactional(readOnly = false, timeout = 3)
public void updateTwice(
// 修改员工姓名的一组参数
Integer empId4EditName, String newName,
// 修改员工工资的一组参数
Integer empId4EditSalary, Double newSalary
) {
// 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
empDao.updateEmpNameById(empId4EditName, newName);
empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
}
- Dao方法中让线程睡眠
public void updateEmpSalaryById(Integer empId, Double salary) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 为了看到操作失败后的效果人为将 SQL 语句破坏
String sql = "update t_emp set emp_salary=? where emp_id=?";
jdbcTemplate.update(sql, salary, empId);
}
注意:sleep操作如果放在执行 SQL 语句后面那就不起作用。
3. 回滚和不回滚的异常
-
默认只针对运行时异常回滚,编译时异常不回滚。
-
设置回滚的异常
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
@Transactional(rollbackFor = Exception.class)
-
设置不回滚的异常
在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。
@Transactional(
noRollbackFor = FileNotFoundException.class
)
- 回滚和不回滚异常同时设置
- 范围不同
不管是哪个设置范围大,都是在大范围内再排除小范围的设定。例如:
- rollbackFor = Exception.class
- noRollbackFor = FileNotFoundException.class
意思是除了 FileNotFoundException 之外,其他所有 Exception 范围的异常都回滚;但是碰到 FileNotFoundException 不回滚。
- 范围一致[好奇]
回滚和不回滚的异常设置了相同范围(这是有多想不开):
- noRollbackFor = FileNotFoundException.class
- rollbackFor = FileNotFoundException.class
此时 Spring 采纳了 rollbackFor 属性的设定:遇到 FileNotFoundException 异常会回滚。
4.事务隔离级别
第三部分: SpringMVC
3.1 概述
3.1.1 优势
-
Spring 家族原生产品,与IOC容器等基础设施无缝对接
-
表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
-
代码清新简洁,大幅度提升开发效率
-
内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
-
性能卓著,尤其适合现代大型、超大型互联网项目要求
3.1.2 配置,搭建环境
- 导入依赖
<dependencies>
<!-- SpringMVC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.1</version>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- ServletAPI -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Spring5和Thymeleaf整合包 -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
<version>3.0.12.RELEASE</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c7qYe6ly-1679163182422)(SSM.assets/image-1678874243675-26.png)]
- 转换module类型
将当前module转换为Web工程,在这个过程中生成web.xml。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-otNZ2OeB-1679163182422)(SSM.assets/image-1678874243675-27.png)]
- web.xml
<!-- 配置SpringMVC中负责处理请求的核心Servlet,也被称为SpringMVC的前端控制器 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<!-- DispatcherServlet的全类名 -->
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- 通过初始化参数指定SpringMVC配置文件位置 -->
<init-param>
<!-- 如果不记得contextConfigLocation配置项的名称,可以到DispatcherServlet的父类FrameworkServlet中查找 -->
<param-name>contextConfigLocation</param-name>
<!-- 使用classpath:说明这个路径从类路径的根目录开始才查找 -->
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做,这些操作放在第一次请求时才执行非常不恰当 -->
<!-- 我们应该将DispatcherServlet设置为随Web应用一起启动 -->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<!-- 对DispatcherServlet来说,url-pattern有两种方式配置 -->
<!-- 方式一:配置“/”,表示匹配整个Web应用范围内所有请求。这里有一个硬性规定:不能写成“/*”。只有这一个地方有这个特殊要求,以后我们再配置Filter还是可以正常写“/*”。 -->
<!-- 方式二:配置“*.扩展名”,表示匹配整个Web应用范围内部分请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
- Spring配置文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WfRy7LCJ-1679163182422)(SSM.assets/image-1678874243676-28.png)]
<!-- 自动扫描包 -->
<context:component-scan base-package="com.atguigu.mvc.handler"/>
<!-- Thymeleaf视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
<property name="order" value="1"/>
<property name="characterEncoding" value="UTF-8"/>
<property name="templateEngine">
<bean class="org.thymeleaf.spring5.SpringTemplateEngine">
<property name="templateResolver">
<bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
<!-- 视图前缀 -->
<property name="prefix" value="/WEB-INF/templates/"/>
<!-- 视图后缀 -->
<property name="suffix" value=".html"/>
<property name="templateMode" value="HTML5"/>
<property name="characterEncoding" value="UTF-8" />
</bean>
</property>
</bean>
</property>
</bean>
3.1.3 第一个实验
创建请求处理器类
- 名称
SpringMVC 对处理请求的类并没有特殊要求,只要是 POJO 即可。我们自己习惯上有两种命名方式:
- XxxHandler:意思是 Xxx 处理器的意思
- XxxController:意思是 Xxx 控制器的意思
这只是一个命名的习惯,不是语法要求。所以往往把处理请求的类叫做『Handler类』,处理请求的方法叫做『Handler方法』。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RmmHD4Nw-1679163182422)(SSM.assets/image-20230315195328588.png)]
2.配置controller
@Controller
public class Hellocontroller {
@RequestMapping("/")
public String showPotal(){
return "portal";
}
// 以后我们会越来越倾向于用一句话来作为请求的URL地址
// 在这样的一句话中使用“/”分隔各个单词
@RequestMapping("/sayhello")
public String sayHello() {
// 方法内部打印日志,证明 SpringMVC 确实调用了这个方法来处理请求
System.out.println("我是 SpringMVC 的 Hello world。");
return "target";
}
}
3.1.4 @RequestMapping
@RequestMapping注解的作用就是将请求的 URL 地址和处理请求的方式(handler方法)关联起来,建立映射关系。
SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的方法来处理这个请求。
- Value属性
- 精确匹配
在@RequestMapping注解指定 URL 地址时,不使用任何通配符,按照请求地址进行精确匹配。
<a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/>
@RequestMapping("/say/hello/to/spring/mvc")
- 模糊匹配
在@RequestMapping注解指定 URL 地址时,通过使用通配符,匹配多个类似的地址。
<h3>测试@RequestMapping注解匹配方式</h3>
<a th:href="@{/fruit/apple}">@RequestMapping模糊匹配[apple]</a><br/>
<a th:href="@{/fruit/orange}">@RequestMapping模糊匹配[orange]</a><br/>
<a th:href="@{/fruit/banana}">@RequestMapping模糊匹配[banana]</a><br/>
@RequestMapping("/fruit/*")
-
单层匹配和多层匹配:
/:只能匹配URL地址中的一层,如果想准确匹配两层,那么就写“//*”以此类推。
/**:可以匹配URL地址中的多层。
其中所谓的一层或多层是指一个URL地址字符串被“/”划分出来的各个层次
这个知识点虽然对于@RequestMapping注解来说实用性不大,但是将来配置拦截器的时候也遵循这个规则。 -
在类级别标记
-
仅标记在方法上的@RequestMapping注解
@RequestMapping("/user/login") @RequestMapping("/user/register") @RequestMapping("/user/logout")
-
分别标记在类和方法上的@RequestMapping注解
在类级别:抽取各个方法上@RequestMapping注解地址中前面重复的部分
@RequestMapping("/user")
-
可以设置多个请求
-
首页地址“/”
在编译时会被解析成首页地址http://localhost:8080/
-
在页面上利用themeleaf ,在th:href="@{/sayhello}"相对路径补全路径,返回给后端
-
@RequestMapping附加请求方式–Method属性
-
在前端页面中,只有超链接method设置为post,其余的超链接请求都是get,地址栏直接访问也是get
-
作用:通过请求的请求方式匹配请求
-
method属性是RequestMethod类型的数组,浏览器所发送的请求方式匹配method属性,该请求才会被注解所标识的方法,进行处理
-
如果路径匹配value属性,但请求方式不匹配
此时报错405
原版 进阶版 @RequestMapping(value = “/emp”,
method = RequestMethod.GET)@GetMapping(“/emp”) @RequestMapping(value = “/emp”,
method = RequestMethod.POST)@PostMapping(“/emp”) -
3.1.5 @RequestParam注解
一名一值
-
使用@RequestParam注解标记handler方法的形参
SpringMVC 会将获取到的请求参数从形参位置给我们传进来
-
必须的参数required 属性
- 设置为 false 表示这个请求参数可有可无:
@RequestParam(value = "userName", required = false)
-
设置为true,但没有提供
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o42Asx3i-1679163182423)(SSM.assets/img008.png)]
-
给请求参数设置默认值
使用 defaultValue 属性给请求参数设置默认值:
@RequestParam(value = "userName", required = false, defaultValue = "missing")
一名多值
-
public String oneNameMultiValue(
// 在服务器端 handler 方法中,使用一个能够存储多个数据的容器就能接收一个名字对应的多个值请求参数 `
@RequestParam("team") List<String> teamList )
模型实体类
- 表单
<form th:action="@{/emp/save}" method="post">
姓名:<input type="text" name="empName"/><br/>
年龄:<input type="text" name="empAge"/><br/>
工资:<input type="text" name="empSalary"/><br/>
<input type="submit" value="保存"/>
</form>
ringMVC 的 Hello world。");
return "target";
}
}
#### 3.1.4 @RequestMapping
@RequestMapping注解的作用就是将请求的 URL 地址和处理请求的方式(handler方法)关联起来,建立映射关系。
SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的方法来处理这个请求。
1. **Value**属性
- 精确匹配
在@RequestMapping注解指定 URL 地址时,**不使用任何通配符**,按照请求地址进行精确匹配。
```HTML
<a th:href="@{/say/hello/to/spring/mvc}">HelloWorld</a><br/>
@RequestMapping("/say/hello/to/spring/mvc")
- 模糊匹配
在@RequestMapping注解指定 URL 地址时,通过使用通配符,匹配多个类似的地址。
<h3>测试@RequestMapping注解匹配方式</h3>
<a th:href="@{/fruit/apple}">@RequestMapping模糊匹配[apple]</a><br/>
<a th:href="@{/fruit/orange}">@RequestMapping模糊匹配[orange]</a><br/>
<a th:href="@{/fruit/banana}">@RequestMapping模糊匹配[banana]</a><br/>
@RequestMapping("/fruit/*")
-
单层匹配和多层匹配:
/:只能匹配URL地址中的一层,如果想准确匹配两层,那么就写“//*”以此类推。
/**:可以匹配URL地址中的多层。
其中所谓的一层或多层是指一个URL地址字符串被“/”划分出来的各个层次
这个知识点虽然对于@RequestMapping注解来说实用性不大,但是将来配置拦截器的时候也遵循这个规则。 -
在类级别标记
-
仅标记在方法上的@RequestMapping注解
@RequestMapping("/user/login") @RequestMapping("/user/register") @RequestMapping("/user/logout")
-
分别标记在类和方法上的@RequestMapping注解
在类级别:抽取各个方法上@RequestMapping注解地址中前面重复的部分
@RequestMapping("/user")
-
可以设置多个请求
-
首页地址“/”
在编译时会被解析成首页地址http://localhost:8080/
-
在页面上利用themeleaf ,在th:href="@{/sayhello}"相对路径补全路径,返回给后端
-
@RequestMapping附加请求方式–Method属性
-
在前端页面中,只有超链接method设置为post,其余的超链接请求都是get,地址栏直接访问也是get
-
作用:通过请求的请求方式匹配请求
-
method属性是RequestMethod类型的数组,浏览器所发送的请求方式匹配method属性,该请求才会被注解所标识的方法,进行处理
-
如果路径匹配value属性,但请求方式不匹配
此时报错405
原版 进阶版 @RequestMapping(value = “/emp”,
method = RequestMethod.GET)@GetMapping(“/emp”) @RequestMapping(value = “/emp”,
method = RequestMethod.POST)@PostMapping(“/emp”) -
3.1.5 @RequestParam注解
一名一值
-
使用@RequestParam注解标记handler方法的形参
SpringMVC 会将获取到的请求参数从形参位置给我们传进来
-
必须的参数required 属性
- 设置为 false 表示这个请求参数可有可无:
@RequestParam(value = "userName", required = false)
-
设置为true,但没有提供
[外链图片转存中…(img-o42Asx3i-1679163182423)]
-
给请求参数设置默认值
使用 defaultValue 属性给请求参数设置默认值:
@RequestParam(value = "userName", required = false, defaultValue = "missing")
一名多值
-
public String oneNameMultiValue(
// 在服务器端 handler 方法中,使用一个能够存储多个数据的容器就能接收一个名字对应的多个值请求参数 `
@RequestParam("team") List<String> teamList )
模型实体类
- 表单
<form th:action="@{/emp/save}" method="post">
姓名:<input type="text" name="empName"/><br/>
年龄:<input type="text" name="empAge"/><br/>
工资:<input type="text" name="empSalary"/><br/>
<input type="submit" value="保存"/>
</form>