Spring-AOP切面编程

一、概要

Spring加入了对AOP编程支持,利用AOP的思想结合Spring的一些API可以实现核心业务与辅助业务的分离,即可以在执行核心业务时,将一些辅助的业务加进来,而辅助业务(如日志,权限控制等)一般是一些公共业务,这样就实现了两者的分离,使得核心业务的代码更加纯粹,而且辅助业务也能得到复用,面向切面编程(AOP)是对面向对象编程(oop)的补充,

还有eclipse基金会的AspectJ开源项目

Spring AOP Spring AOP默认是使用JDK动态代理,如果代理的类没有接口则会使用CGLib代理

AspectJ是语言级别的AOP实现,扩展了Java语言,定义了AOP语法,能够在编译期提供横切代码的织入,所以它有专门的编译器用来生成遵守Java字节码规范的Class文件

所以Spring除了自身实现了AOP外还对也对AspectJ进行了支持,

目前Spring提供了4种实现AOP的方式

  1. 基于代理的AOP
  2. 纯POJO切面
  3. @AspectJ注解驱动的切面(重点)
  4. 注入式AspectJ切面(与Spring并无多大的关系,这个就是使用AspectJ这个框架实现AOP编程)

二、AOP使用场景

  1. Authentication 权限管理

  2. Log 日志记录

  3. Transactions 事务处理

  4. Exception 异常处理

  5. safety control 安全控制

  6. Caching 缓存

  7. performance 性能统计 等等

三、AOP的基本概念

1、术语

术语 描述
方面/切面(Aspect) 通常是一个类,里面可以定义切入点和通知
增强(Advice) 有时候也翻译成通知,这是在方法执行之前或之后采取的实际操作。Spring提供了有5种类型的通知
切入点(Pointcut) 就是带有通知的连接点,在程序中主要体现为书写切入点表达式,负责往"什么地方"插入代码,"通知"负责插入"什么代码
加入点(Join point) 程序执行过程中明确的点,一般是方法的调用,
介绍或者引入(Introduction) 允许我们向现有的类添加新的方法或者属性
编织(Weaving) 把切面应用到目标对象来创建新的代理对象的过程(由框架完成)。织入一般发生在如下几个时机: 1.编译时(AspectJ的织入编译器), 2.运行时完成(动态代理技术)。 3.类加载时(使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码)
目标对象(Target object) 对象被一个或多个方面通知(Advice),该对象将始终是代理的对象。也称为通知(Advice)对象。

2、5种通知(Advice)

通知节点 对应的接口 描述
Before(前) org.apringframework.aop.MethodBeforeAdvice 在方法执行之前运行通知。
After-returning(返回通知) org.springframework.aop.AfterReturningAdvice 只有方法成功完成后才能在方法执行后运行通知。
After-throwing(抛出后) org.springframework.aop.ThrowsAdvice 只有在方法通过抛出异常而退出方法执行之后才能运行通知。
Arround(周围) org.aopaliance.intercept.MethodInterceptor 环绕通知,在目标方法完成前后做增强处理,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。
After(finally)(最终通知) org.springframework.aop.IntroductionInterceptor 在目标方法执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容

3、示例图

image

四、基于代理的AOP

1、概要

横切关注点:跨越应用程序多个模块的方法或功能。(软件系统,可以看做由一组关注点即业务或功能或方法组成。其中,直接的业务关注点是直切关注点,而为直切关注点服务的,就是横切关注点。)即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。

将增强类和拦截条件组合在一起,然后将这个切面配置到 ProxyFactory 中,从而生成代理。

3、导入包

需要引入spring-aop包,只需要引入spring-context自动会引入spring-aop

<dependency>
    <groupId>org.springframework</groupId>
   <artifactId>spring-context</artifactId>
  <version>5.1.4.RELEASE</version>
</dependency>

4、定义连接点

  1. 示例代码

    public interface UserDao {
        public void save(User user);
        public void testException() throws Exception;
    }
    

5、定义实现类

  1. 示例代码

    public class UserDaoImpl implements UserDao {
        @Override
        public void save(User user) {
            System.out.println("3.保存用户");
        }
        
        @Override
        public void testException() throws Exception {
            System.out.println("3.抛出异常");
            throw new Exception(" Exception happened!");
        }
    }
    

6、定义切面

  1. 示例代码

    import org.springframework.aop.MethodBeforeAdvice;
    /**
     * 前置通知
     */
    public class TestBeforeAdvice implements MethodBeforeAdvice {
        /**
         * @param method 被调用的方法
         * @param args 方法参数
         * @param target 代理对象实例
         * @throws Throwable
         */
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("1.前置通知");
        }
    }
    
    /**
     * 后置通知
     */
    import org.springframework.aop.AfterReturningAdvice;
    
    public class TestAfterAdvice implements AfterReturningAdvice {
        /**
         * @param returnValue 返回值
         * @param method      被调用的方法
         * @param args        方法参数
         * @param target      被代理对象
         * @throws Throwable
         */
        @Override
        public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
            System.out.println("5.后置通知");
        }
    }
    
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    /**
     * 环绕通知
     */
    public class TestSurroundAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            //前置横切逻辑
            //方法调用                     
            Object ret = invocation.proceed();
            //后置横切逻辑
            return ret;
        }
    }
    
    import org.springframework.aop.ThrowsAdvice;
    /**
     * 该接口上没有任何方法,但是实现了这个接口的类必须至少实现以下4个方法中的一个
     * 1.public void afterThrowing(Exception ex)
     * 2.public void afterThrowing(RemoteException)
     * 3.public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
     * 4.public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
     */
    public class TestThrowingAdvice implements ThrowsAdvice {
        public void afterThrowing(Exception ex) {
            System.out.println("5.抛出异常" + ex.getMessage());
        }
    
        public void afterThrowing(RemoteException re) {
            System.out.println("5.抛出异常" + re.getMessage());
        }
    
        /**
         * @param method 执行的方法
         * @param args   方法参数
         * @param target 代理的目标对象
         * @param ex     产生的异常
         */
        public void afterThrowing(Method method, Object[] args, Object target, Exception ex) {
            System.out.println("5.出错的方法是: " + method.getName());
            for (Object o : args) {
                System.out.println("5.方法的参数:   " + o.toString());
            }
            System.out.println("5.目标对象的名称: " + target.getClass().getSimpleName());
            System.out.println("5.错误的信息" + ex.getMessage());
        }
    //    public void afterThrowing(Method method, Object[] args, Object target, ServletException ex){
    //    }
    
    }
    

7、测试代码

  1. 示例代码

    public class TestBaseAdvice {
        public static void main(String[] args) {
            //实例化Spring代理工厂
            ProxyFactory factory = new ProxyFactory();
            //设置被代理的对象
            factory.setTarget(new UserDaoImpl());
            //添加通知,横切逻辑
            factory.addAdvice(new TestAfterAdvice());
            factory.addAdvice(new TestBeforeAdvice());
            factory.addAdvice(new TestSurroundAdvice());
            factory.addAdvice(new TestThrowingAdvice());
            //从代理工厂中获得代理对象
            UserDao dao = (UserDao) factory.getProxy();
            dao.save(new User());
            dao.testException();
        }
    }
    
    

8、使用IOC配置AOP

  1. 说明

    方法与类与上面相同

  2. 在resources下新建spring-aop-base.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:p="http://www.springframework.org/schema/p"
           xmlns:util="http://www.springframework.org/schema/util"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
     <!-- 注册被代理的目标对象 -->
        <bean id="target" class="com.wener.example.AOP.base.UserDaoImpl"/>
        <!-- 注册一组通知-->
        <bean id="afterAdvice" class="com.wener.example.AOP.base.TestAfterAdvice"/>
        <bean id="beforeAdvice" class="com.wener.example.AOP.base.TestBeforeAdvice"/>
        <bean id="surroundAdvice" class="com.wener.example.AOP.base.TestSurroundAdvice"/>
        <bean id="throwingAdvice" class="com.wener.example.AOP.base.TestThrowingAdvice"/>
        <!--注册代理对象 -->
        <!--interceptorNames 通知数组 可以是多个也可以是单个 -->
        <!--p:target-ref 被代理的对象-->
        <!--p:proxyTargetClass 被代理对象是否为类,如果是类必须设置true-->
        <bean id="proxy"
              class="org.springframework.AOP.framework.ProxyFactoryBean"
              p:interceptorNames-ref="advices"
              p:target-ref="target"
              p:proxyTargetClass="fasle">
        </bean>
        
        <util:list id="advices">
            <value>afterAdvice</value>
            <value>surroundAdvice</value>
            <value>surroundAdvice</value>
            <value>throwingAdvice</value>
        </util:list>
    </beans>
    
  3. 测试代码

    private static void testAop() {
            ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop-base.xml");
            UserDao dao = (UserDao) context.getBean("proxy");
         // 获取FactoryBean本身
            //ProxyFactoryBean bean = (ProxyFactoryBean) context.getBean("$proxy");
            dao.save(new User());
        }
    
  4. 注意

    • ProxyFactoryBean,就是一个bean对象,不要被前面的Factory扰乱误导,也是要放入BeanFactory被spring管理。

    • ProxyFactoryBean特殊在通过常规的ApplicationContext.getBean(bean Id) 获取的不是FactoryBean这个直接对象,而是调用FactoryBean.getObject()生成的对象,返回给你。

    • ApplicationContext.getBean(&bean Id) ,加上&才能取得FactoryBean这个对象。

    • FactoryBean这样的过程,就是为了方便你定义生成【复杂bean】对象,就是这个bean对象不是简单的new ,设置几个参数,有其他初始化才能完整被使用,比如ProxyFactoryBean。具体执行代码如下:

      ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop-base.xml");
      ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
      proxyFactoryBean.setBeanFactory(ac.getBeanFactory());
      //AOP拦截处理类
      proxyFactoryBean.setInterceptorNames("afterAdvice");
      //代理的接口
      proxyFactoryBean.setInterfaces(UserDao.class);
      //被代理对象
      proxyFactoryBean.setTarget(ac.getBean(UserDao.class));
      //放入bean工厂,实际开发是在config下使用注解,设置多个proxyFactoryBean代理,设置不同bean id
      ac.getBeanFactory().registerSingleton("proxy",proxyFactoryBean);
      UserDao userDao = (UserDao) ac.getBean("proxy");
      userDao.save();
      //获取直接的ProxyFactoryBean对象,加&
      System.out.println(ac.getBean("&proxy"));
      

五、纯POJO切面编程(了解)

1、说明

开发步骤

  1. 创建目标类:定义接口和和接口实现类(jdk动态代理)或者定义类(cglib)
  2. 定义通知
  3. 配置Spring IOC容器

2、示例代码

  1. 添加引用spring-aspects包

    <!--提供对AspectJ的支持 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.1.4.RELEASE</version>
    </dependency>
    
  2. 定义接口

    public interface PojoDao {
        public void test();
    }
    
  3. 定义接口实现类

    public class PojoDaoImpl implements PojoDao {
        @Override
        public void test() {
            System.out.println("核心测试方法");
        }
    }
    
  4. 定义切面类(横切处理类)

    package com.wener.example.aop.pojo;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    /**
     * 普通的java bean 该类不再需要实现任何接口或继承抽象类
     */
    public class PojoAspect {
    
        /**
         * 前置通知
         * @param jp
         */
        public void before(JoinPoint jp) {
            System.out.println("前置通知");
            System.out.println("方法名:" + jp.getSignature());
            System.out.println(",参数:" + jp.getArgs().length);
            System.out.println("代理对象:" + jp.getTarget());
        }
    
        /**
         * 后置通知
         * @param jp
         */
        public void after(JoinPoint jp) {
            System.out.println("后置通知");
        }
    
        /**
         * 返回值通知
         * @param joinPoint
         */
        public void afterReturning(JoinPoint joinPoint) {
            System.out.println(joinPoint);
        }
    
        /**
         * 抛出异常通知
         *
         * @param joinPoint
         */
        public void afterThrowing(JoinPoint joinPoint) {
         
        }
        /**
         * 环绕通知
         *
         * @param pjd ProceedingJoinPoint类型的参数可以决定是否执行目标方法。
         *            而且环绕通知必须有返回值,返回值即为目标方法的返回值
         * @return
         */
        public Object surround(ProceedingJoinPoint pjd) {
            return null;
        }
    
    }
    
    
  5. 在spring配置文件中配置

     aop:advisor,是有顺序的,必须放在aop:pointcut之后
     <?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">
        <!--
        注意
        1> 导入  xmlns:aop="http://www.springframework.org/schema/aop" 空间
        2> Spring AOP 配置都必须定义在 <aop:config>元素内部
        3> 在 <aop:config> 中,注册切面<aop:aspect> 注册切入点
        4> 声明切入点<aop:pointcut>
        5> 在<aop:aspect>中,配置5种通知类型
         <aop:after>
         <aop:around>
         <aop:after-returning>
         <aop:after-throwing>
         <aop:before>
        -->
        <!-- 注册被代理的目标对象 -->
        <bean id="pojoDao" class="com.wener.example.aop.pojo.PojoDaoImpl"/>
        <!--注册通知类-->
        <bean id="aspect" class="com.wener.example.aop.pojo.PojoAspect"/>
        <!-- AOP配置 -->
        <!--声明切面-->
        <aop:config>
            <!--声明切入点-->
            <aop:pointcut id="pointcut" expression="execution(* com.wener.example.aop.pojo.*.*(..))"/>
            <aop:aspect ref="aspect">
                <aop:after method="after" pointcut-ref="pointcut"/>
                <aop:around method="surround" pointcut-ref="pointcut"/>
                <aop:after-returning method="afterReturning" pointcut-ref="pointcut"/>
                <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut"/>
                <aop:before method="before" pointcut-ref="pointcut"/>
            </aop:aspect>
        </aop:config>
    </beans>
    
  6. 举例说明

    1>在spring配置文件中注册
    <bean id="sleepHelper" class="com.werner.webapp.aop.base.SleepHelper">
    </bean>
    2>第二步配置切入点
    Spring使用org.springframework.aop.support.JdkRegexpMethodPointcut来定义正则表达式切点
    1.使用正则表达式 
    2.使用AspectJ表达式
    <bean id="spleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
      <property name="pattern" value=".*sleep"/>
      //pattern属性指定了正则表达式,它匹配所有的sleep方法
    </bean>
    3>配置通知
    org.springframework.aop.support.DefaultPointcutAdvisor
    <bean id="sleepHelperAdvisor"        class="org.springframework.aop.support.DefaultPointcutAdvisor">
     <property name="advice" ref="sleepHelper"/>
     <property name="pointcut" ref="sleepPointcut"/>
     </bean>
    4>切点仅仅是定义了故事发生的地点,还有故事发生的时间以及最重要的故事的内容,就是通知了,我们需要把通知跟切点结合起来,我们要使用的通知者是:
    org.springframework.aop.support.DefaultPointcutAdvisor
       <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
            <property name="advice" ref="sleepHelper"/>
            <property name="pointcut" ref="sleepPointcut"/>
       </bean>
    5> 切入点和通知都配置完成,接下来该调用ProxyFactoryBean产生代理对象了
     <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="human"/>
            <property name="interceptorNames" value="sleepHelperAdvisor" />
            <property name="proxyInterfaces" value="test.spring.AOP.bean.Sleepable" />
    </bean>
     
    

六、基于@Aspect注解编程(重点)

1、说明

Spring 使用了和AspectJ 一样的注解并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)(编译器与织入器暂时不要管)

2、启用@AspectJ支持

  1. 说明

    为了在Spring中使用@AspectJ切面,你首先必须启用Spring对@AspectJ切面配置的支持,并确保开启自动代理。自动代理是指Spring会判断一个bean是否使用了一个或多个切面通知,并据此自动生成相应的代理以拦截其方法调用,并且确保通知在需要时执行

  2. 新建spring-aspect.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">
        <context:component-scan base-package="com.wener.example.aop.aspect"/>
     <!-- 有了这个Spring就能够自动扫描被@Aspect标注的切面了 -->
        <!-- 开启自动代理 -->    
        <aop:aspectj-autoproxy/>
    </beans>
    

2、声明一个切面

  1. 说明

    在代码中定义一个类任意在类上使用@Aspect注解

  2. 示例代码

    import org.aspectj.lang.annotation.Aspect;
    @Aspect
    public class LogAspect {
    }
    

3、声明一个切入点

  1. 说明

    切入点决定了连接点关注的内容,使得我们可以控制通知什么时候执行。Spring AOP只支持Spring bean的方法执行连接点。所以你可以把切入点看做是Spring bean上方法执行的匹配。一个切入点声明有两个部分:

    • 包含名字和任意参数的签名:一个切入点签名通过一个普通的方法定义来提供,并且切入点表达式使用@Pointcut注解来表示(作为切入点签名的方法必须返回void 类型)

    • 切入点表达式:切入点表达式决定了我们关注哪些方法的执行,详细表达式语法后面在说。

  2. 语法格式

    @Pointcut(value="", argNames = "")
    
  3. 参数说明

    • value

      指定切入点表达式

    • argNames

      指定命名切入点方法参数列表参数名字,可以有多个用“,”分隔,这些参数将传递给通知方法同名的参数

  4. 示例代码

    @Aspect
    public class LogAspect {
        // 也可以在通知上定义,当需要复用切入点的时候
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")  
        // 返回值 必须是void类型
        public void log() {
        }
    }
    
  5. 备注

    切入点的定义是非必要的,也可以直接在通知上使用切入点表达式

4、声明通知

4.1、说明

通知是跟一个切入点表达式关联起来的,并且在切入点匹配的方法执行之前或者之后或者前后运行。 切入点表达式可能是指向已命名的切入点的简单引用或者是一个已经声明过的切入点表达式,通知的类型就是我们前面提到过的类型

4.2、前置通知

  1. 说明

    在关注点执行前运行的方法,切面里使用 @Before 注解声明前置通知

  2. 语法格式

    @Before(value = "", argNames = "")
    
  3. 参数说明

    • value :指定切入点表达式或切入点名字;
    • argNames: 用来接收AspectJ表达式中的参数,并指定通知方法中的参数
  4. 示例代码

    import org.springframework.stereotype.Component;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    @Component
    public class LogAspect {
        /**
         * @Pointcut() 切入点表达式
         */
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")
        public void logPointcut() {
    
        }
        /**
         * @Before 前置通知
         * value:指定切入点表达式或命名切入点;
         * argNames:与Schema方式配置中的同义;
         */
        @Before("logPointcut()")
        public void before() {
            System.out.println("前置通知");
        }
    }
    

4.3、后置通知(最终通知)

  1. 说明

    不论一个方法是如何结束的,最终通知都会运行。使用@After 注解来声明。最终通知必须准备处理正常返回和异常返回两种情况。通常用它来释放资源。相当于异常处理里finally的代码

  2. 语法格式

    @After(value = "", argNames = "")
    
  3. 参数

    • value :指定切入点表达式或切入点名字;
    • **argNames: **用来接收AspectJ表达式中的参数,并指定通知方法中的参数
  4. 示例代码

    import org.springframework.stereotype.Component;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    @Component
    public class LogAspect {
        /**
         * @Pointcut() 切入点表达式
         */
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")
        public void logPointcut() {
    
        }
        /**
         * @After 后置通知 
         */
        @After(value = "logPointcut()")
        public void after() {
            System.out.println("后置通知");
        }
    }
    

4.4、返回通知

  1. 说明

    返回后通知通常在一个匹配的方法返回的时候执行。使用 @AfterReturning 注解来声明

  2. 语法格式

    @AfterReturning(value="",pointcut="",returning="",argNames="")
    
  3. 参数说明

    • value:指定切入点表达式或切入点名字;

    • pointcut:指定切入点表达式或命名切入点,如果指定了将覆盖value属性的,pointcut具有高优先级;

    • returning:如果你想获取方法的返回值可以使用该参数,在通知方法中定义参数就可以了

    • argNames:用来接收AspectJ表达式中的参数,并指定通知方法中的参数

  4. 示例代码

    import org.springframework.stereotype.Component;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    @Aspect
    @Component
    public class LogAspect {
        /**
         * @Pointcut() 切入点表达式
         */
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")
        public void logPointcut() {
    
        }
     /**
      * 不获取方法的返回值
         */
        @AfterReturning(value = "logPointcut()")
        public void AfterReturning1() {
            System.out.println("异常通知");
        }
        /**
         * 获取方法的返回值
         * returning的赋值的名字,必须跟通知方法中参数的名字保持一致
         */
        @AfterReturning(value = "logPointcut()", returning = "val")
        public Object afterReturning(Object val) {
            System.out.println("返回后通知");
            return val;
        }
    
    }
    

4.5、异常通知

  1. 说明

    抛出异常通知在一个方法抛出异常后执行。使用@AfterThrowing注解来声明

  2. 语法格式

    @AfterThrowing(value="",pointcut="",throwing="",argNames="")
    
  3. 参数说明

    • value:指定切入点表达式或命名切入点;
    • pointcut:指定切入点表达式或命名切入点,如果指定了将覆盖value属性的,pointcut具有高优先级;
    • throwing:异常类型;并且在通知方法中定义异常参数;
    • argNames:用来接收AspectJ表达式中的参数,并指定通知方法中的参数;
  4. 示例代码

    import org.springframework.stereotype.Component;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    @Aspect
    @Component
    public class LogAspect {
        /**
         * @Pointcut() 切入点表达式
         */
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")
        public void logPointcut() {
         
        }
        /**
         * @AfterThrowing 异常通知 
         *   value:指定切入点表达式或命名切入点;
         *   throwing:异常类型。
         */
        @AfterThrowing("logPointcut()")
        public void afterThrowing() {
            System.out.println("异常通知");
        }
        /**
         * 如果想要限制通知只在某种特定的异常被抛出的时候匹配,同时还想知道异常的一些信息。 
         * 那我们就需要使用throwing属性声明响应
         */
     @AfterThrowing(value = "logPointcut()", throwing = "exception")
        public void afterThrowing(Exception exception) {
            System.out.println("异常通知");
        }
    }
    

4.6、环绕通知

  1. 说明

    环绕通知在一个方法执行之前和之后执行。它使得通知有机会 在一个方法执行之前和执行之后运行。而且它可以决定这个方法在什么时候执行,如何执行,甚至是否执行。 环绕通知经常在某线程安全的环境下,你需要在一个方法执行之前和之后共享某种状态的时候使用。 请尽量使用最简单的满足你需求的通知。(比如如果简单的前置通知也可以适用的情况下不要使用环绕通知)。

    • 使用@Around注解;
    • 环绕通知需要携带ProceedingJoinPoint类型的参数;
    • 且环绕通知必须有返回值,返回值即为有目标方法的返回值。
  2. 语法格式

    @Around(value = "", argNames = "")
    
  3. 参数

    • value :指定切入点表达式或切入点名字;
    • **argNames: **用来接收AspectJ表达式中的参数,并指定通知方法中的参数
  4. 示例代码

    import org.springframework.stereotype.Component;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    @Aspect
    @Component
    public class LogAspect {
        /**
         * @Pointcut() 切入点表达式
         */
        @Pointcut("execution(* com.wener.example.aop.aspect.*.*(..))")
        public void logPointcut() {
    
        }
        /**
         * @Around 环绕通知
         * 比如 缓存切面,如果缓存中有值,就返回该值,否则调用proceed()方法
         * value:指定切入点表达式或命名切入点;
         * 注意 第一个参数必须是 ProceedingJoinPoint对象 具体这个类的更多详细使用看附录:
         */
        @Around(value = "logPointcut()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("环绕通知1");
            Object obj = pjp.proceed();
            System.out.println("环绕通知2");
            return obj;
        }
    }
    

4.7、通知参数

  1. 说明

    若想要在通知方法获取被通知方法的参数共有两种方式:自动获取、手动指定

    • 自动获取参数:通知类型可以通过参数JoinPoint或者 ProceedingJoinPoint 自动获取被通知方法的参数值并调用该方法

    • 手动指定参数:即在配置切面时,需在切面的通知与切面的切点中明确指定参数。

  2. 手动指定

    • 在@pointcut中切入表达式中使用args声明匹配的参数,注意使用&&连接args

    • 在@pointcut中切入表达式中使用参数argNames用来接收AspectJ表达式中的参数,

      argNames属性是用于指定在表达式中应用的参数名与Advice方法参数是如何对应的

    • 在通知方法中定义参数

  3. 手动获取指定参数

    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    @Aspect
    @Component
    public class LogAdviceParamsAspect {
     // 注意参数的个数必须一致,否则匹配不到
        @Before(value = "execution(* com.wener.example.aop.aspect.*.*(..))&& args(id,name)", argNames = "id,name")
        public void testArgs(Object id, Object name) {
            System.out.println(id);
            System.out.println(name);
        }
    }
    
  4. 混用使用

    当同时采用自动获取参数与手动指定参数时,自动获取参数必须是第一个参数,即ProceedingJoinPoint 等参数并需是通知方法定义的第一个参数

    import org.aopalliance.intercept.Joinpoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    public class LogAdviceParamsAspect {
      // args、argNames的参数名与testArgs()方法中参数名 保持一致
        @Before(value = "execution(* com.wener.example.aop.aspect.*.*(..))&& args(id,name)", argNames = "id,name")
        public void testArgs(Object id, Object name) {
            System.out.println(id);
            System.out.println(name);
        }
     // 也可以不用argNames
        @Before(value = "execution(* com.wener.example.aop.aspect.*.*(..))&& args(id,name)")
        public void testArgs(Object id, Object name) {
            System.out.println(id);
            System.out.println(name);
        }
        
        @Around(value = "execution(* com.wener.example.aop.aspect.*.*(..))&&(args(id,name,..))", argNames = "pjp,id,name")
        public Object testAroundArgs(ProceedingJoinPoint pjp, Object id, Object name) throws Throwable {
            System.out.println("Around之前");
            Object obj = pjp.proceed();
            System.out.println();
            return obj;
        }
    }
    

4.8 、引入

  1. 说明

    有时候有一组共享公共行为类。在OOP中,它们必须扩展相同的基类或者实现相同的接口。此外,Java的单继承机制仅允许一个类最多扩展一个基类。所以,不能同时从多个实现类中继承行为。

    解决方案:引入是AOP中的一种特殊的通知。它允许为一个接口提供实现类,使对象动态的实现接口。就像对象在运行时扩展了实现类。而且,可以用多个实现类将多个接口同时引入对象。这可以实现与多重继承相同的效果。

  2. 在开发中用的不是很多,所以不做过多的分析

5、声明代理类

  1. 说明

    被代理的对象,跟前面说的一样,代理接口或者类都可以

  2. 示例代码

    public interface AspectDao {
        public void test();
        public void testParams(int id, String name);
        public void testParams(Joinpoint jp, int id, String name);
    }
    
    @Component("aspectDao")
    public class AspectDaoImpl implements AspectDao {
        @Override
        public void test() {
            System.out.println("核心测试方法");
        }
        @Override
        public void testParams(int id, String name) {
            System.out.println("带参数的方法:" + "ID:" + id + "name:" + name);
        }
    }
    

6、测试

  1. 示例代码

    ApplicationContext context = new ClassPathXmlApplicationContext("spring-aspect.xml");
    AspectDao dao = (AspectDao) context.getBean("aspectDao");
    dao.test();
    dao.testParams(1,"hello");
    

7、总结

  1. 使用@Aspect将POJO声明为切面;
  2. 在切面类中使用@Pointcut进行命名切入点声明;
  3. 定义通知方法,使用5中注解声明,其中value用于定义切入点表达式或引用命名切入点;
  4. 配置文件需要使用<aop:aspectj-autoproxy/>来开启注解风格的@AspectJ支持;
  5. 将切面类和POJO类注册到Spring容器中

执行流程

AOP定义了一个切面(Aspect),一个切面包含了切入点,通知,引入,这个切面上定义了许多的切入点(Pointcut),一旦访问过程中有对象的方法跟切入点匹配那么就会被AOP拦截。此时该对象就是目标对象(Target Object)而匹配的方法就是连接点(Join Point)。紧接着AOP会用过JDK动态代理或者CGLIB生成一个目标对象的代理对象(AOP proxy),这个过程就是织入(Weaving)。这个时候我们就可以按照我们的需求对连接点进行一些拦截处理。可以看到,我们可以引入(Introduction)一个新的接口,让代理对象来实现这个接口来,以实现额外的方法和字段。也可以在连接点上进行通知(Advice),通知的类型包括了前置通知,返回后通知,抛出异常后通知,后置通知,环绕通知。最后也是最骚的是整个过程不会改变代码原有的逻辑

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容