MyBatis&Spring Framrwork
1. MyBatis
1.1 概述
-
MyBatis是一款优秀的持久层框架,用于简化JDBC开发
-
MyBatis本是Apache的一个开源项目iBatis,2010年这个项目迁移到了google code,并改名为MyBatis。2013年迁移到Github
-
持久层
- 负责将数据保存到数据库的那一层代码
- JavaEE三层架构:表现层、业务层、持久层
-
MyBatis简化JDBC
- 硬编码:---->配置文件
- 注册驱动,获取连接
- SQL语句
- 操作繁琐:---->自动完成
- 手动设置参数
- 手动封装给结果集
MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作
- 硬编码:---->配置文件
1.2 MyBatis快速入门
-
需求:查询user表中所有的数据
-
创建user表,添加数据
create database mybatis; use mybatis; drop table if exists tb_user; create table tb_user( id int primary key auto_increment, username varchar(20), password varchar(20), gender char(1), addr varchar(30) ); INSERT INTO tb_user VALUES (1, 'zhangsan', '123', '男', '北京'); INSERT INTO tb_user VALUES (2, '李四', '234', '女', '天津'); INSERT INTO tb_user VALUES (3, '王五', '11', '男', '西安');
-
创建模块,导入坐标
<dependencies> <!--MyBatis依赖--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.5</version> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!--junit单元测试--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- 添加slf4j日志api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.20</version> </dependency> <!-- 添加logback-classic依赖 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- 添加logback-core依赖 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> </dependencies>
-
编写 MyBatis 核心配置文件 --> 替换连接信息 解决硬编码问题
从 XML 中构建 SqlSessionFactory
在模块下的 resources 目录下创建mybatis的配置文件 mybatis-config.xml,内容如下
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!--数据库连接信息--> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/> <property name="username" value="root"/> <property name="password" value="123"/> </dataSource> </environment> </environments> <mappers> <!--加载sql的映射文件--> <mapper resource="UserMapper.xml"/> </mappers> </configuration>
-
编写 SQL 映射文件 --> 统一管理sql语句,解决硬编码问题
在模块的 resources 目录下创建映射配置文件 UserMapper.xml ,内容如下:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- namespace:名称空间 --> <mapper namespace="test"> <select id="selectAll" resultType="com.mark.pojo.User"> select * from tb_user; </select> </mapper>
-
编码
-
定义pojo类
package com.mark.pojo; /** * @ClassName User * @Description TODO * @Author Mark * @Date 2022/8/29 16:04 * @Version 1.0 */ public class User { //alt + 拖动鼠标左键 整列编辑 private Integer id; private String username; private String password; private String gender; private String addr; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getAddr() { return addr; } public void setAddr(String addr) { this.addr = addr; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", gender='" + gender + '\'' + ", addr='" + addr + '\'' + '}'; } }
-
加载核心配置文件,获取SqlSessionFactory对象
-
获取SqlSession对象,执行SQL语句
-
释放资源
package com.mark; import com.mark.pojo.User; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; /** * @ClassName MyBatisDemo * @Description TODO MyBatis快速入门 * @Author Mark * @Date 2022/8/29 16:13 * @Version 1.0 */ public class MyBatisDemo { public static void main(String[] args) throws IOException { //1.加载Mybatis核心配置文件,获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取SqlSession对象,执行SQL语句 SqlSession sqlSession = sqlSessionFactory.openSession(); List<User> users = sqlSession.selectList("test.selectAll"); System.out.println(users); //3.释放资源 sqlSession.close(); } }
-
-
-
在IDEA中配置MySql数据库连接
- 在IDEA右侧点击Database
- 左边“+”--->Data Source--->MySQL
- 输入配置
1.3 Mapper代理开发
-
目的:
- 解决原生方式中的硬编码
- 简化后期执行SQL
-
步骤
-
定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下
修改mybatis-config.xml中sql的映射文件位置
<mappers> <!--加载sql的映射文件--> <mapper resource="com/mark/mapper/UserMapper.xml"/> </mappers>
-
设置SQL映射文件的namespace属性为Mapper接口全限定名
<mapper namespace="com.mark.mapper.UserMapper"> <select id="selectAll" resultType="com.mark.pojo.User"> select * from tb_user; </select> </mapper>
-
在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致
package com.mark.mapper; import com.mark.pojo.User; import java.util.List; public interface UserMapper { List<User> selectAll(); }
-
编码
-
通过SqlSession的getMapper方法获取Mapper接口的代理对象
-
调用对应方法完成sql的执行
package com.mark; import com.mark.mapper.UserMapper; import com.mark.pojo.User; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; /** * @ClassName MyBatisDemo * @Description TODO Mapper代理开发 * @Author Mark * @Date 2022/8/29 16:13 * @Version 1.0 */ public class MyBatisDemo2 { public static void main(String[] args) throws IOException { //1.加载Mybatis核心配置文件,获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取SqlSession对象,执行SQL语句 SqlSession sqlSession = sqlSessionFactory.openSession(); // List<User> users = sqlSession.selectList("test.selectAll"); // System.out.println(users); //⭐2.1获取UserMapper接口的代理对象 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); //⭐2.2执行对应方法 List<User> users = userMapper.selectAll(); System.out.println(users); //3.释放资源 sqlSession.close(); } }
-
-
如果Mapper接口名称和SQL映射文件名称相同,在同一目录下,则可以使用包扫描的方式简化SQL映射文件的加载
<mappers>
<!--加载sql的映射文件-->
<!--<mapper resource="com/mark/mapper/UserMapper.xml"/>-->
<package name="com.mark.mapper"/>
</mappers>
1.4 MyBatis核心配置文件
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--类型别名,在SQL映射文件中不再需要写com.mark.pojo.User,可以直接写user,无大小写区分-->
<typeAliases>
<package name="com.mark.pojo"/>
</typeAliases>
<!--environments:配置数据库连接环境信息,可以配置多个environment,通过default属性来切换不同的environment-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!--数据库连接信息-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--加载sql的映射文件-->
<!-- <mapper resource="com/mark/mapper/UserMapper.xml"/>-->
<package name="com.mark.mapper"/>
</mappers>
</configuration>
在配置标签时应当注意顺序
- configuration(配置)
- properties(属性)
- settings(设置)
- typeAliases(类型别名)
- typeHandlers(类型处理器)
- objectFactory(对象工厂)
- plugins(插件)
- environments(环境配置)
- environment(环境变量)
- transactionManager(事务管理器)
- dataSource(数据源)
- environment(环境变量)
- databaseIdProvider(数据库厂商标识)
- mappers(映射器)
1.5 配置文件完成增删改查
-
创建tb_brand表并添加数据
-- 删除tb_brand表 drop table if exists tb_brand; -- 创建tb_brand表 create table tb_brand ( -- id 主键 id int primary key auto_increment, -- 品牌名称 brand_name varchar(20), -- 企业名称 company_name varchar(20), -- 排序字段 ordered int, -- 描述信息 description varchar(100), -- 状态:0:禁用 1:启用 status int ); -- 添加数据 insert into tb_brand (brand_name, company_name, ordered, description, status) values ('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0), ('华为', '华为技术有限公司', 100, '华为致力于把数字世界带入每个人、每个家庭、每个组织,构建万物互联的智能世界', 1), ('小米', '小米科技有限公司', 50, 'are you ok', 1);
-
创建测试类MyBatisTest
-
创建Brand类
package com.mark.pojo; /** * @ClassName Brand * @Description TODO * @Author Mark * @Date 2022/8/29 18:10 * @Version 1.0 */ public class Brand { // id 主键 private Integer id; // 品牌名称 private String brandName; // 企业名称 private String companyName; // 排序字段 private Integer ordered; // 描述信息 private String description; // 状态:0:禁用 1:启用 private Integer status; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getBrandName() { return brandName; } public void setBrandName(String brandName) { this.brandName = brandName; } public String getCompanyName() { return companyName; } public void setCompanyName(String companyName) { this.companyName = companyName; } public Integer getOrdered() { return ordered; } public void setOrdered(Integer ordered) { this.ordered = ordered; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } @Override public String toString() { return "Brand{" + "id=" + id + ", brandName='" + brandName + '\'' + ", companyName='" + companyName + '\'' + ", ordered=" + ordered + ", description='" + description + '\'' + ", status=" + status + '}'; } }
-
创建BrandMapper接口
package com.mark.mapper; import com.mark.pojo.Brand; import java.util.List; public interface BrandMapper { public List<Brand> selectAll(); }
-
创建BrandMapper.xmlSQL映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.mark.mapper.BrandMapper"> </mapper>
1.5.1 查询所有数据
-
在BrandMapper接口中定义方法selectAll()
public interface BrandMapper { public List<Brand> selectAll(); }
-
alt+enter生成映射文件中的select标签,并编写查询语句
<mapper namespace="com.mark.mapper.BrandMapper"> <select id="selectAll" resultType="brand"> select * from tb_brand; </select> </mapper>
-
编写测试类执行方法查询
public class MyBatisTest { @Test public void testSelectAll() throws IOException { //1.获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取sqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取Mapper接口的代理对象 BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4.执行方法 List<Brand> brands = brandMapper.selectAll(); System.out.println(brands); //5.释放资源 sqlSession.close(); } }
结果映射时数据库表的字段名称和实体类的属性名称不一样,则不能自动封装数据,解决方式有两种:
-
给数据库中字段名称不一样的起别名,让数据库中字段与实体类相同
<select id="selectAll" resultType="brand"> select id, brand_name as brandName, company_name as companyName, ordered, description, status from tb_brand; </select>
-
缺点:每次查询都要定义一次别名
-
sql片段
<sql id="brand_column"> id, brand_name as brandName, company_name as companyName, ordered, description, status </sql> <select id="selectAll" resultType="brand"> select <include refid="brand_column"/> from tb_brand; </select>
- 缺点:不灵活
-
-
-
⭐resultMap:
- 定义<resultMap>标签
- 在<select>标签中使用resultMap属性替换resultType属性
<!--id:唯一标识--> <!--type:映射的类型,支持别名--> <resultMap id="brandResultMap" type="brand"> <!-- id:主键字段的映射 column:表的列名 property:实体类的属性名 result:一般字段的映射 column:表的列名 property:实体类的属性名 --> <result column="brand_name" property="brandName"/> <result column="company_name" property="companyName"/> </resultMap> <select id="selectAll" resultMap="brandResultMap"> select * from tb_brand; </select>
两处细节
-
参数占位符:
- #{}:会将其替换为?,防止SQL注入
- ${}:拼sql。会存在sql注入问题
使用时机:
- 参数传递的时候用#{}
- 表名或者列名不固定时用${}
<select id="selectById" resultMap="brandResultMap"> select * from tb_brand where id = #{id}; </select>
Brand selectById(int id);
//接收参数 int id = 1; //4.执行方法 Brand brand = brandMapper.selectById(id); System.out.println(brand);
-
参数类型:parameterType
-
特殊字符处理:
- 转义字符 如<的转义字符:
<
- CDATA区(大写CD回车即可):
<![CDATA[ ]]>
- 转义字符 如<的转义字符:
条件查询
-
多条件查询
-
散装参数:如果方法中有多个参数,需要使用
@Param("SQL参数占位符名称")
List<Brand> selectByCondition(@Param("status")int status,@Param("companyName")String companyName,@Param("brandName")String brandName);
<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand where status = #{status} and company_name like #{companyName} and brand_name like #{brandName} ; </select>
//接收参数 int status = 1; String companyName = "华为"; String brandName = "华为"; //处理参数 companyName = "%" + companyName + "%"; brandName = "%" + brandName + "%"; //4.执行方法 List<Brand> brands = brandMapper.selectByCondition(status, companyName, brandName); System.out.println(brands);
-
对象参数:对象的属性名称要和参数占位符名称一致
List<Brand> selectByCondition(Brand brand);
//接收参数 int status = 1; String companyName = "华为"; String brandName = "华为"; //处理参数 companyName = "%" + companyName + "%"; brandName = "%" + brandName + "%"; Brand brand = new Brand(); brand.setStatus(status); brand.setCompanyName(companyName); brand.setBrandName(brandName); //4.执行方法 List<Brand> brands = brandMapper.selectByCondition(brand); System.out.println(brands);
-
map集合参数
List<Brand> selectByCondition(Map map);
//接收参数 int status = 1; String companyName = "华为"; String brandName = "华为"; //处理参数 companyName = "%" + companyName + "%"; brandName = "%" + brandName + "%"; Map map =new HashMap(); map.put("status",status); map.put("companyName",companyName); map.put("brandName",brandName); //4.执行方法 List<Brand> brands = brandMapper.selectByCondition(map); System.out.println(brands);
-
1.5.2 添加
添加步骤:
-
编写接口方法:Mapper接口
- 参数:除了id之外的所有数据
- 结果:void
void add(Brand brand);
-
编写SQL语句:SQL映射文件
<insert id="add"> insert into tb_brand(brand_name, company_name, ordered, description, status) values(#{brandName},#{companyName},#{ordered},#{description},#{status}); </insert>
-
执行方法,测试
- MyBatis事务:
- openSession():默认开启事务,进行增删改操作后需要使用sqlSession.commit(),手动提交事务
- openSession(true):可以设置为自动提交事务(关闭事务)
@Test public void testAdd() throws IOException { //接收参数 int status = 1; String companyName = "菠萝手机"; String brandName = "菠萝"; String description = "菠萝手机就是牛"; int ordered = 100; Brand brand = new Brand(); brand.setStatus(status); brand.setCompanyName(companyName); brand.setBrandName(brandName); brand.setDescription(description); brand.setOrdered(ordered); //1.获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取sqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(true); //3.获取Mapper接口的代理对象 BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4.执行方法 brandMapper.add(brand); //提交事务 //sqlSession.commit(); //5.释放资源 sqlSession.close(); }
- MyBatis事务:
主键返回:
在数据添加成功后,需要获取插入数据库的主键的值,如订单和订单项
- 添加订单
- 添加订单项,订单项中需要设置所属订单的id
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into tb_brand(brand_name, company_name, ordered, description, status)
values(#{brandName},#{companyName},#{ordered},#{description},#{status});
</insert>
这时调用方法的getId即可取出id值
alter table tablename auto_increment = 新数字;
可以重新设置自增id开始数字
1.5.3 修改
-
修改全部字段
-
编写接口方法
- 参数:所有数据
- 结果:void
int update(Brand brand);
-
编写SQL语句:SQL映射文件
<update id="update"> update tb_brand set brand_name = #{brandName}, company_name = #{companyName}, ordered = #{ordered}, description = #{description}, status = #{status} where id = #{id}; </update>
-
执行方法,测试
@Test public void testupdate() throws IOException { //接收参数 //接收参数 int status = 1; String companyName = "菠萝手机"; String brandName = "菠萝"; String description = "菠萝手机,手机中的战斗机"; int ordered = 200; int id = 4; Brand brand = new Brand(); brand.setStatus(status); brand.setCompanyName(companyName); brand.setBrandName(brandName); brand.setDescription(description); brand.setOrdered(ordered); brand.setId(id); //1.获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取sqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(true); //3.获取Mapper接口的代理对象 BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4.执行方法 int change = brandMapper.update(brand); System.out.println(change); //5.释放资源 sqlSession.close(); }
-
-
修改动态字段
<update id="update"> update tb_brand <set> <if test="brandName != null and brandName != ''"> brand_name = #{brandName}, </if> <if test="companyName != null and companyName != ''"> company_name = #{companyName}, </if> <if test="ordered != null"> ordered = #{ordered}, </if> <if test="description != null and description != ''"> description = #{description}, </if> <if test="status != null"> status = #{status} </if> </set> where id = #{id}; </update>
1.5.4 删除
-
删除一个
void deleteById(int id);
<delete id="deleteById"> delete from tb_brand where id = #{id}; </delete>
-
批量删除
-
编写接口方法:Mapper数组
- 参数:id数组
- 结果:void
void deleteByIds(@Param("ids")int[] ids);
-
编写SQL语句:SQL映射文件
<!--MyBatis会将数组封装为Map集合 默认情况下key = array,value = 数组 可以使用param注解来改变map集合的默认key的名称 --> <delete id="deleteByIds"> delete from tb_brand where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> ; </delete>
-
执行方法,测试
@Test public void testDeleteByIds() throws IOException { int ids[] = {1,3}; //1.获取SqlSessionFactory String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //2.获取sqlSession对象 SqlSession sqlSession = sqlSessionFactory.openSession(); //3.获取Mapper接口的代理对象 BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class); //4.执行方法 brandMapper.deleteByIds(ids); sqlSession.commit(); //5.释放资源 sqlSession.close(); }
-
1.5.5 参数传递
MyBatis接口方法中可以接收各种各样的参数,MyBatis底层对这些数据进行不同的封装处理方式
-
单个参数
-
POJO类型:可以直接使用,属性名要和参数占位符名称一致,否则要用<resultMap>的<result>修改
-
Map集合:可以直接使用,键名要和参数占位符名称一致 ,否则要用<resultMap>的<result>修改
-
Collection:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名
map.put("collection",collection集合);
map.put("arg0",collection集合);
-
List:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名
map.put("collection",List集合);
map.put("list",List集合);
map.put("arg0",List集合);
-
Array:最终封装为Map集合,可以使用@Param注解,替换Map集合中默认的arg0键名
map.put("array",数组);
map.put("arg0",数组);
-
其他类型:直接使用,如int id,而且这类参数名称叫什么都无所谓,#{}中写什么都能接收
<select id="selectById" resultType="user"> select * from tb_user where id = #{xxx}; </select>
-
-
多个参数
当有多个参数时,MyBatis自动会将他们封装为Map集合
User select(String username,String password);
底层会这样封装
map.put("arg0",参数值1);
map.put("param1",参数值1);
map.put("param2",参数值2);
map.put("arg1",参数值2);
如果不写@param注解可以使用arg或者param获取值
<select id="select" resultType="user"> select * from tb_user where username = #{arg0} and password = #{arg1} ; </select>
而@param注解就是替换默认的arg键名
User select(@param("username")String username,@param("password")String password);
map.put("username",参数值1);
map.put("param1",参数值1);
map.put("param2",参数值2);
map.put("password",参数值2);
<select id="select" resultType="user"> select * from tb_user where username = #{username} and password = #{password} ; </select>
MyBatis提供了ParamNamePesolver类来进行参数封装
总结:Collection、List、Array、多个参数要加@Param注解修改Map中的键名,并用修改后的名称来获取值,这样可读性更高
1.6 注解完成增删改查
使用注解开发会比配置文件开发更为方便
@Select("select * from tn_user where id = #{id}")
public User selectById(int id);
- 查询:@Select
- 添加:@Insert
- 修改:@Update
- 删除:@Delete
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
选择何种方式来配置映射,以及认为是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松的在基于注解和 XML 的语句映射方式间自由移植和切换。
注解开发一般完成比较简单的功能,而配置文件用来完成复杂功能
1.7 动态SQL
SQL语句会随着用户的输入和外部条件的变化而变化,我们称之为动态SQL
MyBatis对动态SQL有强大的支撑:
-
if:条件判断 test:逻辑表达式 <where>替换where关键字
<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand /*where 1 = 1*/ <where> <if test="status != null"> and status = #{status} </if> <if test="companyName != null and companyName != ''"> and company_name like #{companyName} </if> <if test="brandName != null and brandName != ''"> and brand_name like #{brandName} </if> </where> </select>
-
choose(when):选择,类似于Java中的switch语句
- choose标签相当于switch
- when标签相当于case
- otherwise标签相当于default
<select id="selectByCondition" resultMap="brandResultMap"> select * from tb_brand /*where 1 = 1*/ <where> <choose> <when test="status != null"> status = #{status} </when> <when test="companyName != null and companyName != ''"> company_name like #{companyName} </when> <when test="brandName != null and brandName != ''"> brand_name like #{companyName} </when> <!--<otherwise> 1 = 1 </otherwise>--> </choose> </where> </select>
2. Spring
2.1 初识Spring
Spring发展到今天已经形成了一种开发生态圈,Spring提供了若干个项目,每个项目完成特定的功能
- Spring家族
- Spring Framework:Spring所有的技术都依赖它执行,是底层设计型的框架
- Spring Boot:可以再Spring简化开发的基础上加速开发
- Spring Cloud:分布式开发
- Spring发展史
- 1997年出现了EJB思想
- 2004年Rod Johnson写了《J2EE Development without EJB》,Spring1.0问世,使用纯配置开发
- 2006年Spring2.0 引入了注解功能
- 2009年Spring3.0 已经演化成了可以不写配置的开发模式
- 2013年Spring4.0 Spring紧跟JDK版本升级,对个别API进行了些许调整
- 2017年,Spring到达了Spring5.0版本,已经全面支持JDK1.8
2.2 Spring Framework系统架构
Spring Framework是Spring生态圈最基础的项目,是其他项目的根基,其他所有的项目都在他的基础上运行
-
Spring Framework系统架构图
- ⭐Core Container:核心容器:对对象进行管理
- AOP:面向切面编程:在不修改源码的情况下,对源码进行增强
- Aspects:AOP思想实现
- Data Access:数据访问
- Data Integration:数据集成:支持Spring技术与其他技术整合使用(包容其他技术)
- ⭐Transactions:提供了一种开发起来效率非常高的事务控制方案
- Web:Web开发,在学习SpringMVC时进一步探讨
- Test:单元测试与集成测试
-
Spring Framework学习路线
- 核心容器Core Container
- 核心概念(IOC/DI)
- 容器基本操作
- 数据访问/数据集成Data Access/Integration
- 整合数据层技术MyBatis
- 面向切面编程AOP、Aspects
- 核心概念
- AOP基础操作
- AOP实用开发
- 事务Transactions
- 事务实用开发
- 核心容器Core Container
2.3 核心概念
如今JavaWeb代码书写现状:耦合度偏高
//业务层
public class BookServiceImpl implements BookService{
private BookDao bookDao = new BookDaoImpl();
public void save(){
bookDao.save();
}
}
//数据层
public class BookDaoImpl implements BookDao{
piblic void save(){
System.out.println("book dao save...");
}
}
当我们想要修改数据层代码
//数据层
public class BookDaoImpl2 implements BookDao{
piblic void save(){
System.out.println("book dao save...2");
}
}
这时业务层也需要修改,然后源码就要重新编译、测试...
解决方案:
使用对象时,在程序中不要主动使用new产生对象,转为由外部提供对象,即IoC
-
IoC(Inversion of control):控制反转
- 使用对象时,由主动new产生对象转换为由外部提供对象,此过程中对象的创建控制权由程序转移到外部,这种思想称为控制反转
-
Spring技术对IoC思想进行了实现
- Spring提供了一个容器,称为IoC容器,用来充当IoC思想中的“外部”
-
Spring框架中,由主动new产生对象转换为由Ioc容器提供对象
//业务层 public class BookServiceImpl implements BookService{ private BookDao bookDao; public void save(){ bookDao.save(); } }
//数据层 public class BookDaoImpl implements BookDao{ piblic void save(){ System.out.println("book dao save..."); } }
-
IoC容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在IoC容器中统称为Bean
-
Service要依赖dao运行,IoC将会绑定两个对象,这种思想称作DI(Dependency Injection)依赖注入
- 在容器中建立bean和bean之间的依赖关系的整个过程,称为依赖注入
-
IoC的目标:充分解耦
- 使用IoC容器管理bean(IoC)
- 在IoC容器内将有依赖关系的bean进行关系绑定(DI)
-
最终效果:
- 使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系
2.4 IoC入门案例(XML版)
案例基础:
其中两个实现类分别为
public class BookServiceImpl implements BookService {
private BookDao bookDao = new BookDaoImpl();
@Override
public void save() {
System.out.println("book service save...");
bookDao.save();
}
}
public class BookDaoImpl implements BookDao {
@Override
public void save() {
System.out.println("book dao save...");
}
}
-
思路分析
- IoC管什么?
- Bean(Service和Dao)
- 如何将被管理的对象告知(交给)IoC容器?
- 配置
- 被管理的对象交给IoC容器,如何获取到IoC容器?
- 接口
- IoC容器得到后,如何从容器中获取Bean?
- 接口方法
- 使用Spring导入哪些坐标?
- pom.xml
- IoC管什么?
-
代码实现
-
pom.xml引入Spring坐标
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency>
-
在resource中创建配置文件applicationContext.xml
-
配置Bean
- bean表示配置bean
- id属性表示给bean起名字
- class属性表示给bean定义类型
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/> <bean id="bookService" class="com.mark.service.impl.BookServiceImpl"/>
-
获取IOC容器
-
获取Bean
-
调用实现类的方法
public class App2 { public static void main(String[] args) { //获取IoC容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //获取Bean // BookDao bookDao = (BookDao) ctx.getBean("bookDao"); // bookDao.save(); BookService bookService = (BookService) ctx.getBean("bookService"); bookService.save(); } } //book service save... //book dao save...
-
2.5 DI入门案例
-
思路分析
- 基于IoC管理bean
- Service中使用new形式创建的Dao对象是否保留?
- 否
- Service中需要的Dao对象如何进入到Service中?
- 提供方法
- Service与Dao间的关系如何描述?
- 配置
-
案例实现
-
删除业务层中使用new的方式创建的dao对象
private BookDao bookDao;
-
提供对应的set方法
public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; }
-
配置Service与Dao的关系
- property标签表示配置当前bean的属性
- name属性表示配置实现类哪一个具体的属性
- ref属性表示参照哪一个bean
<bean id="bookService" class="com.mark.service.impl.BookServiceImpl"> <property name="bookDao" ref="bookDao"/> </bean>
-
2.6 bean配置
-
bean基础配置
类别 描述 名称 bean 类型 标签 所属 beans标签 功能 定义Spring核心容器管理的对象 格式 <beans> <bean/> <bean> </bean> </beans>
属性列表 id:Bean的id,使用容器可以通过id值获取对应的bean,在一个容器中id值唯一
class:bean的类型,即配置bean的全路径类名范例 <bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.mark.service.impl.BookServiceImpl"></bean>
-
bean别名配置
-
name标签 可以配置多个别名 用空格、逗号、或者分号隔开
<bean id="bookDao" name="dao" class="com.mark.dao.impl.BookDaoImpl"/> <bean id="bookService" name="service service2 bookEbi" class="com.mark.service.impl.BookServiceImpl"> <property name="bookDao" ref="bookDao"></property> </bean>
-
在获取bean时可以使用别名获取,在注入依赖关系时也可以使用别名,但推荐使用id注入ref
<bean id="bookService" name="service service2 bookEbi" class="com.mark.service.impl.BookServiceImpl"> <property name="bookDao" ref="dao"></property> </bean>
BookService bookService = (BookService) ctx.getBean("service"); bookService.save();
-
获取bean无论时通过id获取还是name获取,如果无法获取到,抛出异常:
NoSuchBeanDefinitionException: No bean named 'service4' available
这时要检查配置bean的名字和获取bean时的名字是否对应
-
-
bean作用范围配置
public static void main(String[] args) { ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao1 = (BookDao) ctx.getBean("dao"); BookDao bookDao2 = (BookDao) ctx.getBean("dao"); System.out.println(bookDao1); System.out.println(bookDao2); } //com.mark.dao.impl.BookDaoImpl@37918c79 //com.mark.dao.impl.BookDaoImpl@37918c79
可以发现Spring默认创建的bean是一个单例
当我们想要创建非单例时就需要在bean标签中添加一个属性:scope
-
scope即作用范围
- singleton:不写scope时的默认值
- prototype:此时Spring造出的对象并不是同一个对象,即非单例对象
<bean id="bookDao" name="dao" class="com.mark.dao.impl.BookDaoImpl" scope="prototype"/>
public static void main(String[] args) { ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao1 = (BookDao) ctx.getBean("dao"); BookDao bookDao2 = (BookDao) ctx.getBean("dao"); System.out.println(bookDao1); System.out.println(bookDao2); } //com.mark.dao.impl.BookDaoImpl@37918c79 //com.mark.dao.impl.BookDaoImpl@78e94dcf
-
为什么bean默认为单例?
- 当默认不是单例,每次使用对象都会创建一个新的对象,这对内存会造成影响。而使用单例并不会对业务实现造成影响。
- Spring就是管理那些可以复用的对象
-
哪些bean适合造单例呢?
- 表现层对象:如Servlet
- 业务层对象:如Service
- 数据层对象:如Dao
- 工具对象
-
不适合交给容器进行管理的bean:
- 封装实体的域对象:如domain/pojo
-
2.7 bean的实例化
-
bean是如何创建的
-
bean本质上就是对象,创建bean使用构造方法完成
private BookDaoImpl() { System.out.println("book dao constructor is running..."); }
在创建bookDao时会调用此构造方法,并且无论构造方法是私有还是公共的都可以调用成功
但是,当构造方法有参数时,就无法调用成功。抛出异常从最后一个异常往上看:
NoSuchMethodException: com.mark.dao.impl.BookDaoImpl.<init>()
:没有无参构造方法Failed to instantiate
:错误的实例化BookDaoImpl]: No default constructor found
:该类没有发现默认的构造方法BeanCreationException
:创建Bean异常Error creating bean with name 'bookDao' defined in class path resource [applicationContext.xml]: Instantiation of bean failed
:错误地创建了一个在applicationContext.xml中定义的一个名叫bookDao的bean,实例化失败
-
-
实例化bean的三种方式
-
⭐使用构造方法实例化bean(常用)
-
提供可访问的构造方法(写不写都可以)
public class BookDaoImpl implements BookDao { /* private BookDaoImpl() { System.out.println("book dao constructor is running..."); } */ @Override public void save() { System.out.println("book dao save ..."); } }
-
配置
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
-
无参构造方法如果不存在,将排除异常BeanCreationException
-
-
静态工厂实例化bean(一般用于早期遗留的系统,了解即可)
-
静态工厂
public static OrderDao getOrderDao(){ System.out.println("factory setup..."); return new OrderDaoImpl(); }
-
配置
<!--class配置工程类名 添加factory-method,配置工厂里造对象的方法名--> <bean id="orderDao" class="com.mark.factory.OrderDaoFactory" factory-method="getOrderDao"/>
-
-
实例工厂初始化bean(了解)
-
实例工厂
public class UserDaoFactory { public UserDao getUserDao(){ return new UserDaoImpl(); } }
-
配置
<bean id="userFactory" class="com.mark.factory.UserDaoFactory"/> <bean id="userDao" factory-bean="userFactory" factory-method="getUserDao"/>
-
-
⭐使用FactoryBean实例化bean(第三种初始化的变种、实用)
-
创建FactoryBean
public class UserDaoFactoryBean implements FactoryBean<UserDao> { /** * 代替原始实例工厂中创建对象的方法 * @return new 对象 * @throws Exception */ @Override public UserDao getObject() throws Exception { return new UserDaoImpl(); } /** * 对象的类型 * @return 对象.class */ @Override public Class<?> getObjectType() { return UserDao.class; } /** * 设置非单例 * @return false */ @Override public boolean isSingleton() { return false; } }
-
配置
<bean id="userDao" class="com.mark.factory.UserDaoFactoryBean"/>
-
-
2.8 bean的生命周期控制
-
生命周期:从创建到消亡的完整过程
-
bean生命周期:bean从创建到销毁的整体过程
- 初始化容器
- 创建对象(内存分配)
- 执行构造方法
- 执行属性注入(set操作)
- 执行bean初始化方法
- 使用bean
- 执行业务操作
- 关系/销毁容器
- 执行bean销毁方法
- 初始化容器
-
bean生命周期控制:在bean创建后到销毁前做一些事情
-
如何配置bean生命周期的方法?方式一:配置方式
-
在实现类创建两个方法
public class BookDaoImpl implements BookDao { @Override public void save() { System.out.println("book dao save ..."); } /** * 表示bean初始化对应的操作 */ public void init(){ System.out.println("init..."); } /** * 表示bean销毁前对应的操作 */ public void destory(){ System.out.println("destory..."); } }
-
配置两个方法
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>
-
-
在程序运行结束后就会关闭虚拟机,导致销毁的方法并没有机会执行
-
在虚拟机退出之前,关闭IoC容器(暴力方式)
public class AppForLifeCycle { public static void main( String[] args ) { // ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); bookDao.save(); ctx.close(); } }
-
设置关闭钩子
public class AppForLifeCycle { public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //注册关闭钩子(任何时刻都可以) ctx.registerShutdownHook(); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); bookDao.save(); //ctx.close(); } }
-
-
使用接口配置bean生命周期的方法:不需要配置初始化和销毁方法
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean { private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } @Override public void save() { System.out.println("book service save ..."); bookDao.save(); } /** * bean销毁前执行 * @throws Exception */ @Override public void destroy() throws Exception { System.out.println("Service destory..."); } /** * 在属性设置完之后初始化bean * @throws Exception */ @Override public void afterPropertiesSet() throws Exception { System.out.println("Service init..."); } }
2.9 依赖注入方式
-
思考:
- 向一个类中传递数据的方式有几种?
- 普通方法(set方法)
- 构造方法
- 依赖注入描述了在容器中建立bean与bean之间依赖关系的过程,如果bean运行需要的是数字或字符串呢?
- 引用类型
- 简单类型(基本数据类型与String)
- 向一个类中传递数据的方式有几种?
-
依赖注入方式:
-
setter注入
-
简单类型
-
在bean中定义简单类型属性并提供可访问的set方法
public class BookDaoImpl implements BookDao { private int connectionNum; private String dataBaseName; public void setConnectionNum(int connectionNum) { this.connectionNum = connectionNum; } public void setDataBaseName(String dataBaseName) { this.dataBaseName = dataBaseName; } @Override public void save() { System.out.println("book dao save ..."+connectionNum+","+dataBaseName); } }
-
配置中使用property标签value属性注入简单类型数据
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"> <property name="connectionNum" value="500"></property> <property name="dataBaseName" value="mysql"></property> </bean>
-
-
引用类型
-
在bean中定义引用类型属性并提供可访问的set方法
public class BookServiceImpl implements BookService { private BookDao bookDao; @Override public void save() { System.out.println("book service save..."); bookDao.save(); } public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } }
-
配置中使用property标签的ref属性注入引用类型对象
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/> <bean id="bookService" class="com.mark.service.impl.BookServiceImpl"> <property name="bookDao" ref="bookDao"></property> </bean>
-
-
-
构造器注入
-
简单类型
-
在bean中定义简单类型属性并提供可访问的构造方法
public class BookDaoImpl implements BookDao { private int connectionNum; private String dataBaseName; public BookDaoImpl(int connectionNum, String dataBaseName) { this.connectionNum = connectionNum; this.dataBaseName = dataBaseName; } @Override public void save() { System.out.println("book dao save ..."); } }
-
配置中使用constructor-arg标签value属性注入简单类型数据
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"> <constructor-arg name="connectionNum" value="500"></constructor-arg> <constructor-arg name="dataBaseName" value="mysql"></constructor-arg> </bean>
-
解决形参会变名的问题:与形参名不耦合
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"> <constructor-arg type="int" value="500"></constructor-arg> <constructor-arg type="java.lang.String" value="mysql"></constructor-arg> </bean>
-
根据参数位置匹配注入简单类型数据:解决参数的类型重复问题
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"> <constructor-arg index="0" value="500"></constructor-arg> <constructor-arg index="1" value="mysql"></constructor-arg> </bean>
-
-
-
引用类型
-
在bean中定义引用类型属性并提供可访问的构造方法
public class BookServiceImpl implements BookService{ private BookDao bookDao; private UserDao userDao; public BookServiceImpl(BookDao bookDao, UserDao userDao) { this.bookDao = bookDao; this.userDao = userDao; } @Override public void save() { System.out.println("book service save ..."); bookDao.save(); userDao.save(); } }
-
配置中使用constructor-arg标签的ref属性注入引用类型对象
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"></bean> <bean id="userDao" class="com.mark.dao.impl.UserDaoImpl"></bean> <bean id="bookService" class="com.mark.service.impl.BookServiceImpl"> <!--这里的name是构造方法的形参名--> <constructor-arg name="bookDao" ref="bookDao"></constructor-arg> <constructor-arg name="userDao" ref="userDao"></constructor-arg> </bean>
-
-
-
-
两种依赖注入方式的选择
- 强制依赖使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
- 可选依赖使用setter注入进行,灵活性强
- Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
- 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
- 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
- 自己开发的模块推荐使用setter注入
2.10 依赖自动装配
-
IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配
-
自动装配方式
-
按类型(常用)
<bean class="com.mark.dao.impl.BookDaoImpl"/> <bean id="bookService" class="com.mark.service.impl.BookServiceImpl" autowire="byType"/>
前提是需要提供set方法
-
按名称
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/> <!--使用bean中set方法的属性名和配置中bean的id名进行匹配--> <bean id="bookService" class="com.mark.service.impl.BookServiceImpl" autowire="byName"/>
-
按构造方法
-
不启用自动装配
-
-
依赖自动装配特征
- 自动装配用于引用类型依赖注入,不能对简单类型进行操作
- 使用按类型装配时(byType)必须保证容器中相同类型的bean唯一,推荐使用
- 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
- 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效
2.11 集合注入
-
数组
<property name="array"> <array> <value>100</value> <value>120</value> <value>140</value> </array> </property>
-
List
<property name="list"> <list> <value>王昭君</value> <value>嫦娥</value> <value>李元芳</value> </list> </property>
-
Set
<property name="set"> <set> <value>王昭君</value> <value>王昭君</value> <value>嫦娥</value> <value>嫦娥</value> </set> </property>
-
Map
<property name="map"> <map> <entry key="country" value="China"/> <entry key="province" value="Shaanxi"/> <entry key="city" value="Xian"/> </map> </property>
-
Properties
<property name="properties"> <props> <prop key="country">China</prop> <prop key="province">Shaanxi</prop> <prop key="city">Baoji</prop> </props> </property>
2.12 案例:数据源对象管理
-
第三方资源配置管理
-
导入druid坐标
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency>
-
配置数据源对象作为spring管理的bean
<bean class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql:////spring_db"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean>
-
2.13 加载properties文件
-
开启context命名空间
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> </beans>
-
使用context空间加载properties文件
<context:property-placeholder location="jdbc.properties"/>
-
使用属性占位符${}读取文件中的属性
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
-
注意:
-
当配置文件的属性名与系统的环境变量值相同时,获取到的是系统变量值
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"> <property name="name" value="${username}"/> </bean>
上述配置读出的username一定是电脑用户名
因此想要避免读取系统变量值,可以在加载文件时添加system-properties-mode属性
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>
-
当要加载多个配置文件时可以使用逗号隔开,也可以使用*.properties加载全部properties文件
<context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
<context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
标准格式:
<!--添加classpath只读取当前模块的文件--> <context:property-placeholder location="classpath*.properties" system-properties-mode="NEVER"/>
从类路径或jar包中搜索并加载properties文件:
<context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>
-
2.14 容器
-
创建容器
-
加载类路径下的配置文件
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
-
从文件系统下加载配置文件
ApplicationContext ctx2 = new FileSystemXmlApplicationContext("D:\\Code\\IJavaMySpring\\Spring_10_Container\\src\\main\\resources\\applicationContext.xml");
-
-
获取bean
-
方式一:使用bean名称获取
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
-
方式二:使用bean名称获取并指定类型
BookDao bookDao = ctx.getBean("bookDao",BookDao.class);
-
方式三:使用bean类型获取
BookDao bookDao = ctx.getBean(BookDao.class);
对应的Bean只能有一个
-
-
容器类层次结构
-
BeanFactory
public class AppForBeanFactory { public static void main(String[] args) { Resource resources = new ClassPathResource("applicationContext.xml"); BeanFactory bf = new XmlBeanFactory(resources); BookDao bookDao = bf.getBean(BookDao.class); bookDao.save(); } }
BeanFactory完毕后,所有的bean均为延迟加载,而ApplicationContext是全部立即加载
2.15 注解开发
-
注解开发定义bean
-
使用
@Component
定义bean@Component("bookDao") public class BookDaoImpl implements BookDao { @Override public void save() { System.out.println("book dao save ..."); } }
Component是组件的意思,该注解可以代替配置
<bean id="bookDao" class="com.mark.dao.impl.BookDaoImpl"/>
-
核心配置文件中通过组件扫描加载bean
<!--扫描com.mark.dao.impl下的组件--> <context:component-scan base-package="com.mark.dao.impl"/>
-
-
Spring提供了
@Component
注解的三个衍生注解-
@Controller
:用于表现层bean定义 -
@Service
:用于业务层bean定义@Service public class BookServiceImpl implements BookService { }
-
@Repository
:用于数据层bean定义@Repository("bookDao") //Repository是仓库的意思,这里译为数据仓库 public class BookDaoImpl implements BookDao { }
public class App { public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); System.out.println(bookDao); BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService); } } //com.mark.dao.impl.BookDaoImpl@459e9125 //com.mark.service.impl.BookServiceImpl@692f203f
-
-
纯注解开发
Spring3.0升级了纯注解开发模式,使用Java类代替了配置文件,开启了Spring快速开发通道
-
在config包下创建类SpringConfig,添加注解
@Configuration
,用于设定当前类为配置类,这里的@Configuration就代替了配置文件的<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd "> </beans>
-
添加注解
@ComponentScan("com.mark")
,用于设定扫描路径,此注解只能添加一次,多个数据需要用数组格式,这个注解就代替了<context:component-scan base-package="com.mark"/>
@Configuration //@ComponentScan("com.mark") @ComponentScan({"com.mark.dao","com.mark.service"}) public class SpringConfig { }
-
创建容器时使用注解配置ctx:AnnotationConfigApplicationContext
public class AppForAnnotation { public static void main(String[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); BookDao bookDao = (BookDao) ctx.getBean("bookDao"); System.out.println(bookDao); BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService); } }
-
2.16 bean管理
-
bean作用范围
- 注解@Scope
- singleton:单例模式
- prototype:非单例模式
- 注解@Scope
-
bean生命周期
@PostConstruct
注解:初始化方法@PreDestroy
注解:摧毁方法
两个注解添加前要增加jsr-api坐标
@Repository @Scope("singleton") public class BookDaoImpl implements BookDao { @Override public void save() { System.out.println("book dao save ..."); } @PostConstruct public void init() { System.out.println("book dao init ..."); } @PreDestroy public void destory() { System.out.println("book dao destory ..."); } }
2.17 依赖注入
-
自动装配
-
在bean中添加
@Autowired
注解@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; /* public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } */ @Override public void save() { System.out.println("book service save ..."); bookDao.save(); } }
@Autowired
注解根据类型装配,使用暴力反射注入,即使没有set方法依旧可以成功 -
当有两个相同类型的类时,就不能用类型装配,需要使用名字注入,使用注解
@Qualifier
,而且@Autowired不可以去掉,@Qualifier无法单独使用@Autowired @Qualifier("bookDao2") private BookDao bookDao; public void setBookDao(BookDao bookDao) { this.bookDao = bookDao; } @Override public void save() { System.out.println("book service save ..."); bookDao.save(); }
-
使用@Value实现简单类型注入
@Repository("bookDao") public class BookDaoImpl implements BookDao { @Value("Mark") private String name; @Override public void save() { System.out.println("book dao save ..."+name); } }
-
将配置文件的数据注入简单类型
-
在配置类添加注解
@PropertySource
@Configuration @ComponentScan("com.mark") @PropertySource("jdbc.properties") public class SpringConfig { }
-
获取值直接使用占位符${},里面写属性名即可
@Repository("bookDao") public class BookDaoImpl implements BookDao { @Value("${name}") private String name; @Override public void save() { System.out.println("book dao save ..."+name); } }
如果配置文件有多个,可以使用数组形式:
@PropertySource("{jdbc.properties,jdbc.propertiesjdbc.properties}")
这里不支持使用通配符:
*.properties
-
-
2.18 第三方bean管理
-
使用
@Bean
配置第三方bean@Configuration public class SpringConfig { //1.定义一个方法,获得要管理的对象 //2.添加@Bean,表示当前方法的返回值是一个bean @Bean public DataSource dataSource(){ DruidDataSource ds =new DruidDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl("jdbc:mysql:///spring_db"); ds.setUsername("root"); ds.setPassword("123"); return ds; } }
-
不建议将该@Bean写到SpringConfig中,应当使用第三方的配置类管理第三方bean
-
方式一:导入式
public class JDBCConfig { @Bean public DataSource dataSource(){ DruidDataSource ds =new DruidDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl("jdbc:mysql:///spring_db"); ds.setUsername("root"); ds.setPassword("123"); return ds; } }
-
使用
@Import
注解手动加入配置到核心配置,此注解只添加一次,多个配置类要用数组格式@Configuration @Import(JDBCConfig.class) public class SpringConfig { }
-
-
方式二:扫描式(不推荐)
-
@Configuration public class JDBCConfig { @Bean public DataSource dataSource(){ DruidDataSource ds =new DruidDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setUrl("jdbc:mysql:///spring_db"); ds.setUsername("root"); ds.setPassword("123"); return ds; } }
-
使用
@ComponentScan
注解扫描配置类所在的包,加载对应的配置类信息@Configuration @ComponentScan("com.mark.config") public class SpringConfig { }
-
-
-
为第三方bean注入资源
-
简单类型依赖注入
public class JDBCConfig { @Value("com.mysql.cj.jdbc.Driver") private String driver; @Value("jdbc:mysql:///spring_db") private String url; @Value("root") private String username; @Value("123") private String password; @Bean public DataSource dataSource(){ DruidDataSource ds =new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
-
引用类型注入
public class JDBCConfig { @Bean public DataSource dataSource(BookDao bookDao){ System.out.println(bookDao); DruidDataSource ds =new DruidDataSource(); ds.setDriverClassName(driver); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
- 引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象
-
2.19 XML配置和注解配置对比
2.20 Spring整合MyBatis
-
MyBatis程序核心对象分析
-
步骤:
-
导入所需坐标
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <!--Spring操作JDBC对应的坐标--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!--MyBatis与Spring进行整合的坐标--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.0</version> </dependency> </dependencies>
-
添加MyBatisConfig配置类
public class MyBatisConfig { @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){ SqlSessionFactoryBean ssfb =new SqlSessionFactoryBean(); ssfb.setTypeAliasesPackage("com.mark.domain"); ssfb.setDataSource(dataSource); return ssfb; } @Bean public MapperScannerConfigurer mapperScannerConfigurer(){ MapperScannerConfigurer msc =new MapperScannerConfigurer(); msc.setBasePackage("com.mark.dao"); return msc; } }
第一个bean代替了
<typeAliases> <package name="com.itheima.domain"/> </typeAliases> <environments default="mysql"> <environment id="mysql"> <transactionManager type="JDBC"></transactionManager> <dataSource type="POOLED"> <property name="driver" 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> </dataSource> </environment> </environments>
第二个bean代替了
<mappers> <package name="com.itheima.dao"></package> </mappers>
-
2.21 Spring整合Junit
-
添加依赖
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.10.RELEASE</version> </dependency>
-
给测试类添加注解
//使用Spring整合Junit专用的类加载器 @RunWith(SpringJUnit4ClassRunner.class) //指定Spring的配置 @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testFindById(){ System.out.println(accountService.findById(2)); } }
2.22 AOP简介
-
AOP核心概念
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
OOP(Object Oriented Programming)面向对象编程
-
AOP作用
在不惊动原始设计的基础上为其进行功能增强
-
Spring理念:无侵入式/无入侵式
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
- 在SpringAOP中,理解为方法的执行
- 切入点(Pointcut):匹配连接点的式子
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 一个具体方法:com.mark.dao包下的BookDao:接口中的无形参无返回值的save方法
- 匹配多个方法:所有的save方法、所有的get开头的方法、所有以Dao结尾的接口中的任意方法、所有带有一个参数的方法
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 通知(Advice):在切入点处执行的操作,也就是共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
- 通知类:定义通知的类
- 切面(Aspect):描述通知与切入点的对应关系:执行位置与共性功能的关系
- 连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
2.23 AOP入门案例
要求:在接口执行前输出当前系统时间
开发模式:XML or 注解
步骤:
-
导入坐标
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <!--aspect用于实现AOP切面--> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies>
-
制作连接点方法(原始操作,Dao接口与实现类)
@Repository public class BookDaoImpl implements BookDao { @Override public void save() { System.out.println(System.currentTimeMillis()); System.out.println("book dao save ..."); } @Override public void update(){ System.out.println("book dao update ..."); } }
-
制作共性功能(通知类与通知)
-
定义切入点
- 切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
-
绑定切入点与通知关系(切面)
//通知类必须配置成Spring管理的bean @Component //1.告诉Spring,MyAdvice是用来做AOP的 @Aspect public class MyAdvice { //3.定义切入点 @Pointcut("execution(void com.mark.dao.BookDao.update())") private void pt(){} //4.绑定切入点和通知 @Before("pt()") //2.定义通知 public void method(){ System.out.println(System.currentTimeMillis()); } }
-
Spring核心配置中标明使用注解开发AOP
@Configuration @ComponentScan("com.mark") @EnableAspectJAutoProxy public class SpringConfig { }
2.24 AOP工作流程
-
Spring容器启动
-
读取所有切面配置中的切入点,即已经在@Before中表明的切入点
-
初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
- 匹配失败,创建对象
- 匹配成功,创建原始对象(目标对象)的代理对象
-
获取bean执行方法
- 获取bean,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
-
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
-
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
2.25 AOP切入点表达式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
-
切入点表达式描述方式:
-
描述方式一:执行com.mark.dao包下的BookDao接口中的无参数update方法
execution(void com.mark.dao.BookDao.update())
-
描述方式二:执行com.mark.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.mark.dao.BookDaoImpl.update())
-
-
切入点表达式标准格式:
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
- 动作关键字:描述切入点的行为动作,例如execution表示执行到指定的切入点
- 访问修饰符:public,private等,可省略
- 返回值
- 包名
- 类/接口名
- 方法名
- 参数
- 异常名:方法定义中抛出指定异常,可以省略
-
通配符
可以使用通配符描述切入点,快速描述
-
*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配夫出现@Pointcut("execution( * com.mark.*.BookDao.find*(*))")
表示匹配com.mark包下任意包中的BookDao类或接口中所有find开头的带有一个参数的方法
-
..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写@Pointcut("execution(public User com..BookDao.findById(..))")
表示匹配com包下的任意包中的BookDao类或接口中所有名称为findById的方法
-
+
:装用于匹配子类类型@Pointcut("execution(* *..*Service+.*(..))")
表示匹配任意返回值任意包下的以Service结尾的类或者接口的子类的任意参数的任意方法
-
-
书写技巧
-
所有代码按照标准规范开发,否则以下技巧全部失效
-
描述切入点通常描述接口,而不描述实现类
-
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
-
返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
-
包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
-
接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
-
方法名书写以动词进行精准匹配,名词采用*匹配,例如getByld书写成getBy*,selectAll书写成selectAll
-
参数规则较为复杂,根据业务方法灵活调整
-
通常不使用异常作为匹配规则
@Pointcut("execution(* com.mark.*.*Service.find*(..))")
-
2.26 AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
AOP通知共分为5种类型
-
前置通知
@Before("pt()") public void before() { System.out.println("before advice ..."); }
-
后置通知
@After("pt()") public void before() { System.out.println("before advice ..."); }
-
环绕通知(重点、常用)
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { System.out.println("around before advice ..."); //表示对原始操作的调用 Object ret = pjp.proceed(); System.out.println("around after advice ..."); return ret; }
@Around注意事项:
- 环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
- 通知中如果未使用ProceedingJoinPoint对原始方法进行调用将跳过原始方法的执行
- 对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Object类型
- 原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
- 由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
-
返回后通知(了解)
@AfterReturning("pt2()") public void afterReturning() { System.out.println("afterReturning advice ..."); }
只有方法没有抛出异常正常结束时才会运行
-
抛出异常后通知(了解)
@AfterThrowing public void afterThrowing() { System.out.println("afterThrowing advice ..."); }
只有方法抛出异常后才会运行
2.27 案例:测控业务层接口万次执行效率
-
需求:任意业务层接口执行均可显示其执行效率(执行时长)
-
分析:
- 业务功能:业务层接口执行前后分别记录时间
- 通知类型选择前后均可增强的类型:环绕通知
-
实现:
@Component @Aspect public class AppAdvice { //匹配业务层所有方法 @Pointcut("execution(* com.mark.service.*Service.*(..))") private void servicePt() { } @Around("AppAdvice.servicePt()") public void runSpeed(ProceedingJoinPoint pjp) throws Throwable { //代表一次执行的签名信息 Signature signature = pjp.getSignature(); //类型 //System.out.println(signature.getDeclaringType()); //类型名 //System.out.println(signature.getDeclaringTypeName()); //方法名 //System.out.println(signature.getName()); String className = signature.getDeclaringTypeName(); String methodName = signature.getName(); long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { pjp.proceed(); } long end = System.currentTimeMillis(); System.out.println("万次执行" + className + "." + methodName + "所用时间:" + (end - start) + "ms"); } } ```
2.28 AOP通知获取数据
-
获取切入点方法的参数
-
JoinPoint
:适用于前置、后置、返回后、抛出异常后通知@Before("pt()") public void before(JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("before advice ..." ); } @After("pt()") public void after(JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("after advice ..."); }
-
ProceedJointPoint
:适用于环绕通知@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); args[0]=666; //System.out.println(Arrays.toString(args)); Object ret = pjp.proceed(args); return ret; }
-
-
获取切入点方法的返回值
-
返回后通知
//ret作为返回值 returning要和形参对应 @AfterReturning(value = "pt()",returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..."+ret); }
如果要获取参数添加JoinPoint,
JoinPoint jp
必须在前@AfterReturning(value = "pt()",returning = "ret") public void afterReturning(JoinPoint jp, Object ret) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); System.out.println("afterReturning advice ..."+ret); }
-
环绕通知
@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object ret = pjp.proceed(); return ret; }
-
-
获取切入点方法的异常
-
抛出异常后通知
@AfterThrowing(value = "pt()",throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..."+t); }
-
环绕通知
@Around("pt()") public Object around(ProceedingJoinPoint pjp){ Object ret = null; try { ret = pjp.proceed(); } catch (Throwable e) { e.printStackTrace(); } return ret; }
-
2.28 百度网盘密码数据兼容处理
-
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理
-
分析:
- 在业务方法执行之前对所有的输入参数进行格式处理:trim()
-
实现:
@Component @Aspect public class DataAdvice { @Pointcut("execution(boolean com.mark.service.ResourcesService.openURL(*,*))") private void servicePt() { } @Around("DataAdvice.servicePt()") public Object trimStr(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); for (int i = 0; i < args.length; i++) { //判断参数是不是字符串 if (args[i].getClass().equals(String.class)) { args[i] = args[i].toString().trim(); } } Object ret = pjp.proceed(args); return ret; } }
2.29 Spring事务简介
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
-
案例:模拟银行账户间转账业务
-
需求:实现任意两个账户间转账操作
-
需求微缩:A账户减钱、B账户加钱
-
分析:
- 数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
- 业务层提供转账操作(transfer),调用减钱与加钱的操作
- 提供两个账号和操作金额执行转账操作
- 基于Spring整合MyBatis环境搭建上述操作
-
结果分析:
- 程序正常执行时,账户金额A减B加
- 程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
-
实现:
-
给要添加事务的实现类的接口添加注解
@Transactional
@Transactional public void transfer(String out,String in ,Double money) ;
Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
-
在JDBC配置类中添加事务管理器配置
PlatformTransactionManager
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource){ DataSourceTransactionManager ptm = new DataSourceTransactionManager(); ptm.setDataSource(dataSource); return ptm; }
-
在Spring配置类中打开使用注解式事务驱动
@EnableTransactionManagement
@Configuration @ComponentScan("com.mark") @PropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MybatisConfig.class}) @EnableTransactionManagement public class SpringConfig { }
-
-
2.30 Spring事务角色
- 事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
2.31 事务相关配置
-
事务配置
//开启只读、永不超时 @Transactional(readOnly = true,timeout = -1) public void transfer(String out,String in ,Double money) ;
除了运行时异常和Error系异常会回滚,除此之外都不回滚,即便出现异常也不回滚,如
IOException()
为了保证正确回滚,需要添加属性
rollbackFor
@Transactional(rollbackFor = {IOException.class}) public void transfer(String out,String in ,Double money) throws IOException;
-
案例:转账业务追加日志
-
需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
-
需求微缩:A账户减钱、B账户加钱,数据库记录日志
-
分析:
-
基于转账操作案例添加日志模块,实现数据库中记录日志
-
业务层转账操作(transfer),调用减钱、加钱与记录日志功能
public interface LogDao { @Insert("insert into tnl_log (info,createDate) values(#{info},now())") void log(String info); }
public interface LogService { @Transactional void log(String out, String in, Double money); }
@Service public class LogServiceImpl implements LogService { @Autowired private LogDao logDao; @Override public void log(String out,String in,Double money ) { logDao.log("转账操作由"+out+"到"+in+",金额:"+money); } }
-
-
实现效果预期:
-
无论转账操作是否成功,均进行转账操作的日志留痕
@Override public void transfer(String out,String in ,Double money) throws IOException{ try { accountDao.outMoney(out,money); int i = 1/0; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); } }
-
-
存在的问题
- 日志的记录与转账操作隶属于同一个事务,同成功同失败
-
实现效果预期改进:
- 无论转账操作是否成功,日志必须保留
-
-
事务传播行为
事务协调员对事务管理员所携带事务的处理态度
-
在业务层接口上添加Spring事务,设置事务传播行为REQUIRES_NEW(需要新事务)
public interface LogService { @Transactional(propagation = Propagation.REQUIRES_NEW) void log(String out, String in, Double money); }
几种事务传播行为:
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)