[华为云在线课程][Spring入门][AOP][二][学习笔记]

John2021 发表于 2022/06/25 07:39:34 2022/06/25
【摘要】 1.AOP简介AOP(Aspect Oriented Programming),面向切面编程。意思就是在程序运行时,不改变源码前提下,动态增强方法的功能,例如:日志,事务,数据库操作等等。这些操作往往有很多模板代码,可以利用AOP消除这些臃肿的模板代码。概念说明切点要添加代码的地方通知(增强)向切点动态添加的代码切面切点+通知连接点切点的定义 2.基于JDK动态代理实现的AOPAOP实际上...

1.AOP简介

AOP(Aspect Oriented Programming),面向切面编程。意思就是在程序运行时,不改变源码前提下,动态增强方法的功能,例如:日志,事务,数据库操作等等。这些操作往往有很多模板代码,可以利用AOP消除这些臃肿的模板代码。

概念 说明
切点 要添加代码的地方
通知(增强) 向切点动态添加的代码
切面 切点+通知
连接点 切点的定义

2.基于JDK动态代理实现的AOP

AOP实际上是基于Java的动态代理来实现的。Java中的动态代理有两种实现方式:cglib和jdk。
以下是一个基于jdk的动态代理实现。
定义一个计算器接口:

package org.aopexample;

public interface Calculator {
    int add(int number1, int number2);
}

实现计算器接口:

package org.aopexample;

public class CalculatorImpl implements Calculator {
    @Override
    public int add(int number1, int number2) {
        return number1 + number2;
    }
}

定义代理类:

package org.aopexample;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class CalculatorProxy {
    public static Object getInstance(CalculatorImpl calculator) {
        return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(),
                calculator.getClass().getInterfaces(), new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println(method.getName() + "方法开始执行。。。");
                        Object invoke = method.invoke(calculator, args);
                        System.out.println(method.getName() + "方法结束执行。。。");
                        return invoke;
                    }
                });
    }
}

newProxyInstance方法接收三个参数,第一个是classloader,第二个是代理多项实现的接口,第三个是代理对象方法的处理器,所有要额外添加的行为都在invoke中实现。

package org.aopexample;

public class CalculatorMain {
    public static void main(String[] args) {
        CalculatorImpl calculator = new CalculatorImpl();
        Calculator calc = (Calculator) CalculatorProxy.getInstance(calculator);
        int add = calc.add(1, 2);
        System.out.println(add);
    }
}

输出结果:

add方法开始执行。。。
add方法结束执行。。。
3

3.AOP五种通知类型

前置,后置,异常,返回,环绕。
根据上面的计算器例子来进行功能增强。
首先引入Spring依赖(AOP)

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.4</version>
    </dependency>
</dependencies>

4.方法中统一定义切点及非侵入式定义切点

导入依赖后就可以开始定义切点,定义切点有两种方式:使用自定义注解,使用规则。
自定义注解是侵入式的,这种方式在实际开发中不推荐仅作了解。使用规则定义切点是无入侵式的,一般推荐使用这个。

4.1.自定义注解

首先定义一个注解

package org.aopexample;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}

然后在需要拦截方法上添加@Action注解,以计算器代码为例即在add方法上添加@Action,表示该方法将会被AOP拦截,其他未添加该注解的方法则不受影响。

package org.aopexample.calculate;

import org.aopexample.Action;
import org.springframework.stereotype.Component;

@Component
public class CalculatorImpl1 {
    @Action
    public int add(int number1, int number2) {
        return number1 + number2;
    }

    public void min(int number1, int number2) {
        System.out.println(number1 + "-" + number2 + "=" + (number1 - number2));
    }
}

接下来定义通知(增强、Advice)

package org.aopexample.useanno;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect //表示这是一个切面
public class LogAspect {
    /**
     * 前置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @Before 注解表示是一个前置通知,即在目标方法执行之前执行,注解中需要填入切点
     */
    @Before(value = "@annotation(Action)")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了。。。");
    }

    /**
     * 后置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("@annotation(Action)")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了。。。");
    }

    /**
     * @AfterReturning 表示是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的returning属性
     * 表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数一致,
     * 否则拦截不到,如果想拦截所有的话(包括返回值为void),则方法返回值参数可以为Object
     */
    @AfterReturning(value = "@annotation(Action)", returning = "r")
    public void returning(JoinPoint joinPoint, Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:" + r);
    }

    /**
     * 异常通知
     * e表示目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者
     * 所抛出的异常的父类,只有这样才会捕获。如果想拦截所有,参数类型声明为Exception
     */
    @AfterThrowing(value = "@annotation(Action)", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛出异常了:" + e.getMessage());
    }

    /**
     * 环绕通知
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似
     * 在这里通过反射执行方法
     *
     * @return 这里的返回值类型最好是Object,和拦截到的方法相匹配
     */
    @Around("@annotation(Action)")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //相当于method.invoke方法,我们可以在这个方法的前后分别添加日志,相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

在配置类中开启包扫描和自动代理

package org.aopexample.useanno;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy//开启自动代理
public class JavaConfig {
}

在启动方法中调用

package org.aopexample.useanno;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(JavaConfig.class);
        CalculatorImpl1 myCalculator = applicationContext.getBean(CalculatorImpl1.class);
        myCalculator.add(3, 4);
        myCalculator.min(3, 4);
    }
}

发现切点的定义不够灵活,因为切点写在注解里,这样要修改切点每个方法都要修改,我们可以统一定义切点,统一调用。

package org.aopexample.useanno;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect //表示这是一个切面
public class LogAspect1 {
    /**
     * 统一定义切点
     * */
    @Pointcut("@annotation(Action)")
    public void pointcut() {
        
    }
    
    /**
     * 前置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @Before 注解表示是一个前置通知,即在目标方法执行之前执行,注解中需要填入切点
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了。。。");
    }

    /**
     * 后置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了。。。");
    }

    /**
     * @AfterReturning 表示是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的returning属性
     * 表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数一致,
     * 否则拦截不到,如果想拦截所有的话(包括返回值为void),则方法返回值参数可以为Object
     */
    @AfterReturning(value = "pointcut()", returning = "r")
    public void returning(JoinPoint joinPoint, Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:" + r);
    }

    /**
     * 异常通知
     * e表示目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者
     * 所抛出的异常的父类,只有这样才会捕获。如果想拦截所有,参数类型声明为Exception
     */
    @AfterThrowing(value = "pointcut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛出异常了:" + e.getMessage());
    }

    /**
     * 环绕通知
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似
     * 在这里通过反射执行方法
     *
     * @return 这里的返回值类型最好是Object,和拦截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //相当于method.invoke方法,我们可以在这个方法的前后分别添加日志,相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

4.2.使用规则

使用注解是侵入式的,要将其改为非入侵式的。重新定义切点,不需要@Action注解,要拦截的目标方法不用添加@Action注解。下面使用规则定义

package org.aopexample.useanno;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect //表示这是一个切面
public class LogAspect2 {
    /**
     * 统一定义切点
     * */
    @Pointcut("@annotion(Action)")
    public void pointcut2() {

    }

    /**
     * 统一定义切点
     * 第一个 * 表示要拦截的目标方法返回值任意(也可以明确返回值类型)
     * 第二个 * 表示包中任意类(可以指定类)
     * 第三个 * 表示类中任意方法
     * 最后两个点表示方法参数任意,个数任意,类型任意
     * */
    @Pointcut("execution(* org.aopexample.useanno.*.*(..))")
    public void pointcut() {

    }

    /**
     * 前置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @Before 注解表示是一个前置通知,即在目标方法执行之前执行,注解中需要填入切点
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了。。。");
    }

    /**
     * 后置通知
     * joinPoint 包含了目标方法的关键信息
     *
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了。。。");
    }

    /**
     * @AfterReturning 表示是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的returning属性
     * 表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数一致,
     * 否则拦截不到,如果想拦截所有的话(包括返回值为void),则方法返回值参数可以为Object
     */
    @AfterReturning(value = "pointcut()", returning = "r")
    public void returning(JoinPoint joinPoint, Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:" + r);
    }

    /**
     * 异常通知
     * e表示目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者
     * 所抛出的异常的父类,只有这样才会捕获。如果想拦截所有,参数类型声明为Exception
     */
    @AfterThrowing(value = "pointcut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛出异常了:" + e.getMessage());
    }

    /**
     * 环绕通知
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似
     * 在这里通过反射执行方法
     *
     * @return 这里的返回值类型最好是Object,和拦截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //相当于method.invoke方法,我们可以在这个方法的前后分别添加日志,相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

5.XML配置AOP

首先导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>benchmarks</artifactId>
        <version>3.12.13</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.4</version>
    </dependency>
</dependencies>

定义通知(增强),不需要添加注解,因为注解在Spring配置文件中定义

package org.aopexample.xml;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;

public class LogAspect {
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了。。。");
    }

    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了。。。");
    }

    public void returning(JoinPoint joinPoint, Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:" + r);
    }

    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛出异常了:" + e.getMessage());
    }

    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //相当于method.invoke方法,我们可以在这个方法的前后分别添加日志,相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

在Spring配置文件中进行AOP配置

<!--    XML配置AOP-->
<bean class="org.aopexample.xml.LogAspect" id="logAspect"/>
<bean class="org.aopexample.xml.CalculatorImpl1" id="calculatorImpl1"/>
<aop:config>
    <aop:pointcut id="pc1" expression="execution(* org.aopexample.xml.*.*(..))"/>
    <aop:aspect ref="logAspect">
        <aop:before method="before" pointcut-ref="pc1"/>
        <aop:after method="after" pointcut-ref="pc1"/>
        <aop:after-returning method="returning" pointcut-ref="pc1" returning="r"/>
        <aop:after-throwing method="afterThrowing" pointcut-ref="pc1" throwing="e"/>
        <aop:around method="around" pointcut-ref="pc1"/>
    </aop:aspect>
</aop:config>
package org.aopexample.xml;

import org.aopexample.useanno.Action;
import org.springframework.stereotype.Component;

//@Component
public class CalculatorImpl1 {
    public int add(int number1, int number2) {
        return number1 + number2;
    }

    public void min(int number1, int number2) {
        System.out.println(number1 + "-" + number2 + "=" + (number1 - number2));
    }
}

在启动方法加载配置文件

package org.aopexample.xml;

import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationcontext.xml");
        CalculatorImpl1 calculatorImpl1 = applicationContext.getBean(CalculatorImpl1.class);
        calculatorImpl1.add(1, 2);
        calculatorImpl1.min(1, 2);
    }
}

输出结果

add方法开始执行了。。。
add方法返回:3
add方法执行结束了。。。
min方法开始执行了。。。
1-2=-1
min方法执行结束了。。。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区),文章链接,文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:cloudbbs@huaweicloud.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。