Spring好好学三(AOP)

Spring好好学三(AOP)

大纲

  • 1、AOP概念

  • 2、AOP代理实现

1、AOP概念

        AOP又名Aspect Oriented Programming 意为 ‘面向切面编程’通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术。AOP思想是OOP(面向对象)的延续 在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP中的基本单元是 Aspect(切面),AOP是软件行业的热点,也是Spring框架中的一个重要内容,是函数式编程的一种延伸范式。

        在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,动态地将代码在不改变原有的逻辑情况下切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

        面向对象的特点是继承、多态和封装,而封装就要求将不同的功能分散到不同的对象中,也称为职责分配原则,这样做的好处是降低了代码的复杂程度,使类可重用。但是,若在两类中都需要调用相同的方法,则会存在重复代码,若将重复内容抽取出来则会让两个独立的类出现耦合。

        而AOP,则可以相同的代码逻辑抽取到一个切面中,需要时再切入对象中去,从而改变其原有的行为。

1.1 AOP相关术语

  • (1)Aspect 切面

        切面是由切入点(pointCut)与通知(Advice)组成,它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中。可以简单地认为, 使用 @Aspect 注解的类就是切面。

  • (2)连接点(Join Point)

        官方解释如下:

a point during the execution of a program, such as the execution of a 
method or the handling of an exception. In Spring AOP, a join point 
always represents a method execution.
程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理.
在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.

        连接点(Join Point):在应用执行过程中能够插入切面的一个点。 JointPoint是程序运行过程中可识别的点,这个点可以用来作为**AOP**切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。

        AOP中的Joinpoint可以有多种类型:**构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。**也就是说在AOP的概念中我们可以在上面的这些Joinpoint上织入我们自定义的Advice,但是在Spring中却没有实现上面所有的joinpoint,确切的说,Spring只支持方法执行类型的Joinpoint。

  • (3)切点(PointCut)

        切点(Pointcut):一组连接点的总称,用于指定某个增强应该在何时被调用, 切点的作用是提供一组规则来匹配连接点joinPoint,给满足规则的连接点添加Advice。

在 Spring AOP 中, 所有的方法执行都是 Join point. 而 point cut 是一个描述信息, 
它修饰的是 Join point, 通过 point cut, 我们就可以确定哪些 Join point 可以被织入 Advice. 
因此Join point 和 point cut 本质上就是两个不同纬度上的东西.
总结:advice 是在 Join point 上执行的, 而 point cut 规定了哪些Join point​​​​​​​可以
执行哪些 advice
  • (4)目标对象(Target)
织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 
总是一个代理对象(proxied object)
注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.
  • (5)织入(Weaving)
将 aspect 和其他对象连接起来, 并创建代理对象 adviced object 的过程.
根据不同的实现技术, AOP织入有三种方式:

编译器织入, 这要求有特殊的Java编译器.
类装载期织入, 这需要有特殊的类装载器.
动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.
advice 的类型

before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 
但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 
不能人为地决定是否继续执行 join point 中的代码)

after return advice, 在一个 join point 正常返回后执行的 advice
after throwing advice, 当一个 join point 抛出异常后执行的 advice
after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice.
around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.
  • 关于AOP proxy
Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 
通过它, 我们可以为任意的接口实现代理.如果需要为一个类实现代理, 那么可以使用 CGLIB 
代理. 当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 
AOP 代理了. 即如果我们需要为一个方法织入 Advice , 但是这个方法不是一个接口所提供的
方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议
基于接口编程, 对接口进行 AOP 而不是类
  • 通知类型
前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。

后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。

异常通知(After throwing advice):在方法抛出异常退出时执行的通知。

最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

1.2 为啥要使用AOP

        利用AOP的思想来对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

        在Spring AOP中业务逻辑仅仅只关注业务本身,将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

        AOP相当于一个拦截器,去拦截一些处理,例如:当一个方法执行的时候,Spring 能够拦截正在执行的方法,在方法执行的前或者后增加额外的功能和处理,就是我们希望通过使用AOP来对我们的代码进行耦合度的降低 把原本杂乱交错的关系给分离开来 进而改变这些行为的时候不会因为杂乱的关系互相影响。

2、基于注解AOP开发

        基于XML的声明式AspectJ存在一些不足,需要在Spring配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解。

注解名称注释
@Aspect用来定义 一个切面
@pointcut用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。
@Before用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
@AfterReturning用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式
@After用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
@Around用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。
@AfterThrowing用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。
@DeclareParents于定义引介通知,相当于IntroductionInterceptor (不要求掌握)。

2.1 切入点表达式

        学习使用Aspect进行AOP开发之前,要先了解一下切入点表达式。 AspectJ最强大的地方就在于他的切入点表达式:

execution() ,用于描述方法 ,语法格式为

 execution([权限修饰符] [返回值类型] [类的完全限定名] [方法名称]([参数列表]) 
  • 返回值类型、方法名、参数列表是必须配置的选项,权限修饰符、类的完全限定名则为可选配选项。

  • 返回值类型: *表示任意返回值; 如果返回值为对象,则需指定全路径的类名。 类的完全限定名:包名+类名

  • 方法名: * 代表所有方法, set*代表以set开头的所有方法, *do 代表以do结尾的所有有方法, addUser 代表固定方法

  • 参数列表:

        (…)代表所有参数;

        (*) 代表只有一个参数,且参数类型为任意类型;

        ( *,String) 代表有两个参数,第一个参数可以为任何值,第二个为 String 类型的值。

2.1 AOP实现案例

Spring AOP的实现方式是动态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标
类中,这样往往是通过动态代理技术完成的;
如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),
Spring AOP采用的就是基于运行时增强的代理技术。所以我们看下如下的两个例子(例子代码在新窗口打开 中05模块):
基于JDK代理例子
基于Cglib代理例子
  • 基于JDK代理例子

代码如下:

public class BusinessController implements BusinessService {
    @Override
    @RequestMapping("/doBusiness")
    public String doBusiness() {
        log.info("开始执行业务代码");
        return "success";
    }
} 



@Aspect
@Component
@Slf4j
public class AspectjLog {

    @Pointcut("execution(* com.xuzaiya.springboot_test.controller.aop.BusinessController.*(..))")
    public void logAop() {
    }

    @Before("logAop()")
    public void logBefore(JoinPoint joinPoint) {
        log.info("前置通知");
    }

    @AfterReturning("logAop()")
    public void logAfterReturning() {
        log.info("后置通知");
    }

    @After("logAop()")
    public void logAfter() {
        log.info("最终通知");
    }

    @AfterThrowing("logAop()")
    public void logAfterThrow() {
        log.info("异常通知");
    }

    //环绕通知功能很强,可以替代上面的所有通知
    @Around("logAop()")
    public void logAround(ProceedingJoinPoint jp) {
        try {
            log.info("环绕通知-进入方法");
            jp.proceed();
            log.info("环绕通知-退出方法");
        } catch (Throwable throwable) {

            throwable.printStackTrace();
        }
    }
} 

代码运行结果:

2024-03-25 20:23:27.860  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.AspectjLog          : 环绕通知-进入方法
2024-03-25 20:23:27.861  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.AspectjLog          : 前置通知
2024-03-25 20:23:27.863  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.BusinessController  : 开始执行业务代码
2024-03-25 20:23:27.864  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.AspectjLog          : 后置通知
2024-03-25 20:23:27.864  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.AspectjLog          : 最终通知
2024-03-25 20:23:27.864  INFO 14660 --- [nio-8099-exec-1] c.x.s.controller.aop.AspectjLog          : 环绕通知-退出方法
  • 非接口使用Cglib代理

代码:

@RestController
@Slf4j
public class CglibProxyController {
    @RequestMapping("/cglibTest")
    public String cglibTest() {
        log.info("开始执行业务代码");
        return "success";
    }
}

定义切面:

@Pointcut("execution(* com.xuzaiya.springboot_test.controller.aop.CglibProxyController.*(..))")
    public void logAop() {
    } 
其他通知方法跟上面类似。

运行结果:

2024-03-25 20:32:33.183  INFO 3524 --- [nio-8099-exec-5] c.x.s.controller.aop.AspectjLog          : 环绕通知-进入方法
2024-03-25 20:32:33.183  INFO 3524 --- [nio-8099-exec-5] c.x.s.controller.aop.AspectjLog          : 前置通知
2024-03-25 20:32:33.188  INFO 3524 --- [nio-8099-exec-5] c.x.s.c.aop.CglibProxyController         : 开始执行业务代码
2024-03-25 20:32:33.189  INFO 3524 --- [nio-8099-exec-5] c.x.s.controller.aop.AspectjLog          : 后置通知
2024-03-25 20:32:33.189  INFO 3524 --- [nio-8099-exec-5] c.x.s.controller.aop.AspectjLog          : 最终通知
2024-03-25 20:32:33.189  INFO 3524 --- [nio-8099-exec-5] c.x.s.controller.aop.AspectjLog          : 环绕通知-退出方法

2.2 JDK动态代理和CGlib代理

2.2.1 JDK动态代理

        JDK动态代理的目标对象必须要有接口实现,也就是说:委托类必须要继承接口

在JDK中,有一个Proxy类(名词,代理人)。Proxy类是专门完成代理的操作类,可以通过此类为一个或多个接口动态的生成实现类。
这个类提供的有一个静态方法:newProxyInstance()方法。这个方法的目的就是给我们的
目标对象(委托对象)返回一个代理对象。

newProxyInstance()方法需要有三个参数:

        (1) 类加载器(ClassLoader对象)         (2) 接口集合(一个Interface对象的数组,就是需要代理对象代理那些共同行为,也是委托对象继承的共同行为接口)         (3) 一个InvocationHandler接口对象(当然可以是它的一个实现类对象)。这个接口中有一个invoke()方法。invoke()方法起到的作用很大,当代理对象调用共同行为方法的时候,invoke()方法就会被自动调用执行。

  • 代码示例

(1) 接口

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public interface MyInterface {
    String sayHi(String para);
} 

(2)目标类

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public class MyClass implements MyInterface{
    @Override
    public String sayHi(String para) {
        System.out.println("你好:" + para);
        return "success";
    }
} 

(3)代理类

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public class MyClassProxy implements InvocationHandler {

    private Object target;
    private void before(){
        System.out.println("Before invoke method...");
    }

    private void after(){
        System.out.println("After invoke method...");
    }

    public MyClassProxy(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(proxy instanceof MyInterface){
            System.out.println(proxy.getClass() + "is instance of MyInterface");
        }
        before();
        Object result = method.invoke(target,args);
        after();
        return result;
    }
} 

(4) 客户端测试

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public class Client {
    public static void main(String[] args) {
        MyClass target = new MyClass();
        InvocationHandler handler = new MyClassProxy(target);
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),handler);
        proxy.sayHi("Java");
    }
} 

测试结果:

class com.sun.proxy.$Proxy0is instance of MyInterface
Before invoke method...
你好:Java
After invoke method...

Process finished with exit code 0

2.2.2 Cglib代理

        cglib是是第三方的工具库。其原理是继承,cglib通过继承目标类,创建他的子类,在子类当中重写父类的相关方法,实现功能的增强。

        代理类去继承目标类,每次调用代理类的方法都会被方法拦截器拦截,在拦截器中才是调用目标类的该方法的逻辑。

  • cglib实现动态代理的原理
1.生成一个空的字节码对象
2.通过字节码对象生成目标类对象的子类 进行增强
3.实现拦截器,通过连接器实现代理的类方法
4.创建代理对象
  • 代码示例

        CGLib动态代理中提供了一个类Enhance,需要用它生成一个空的字节码对象,所以我们需要导入外部的jar包依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

(1)目标类

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public class ClothesFactory {
    public void clothes(String size) {
        System.out.println("已经为您制作好了一整套size为"+size+"的衣服。。。。。。。。");
    }
}

(2)拦截器

public class MyMethodInterceptor implements MethodInterceptor {
    /**
     *
     * @param o   代理对象
     * @param method 目标对象中的方法
     * @param objects 目标对象中方法的参数
     * @param methodProxy 代理对象中代理方法对象
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("前置增强");
        methodProxy.invokeSuper(o,objects);  // 动态的回调父类当中的方法
        System.out.println("后置增强");
        return o;
    }

}

(3)代理类

public class CglibProxy  {

    public static Object createProxy(String path) throws ClassNotFoundException {
        Enhancer enhancer = new Enhancer();  //生成空的字节码对象
        enhancer.setSuperclass(Class.forName(path)); //通过字节码对象生成目标类对象的子类 进行增强
        enhancer.setCallback(new MyMethodInterceptor()); //实现拦截器,通过连接器实现代理的类方法
        Object o = enhancer.create();//创建代理对象
        return o;
    }
}

(4)客户端测试

/**
 * 功能说明:
 *
 * @author yangxu40939
 * @date 2024/3/25
 */
public class Client {
    public static void main(String[] args) throws ClassNotFoundException {
        //生成代理对象
        ClothesFactory proxy = (ClothesFactory) CglibProxy.createProxy("com.xuzaiya.springboot_test.controller.cglib.ClothesFactory");
        //方法调用
        proxy.clothes("xxxL");
    }
}

(5)测试结果

前置增强
已经为您制作好了一整套size为xxxL的衣服。。。。。。。。
后置增强

Process finished with exit code 0
end
  • 作者:旭仔(联系作者)
  • 发表时间:2024-04-27 13:53
  • 版权声明:自由转载-非商用-非衍生-保持署名
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论