spring高级之AOP详解

本文深入解析Spring AOP原理及动态代理机制,通过实例演示如何利用动态代理增强对象方法,实现日志记录等功能。并详细介绍AOP术语、流程及Spring AOP配置。

前言

这是之前开始学spring的时候的笔记,现在添加了一些理解,然后把他搬到博客上来。

动态代理模式演示:

这里仅是动态代理的演示,要查看详细的可以查阅相关博文。
动态代理的本质就是增强对象方法,在不修改目标类的情况动态生成一个代理类和代理对象,然后在目标对象的方法执行前、后、等地方可以执行一点逻辑。比如日志等。

建议要理解Spring的AOP之前要理解好动态代理,因为AOP底层是动态代理。

以下是基于jdk的动态代理写的demo,jdk的动态代理只能代理接口,要代理类的话可以使用cglib动态代理。


/**
 * 目标接口,就是要代理的接口
 */
public interface MathI {

    public  Integerdivision(int i,int j);
}

/**
 * 目标实现类,就是要增强的类。
 */
public class MathImpl implements MathI {
    @Override
    public Integer division(int i, int j) {
        return i / j;
    }
}

/**
 * 日志工具类
 */
public class MyLogger {

    public void before(String methodName,Object[] args){
        System.out.println(methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }

    public void after(String methodName,Object result){
        System.out.println(methodName + "执行后,结果{" + result + "}");
    }

    public void throwing(String methodName,Throwable e){
        System.out.println(methodName + "执行抛出异常{" + e.getCause() + "}");
    }

    public void always(){
        System.out.println("总是你");
    }
}

/**
 * 代理工厂,用于生成代理对象。
 */
public class ProxyFactory {

    //目标对象
    private Object targetObject;

    //目标对象的Class类对象
    private Class targetClass;

    //目标类实现的接口
    private Class[] interfaces;

    //类加载器
    private ClassLoader classLoader;

    //日志工具类
    private MyLogger myLogger = new MyLogger();

    //一个接口。代理对象执行目标接口对应的方法,其实就是执行invoke方法。
    private InvocationHandler handler = new InvocationHandler() {
        /**
         *
         * @param proxy 代理对象
         * @param method  目标对象的方法
         * @param args  方法的参数
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            String methodName = method.getName();
            Object result = null;
            //在目标对象方法执行前的动作
            myLogger.before(methodName,args);
            try {
                //执行目标方法。
                result = method.invoke(targetObject,args);

            }catch (Throwable e){
                //目标方法执行抛出异常后执行的动作
                myLogger.throwing(methodName,e);
                throw e;
            }finally {
                //总是会执行的动作
                myLogger.always();
            }

            //执行目标方法之后执行的动作
            myLogger.after(methodName,result);
            return result;
        }
    };

    public ProxyFactory(Object targetObject) {
        this.targetObject = targetObject;
        this.targetClass = targetObject.getClass();
        this.classLoader = this.targetClass.getClassLoader();
        this.interfaces = this.targetClass.getInterfaces();
    }

    public Object getProxy(){
        //生成一个代理对象
        return Proxy.newProxyInstance(classLoader, interfaces,handler);
    }
}

结果:
在这里插入图片描述
有异常时的结果:异常后就不会执行after()方法了。
在这里插入图片描述

AOP

概述

AOP(Aspect Oriented Programming):面向切面编程,是一种方法论,是一种对OOP面向对象编程的补充。

传统的OOP面向对象编程如果要实现上述功能,在方法执行的前后加日志等一些公共的,与业务逻辑无关的代码,要修改原有代码,把非业务代码耦合到业务代码中。或者使用继承实现子类来实现功能的增强。属于纵向继承机制,这样会使得代码耦合度过高,继承体系复杂。

/**
* 使用OOP继承实现上面的日志功能。但无疑耦合度会变高和继承关系会边复杂。
*/
public class MathImplPlus extends MathImpl {

    private MyLogger myLogger = new MyLogger();

    @Override
    public Integer division(int i, int j) {
        String methodName = "division";
        myLogger.before(methodName,new Object[]{i,j});
        Integer result = null;
        try {
            result = super.division(i,j);
            myLogger.after(methodName,result);
            return result;
        }catch (Throwable e){
            myLogger.throwing(methodName,e);
        }finally {
            myLogger.always();
        }
       
        return result;
    }
}

AOP:面向切面,使用的是横向抽取机制。将公共的、与业务无关的代码抽取成一个切面,然后在程序运行时,把他切入到相应的方法代码处。

AOP的好处:

  • 将公共代码抽取出来,代码不分散,便于维护和升级。
  • 使得业务模块更加简洁,只有核心业务代码。

AOP图解:
在这里插入图片描述

AOP术语:

AOP术语是在进行AOP编码时一些必要的东西,要理解式记着,在编写AOP代码时会如鱼得水。

横切关注点:

从目标方法中抽取出来的同一类非业务逻辑代码,比如上面demo的日志前置逻辑,日志后置逻辑等都是一个个关注点。类比上面的MyLogger类中的四个方法。

切面:

由同一类(不同类也行)横向关注点组成(封装成)的一个类,切面就是由横切点组成的类。类比上面的MyLogger类。

通知:

通知相对于目标对象的目标方法而言,就是把关注点作用在目标方法的哪个时期,是方法调用前还是返回后等等。每个关注点就体现为一个个通知方法。

通知可以分为五类(实际上就是4类):

  • 前置通知:在方法调用前调用的通知方法。类比为上面的before方法。用@Before注解定义。
  • 在这里插入图片描述
  • 后置返回通知:在方法执行正常返回后(没有抛出异常)执行的通知方法。类比为上面的after()方法。如果方法没有正常返回,例如抛出异常,就不会执行该后置返回通知。
  • 异常通知:目标方法执行抛出异常后执行的通知方法。类比为上面的throwing方法。
  • 最终通知:目标方法执行后执行的通知方法,与后置返回通知的区别是最终通知无论方法是否正常返回都会执行的通知方法。类比上面的always方法。
目标对象:

说白了就是要被代理的对象,就是需要增强的对象,类比上面的MathImpl。

连接点:

就是要进行增强的方法,目标方法,类比上面的MathImpl类中的division方法。

切入点:

定位到连接点的方式,在spring里面就是一个表达式,使用该表达式可以定位要进行增强的连接点。

AOP流程:

  1. 定义切面。
  2. 定义关注点,并且定义关注点的通知类型。
  3. 使用切入点表达式定义关注点需要关注增强哪些连接点。
  4. 使用关注点横切到连接点上。

AspectJ框架:

AspectJ是java社区里面最流行的AOP框架。
在spring2.0以上,可以使用基于AspectJ注解或者使用基于XML配置的AOP。

所需要的基本jar包:
在这里插入图片描述

切入点表达式

切入点表达式是通知定位到相应连接点的表达式,相当于一个条件。spring会根据该条件定位到指定的连接点。

语法:
execution(权限修饰符 返回值类型的全限定名 类全限定名.方法名(参数类型列表))

比如上面如果要定位到division方法,可以使用切入点表达式:

//这个是精确匹配,直接匹配到MathImpl方法。也可以实现模糊匹配,匹配条件符合的连接点。
"execution(public java.lang.Integer com.cong.springdemo.aopdemo.MathImpl.division(int,int))"

模糊匹配语法:

  1. 使用 * 号可以代表任意任意(返回值类型 + 权限修饰符)、包名、方法名。
  2. 使用 .. 符号可以代表任意参数列表。
//下面两个表达式代码任意方法。第一个*代表(任意返回值类型 + 任意权限修饰符),用一个*号代表2个。不能用两个。
//第二个*号代表任意方法名。  或者第二个表达式中 *.*代表任意类中的任意方法名。
//..符号代表任意参数类型列表。
"execution(* *(..))"  或者 "execution(* *.*(..))"

//com.cong.springdemo.aopdemo包下的任意类中的任意方法。如果是jdk动态代理实现的话是接口中的方法。
"execution(* com.cong.springdemo.aopdemo.*.*(..))"

//com.cong.{任意包名}.aopdemo.任意类名.任意方法名  的方法。*也代表了一个包层级结构。
"execution(* com.cong.*.aopdemo.*.*(..))"


Spring AOP的相关注解和xml配置:

@Before 作用在横切点方法上,标记该横切点为前置通知。
@After 作用在横切点方法上,标记该横切点为最终通知。
@AfterReturning 作用在横切点方法上,标记该横切点为成功返回通知。
@AfterThrowing 作用在横切点方法上,标记该横切点为异常通知。
@Around 作用在横切点方法上,标记该横切点为环绕通知。
@Aspect 作用在类上,标记该类为切面。
@Pointcut 用于定义一个切入点表达式,用于实现表达式的复用。
@Order 用于定义切面的优先级或者横切关注点的优先级。

必要点:切面和目标对象都要成为spring的组件,让spring管理才能实现AOP。

配置文件配置要点:

  1. 要开启AspectJ切面自动代理
  2. 要开启包扫描。
  <!-- 开启基于aspectj的AOP自动代理 -->
   <aop:aspectj-autoproxy />
  
  <!-- 扫描组件 -->
  <context:component-scan base-package="com.cong.springdemo"/>
/**
 * 目标接口
 */
public interface MathI {

    public  Integer division(int i,int j);
}

@Component  //目标对象也要交由spring ioc容器管理
public class MathImpl implements MathI {
    @Override
    public Integer division(int i, int j) {
        System.out.println("执行division中");
        return i / j;
    }
}


/**
 * 日志工具切面类
 */

@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
public class MyLogger {

	//前置通知,用@Before注解定义。里面的value值是切入点表达式,表示该通知要作用到哪些连接点上。
	@Before(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
    public void before(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }

	@AfterReturning(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行后,结果{" + result + "}");
    }

	@AfterThrowing(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行抛出异常{" + e + "}");
    }

	@After(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
    public void always(){
        System.out.println("总是你");
    }
}

//测试类
public class Test {
	
	public static void main(String[] args) {
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
		//生成的代理类会实现与目标类一样的接口,所以目标类与代理类实际上属于“兄弟类”,
		//不能相互转型,只能转型为们的接口。
		MathI mathI = (MathI) context.getBean("mathImpl");
		mathI.division(5, 0);
	}

}

spring-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"
       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">
  
  
  <!-- 开启基于aspectj的AOP自动代理 -->
   <aop:aspectj-autoproxy />
  
  <!-- 扫描组件 -->
  <context:component-scan base-package="com.cong.springdemo"/>
  
 
   		
 
</beans>

执行结果:
在这里插入图片描述
方法异常后结果:
在这里插入图片描述
可以看出这个与上述的动态代理例子十分相似。

环绕通知:
后置通知实际上就是前面四种通知的总和,可以在环绕通知中设置以上四种通知。看过代码你会发现环绕通知跟开始的动态代理demo是一个样的。

环绕通知代码demo(用@Around注解标注):

@Around(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
		
		//执行前置通知
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "执行前,参数{" + Arrays.toString(args) + "}");
        
        Object result = null;
        
        try {
        	//执行方法
			result = point.proceed();
		} catch (Throwable e) {
			//异常通知
			System.out.println(methodName + "执行抛出异常{" + e + "}");
			throw e;
		}finally {
			//最终通知
			System.out.println("总是你");
		}
        
        //后置返回通知
        System.out.println(methodName + "执行后,结果{" + result + "}");
        return result;
	}

执行结果:
在这里插入图片描述
异常后的结果:
在这里插入图片描述
可见与上面四种通知配合出来的结果一个样。

一些细节:
@Pointcut注解的使用:

该注解用于定义切入点表达式,以达到复用。避免像上面那样,每个横切关注点都要定义一个切入点表达式。

@Pointcut注解只能作用在方法上,可以定义一个空方法。然后加上注解,填写好切入点表达式。

然后在本来要写切入点表达式的横切关注点上用方法名()代替便可,类似下面的pointcut()

使用:

@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
public class MyLogger {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//前置通知,用@Before注解定义。里面的value值是切入点表达式,表示该通知要作用到哪些连接点上。
	@Before(value = "pointcut()")
    public void before(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println(methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }

	@AfterReturning(value = "pointcut()",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行后,结果{" + result + "}");
    }

	@AfterThrowing(value = "pointcut()",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行抛出异常{" + e + "}");
    }

	@After(value = "pointcut()")
    public void always(){
        System.out.println("总是你");
    }
}

结果与上面demo一样。

JoinPoint 与 ProceedingJoinPoint

该接口封装这对拦截的方法的信息,可以获得拦截的方法的参数,方法名等信息。ProceedingJoinPoint是JoinPoint 的子接口,提供更加强大的功能,可以执行目标方法等。

@AfterReturning

该注解有个returning的成员变量,可以获取目标方法执行后的返回值。

使用方法:先在注解使用时,指定该返回值的名称,然后在对应的通知方法中定义一个形参名称与注解中returning 的值一样的形参。 就会把一个Object类型的返回值注入到该形参中。

这个返回值形参的定义一般定义为Object,除非你能非常确定目标方法返回值的类型,才可以定义为该类型,否则如果类型转换失败的话,该通知就会不生效。比如你目标方法返回一个Integer类型返回值,但是通知方法中形参定义的类型是String类型,该通知方法就不会作用在该目标方法上。

@AfterReturning(value = "pointcut()",
			returning = "result")
    public void after(JoinPoint point,Object result){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行后,结果{" + result + "}");
    }
AfterThrowing注解

该注解有个成员变量throwing能让我们捕获到异常的类型。

使用方法与上面@AfterReturning方法的返回值类似。

throwing 设置的值与形参的名称要一致。会为该形参注入一个Throwable 类型的异常。形参也可以定义特定类型的异常。但是如果异常类型转换失败的话,该异常处理通知就会对该目标方法失效。不会执行。

@AfterThrowing(value = "pointcut()",
			throwing = "e")
    public void throwing(JoinPoint point,Throwable e){
		String methodName = point.getSignature().getName();
        System.out.println(methodName + "执行抛出异常{" + e + "}");
    }
@Order注解

同一个连接点,同一个通知可以有多个横切关注点同时作用,此时就要指定横切关注点的优先级。默认哪个切面被spring先加载,里面的横切关注点优先级就高,就会先作用于目标方法中。同一切面的横切关注点,谁先被扫描到,谁的优先级就高。大概就是clazz.getMethods()之类的方法返回的方法数组,谁在前面谁的优先级就高。可能与方法名有关。

可以用@Order注解显示地指定前面和横切关注点的优先级。

该注解位于 org.springframework.core.annotation 包中,是spring的注解。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Documented
public @interface Order {
	//这个定义的是优先级,值越小优先级越高,默认int类型的最大值。
    int value() default 2147483647;
}

demo:定义三个切面:

@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
//@Order(value = 2)
public class MyLogger {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger前置通知before1==>" + methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }
	
}


@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
//@Order(value = 3)
public class MyLogger1 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//@Order(value = 0)
	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger1前置通知before1==>" + methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }
	
}

@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
//@Order(value = -5)
public class MyLogger2 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	//@Order(value = 100)
	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger2前置通知before1==>" + methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }

}


@Aspect   //此注解把MyLogger类标注为一个切面
@Component  //切面对象要交给交给spring 容器管理才能实现AOP。
public class MyLogger4 {
	
	@Pointcut(value = "execution(* com.cong.springdemo.aopdemo.MathImpl.division(int, int))")
	public void pointcut() {
		
	}

	@Before(value = "pointcut()")
    public void before1(JoinPoint point){
		String methodName = point.getSignature().getName();
		Object[] args = point.getArgs();
        System.out.println("MyLogger4前置通知before1==>" + methodName + "执行前,参数{" + Arrays.toString(args) + "}");
    }
	

}

分析:
第一次不使用Order注解的默认情况:默认顺序。
在这里插入图片描述

第二次把切面类上的Order的注解的注释都去掉。然后MyLogger4仍然不用Order注解。

在这里插入图片描述
解释:因为 MyLogger4没有用Order注解,所以优先级最低,MyLogger2 Order注解值为-5 ,MyLogger为 2,MyLogger1 为3,所以优先级 2 >MyLogger>1>4。

有挺多种情况的,这里就不一一贴图了,就总结一下,有兴趣可以自己尝试:

  1. 定义在切面上的Order注解先比较,再比较定义在方法上的注解。假如 我切面1优先级定义为0,而里面的横切关注点定义为10000,切面2优先级定义为2,里面的横切关注点定义为-10000,仍然是切面1的横切关注点的优先级高。因为切面1优先级比切面2要高。
  2. 同一切面里面的横切关注点比较优先级才有意义。不同切面的横切关注点优先级取决于切面。
  3. 相同优先级的切面或者横切关注点采用默认优先级比较方法。
  4. 除非你用Order注解定义的优先级值为int的最大值,否则用Order注解定义的切面或者横切关注点比没有用Order注解的优先级要高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值