spring第三天---动态代理与AOP

  • 时间:
  • 来源:互联网
  • 文章标签:

Spring第三天—动态代理与AOP

文章目录

    • Spring第三天---动态代理与AOP
      • 1.1 转账方法演示事务问题
      • 1.2 使用事务管理工具类保证事务一致性,并配置spring的ioc
      • 1.3 代理!!!
        • 1.3.1 基于接口的动态代理
        • 1.3.2 基于子类的动态代理
        • 1.3.3 基于动态代理实现事务控制
      • 1.4 Spring中AOP术语
      • 1.5 spring中基于xml的AOP配置步骤
        • 1.5.1 bean.xml引用约束xmlns:aop
        • 1.5.2 把通知Bean也交给spring来管理
        • 1.5.3 使用aop:config标签表明开始AOP的配置
        • 1.5.4 使用aop:aspect 标签表明配置切面
        • 1.5.5 在aop:aspect标签的内部使用对应标签来配置通知的类型
        • 1.5.6 进一步改造
          • 1.5.6.1其他通知类型:
          • ==1.5.6.2环绕通知==
      • 1.6 Spring基于注解的AOP配置

1.1 转账方法演示事务问题

先是拿转账的案例,举了一下异常时的不合理。


public void transfer(String sourceName, String targetName, Float money) {
        //1. 根据名称查询转出账户
        Account source = accountDao.findAccountByName(sourceName);
        //2. 根据名称查询转入账户
        Account target = accountDao.findAccountByName(targetName);
        //3. 转出账户减钱
        source.setMoney(source.getMoney() - money);
        //4. 转入账户加钱
        target.setMoney(target.getMoney() + money);
        //5. 更新转出账户
        accountDao.updateAccount(source);

        int i = 1/0;

        //6. 更新转入账户
        accountDao.updateAccount(target);
    }

由于初始代码执行一次数据库就获取一个Connection,导致每个Connection的事务都是独立的,多个事务。会出现有的事务成功了,有的事务失败了。(转出扣款成功、转入加钱失败)

  • 解决方法
    ThreadLocal
    每个线程绑定一个Connection + 手动commit 保证事务一致性。
    编写事务管理工具类和ConnectionUtils。

/**
 * @author Ven
 * @version 1.0 2020/12/9
 *
 * 连接的工具类,它用于从数据源中获取一个连接,并且实现和线程的绑定
 */
public class ConnectionUtils {

    private  ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    private DataSource dataSource;

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * 获取当前线程上的连接
     * @return
     */
    public Connection getThreadConnection(){

        try {
            //1. 先从ThreadLocal上获取
            Connection conn = tl.get();
            //2.判断当前线程上是否有连接
            if(conn == null){
                //3.从数据源中获取一个连接,并且存入ThreadLocal中
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回当前线程上的连接
            return conn;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /**
     * 把连接和线程解绑
     */
    public void removeConnection(){

        tl.remove();
    }
}

线程解绑


/**
* 释放连接--事务管理工具类中
*/
public void release(){
    try {

        connectionUtils.getThreadConnection().close(); // 还回连接池中,会有个问题就是服务时线程到时候也是还回线程池中,
        // 那么下次获取这个线程时,该线程绑定了数据库连接,但数据库连接被关闭了,不能用了,可能会有问题。
        connectionUtils.removeConnection();   //多一句这个,解绑
    } catch (Exception e) {
        e.printStackTrace();
    }

}

1.2 使用事务管理工具类保证事务一致性,并配置spring的ioc

注意:bean.xml中dataSource不再配置在QueryRunner下、dao层同步修改使用ConnectionUtils与数据库交互,改为都由ConnectionUtils注入。
类成员提供set方法,以便spring注入。bean.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置Service-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
        <!-- 注入dao-->
        <property name="accountDao" ref="accountDao"></property>
        <!--注入事务管理器-->
        <property name="txManager" ref="txManager"></property>
    </bean>
    <!--配置Dao对象-->
    <bean id="accountDao" class="com.itheima.dao.impl.AccoutDaoImpl">
        <!--注入QueryRunner-->
        <property name="runner" ref="runner"></property>
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>

    <!--配置QueryRunner-->
    <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
    </bean>

    <!--配置数据源-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--连接数据库的必备信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/eesy"></property>
        <property name="user" value="root"></property>
        <property name="password" value="root"></property>

    </bean>
    <!--    配置Connection的工具类 ConnectionUtils-->
    <bean id="connectionUtils" class="com.itheima.utils.ConnectionUtils">
        <!--注入数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事务管理器-->
    <bean id="txManager" class="com.itheima.utils.TranscationManager">
        <!--注入ConnectionUtils-->
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

1.3 代理!!!

1.2 虽然实现了事务的一致性,但每个方法中都引用了事务工具类中的开启事务、提交事务、回滚事务、释放连接方法。 造成了方法之间的耦合,导致如果事务工具类修改下方法名,那都得相应改变。

以买电脑为例,原来消费者可以直接从厂家买,厂家直接提供货、售后。
但多了后,发展成了 中间有代理商的模式。 代理商和厂家有约定,代理利润是 销售价的20%等等,有一定的约束。 然后消费者从代理商买电脑。

  • 动态代理:
    • 特点: 字节码随用随创建,随用随加载
    • 作用: 不修改源码的基础上对方法增强
    • 分类:
      • 基于接口的动态代理
      • 基于子类的动态代理

1.3.1 基于接口的动态代理

  • 基于接口的动态代理
  • 涉及的类:Proxy
  • 提供者:JDK官方
  • 如何创建代理对象:
    使用Proxy类中的newProxyInstance方法
  • 创建代理对象的要求:
    被代理类最少实现一个接口,如果没有则不能使用
  • newProxyInstance方法的参数:
    • ClassLoader:类加载器
      它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法 : ***.getClass().getClassLoader()
    • Class[]:字节码数组
      它是用于让代理对象和被代理对象有相同方法。固定写法: ***.getClass().getInterfaces()
    • InvocationHandler:用于提供增加的代码
      它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
      此接口的实现类都是谁用谁写。

下面代码很重要!!!!


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        //匿名内部类访问外部成员变量时,外部成员要求是 最终的 final的。
        //producer相当于厂家,被代理对象。 厂家需遵循一些约定(producer实现了IProducer接口)
        final Producer producer = new Producer();
        /**
         * 动态代理:
         *    特点: 字节码随用随创建,随用随加载
         *    作用: 不修改源码的基础上对方法增强
         *    分类:
         *          基于接口的动态代理
         *          基于子类的动态代理
         *     基于接口的动态代理
         *          涉及的类:Proxy
         *          提供者:JDK官方
         *     如何创建代理对象:
         *          使用Proxy类中的newProxyInstance方法
         *     创建代理对象的要求:
         *          被代理类最少实现一个接口,如果没有则不能使用
         *      newProxyInstance方法的参数:
         *          ClassLoader:类加载器
         *              它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法 : ***.getClass().getClassLoader()
         *          Class[]:字节码数组
         *              它是用于让代理对象和被代理对象有相同方法。固定写法。 ***.getClass().getInterfaces()
         *          InvocationHandler:用于提供增加的代码
         *              它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *              此接口的实现类都是谁用谁写。
         *
        */

        IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
            /**
             * 作用: 执行被代理对象的任何接口方法都会经过该方法,相当于拦截
             * 方法参数的含义
             * @param proxy  代理对象的引用
             * @param method  当前执行的方法
             * @param args    当前执行方法所需的参数
             * @return        和被代理对象方法有相同的返回值
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //提供增强的代码
                Object returnValue = null;
                //1.获取方法执行的参数
                Float money = (Float) args[0];
                //2.判断当前方法是不是销售
                if ("saleProduct".equals(method.getName())){
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });

        proxyProducer.saleProduct(10000f);
    }
}

1.3.2 基于子类的动态代理

注:此时Producer不是IProducer接口的实现类

  • 基于子类的动态代理
  • 涉及的类:Enhancer
  • 提供者:第三方cglib库
  • 如何创建代理对象:
    使用Enhancer类中的create方法
  • 创建代理对象的要求:
    被代理类不能是最终类 ,因为如果是最终类,就不能再创建子类了,也就不能创建代理对象了
  • create方法的参数:
    • Class:字节码
      它是用于被代理对象的字节码;
    • Callback:用于提供增加的代码
      它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
      此接口的实现类都是谁用谁写。
      我们一般写的都是该接口的子接口实现类

下面代码很重要!!!!


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 模拟一个消费者
 */
public class Client {

    public static void main(String[] args) {
        
        final Producer producer = new Producer();
        /**
         * 动态代理:
         *    特点: 字节码随用随创建,随用随加载
         *    作用: 不修改源码的基础上对方法增强
         *    分类:
         *          基于接口的动态代理
         *          基于子类的动态代理
         *     基于子类的动态代理
         *          涉及的类:Enhancer
         *          提供者:第三方cglib库
         *     如何创建代理对象:
         *          使用Enhancer类中的create方法
         *     创建代理对象的要求:
         *          被代理类不能是最终类 ,因为如果是最终类,就不能再创建子类了,也就不能创建代理对象了
         *      create方法的参数:
         *          Class:字节码
         *              它是用于被代理对象的字节码;
         *
         *          Callback:用于提供增加的代码
         *              它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
         *              此接口的实现类都是谁用谁写。
         *              我们一般写的都是该接口的子接口实现类: MethodInterceptor
         *
        */

        Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {

            /**
             * 执行此被代理对象的任何方法都会经过该方法
             * @param proxy
             * @param method
             * @param args
             *      以上三个参数和基于接口的动态代理中invoke方法的参数时一样的。
             * @param methodProxy : 当前执行方法的代理对象 //很少用到
             * @return
             * @throws Throwable
             */
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnValue = null;
                //1.获取方法执行的参数
                Float money = (Float) args[0];
                //2.判断当前方法是不是销售
                if ("saleProduct".equals(method.getName())){
                    returnValue = method.invoke(producer, money*0.8f);
                }
                return returnValue;
            }
        });

        cglibProducer.saleProduct(12000f);

    }
}

1.3.3 基于动态代理实现事务控制

AccountServiceImpl 里剥离了开启事务、提交事务等操作方法和事务管理器对象,只有最初始的dao层相应操作。事务控制交由BeanFactory中返回的accountService动态代理实现。

BeanFactory动态代理中:
//2. 执行操作
rtValue = method.invoke(accountService, args);
//以及需要加个final
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}


/**
 * @author Ven
 * @version 1.0 2020/12/9
 * 用于创建service的代理对象的工厂
 */
public class BeanFactory {

    private IAccountService accountService;

    private TranscationManager txManager;

    public void setTxManager(TranscationManager txManager) {
        this.txManager = txManager;
    }

    //需要加个final
    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 获取Service代理对象
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {

            /**
             * 添加事务的支持
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object rtValue = null;
                try {
                    //1. 开启事务
                    txManager.beginTransaction();
                    //2. 执行操作
                    rtValue = method.invoke(accountService, args);
                    //3.提交事务
                    txManager.commit();
                    //4.返回结果
                    return rtValue;
                } catch (Exception e) {
                    //5.回滚操作
                    txManager.rollback();
                    throw new RuntimeException(e);
                } finally {
                    //6. 释放连接
                    txManager.release();
                }
            }
        });
    }
}

同时bean.xml ioc也要相应改变,注意代理service对象创建方式


<!--配置代理的service-->
<bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

<!--配置beanfactory-->
<bean id="beanFactory" class="com.itheima.factory.BeanFactory">
    <!--注入service-->
    <property name="accountService" ref="accountService"></property>
    <!--注入事务管理器-->
    <property name="txManager" ref="txManager"></property>
</bean>

然后测试类AccountServiceTest,Qualifier指明使用动态代理返回的对象。


@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;

1.4 Spring中AOP术语

  • Joinpoint(连接点): 被拦截到的方法.

    • 理解:业务层方法全是连接点,连接业务和动态代理增强的方法的点。增强只能通过这些方法。
  • Pointcut(切入点): 我们对其进行增强的方法.

举例 IAccountService.java 接口 和 BeanFactory.java (创建service的代理对象的工厂):


 /**
     * 删除
     * @param accountId
     */
    void deleteAccount(Integer accountId);

    /**
     * 转账
     * @param sourceName   转出账户名称
     * @param targetName   转入账户名称
     * @param money        转账金额
     */
    void transfer(String sourceName, String targetName, Float money);

    void test();    //它是连接点 Joinpint,因为在接口,业务层方法全是连接点,连接业务和动态代理增强的方法的点。  增强只能通过这些方法
    //切入点: 增强的方法才是,test不是切入点,只是连接点。
    //所有的切入点Pointcut,都是连接点。  但不是所有的连接点都是切入点。 只有被增强的才是切入点

/**
     * 获取Service代理对象
     * @return
     */
    public IAccountService getAccountService() {
        return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() {

            /**
             * 添加事务的支持
             * @param proxy
             * @param method
             * @param args
             * @return
             * @throws Throwable
             */
             //整个的invoke方法在执行就是环绕通知
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                //这个方法没有被增强,不是切入点
                if("test".equals(method.getName())){
                    return method.invoke(accountService, args);
                }

                Object rtValue = null;
                try {
                    //1. 开启事务
                    txManager.beginTransaction();  //--->前置通知
                    //2. 执行操作
                    rtValue = method.invoke(accountService, args);  //--->环绕通知中有明确的切入点方法调用
                    //3.提交事务
                    txManager.commit();  //--->后置通知
                    //4.返回结果
                    return rtValue;
                } catch (Exception e) {
                    //5.回滚操作
                    txManager.rollback();  //--->异常通知
                    throw new RuntimeException(e);
                } finally {
                    //6. 释放连接
                    txManager.release();  //--->最终通知
                }
            }
        });
    }

  • Advice(通知/增强): 对切入点进行的增强操作
    包括前置通知,后置通知,异常通知,最终通知,环绕通知
    以切入点方法为划分依据。

-Target(目标对象):
代理的目标对象,即被代理对象。(accountService)

  • Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。

    • 动态代理技术创建一个新的对象,返回一个代理对象,返回代理对象过程中从中增强方法实现事务支持,这个加入事务支持过程,叫织入。
  • Proxy(代理):
    一个类被AOP织入增强后,就产生一个结果代理类。

    • 即代理对象 : (IAccountService) Proxy
  • Aspect(切面): 是切入点和通知(引介)的结合

    • 建立切入点方法和通知方法在执行调用的对应关系,叫切面。
    • 如果配置方式,即 得有事务相关的方法、accountService某些方法
      配置说明哪个service、哪些方法、然后哪些方法在accountService方法前先执行、后执行,这叫切面。

运行阶段(Spring 框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对
象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

1.5 spring中基于xml的AOP配置步骤

  • bean.xml文件中

1.5.1 bean.xml引用约束xmlns: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">
</beans>

1.5.2 把通知Bean也交给spring来管理


<!--配置Logger类-->
<bean id="logger" class="com.itheima.utils.Logger"></bean>

1.5.3 使用aop:config标签表明开始AOP的配置

1.5.4 使用aop:aspect 标签表明配置切面

  • id属性:是给切面提供一个唯一标识
  • ref属性:是指定通知类bean的id

1.5.5 在aop:aspect标签的内部使用对应标签来配置通知的类型

我们现在示例是让printLog方法在切入点方法执行之前执行,所以是前置通知

  • aop:before:表示配置前置通知

    • method属性:用于指定Logger类中哪个方法是前置通知
    • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
  • 切入点表达式的写法:

    • 关键字:execution(表达式)
    • 表达式:
      访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
      • 标准的表达式写法:
        public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
      • 访问修饰符可以省略
        void com.itheima.service.impl.AccountServiceImpl.saveAccount()
      • 返回值可以使用通配符表示任意返回值 此写法不会改其他,因为方法名写死了
        * com.itheima.service.impl.AccountServiceImpl.saveAccount()
      • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个
        * *.*.*.*.AccountServiceImpl.saveAccount()
      • 包名可以使用…表示当前包及其子包
        * *..AccountServiceImpl.saveAccount()
      • 类名和方法名都可以使用*来实现通配 此时只有两个有增强,因为update有参数,不适配
        * *..*.*()
      • 参数列表:
        可以直接写数据类型;
        • 基本类型直接写名称 int
        • 引用类型写包名.类名的方式 java.lang.String
        • 可以使用通配符表示任意类型,但是必须有参数
        • 可以使用…表示有无参数均可,有参数可以是任意类型
          全统配写法: 不暂同,不然所有方法都增强了
          * *..*.*(..)
        • 实际开发中切入点表达式的通常写法:
          切到业务层实现类下的所有方法
          * com.itheima.service.impl.*.*(..)

具体示例,重要!!:


<?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,把service对象配置进来-->
    <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

    <!--spring中基于xml的AOP配置步骤
        1、把通知Bean也交给spring来管理
        2、使用aop:config标签表明开始AOP的配置
        3、使用aop:aspect标签表明配置切面
            id属性:是给切面提供一个唯一标识
            ref属性:是指定通知类bean的id
        4、在aop:aspect标签的内部使用对应标签来配置通知的类型
                我们现在示例是让printLog方法在切入点方法执行之前执行,所以是前置通知
                aop:before:表示配置前置通知
                    method属性:用于指定Logger类中哪个方法是前置通知
                    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

           切入点表达式的写法:
                关键字:execution(表达式)
                表达式:
                    访问修饰符  返回值  包名.包名.包名...类名.方法名(参数列表)
                标准的表达式写法:
                    public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
                访问修饰符可以省略
                    void com.itheima.service.impl.AccountServiceImpl.saveAccount()
                返回值可以使用通配符表示任意返回值  此写法不会改其他,因为方法名写死了
                    * com.itheima.service.impl.AccountServiceImpl.saveAccount()
                包名可以使用通配符,表示任意包。但是有几级包,就需要写几个
                    * *.*.*.*.AccountServiceImpl.saveAccount()
                包名可以使用..表示当前包及其子包
                    * *..AccountServiceImpl.saveAccount()
                类名和方法名都可以使用*来实现通配   此时只有两个有增强,因为update有参数,不适配
                    * *..*.*()
                参数列表:
                    可以直接写数据类型;
                        基本类型直接写名称  int
                        引用类型写包名.类名的方式   java.lang.String
                    可以使用通配符表示任意类型,但是必须有参数
                    可以使用..表示有无参数均可,有参数可以是任意类型
                全统配写法:    不暂同,不然所有方法都增强了
                * *..*.*(..)

                实际开发中切入点表达式的通常写法:
                    切到业务层实现类下的所有方法
                        * com.itheima.service.impl.*.*(..)

    -->

    <!--配置Logger类-->
    <bean id="logger" class="com.itheima.utils.Logger"></bean>

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


</beans>

1.5.6 进一步改造

前置通知<aop:before>

1.5.6.1其他通知类型:
  • 后置通知(在切入点方法正常执行之后执行 它和异常通知永远只能执行一个)
    <aop:after-returning>

  • 异常通知(在切入点方法产生异常之后执行 它和后置通知永远只能执行一个)
    <aop:after-throwing>

  • 最终通知(无论切入点方法是否正常执行它都会在其后面执行)
    <aop:after>

  • 配置环绕通知(spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式)
    <aop:around>

同时,切入点表达式也可以提炼:
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/> 此标签可写在aop:aspect标签内部(只能当前切面使用),也可以写在aspect外面,此时就所有切面可用。不过写外面时一定要写在切面前面。

具体示例:


<!--配置AOP-->
<aop:config>

    <!--配置切入点表达式:id属性用于指定表达式的唯一标识。 expression属性用于指定表达式内容
            此标签写在aop:aspect标签内部只能当前切面使用
            它还可以写在aop:aspect外面,此时就变成了所有切面可用。  注意要按约束来,必须配置在切面之前,之后会报错。
        -->
    <aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"/>
    <!--配置切面-->
    <aop:aspect id="logAdvice" ref="logger">
        <!--配置前置通知:在切入点方法执行之前执行
        <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>-->

        <!--配置环绕通知 详细的注释请看Logger类中-->
        <aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>

    </aop:aspect>
</aop:config>

1.5.6.2环绕通知

<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around> 如果只配置这个,那么会发现有问题,切入点方法还没执行,而通知方法执行了。需手动指定切入点方法调用。

  • Spring框架为我们提供了一个接口:ProceedingJoinPoint。 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法
    该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供该接口的实现类供我们使用。

Object[] args = pjp.getArgs(); //得到方法执行所需的参数
Object rtValue = pjp.proceed(args); //明确调用业务层方法(切入点方法)


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

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

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

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

        return rtValue;
    } catch (Throwable t) {   //必须是 Throwable ,Exception不行 拦不住

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

}

1.6 Spring基于注解的AOP配置

  • 1.增加bean.xml中关于context的约束

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

  • 2.配置spring创建容器时要扫描的包

  • 3.相应类加上注解

    • @Pointcut("execution(* com.itheima.service.impl.*.*(..))")
      private void pt1(){} 切入点表达式
    • @Before("pt1()") 前置通知
    • @AfterReturning("pt1()") 后置通知
    • @AfterThrowing("pt1()") 异常通知
    • @After("pt1()") 最终通知
    • @Around("pt1()") 环绕通知

@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方法开始记录日志了。。。");
    }

    /**
     * 环绕通知
     * spring中的环绕通知:
     *      它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
     *
     */
    @Around("pt1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp){
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs();  //得到方法执行所需的参数

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

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

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

            return rtValue;
        } catch (Throwable t) {   //必须是 Throwable ,Exception不行 拦不住

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

    }

  • 4.配置spring开启注解AOP的支持
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

  • 5.不采用环绕通知,用spring注解的话,最终通知和后置通知会存在 顺序执行的bug,不建议。尽量用环绕通知,手动控制执行顺序

本文链接http://www.taodudu.cc/news/show-1944626.html