1.动态代理:代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。
2.可以使用动态代理将日志代码动态的在目标方法执行前后先进行执行。和业务逻辑解耦
JDK默认的动态代理,如果目标对象没有实现任何接口,就无法为其创建动态代理对象,因此使用Spring来解决原先动态代理存在的问题
Spring实现了AOP功能:其底层实现就是动态代理。(实现简单,而且没有强制要求目标对象必须实现接口)
3.AOP术语
>横切关注点:从每个方法中抽取出来的同一类非核心业务。
>切面(Aspect): 封装横切关注点信息的类,每个关注点体现为一个通知方法。
>通知(Advice):切面必须要完成的各个具体工作
>目标(Target):被通知的对象
>代理(Proxy):
>连接点(Joinpoint): 横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。在应用程序中可以使用横纵两个坐标来定位一个具体的连接点
>切入点(pointcut):定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
4.AOP简单配置
>导包
>配置:①将目标类和切面类加入IOC容器中
②加入@Aspect(切面类注解)
③在Spring中设置切面类何时运行
④开启基于注解的AOP模式
package com.cuco.utils;
import java.lang.reflect.Method;
import java.util.Arrays;
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 {
/*try{
* @Before
* method.invoke(obj,args);
* @AfterReturning
* }catch(e){
* @AfterThrowing
* }finally{
* @After
* }
*
* 5个通知注解
* 通过注解告诉Spring每个方法在什么时候运行
* @Before:在目标方法之前运行 前置通知
* @After:在目标方法结束之后运行 后置通知
* @AfterReturning:在目标方法正常返回之后 返回通知
* @AfterThrowing:在目标方法抛出异常后运行 异常通知
* @Around:环绕 环绕通知
* */
//想在目标方法执行之前进行;写切入点表达式
//execution(权限访问符 返回值类型 方法签名)
@Before("execution(public int com.cuco.impl.MyMathCalculator.*(int, int))")
public static void logStart(){
System.out.println("[method.getName()]方法开始执行,使用参数列表[Arrays.asList(args)]");
}
//想在目标方法正常执行完成之后进行
@AfterReturning("execution(public int com.cuco.impl.MyMathCalculator.*(int, int))")
public static void logReturn(){
System.out.println("[method.getName()]方法执行结束,执行结果为[result]");
}
//想在目标方法出现异常时进行
@AfterThrowing("execution(public int com.cuco.impl.MyMathCalculator.*(int, int))")
public static void logException(){
System.out.println("[method.getName()]方法执行异常");
}
//想在目标方法结束时进行
@After("execution(public int com.cuco.impl.MyMathCalculator.*(int, int))")
public static void logEnd(){
System.out.println("[method.getName()]方法执行结束");
}
}
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<context:component-scan base-package="com.cuco"></context:component-scan>
<!-- 开启基于注解的AOP功能 :aop名称空间 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
5.AOP细节
1>AOP底层就是动态代理,IOC容器中保存的组件是其代理对象:返回的Bean类型为$Proxy12,该返回类型并非本类类型
因为IOC容器中保存的是代理对象,所以不应该将实现接口的目标类作为参数传入ioc.getBean()方法中:ioc.getBean(MyMathCalculator.class) (Spring会在容器中寻找类为MyMathCalculator,但实际上没有,因为其真正的类型是$Proxy12)
目标对象和代理对象的公共之处就在于他们都实现了Calculator中同一组接口,因此将Calculator为代理对象的父类,将Calculator.class传入ioc.getBean()
还应注意的是,当传入类中并没有接口,cglib会创建好代理对象。
2>切入点表达式写法(通配符)
3>通知方法的执行顺序:
(1)正常执行的执行顺序:前置通知→后置通知→返回通知
(2)异常执行的执行顺序:前置通知→后置通知→异常通知
4>JoinPoint在通知方法运行时,获取目标方法的详细信息:只需要为通知方法的参数列表中写入JoinPoint的参数
JoinPoint joinPoint:封装了目标方法的详细信息
5>以及接收执行结果和接收抛出的异常信息,使用注解中的属性returning=“ ”、throwing=“ ”接收。代码如下
@Aspect
@Component
public class LogUtils {
//想在目标方法执行之前进行;写切入点表达式
//execution(权限访问符 返回值类型 方法签名)
@Before("execution(public int com.cuco.impl.MyMathCalculator.*(int, int))")
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)+"]");
}
//想在目标方法正常执行完成之后进行
//需要告诉Spring result接收执行结果:returning=" "是告诉Spring用result来接收返回值
@AfterReturning(value="execution(public int com.cuco.impl.MyMathCalculator.*(int, int))",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.cuco.impl.MyMathCalculator.*(int, int))",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.cuco.impl.MyMathCalculator.*(int, int))")
public static void logEnd(JoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
System.out.println("["+name+"]方法执行结束");
}
}
6>通知方法是Spring利用反射调用的,每次方法调用得确定这个方法的参数表的值
7>抽取可重用的切入点表达式
8>@Around :环绕通知
- 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
- 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
- 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。
- 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
/*
* 环绕通知
* 问题:当配置环绕通知以后,切入点方法就不会执行,而是执行通知方法
* 分析:动态代理的环绕通知有明确的切入点方法调用
* 解决:Spring提供了一个接口:ProceedingJoinPoint。该接口有一个方法proceed(),此方法相当于明确调用切入点方法
* 该接口可作为环绕通知方法的参数,程序执行时,框架回味我们提供该接口的实现类供我们使用
* Spring中的环绕通知:
* 其是Spring框架为我们提供的一种可以指定增强方法的代码在切入点方法何时执行
* */
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
System.out.println("Logger类中的afterPrintLog方法开始记录日志了");
Object[] args = pjp.getArgs(); //得到方法执行所需要的参数
rtValue = pjp.proceed(args);//明确调用切入点方法
return rtValue;
}catch (Throwable t){
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的方法最终结束记录日志了");
}
}
}
基于XML配置的通知方法
<!-- 配置AOP -->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置前置通知:在切入点方法之前执行通知-->
<aop:before method="beforePrintLog" pointcut="execution(* tj.ustb.student.service.StudentService.*(..))"></aop:before>
<!--配置后置返回通知:在切入点方法正常执行后通知-->
<aop:after-returning method="afterReturningPrintLog" pointcut="execution(* tj.ustb.student.service.StudentService.*(..))"></aop:after-returning>
<!--配置异常通知:在切入点方法执行异常后通知-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut="execution(* tj.ustb.student.service.StudentService.*(..))"></aop:after-throwing>
<!--配置后置通知:无论切入点方法执行是否异常最后都要执行该通知方法-->
<aop:after method="afterPrintLog" pointcut="execution(* tj.ustb.student.service.StudentService.*(..))"></aop:after>
</aop:aspect>
</aop:config>