AOP:Aspect Oriented Programming:面向切面编程,基于oop基础之上的。指的是程序运行期间,将某段代码动态的切入到指定方法的指定位置进行运行的编程方式。
想象下面的场景,开发中在多个模块间有某段重复的代码,我们通常是怎么处理的?显然,没有人会靠“复制粘贴”吧。在传统的面向过程编程中,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,也就是所谓的工具类,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。然而需求总是变化的,有一天,新增了一个需求,需要再多处做修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具有相同的修改的问题我们都可以通过 AOP 来解决。
场景:计算器运行计算方法的时候进行日志记录。
方式一:给每一个方法代码中添加一个日志记录相关的代码。
缺点:
方式二:将日治管理的代码进行封装,然后需要的时候进行调用,日志工具类。
缺点:
方式三:使用动态代理的方式增加日志管理
动态代理
package com.atguigu.proxy;
import static org.hamcrest.CoreMatchers.nullValue;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import com.atguigu.inter.Calculator;
import com.atguigu.utils.LogUtils;
//生成代理对象的类:使用反射包下的代理类
public class CaculatorProxy {
public static Calculator getProxy(final Calculator calculator) {
//方法执行器:帮我们目标对象执行目标方法
InvocationHandler handler=new InvocationHandler() {
/**
* proxy:代理对象,给jdk使用,我们不要动。
* method:当前要执行的目标对象的方法。
* Object[] args:方法调用的时候传入的参数值
*
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result=null;
try {
LogUtils.logStart(method, args);
//利用反射执行目标方法:result是目标方法执行之后的返回值
result=method.invoke(calculator, args);
LogUtils.logReturn(method, result);
} catch (Exception e) {
LogUtils.logException(method, e);
}finally {
LogUtils.logEnd(method);
}
return result;
}
};
//获取被代理类实现的接口
Class<?>[] interfaces=calculator.getClass().getInterfaces();
//获取代理类对象
Object proxy=Proxy.newProxyInstance(calculator.getClass().getClassLoader(), interfaces, handler);
return (Calculator)proxy;
}
}
LogUtils:
package com.atguigu.utils;
import java.lang.reflect.Method;
public class LogUtils {
public static void logStart(Method method,Object...objects ) {
System.out.println("["+method.getName()+"]开始执行。");
}
public static void logReturn(Method method,Object result ) {
System.out.println("["+method.getName()+"]正常计算完成。");
}
public static void logException(Method method,Exception e) {
System.err.println("["+method.getName()+"]计算过程出错,出现了异常::"+e.getCause());
}
public static void logEnd(Method method) {
System.err.println("["+method.getName()+"]结束了");
}
}
优点:
缺点:
JDK默认的动态代理,如果目标对象没有实现任何接口,代理对象就无法被创建。
写起来太难。
3 总结
Spring实现AOP功能,实质是动态将某段代码切入到指定的方法,不需要将代码直接写死,底层就是利用动态代理实现的。使用Spring可以简单创建动态代理,实现简单且不强制要求实现接口。
类别 | 机制 | 原理 | 优点 | 缺点 |
---|---|---|---|---|
静态AOP | 静态织入 | 在编译期,切面直接以字节码的形式编译到目标自己码文件中 | 对系统无性能影响 | 灵活度低 |
动态AOP | JDK动态代理 | 在运行期,目标的类加载之后,为接口生成代理类,将切面织入到代理类中 | 相对于静态AOP灵活一些 | 切入的关注点需要实现接口,对系统的性能有一定的影响 |
动态字节码生成 | CGLIB | 在运行期,目标的类加载之后,动态生成目标类的子类,将切面逻辑添加到子类中去 | 没有接口也可以织入 | 扩展类的实例方法用final修饰,则无法进行织入 |
自定义类加载器 | 在运行期,目标类加载前,将切面逻辑加到目标的字节码里 | 对绝大多数类进行织入 | 代码中如果使用其他类加载器,则这些类将不会被织入 | |
字节码转换 | 在运行期,所有类加载器的加载字节码文件前进行拦截 | 对所有类进行织入 |
1 AOP的术语
2 图解
1 导包
面向切面编程加强的包:即便目标对象没有实现任何接口也能创建动态代理,子类代理的方式。
2 写配置
注意:
注解需要声明切入点表达式:execution(访问权限符 返回值类型 方法签名)
execution(public int com.atguigu.inter.impl.MyMathCalculator.add(int, int)):表示只在加法之前。
execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int)):表示所有方法之前。
如果成功会存在标志:小箭头
通知方法类的代码:
package com.atguigu.utils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogUtils {
/**
* 告诉Spring每个方法都什么时候执行。
* try{
* @Before
* method.invoke(obj,args)
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
* 通知注解:
* @Before:方法执行之前 :前置通知+@Around
* @After:目标方法结束之后
* @AfterReturning:目标方法正常返回之后 :返回通知
* @AfterThrowing:目标方法抛出异常之后运行 :异常通知
* @Around:环绕 :后置通知+@After
* 注意:before需要声明切入点表达式:execution(访问权限符 返回值类型 方法签名)
* execution(public int com.atguigu.inter.impl.MyMathCalculator.add(int, int)):表示只在加法之前。
* execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int)):表示所有方法之前。
*/
@Before("execution(public int com.atguigu.inter.impl.MyMathCalculator.add(int, int))")
public static void logStart() {
System.out.println("[xxx]开始执行。");
}
@AfterReturning("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int))")
public static void logReturn() {
System.out.println("[xxx]正常计算完成。");
}
@AfterThrowing("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int))")
public static void logException() {
System.out.println("[xxx]计算过程出错,出现了异常::");
}
@After("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int))")
public static void logEnd() {
System.out.println("[xxx]最终结束了");
}
}
3 AOP测试
package com.atguigu.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.atguigu.inter.Calculator;
public class AOPTest {
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
@Test
public void test01() {
//需要从ioc中那。如果想用类型来获取,一定要用他的接口类型。
Calculator calculator=ioc.getBean(Calculator.class);
int result=calculator.add(1, 3);
System.out.println(result);
}
}
结果展示:
输出的顺序似乎有所不对?
@Test
public void test01() {
//需要从ioc中那。如果想用类型来获取,一定要用他的接口类型。
Calculator calculator=ioc.getBean(Calculator.class);
System.out.println(calculator);
System.out.println(calculator.getClass());
}
代码输出:
分析:
问题:为什么CGLIB可以没有接口的情况下实现动态代理?
我们去除掉实现的接口的情况下,我们依旧可以创建动态代理,在这种情况下,获取到的是本类的类型,但是CGLIB也帮我们创建了其子类对象,因此CGLIB也称为子类代理。
@Test
public void test02() {
//不存在接口的情况下
MyMathCalculator calculator=ioc.getBean(MyMathCalculator.class);
calculator.add(1, 2);
System.out.println(calculator);
System.out.println(calculator.getClass());
}
固定格式:execution(访问权限符 返回值类型 方法全类名(参数列表))
通配符:
1)*
//MyMath开头,r结尾的类的下的所有的方法(参数是int,int)
execution(public int com.atguigu.inter.impl.MyMath*r.*(int, int))"
//MyMath开头类的下的所有参数为(int,任意)的方法。
execution(public int com.atguigu.inter.impl.MyMath*.*(int, *))"
2)…
//MyMath开头类的下的所有的方法。
execution(public int com.atguigu.inter.impl.MyMath*.*(..))"
//匹配com.atguigu下任意包下的MyMath开头的类的任意方法
execution(public int com.atguigu..MyMath*.*(..))"
记住两种:
execution():execution(public int com.atguigu.impl.MyMathCalculator.add(int,int))
execution(* * . *(..)) //任意包下的任意类,千万不能写
拓展:
可以使用逻辑运算符:
我们如果无法在方法运行的时候无法获取方法的详细信息,那么就是没有用的,因此我们需要获取到目标方法的详细信息。
步骤:
步骤:
代码:
package com.atguigu.utils;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogUtils {
/**
* 告诉Spring每个方法都什么时候执行。
* try{
* @Before
* method.invoke(obj,args)
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
* 通知注解:
* @Before:方法执行之前 :前置通知+@Around
* @After:目标方法结束之后
* @AfterReturning:目标方法正常返回之后 :返回通知
* @AfterThrowing:目标方法抛出异常之后运行 :异常通知
* @Around:环绕 :后置通知+@After
* 注意:before需要声明切入点表达式:execution(访问权限符 返回值类型 方法签名)
* execution(public int com.atguigu.inter.impl.MyMathCalculator.add(int, int)):表示只在加法之前。
* execution(public int com.atguigu.inter.impl.MyMathCalculator.*(int, int)):表示所有方法之前。
*
*/
@Before("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
//获取到方法的签名
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]开始执行。参数为:"+Arrays.asList(args));
}
@AfterReturning(value="execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))",returning="result")
public static void logReturn(JoinPoint joinPoint,Object result) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]正常执行完成。计算结果为:"+result);
}
@AfterThrowing(value="execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))",throwing="exception")
public static void logException(JoinPoint joinPoint,Exception exception) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]计算过程出错,出现了异常::"+exception);
}
@After("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))")
public static void logEnd(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]最终结束了");
}
}
测试代码:
@Test
public void test01() {
//需要从ioc中拿。如果想用类型来获取,一定要用他的接口类型。
Calculator calculator=ioc.getBean(Calculator.class);
System.out.println(calculator.add(1, 4));
System.out.println("================");
int result=calculator.div(10, 0);
System.out.println(result);
}
代码结果展示:
需要注意指定的异常类型和返回类型尽量往大写,否则可能会出现错误。
步骤:
@Pointcut("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))")
public void MyExecution() {
}
最终代码:
@Pointcut("execution(public int com.atguigu.inter.impl.MyMathCalculator.*(..))")
public void MyExecution() {
}
@Before("MyExecution()")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
//获取到方法的签名
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]开始执行。参数为:"+Arrays.asList(args));
}
@AfterReturning(value="MyExecution()",returning="result")
public static void logReturn(JoinPoint joinPoint,Object result) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]正常执行完成。计算结果为:"+result);
}
@AfterThrowing(value="MyExecution()",throwing="exception")
public static void logException(JoinPoint joinPoint,Exception exception) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]计算过程出错,出现了异常::"+exception);
}
@After("MyExecution()")
public static void logEnd(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("["+name+"]最终结束了");
}
@Around:是Spring中最强大的通知方法,实质上就是动态代理,就是反射帮我们调用目标方法。相当于其他四种注解的四合一版本。
实现步骤:
代码结果演示:
环绕通知抛出异常
分析:
正常执行:环绕通知优先于普通通知执行。
只有普通通知:【普通前置】>目标方法执行>【普通后置】==>【普通方法返回】
加上环绕通知:【环绕前置执行】>【普通前置】>try{目标方法执行}>【环绕返回通知】>【环绕后置通知】>【普通后置】>【普通返回】
执行出错:环绕依旧优先于普通执行,环绕处理。
只有普通通知:【普通前置】>目标方法执行>【普通后置】==>【普通异常】
加上环绕通知:【环绕前置执行】>【普通前置】>try{目标方法执行}>【环绕异常通知】>【环绕后置通知】>【普通后置】>【普通异常】
总结:
注意:
- 异常和返回是不共存的。
- 前置顺序无所谓,其他顺序严重相关。
- 环绕通知可以直接影响到方法的执行,可以影响到返回值和参数,其他普通通知只是再合适的时机进行调用。
再次创建一个普通通知类,查看两个普通切面顺序代码如下
package com.atguigu.utils;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class ValidateApsect {
@Before("com.atguigu.utils.LogUtils.myExecution()")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
//获取到方法的签名
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("ValidateApsect:["+name+"]开始执行。参数为:"+Arrays.asList(args));
}
@AfterReturning(value="com.atguigu.utils.LogUtils.myExecution()",returning="result")
public static void logReturn(JoinPoint joinPoint,Object result) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("ValidateApsect:["+name+"]正常执行完成。计算结果为:"+result);
}
@AfterThrowing(value="com.atguigu.utils.LogUtils.myExecution()",throwing="exception")
public static void logException(JoinPoint joinPoint,Exception exception) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("ValidateApsect:["+name+"]计算过程出错,出现了异常::"+exception);
}
@After("com.atguigu.utils.LogUtils.myExecution()s")
public static void logEnd(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
//获取方法名称
String name=signature.getName();
System.out.println("ValidateApsect["+name+"]最终结束了");
}
}
结果展示:
分析:
1.两个普通的切面顺序是由类名的字典序决定的。
2.可以使用@order(int)来改变其执行顺序。
环绕+两个普通切面
环绕只是影响当前切面(哪个切面类中),无法影响其他切面。
实现步骤和基于注解的aop的步骤是相同的,因此我们只需要实现这个步骤就可以了。
<bean id="myMathCalculator" class="com.atguigu.inter.impl.MyMathCalculator">bean>
<bean id="validateApsect" class="com.atguigu.utils.ValidateApsect">bean>
<bean id="logUtils" class="com.atguigu.utils.LogUtils">bean>
<aop:config>
<aop:pointcut expression="execution(* com.atguigu.inter.impl.MyMathCalculator.*(..))" id="globalPoint"/>
<aop:aspect ref="logUtils">
<aop:pointcut expression="execution(* com.atguigu.inter.impl.MyMathCalculator.*(..))" id="mypoint"/>
<aop:before method="logStart" pointcut-ref="mypoint"/>
<aop:after-returning method="logReturn" pointcut-ref="mypoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="mypoint" throwing="exception"/>
<aop:after method="logEnd" pointcut-ref="mypoint"/>
<aop:around method="myAround" pointcut-ref="mypoint"/>
aop:aspect>
<aop:aspect ref="validateApsect">
<aop:before method="logStart" pointcut-ref="globalPoint"/>
<aop:after-returning method="logReturn" pointcut-ref="globalPoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="globalPoint" throwing="exception"/>
<aop:after method="logEnd" pointcut-ref="globalPoint"/>
aop:aspect>
aop:config>
注意:需要实现接口。
环绕通知和普通通知的优先级问题:
1) 目标方法的调用由环绕通知决定,即你可以决定是否调用目标方法,而前置和后置通知 是不能决定的,他们只是在方法的调用前后执行通知而已,即目标方法肯定是要执行的。
2) 环绕通知可以控制返回对象,即你可以返回一个与目标对象完全不同的返回值,虽然这很危险,但是你却可以办到。而后置方法是无法办到的,因为他是在目标方法返回值后调用。