转载

Spring的Aop

1、AOP的相关术语

Joinpoint(连接点): 目标类中的所有方法都是连接点
Pointcut(切入点): 目标类类中会被增强的方法都是切入点
Advice(通知): 所谓通知是指拦截到Pointcut之后所要做的事情就是通知.通知分为前置通知,后置通知,异常通知,最终通知,环绕通知
Aspect(切面): 通知与切入点的结合(所谓的切面就是用来说明通知与切入点的关系,即:通知在切入点执行的什么时候执行)
Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction可以在运行期为类动态地添加一些方法或Field
Target(目标对象): 目标类对象(要增强的类)
Proxy(代理对象): 一个目标类被AOP织入增强后,就产生一个结果代理类
Weaving(织入): 是把增强功能应用到目标的过程,即:把advice应用到target的过程

2、Spring的AOP的基本配置步骤

需求:在不修改已有代码的前提下,在项目现有所有类的方法前后打印日志

接口

public interface AccountService {
    /**
     * 模拟保存账户
     */
    void saveAccount();

    /**
     * 模拟更新账户
     */
    void updateAccount(int i);

    /**
     * 模拟删除账户
     */
    int deleteAccount();
}

实现类

public class AccountServiceImpl implements AccountService {

    public void saveAccount() {
        System.out.println("执行了保存");
    }

    public void updateAccount(int i) {
        System.out.println("执行了更新");
    }

    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

通知类

/**
 * 日志打印类
 */
public class Logger {
    /**
     * 用于打印日志,计划让其在切入点方法执行前执行(切入点方法就是业务层方法)
     */
    public void printLog() {
        System.out.println("Logger的printLog()执行了");
    }
}

配置文件

<?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"
       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">

    <!-- 配置spring的IOC,将业务层的AccountServiceImpl配置到容器中 -->
    <bean id="accountServiceImpl" class="com.itcast.service.impl.AccountServiceImpl"></bean>

    <!--
        spring中基于xml的AOP配置:
            1.将通知Bean配置到spring的IOC容器中
            2.使用<aop:config>标签表明开始AOP的配置
            3.使用<aop:aspect>标签表明开始配置切面
                    属性:
                        id:切面的唯一标识符
                        ref:用于指向通知bean的id
            4.在<aop:aspect>标签的内部使用对应的标签来配置通知的类型
                    前置通知:<aop:before>
                        method属性:用于指定Logger类中的哪个方法是前置通知
                        pointcut属性:用于指定切入点表达式,该表达式的含义是指对业务层的哪些方法增强
                            切入点表达式的语法形式:
                                关键字:execution(表达式)
                                表达式:
                                    访问权限 返回值 包名.包名...类名.方法名(参数列表)
                                        标准的表达式写法:
                                            public void com.itcast.service.impl.AccountServiceImpl.saveAccount()
                                        访问修饰符可以省略:
                                            void com.itcast.service.impl.AccountServiceImpl.saveAccount()
                                        返回值可以使用通配符,表示任意返回值
                                            * com.itcast.service.impl.AccountServiceImpl.saveAccount()
                                        包名可以使用通配符,表示任意包,但是有几级包,就需要写几个*.
                                            * *.*.*.*.AccountServiceImpl.saveAccount()
                                        包名可以使用..表示当前包及其子包
                                            * *..AccountServiceImpl.saveAccount()
                                        包名和方法名也可以使用*来实现通配
                                            * *..*.*()
                                        参数列表
                                            可以直接写数据类型
                                                基本类型直接写名称:* *..*.*(int)
                                                引用类型写包名.类名:* *..*.*(java.lang.String)
                                            可以使用通配符表示任意类型,但必须是有参数:* *..*.*(*)
                                            可以使用..表示有无参数均可:* *..*.*(..)
                                        全通配写法:
                                            * *..*.*(..)
                                        实际开发中实际切入点表达式的通常写法:切到业务层下的所有实现类
                                            * com.itcast.service.impl.*.*(..)
                    后置通知:<aop:after>
                    异常通知:<aop:after-throwing>
                    最终通知:<app:after>
                    环绕通知:<aop:around>
                    我们现在示例是让printLog()方法在切入点方法执行之前先执行,属于前置通知

    -->
    <bean id="logger" class="com.itcast.utils.Logger"></bean>

    <aop:config>
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置通知的类型,并且建立通知方法和切入点方法的关联关系 -->
            <aop:before method="printLog" pointcut="execution(* com.itcast.service.impl.*.*(..))"></aop:before>
        </aop:aspect>
    </aop:config>
    
</beans>

测试类

public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = applicationContext.getBean("accountServiceImpl",AccountService.class);
        accountService.saveAccount();
        accountService.updateAccount(1);
        accountService.deleteAccount();
    }
}

测试结果

Logger的printLog()执行了
执行了保存
Logger的printLog()执行了
执行了更新
Logger的printLog()执行了
执行了删除

3、四种常用的通知类型

XML中的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"
       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">

    <!-- 配置spring的IOC,将业务层的AccountServiceImpl配置到容器中 -->
    <bean id="accountServiceImpl" class="com.itcast.service.impl.AccountServiceImpl"></bean>

    <bean id="printLog" class="com.itcast.utils.Logger"></bean>

    <aop:config>
        <!--
            配置切入点表达式,id属性用于指定表达式的唯一标识符,expression属性用于指定表达式内容,此标签写在
            <aop:aspect>标签内部,只能当前切面内使用。它可以配置<aop:aspect>标签为,这样就可以所有的切面都可以复用
        -->
        <aop:pointcut id="pt1" expression="execution(* com.itcast.service.impl.*.*(..))"/>

        <aop:aspect id="logAdvice" ref="printLog">
            
            <!------------------------------- 使用<aop:pointcut>标签简化配置前 ------------------------------->
            <!--  
            <!-- 前置通知 -->
            <aop:before method="beforePrintLog" pointcut="execution(* com.itcast.service.impl.*.*(..))"></aop:before>

            <!-- 后置通知 -->
            <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.itcast.service.impl.*.*(..))"></aop:after-returning>

            <!-- 异常通知 -->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* com.itcast.service.impl.*.*(..))"></aop:after-throwing>

            <!-- 最终通知 -->
            <aop:after method="afterPrintLog" pointcut="execution(* com.itcast.service.impl.*.*(..))"></aop:after>
            -->

            <!------------------------------- 使用<aop:pointcut>标签简化配置后 ------------------------------->

            <!-- 前置通知 -->
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>

            <!-- 后置通知 -->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>

            <!-- 异常通知 -->
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>

            <!-- 最终通知 -->
            <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
        
    </aop:config>

</beans>

测试类

public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = applicationContext.getBean("accountServiceImpl",AccountService.class);
        accountService.saveAccount();
    }
}

测试结果

前置通知:Logger的printLog()执行了
执行了保存
后置通知:Logger的printLog()执行了
最终通知:Logger的printLog()执行了

4、配置环绕通知

通知类

/**
 * 用于记录日志的工具类
 */
public class Logger {
    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("前置通知:Logger的printLog()执行了");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger的printLog()执行了");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger的printLog()执行了");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog() {
        System.out.println("最终通知:Logger的printLog()执行了");
    }

    /**
     * 环绕通知
     * 问题:
     *      当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
     * 分析:
     *      通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用
     * 解决:
     *      Spring框架为我们提供了一个接口,Pro
   */
    public Object aroundPrintLog(ProceedingJoinPoint joinPoint) {
        // 调用proceed()方法即调用切入点方法
        Object retValue = null;
        try {
            System.out.println("前置通知:Logger的aroundPrintLog()执行了");
            // 获取切入点方法执行时所需的参数
            Object args[] = joinPoint.getArgs();
            retValue = joinPoint.proceed(args);
            System.out.println("后置通知:Logger的aroundPrintLog()执行了");
            return retValue;
        }catch (Throwable throwable) {
            System.out.println("异常通知:Logger的aroundPrintLog()执行了");
            throw new RuntimeException(throwable);
        }finally {
            System.out.println("最终通知:Logger的aroundPrintLog()执行了");
        }
    }
}

XML中的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"
       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">

    <!-- 配置spring的IOC,将业务层的AccountServiceImpl配置到容器中 -->
    <bean id="accountServiceImpl" class="com.itcast.service.impl.AccountServiceImpl"></bean>

    <bean id="printLog" class="com.itcast.utils.Logger"></bean>

    <aop:config>
        <!--
            配置切入点表达式,id属性用于指定表达式的唯一标识符,expression属性用于指定表达式内容,此标签写在
            <aop:aspect>标签内部,只能当前切面内使用。它可以配置<aop:aspect>标签为,这样就可以所有的切面都可以复用
        -->
        <aop:pointcut id="pt1" expression="execution(* com.itcast.service.impl.*.*(..))"/>

        <aop:aspect id="logAdvice" ref="printLog">
            <!-- 配置环绕通知,详细注释请查询Logger类 -->
            <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

</beans>

测试类

public class TestAOP {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        AccountService accountService = applicationContext.getBean("accountServiceImpl",AccountService.class);
        accountService.saveAccount();
    }
}

测试结果

前置通知:Logger的aroundPrintLog()执行了
执行了保存
后置通知:Logger的aroundPrintLog()执行了
最终通知:Logger的aroundPrintLog()执行了

5、基于注解的AOP配置

接口

/**
 * 账户的业务层接口
 */
public interface AccountService {

    /**
     * 模拟保存账户
     */
   void saveAccount();

    /**
     * 模拟更新账户
     * @param i
     */
   void updateAccount(int i);

    /**
     * 删除账户
     * @return
     */
   int  deleteAccount();
}

实现类

/**
 * 账户的业务层实现类
 */
@Service("accountService")
public class AccountServiceImpl implements IAccountService{

    @Override
    public void saveAccount() {
        System.out.println("执行了保存");
    }

    @Override
    public void updateAccount(int i) {
        System.out.println("执行了更新");

    }

    @Override
    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}

XML配置文件

<?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
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置spring创建容器时要扫描的包-->
    <context:component-scan base-package="com.itheima"></context:component-scan>

    <!-- 配置spring开启注解AOP的支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

通知类

在这里插入代码片/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 */
@Component("logger")
//表示当前类是一个切面类
@Aspect
public class Logger {

    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1(){}

    /**
     * 前置通知
     */
    @Before("pt1()")
    public  void beforePrintLog(){
        System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
    }

    /**
     * 后置通知
     */
    @AfterReturning("pt1()")
    public  void afterReturningPrintLog(){
        System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
    }
    /**
     * 异常通知
     */
    @AfterThrowing("pt1()")
    public  void afterThrowingPrintLog(){
        System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
    }

    /**
     * 最终通知
     */
    @After("pt1()")
    public  void afterPrintLog(){
        System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
    }
}

测试类

/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }
}

测试结果

前置通知Logger类中的beforePrintLog方法开始记录日志了。。。
执行了保存
最终通知Logger类中的afterPrintLog方法开始记录日志了。。。
后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。

观察返回结果就会发现通知的输出顺序有问题,这个时spring在使用注解环绕通知时存在的问题

基于注解的AOP,使用环绕通知不会存在顺序问题

/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 */
@Component("logger")
//表示当前类是一个切面类
@Aspect
public class Logger {

    @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
    private void pt1(){}
    
    /**
     * 环绕通知
     * 问题:
     *      当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。
     * 分析:
     *      通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有。
     * 解决:
     *      Spring框架为我们提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *      该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。
     *
     * spring中的环绕通知:
     *      它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式。
     */
    @Around("pt1()")
    public Object aroundPringLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try{
            Object[] args = pjp.getArgs();//得到方法执行所需的参数

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");

            rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)

            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");

            return rtValue;
        }catch (Throwable t){
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
            throw new RuntimeException(t);
        }finally {
            System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
        }
    }
}

测试类

/**
 * 测试AOP的配置
 */
public class AOPTest {
    public static void main(String[] args) {
        //1.获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.获取对象
        IAccountService as = (IAccountService)ac.getBean("accountService");
        //3.执行方法
        as.saveAccount();
    }
}

测试结果

Logger类中的aroundPringLog方法开始记录日志了。。。前置
执行了保存
Logger类中的aroundPringLog方法开始记录日志了。。。后置
Logger类中的aroundPringLog方法开始记录日志了。。。最终
原文  https://segmentfault.com/a/1190000022700010
正文到此结束
Loading...