day03-Spring

今日目标

  • 能够理解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结尾的接口中的任意方法,所有带有一个参数的方法
  • 通知/增强(Advice):在切入点前后执行的操作,也就是增强的共性功能

    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:通知方法所在的类叫做通知类

  • 切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。 (方法+增强 称为切面)

  • Weaving (织入):(动词) 是指把增强应用到目标对象来创建新的代理对象的过程。 spring采用动态代理织入,而 AspectJ采用编译期织入和类装载期织入

2 AOP入门案例【重点】

写一个计算器功能:计算除法。并且用日志方式打印传递的参数、和结果、结束后通知用户计算完成!如果出现异常打印异常信息。

2.1 AOP入门案例思路分析
  • 案例设定:案例AOP实现-计算器功能
  • 开发模式:XML or 注解
  • 思路分析:
    1. 导入坐标(pom.xml)
    2. 制作连接点方法(原始操作,dao接口与实现类)
    3. 制作共性功能(通知类与通知)
    4. 定义切入点
    5. 绑定切入点与通知关系(切面)
    6. 开启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工作流程

在这里插入图片描述

  1. Spring容器启动
  2. 读取所有切面配置中的切入点
  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
    • 匹配失败,创建原始对象
    • 匹配成功,创建原始对象(目标对象)的代理对象
  4. 获取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;
}

环绕通知注意事项

  1. 环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
  2. 环绕通知方法的返回值建议写成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;
}

注意事项

  1. 事务管理器要根据实现技术进行选择
  2. MyBatis框架使用的是JDBC事务
【第二步】在业务层接口上添加Spring事务管理
public interface AccountService {
    //配置当前接口方法具有事务
    @Transactional
    public void transfer(String out,String in ,Double money) ;
}

注意事项

  1. Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
  2. 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
【第三步】开启注解式事务驱动
@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开发流程:(重中之重)

  1. 导入依赖

  2. @EnableAspectJAutoProxy 开启功能

  3. 写aop通知类

  4. 声明 变成切面类

    @Component
    @Aspect

  5. 写切点配置@PointCut()

  6. 写通知业务@Around()

  7. AOP:在不惊动原始设计的基础上为其进行功能增强。

    1. 原理:动态代理

      **目标对象(Target):**被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强。
      **代理对象(Proxy):**代理后生成的对象,由Spring帮我们创建代理对象。

    2. 重要概念:

      • 代理(Proxy):SpringAOP的核心本质是采用代理模式实现的

      • 连接点(JoinPoint): 在SpringAOP中,理解为任意方法的执行

      • 切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述

      • 通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法

      • 切面(Aspect):描述通知与切入点的对应关系

      • 目标对象(Target):被代理的原始对象成为目标对象

  8. 事务管理

  9. 如何操作事务1.声明2.管理3.开启

     1. 事务角色(事务管理员  事务协调员)
     2. 事务传播行为
    

在的问题就是 日志的记录与转账操作隶属同一个事务,同成功同失败,要想解决这个问题,我们需要设置事务的传播行为

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值