2024年9月26日--- Spring-AOP

SpringAOP

在学习编程过程中,我们对于公共方法的处理应该是这样的一个过程,初期阶段如下

f1(){
   Date now = new Date();
   System.out.println("功能执行之前的各种前置工作"+now)
   //...功能代码
   //...功能代码
   System.out.println("功能执行之前的各种后置工作")
}
​
f2(){
   System.out.println("功能执行之前的各种前置工作")
   //...功能代码
   //...功能代码
   System.out.println("功能执行之前的各种后置工作")
}
.....

然后中期阶段,我们学会了封装

public class AspectConfig{
   static void before(){
      System.out.println("功能执行之前的各种前置工作")
   }
   static void after(){
      System.out.println("功能执行之前的各种后置工作")
   }
}   
AspectConfig.before()
f1()
AspectConfig.after()
   
AspectConfig.before()---切面--  块  -份--模块
f2()
AspectConfig.after()

现在,我们可以采用另外一种更简单的方式,AOP思想,对代码进行无侵入式实现,这样更高级了。

public class AspectConfig{
   @express(*f*)
   static void before(){
      System.out.println("功能执行之前的各种前置工作")
   }
   //@expres(*f*)
   static void after(){
      System.out.println("功能执行之前的各种后置工作")
   }
}   
m(){
    f1()
}
n(){
   f2()
}

1.1 AOP思想概述

1.1.1 思想简介

AOP(Aspect Oriented Programming)意为:面向切面编程,它是一种思想,是对某⼀类事情的集中处理。

它的核心思想是 将方法在执行过程中切分为多个部分,也就是多个横切关注点,这样可以与业务逻辑代码分离开来,使代码模块化,进而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP是OOP(面向对象编程)的一个补充,它允许开发者以声明方式实现关注点,而不是通过在业务逻辑代码中散布大量重复代码。

1.1.2 AOP的实现方式

AOP 可以通过多种方式实现,包括:

  • 编译时增强:在编译期间通过修改字节码来实现AOP。

  • 类加载时增强:在类加载到JVM时通过字节码操作实现AOP。

  • 动态代理:在程序运行时,通过代理对象来实现AOP。

1.1.3 AOP的应用场景

1)日志记录

2)声明式事务管理

3)登录安全验证

4)统一异常处理

1.1.3 AOP中的核心概念

1)横切关注点

在程序中,可以跨越多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 … 简单来说,就是程序员要关注的事情,抽象概念

2)切面(Aspect)

横切关注点被模块化的一个体现,它是一个类。这个类由通知(Advice)和切点(Pointcut)组成,既包含了横切逻辑的定义,也包含了连接点的定义。

3)通知(Advice)

通知,实际上是一个拦截器,它定义了切面是做什么以及何时使用,即在某个特定的连接点上执行的动作,它是切面的具体实现

以目标方法为参照点,根据放置位置的地方不同,通知分为如下5种类型通知:

  • 前置通知(Before):在方法执行前执行。

  • 后置通知(After):在方法执行后执行。

  • 返回通知(After Returning):在方法成功返回后执行。

  • 异常通知(After Throwing):在方法抛出异常后执行。

  • 环绕通知(Around):包围方法执行的前后。

4)切入点(PointCut)

Advice 执行的 “地点”的定义。 简单理解:就是用来定义位置

5)连接点(JointPoint)

与切入点匹配的执行点。这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。 简单理解:就是具体切入的位置

6)目标(Target)

就是被切面作用的对象,包含连接点的对象

1.2 Spring AOP简介

AOP是一个思想,是一个编程范式。SpringAOP是对这个思想的实现。

Spring AOP 是构建在动态代理基础上的。因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK ProxyCGLIB ⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。

织⼊(Weaving):代理的⽣成时机

织入就是什么时候把代理的代码放进运行的代码中。在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:

编译期(编译阶段):

切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就 是以这种⽅式织⼊切⾯的。

类加载期:

切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载 时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。

运⾏期:

切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。

1.3 XML配置方式

1.3.1 导入依赖

<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.9.4</version>
</dependency>

1.3.2 案例1

UserService.java

public interface UserService {
     void add();
     void delete();
     void update();
     void search();
}

UserServiceImpl.java

public class UserServiceImpl implements UserService{
    @Override
    public void add() {
        System.out.println("---增加用户---");
    }
​
    @Override
    public void delete() {
        System.out.println("---删除用户---");
    }
​
    @Override
    public void update() {
        System.out.println("---修改用户---");
    }
​
    @Override
    public void search() {
        System.out.println("---查询用户---");
    }
}
​

编写两个通知类型,一个前置通知,一个后置通知

import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Log implements MethodBeforeAdvice {
    /**
     *
     * @param method    目标对象的方法
     * @param objects   被调用的方法的参数
     * @param o             目标对象
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println(o.getClass().getName()+"的"+method.getName()+"正在执行····");
    }
}
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
​
public class AfterLog implements AfterReturningAdvice {
    /**
     *
     * @param o             方法的返回值
     * @param method        目标对象的方法
     * @param objects       目标对象的方法的参数
     * @param o1            目标对象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("执行了"+o1.getClass().getName()+"的"+method.getName()+"方法," +
                "返回值:"+o);
    }
}

注册bean,实现aop切入 , 注意导入约束

<?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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
​
    <!--注册-->
    <bean id="userService" class="com.shuilidianli.service.UserServiceImpl"></bean>
    <bean id="log" class="com.shuilidianli.aop.Log"></bean>
    <bean id="afterLog" class="com.shuilidianli.aop.AfterLog"></bean>
​
    <!--aop配置-->
    <aop:config>
        <!--切入点 expression: 表达式匹配要执行的方法-->
        <aop:pointcut id="pointcut" expression="execution(* com.shuilidianli.service.UserServiceImpl.*(..))"/>
        <!--执行环绕:advice-ref执行方法,pointcut-ref:切入点-->
        <aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
</beans>
​

测试

import com.shuilidianli.service.UserService;
import com.shuilidianli.web.EmpController;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class AopTest2 {
    @Test
    public void test1(){
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("beans.xml");
        UserService us =
                (UserService) ctx.getBean("userService");
        us.search();
​
    }
}

测试结果:

Aop的重要性 : 很重要 . 一定要理解其中的思路 , 主要是思想的理解这一块 .

Spring的Aop就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 , 当执行领域业务时 , 将会把公共业务加进来 . 实现公共业务的重复利用 . 领域业务更纯粹 , 程序员专注领域业务 , 其本质还是动态代理

1.3.2 案例2

自定义类来实现Aop

目标业务类不变依旧是userServiceImpl

1)自己定义一个切入类

package com.shuilidianli.aop;
​
public class DiyPointcut {
   public void before(){
      System.out.println("---------方法执行前---------");
   }
   public void after(){
      System.out.println("---------方法执行后---------");
   }
}

2)注册配置

<!--第二种方式自定义实现-->
<!--注册bean-->
​
<bean id="diy" class="com.shuilidianli.aop.DiyPointcut"></bean>
<!--aop的配置-->
<aop:config>
   <!--第二种方式:使用AOP的标签实现-->
   <aop:aspect ref="diy">
      <aop:pointcut id="diyPointcut" expression="execution(* com.shuilidianli.service.UserServiceImpl.*(..))"/>
      <aop:before method="before" pointcut-ref="diyPointcut"/>
      <aop:after method="after" pointcut-ref="diyPointcut"/>
   </aop:aspect>
</aop:config>

3)测试

package com.shuilidianli.test;
​
import com.shuilidianli.service.UserService;
import com.shuilidianli.web.EmpController;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class AopTest2 {
    @Test
    public void test1(){
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("beans.xml");
        UserService us =
                (UserService) ctx.getBean("userService");
        us.search();
​
    }
}

测试结果:

1.4 注解方式

1.4.1 导入依赖,开启注解扫描

<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.9.4</version>
</dependency>
<context:component-scan base-package="com"/>
​
<!-- 开启aop组件注解扫描 -->
<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

aop:aspectj-autoproxy:说明

通过aop命名空间的<aop:aspectj-autoproxy />声明自动为spring容器中那些配置@aspect切面的bean创建代理,织入切面。
​
当然,spring 在内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现的细节已经被<aop:aspectj-autoproxy />隐藏起来了
​
<aop:aspectj-autoproxy />有一个proxy-target-class属性,默认为false,表示使用jdk动态代理织入增强,
​
当配为<aop:aspectj-autoproxy  poxy-target-class="true"/>时,表示使用CGLib动态代理技术织入增强。
​
不过即使proxy-target-class设置为false,如果目标类没有声明接口,则spring将自动使用CGLib动态代理。
​

1.4.2 切入点表达式语法

1)语法如下:

"execution(修饰词 返回值类型 类全限定名.方法名(形参列表) 异常)"
   
public int f1(int a,int b)throw Exception{
   
}

2)Spring AOP的切入点表达式非常灵活,支持模糊配置。

eg1 : execution(* 全类名.*(..))
   第一个 "*" 表示支持任意修饰符及返回值类型;第二个 "*" 表示支持该类中的任意方法;形参列表中的".."则表示可以匹配任意数量和类型的参数。(PS : 若目标类、接口与当前切面类在同一个包下,可以省略包名,只写类名)
​
eg2 : execution(public * 全类名.*(..))
   表示支持该类中的所有公有的方法
​
eg3 : execution(public double 全类名.*(..))
   表示支持该类中所有公有的且返回值为double的方法
​
eg4 : execution(public double 全类名.*(double, ..))
   表示支持该类中所有形参列表第一个参数为double类型,且后续参数可以是任意数量任意类型的,公有的返回值为double的方法。
​
eg5 : execution(double 全类名.*(double, double)
     表示支持该类中所有形参列表为两个double类型,公有的且返回值为double类型的方法。

3) 在AspectJ(另一个框架)中,切入点表达式可以通过"&&","||","!"等操作符结合起来。

eg : execution(* *.add(int, ..)) || execution(* *.subtract(int, ..))——表示支持任意类中的任意访问修饰符和任意返回值类型的,且形参列表第一个参数为int类型的add 或 subtract方法。

4)注意事项:

(1) 当切入点表达式直接指向了接口某个实现类的方法(非实现类特有方法),这时切入点表达式仅会对该实现类生效(动态代理 + 反射),即接口的其他实现类不会生效(不会得到代理对象,即使你以接口类型作为接收)。
​
(2) 当切入点表达式指向接口的方法时,切入表达式会对该接口的所有实现类生效。
​
(3) 切入点表达式也可以切入到没有实现接口的类的横切关注点中。(CGlib动态代理模式)
​
PS : JDK Proxy动态代理和CGlib动态代理的区别
​
- JDK动态代理是面向接口的,只能增强实现类中重写了接口中的方法。而CGlib是面向父类的,可以增强父类的所有方法。
- JDK得到的对象是JDK代理对象实例,而CGlib得到的对象是被代理对象的子类。

1.4.3 案例3

AnnotationPointcut.java

package com.shuilidianli.aop;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
​
@Component
@Aspect
public class AnnotationPointcut {
   
    @Before("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")
    public void before(){
        System.out.println("---------方法执行前---------");
    }
   
    @After("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")
    public void after(){
        System.out.println("---------方法执行后---------");
    }
   
    @Around("execution(* com.shuilidianli.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("---环绕前---");
        System.out.println("调用的方法:"+joinPoint.getSignature());
        //执行目标方法
        Object proceed = joinPoint.proceed();
        System.out.println("---环绕后---");
        System.out.println(proceed);
    }
}
​

测试: 注意给UserServiceImpl添加注解

package com.shuilidianli.test;
​
import com.shuilidianli.service.UserService;
import com.shuilidianli.web.EmpController;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class AopTest2 {
    @Test
    public void test1(){
        ApplicationContext ctx =
                new ClassPathXmlApplicationContext("beans.xml");
        UserService us =
                (UserService) ctx.getBean("userServiceImpl");
        us.search();
​
    }
}
​

测试结果:

1.4.4 案例4

1)EmpController.java

package com.sldl.controller;
​
import org.springframework.stereotype.Controller;
​
@Controller   //添加Bean注解
public class EmpController {
​
    public void findAll(){
        System.out.println("---正在查询所有员工信息---");
    }
​
    public void addEmp(){
        System.out.println("---正在添加一个员工信息---");
        String str = null;
        System.out.println(str.length());
    }
}
​

2)Operation.java

package com.sldl.log;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
​
@Component   // 添加Bean注解
@Aspect      // 添加Aop注解
public class OperationLog {
    //后置通知注解
    @After("within(com.sldl.controller..*)")
    public void log(){
        System.out.println("记录日志");
    }
    //环绕通知注解
    @Around("within(com.sldl.controller..*)")
    public Object log1(ProceedingJoinPoint p) throws Throwable{
        //获取目标组件的名字
        String className = p.getTarget().getClass().getName();
        //获取目标组件里执行的方法名
        String methodName = p.getSignature().getName();
        System.out.println("------------");
        //执行目标组件
        Object obj = p.proceed();
        System.out.println(
                "xxx正在执行"+className
                        +"里的"+methodName+"方法");
        return obj;
    }
    //异常抛出通知
    @AfterThrowing(pointcut="within(com.sldl.controller..*)",throwing="e")
    public void log2(Exception e){
        System.out.println(e.toString());
        StackTraceElement[] eles =
                e.getStackTrace();
        System.out.println(eles[0]);
        System.out.println(eles[1]);
    }
}
​

3)beans.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: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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
​
    <!--开启注解扫描功能-->
    <context:component-scan base-package="com.sldl"/>
​
    <!--开启AOP注解扫描-->
    <aop:aspectj-autoproxy proxy-target-class="true"/>
​
</beans>

4)AOPTest

package com.sldl.test;
​
import com.sldl.controller.EmpController;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
​
public class AOPTest {
    @Test
    public void test1(){
        ApplicationContext ctx =
            new ClassPathXmlApplicationContext("beans.xml");
​
        EmpController ec =
                ctx.getBean("empController", EmpController.class);
​
       //ec.findAll();
        ec.addEmp();
​
    }
}

测试结果:

1.4.5 多个通知的执行顺序

五个通知都有的情况下,先后执行顺序

  1. 一定限制性环绕前一步:

  2. 再执行前置通知

  3. 目标方法

  4. 环绕通知的后一部分

  5. 在执行后置通知

  6. 在执行返回通知

  7. 最后执行异常通知

上一篇:利用 Local Data 导入文件到 OceanBase 的方法


下一篇:腾讯云新开端口