今日目标
- 能够理解AOP的作用
- 能够完成AOP的入门案例
- 能够理解AOP的工作流程
- 能够说出AOP的五种通知类型
- 能够完成"测量业务层接口万次执行效率"案例
- 能够掌握Spring事务配置
一、AOP
1 AOP简介
1.1 AOP简介和作用【理解】
- AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
- OOP(Object Oriented Programming)面向对象编程
- 作用:在不惊动原始设计的基础上为其进行功能增强。简单的说就是在不改变方法源代码的基础上对方法进行功能增强。
- Spring理念:无入侵式/无侵入式
- 原理:动态代理
- 应用:1. 日志 2. 异常捕获、处理 3. 监控统计代码 4. 记录过程。
1.2 AOP中的核心概念【理解】
-
连接点(JoinPoint):正在执行的方法,例如:update()、delete()、select()等都是连接点。可以被增强的方法叫连接点 (假设我有一百个方法 那这些方法都可以被增强,这些方法都可以叫做连接点 )
-
切入点(Pointcut):进行功能增强了的方法,例如:update()、delete()方法,select()方法没有被增强所以不是切入点,但是是连接点。
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义 真正要增强的方法
(假设你参加人大代表会议 当你被选上的时候 你才能成为切入点 否则只是连接点)
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
- 一个具体方法:com.gaohe.dao包下的BookDao接口中的无形参无返回值的save方法
- 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
- 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
-
通知/增强(Advice):在切入点前后执行的操作,也就是增强的共性功能
- 在SpringAOP中,功能最终以方法的形式呈现
-
通知类:通知方法所在的类叫做通知类
-
切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。 (方法+增强 称为切面)
-
Weaving (织入):(动词) 是指把增强应用到目标对象来创建新的代理对象的过程。 spring采用动态代理织入,而 AspectJ采用编译期织入和类装载期织入
2 AOP入门案例【重点】
写一个计算器功能:计算除法。并且用日志方式打印传递的参数、和结果、结束后通知用户计算完成!如果出现异常打印异常信息。
2.1 AOP入门案例思路分析
- 案例设定:案例AOP实现-计算器功能
- 开发模式:XML or 注解
- 思路分析:
- 导入坐标(pom.xml)
- 制作连接点方法(原始操作,dao接口与实现类)
- 制作共性功能(通知类与通知)
- 定义切入点
- 绑定切入点与通知关系(切面)
- 开启AOP功能
2.2 AOP入门案例实现
2.2.1 xml版本(补充)
① 导入 AOP 相关坐标
② 创建目标接口和目标类(内部有切点)
③ 创建切面类(内部有增强方法)
④ 将目标类和切面类的对象创建权交给 spring
⑤ 在 applicationContext.xml 中配置织入关系
⑥ 测试代码
① 导入 AOP 相关坐标
<!--导入spring的context坐标, context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
② 创建目标接口和目标类(内部有切点)
dao
public interface Counter {
int add(int i,int j);
int div(int i,int j);
}
daoImpl
public class CounterImpl implements Counter {
@Override
public int add(int i, int j) {
System.out.println("add run!");
int r = i+j;
return r;
}
@Override
public int div(int i, int j) {
System.out.println("div run!");
int r = i/j;
return r;
}
}
③ 创建切面类(内部有增强方法)
/*③ 创建切面类(内部有增强方法)
*/
public class MyAspect {
//前置增强方法
public void before(){
System.out.println("前置增强方法运行...");
}
}
④ 将目标类和切面类的对象创建权交给 spring
<!--配置切面对象-->
<bean id="myAspect" class="com.itgaohe.aop.MyAspect"/>
<!--配置目标对象-->
<bean id="counter" class="com.itgaohe.dao.CounterImpl"/>
⑤ 在 applicationContext.xml 中配置织入关系 导入aop命名空间
⑤ 在 applicationContext.xml 中配置织入关系 配置切点表达式和前置增强的织入关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置切面对象-->
<bean id="myAspect" class="com.itgaohe.aop.MyAspect"/>
<!--配置目标对象-->
<bean id="counter" class="com.itgaohe.dao.CounterImpl"/>
<!--配置织入:告诉spring框架 哪些方法(切点)需要进行哪些增强(前置、后置-->
<aop:config>
<!-- 声明切面 告诉spring框架(如果不告诉 spring框架只将myAspect认为是一个普通bean)-->
<aop:aspect ref="myAspect">
<!--切面:切点+通知-->
<!--配置Target的method方法执行时要进行myAspect的before方法前置增强-->
<!--声明切点 和切点表达式-->
<aop:before method="before" pointcut="execution(* com.itgaohe.dao.CounterImpl.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
⑥ 测试代码
public class Test02 {
public static void main(String[] args) throws ParseException {
ApplicationContext app = new ClassPathXmlApplicationContext("SpringConfig.xml");
Counter bean = app.getBean("counter",Counter.class);
int add = bean.add(1, 2);
System.out.println(add);
System.out.println(bean.getClass());
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:SpringConfig.xml")
public class Test01 {
@Autowired
private Counter counter;
@Test
public void show(){
counter.add(1, 2);
counter.div(5,3);
}
}
运行结果
前置增强方法运行...
add run!
前置增强方法运行...
div run!
2.2.2注解方式(重点)
【第一步】导入aop相关坐标
<dependencies>
<!--spring核心依赖,会将spring-aop传递进来-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<!--切入点表达式依赖,目的是找到切入点方法,也就是找到要增强的方法-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
</dependencies>
【第二步】定义dao接口与实现类
public interface Counter {
int add(int i,int j);
int div(int i,int j);
}
@Repository("counterImpl")
public class CounterImpl implements Counter {
@Override
public int add(int i, int j) {
System.out.println("add run!");
// System.out.println(2/0);//afterThrowing advice ...
int r = i+j;
return r;
}
@Override
public int div(int i, int j) {
System.out.println("div run!");
int r = i/j;
return r;
}
}
【第三步】定义通知类,制作通知方法
【第四步】定义切入点表达式、配置切面(绑定切入点与通知关系)
//1.注入容器
@Component
//2.标注是切面类
@Aspect
public class MyAdvice {
//3.切面类=切点+通知
//4.设置切点
// @Pointcut("execution(* com.itgaohe.dao.Counter.add(int,int))")//匹配方法名为add方法 参数有两个int
// @Pointcut("execution(* com.itgaohe.dao.Counter.add(*,*))")//匹配方法名为add方法 参数有两个任意类型
// @Pointcut("execution(* com.itgaohe.dao.Counter.add(..))")//匹配方法名为add方法 参数有任意类型多个参数
// @Pointcut("execution(* com.itgaohe.dao.Counter.*(..))")//匹配counter类中任意方法名 参数有任意个
// @Pointcut("execution(* com.itgaohe.dao.Counter.a*(..))")//匹配counter类中a开头的方法名 参数有任意个
@Pointcut("execution(* com.itgaohe.dao.*.*(..))")//匹配方法名为dao包下所有的方法
private void pt() {
}
/*
- 名称:@Before
- 类型:**==方法注解==**
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
* */
@Before("pt()")
public void before(JoinPoint jp) {
//或其目标对象
System.out.println("before advice ...");
// System.out.println(jp.getSignature());
}
/*- 名称:@After
- 类型:==**方法注解**==
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行*/
@After("pt()")
public void after(JoinPoint jp) {
System.out.println("after advice ...");
// System.out.println(jp.getSignature());
}
/*
* - 名称:@AfterReturning(了解)
- 类型:**==方法注解==**
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
* value 切点方法 returning 返回的值Object
returning 的名字和参数的名字必须一致
JoinPoint 如果出现必须在第一位
* */
@AfterReturning(value = "pt()", returning = "r")
public void method4(JoinPoint jp, Object r) {
System.out.println("返回通知" + r);
System.out.println(jp.getSignature().getName());//add
}
/*
* - 名称:@AfterThrowing(了解)
- 类型:**==方法注解==**
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
* value 切点方法 throwing 返回的异常对象的名字
异常对象的名字 和参数对象的名字一致
JoinPoint 如果出现必须在第一位
* */
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(JoinPoint jp, Throwable t) {
System.out.println("afterThrowing advice ...");
}
/*
* - 名称:@Around(重点,常用)
- 类型:**==方法注解==**
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
* */
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
Object ret = pjp.proceed();
System.out.println("ret:" + ret);
System.out.println("around after advice ...");
return ret;
}
}
【第五步】在配置类中进行Spring注解包扫描和开启AOP功能
@Configuration
@ComponentScan("com.itgaohe")
//开启注解开发AOP功能
@EnableAspectJAutoProxy
public class SpringConfig {
}
测试类和运行结果
public class App {
public static void main(String[] args) {
ApplicationContext app = new AnnotationConfigApplicationContext(SpringConfig.class);
Counter bean = app.getBean("counterImpl",Counter.class);
int add = bean.add(1, 2);
System.out.println(add);
System.out.println(bean.getClass());//class com.sun.proxy.$Proxy23
}
}
3 AOP工作流程【理解】
问题导入
什么是目标对象?什么是代理对象?
3.1 AOP核心概念
目标对象(Target):被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强。
代理对象(Proxy):代理后生成的对象,由Spring帮我们创建代理对象。
3.2 在测试类中验证代理对象
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
//打印对象的类名
System.out.println(bookDao.getClass());
}
}
3.3 AOP工作流程
- Spring容器启动
- 读取所有切面配置中的切入点
- 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
- 匹配失败,创建原始对象
- 匹配成功,创建原始对象(目标对象)的代理对象
- 获取bean执行方法
- 获取的bean是原始对象时,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作
4 AOP切入点表达式(理解)
4.1 语法格式
-
切入点:要进行增强的方法
-
切入点表达式:要进行增强的方法的描述方式
- 描述方式一:执行com.itgaohe.dao包下的BookDao接口中的无参数update方法
execution(void com.gaohe.dao.BookDao.update())
- 描述方式二:执行com.gaohe.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.gaohe.dao.impl.BookDaoImpl.update())
-
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.gaohe.service.UserService.findById(int))
- 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
- 访问修饰符:public,private等,可以省略
- 返回值:写返回值类型
- 包名:多级包使用点连接
- 类/接口名:
- 方法名:
- 参数:直接写参数的类型,多个类型用逗号隔开
- 异常名:方法定义中抛出指定异常,可以省略
4.2 通配符
目的:可以使用通配符描述切入点,快速描述。
- :单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现 匹配一个
匹配com.itgaohe包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
execution(public * com.itgaohe.*.UserService.find*(*))
- … :多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写 匹配多个
匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
execution(public User com..UserService.findById(..))
- +:专用于匹配子类类型
execution(* *..*Service+.*(..)) 匹配任意包中以service结尾的类和这个类的子类
4.3 书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效
- 描述切入点通**常描述接口**,而不描述实现类
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
- 包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
- 方法名书写以动词进行精准匹配,名词采用匹配,例如getById书写成getBy,selectAll书写成selectAll
- 参数规则较为复杂,根据业务方法灵活调整
- 通常**不使用异常作为匹配**规则
eg:
public class MyAdvice {
//@Pointcut("execution(* com.itgaohe.dao.Counter.add(int,int))")//匹配方法名为add方法 参数有两个int
//@Pointcut("execution(* com.itgaohe.dao.Counter.add(*,*))")//匹配方法名为add方法 参数有两个任意类型
//@Pointcut("execution(* com.itgaohe.dao.Counter.add(..))")//匹配方法名为add方法 参数有任意类型多个参数
//@Pointcut("execution(* com.itgaohe.dao.Counter.*(..))")//匹配counter类中任意方法名 参数有任意个
//@Pointcut("execution(* com.itgaohe.dao.Counter.a*(..))")//匹配counter类中a开头的方法名 参数有任意个
@Pointcut("execution(* com.itgaohe.dao.*.*(..))")//匹配方法名为dao包下所有的方法
private void pt() {}}
eg:
切入点表达式——范例
execution(* *(..))//任意方法
execution(* *..*(..))//当前类及其子类中所有方法
execution(* *..*.*(..))//拦截当前包或者子包中定义的所有方法
execution(public * *..*.*(..))//拦截所有修饰符是public的方法
execution(public int *..*.*(..)) //拦截所有返回值是int的方法
execution(public void *..*.*(..))//拦截所有返回值是void的方法
execution(public void com..*.*(..))// public void 拦截com包或者子包中定义的方法
execution(public void com..service.*.*(..))//public void 拦截com包下所有service包中定义的方法
execution(public void com.gaohe.service.*.*(..))//public void 拦截com包下所有service包中定义的方法
execution(public void com.gaohe.service.User*.*(..))//User开头的方法
execution(public void com.gaohe.service.*Service.*(..))//Service结尾的方法
execution(public void com.gaohe.service.UserService.*(..))//
execution(public User com.gaohe.service.UserService.find*(..))//
execution(public User com.gaohe.service.UserService.*Id(..))//
execution(public User com.gaohe.service.UserService.findById(..))//
execution(public User com.gaohe.service.UserService.findById(int))//
execution(public User com.gaohe.service.UserService.findById(int,int))//
execution(public User com.gaohe.service.UserService.findById(int,*))//
execution(public User com.gaohe.service.UserService.findById(*,int))//
execution(public User com.gaohe.service.UserService.findById())//
execution(List com.gaohe.service.*Service+.findAll(..))//service包中以service结尾的类或者其子类汇总的findAll方法
-- 常用
-- 限定增强所有的查询方法
execution(* com.gaohe.service.*.find*(..))
-- 限定增强具体某个方法
execution(* com.gaohe.service.UserService.findById(int))
5 AOP通知类型【重点】
随堂案例:
写一个计算器功能:计算除法。并且用日志方式打印传递的参数、和结果、结束后通知用户计算完成!如果出现异常打印异常信息。
5.1 AOP通知分类
- AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
- AOP通知共分为5种类型
- 前置通知:在切入点方法执行之前执行
- 后置通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,后置通知都会执行。
- **环绕通知(重点):**手动调用切入点方法并对其进行增强的通知方式。
- 返回后通知(了解):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行。
- 抛出异常后通知(了解):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行。
5.2 AOP通知详解
5.2.1 前置通知
- 名称:@Before
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
- 范例:
@Before("pt()")
public void before(JoinPoint jp) {
System.out.println("before advice ...");
}
5.2.2 后置通知
- 名称:@After
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
- 范例:
@After("pt()")
public void after(JoinPoint jp) {
System.out.println("after advice ...");
}
5.2.3 返回后通知
- 名称:@AfterReturning(了解)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
- 范例:
// value 切点方法 returning 返回的值Object
// returning 的名字和参数的名字必须一致
// JoinPoint 如果出现必须在第一位
@AfterReturning(value = "pt()",returning = "r")
public void method4(JoinPoint jp,Object r){
System.out.println("返回通知"+r);
System.out.println(jp.getSignature().getName());
}
5.2.4 抛出异常后通知
- 名称:@AfterThrowing(了解)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
- 范例:
// value 切点方法 throwing 返回的异常对象的名字
// 异常对象的名字 和参数对象的名字一致
// JoinPoint 如果出现必须在第一位
@AfterThrowing("pt()",throwing = "t")
public void afterThrowing(JoinPoint jp,Throwable t) {
System.out.println("afterThrowing advice ...");
}
5.2.5 环绕通知
- 名称:@Around(重点,常用)
- 类型:方法注解
- 位置:通知方法定义上方
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
- 范例::
@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;
}
环绕通知注意事项
- 环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
- 环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。
二、AOP案例
1 案例-测量业务层接口万次执行效率
1.1 需求和分析
需求:任意业务层接口执行均可显示其执行效率(执行时长)
分析:
①:业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
②:通知类型选择前后均可以增强的类型——环绕通知
步骤:
获取执行的签名对象
获取接口/类全限定名
获取方法名
记录开始时间
执行万次操作
记录结束时间
打印执行结果
1.2 代码实现
【前置工作】环境准备
1.导入数据库 a.sql
2.导入依赖
3.引入实体类
4.引入配置文件 jdbc.properties
5.编写dao service
A
@Alias("account")
@ToString
@Data
@AllArgsConstructor
@NoArgsConstructor
public class A {
private Integer id;
private String name;
}
ADao
public interface ADao {
@Select("select * from a")
List<A> findAll();
@Select("select * from a where id = #{id} ")
A findById(int id);
}
AService
public interface AService {
A findById(int id);
List<A> findAll();
}
AServiceImpl
@Service
public class AServiceImpl implements AService {
@Autowired
private ADao aDao;
@Override
public A findById(int id) {
return aDao.findById(id);
}
@Override
public List<A> findAll() {
return aDao.findAll();
}
}
【第一步】编写通知类
//获取执行的签名对象Signature signature = pjp.getSignature();
//获取接口/类全限定名String className = signature.getDeclaringTypeName();
//获取方法名String methodName = signature.getName();
//记录开始时间 long start = System.currentTimeMillis();
@Component
@Aspect
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.gaohe.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("ProjectAdvice.servicePt()") //本类类名可以省略不写
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
//获取接口/类全限定名
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");
}
}
【第二步】在SpringConfig配置类上开启AOP注解功能
SpringConfig
@Configuration
@ComponentScan("com.itgaohe")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy//开启注解开发AOP功能
public class SpringConfig {
}
JdbcConfig
package com.itgaohe.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import javax.sql.DataSource;
import java.beans.PropertyVetoException;
/*
* 标注数据源配置
*
* */
@PropertySource(value = {"classpath:jdbc.properties"})
public class JdbcConfig {
//读取配置文件中的信息
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean("dataSource")//使用在方法上,标注将该方法的返回值存储到 Spring 容器中
public DataSource getDataSource() throws PropertyVetoException {
DruidDataSource dds = new DruidDataSource();
dds.setDriverClassName(driver);
dds.setUrl(url);
dds.setUsername(username);
dds.setPassword(password);
return dds;
}
}
MybatisConfig
public class MybatisConfig {
//定义bean,SqlSessionFactoryBean,用于产生SqlSessionFactory对象
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itgaohe.pojo");
ssfb.setDataSource(dataSource);
return ssfb;
}
//定义bean,返回MapperScannerConfigurer对象
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer sc = new MapperScannerConfigurer();
sc.setBasePackage("com.itgaohe.dao");
return sc;
}
}
【第三步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AService aService;
@Test
public void testFindById(){
A a = aService.findById(2);
}
@Test
public void testFindAll(){
List<A> list = aService.findAll();
}
}
2 案例-百度网盘密码数据兼容处理
2.1 需求和分析
需求:对百度网盘分享链接输入密码时尾部多输入的空格做兼容处理
分析:
①:在业务方法执行之前对所有的输入参数进行格式处理——trim()
②:使用处理后的参数调用原始方法——环绕通知中存在对原始方法的调用
2.2 代码实现
【前置工作】环境准备
//-------------service层代码-----------------------
public interface UserService {
void getUrl(String url,String code);
}
@Service
public class UserServiceImpl implements UserService {
public void getUrl(String url, String code) {
System.out.println(url);
System.out.println(code.length());
System.out.println("资源获取成功。。");
}
}
【第一步】编写通知类
//1. 声明他是一个通知类
@Component
@Aspect
public class MyAdvice4 {
// 2.切入点
@Pointcut("execution(* com.itgaohe.service.UserService.get*(..))")
public void pt(){}
// 3. 环绕通知
@Around("MyAdvice4.pt()")
public void method(ProceedingJoinPoint pjp){
// 1.获取源参数
Object[] args = pjp.getArgs();
// 2.处理参数
for (int i = 0; i < args.length; i++) {
// 把参数转化为字符串
String string = args[i].toString();
// 把字符串的前后空格干掉 在赋值回原数组
args[i] = string.trim();
System.out.println(args[i]);
}
// 3.调用源方法
try {
// 调用源方法
pjp.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return;
}
}
【第二步】在SpringConfig配置类上开启AOP注解功能
@Configuration
@ComponentScan("com.itgaohe")
@EnableAspectJAutoProxy
public class SpringConfig {
}
【第三步】运行测试类,查看结果
@Autowired
private UserService userService;
@Test
public void testurl(){
userService.getUrl(" http://www.itgaohe.com","1234 ");
}
3 AOP开发总结SM
3.1 AOP的核心概念
- 概念:AOP(Aspect Oriented Programming)面向切面编程,一种编程范式
- 作用:在不惊动原始设计的基础上为方法进行功能增强
- 实现原理:jdk动态代理
- 应用场景:1.日志2. 监控3. 参数预处理4.异常捕获
- 核心概念
- 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
- 连接点(JoinPoint): 在SpringAOP中,理解为任意方法的执行
- 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
- 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
- 切面(Aspect):描述通知与切入点的对应关系
- 目标对象(Target):被代理的原始对象成为目标对象
3.2 切入点表达式语法
-
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)
- execution(* com.itgaohe.service.Service.(…))
-
切入点表达式描述通配符:
- 作用:用于快速描述,范围描述
- *:匹配任意符号(常用)
- … :匹配多个连续的任意符号(常用)
- +:匹配子类类型
-
切入点表达式书写技巧
1.按标准规范开发
2.查询操作的返回值建议使用*匹配
3.减少使用…的形式描述包
4.对接口进行描述,使用*表示模块名,例如UserService的匹配描述为*Service
5.方法名书写保留动词,例如get,使用*表示名词,例如getById匹配描述为getBy*
6.参数根据实际情况灵活调整
3.3 五种通知类型
- 前置通知
- 后置通知
- 环绕通知(重点)
- 环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
- 环绕通知可以隔离原始方法的调用执行
- 环绕通知返回值设置为Object类型
- 环绕通知中可以对原始方法调用过程中出现的异常进行处理
- 返回后通知
- 抛出异常后通知
三、Spring事务管理
举例子 在jdbc的时候 提交事务 都是提交事务
1 Spring事务简介【重点】
回顾
事务作用:在数据层保障一系列的数据库操作同成功同失败
特性:原子性 一致性 隔离性 持久性
操作:开启、提交、回滚。
1、原子性:表示事务内所有操作为一个整体,要么全部成功,要么全部失败。
2、一致性:表示事务内一个操作失败了,事务会回滚到初始状态。
3、隔离性:事务查看数据事所处的状态,要么事另一个并发事务修改之前的状态,要么是另一个并发事务修改之后的状态,事务不会查看中间数据的状态。
4、持久性:事务完成之后,对数据的影响是永久的。
mysql事务操作是哪个层面的呢?业务层还是数据层
Spring提供的事务管理是数据层的事务还是业务层的事务? 业务层
1.1 Spring事务作用
- 事务作用:在数据层保障一系列的数据库操作同成功同失败
- Spring事务作用:在数据层或**业务层**保障一系列的数据库操作同成功同失败
更加 灵活、简便 操作事务
1.2 需求和分析
- 需求:实现任意两个账户间转账操作
- 需求微缩:A账户减钱,B账户加钱
- 结果分析:
①:程序正常执行时,账户金额A减B加,没有问题
②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
1.3 代码实现
【前置工作】环境准备
Spring整合Mybatis相关代码(依赖、JdbcConfig、MybatisConfig、SpringConfig)省略。
//pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private int id;
private String name;
private double money;
}
//dao
public interface AccountDao {
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String inMan, @Param("money") double inMoney);
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String outMan, @Param("money") double outMoney);
}
//service
public interface AccountService {
/**
* 转账操作
* @param out 传出方
* @param in 转入方
* @param money 金额
*/
public void transfer(String out,String in ,Double money) ;
}
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void transfer(String out,String in ,Double money) {
accountDao.outMoney(out,money);
int i = 1/0;
accountDao.inMoney(in,money);
}
}
【第一步】设置事务管理器(将事务管理器添加到IOC容器中)
说明:可以在JdbcConfig中配置事务管理器
//配置事务管理器,mybatis使用的是jdbc事务
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource){
DataSourceTransactionManager dtm = new DataSourceTransactionManager();
dtm.setDataSource(dataSource);
return dtm;
}
注意事项
- 事务管理器要根据实现技术进行选择
- MyBatis框架使用的是JDBC事务
【第二步】在业务层接口上添加Spring事务管理
public interface AccountService {
//配置当前接口方法具有事务
@Transactional
public void transfer(String out,String in ,Double money) ;
}
注意事项
- Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
- 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
【第三步】开启注解式事务驱动
@Configuration
@ComponentScan("com.itgaohe")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
//开启注解式事务驱动
@EnableTransactionManagement
public class SpringConfig {
}
【第四步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() throws IOException {
accountService.transfer("Tom","Jerry",100D);
}
}
如果不开启事务控制 @EnableTransactionManagement
则在转账过程中 如果出现问题 则钱仍然可以穿递过去。 没有做到事务的控制。
2 Spring事务角色【理解】
2.1 Spring 事务角色
- 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
- 事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
这里一共有三个事务 但是后面两个事务最后被transfer方法管理。 在这里 transfer做事务管理员
后两个做事务协调员
3 Spring事务相关配置
问题导入
什么样的异常,Spring事务默认是不进行回滚的?
3.1 事务配置
案例引入
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String outMan, String inMan, double money) throws IOException {
accountDao.outMoney(outMan,money);
if (true){throw new IOException();}//手动抛出一个异常
accountDao.inMoney(inMan,money);
}
}
进行测试发现 此时事务并没有进行控制 原因是对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译期异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。
@Transactional(rollbackFor = {IOException.class})
public interface AccountService {
//配置当前接口方法具有事务
public void transfer(String outMan,String inMan,double money) throws IOException;
}
说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。
@Transactional事务可处理的异常图:
默认情况下处理 RuntimeException Error两种,然后回滚
如果配置了rollback-for,那么会判断Exception是否符合配置,然后回滚
3.2 自学部分
TransactionDefinition
TransactionDefinition 是事务的定义信息对象,
作用 是 内部封装的是控制事务的一些参数的
里面有如下方法:
方法 | 说明 |
---|---|
int getIsolationLevel() | 获得事务的隔离级别 |
int getPropogationBehavior() | 获得事务的传播行为 |
int getTimeout() | 获得超时时间 |
boolean isReadOnly() | 是否只读 |
1.事务隔离级别
设置隔离级别,可以解决事务并发产生的问题,如脏读、不可重复读和虚读。
ISOLATION_DEFAULT
ISOLATION_READ_UNCOMMITTED //上述问题都解决不了
ISOLATION_READ_COMMITTED //读已提交 可以解决脏读问题
ISOLATION_REPEATABLE_READ //可重复读 解决 不可重复读的问题
ISOLATION_SERIALIZABLE //串行化 上述问题都能解读 但是性能低
2.事务传播行为
作用:解决业务方法在调用业务方法统一性的问题
解释:在业务方法A中调用业务方法B 如果 B方法在调用前都进行了事务控制 会出现重复或统一的问题 ,事务传播行为就是用来解决这些问题。
(以下不需要记 如果开发中用到的话比较严格的时候 去查就行)
1.REQUIRED :如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。一般的选择(默认值)
解释: a业务方法 调用b业务方法 b业务方法看a有没有事务,如果a没有事务 那么b就新建一个事务 如果a有事务,那么b就加入这个事务
2.SUPPORTS :支持当前事务,如果当前没有事务,就以非事务方式执行(没有事务)
解释: a调用b b看a有无事务,如果有 就支持 ,如果没有就非事务方式执行。
3.MANDATORY:使用当前的事务,如果当前没有事务,就抛出异常
解释:a调用b b看a有无事务 有就用当前事务 没有就抛出异常
4.REQUERS_NEW:新建事务,如果当前在事务中,把当前事务挂起。
5.NOT_SUPPORTED :以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
6.NEVER:以非事务方式运行,如果当前存在事务,抛出异常
7.NESTED :如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行 REQUIRED 类似的操作
超时时间:默认值是-1,没有超时限制。如果有,以秒为单位进行设置
有必要超时时间 如果时间太长 不行!
是否只读:建议查询时设置为只读
用A和B去理解 A如果没有当前事务 那就新建一个事务B
3. TransactionStatus
事务的状态对象
随着事务的执行 状态会发生变化!(以下api对你不重要 理解)
是主动还是被动信息? 被动! 状态自己变化!
TransactionStatus 接口提供的是事务具体的运行状态,方法介绍如下。
方法 | 说明 |
---|---|
boolean hasSavepoint() | 是否存储回滚点 |
boolean isCompleted() | 事务是否完成 |
boolean isNewTransaction() | 是否是新事务 |
boolean isRollbackOnly() | 事务是否回滚 |
4 案例:转账业务追加日志
需求和分析
- 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕
- 需求微缩:A账户减钱,B账户加钱,数据库记录日志
- 分析:
①:基于转账操作案例添加日志模块,实现数据库中记录日志
②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能 - 实现效果预期:
无论转账操作是否成功,均进行转账操作的日志留痕 - 存在的问题:
日志的记录与转账操作隶属同一个事务,同成功同失败 - 实现效果预期改进:
无论转账操作是否成功,日志必须保留 - 事务传播行为:事务协调员对事务管理员所携带事务的处理态度
【准备工作】环境整备
sql
USE spring_db;
CREATE TABLE tbl_log(
id INT PRIMARY KEY AUTO_INCREMENT,
info VARCHAR(255),
createDate DATE
);
//pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Log {
private int id;
private String info;
private Date createDate;
}
//dao
public interface LogDao {
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
//service
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional
void log(String out, String in, Double money);
}
//serviceImpl
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
public void log(String out,String in,Double money) {
logDao.log("转账操作由"+out+"到"+in+",金额:"+money);
}
}
切面编写
@Component
@Aspect
public class AccountLogAdvice {
@Autowired
private LogService logService;
@Pointcut("execution(* com.itgaohe.service.AccountService.*(..))")
public void pt(){}
@AfterReturning(value = "pt()",returning = "obj")
public void returning(JoinPoint jp,Object obj){
System.out.println("转账成功....");
// 把转账信息保存到数据库log中
Object[] args = jp.getArgs();
String inName = (String) args[0];
String outName = (String) args[1];
Double money = (Double) args[2];
String code = "转账成功";
logService.log(inName,outName,money,code);
}
@AfterThrowing(value = "pt()",throwing = "e")
public void throwing(JoinPoint jp,Throwable e){
System.out.println("转账出问题了...");
// 把转账信息保存到数据库log中
Object[] args = jp.getArgs();
String inName = (String) args[0];
String outName = (String) args[1];
Double money = (Double) args[2];
String code = "转账失败";
logService.log(inName,outName,money,code);
}
}
【第一步】在AccountServiceImpl中调用logService中添加日志的方法
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
public void transfer1(String out,String in ,Double money) {
// 无论转账操作是否成功,均进行转账操作的日志留痕
try {
accountDao.outMoney(outMan, money);
//System.out.println(2/0);
accountDao.inMoney(inMan, money);
} finally {
logService.log(outMan, inMan, money);
}
}
}
如果转账过程中出现了异常 则转账和日志打印都不进行 但是我们的要求是 无论转账操作是否成功,日志必须保留,所以在这个过程中存在的问题就是 日志的记录与转账操作隶属同一个事务,同成功同失败,要想解决这个问题,我们需要设置事务的传播行为
【第二步】在LogService的log()方法上设置事务的传播行为
public interface LogService {
//propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
void log(String out, String in, Double money);
}
【第三步】运行测试类,查看结果
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer1() throws IOException {
accountService.transfer1("Tom","Jerry",50D);
}
}
5 事务传播行为(面试题)
今日小结
AOP开发流程:(重中之重)
-
导入依赖
-
@EnableAspectJAutoProxy 开启功能
-
写aop通知类
-
声明 变成切面类
@Component
@Aspect -
写切点配置@PointCut()
-
写通知业务@Around()
-
AOP:在不惊动原始设计的基础上为其进行功能增强。
-
原理:动态代理
**目标对象(Target):**被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强。
**代理对象(Proxy):**代理后生成的对象,由Spring帮我们创建代理对象。 -
重要概念:
-
代理(Proxy):SpringAOP的核心本质是采用代理模式实现的
-
连接点(JoinPoint): 在SpringAOP中,理解为任意方法的执行
-
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
-
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
-
切面(Aspect):描述通知与切入点的对应关系
-
目标对象(Target):被代理的原始对象成为目标对象
-
-
-
事务管理
-
如何操作事务1.声明2.管理3.开启
1. 事务角色(事务管理员 事务协调员) 2. 事务传播行为
在的问题就是 日志的记录与转账操作隶属同一个事务,同成功同失败,要想解决这个问题,我们需要设置事务的传播行为