SpringAOP复习

SpringAOP

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。他是一种可以在不修改原来核心代码的情况侠给程序动态统一进行增强的一种技术

SpringAOP:批量对Spring容器中的bean方法做增强,并且这种增强不会与原来方法中的代码耦合

1.快速入门

1.1需求

要求让08_SpringAOP模块中service包下所有类的所有方法在调用前都输出:方法被调用了

1.2准备工作

第一步:引入依赖

  <!--SpringIOC相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.9.RELEASE</version>
        </dependency>
        <!--AOP相关依赖-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>

第二步:开启组件扫描

<context:component-scan base-package="com.sangeng"></context:component-scan>

第三步:相关bean注入容器中

@Service
public class PhoneService {

    public void deleteAll(){
        System.out.println("PhoneService中deleteAll的核心代码");
    }
}
@Service
public class UserService {


    public void deleteAll(){
        System.out.println("UserService中deleteAll的核心代码");
    }
}

1.3实现AOP

①开启AOP注解支持
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--开启组件扫描-->
    <context:component-scan base-package="com.sangeng"></context:component-scan>
    <!--开启aop注解支持-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>
②创建切面类

创建一个类,在类上加上@Component和@Aspect

使用@Pointcut注解来指定要被增强的方法

使用@Before注解来给我们的增强代码所在的方法进行标识,并且指定了增强代码是在被增强方法执行之前执行的

package com.sangeng.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {

     //写一个pt方法,作用是用来承载PointCut
     //PointCut注解来声明要对哪些方法进行增强
     @Pointcut("execution(* com.sangeng.service.*.*(..))")
     public void pt(){}

     //用@Before注解来指定该方法中是增强的代码,并且指定被哪个切点表达式指定的方法前执行前执行的
     //@Before的属性写上加了@Pointcut注解的方法: 方法名()
     @Before("pt()")
     public void methodBefore(){
          System.out.println("方法被调用了");
     }

}
③测试
package com.sangeng;


import com.sangeng.service.PhoneService;
import com.sangeng.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Demo {
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserService userService = app.getBean(UserService.class);
        PhoneService phoneService = app.getBean(PhoneService.class);
        userService.deleteAll();
        phoneService.deleteAll();
    }
}

2.AOP核心概念

  • Jointpoint(连接点):所谓连接点是指那些可以被增强的点。在spring中,这些点指的的方法,因为spring只支持方法类型的连接点

  • Pointcut(切入点):所谓切入点是指被增强的连接点(方法)

  • Advice(通知/ 增强):所谓通知是指具体增强的代码

  • Target(目标对象):被增强的对象就是目标对象

  • Aspect(切面):是切入点和通知(引介)的结合

  • Proxy (代理):一个类被 AOP 增强后,就产生一个结果代理类

3.切点确定

3.1切点表达式

可以使用切点表达式来表示要对哪些方法进行增强

写法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  • 访问修饰符可以省略,大部分情况下省略
  • 返回值类型、包名、类名、方法名可以使用星号* 代表任意
  • 包名与类名之间一个点 . 代表当前包下的类,两个点 … 表示当前包及其子包下的类
  • 参数列表可以使用两个点 … 表示任意个数,任意类型的参数列表
execution(* com.sangeng.service.*.*(..))   表示com.sangeng.service包下任意类,方法名任意,参数列表任意,返回值类型任意
   
execution(* com.sangeng.service..*.*(..))   表示com.sangeng.service包及其子包下任意类,方法名任意,参数列表任意,返回值类型任意
    
execution(* com.sangeng.service.*.*())     表示com.sangeng.service包下任意类,方法名任意,要求方法不能有参数,返回值类型任意
    
execution(* com.sangeng.service.*.delete*(..))     表示com.sangeng.service包下任意类,参数列表任意,返回值类型任意,方法名要求已delete开头

3.2切点函数@annotation

我们也可以在要增强的方法上加上注解。然后使用@annotation来表示对加了什么注解的方法进行增强

写法:@annotation(注解的全类名)

第一步:自定义注解如下

package com.sangeng.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})//该注解可以加在方法上
@Retention(RetentionPolicy.RUNTIME)
public @interface KekeAnnotation {
}

切面类中使用@annotation来确定要增强的方法

package com.sangeng.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {
    
     @Pointcut("@annotation(com.sangeng.aspect.KekeAnnotation)")
     public void pt(){}

     //用@Before注解来指定该方法中是增强的代码,并且指定被哪个切点表达式指定的方法前执行前执行的
     //@Before的属性写上加了@Pointcut注解的方法: 方法名()
     @Before("pt()")
     public void methodBefore(){
          System.out.println("方法被调用了");
     }

}

给需要增强的方法增加注解

package com.sangeng.service;


import com.sangeng.aspect.KekeAnnotation;
import org.springframework.stereotype.Service;

@Service
public class PhoneService {


    @KekeAnnotation
    public void deleteAll(){
        System.out.println("PhoneService中deleteAll的核心代码");
    }
}

测试

image-20231218104941170

4.通知分类

  • @Before:前置通知,在目标方法执行前执行

  • @AfterReturning: 返回后通知,在目标方法执行后执行,如果出现异常不会执行

  • @After:后置通知,在目标方法之后执行,无论是否出现异常都会执行

  • @AfterThrowing:异常通知,在目标方法抛出异常后执行

  • @Around:环绕通知,围绕着目标方法执行

环绕通知非常特殊,它可以对目标方法进行全方位的增强,我们重点学习@Around环绕通知

package com.sangeng.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {

     @Pointcut("@annotation(com.sangeng.aspect.KekeAnnotation)")
     public void pt(){}

     @Around("pt()")
     public void around(ProceedingJoinPoint pjp){
          System.out.println("目标方法执行前");
          try {
               pjp.proceed(); //目标方法执行
               System.out.println("目标方法执行后");
          } catch (Throwable e) {
               e.printStackTrace();
               System.out.println("目标方法出现异常");
          } finally {
               System.out.println("finally代码块");
          }
     }

}

5.获取被增强方法相关信息

我们实际对方法进行增强时往往还需要获取到被增强代码的相关信息,比如方法名,参数,返回值,异常对象等

我们可以在除了环绕通知外的所有通知方法中增加一个JoinPoint类型的参数。这个参数封装了被增强方法的相关信息。**我们可以通过这个参数获取到除了异常对象和返回值之外的所有信息

@Before("pt()")
    public void methodbefore(JoinPoint jp){
        Object[] args = jp.getArgs();//方法调用时传入的参数
        Object target = jp.getTarget();//被代理对象
        MethodSignature signature = (MethodSignature) jp.getSignature();//获取被被增强方法签名封装的对象
        System.out.println("Before方法被调用了");
    }

案例:

需求:要求让所有service包下类的所有方法被调用前都输出全类名,方法名,以及调用时传入的参数

package com.sangeng.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;

@Component
@Aspect
public class MyAspect {


     @Pointcut("execution(* com.sangeng.service..*.*(..))")
     public void pt(){}

     @Before("pt()")
     public void beforeMethod(JoinPoint jp){
          MethodSignature signature = (MethodSignature) jp.getSignature();
          //获取参数
          Object[] args = jp.getArgs();
          Method method = signature.getMethod();
          //获取方法名
          String methodName = method.getName();
          //获取全类名
          String declaringTypeName = signature.getDeclaringTypeName();
          System.out.println("全类名:" + declaringTypeName);
          System.out.println("方法名:" + methodName);
          System.out.println("参数:" + Arrays.toString(args));
     }
}

如果需要获取被增强方法中的异常对象或者返回值则需要在方法参数上增加一个对应类型的参数,并且使用注解的属性进行配置。这样Spring会把你想获取的数据赋值给对应的方法参数

但是上述的方法比较麻烦,直接在环绕通知方法中增加一个ProceedingJoinPoint类型的参数。这个参数封装了被增强方法的相关信息

该参数的proceed()方法被调用相当于被增强方法被执行,调用后的返回值就相当于被增强方法的返回值

@Around(value = "pt()")
    public Object around(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();//方法调用时传入的参数
        Object target = pjp.getTarget();//被代理对象
        MethodSignature signature = (MethodSignature) pjp.getSignature();//获取被被增强方法签名封装的对象
        Object ret = null;
        try {
            ret = pjp.proceed();//ret就是目标方法执行后的返回值
        } catch (Throwable throwable) {
            throwable.printStackTrace();//throwable就是出现异常时的异常对象
        }
        return ret;
    }

6.多切面顺序问题

在实际项目中我们可能会存在配置了多个切面的情况。这种情况下我们很可能需要控制切面的顺序

我们在默认情况下Spring有它自己的排序规则。(按照类名排序)

默认排序规则往往不符合我们的要求,我们需要进行特殊控制

如果是注解方式配置的AOP可以在切面类上加**@Order注解来控制顺序。@Order中的属性越小优先级越高**

如果是XML方式配置的AOP,可以通过调整配置顺序来控制

例如:

@Component
@Aspect
@Order(2)
public class APrintLogAspect {
    //省略无关代码
}
@Component
@Aspect
@Order(1)
public class CryptAspect {
    //省略无关代码
}

7.动态代理

实际上Spring的AOP其实底层就是使用动态代理来完成的。并且使用了两种动态代理分别是JDK的动态代理和Cglib动态代理。所以我们接下去来学习下这两种动态代理,理解下它们的不同点

7.1JDK动态代理

JDK的动态代理使用的java.lang.reflect.Proxy这个类来进行实现的。要求被代理(被增强)的类需要实现了接口。并且JDK动态代理也只能对接口中的方法进行增强

public static void main(String[] args) {
        AIControllerImpl aiController = new AIControllerImpl();
        //使用动态代理增强getAnswer方法
        //1.JDK动态代理
        //获取类加载器
        ClassLoader cl = Demo.class.getClassLoader();
        //被代理类所实现接口的字节码对象数组
        Class<?>[] interfaces = AIControllerImpl.class.getInterfaces();
        AIController proxy = (AIController) Proxy.newProxyInstance(cl, interfaces, new InvocationHandler() {
            //使用代理对象的方法时 会调用到invoke
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //proxy   是代理对象
                //method 是当前被调用的方法封装的Method对象
                //args   是调用方法时传入的参数
                //调用被代理对象的对应方法
                //判断 当前调用的是否是getAnswer方法
                if(method.getName().equals("getAnswer")){
                    System.out.println("增强");
                }
                Object ret = method.invoke(aiController, args);
                return ret;
            }
        });
        String answer = proxy.getAnswer("三连了吗?");
		System.out.println(answer);
    }

7.2Cglib动态代理

使用的是org.springframework.cglib.proxy.Enhancer类进行实现的

public class CglibDemo {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(AIControllerImpl.class);
        enhancer.setCallback(new MethodInterceptor() {
            //使用代理对象执行方法是都会调用到intercept方法
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                //判断当前调用的方法是不是getAnswer方法 如果是进行增强
                if ("getAnswer".equals(method.getName())){
                    System.out.println("被增强了");
                }
                //调用父类中对应的方法
                Object ret = methodProxy.invokeSuper(o, objects);
                return ret;
            }
        });
        //生成代理对象
        AIControllerImpl proxy = (AIControllerImpl) enhancer.create();
//        System.out.println(proxy.getAnswer("你好吗?"));
        System.out.println(proxy.fortuneTelling("你好吗?"));
    }
}

7.3两种动态代理方案总结

​ JDK动态代理要求被代理(被增强)的类必须要实现接口,生成的代理对象相当于是被代理对象的兄弟

​ Cglib的动态代理不要求被代理(被增强)的类要实现接口,生成的代理对象相当于被代理对象的子类对象

Spring的AOP默认情况下优先使用的是JDK的动态代理,如果使用不了JDK的动态代理才会使用Cglib的动态代理

7.4切换默认动态代理方式

如果我们是采用注解方式配置AOP的话:

设置aop:aspectj-autoproxy标签的proxy-target-class属性为true,代理方式就会修改成Cglib

<aop:aspectj-autoproxy proxy-target-class="true"/>

8.Spring声明式事务

8.1配置事务管理器和事务注解驱动

在spring的配置文件中添加如下配置:

    <!--把事务管理器注入Spring容器,需要配置一个连接池-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--开启事务注解驱动,配置使用的事务管理器-->
    <tx:annotation-driven transaction-manager="txManager"/>

8.2添加注解

在需要进行事务控制的方法或者类上添加@Transactional注解就可以实现事务控制。

    @Transactional
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);
//        System.out.println(1/0);
        //减少
        accoutDao.updateMoney(outId,-money);
    }

注意:如果加在类上,这个类的所有方法都会受事务控制,如果加在方法上,就是那一个方法受事务控制。

注意,因为声明式事务底层是通过AOP实现的,所以最好把AOP相关依赖都加上。

       <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.6</version>
        </dependency>

9.属性配置

9.1事务传播行为propagation

测试案例:

@Service
public class TestServiceImpl {
    @Autowired
    AccountService accountService;

    @Transactional
    public void test(){
        accountService.transfer(1,2,10D);
        accountService.log();
    }
}
public class AccountServiceImpl implements AccountService {
	//...省略其他不相关代码
    @Transactional
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }

}

当事务方法嵌套调用时,需要控制是否开启新事务,可以使用事务传播行为来控制

属性值行为
REQUIRED(必须要有,默认值)外层方法有事务,内层方法就加入。外层没有,内层就新建
REQUIRES_NEW(必须要有新事务,其实就是创建了一个新的连接)外层方法有事务,内层方法新建。外层没有,内层也新建
SUPPORTS(支持有)外层方法有事务,内层方法就加入。外层没有,内层就也没有
NOT_SUPPORTED(支持没有)外层方法有事务,内层方法没有。外层没有,内层也没有
MANDATORY(强制要求外层有)外层方法有事务,内层方法加入。外层没有。内层就报错
NEVER(绝不允许有)外层方法有事务,内层方法就报错。外层没有。内层就也没有

上述案例中,test()方法里面有两个业务1.转账操作 2.打印日志,并且该方法加了声明式事务注解。这两个业务也都加了事务注解。那么测试的时候转账是成功的,但是打印r日志的时候出现了除0异常,事务就会回滚,注意这里是回滚所有的业务,因为@Transactional注解的默认传播行为,只建立一个连接c1

但我们并不希望这样,因为转账业务是成功的,仅仅由于日志打印出现异常就导致转账业务回滚,显然得不偿失,日志记录可以少记录一天,但是转账无需回滚

如何解决这一问题?可以修改转账业务的事务传播行为@Transactional(propagation = Propagation.REQUIRES_NEW),即创建了一个新的连接c2,开启了一个新的事务,当执行到转账业务的时候,转账业务成功,这个连接就会提交事务,即使日志打印的业务出现异常回滚,也是只回滚c1,转账业务并不会回滚

代码修改如下:

@Transactional(propagation = Propagation.REQUIRES_NEW)
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);
        //减少
        accoutDao.updateMoney(outId,-money);
    }

9.2隔离级别isolation

Isolation.DEFAULT 使用数据库默认隔离级别

Isolation.READ_UNCOMMITTED

Isolation.READ_COMMITTED

Isolation.REPEATABLE_READ

Isolation.SERIALIZABLE

@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);
        //减少
        accoutDao.updateMoney(outId,-money);
    }

9.3只读readOnly

如果事务中的操作都是读操作,没涉及到对数据的写操作可以设置readOnly为true。这样可以提高效率

    @Transactional(readOnly = true)
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }

Isolation.READ_UNCOMMITTED

Isolation.READ_COMMITTED

Isolation.REPEATABLE_READ

Isolation.SERIALIZABLE

@Transactional(propagation = Propagation.REQUIRES_NEW,isolation = Isolation.READ_COMMITTED)
    public void transfer(Integer outId, Integer inId, Double money) {
        //增加
        accoutDao.updateMoney(inId,money);
        //减少
        accoutDao.updateMoney(outId,-money);
    }

9.3只读readOnly

如果事务中的操作都是读操作,没涉及到对数据的写操作可以设置readOnly为true。这样可以提高效率

    @Transactional(readOnly = true)
    public void log() {
        System.out.println("打印日志");
        int i = 1/0;
    }