Spring--AOP

AOP

概述

AOP(Aspect Oriented Programming):面向切面编程

面向切面编程,指在程序运行期间,将某段代码动态地切入到指定方法的指定位置进行运行的编程方式

常见场景:日志记录,编写一个计算器,需要在目标方法开始,返回,出现异常,结束时记录信息

第一种方法:直接编写在方法内部

缺点:维护修改困难,核心功能和辅助功能耦合

第二种方法:使用动态代理

public class CalculatorProxy {

    public static Calculator getProxy(Calculator calculator){

        ClassLoader classLoader = calculator.getClass().getClassLoader();
        Class<?>[] interfaces = calculator.getClass().getInterfaces();
        //方法执行器,用于执行目标方法
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /*
                Object proxy:代理对象,提供给JDK使用
                Method method:当前将要执行的目标对象的方法
                Object[] args:目标对象的方法执行所需要的参数
                 */
                Object result = null;
                try {
                    LogUtils.logStart(method,args);
                    result = method.invoke(calculator, args);
                    LogUtils.logReturn(method,result);
                } catch (Exception e) {
                    LogUtils.logException(method,e);
                } finally {
                    LogUtils.logEnd(method);
                }
                return result;
            }

        };

        //如果目标对象没有实现任何接口,即interfaces为空,则无法创建代理对象
        Object proxy = Proxy.newProxyInstance(classLoader, interfaces, handler);
        return (Calculator) proxy;

    }
}

缺点:编写困难,且jdk的动态代理,若目标对象没有实现任何接口,则无法创建代理对象

第三种方法:利用Spring框架实现AOP功能

AOP专业术语

img

步骤

  1. 导包

    commons-logging-1.1.1.jar
    spring-aop-5.2.6.RELEASE.jar
    spring-beans-5.2.6.RELEASE.jar
    spring-context-5.2.6.RELEASE.jar
    spring-core-5.2.6.RELEASE.jar
    spring-expression-5.2.6.RELEASE.jar

    Spring支持面向切面编程的包是

    spring-aspects-5.2.6.RELEASE.jar

    扩展功能包

    com.springsource.net.sf.cglib-2.2.0.jar
    com.springsource.org.aopalliance-1.0.0.jar
    com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar

  2. 写配置

    • 将目标类和切面类(封装了通知方法)加入到ioc容器中
    • 切面类添加注解@Aspect
    • 添加通知方法注解
    /*
    五个通知注解
    @Before:前置通知,在目标方法调用之前运行
    @After:后置通知,在目标方法结束之后运行
    @AfterReturning:返回通知,在目标方法正常返回之后运行
    @AfterThrowing:异常通知,在目标方法抛出异常之后通知
    @Around:环绕通知,在目标方法前后执行
    
    try{
        @Before
        method.invoke(obj,args);
        @AfterReturning
    }catch(e){
        @AfterThrowing
    }finally{
        @After
    }
     */
    
    @Aspect
    @Component
    public class LogUtils {
    
        //在方法开始之前执行
        @Before(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
        public static void logStart(){
            System.out.println("方法开始执行");
        }
    
        //在方法正常返回之后执行
        @AfterReturning(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
        public static void logReturn(){
            System.out.println("方法执行完成");
        }
    
        //在方法出现异常时执行
        @AfterThrowing(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
        public static void logException(){
            System.out.println("方法出现异常");
        }
    
        //在方法最终结束后运行
        @After(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
        public static void logEnd(){
            System.out.println("方法最终执行完成");
        }
    }
    
  3. 测试

@Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");

        Calculator bean = context.getBean(Calculator.class);

        bean.add(1,2);
    }

细节点

  1. IOC容器中保存的是组件的代理对象
@Test
    public void test4(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");

        //从ioc容器中获取目标对象,一定要使用接口类型,AOP底层使用动态代理,创建的是目标对象的代理对象
//        Calculator bean = context.getBean(Calculator.class);

        //没有接口即使用本类型获取
        MyMathCalculator bean = (MyMathCalculator) context.getBean("myMathCalculator");
        bean.add(1,2);
        System.out.println(bean);//com.th1024.bean.impl.MyMathCalculator@53fb3dab
        System.out.println(bean.getClass());//com.th1024.bean.impl.MyMathCalculator$$EnhancerBySpringCGLIB$$97cc1b67,CGLIB创建的代理对象
    }

补充:CGLIB可以为没有实现接口的对象创建代理对象

  1. 切入点表达式的写法,通配符
/*
切入点表达式
固定写法:execution(访问权限符 返回值类型 方法全类名(参数列表))
其中访问权限符可省略不写,默认为public
通配符:
    *:
        1. 匹配一个或多个字符:execution(public int com.th1024.bean.impl.MyMath*r.*(..))
        2. 匹配任意一个参数:execution(public int com.th1024.bean.impl.MyMath*r.*(int,*)),第二个参数可为任意类型
        3. 匹配一层路径
        4. 匹配所有返回值类型
    ..:
        1. 匹配任意多个参数,任意类型参数
        2. 匹配任意多层路径:execution(public int com.th1024..MyMath*r.*(int,*))

最精确的:execution(public int com.th1024.bean.impl.MyMathCalculator.add(int,int))
最模糊的:execution( * *.*(..))
 */
  1. 通知方法的执行顺序
/*
    通知方法执行顺序
    正常执行:@Before(前置通知)-->@After(后置通知)-->@AfterReturning(正常返回)
    异常执行:@Before(前置通知)-->@After(后置通知)-->@AfterThrowing(抛出异常)
     */
    @Test
    public void test5(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");

        MyMathCalculator bean = (MyMathCalculator) context.getBean("myMathCalculator");
        bean.add(1,2);
        System.out.println("=============");
//        bean.div(2,1);
//        bean.div(1,0);
    }
  1. JoinPoint获取目标方法的信息
@Before(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
public static void logStart(JoinPoint joinPoint){
    //获取到目标方法运行时使用的参数
    Object[] args = joinPoint.getArgs();
    //获取到目标方法签名
    Signature signature = joinPoint.getSignature();
    System.out.println("logUtils:【"+signature.getName()+"】方法开始执行,参数列表为"+ Arrays.asList(args));
}
  1. @AfterReturning、@AfterThrowing设置参数接收返回值和异常
//returning="result",设置result接收方法返回值
@AfterReturning(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))",returning = "result")
public static void logReturn(JoinPoint joinPoint,Object result){
    System.out.println("logUtils:【"+joinPoint.getSignature().getName()+"】方法执行完成,返回结果为"+ result);
}

//throwing="e",设置e接收方法抛出的异常
@AfterThrowing(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))",throwing = "e")
public static void logException(JoinPoint joinPoint,Exception e){
    System.out.println("logUtils:【"+joinPoint.getSignature().getName()+"】方法出现异常,异常信息为"+ e);
}

Spring对通知方法的要求为参数一定要正确,不然无法识别为通知方法或者无法正确接收值

  1. 抽取可重用的切入点表达式
//抽取相同的切入点
@Pointcut(value = "execution(public int com.th1024.bean.impl.MyMathCalculator.*(..))")
public void pointDemo() {}

@Before(value = "pointDemo()")
    public static void logStart(JoinPoint joinPoint){
        //获取到目标方法运行时使用的参数
        Object[] args = joinPoint.getArgs();
        //获取到目标方法签名
        Signature signature = joinPoint.getSignature();
        System.out.println("logUtils:【"+signature.getName()+"】方法开始执行,参数列表为"+ Arrays.asList(args));
    }
  1. 环绕通知
/*
 @Around:环绕通知,是Spring中强大的通知方法
 类似动态代理,集合了其它四种通知的功能

 环绕通知优先于普通通知执行,顺序:

 普通前置
 {
    环绕前置
    目标方法执行
    环绕方法返回/异常
    环绕后置
 }
 普通后置
 普通方法返回/异常

 环绕前置-->普通前置-->环绕方法返回/异常-->环绕后置-->普通后置-->普通返回
 */

@Around(value = "pointDemo()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Object[] args = proceedingJoinPoint.getArgs();
    String name = proceedingJoinPoint.getSignature().getName();
    Object proceed = null;

    try {
        //@Before()
        System.out.println("环绕前置:【"+name+"】方法开始执行,参数列表为"+ Arrays.asList(args));
        //利用反射调用目标方法--method.invoke(obj,args)
        proceed = proceedingJoinPoint.proceed(args);
        //@AfterReturning
        System.out.println("环绕返回:【"+name+"】方法执行完成,返回结果为" + proceed);
    } catch (Exception e) {
        //@AfterThrowing
        System.out.println("环绕异常:【"+name+"】方法出现异常,异常信息为" + e);
        //为避免其他通知方法接收不到异常信息,将异常抛出
        throw new RuntimeException(e);
    } finally {
        //@After
        System.out.println("环绕后置:【"+name+"】方法最终执行完成");
    }

    return proceed;
}
  1. 多切面运行顺序

创建另一个切面类,ValidateAspect

@Aspect
@Component
@Order(2)
public class ValidateAspect {



    @Before(value = "com.th1024.utils.LogUtils.pointDemo()")
    public void vaStart(JoinPoint joinPoint){
        //获取到目标方法运行时使用的参数
        Object[] args = joinPoint.getArgs();
        //获取到目标方法签名
        Signature signature = joinPoint.getSignature();
        System.out.println("validate:【"+signature.getName()+"】方法开始执行,参数列表为"+ Arrays.asList(args));
    }

    //returning="result",设置result接收方法返回值
    @AfterReturning(value = "com.th1024.utils.LogUtils.pointDemo()",returning = "result")
    public void vaReturn(JoinPoint joinPoint, Object result){
        System.out.println("validate:【"+joinPoint.getSignature().getName()+"】方法执行完成,返回结果为"+ result);
    }

    //throwing="e",设置e接收方法抛出的异常
    @AfterThrowing(value = "com.th1024.utils.LogUtils.pointDemo()",throwing = "e")
    public void vaException(JoinPoint joinPoint,Exception e){
        System.out.println("validate:【"+joinPoint.getSignature().getName()+"】方法出现异常,异常信息为"+ e);
    }

    @After(value = "com.th1024.utils.LogUtils.pointDemo()")
    public void vaEnd(JoinPoint joinPoint){
        System.out.println("validate:【"+joinPoint.getSignature().getName()+"】方法最终执行完成");
    }
}

执行测试

@Test
    public void test5(){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");

        MyMathCalculator bean = (MyMathCalculator) context.getBean("myMathCalculator");
        bean.add(1,2);
        System.out.println("=============");
//        bean.div(2,1);
//        bean.div(1,0);
    }

输出结果

环绕前置:【add】方法开始执行,参数列表为[1, 2]
logUtils:【add】方法开始执行,参数列表为[1, 2]
validate:【add】方法开始执行,参数列表为[1, 2]
validate:【add】方法最终执行完成
validate:【add】方法执行完成,返回结果为3
环绕返回:【add】方法执行完成,返回结果为3
环绕后置:【add】方法最终执行完成
logUtils:【add】方法最终执行完成
logUtils:【add】方法执行完成,返回结果为3

图示

img

AOP的应用

  • AOP添加日志记录保存到数据库
  • AOP做权限检查
  • AOP做安全检查
  • AOP做事务控制

基于配置的AOP

<!--基于配置的AOP-->
<bean id="myMathCalculator_xml" class="com.th1024.bean.impl.MyMathCalculator_xml"></bean>
<bean id="validateAspect_xml" class="com.th1024.proxy.ValidateAspect_xml"></bean>
<bean id="logUtils_xml" class="com.th1024.utils.LogUtils_xml"></bean>

<!--AOP名称空间-->
<aop:config>
    <!--提取切入点-->
    <aop:pointcut id="pointDemo" expression="execution(* com.th1024.bean.impl.MyMathCalculator_xml.*(..))"/>
    <!--指定切面-->
    <aop:aspect ref="logUtils_xml" order="1">
        <aop:before method="logStart" pointcut-ref="pointDemo"/>
        <aop:after-returning method="logReturn" pointcut-ref="pointDemo" returning="result"/>
        <aop:after-throwing method="logException" pointcut-ref="pointDemo" throwing="e"/>
        <aop:after method="logEnd" pointcut-ref="pointDemo"/>
        <aop:around method="myAround" pointcut-ref="pointDemo"/>
    </aop:aspect>
    <aop:aspect ref="validateAspect_xml" order="0">
        <aop:before method="vaStart" pointcut-ref="pointDemo"/>
        <aop:after-returning method="vaReturn" pointcut-ref="pointDemo" returning="result"/>
        <aop:after-throwing method="vaException" pointcut-ref="pointDemo" throwing="e"/>
        <aop:after method="vaEnd" pointcut-ref="pointDemo"/>
    </aop:aspect>
</aop:config>

<!--
注解:快速方便
配置:功能完善
重要的切面用配置,不重要的用注解
-->
posted on 2021-06-02 21:56  来点番茄酱  阅读(71)  评论(0)    收藏  举报