1. Spring AOP 系列1 —— 初识Spring AOP

1. Spring AOP 系列1 —— 初识Spring AOP

1. 什么是AOP

AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,提升开发效率。

AOP的拦截功能是由java中的动态代理来实现的。说白了,就是在目标类的基础上增加切面逻辑,生成增强的目标类(该切面逻辑或者在目标类函数执行之前,或者目标类函数执行之后,或者在目标类函数抛出异常时候执行。不同的切入时机对应不同的Interceptor的种类,如BeforeAdviseInterceptor,AfterAdviseInterceptor以及ThrowsAdviseInterceptor等)。那么动态代理是如何实现将切面逻辑(advise)织入到目标类方法中去的呢?下面我们就来详细介绍并实现AOP中用到的两种动态代理。AOP的源码中用到了两种动态代理来实现拦截切入功能:jdk动态代理和cglib动态代理。两种方法同时存在,各有优劣。jdk动态代理是由java内部的反射机制来实现的,cglib动态代理底层则是借助asm来实现的。总的来说,反射机制在生成类的过程中比较高效,而asm在生成类之后的相关执行过程中比较高效(可以通过将asm生成的类进行缓存,这样解决asm生成类过程低效问题)。还有一点必须注意:jdk动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,jdk动态代理不能应用。由此可以看出,jdk动态代理有一定的局限性,cglib这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。

2. 应该场景

AOP是处理一些横切行问题。这些横切性问题不会影响到主逻辑的实现,但是会散落到代码的各个部分,难以维护。AOP就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

3. AOP与OOP的区别

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

通过下面的图可以清晰的理解AOP与OOP的区别:

4. AOP中的概念

  1. AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
  2. Join point(连接点):连接点就是Advice在应用程序上执行的点或时机,表示在程序中明确定义的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。
  3. Advice(通知):Advice 定义了在 Pointcut里面定义的程序点具体要做的操作,AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)。
    • Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即
    • AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
    • AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
    • After:在目标方法完成之后做增强,无论目标方法是否成功完成。@After可以指定一个切入点表达式
    • Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint
  4. Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
  5. Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  6. Target(目标对象):织入 Advice 的目标对象。
  7. Weave(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程。
  8. Target Object(目标对象): 包含连接点的对象。也被称作被通知或被代理对象。POJO(Plain Ordinary Java Object)简单的Java对象,实际就是普通JavaBeans。
  9. introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

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

5. 实战

pom.xml

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-aop</artifactId>    <version>2.2.4.RELEASE</version></dependency>

四种实现方式

  1. 经典的基于代理的AOP
  2. @AspectJ注解
  3. 纯POJO切面,通过aop:config标签配置
AOP配置元素描述aop:advisor定义AOP通知器aop:after定义AOP后置通知(不管该方法是否执行成功)aop:after-returning在方法成功执行后调用通知aop:after-throwing在方法抛出异常后调用通知aop:around定义AOP环绕通知aop:aspect定义切面aop:aspect-autoproxy定义@AspectJ注解驱动的切面aop:before定义AOP前置通知aop:config顶层的AOP配置元素,大多数的aop:*包含在aop:config元素内aop:declare-parent为被通知的对象引入额外的接口,并透明的实现aop:pointcut定义切点
  1. 注入式AspectJ切面
AspectJ指示器描述arg()限制连接点匹配参数为指定类型的执行方法@args()限制连接点匹配参数由指定注解标注的执行方法execution()用于匹配是连接点的执行方法this()限制连接点匹配AOP代理的Bean引用为指定类型的类target()限制连接点匹配目标对象为执行类型的类@target()限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解within()限制连接点匹配指定的类型@within()限制连接点匹配指定注解所标注的类型@annotation()限制匹配带有指定注解连接点

1. 原生spring实现

定义一个通用接口,所有实现此接口的类都有一个咸鱼方法和一个测试aop的方法

public interface HelloWorld {    void saltedFish();    void testPrintTime();}

实现1

public class HelloWorldImpl1 implements HelloWorld{    @Override    public void saltedFish() {        System.out.println("this is a salted fish =========== 1");    }    @Override    public void testPrintTime() {        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("testPrintTime 1=============111111111");    }}

实现2

public class HelloWorldImpl2 implements HelloWorld{    @Override    public void saltedFish() {        System.out.println("this is a salted fish =========== 2");    }    @Override    public void testPrintTime() {        try {            Thread.sleep(500);        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("testPrintTime 2=============22222222");    }}

定义一个Advice,实现在连接点之前之后该干的事

public class TimeHandler implements MethodBeforeAdvice, AfterReturningAdvice {    Long before = 0L;    @Override    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {        Long after = System.currentTimeMillis();        System.out.println("==========代理后time, " + after + " ======= 间隔: " + (after - before) + "==========");    }    @Override    public void before(Method method, Object[] objects, Object o) throws Throwable {        before = System.currentTimeMillis();        System.out.println("==========代理前time:" + before + "===========");    }}

通过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"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">    <!-- 定义 -->    <bean id="h1" class="com.example.demo.aop.HelloWorldImpl1"></bean>    <bean id="h2" class="com.example.demo.aop.HelloWorldImpl2"></bean>    <!--  定义advice -->    <bean id="timeHandler" class="com.example.demo.aop.TimeHandler"></bean>    <!--  定义point cut  -->    <bean id="timePointCut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">        <property name="pattern" value=".*testPrintTime"></property>    </bean>    <!-- 切面 关联切入点与通知 -->    <bean id="timeHandlerAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">        <property name="advice" ref="timeHandler"></property>        <property name="pointcut" ref="timePointCut"></property>    </bean>    <!-- 设置代理-->    <bean id="proxy1" class="org.springframework.aop.framework.ProxyFactoryBean">        <!--  代理的对象 -->        <property name="target" ref="h1"></property>        <!-- 使用的切面 -->        <property name="interceptorNames" value="timeHandlerAdvisor"></property>        <!-- 代理接口 -->        <property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>    </bean>    <!-- 设置代理-->    <bean id="proxy2" class="org.springframework.aop.framework.ProxyFactoryBean">        <!--  代理的对象 -->        <property name="target" ref="h2"></property>        <!-- 使用的切面 -->        <property name="interceptorNames" value="timeHandlerAdvisor"></property>        <!-- 代理接口 -->        <property name="interfaces" value="com.example.demo.aop.HelloWorld"></property>    </bean></beans>

测试类

public class AOPTest {    public static void main(String[] args) {        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("./application.xml");        HelloWorld helloWorld1 = (HelloWorld) applicationContext.getBean("proxy1");        HelloWorld helloWorld2 = (HelloWorld) applicationContext.getBean("proxy2");        helloWorld1.saltedFish();        System.out.println("---------------------");        helloWorld1.testPrintTime();        System.out.println("=======================");        helloWorld2.saltedFish();        System.out.println("---------------------");        helloWorld2.testPrintTime();    }}

测试结果

this is a salted fish =========== 1---------------------==========代理前time:1582477901748===========testPrintTime 1=============111111111==========代理后time, 1582477902750 ======= 间隔: 1002=================================this is a salted fish =========== 2---------------------==========代理前time:1582477902750===========testPrintTime 2=============22222222==========代理后time, 1582477903250 ======= 间隔: 500==========
2. 基于AspectJ注解实现

对于将纯POJO申明成切面的方式中,如果不使用@AspectJ,那么就需要使用使用繁琐的XML配置,因此Spring借鉴了AspectJ的切面,以提供注解驱动的AOP,但是本质上依然是使用的SpringAop的动态代理的方式,只是变成模型几乎与AspectJ完全一样。

要在 Springboot中声明 AspectJ 切面, 需在 IOC 容器中将切面声明为 Bean 实例 即加入@Component 注解;当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理。在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类。

pom.xml

<dependency>    <groupId>org.springframework.boot</groupId>    <artifactId>spring-boot-starter-aop</artifactId></dependency>

配置类

@Aspect@Configuration      // 一定要加此注解,或者在入口引入public class AopLogger {    /**     * 标识这个方法是个前置通知,  切点表达式表示执行任意类的任意方法.     * 第一个 * 代表匹配任意修饰符及任意返回值,     * 第二个 * 代表任意类的对象,     * 第三个 * 代表任意方法,     * 参数列表中的 ..  匹配任意数量的参数     *     * @param joinPoint     */    @Before("execution(* com.example.demo.aop..*.*(..))")    public void before(JoinPoint joinPoint) {        String methodName = joinPoint.getSignature().getName();        Object result = Arrays.asList(joinPoint.getArgs());        System.out.printf("method name is: %s, args is: %s%n", methodName, result);    }    @After("execution (* com.example.demo.aop..*.*(..))")    public void after(JoinPoint joinPoint) {        String methodName = joinPoint.getSignature().getName();        System.out.println("after log method name is: " + methodName);    }    //    @AfterReturning(value = "execution(**.*(..))", returning = "result")    @AfterReturning(value = "execution (* com.example.demo.aop..*.*(..))", returning = "result")    public void afterReturn(JoinPoint joinPoint, Object result) {        String methodName = joinPoint.getSignature().getName();        System.out.printf("method name is: %s, and the result is: %s%n", methodName, result);    }    //    @AfterThrowing(value = "execution(**.*(..))", throwing = "e")    @AfterThrowing(value = "execution (* com.example.demo.aop..*.*(..))", throwing = "e")    public void afterThrow(JoinPoint joinPoint, Exception e) {        String methodName = joinPoint.getSignature().getName();        System.out.printf("method name is: %s, and the exception is: %s%n", methodName, e);    }    //    @Around("execution(**.*(..))")    @Around("execution (* com.example.demo.aop..*.*(..))")    public Object around(ProceedingJoinPoint joinPoint) {        StopWatch stopWatch = new StopWatch();        String name = joinPoint.getSignature().getName();        System.out.println("===============↓↓↓↓↓↓↓↓ " + name + " ↓↓↓↓↓↓↓↓=================");        stopWatch.start();        try {            return joinPoint.proceed();        } catch (Throwable throwable) {            throwable.printStackTrace();        } finally {            stopWatch.stop();            System.out.println("===============↑↑↑↑↑↑↑↑ " + name + " ↑↑↑↑↑↑↑↑=================");        }        return null;    }}

测试方法

@Testpublic void testAspectJ() {    helloWorldImpl1.saltedFish();    helloWorldImpl2.testPrintTime();}

测试结果

===============↓↓↓↓↓↓↓↓ saltedFish ↓↓↓↓↓↓↓↓=================method name is: saltedFish, args is: []this is a salted fish =========== 1===============↑↑↑↑↑↑↑↑ saltedFish ↑↑↑↑↑↑↑↑=================after log method name is: saltedFishmethod name is: saltedFish, and the result is: null===============↓↓↓↓↓↓↓↓ testPrintTime ↓↓↓↓↓↓↓↓=================method name is: testPrintTime, args is: []testPrintTime 2=============22222222===============↑↑↑↑↑↑↑↑ testPrintTime ↑↑↑↑↑↑↑↑=================after log method name is: testPrintTimemethod name is: testPrintTime, and the result is: null
免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部