飞道的博客

【Spring】AOP

398人阅读  评论(0)

动态代理

    /**动态代理
     * 特点: 字节码随用随创建,随用随加载
     * 作用:在不修改代码的情况下对方法进行增强
     * 分类:
     *      基于接口的动态代理
     *      基于子类的动态代理
     * 基于接口的动态代理:
     *      涉及到的类:Proxy
     *      提供者:JDK官方
     * 如何创建代理对象:
     *      使用Proxy类当中的newProxyInstance方法
     * 创建代理对象的要求:
     *      被代理类最少实现一个接口,如果没有则不能使用
     * newProxyInstance方法的参数:
     *      ClassLoader:加载代理对象的字节码,和被代理对象使用相同的类加载器,固定写法
     *      Class[]:用于让代理对象和被代理对象具有相同的方法,固定写法
     *      InvocationHandler:用于提供增强的代码
     */

实现一个有接口的类的动态代理

厂家接口:

/**
 * 生产厂家
 * @author liwenlong
 * @data 2020/5/17
 */
public interface IProducer {
    public void saleProduct(double money);

    public void afterService(double money);
}

厂家实现:

/**
 * 一个生产厂家
 * @author liwenlong
 * @data 2020/5/17
 */
public class Producer implements IProducer{
    //销售
    public void saleProduct(double money){
        System.out.println("销售产品,并拿到"+money+"元。");
    }
    //售后
    public void afterService(double money){
        System.out.println("提供售后服务,并拿到"+money+"元。");
    }
}

客户:

/**
 * 一个消费者
 * @author liwenlong
 * @data 2020/5/17
 */
public class Client {
    public static void main(String[] args) {
        final Producer producer = new Producer();
        //动态代理
        IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(), new InvocationHandler() {
                    /**
                     * 作用:任何被代理类对象的接口中的方法都会经过invoke方法
                     * @param proxy     代理对象的引用
                     * @param method    当前执行的方法
                     * @param args      当前执行方法需要的参数
                     * @return          和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //提供增强的代码
                Object returnVal = null;
                //1.获取方法执行的参数
                Double money = (Double) args[0];
                //2.判断当前执行的是哪个方法
                if(method.getName().equals("saleProduct")){
                    returnVal = method.invoke(producer,money*0.8);
                }
                return returnVal;
            }
        });
        proxyProducer.saleProduct(1000.0);
    }
}

打印结果:
销售产品,并拿到800.0元。

实现基于子类的动态代理

    /**动态代理
     * 特点: 字节码随用随创建,随用随加载
     * 作用:在不修改代码的情况下对方法进行增强
     * 分类:
     *      基于接口的动态代理
     *      基于子类的动态代理
     * 基于子类的动态代理:
     * 涉及到的类:Enhancer
     *      提供者:第三方cglib库
     * 如何创建代理对象:
     *      使用Enhancer类当中的create方法
     * 创建代理对象的要求:
     *      被代理类不能是最终类(final)
     * create方法的参数:
     *      class:用于指定被代理对象的字节码
     *      callback:用于提供增强的代码,一般写该接口的子接口实现类:MethodInterceptor
     */

需要先在maven中导入jar包:

    <dependencies>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2.2</version>
        </dependency>
    </dependencies>

产品类不再实现接口:

public class Producer2 {
    //销售
    public void saleProduct(double money){
        System.out.println("销售产品,并拿到"+money+"元。");
    }
    //售后
    public void afterService(double money){
        System.out.println("提供售后服务,并拿到"+money+"元。");
    }
}

代理类:

public class Client2 {
    public static void main(String[] args) {
        final Producer2 producer = new Producer2();
        //动态代理
        Producer2 cglibProducer = (Producer2)Enhancer.create(producer.getClass(), new MethodInterceptor() {
            /**
             * 执行任何方法都会经过这里
             * @param o
             * @param method
             * @param objects
             * 以上三个参数与机遇接口的invocation方法的参数一样
             * @param methodProxy :当前执行方法的代理对象
             * @return
             * @throws Throwable
             */
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                //提供增强的代码
                Object returnVal = null;
                //1.获取方法执行的参数
                Double money = (Double) objects[0];
                //2.判断当前执行的是哪个方法
                if(method.getName().equals("saleProduct")){
                    returnVal = method.invoke(producer,money*0.8);
                }
                return returnVal;
            }
        });
        cglibProducer.saleProduct(1200.0);
    }
}

打印结果:
销售产品,并拿到960.0元。

使用动态代理解决上篇中臃肿的代码

将Service层改回原来的代码:

public class AccountServiceImpl implements IAccountService {

    private IAccountDao accountDao;

    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    public List<Account> findAllAccount() {
        return  accountDao.findAllAccount();
    }

    public Account findAccountById(Integer id) {
        return  accountDao.findAccountById(id);
    }

    public void insertAccount(Account account) {
        accountDao.insertAccount(account);
    }

    public void updateAccount(Account account) {
        accountDao.updateAccount(account);
    }

    public void deleteAccount(Integer id) {
        accountDao.deleteAccount(id);
    }

    public void transfer(String sourceName, String targetName, Double money) {

        //根据名称查询转出账户
        Account sourceAccount = accountDao.findAccountByName(sourceName);
        //根据名称查询转入账户
        Account targetAccount = accountDao.findAccountByName(targetName);
        //转出账户减额
        sourceAccount.setMoney(sourceAccount.getMoney() - money);
        //转入账户增额
        targetAccount.setMoney(targetAccount.getMoney() + money);
        //更新账户
        accountDao.updateAccount(sourceAccount);
        accountDao.updateAccount(targetAccount);
        
    }
}

新建一个BeanFactory类对IAccountService进行动态代理,将事务的处理的代理写入到动态代理:

public class BeanFactory {
    private IAccountService accountService;

    private TransactionManager transactionManager;

    public void setTransactionManager(TransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public final void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    //获取代理对象
    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 returnVal = null;
                        try {
                            //开启事务
                            transactionManager.beginTransaction();
                            //执行任务
                            returnVal = method.invoke(accountService, args);
                            //提交事务
                            transactionManager.commit();
                            //返回结果
                            return returnVal;
                        } catch (Exception e) {
                            //回滚操作
                            transactionManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //释放连接
                            transactionManager.release();
                        }
                    }
                });
    }
}

然后对原来的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="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean>

    <!--配置BeanFactory-->
    <bean id="beanFactory" class="com.lwl.factory.BeanFactory">
        <property name="transactionManager" ref="transactionManager"></property>
        <property name="accountService" ref="accountService"></property>
    </bean>

    <!--配置Service-->
    <bean id="accountService" class="com.lwl.service.impl.AccountServiceImpl">
        <!--注入Dao对象-->
        <property name="accountDao" ref="accountDao"></property>
    </bean>

    <!--配置Dao对象-->
    <bean id="accountDao" class="com.lwl.dao.impl.AccountDaoImpl">
        <!--注入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/test"></property>
        <property name="user" value="root"></property>
        <property name="password" value="1022"></property>
    </bean>

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

    <!--配置事务管理器-->
    <bean id="transactionManager" class="com.lwl.utils.TransactionManager">
        <property name="connectionUtils" ref="connectionUtils"></property>
    </bean>
</beans>

改造完成。

AOP

相关概念

AOP是Spring框架面向切面的编程思想,AOP采用一种称为“横切”的技术,将涉及多业务流程的通用功能抽取并单独封装,形成独立的切面,在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

Spring框架的AOP机制可以让开发者把业务流程中的通用功能抽取出来,单独编写功能代码。在业务流程执行过程中,Spring框架会根据业务流程要求,自动把独立编写的功能代码切入到流程的合适位置。

例如,在一个业务系统中,用户登录是基础功能,凡是涉及到用户的业务流程都要求用户进行系统登录。如果把用户登录功能代码写入到每个业务流程中,会造成代码冗余,维护也非常麻烦,当需要修改用户登录功能时,就需要修改每个业务流程的用户登录代码,这种处理方式显然是不可取的。比较好的做法是把用户登录功能抽取出来,形成独立的模块,当业务流程需要用户登录时,系统自动把登录功能切入到业务流程中。

作用:在程序运行期间,不修改源码对已有方法进行增强。

AOP的实现方式: 动态代理

相关术语

● Join point

表示连接点。指那些被拦截到的点,也就是业务流程在运行过程中需要插入切面的具体位置。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。例如,前面案例的Service层中的所有方法都是连接点。

● Pointcut

表示切入点。用于定义通知应该切入到哪些连接点上,不同的通知通常需要切入到不同的连接点上。例如,前面的案例中在BeanFactory中增加一个判断,只有对指定的方法进行动态代理的增强,切入点就是被增强的方法。

● Advice

表示通知。是切面的具体实现方法。所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)五种。哪种通知是以method.invoke为界限。

● Target

表示目标对象,也就是被代理对象。被一个或者多个切面所通知的对象。例如,前面案例的accountService对象。

● Proxy

表示代理对象。将通知应用到目标对象之后被动态创建的代理对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。

● Weaving

表示切入,也称为织入。将切面(增强)应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期。

● Aspect

表示切面,是切入点和通知的结合。

Spring基于xml的AOP

案例:有一个模拟的用户类,可以实现增删改查操作(模拟实现)。现在想要通过AOP对该类进行增强,记录日志。
创建IAccountService:

/**
 * 账户的业务外层接口
 * @author liwenlong
 * @data 2020/5/17
 */
public interface IAccountService {
    //模拟保存账户
    void saveAccount();
    //模拟更新账户
    void updateAccount(int i);
    //模拟删除账户
    int deleteAccount();
}

创建实现类:

/**
 * 账户的业务层实现类
 * @author liwenlong
 * @data 2020/5/17
 */
public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("执行了保存");
    }

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

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

创建记录日志的类(模拟):

/**
 * 用于记录日志的工具类,他提供了公共的代码
 * @author liwenlong
 * @data 2020/5/17
 */
public class Logger {
    //用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层的方法)
    public void printLog(){
        System.out.println("Logger中的printLog的方法开始记录了");
    }
}

Spring中基于xml的AOP配置步骤:

   1.把通知的Bean也交给Spring进行管理
   2.使用aop:config标签表明开始AOP的配置
   3.使用aop:aspect标签表明配置切面
        id属性:给切面提供唯一标志
        ref属性:通知类bean的id
   4.在aop:aspect标签的内部使用对应的标签来配置通知的类型
            method属性:用于指定Logger类中哪个方法是前置通知。
            pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中的哪些方法进行增强

        切入点表达式的写法:
            关键字:execution(表达式)
            表达式:访问修饰符 返回值 包名.包名...包名.方法名(参数列表)
            例如:public void com.lwl.service.impl.AccountServiceImpl.updateAccount(int i)
            		访问修饰符可以省略:void com.lwl.service.impl.AccountServiceImpl.updateAccount(int i)
            		返回值可以使用通配符(表示任意返回类型):* com.lwl.service.impl.AccountServiceImpl.updateAccount(int i)
            		包名可以用通配符(表示任意包)。有几级的包就写几个*.  
            		包名可以使用两个点表示当前包及其子包:..
            		参数类别可以写数据类型 
             实际开发中切入点表达式的通常写法:
             	切到业务层实现类下的所有方法:* com.lwl.service.impl.*.*(..)

配置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"
       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.lwl.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方法再切入点方法执行之前执行,所以是前置通知
                method属性:用于指定Logger类中哪个方法是前置通知。
                pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中的哪些方法进行增强

            切入点表达式的写法:
                关键字:execution(表达式)
                表达式:访问修饰符 返回值 包名.包名...包名.方法名(参数列表)
                例如:public void com.lwl.service.impl.AccountServiceImpl.updateAccount(int i)
    -->

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

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

</beans>

进行测试:

public class AOPTest {
    @Test
    public void aopTest(){
        //获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //获取对象
        IAccountService accountService = (IAccountService)ac.getBean("accountService");
        //执行方法
        accountService.saveAccount();
    }
}

输出结果:
Logger中的printLog的方法开始记录了
执行了保存

这就是Spring替我们实现了动态代理,增强了AccountService中的saveAccount()方法。上面bean.xml中只对saveAccount()方法进行了增强,那么如果想要把AccountService中所有的方法都进行增强,那么可以将切入点表达式写成:

* com.lwl.service.impl.*.*(..)

进一步,可以配置前置、后置、异常和最终通知:

public class Logger {
    //用于打印日志,计划让其在切入点方法执行之前执行(切入点方法就是业务层的方法)
    public void beforePrintLog() {
        System.out.println("前置通知中的beforePrintLog的方法开始记录了");
    }

    public void afterReturningPrintLog() {
        System.out.println("后置通知中的afterReturningPrintLog的方法开始记录了");
    }

    public void afterThrowPrintLog() {
        System.out.println("异常通知中的afterThrowPrintLog的方法开始记录了");
    }

    public void afterPrintLog() {
        System.out.println("最终通知afterPrintLog的方法开始记录了");
    }
}

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"
       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.lwl.service.impl.AccountServiceImpl"></bean>

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

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知,在切入点之前执行-->
            <aop:before method="beforePrintLog" pointcut="execution(* com.lwl.service.impl.*.*(..))"></aop:before>
            <!--配置后置通知,在切入点正常执行之后执行。和异常通知永远只能执行一个-->
            <aop:after-returning method="afterReturningPrintLog" pointcut="execution(* com.lwl.service.impl.*.*(..))"></aop:after-returning>
            <!--配置异常通知,在切入点异常执行之后执行。-->
            <aop:after-throwing method="afterThrowPrintLog" pointcut="execution(* com.lwl.service.impl..*.*(..))"></aop:after-throwing>
            <!--配置最终通知,无论如何都会在其他后面执行-->
            <aop:after method="afterPrintLog" pointcut="execution(* com.lwl.service.impl.*.*(..))"></aop:after>

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

</beans>

进行测试:

public class AOPTest {
    @Test
    public void aopTest(){
        //获取容器
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //获取对象
        IAccountService accountService = (IAccountService)ac.getBean("accountService");
        //执行方法
        accountService.saveAccount();
    }
}

运行结果:
前置通知中的beforePrintLog的方法开始记录了
执行了保存
后置通知中的afterReturningPrintLog的方法开始记录了
最终通知afterPrintLog的方法开始记录了

还有一个标签:配置切入点表达式,就不用像上面那样重复的复制粘贴切入点表达式了:

    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <!--配置前置通知,在切入点之前执行-->
            <aop:before method="beforePrintLog" pointcut-ref="pt"></aop:before>
            <!--配置后置通知,在切入点正常执行之后执行。和异常通知永远只能执行一个-->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt"></aop:after-returning>
            <!--配置异常通知,在切入点异常执行之后执行。-->
            <aop:after-throwing method="afterThrowPrintLog" pointcut-ref="pt"></aop:after-throwing>
            <!--配置最终通知,无论如何都会在其他后面执行-->
            <aop:after method="afterPrintLog" pointcut-ref="pt"></aop:after>

            <aop:pointcut id="pt" expression="execution(* com.lwl.service.impl.*.*(..))"></aop:pointcut>
        </aop:aspect>
    </aop:config>

上面的通知中没有环绕通知:

<?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.lwl.service.impl.AccountServiceImpl"></bean>
    <!--配置Logger类-->
    <bean id="logger" class="com.lwl.utils.Logger"></bean>
    <!--配置AOP-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="logAdvice" ref="logger">
            <aop:pointcut id="pt" expression="execution(* com.lwl.service.impl.*.*(..))"></aop:pointcut>
            <!--配置环绕通知-->
            <aop:around method="aroundPrintLog" pointcut-ref="pt"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

增加环绕通知方法

    //当配置了环绕通知以后,
    public void aroundPrintLog(){
        System.out.println("环绕通知");
    }

问题:
当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
分析:
通过对比动态代理的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用.而我们的没有
解决:
Spring框架为我们提供了一个接口:proceedingJoinPoint .该接口有一个方法proceed()此方法就相当于明确调用切入点方法.
该接口可以作为环绕通知的方法参数,在程序执行时,Spring框架会为我们提供该接口的实现类供我们使用

public Object aroundPrintLog(ProceedingJoinPoint joinPoint){
        Object returnVal = null;
        try {
            Object[] args = joinPoint.getArgs();//得到方法运行需要的参数
            beforePrintLog(); //这行代码写在proceed之前就代表前置通知
            joinPoint.proceed();//明确调用业务层方法
            afterReturningPrintLog();//这行代码写在proceed之后就代表前置通知
        } catch (Throwable t) {
            afterThrowPrintLog();//这行代码写在异常里就代表异常通知
            t.printStackTrace();
        }finally {
            afterPrintLog();//这行代码写在finally里就代表最终通知
        }
        return returnVal;
}
Spring中的环绕通知:
它是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式

参考:详解Spring框架的AOP机制


转载:https://blog.csdn.net/zzu_seu/article/details/106176409
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场