Spring之AOP编程

时间:2023-03-08 20:16:57
Spring之AOP编程
一、AOP简介
    AOP的英文全称是Aspect Oriented Programming,意为:面向切面编程。
    AOP采取横向抽取的机制,取代了传统纵向继承体系的代码复用。AOP常用于:事务管理,性能监视,安全检查,缓存,日志等。
    Spring AOP使用纯Java实现,在程序运行期间通过代理的方式向目标类织入增强代码。
    AspectJ是一个基于Java语言的AOP框架,Spring在2.0版本引入了对Aspect的支持,AspectJ扩展了Java语言,提供了一个专门的编译器,在编译期间提供横向代码的织入。
二、spring-aop的实现原理
    spring-aop的底层采用代理机制进行实现。
    如果目标类实现了接口,spring默认采用jdk的动态代理;
    如果目标类没有实现接口,spring默认采用cglib字节码增强。不过,我们可以声明强制使用cglib的代理方式。
三、AOP的术语【重要】
    1. target(目标类),需要被代理的类。我们将会用:UserService + UserServiceImpl作为目标类。
    2. joinPoint(连接点),连接点是指那些可能被拦截到的方法。例如:目标类中的所有方法
    3. pointCut(切入点),已经被增强的连接点。例如:目标类中的addUser()方法。切入点是连接点的一个子集。
    4. advice(通知 / 增强),增强代码。例如:切面类中的before、after方法。
    5. weaving(织入),指把通知(advice)应用到目标对象(target)来创建新的代理对象(proxy)的过程。可知,织入是一个过程。
    6. proxy(代理类)
    7. aspect(切面),切面是指切入点(pointcut)和通知(advice)的结合。
四、用JDK的动态代理实现AOP
    JDK的动态代理是对装饰者设计模式的简化。使用的前提是目标类必须实现接口。
    用JDK动态代理有三个主要模块需要实现:
    1. 目标类:接口+实现类(UserService + UserServiceImpl)
    2. 切面类:切面类用于存放通知(MyAspect)
    3. 工厂类:工厂类用来生成代理对象
    4. 测试类
    下面给出一个JDK动态代理实现AOP的例子
    1. 目标类(接口+实现类)
package cn.african.service;
public interface UserService {
    public void addUser();
    public void updateUser();
    public void deleteUser();
}

package cn.african.service;
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("addUser...");
    }
    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }
    @Override
    public void deleteUser() {
        System.out.println("deleteUser...");
    }
}

    2. 切面类

package cn.african.aspect;
public class MyAspect {
    public void before() {
        System.out.println("before");
    }
    public void after() {
        System.out.println("after");
    }
}

    3. 工厂类

package cn.african.factory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import cn.african.aspect.MyAspect;
import cn.african.service.UserService;
import cn.african.service.UserServiceImpl;

public class MyFactoryBean {

    public static UserService createService() {
        // 1. 目标对象
        UserService userService = new UserServiceImpl();
        // 2. 切面对象
        MyAspect myAspect = new MyAspect();
        // 3. 代理对象
        UserService proxyService = (UserService) Proxy.newProxyInstance(
                MyFactoryBean.class.getClassLoader(),
                userService.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //前置通知
                        myAspect.before();
                        //执行目标类的方法
                        Object object = method.invoke(userService, args);
                        //后置通知
                        myAspect.after();
                        return object;
                    }
                });
        //4. 返回代理对象
        return proxyService;
    }
}
    我们重点分析工厂类中代理对象的生成:
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,

InvocationHandler h)

            参数1:loader,类加载器。因为动态代理是在运行时创建代理对象,而任何类都需要类加载器将其加载到内存,所以我们要给将来运行期间产生的这个代理类提供一个类加载器。   一般情况下,使用  当前类.class.getClassLoader(),或者  目标实例.getClass().getClassLoader();其实他们俩是一个ClassLoader。
            参数2:interfaces,代理类需要实现的所有接口。有两个方式来提供这些接口:
                        方式1:目标实例.getClass().getInterfaces();但是这种方式只能获得自己的接口,不能获得父类的接口。
                        方式2:new Class[ ] { UserService.class };这种方式可以获得父类的接口
            参数3:InvocationHandler,处理类,这是JDK反射包中提供的接口,里面只有一个invoke方法,代理类的每一个方法执行时,都将调用一次invoke方法。InvocationHandler需要实现,一般采用匿名内部类来实现。
                        我们在分析这个invoke方法,方法的格式为:public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
                        参数3.1:proxy,代理对象
                        参数3.2:method:代理对象当前要执行的方法
                        参数3.3:args:要执行的方法的入参
    4. 测试类
package cn.african.test;

import org.junit.Test;

import cn.african.factory.MyFactoryBean;
import cn.african.service.UserService;

public class TestJdkProxy {

    @Test
    public void testJdkProxy() {
        UserService userService = MyFactoryBean.createService();
        userService.addUser();
        System.out.println("--------------");
        userService.updateUser();
        System.out.println("--------------");
        userService.deleteUser();
    }
}
 打印结果:
Spring之AOP编程
五、用CGLIB实现AOP 
    如果目标类没有实现接口,就无法使用JDK的动态代理来生成代理类,这时候我们采用纵向继承机制来实现目标类的代码增强,这就是cglib要完成的工作。
    cglib代理是在程序运行期间,创建目标类的子类,在子类中对目标类的方法进行增强。
    cglib有两个jar包依赖:cglib.jar和asm.jar,但这两个jar都已经被整合在spring-core.jar中了。
    下面我们手动实现一个cglib代理:
    1. 目标类
package cn.african.service;
public class UserServiceImpl {
    public void addUser() {
        System.out.println("addUser...");
    }
    public void updateUser() {
        System.out.println("updateUser...");
    }
    public void deleteUser() {
        System.out.println("deleteUser...");
    }
}

    2. 切面类

package cn.african.aspect;
public class MyAspect {
    public void before() {
        System.out.println("before");
    }

    public void after() {
        System.out.println("after");
    }
}
    3. 工厂类                  
    采用cglib,底层将创建目标类的子类
package cn.african.factory;

import java.lang.reflect.Method;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import cn.african.aspect.MyAspect;
import cn.african.service.UserService;
import cn.african.service.UserServiceImpl;

public class CglibBeanFactory {

    public static UserService createService() {
        // 目标类
        UserServiceImpl userService = new UserServiceImpl();
        // 切面类
        MyAspect myAspect = new MyAspect();
        // 1. 核心类
        Enhancer enhancer = new Enhancer();
        // 2. 设置父类
        enhancer.setSuperclass(UserServiceImpl.class);
        // 3. 设置回调函数
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)
                    throws Throwable {
                myAspect.before();
                Object object = method.invoke(userService, args);
                myAspect.after();
                return object;
            }
        });
        // 4. 创建代理
        UserServiceImpl proxyService = (UserServiceImpl) enhancer.create();
        return proxyService;
    }
}
    我们解释一下这个回调函数:
    MethodInterceptor接口类似于JDK中的InvocationHandler接口,而intercept方法类似于InvocationHandler接口中的invoke方法。
    前三个参数和invoke方法中的三个参数是一样的。
    第四个参数methodProxy是方法代理,可以通过它执行代理类的父类,也就是执行目标类。
    Object object = method.invoke(userService, args) 可以替换成 methodProxy.invokeSuper(proxy, args),两个效果是一样的。
    4. 测试类
package cn.african.test;

import org.junit.Test;

import cn.african.factory.CglibBeanFactory;
import cn.african.service.UserService;

public class TestCglibProxy {

    @Test
    public void demo01() {
        UserService userService = CglibBeanFactory.createService();
        userService.addUser();
        System.out.println("--------------");
        userService.updateUser();
        System.out.println("--------------");
        userService.deleteUser();
    }
}
打印结果:
Spring之AOP编程
六、半自动代理实现AOP
    我们之所以称之为半自动代理,是因为Spring自动创建了代理对象,但是却需要我们手动去容器中获取这个代理对象。
    在实现这个半自动的demo之前,我们首先介绍一下aop联盟(aopalliance)定义的一套通知(advice)接口规范。
    音乐博士Rod Johnson童鞋在aop联盟的规范里定义了一个空的通知接口Advice:
package org.aopalliance.aop;
/**
 * Tag interface for Advice. Implementations can be any type
 * of advice, such as Interceptors.
 * @author Rod Johnson
 * @version $Id: Advice.java,v 1.1 2004/03/19 17:02:16 johnsonr Exp $
 */
public interface Advice {}
    Spring按照通知在目标类方法上连接点的位置,把通知又分为以下五大接口规范:
    1. 前置通知:org.springframework.aop.MethodBeforeAdvice
    2. 后置通知:org.springframework.aop.AfterReturningAdvice
    3. 环绕通知:org.aopalliance.intercept.MethodInterceptor
        注意环绕通知并不是定义在spring中,而是在aop联盟的规范中。同时也要注意在cglib代理中设置回调函数的时候new了一个org.springframework.cglib.proxy.MethodInterceptor的匿名实现,这是两个不同的MethodInterceptor,注意区分。环绕通知是我们的重点。
    4. 异常抛出通知:org.springframework.aop.ThrowsAdvice
    5. 引介通知:org.springframework.aop.IntroductionInterceptor
        引介通知用来在目标类中添加一些新的属性和方法,但是很少用。
    我们先模拟一下环绕通知的流程。注意,环绕通知必须手动执行目标方法。
try{
    //1. 前置通知
    //2. 执行目标方法
    //3. 后置通知
}catch{
    //4. 抛出异常通知
}
    1,2,3如果有任何一处抛出异常,都会停止后面代码的执行,进入catch块。
    所以,如果在前置通知中抛出异常,可以阻止目标方法的执行。
    后置通知是在目标方法执行后执行,所以它可以获得目标方法的返回值。
    下面我们来实现一个半自动代理的demo:
    1. 目标类
package cn.african.service;
public interface UserService {
    void addUser();
    void updateUser();
    void deleteUser();
}

package cn.african.service;
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("addUser...");
    }
    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }
    @Override
    public void deleteUser() {
        System.out.println("deleteUser...");
    }
}
    2. 切面类
        切面类中装有通知,我们环绕通知为例,所以这个类需要实现环绕通知的接口org.aopalliance.intercept.MethodInterceptor。同时要注意到,环绕通知必须手动执行目标方法。
package cn.african.aspect;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class MyAspect implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 前置通知
        System.out.println("before");
        // 手动执行目标方法,这是一个放行方法
        Object object = invocation.proceed();
        // 后置通知
        System.out.println("after");
        return object;
    }
}

    3. bean-aop.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-4.3.xsd">

    <!-- 1. 目标类 -->
    <bean id="userService" class="cn.african.service.UserServiceImpl"></bean>

    <!-- 2. 切面类 -->
    <bean id="myAspect" class="cn.african.aspect.MyAspect"></bean>

    <!-- 3. 代理类 -->
    <bean id="proxyService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="cn.african.service.UserService"></property>
        <property name="target" ref="userService"></property>
        <property name="interceptorNames" value="myAspect"></property>
        <!-- <property name="optimize" value="true"></property> -->    <!--如果声明optimize=true,无论是否有接口,都采用cglib代理-->
    </bean>
</beans>
    我们分下一下代理类的配置:
    org.springframework.aop.framework.ProxyFactoryBean是FactoryBean的一个实现,它是一个代理类的工厂Bean,专门用来生成代理对象,底层会调用ProxyFactoryBean中的getObject()方法来返回这个代理对象。
    interfaces:确定接口
    target:确定目标类
    interceptorNames:切面类实例的Bean ID,但是这里使用value,却没有使用ref,这是因为setInterceptorNames接收的参数是个String的可可变参数,本质上是一个String[ ]。
    optimize:当值为true时,强制使用cglib代理;默认值为false。
    4. 测试类
package cn.african.test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.african.service.UserService;

public class TestAop {

    @Test
    public void demo01() throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans-aop.xml");
        UserService userService = context.getBean("proxyService", UserService.class);
        userService.addUser();
        System.out.println("----------------");
        userService.updateUser();
        System.out.println("----------------");
        userService.deleteUser();
        context.getClass().getMethod("close").invoke(context);
    }
}

打印结果:

Spring之AOP编程

七、Spring AOP编程
    从Spring容器中获取目标对象,如果配置了AOP,Spring将自动生成代理对象并返回。
    想要确定目标类,得使用AspectJ的切入点表达式。关于AspectJ我们将在另一篇文章中详细介绍。
    下面是一个AOP编程的demo:
    1. 目标类
package cn.african.service;
public interface UserService {
    public void addUser();
    public void updateUser();
    public void deleteUser();
}

package cn.african.service;
public class UserServiceImpl implements UserService {
    @Override
    public void addUser() {
        System.out.println("addUser...");
    }
    @Override
    public void updateUser() {
        System.out.println("updateUser...");
    }
    @Override
    public void deleteUser() {
        System.out.println("deleteUser...");
    }
}
    2. 切面类
    我们使用环绕通知,环绕通知要手动执行目标方法。
package cn.african.aspect;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class MyAspect implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 前置通知
        System.out.println("before");
        // 手动执行目标方法,这是一个放行方法
        Object object = invocation.proceed();
        // 后置通知
        System.out.println("after");
        return object;
    }
}

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

    <!-- 1. 目标类 -->
    <bean id="userService" class="cn.african.service.UserServiceImpl"></bean>

    <!-- 2. 切面类 -->
    <bean id="myAspect" class="cn.african.aspect.MyAspect"></bean>

    <!-- 3. AOP配置 -->
    <aop:config>
        <aop:pointcut expression="execution(* cn.african.service.UserServiceImpl.*(..))" id="myPointcut"/>
        <aop:advisor advice-ref="myAspect" pointcut-ref="myPointcut"/>
    </aop:config>
</beans>
    下面解释一下这段AOP的配置:
    1. 使用<aop:config>必须要导入aop的命名空间。
    2. aop编程就是在<aop:config>之间进行配置:
            <aop:pointcut>:切入点,从目标对象中获取具体的方法
            <aop:advisor>:这个标签描述一个特殊的切面,这个特殊的切面是一个通知和一个切入点的结合。
                        advice-ref:通知的引用
                        pointcut-ref:切入点的引用
                        注意advisor和advice的区别!!!
    3. 切入点表达式
            execution(* cn.african.service.UserServiceImpl.*(..))
            execution:按照官方的解释是: for matching method execution join points, this is the primary pointcut designator(指示符) you will use when working with Spring AOP。
             execution(     *      cn.african.service.  UserServiceImpl. *        (..)  )
                            返回类型任意    包名              类名           方法任意   参数任意
 
    4. 测试类
package cn.african.test;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.african.service.UserService;

public class TestAop {

    @Test
    public void demo01() throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans-aop.xml");
        UserService userService = context.getBean("userService", UserService.class);
        userService.addUser();
        System.out.println("----------------");
        userService.updateUser();
        System.out.println("----------------");
        userService.deleteUser();
        context.getClass().getMethod("close").invoke(context);
    }
}

打印结果:

Spring之AOP编程

Spring之AOP编程
Spring之AOP编程