<span>Spring 详解(二)------- AOP关键概念以及两种实现方式</span>



### 1. AOP 关键词
- target:目标类,需要被代理的类。例如:ArithmeticCalculator - Joinpoint(连接点):所谓连接点是指那些可能被拦截到的方法。例如:所有的方法 - PointCut 切入点:已经被增强的连接点。例如:add() - advice:通知/增强,增强代码。例如:showRaram、showResult - Weaving(织入):是指把增强 advice 应用到目标对象 target 来创建新的代理对象proxy的过程. - proxy 代理类:通知+切入点 - Aspect(切面)::是切入点 pointcut 和通知 advice 的结合

2. AOP 的作用


当我们为系统做参数验证,登录权限验证或者日志操作等,为了实现代码复用,我们可能把日志处理抽离成一个新的方法。但是这样我们仍然必须手动插入这些方法,这样的话模块之间高耦合,不利于后期的维护和功能的扩展,有了 AOP 我们可以将功能抽成一个切面,代码复用好,低耦合。

3. AOP 的通知类型


Spring 按照通知 Advice 在目标类方法的连接点位置,可以分为5类 - 前置通知[Before advice]:在连接点前面执行,前置通知不会影响连接点的执行,除非此处抛出异常。 - 正常返回通知[After returning advice]:在连接点正常执行完成后执行,如果连接点抛出异常,则不会执行。 - 异常返回通知[After throwing advice]:在连接点抛出异常后执行。 - 返回通知[After (finally) advice]:在连接点执行完成后执行,不管是正常执行完成,还是抛出异常,都会执行返回通知中的内容。 - 环绕通知[Around advice]:环绕通知围绕在连接点前后,比如一个方法调用的前后。这是最强大的通知类型,能在方法调用前后自定义一些操作。环绕通知还需要负责决定是继续处理join point(调用ProceedingJoinPoint的proceed方法)还是中断执行。
Spring 中使用五种通知
1. 前置通知
    <aop:before method="" pointcut="" pointcut-ref=""/>
        method : 通知,及方法名
        pointcut :切入点表达式,此表达式只能当前通知使用。
        pointcut-ref : 切入点引用,可以与其他通知共享切入点。
    通知方法格式:public void myBefore(JoinPoint joinPoint){
        参数1:org.aspectj.lang.JoinPoint  用于描述连接点(目标方法),获得目标方法名等
2. 后置通知  目标方法后执行,获得返回值
    <aop:after-returning method="" pointcut-ref="" returning=""/>
        returning 通知方法第二个参数的名称
   通知方法格式:public void myAfterReturning(JoinPoint joinPoint,Object result){
        参数1:连接点描述
        参数2:类型Object,参数名 returning="result" 配置的
3. 异常通知  目标方法发生异常后
    <aop:after-throwing method="testException" throwing="e"
    pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>
        throwing 发生的异常
   通知方法格式:public Object testRound(ProceedingJoinPoint pjp){
        参数1:ProceedingJoinPoint
        返回值为 reslut

### 4. 基于 xml 的配置方式 xml 配置文件 <xml> <beans xmlns="http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;beans" xmlns&#58;aop="http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;aop" xmlns&#58;context="http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;context" xmlns&#58;xsi="http&#58;&#47;&#47;www&#46;w3&#46;org&#47;2001&#47;XMLSchema&#45;instance" xsi&#58;schemalocation="http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;beans http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;beans&#47;spring&#45;beans&#46;xsd http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;context http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;context&#47;spring&#45;context&#46;xsd http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;aop http&#58;&#47;&#47;www&#46;springframework&#46;org&#47;schema&#47;aop&#47;spring&#45;aop&#46;xsd">
<context:component-scan base-package="com.anqi">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--1、 创建目标类 -->
<bean id="arithmeticCalculator" class="com.anqi.testAop.ArithmeticCalculatorImpl"></bean>
<!--2、创建切面类(通知)  -->
<bean id="logAspect" class="com.anqi.testAop.MyLogger"></bean>
<aop:config>
    <aop:aspect ref="logAspect">
        <!-- 切入点表达式 也可以在通知内部分别设置切入点表达式 -->
        <aop:pointcut expression="execution(* com.anqi.testAop.*.*(..))" id="myPointCut"/>
        <!-- 配置前置通知,注意 method 的值要和 对应切面的类方法名称相同 -->
        <aop:before method="before" pointcut-ref="myPointCut" />
        <aop:after method="after" pointcut-ref="myPointCut" />
        <aop:after-returning method="testAfterReturn" returning="result" pointcut-ref="myPointCut"/>
        <aop:after-throwing method="testException" throwing="e" pointcut="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))"/>
        <!--<aop:around method="testRound"  pointcut-ref="myPointCut"  /> 最强大,但是一般不使用-->
    </aop:aspect>
</aop:config>
</beans> </xml>

目标类

public interface ArithmeticCalculator {
    int add(int i, int j);
    int sub(int i, int j);

    int mul(int i, int j);
    int div(int i, int j);
}

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

切面类 ``` java import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import java.util.Arrays;

/**

  • 创建日志类
    */
    public class MyLogger {

    public void before(JoinPoint joinPoint) {
    System.out.println("前置通知 参数为["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");
    }
    public void after(JoinPoint joinPoint) {
    System.out.println("后置通知 "+ joinPoint.getSignature().getName());
    }

    public void testException(JoinPoint joinPoint, Throwable e) {
    System.out.println("抛出异常: "+ e.getMessage());
    }

    public void testAfterReturn(JoinPoint joinPoint, Object result) {
    System.out.println("返回通知,返回值为 " + result);
    }

    public Object testRound(ProceedingJoinPoint pjp) {
    Object result = null;
    String methodName = pjp.getSignature().getName();
    Object[] args = pjp.getArgs();
    try {
    //前置通知
    System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
    //执行目标方法
    result = pjp.proceed();
    //返回通知
    System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);

     }catch(Throwable e) {
         //异常通知
         System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);
     }
     //后置通知
     System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);
     return result;
    

    }
    }

<br/>
测试
``` java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
    public static void main(String[] args) {
        ApplicationContext application = new ClassPathXmlApplicationContext("spring-context.xml");
        ArithmeticCalculator a = application.getBean(ArithmeticCalculator.class);
        int result = a.add(1,2);
        System.out.println(result);
        System.out.println(a.div(5,0));
    }
}
/*
    前置通知 参数为[1,2]
    后置通知 add
    返回通知,返回值为 3
    3
    前置通知 参数为[5,0]
    后置通知 div
    抛出异常: / by zero
*/

5. 基于注解的配置方式

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.anqi">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!-- 使 AspectJ 注解起作用: 自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy/>
</beans>

目标类 ``` java public interface ArithmeticCalculator { int add(int i, int j); int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);

}
import org.springframework.stereotype.Service;

@Service
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}

@Override
public int sub(int i, int j) {
    int result = i - j;
    return result;
}

@Override
public int mul(int i, int j) {
    int result = i * j;
    return result;
}

@Override
public int div(int i, int j) {
    int result = i / j;
    return result;
}

}

<br>
切面
``` java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;

/**
 * 创建日志类
 */
@Aspect
@Component
public class MyLogger {

    @Before("execution(* com.anqi.testAop.*.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知 参数为["+joinPoint.getArgs()[0]+","+joinPoint.getArgs()[1]+"]");
    }
    @After("execution(* com.anqi.testAop.*.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("后置通知 "+ joinPoint.getSignature().getName());
    }

    @AfterThrowing(value="execution(* com.anqi.testAop.ArithmeticCalculator.div(..))", throwing = "e")
    public void testException(JoinPoint joinPoint, Throwable e) {
        System.out.println("抛出异常: "+ e.getMessage());
    }

    @AfterReturning(value="execution(* com.anqi.testAop.*.*(..))", returning = "result")
    public void testAfterReturn(JoinPoint joinPoint, Object result) {
        System.out.println("返回通知,返回值为 " + result);
    }

    @Around("execution(* com.anqi.testAop.*.*(..))")
    public Object testRound(ProceedingJoinPoint pjp) {
        Object result = null;
        String methodName = pjp.getSignature().getName();
        Object[] args = pjp.getArgs();
        try {
            //前置通知
            System.out.println("!!!前置通知 --> The Method"+methodName+" begins"+ Arrays.asList(args));
            //执行目标方法
            result = pjp.proceed();
            //返回通知
            System.out.println("!!!返回通知 --> The Method"+methodName+" ends"+ args);

        }catch(Throwable e) {
            //异常通知
            System.out.println("!!!异常通知 --> The Method"+methodName+" ends with"+ result);
        }
        //后置通知
        System.out.println("!!!后置通知 --> The Method"+methodName+" ends"+ args);
        return result;
    }
}


输出结果与第一种方式一致,这里就不再赘述了。

6. 切面的优先级


可以使用@Order来指定切面的优先级 ``` java //参数验证切面 @Order @Aspect @Component public class ValidateAspect {

@Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
public void validateArgs(JoinPoint join) {
String methodName = join.getSignature().getName();
Object[] args = join.getArgs();
System.out.println("validate"+methodName+Arrays.asList(args));
}
}

//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Order
@Aspect
@Component
public class LoggingAspect2 {

/**

  • 声明该方法是一个前置通知: 在目标方法开始之前执行
  • @param join
    /
    @Before("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.
    (int, int))")
    public void beforeMehod(JoinPoint join) {
    String methodName = join.getSignature().getName();
    List<object> args = Arrays.asList(join.getArgs());
    System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
    }
    }</object>
### 7. 重用切点表达式
``` java
//把这个类声明为一个切面:需要把该类放入到 IOC 容器中, 再声明为一个切面
@Order
@Aspect
@Component
public class LoggingAspect {

    /**
     * 定义一个方法, 用于声明切入点表达式, 一般地, 该方法中再不需要填入其他代码
     */
    @Pointcut("execution(public int com.anqi.spring.aop.order.ArithmeticCalculator.*(int, int))")
    public void declareJointPointExpression() {}


    /**
     * 声明该方法是一个前置通知: 在目标方法开始之前执行
     * @param join
     */
    @Before("declareJointPointExpression()")
    public void beforeMehod(JoinPoint join) {
        String methodName = join.getSignature().getName();
        List<Object> args = Arrays.asList(join.getArgs());
        System.out.println("前置通知 --> The Method"+methodName+" begins"+ args);
    }
}

### 8. 两种方式的比较(摘自 spring 官方文档)
如果您选择使用Spring AOP,则可以选择@AspectJ或XML样式。需要考虑各种权衡。

XML样式可能是现有Spring用户最熟悉的,并且由真正的POJO支持。当使用AOP作为配置企业服务的工具时,XML可能是一个不错的选择(一个好的测试是你是否认为切入点表达式是你可能想要独立改变的配置的一部分)。使用XML样式,从您的配置可以更清楚地了解系统中存在哪些方面。

XML风格有两个缺点。首先,它没有完全封装它在一个地方解决的要求的实现。DRY原则规定,系统中的任何知识都应该有单一,明确,权威的表示。使用XML样式时,有关如何实现需求的知识将分支到支持bean类的声明和配置文件中的XML。使用@AspectJ样式时,此信息封装在单个模块中:方面。其次,XML样式在它所表达的内容方面比@AspectJ样式稍微受限:仅支持“单例”方面实例化模型,并且不可能组合在XML中声明的命名切入点。例如,

@Pointcut("execution(* get*())")
public void propertyAccess() {}

@Pointcut("execution(org.xyz.Account+ *(..))")
public void operationReturningAnAccount() {}

@Pointcut("propertyAccess() && operationReturningAnAccount()")
public void accountPropertyAccess() {}

在XML样式中,您可以声明前两个切入点:

<aop:pointcut id="propertyAccess"
        expression="execution(* get*())"/>

<aop:pointcut id="operationReturningAnAccount"
        expression="execution(org.xyz.Account+ *(..))"/>

XML方法的缺点是您无法 accountPropertyAccess通过组合这些定义来定义切入点。

@AspectJ 样式支持额外的实例化模型和更丰富的切入点组合。它具有将方面保持为模块化单元的优点。它还具有以下优点:Spring AOP 和 AspectJ 都可以理解(并因此消耗)@AspectJ 方面。因此,如果您以后决定需要 AspectJ 的功能来实现其他要求,则可以轻松迁移到基于 AspectJ 的方法。总而言之,只要您的方面不仅仅是简单的企业服务配置,Spring 团队更喜欢 @AspectJ 风格。

全部评论

相关推荐

03-03 23:12
已编辑
北京邮电大学 Java
书海为家:我来给一点点小建议,因为毕竟还在学校不像工作几年的老鸟有丰富的项目经验,面试官在面试在校生的时候更关注咱们同学的做事逻辑和思路,所以最好在简历中描述下自己做过项目的完整过程,比如需求怎么来的,你对需求的解读,你想到的解决办法,遇到困难如何找人求助,最终项目做成了什么程度,你从中收获了哪些技能,你有什么感悟。
你的简历改到第几版了
点赞 评论 收藏
分享
上周组里招人,我面了六个候选人,回来跟同事吃饭的时候聊起一个让我挺感慨的现象。前三个候选人,算法题写得都不错。第一道二分查找,五分钟之内给出解法,边界条件也处理得干净。第二道动态规划,状态转移方程写对了,空间复杂度也优化了一版。我翻他们的简历,力扣刷题量都在300以上。后三个呢,就有点参差不齐了。有的边界条件没处理好,有的直接说这道题没刷过能不能换个思路讲讲。其中有一个女生,我印象特别深——她拿到题之后没有马上写,而是先问我:“面试官,我能先跟你确认一下我对题目的理解吗?”然后她把自己的思路讲了一遍,虽然最后代码写得不是最优解,但整个沟通过程非常顺畅。这个女生的代码不是最优的,但当我问她“如果这里是线上环境,你会怎么设计’的时候,她给我讲了一套完整的方案——异常怎么处理、日志怎么打、怎么平滑发布。她对这是之前在实习的时候踩过的坑。”我在想LeetCode到底在筛选什么?我自己的经历可能有点代表性。我当年校招的时候,也是刷了三百多道题才敢去面试。那时候大家都刷,你不刷就过不了笔试关。后来工作了,前三年基本没再打开过力扣。真正干活的时候,没人让你写反转链表,也没人让你手撕红黑树。更多的是:这个接口为什么慢了、那个服务为什么OOM了、线上数据对不上了得排查一下。所以后来我当面试官,慢慢调整了自己的评判标准。算法题我还会出,但目的变了。我出算法题,不是想看你能不能背出最优解。而是想看你拿到一个陌生问题的时候,是怎么思考的。你会先理清题意吗?你会主动问边界条件吗?你想不出来的时候会怎么办?你写出来的代码,变量命名乱不乱、结构清不清楚?这些才是工作中真正用得到的能力。LeetCode是一个工具,不是目的。它帮你熟悉数据结构和常见算法思路,这没问题。但如果你刷了三百道题,却说不清楚自己的项目解决了什么问题、遇到了什么困难、你是怎么解决的,那这三百道题可能真的白刷了。所以还要不要刷LeetCode?要刷,但别只刷题。刷题的时候,多问自己几个为什么:为什么用这个数据结构?为什么这个解法比那个好?如果换个条件,解法还成立吗?把刷题当成锻炼思维的方式,而不是背答案的任务。毕竟面试官想看到的,从来不是一台背题机器,而是一个能解决问题的人。
AI时代还有必要刷lee...
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务