MyBatis
约 8274 字大约 28 分钟
2026-05-26
MyBatis
MyBatis是一款优秀的持久层框架,用于简化JDBC的开发,
-- 需求: 使用MyBatis查询所有用户数据
-- 步骤一: 准备工作(创建springboot工程、数据库表user、实体类User)
-- 步骤二: 引I入Mybatis的相关依赖,配置Mybatis(数据库的配置信息)
-- 步骤三: 编写SQL语句(注解/XML)-- 步骤一: 创建user数据表 并向里面添加数据
create table user
(
id int unsigned auto_increment comment '用户id 唯一标识'
primary key,
name varchar(100) not null comment '姓名',
age tinyint unsigned null comment '年龄',
gender tinyint unsigned null comment '性别 1男 2女',
phone varchar(11) null comment '电话',
constraint name
unique (name)
)
comment '用户表';
-- 向表中添加数据
白眉鹰王 55
金毛狮王 45
青翼蝠王 38
紫衫龙王 42
光明左使 37
光明右使 48// 步骤二: Javabean类 即实体类User
package com.zkwy.mybatis.pojo;
public class User {
private Integer id;
private String name;
private Short age;
private Short gender;
private String phone;
......
}# 步骤三: 数据库的配置信息
# src/main/resoures/application.properties
#驱动类名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#数据库连接的url
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
#连接数据库的用户名
spring.datasource.username=root
#连接数据库的密码
spring.datasource.password=123456// 步骤四: 定义一个接口:
package com.zkwy.mybatis.mapper;
import com.zkwy.mybatis.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
//
@Mapper
public interface UserMapper {
// 查询所有的用户信息
@Select("select * from user")
public List<User> list();
// "select * from user" 配置提示
// 选择SQL语句 右击选择(显示上下文操作)
// 在选择(语言注入设置)
// 在选择(id 选择mySQL)
}// 步骤五: 单元测试一下数据 test/...
package com.zkwy.mybatis;
import com.zkwy.mybatis.mapper.UserMapper;
import com.zkwy.mybatis.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
@SpringBootTest
class MybatisApplicationTests {
@Autowired
private UserMapper userMapper;
@Test
public void testListUser(){
List<User> userList = userMapper.list();
for (User user : userList) {
System.out.println("获取的数据 " + user);
}
}
}配置SQL提示
// 下面的SQL语句 配置提示信息
@Select("select * from user")
// 步骤一: 选择 "select * from user" 这条SQL语句右击
// 步骤二: 右击选择 第二个(显示上下文操作)
// 步骤三: 再选择 倒数第一个(语言注入设置)
// 步骤四: 选择语言ID 下拉选择MySQLJDBC介绍
JDBC:(Java Data Base Connectivity),就是使用Java语言操作关系型数据库的一套APl 驱动jar包
数据库连接池
- 数据库连接池是个容器,负责分配、管理数据库连接(Connection)
- 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
- 释放空闲时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
**优点: **
- 资源重用
- 提升系统响应速度
- 避免数据库连接遗漏
标准接口: DataSource
// 官方(sun)提供的数据库连接池接口,由第三方组织实现此接口。
// 功能:获取连接 Connection getConnection() throws SQLException;常见产品
- C3P0
- DBCP
- Druid
- Hikari ( springboot默认 )
Druid( 德鲁伊 )
- Druid 连接池是阿里巴巴开源的数据库连接池项目
- 功能强大,性能优秀,是Java语言最好的数据库连接池之一
切换连接池
<!-- 步骤一: 下载依赖 pom.xml
https://mvnrepository.com/artifact/com.alibaba/druid
-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency># 步骤二:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis
spring.datasource.username=root
spring.datasource.password=123456lombok
Lombok是一个实用的Java类库,能通过注解的形式自动生成构造器、getter/setter、equals、hashcode、toString等方法,并可以自动化生成目志变量,简化java开发、提高效率。
| 注解 | 作用 |
|---|---|
| @Getter/@Setter | 为所有的属性提供get/set方法 |
| @ToString | 会给类自动生成易阅读的toString方法 |
| @EqualsAndHashCode | 根据类所拥有的非静态字段自动重写equals方法和hashcode方法 |
| @Data | 提供了更综合的生成代码功能(@Getter+@Setter+@ToString+@EqualsAndHashCode) |
| @NoArgsConstructor | 为实体类生成无参的构造器方法 |
| @AllArgsConstructor | 为实体类生成除了static修饰的字段之外带有各参数的构造器方法。 |
// 注意事项:
lombok会在编译时,自动生成对应的java代码。我们使用lombok时,还需要安装一个lombok的插件(idea自带)。MyBatis 操作
- 准备数据库表emp
- 创建一个新的springboot工程,选择引入对应的起步依赖(mybatis、mysql驱动、lombok)
- application.properties中引l入数据库连接信息
- 创建对应的实体类Emp(实体类属性采用驼峰命名)
- 准备Mapper接口EmpMapper
-- 数据库相关的数据信息
-- @Mapper // 程序在运行时 创建接口的代理对象 并且将该对象放在IOC容器中package com.fayuan.demo01.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Emp {
private Integer id; //ID
private String username; //用户名
private String password;//密码
private String name; //姓名
private Short gender;//性别,1男,2 女
private String image;//图像url
private Short job;//职位,说明:1班主任,2讲师,3学工
private Integer deptId; // 部门ID
private LocalDate entryDate;//入职日期
private LocalDateTime createTime;//创建时间
private LocalDateTime updateTime;//修改时间
}package com.fayuan.demo01.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 程序在运行时 创建接口的代理对象 并且将该对象放在IOC容器中
public interface EmpMapper {
}删除
// 根据主键删除
// SQL语句
delete from tb_emp where id = 30;
// 接口方法
@Delete("select * from tb_emp where emp_id = #{id}")
public void delete(Integer id);
// 注意事项:
// 如果mapper接口方法形参只有一个普通类型的参数,#(..}里面的属性名可以随便写,如:#{id)、#(value)日志输出
可以在application.properties中,打开mybatis的日志,并指定输出到控制台。
#指定mybatis输出日志的位置,输出控制台
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl预编译SQL
- 性能更高
- 更加安全( 防止SQL注入 )
SQL注入是通过操作输入的数据来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法
插入
// 接口方法
@Options(keyProperty = "empId" , useGeneratedKeys = true) // 会自动将生成的主键值, 赋值给Emp的empId
@Insert("insert into tb_emp(user_name, name, gender, image, job, entry_data, dept_id, create_time, update_time) value (#{userName}, #{name}, #{gender}, #{image}, #{job} , #{entryData} , #{deptId} ,#{createTime} ,#{updateTime})")
public void insert(Emp emp); // Emp 实体类
// 测试 添加数据
@Test
public void insert(){
// 创建员工对象
Emp emp = new Emp();
// 对应的属性添加属性值
emp.setUserName("test001");
emp.setName("name");
emp.setGender((short)1);
emp.setImage("测试001.jpg");
emp.setJob((short)1);
emp.setEntryData(LocalDate.of(2024,10,21));
emp.setDeptId(1);
emp.setCreateTime(LocalDateTime.now());
emp.setUpdateTime(LocalDateTime.now());
// 将Emp实体类 传递进去
empMapper.insert(emp);
// 接口方法注解了@Options则可以返回当前添加数据的empId值
System.out.println( "empId的值" +emp.getEmpId());
}// 需求: 新增(主键返回)
// 描述:在数据添加成功后,需要获取插入数据库数据的主键。
// 如:添加套餐数据时,还需要维护套餐菜品关系表数据。
// 关键点: 接口方法上面添加一条注解信息 可以获取主键信息
@Options(keyProperty = "empId" , useGeneratedKeys = true)更新
@Update("update tb_emp set username = #{username},name = #{name}, gender = #{gender}, image = #{image}, job = #{job}, entry_data = #{entryData}, update_time = #{updateTime} where id = #{id} ")
public void update(Emp emp);// 修改数据
@Test
public void testUpdate(){
Emp emp = new Emp();
emp.setId(38);
emp.setUsername("test001");
emp.setName("测试001-修改");
emp.setGender((short)1);
emp.setImage("1.png");
emp.setJob((short)2);
emp.setEntryData(LocalDate.of(2023,10,20));
emp.setUpdateTime(LocalDateTime.now());
empMapper.update(emp);
}新增(主键返回)
// 需要在接口上面新增一个注解 @Options
@Options(keyProperty = "dish_id", useGeneratedKeys = true)
@Insert(...) values (...))
void addDish(Dish dish);ID查询
// ID 查询
@Select("select * from tb_emp where id = #{id}")
public Emp getById(Integer id);@Test
public void testSelect(){
Emp emp = empMapper.getById(38);
// create_time 有下划线的字段 返回的数据是 null
System.out.println( "======"+ emp);
}条件查询
@Param("字段名称") 多个参数的时候需要使用注解
// 条件查询员工
@Select("select * from tb_emp where name like '%${name}%' and gender=#{gender} and entry_data between #{begin} and #{end} order by update_time desc")
public List<Emp> list(String name , short gender , LocalDate begin , LocalDate end);
// 优化 name 推荐
@Select("select * from tb_emp where name like concat('%' ,#{name} ,'%') and gender=#{gender} and entry_data between #{begin} and #{end} order by update_time desc")
public List<Emp> list(@Param("name") String name , @Param("gender") Short gender , @Param("begin") LocalDate begin , @Param("end") LocalDate end);
// 注意点: 需要加注解 才能运行成功 @Param("字段名称")@Test
public void testSelectWhere(){
List<Emp> list = empMapper.list("冯" , (short)1 , LocalDate.of(1998,1,1) , LocalDate.of(2024,1,1));
System.out.println( "==== " + list);
}
// 参考文档: @Param注解
// https://blog.csdn.net/weixin_43866613/article/details/138154057数据封装 _ 转化驼峰
- 实体类属性名和数据库表查询返回的字段名一致,mybatis会自动封装,
- 如果实体类属性名和数据库表查询返回的字段名不一致,不能自动封装。
// 方案一: 给字段起别名, 让别名与实体类属性一致
select ... from tb_emp where id = 50;
// 方案二:通过@Results,@Result注解手动映射封装
@Results({
@Result(column = "dept_id", property = "deptId")
@Result (column="create_time",property"createTime")
@Result(column"update_time",property"updateTime")
})
// 方案三:开启mybatis的驼峰命名自动映射开关
// 快捷键: camel 回车
mybatis.configuration.map-underscore-to-camel-case=trueXML映射文件
规范
- XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
- XML映射文件的namespace属性为Mapper接口全限定名一致。
- XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致。
插件
MybatisX是一款基于IDEA的快速开发Mybatis的插件,为效率而生。
使用Mybatis的注解,主要是来完成一些简单的增删改查功能。如果需要实现复杂的SQL功能,建议使用XML来配置映射语句。
官方说明:https://mybatis.net.cn/getting-started.html
MyBatis动态SQL
随着用户的输入或外部条件的变化而变化的SQL语句,我们称为动态SQL。
if where set
<if>
<where>
<set>
// 条件判断 <where>...</where>
// 更新数据 <set>...</set><if test = "name != null">
name like concat('%' , #{name} , '%')
</if>
<if test = "gender != null">
and gender = #{gender}
</if>
// 去除 and
将where 变成标签 <where></where>
// 更新员工信息
将set 变成标签 <set></set>foreach
// collection:遍历的集合
// item:遍历出来的元素
// soparator:分隔符
// open:遍历开始前拼接的sQL片段
// close:逾历结束后拼接的sQL片段
<foreach collection="" item="" separator="" open="" close=""></foreach>sql、include
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zkwy.mybatis.mapper.EmpMapper">
<!-- 抽取相同的sql语句 -->
<sql id="commentSelect">
select * from tb_emp
</sql>
<select id="list" resultType="com.zkwy.mybatis.pojo.Emp">
<!-- 使用抽取的sql -->
<include refid="commentSelect"/>
<where>
<if test="name != null">
name like concat('%', #{name} , '%')
</if>
<if test="gender != null">
and gender = #{gender}
</if>
<if test="begin != null and end != null">
and entry_data between #{begin} and #{end}
</if>
</where>
order by update_time desc
</select>
</mapper>案例
使用的一个文件单独使用
- 准备数据库表(dept、emp)
- 创建springboot工程,引入对应的起步依赖(web、mybatis、mysq驱动、lombok)
- 配置文件application.properties中引I入mybatis的配置信息,准备对应的实体类
- 准备对应的Mapper、Service(接口、实现类)、Controller基础结构
-- 步骤一: 准备数据库表(dept、emp)
-- 创建数据库
create database tlias;
-- 使用该数据库
use tlias;
-- 创建部门管理
create table dept
(
dept_id bigint unsigned auto_increment comment '主键ID',
name varchar(16) not null comment '部门名称',
create_time datetime null comment '创建时间',
update_time datetime null comment '更新时间',
constraint dept_id
primary key (dept_id),
constraint name
unique (name)
)
comment '部门表';
-- 向部门里面添加数据
insert into dept (name, create_time, update_time)
values ('学工部', now(), now()),
('教研部', now(), now()),
('咨询部', now(), now()),
('就业部', now(), now()),
('人事部', now(), now());
-- 创建员工表
create table emp
(
emp_id int unsigned auto_increment comment '主键 唯一标识'
primary key,
username varchar(32) null comment '用户名',
password varchar(32) default '123456' null comment '用户登录密码',
name varchar(16) not null comment '员工姓名',
gender tinyint unsigned not null comment '性别: 1 男; 2 女',
image varchar(512) null comment '照片url',
job tinyint unsigned null comment '职位: 1 班主任; 2 讲师; 3 学工主管; 4 教研主管',
entry_data date null comment '入职日期',
dept_id int unsigned null comment '归属的部门ID',
create_time datetime null comment '创建时间',
update_time datetime not null comment '更新时间',
constraint name
unique (name)
)
comment '员工表';
-- 向员工表里面添加数据
insert into emp(username, name, gender, image, job, entry_data, dept_id, create_time, update_time)
values ('jinyong', '金庸', 1, '1.jpg', 4,'2000-01-01', 2, now(), now()),
('zhangwuji', '张无忌', 1, '1.jpg',2, '2015-01-01', 2, now(), now()),
('yangxiao', '杨逍', 1, '1.jpg',2, '2008-05-01', 2, now(), now()),
('weiyixiao', '韦一笑', 1, '1.jpg',2, '2007-01-01', 2, now(), now()),
('changyuchun', '常遇春', 1, '1.jpg',2, '2012-12-05', 2, now(), now()),
('xiaozhao', '小昭', 2, '1.jpg', 3,'2013-09-05', 1, now(), now()),
('jixiaofu', '纪晓芙', 2, '1.jpg',1, '2005-08-03', 1, now(), now()),
('zhouzhiruo', '周芷若', 2, '1.jpg',1, '2014-11-09', 1, now(), now()),
('dingminjun', '丁敏君', 2, '1.jpg',1, '2011-03-11', 1, now(), now()),
('haomin', '赵敏', 2, '1.jpg',1, '2013-09-05', 2, now(), now()),
('1uzhangke', '鹿杖客', 1, '1.jpg',4, '2007-02-01', 3, now(), now()),
('hebiweng', '鹤笔翁', 1, '1.jpg',1, '2008-08-18', 3, now(), now()),
('fangdongbai', '方东白', 1, '1.jpg',3, '2012-11-01', 3, now(), now()),
('zhangsanfeng', '张三丰', 1, '1.jpg',3, '2002-08-01', 2, now(), now()),
('yulianzhou', '俞莲舟', 1, '1.jpg', 1,'2011-05-01', 4, now(), now()),
('songyuanqia', '宋远桥', 1, '1.jpg', 2,'2007-01-01', 3, now(), now()),
('chenyouliang', '陈友谅', 1, '1.jpg', null,'2015-03-21', null, now(), now());Spring 整合 JDBC
jdbc template
Spring+MyBatis框架整合。Spring+JDBC整合(原生) Spring提供了JdbcTemplate模板类,来快速高效的访问数据(底层封装JDBC的API)
- 创建jdbcTemplate模版类的对象
- 操作数据库: (1) update(sql,sql语句中的参数的取值):增删改操作,调用方法 (2) query(sql,sql语句中的参数的取值):查询操作,调用该方法
String sql = "select * from tb_user where user_name =? and password =?";
jdbcTemplate.query(sql, '小冯' , '123456')事务管理
概念:
事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。
操作:
- 开启事务(一组操作开始前,开启事务):starttransaction/begin;
- 提交事务(这组操作全部成功后,提交事务):commit;
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback;
Spring事务管理
- 注解:@Transactional
- 位置:业务(service)层的方法上、类上、接口上
- 作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
@Transactional // 事务的管理 添加此注解
@Override
public void deleteDept(Integer deptId) {
// 删除部门
deptMapper.deleteDept(deptId);
int i = 1 / 0; // 会报错 会执行事务的回滚 要么同时成功,要么同时失败
// 删除部门下 对应的所有的员工信息
empMapper.deleteDeptEmp(deptId);
}# 开启事务日志管理
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug事务进阶
rollbackFor
// Exception.class 所有的异常信息
// @Transactional(rollbackFor = Exception.class)- 默认情况下,只有出现RuntimeException才回滚异常。rollbackFor属性用于控制出现何种异常类型,回滚事务。
@Transactional // 事务的管理
@Override
public void deleteDept(Integer deptId) {
// 删除部门
deptMapper.deleteDept(deptId);
if(true){
throw new Exception("出错啦...");
}
// @Transactional 注解这时候不会回滚
// @Transactional(rollbackFor = Exception.class) 需要此注解才会回滚
// 删除部门下 对应的所有的员工信息
empMapper.deleteDeptEmp(deptId);
}propagation
- 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
// @Transactional(propagation = Propagation.REQUIRES)
// @Transactional(propagation = Propagation.REQUIRES_NEW)场景
- REQUIRED:大部分情况下都是用该传播行为即可。
- REQUIRES_NEW:当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。
案例
解散部门时,记录操作日志
// 需求:解散部门时,无论是成功还是失败,都要记录操作日志。
// 步骤:
// 1.解散部门:删除部门、删除部门下的员工
// 2.
// @Transactional(propagation = Propagation.REQUIRES_NEW)
try{
}finally{
// 不管执行成功还是失败 都会记录相应的日志
}@Transactional(rollbackFor = Exception.class) // 事务的管理 所有的异常都会执事务的回滚
@Override
public void deleteDept(Integer deptId) {
try {
// 删除部门
deptMapper.deleteDept(deptId);
int i = 1 / 0; // 会报错 会执行事务的回滚 要么同时成功,要么同时失败
// if(true){
// throw new Exception("出错啦...");
// }
// @Transactional 注解这时候不会回滚
// @Transactional(rollbackFor = Exception.class) 需要此注解才会回滚
// 删除部门下 对应的所有的员工信息
empMapper.deleteDeptEmp(deptId);
}finally {
DeptLog deptLog = new DeptLog();
// 设定创建的时间
deptLog.setCreateTime(LocalDateTime.now());
// 设定操作的日志信息
deptLog.setDescription("执行了解散部门的操作, 此次解散的部门id是: " + deptId);
deptLogService.AddDeptLog(deptLog);
}
}
// 在接口实现类中添加此注解
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public void AddDeptLog(DeptLog deptLog) {
deptLogMapper.AddDeptLog(deptLog);
}AOP基础
使用场景如下:
- 记录操作日志
- 权限控制
- 事务管理
- ......
AOP概述
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。
// 场景:
// 案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时
// 实现: SpringAOP
// 动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。SpringAOP快速入门
// 需求: SpringAOP快速入门:统计各个业务层方法执行耗时- 导入依赖:在pom.xml中导入AOP的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>- 编写AOP程序:针对于特定方法根据业务需要进行编程
package com.zkwy.tlias.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component // 当前类交给IOC管理
@Aspect // AOP类
public class TimeAspect {
// 定义一个方法
// 步骤五: 需要统计那些方法 * 返回值任意类型 com.zkwy.tlias.service.*.*(..)) 切入点表达式
@Around(" execution(* com.zkwy.tlias.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 步骤一: 记录开始时间
Long begin = System.currentTimeMillis();
// 步骤二: 调用原始方法运行
Object result = joinPoint.proceed();
// 步骤三: 记录结束时间
Long end = System.currentTimeMillis();
// 步骤四: 输出日志信息
// joinPoint.getSignature() 调用的方法
log.info(joinPoint.getSignature() + " 方法执行耗时: {}ms" , end - begin);
return result;
}
}AOP核心概念
- 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
- 通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
- 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
- 切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
- 目标对象:Target,通知所应用的对象
AOP进阶
通知类型
- @Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
- @Before:前置通知,此注解标注的通知方法在目标方法前被执行
- @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
- @AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
- @AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
注意事项
- @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
- @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
package com.zkwy.tlias.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
//@Order(1) // 可以定义通知的顺序
@Slf4j
@Component // 当前类交给IOC管理
@Aspect // AOP类 AOP即面向切面编程 面向方面编程
public class TimeAspect {
// 定义一个切入点表达式
@Pointcut("execution(* com.zkwy.tlias.service.*.*(..))")
// private 只能在此此文件使用 若外部也想使用此切入点 需要将private替换成public
private void pt(){}
// 定义一个方法
// 步骤五: 需要统计那些方法 @Around 通知类型 * 返回值任意类型 com.zkwy.tlias.service.*.*(..)) 切入点表达式
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
// 步骤一: 记录开始时间
Long begin = System.currentTimeMillis();
// 步骤二: 调用原始方法运行 @Around环绕通知 需要让原始方法执行,其余的通知类型不需要 返回的类型Object
Object result = joinPoint.proceed();
// 步骤三: 记录结束时间
Long end = System.currentTimeMillis();
// 步骤四: 输出日志信息
// joinPoint.getSignature() 调用的方法
log.info(joinPoint.getSignature() + " 方法执行耗时: {}ms" , end - begin);
return result;
}
// 前置通知 @Before
@Before("pt()")
public void before(){
log.info("前置通知 @Before... ");
}
// 后置通知 @BAfter
@After(" execution(* com.zkwy.tlias.service.*.*(..) ) ")
public void after(){
log.info("后置通知 @After...");
}
// 返回后通知 @AfterReturning
@AfterReturning(" execution(* com.zkwy.tlias.service.*.*(..))")
public void afterReturning(){
log.info("返回后通知 @AfterReturning...");
}
// 异常后通知
@AfterThrowing("pt()")
public void afterThrowing(){
log.info("异常后通知 @AfterThrowing...");
}
}@Pointcut 切入点
该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
@Pointcut("execution(* com.itheima.Service.impl.DeptServiceImpl.*(..))")
public void pt() {}
@Around("pt()")
public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable{}通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
- 不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
- 用@Order(数字)加在切面类上来控制顺序
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
切入点表达式
- 切入点表达式: 即 通知类型 ==> @Around(" 切入点表达式 "); 注解 ==> @Pointcut(" 切入点表达式 ")
- 作用: 主要用来决定项目中的哪些方法需要加入通知
// 切入点表达式:描述切入点方法的一种表达式
// 作用:主要用来决定项目中的哪些方法需要加入通知
// 常见形式:
// 1.execution(...)):根据方法的签名来匹配
// 2.@annotation(...):根据注解匹配execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为
// 其中带?的表示可以省略的部分
// 包名.类名. 不建议省略
execution(访问修饰符? 返回值 包名.类名.? 方法名(方法参数) throws异常?)可以使用通配符描述切入点
- ***** : 单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
- .. : 多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
@annotation
@annotation切入点表达式,用于匹配标识有特定注解的方法。
// 步骤一: 定义注解
package com.zkwy.tlias.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 元注解 运行时执行
@Target(ElementType.METHOD) // 在方法上执行
public @interface Log {
}// 步骤二: 在方法上添加注解 @Log
@Log
@Override
public PageBean getEmpList(Integer pageNum, Integer pageSize, String name, Short gender, LocalDate begin, LocalDate end) {
// 步骤一: 设置分页参数
PageHelper.startPage(pageNum , pageSize);
// 步骤二: 执行查询
List<Emp> empList = empMapper.getEmpList(name, gender, begin, end);
Page<Emp> p = (Page<Emp>) empList;
// 步骤三: 封装PageBean对象
return new PageBean(p.getPageNum(), p.getPageSize(), p.getTotal(), p.getResult());
}// 步骤三: 使用注解的方式来定义一个切入点表达式
@Pointcut("@annotation(com.zkwy.tlias.aop.Log)")
private void pt(){}连接点
Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
- 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint
- 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
package com.zkwy.tlias.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; @Slf4j // 打印日志信息 @Component // 交给IOC管理 @Aspect // AOP 面向切面编程 面向方面编程 public class MyAspect { // 切入点表达式 @Pointcut("execution(* com.zkwy.tlias.service.DeptService.*(..))") private void pt(){} // 前置通知 @Before("pt()") public void before(JoinPoint joinPoint){ log.info("前置通知 @Before..."); } // 环绕通知 API都一样 使用环绕通知来测试 @Around("pt()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { log.info("环绕通知开始..."); // 1. 获取 目标对象的类名 String className = joinPoint.getTarget().getClass().getName(); log.info("获取目标对象的类名: {} " , className); // 2. 获取 目标方法的方法名 String methodName = joinPoint.getSignature().getName(); log.info("获取目标对象的方法名: {}", methodName); // 3. 获取 目标方法运行时传入的参数 Object[] args = joinPoint.getArgs(); log.info("获取目标方法运行时传入的参数: {}" , Arrays.toString(args)); // 4. 获取 目标方法执行 只有在环绕通知里面才有原始方法的执行 Object result = joinPoint.proceed(); // 5. 获取 目标方法运行的返回值 我们可以对返回值的数据进行重新处理 类似于方法的重载 log.info("获取目标方法运行的返回值: {}" , result); log.info("环绕通知结束..."); // 我们可以对返回值的数据进行重新处理 类似于方法的重载 return result; } }
APO案例
// AOP(面向切面编程 面向方面编程) 综合案例 --- 操作日志记录在数据库表中
// 将案例中增、删、改相关接口的操作日志记录到数据库表中。
// 日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
// 思路分析:
// 1.需要对所有业务中的增 删 改 方法添加统一的功能, 使用AOP技术最为方便 @Around 环绕通知
// 2.由于增 删 改 方法名没有规律, 可以自定义 @Log 注解完成目标方法的匹配
// 步骤一: 准备
// 1.在工程中引入 AOP(面向切面编程) 依赖
// 2.创建数据库表结构, 并创建对应的实体类(Javabean类)
// 步骤二: 编码
// 1.自定义注解 @Log
// 2.定义切面类, 完成记录操作日志的逻辑// 步骤一: 创建实体类(Javabean类)
package com.zkwy.tlias.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
private Integer operateId; // 唯一标识 ID
private Integer operateUser; // 操作人ID
private LocalDateTime operateTime; // 操作的时间
private String className; // 操作的类名
private String methodName; // 操作的方法名
private String methodParams; // 操作方法的参数
private String returnValue; // 操作方法的返回值
private Long costTime; // 操作耗时
}// Log注解
package com.zkwy.tlias.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 元注解 运行时执行
@Target(ElementType.METHOD) // 元注解 在方法上执行
public @interface Log {
}// OperateLogMapper
package com.zkwy.tlias.mapper;
import com.zkwy.tlias.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OperateLogMapper {
// 向日志表中记录对应的数据
@Insert("insert into operate_log(operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) values (#{operateUser} , #{operateTime} , #{className} , #{methodName} , #{methodParams} , #{returnValue} , #{costTime})")
void AddOperateLog(OperateLog log);
}// 切面类 AOP(面向切面编程 面向方面编程)
package com.zkwy.tlias.aop;
import com.alibaba.fastjson.JSONObject;
import com.zkwy.tlias.mapper.OperateLogMapper;
import com.zkwy.tlias.pojo.OperateLog;
import com.zkwy.tlias.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;
@Slf4j
@Component // 交给IOC管理
@Aspect // AOP(面向切面编程) 切面类
public class LogAspect {
@Autowired
HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
// 环绕通知
@Around("@annotation(com.zkwy.tlias.anno.Log)")
public Object recordLod(ProceedingJoinPoint joinPoint) throws Throwable {
// 步骤四: 向operateLog里面添加数据
// 操作人ID 需要在 JWT 令牌中获取
String jwt = request.getHeader("token"); // 获取jwt令牌
Claims claims = JwtUtils.parseJWT(jwt); // 解析jwt令牌
Integer operateUserEmpId = (Integer) claims.get("empId");
// 操作时间
LocalDateTime operateTime = LocalDateTime.now();
// 操作的类名
String className = joinPoint.getTarget().getClass().getName();
// 操作的方法名
String methodName = joinPoint.getSignature().getName();
// 操作方法的参数
Object[] args = joinPoint.getArgs();
String methodParams = Arrays.toString(args);
// 原始方法执行前 记录开始时间
Long begin = System.currentTimeMillis();
// 步骤一: 调用原始目标方法运行
Object result = joinPoint.proceed();
// 原始方法结束时 记录结束时间
Long end = System.currentTimeMillis();
// 操作的返回值 就是 result 将对象转化为json字符串
String returnValue = JSONObject.toJSONString(result);
// 操作耗时
long costTime = end - begin;
// 步骤三: 创建日志对象
OperateLog operateLog = new OperateLog(null , operateUserEmpId , operateTime , className , methodName , methodParams , returnValue , costTime);
// 步骤二: 向日志表中添加数据
operateLogMapper.AddOperateLog(operateLog);
log.info("AOP记录操作日志: {}" ,operateLog);
return result;
}
}Spring原理篇
配置优先级
SpringBoot 中支持三种格式的配置文件
// java系统属性 // 命令行参数# application.properties 优先级高 server.port = 8081# application.yml 优先级中 推荐使用 server: port: 8082# application.yaml 优先级低 server: port: 8083注意事项:
- 虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置(yml是主流)
Bean管理 IOC
获取bean
默认情况下,Spring项目启动时,会把bean都创建好放在lOC容器中,如果想要主动获取这些bean,可以通过如下:
方式:
// 据name获取bean: Object getBean(String name)
// 根据类型获取bean: <T> T getBean(Class<T> requiredType)
// 根据name获取bean(带类型转换):<T> T getBean(String name, Class<T> requiredType)@Autowired
private ApplicationContext applicationContext; // IOC容器对象
@Test
public void testGetBean(){
// 根物ben的名称获取
// 类名 bean = ( 类名 )applicationContext.getBean("类名(首字母小写)");
// 根据bean的类型获取
// 类名 bean = applicationContext.getBean(类名.class);
// 根据bean的名称及类型获取
// 类名 bean = applicationContext.getBean("类名(首字母小写)" , 类名.class);
}bean作用域
- Spring支持五种作用域,后三种在web环境才生效:
| 作用域 | 说明 |
|---|---|
| singleton | 容器内同名称的bean只有一个实例(单例)(默认) |
| prototype | 每次使用该bean时会创建新的实例(非单例) |
| request | 每个请求范围内会创建新的实例(web环境中,了解) |
| session | 每个会话范围内会创建新的实例(web环境中,了解) |
| application | 每个应用范围内会创建新的实例(web环境中,了解) |
- 可以通过@Scope注解来进行配置作用域:
@Scope("prototype")
@RestController
@RequestMapping("/depts")
public class DeptController{
}
// 注意事项
// 默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)。
// prototype的bean,每一次使用该bean的时候都会创建一个新的实例。
// 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性第三方bean
- 如果要管理的bean对象来自于第三方(不是自定义的),是无法用@Component及衍生注解声明bean的,就需要用到@Bean注解。
- 若要管理的第三方bean对象,建议对这些bean进行集中分类配置,可以通过@Configuration注解声明一个配置类。
@Configuration
public class WebConfig{
@Bean
public Object outBean(){
...
return null;
}
}SpringBoot原理
起步依赖
......
自动配置
SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了ioc容器中,不需要我手动去声明,从而简化了开发,省去了繁琐的配置操作。
// 方案一:@ComponentScan组件扫描
// 在启动文件下配置
@ComponentScan({"com.example","com.itheima"})
@SpringBootApplication
public class SpringbootwebConfig2Application {}
// 方案二: @Import导入。使用@lmport导入的类会被Spring加载到ioc容器中,导入形式主要有以下几种:
// 导入 普通类
// 导入 配置类
// 导入 ImportSelector 接口实现
// @EnableXxxx注解,封装@Import注解 推荐使用@conditional
// 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到SpringIOc容器中。
// 使用的位置: 方法, 类
// @conditional身是一个父注解,派生出大量的子注解:
// @ConditionalOnClass:判断环境中是否有对应字节码文件,才注册bean到ioc容器。
// @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOc容器。
// @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到Ioc容器。案例
自定义starter 依赖
// 场景:
// 在实际开发中,经常会定义一些公共组件,提供给各个项目团队使用。而在SpringBoot的项目中,一般会将这些公共组件封装为SpringBoot 的 starter。// 需求: 自定义aliyun-oss-spring-boot-starter,完成阿里云OSS操作工具类AliyunOsSUtils的自动配置。
// 目标:引入起步依赖引入之后,要想使用阿里云OSS,注入AliyunOSSUtils直接使用即可。// 步骤:
// 创建aliyun-oss-spring-boot-starter模块
// 创建aliyun-oss-spring-boot-autoconfigure模块,在starter中引l入该模块
// 在aliyun-oss-spring-boot-autoconfigure模块中的定义自动配置功能,并定义自动配置文件META-INF/spring/xxxx.importsWeb后端开发总结
三层架构
- controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据。
- service:业务逻辑层,处理具体的业务逻辑。
- dao:数据访问层(Data Access Object)(持久层),负责数据访问操作,包括数据的增、删、改、查。
