java之路——AOP知识详解以及程序中的应用

在这里插入图片描述

创作不易,希望能得到大家的一个小在这里插入图片描述


前言

在学习AOP中,来了解spring的主要内容。
Spring 的核心内容包括 IOC,AOP等。

IOC
Spring 的 IOC(Inversion of Control)是指通过容器帮用户管理组件及实现对象之间解耦。Spring 中的 IOC 容器可以自动管理对象之间的依赖关系和生命周期。用户只需要通过配置或者注解的方式告诉 Spring 哪些组件需要注入到哪些对象中,Spring 的 IOC 容器就会自动为用户完成依赖注入。

AOP
Spring 的 AOP 是指基于切面的编程,它可以帮助用户通过切面管理系统中的横切关注点,例如日志、事务、安全等等。

Spring 的 AOP 通过动态代理的方式实现,在运行时会根据用户的配置信息在目标对象的方法周围动态的织入切面。这种方式不仅可以让用户通过配置来管理系统的横切关注点,同时也更加灵活,方便做到优先级和模块独立。

在这里插入图片描述

一、AOP是什么?

AOP 全称为面向切面编程,它是一种编程范式,旨在通过将业务逻辑分解成可重用的部分,并织入每个分解单元的横切关注点来提高程序的模块化、可维护性和可重用性。

AOP 提供了一种方式,可以通过拦截器(也称为切点和通知)来对一个应用程序进行分层划分,使得处理日志、事务、安全、性能等方面的问题与业务逻辑部分相分离,以便于管理和模块化。这种方式对于实现系统功能的重用、层次化、更好的遵守设计模式和依赖倒置的原则都很有帮助。

在程序执行过程中,每次调用业务方法,AOP 框架都会自动匹配切面上定义的切点和通知,以达到织入相应的横切行为的目的。通常,切点标识了需要织入的代码位置,而通知是实现织入行为的代码。

AOP 与 OOP(面向对象编程)不同,OOP 主要关注对象的数据和行为,而 AOP 则主要关注横切关注点的分离和重用,它弥补了 OOP 的不足,使程序结构更加灵活和可扩展。

Spring 框架中的 AOP 提供了一种在运行时织入代码的方式,简化了开发人员的代码编写。Spring 的 AOP 实现大量使用了代理模式和装饰者模式,可以满足开发人员对业务逻辑分离和横切关注点技术的需求。

二、使用场景

AOP 在实际开发中有着广泛的应用,包括但不限于以下几个方面:

1. 日志记录: 对系统中的操作进行跟踪和记录,方便定位问题,提高问题处理效率。
2. 缓存管理: 对数据进行缓存管理,降低系统的响应时间和数据库压力。
3. 性能监控: 对系统响应时间、CPU 和内存利用率进行监控,以便及时发现系统中的瓶颈。
4. 事务处理: 对数据库的事务进行管理,保障数据的一致性和可靠性。
5. 权限控制: 对系统中的访问控制进行管理,提供精细的权限控制机制。
6. 异常处理: 对系统中的异常进行集中管理,以便及时处理和解决。
7. 数据校验: 对数据进行校验和验证,以保证数据的有效性和合法性。
8. 消息队列: 对消息队列的消费和生产进行管理,保证消息的可靠性和事务性。

在 Spring 中,AOP 的应用很广泛,如可以通过 Spring 的 AOP 框架实现对日志、事务、异常等方面的管理,以提高代码的重用性和系统的可维护性。同时,Spring 的 AOP 框架也提供了注解和 XML 配置方式,方便用户根据实际需求选择不同的开发方式,进行系统开发和维护。

三、技术要概

AOP 技术中的主要技术包括切面(Aspect)、切点(Pointcut)、通知(Advice)和织入(Weaving)。

1. 切面(Aspect): 切面是横切关注点的抽象,它可以看做是与主要业务逻辑相对应的功能逻辑。切面可以包含多个通知和切点,用于描述对哪些方法进行横切处理。

2. 切点(Pointcut): 切点是指被织入的程序处,开发者可以通过表达式或其他方式指定切点所在的方法或类。切点用于描述在什么地方进行织入行为。

3. 通知(Advice): 通知是在切面的某个切点上执行的行为,包括 Before、After、Around、AfterReturning、AfterThrowing 等几种。通知用于描述何时进行织入行为和织入什么行为。

4. 织入(Weaving): 织入是将切面连接到目标对象上,并创建新的代理对象的过程。织入行为可以在编译期间、类加载期间或运行期间进行,Spring 的 AOP 框架是在运行期间进行织入。

除了以上几个技术,还有其他一些相关的概念和技术,如切面优先级、引入(Introduction)、目标对象和代理对象等。

四、技术详解

1. 切面

在 Spring 框架中,我们可以使用注解或 XML 配置来定义切面,下面分别介绍两种方式:

1. 使用注解来定义切面:

首先需要在 Spring 配置文件中开启 AspectJ 支持:

<aop:aspectj-autoproxy />

然后使用 @Aspect 注解表示这是一个切面:

@Aspect
public class LogAspect {
    // 切点定义
    @Pointcut("execution(* org.example.service.*.*(..))")
    public void servicePointcut() {}

    // 前置通知,记录方法调用入参和当前时间
    @Before("servicePointcut()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("调用 " + method + " 方法");
        System.out.println("入参: " + Arrays.toString(args));
        System.out.println("时间: " + new Date());
    }
}

上面的代码中,我们使用 @Aspect 注解表示这是一个切面,使用 @Pointcut 注解定义切点,在切点表达式中使用 execution 表示切点针对哪些方法进行匹配。在切面中,使用 @Before 注解定义前置通知,通过 JoinPoint 对象获取方法名和入参,然后进行日志记录。

2. 使用 XML 配置来定义切面:

在 Spring 配置文件中,定义切面和切点:

<bean id="logAspect" class="org.example.aspect.LogAspect" />
<aop:config>
    <aop:aspect ref="logAspect">
        <aop:pointcut id="servicePointcut"
            expression="execution(* org.example.service.*.*(..))" />
  
        <aop:before pointcut-ref="servicePointcut" method="logMethodEntry" />
    </aop:aspect>
</aop:config>

在上述代码中,我们使用 <bean> 定义了一个名为 logAspect 的 bean,表示切面的实例化对象。使用 <aop:config> 进行 AOP 配置,使用 <aop:aspect> 定义切面,使用 <aop:pointcut> 定义切点,并在切点表达式中使用 execution 表示切点针对哪些方法进行匹配。最后,使用 <aop:before> 定义前置通知,设置 pointcut-ref 指向刚定义的切点 ID,设置 method 属性指向实际执行的方法名。

在上面的代码中可以实现对 service 包中所有方法进行前置通知,记录方法的入参和调用时间。

2. 切点

在AOP中,切点(Pointcut)用于描述哪些类的哪些方法将会被增强(即应用横切关注点)。

下面以Spring AOP的方式实现一个日志切点,记录service包下所有方法的入参和时间:

@Aspect
@Component
public class LogAspect {

    /**
     * servicePointcut 表示切点名
     * public * org.example.service..*.*(..) 表示匹配所有public修饰符、返回类型任意、包名为org.example.service(包括子包)、类名任意、方法名任意的方法,参数任意
     */
    @Pointcut("execution(public * org.example.service..*.*(..))")
    public void servicePointcut() {}

    @Before("servicePointcut()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("调用" + method + "方法");
        System.out.println("入参" + Arrays.toString(args));
        System.out.println("时间" + new Date());
    }

}

在上面的代码中,使用 @Pointcut 定义切点,切点表达式 execution(public * org.example.service..*.*(..)) 表示匹配所有public修饰符、返回类型任意、包名为org.example.service(包括子包)、类名任意、方法名任意的方法,参数任意。同时,使用@Before注解定义了前置通知 logMethodEntry,在匹配到切点时,会自动执行该切面逻辑,在本例中是记录方法的入参和调用时间。
在实际业务开发中,我们可以根据实际需求编写不同的切点表达式,来实现不同颗粒度的方法拦截和增强,比如只对某些具体的方法进行拦截,或者只对某些接口的实现类进行增强。

3.通知(Advice)

AOP通知(Advice)是AOP的核心概念之一,用于描述在什么时候、在什么位置、执行什么逻辑。Spring AOP提供了5种通知类型:

1、前置通知(Before advice): 在目标方法执行前执行;
2、后置通知(After returning advice): 在目标方法执行后执行,如果方法发生异常,则不执行;
3、异常通知(After throwing advice): 在目标方法抛出异常时执行;
4、后置通知(After advice): 在目标方法执行后执行,无论方法是否发生异常都会执行;
5、环绕通知(Around advice): 可以在目标方法的前、后执行逻辑,还可以决定是否直接调用目标方法;

下面以Spring AOP的方式实现一个日志切面,记录service包下所有方法的入参和时间:

@Aspect
@Component
public class LogAspect {

    /**
     * servicePointcut 表示切点名
     * public * org.example.service..*.*(..) 表示匹配所有public修饰符、返回类型任意、包名为org.example.service(包括子包)、类名任意、方法名任意的方法,参数任意
     */
    @Pointcut("execution(public * org.example.service..*.*(..))")
    public void servicePointcut() {}

    @Before("servicePointcut()")
    public void logMethodEntry(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("调用" + method + "方法");
        System.out.println("入参" + Arrays.toString(args));
        System.out.println("时间" + new Date());
    }

    @AfterReturning(pointcut = "servicePointcut()", returning = "result")
    public void logMethodExit(JoinPoint joinPoint, Object result) {
        String method = joinPoint.getSignature().getName();
        System.out.println("调用" + method + "方法");
        System.out.println("返回值" + result);
        System.out.println("时间" + new Date());
    }

    @AfterThrowing(pointcut = "servicePointcut()", throwing = "e")
    public void logException(JoinPoint joinPoint, Exception e) {
        String method = joinPoint.getSignature().getName();
        System.out.println("调用" + method + "方法");
        System.out.println("异常" + e);
        System.out.println("时间" + new Date());
    }

    @After("servicePointcut()")
    public void logMethodComplete(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        System.out.println("调用" + method + "方法");
        System.out.println("时间" + new Date());
    }

}

在上面的代码中,实现了4种不同类型的通知,分别是前置通知 logMethodEntry、后置通知 logMethodExit、异常通知 logException 和后置通知 logMethodComplete
前置通知在方法执行前打印方法入参和调用时间;
后置通知在方法正常返回后打印方法返回值和调用时间;
异常通知在方法抛出异常时打印异常和调用时间;
后置通知在方法完成(包括正常返回和异常返回)后打印调用时间。

还有一种特殊的通知类型是环绕通知,可以在方法前后执行逻辑,并且可以决定是否直接调用目标方法。环绕通知适用于需要对目标方法进行全面控制的场景。

4.织入

AOP织入即把切面逻辑应用到目标对象的过程,Spring AOP中支持三种织入方式:

1、编译时织入,需要特定的编译器才能实现;

2、类加载时织入(aspectj weaving),需要特定的类加载器才能实现;

3、运行时织入,Spring AOP就是采用这种方式实现的织入;

我们来看一个示例代码,解释如何通过注解实现AOP织入:

@Aspect
@Component
public class LogAspect {

    @Pointcut("execution(public int org.example.service.Calculator.add(int, int)) "
            + "&& args(a, b)")
    public void pointCut(int a, int b) {}

    @Before(value = "pointCut(a, b)", argNames = "a,b")
    public void before(int a, int b) {
        System.out.println("Before Advice:" + a + "," + b);
    }

    @AfterReturning(value = "pointCut(a, b)", argNames = "a,b,returnValue",
            returning = "returnValue")
    public void afterReturning(int a, int b, int returnValue) {  
        System.out.println("AfterReturning Advice:" + a + "," + b + "," + returnValue);
    }

    @AfterThrowing(value = "pointCut(a, b)", argNames = "a,b,ex", throwing = "ex")
    public void afterThrowing(int a, int b, Exception ex) {
        System.out.println("AfterThrowing Advice:" + a + "," + b + "," + ex.getMessage());
    }

    @After(value = "pointCut(a, b)", argNames = "a,b")
    public void after(int a, int b) {
        System.out.println("After Advice:" + a + "," + b);
    }

    @Around(value = "pointCut(a, b)", argNames = "joinPoint,a,b")
    public Object around(ProceedingJoinPoint joinPoint, int a, int b) {
        Object result = null;
        try {
            System.out.println("Around Before Advice:" + a + "," + b);
            result = joinPoint.proceed();
            System.out.println("Around AfterReturning Advice:" + a + "," + b + "," + result);
        } catch (Throwable e) {
            System.out.println("Around AfterThrowing Advice:" + a + "," + b + "," + e.getMessage());
        } finally {
            System.out.println("Around After Advice:" + a + "," + b);
        }
        return result;
    }

}

在上面的代码中,我们定义了一个切面类 LogAspect,并通过注解的方式实现了前置通知 @Before、后置通知 @AfterReturning、异常通知 @AfterThrowing、后置通知 @After 和环绕通知 @Around,分别对目标方法进行了不同类型的拦截。

我们使用注解 @Aspect 来定义切面类,并使用注解 @Component 将切面类注入到Spring IOC容器中。使用注解 @Pointcut 声明切点表达式,并配合注解 @Args 指定参数名,使得切点可以匹配到带参数的方法。接下来,我们定义了一些具体的切面方法,并配合注解 @Before@AfterReturning@AfterThrowing@After@Around 来实现前置、后置、异常和环绕通知的功能。

最后,我们还需要在目标类中调用切面方法,实现AOP织入,需要注意的是,在目标类中必须要使用Spring AOP提供的代理对象来调用方法,否则切面将无法起作用。

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
目前整个开发社区对AOP(Aspect Oriented Programing)推崇备至,也涌现出大量支持AOP的优秀Framework,--Spring, JAC, Jboss AOP 等等。AOP似乎一时之间成了潮流。Java初学者不禁要发出感慨,OOP还没有学通呢,又来AOP。本文不是要在理论上具体阐述何为AOP, 为何要进行AOP . 要详细了解学习AOP可以到它老家http://aosd.net去瞧瞧。这里只是意图通过一个简单的例子向初学者展示一下如何来进行AOP.   为了简单起见,例子没有没有使用任何第三方的AOP Framework, 而是利用Java语言本身自带的动态代理功能来实现AOP.   让我们先回到AOP本身,AOP主要应用于日志记录,性能统计,安全控制,事务处理等方面。它的主要意图就要将日志记录,性能统计,安全控制等等代码从商业逻辑代码清楚的划分出来,我们可以把这些行为一个一个单独看作系统所要解决的问题,就是所谓的面向问题的编程(不知将AOP译作面向问题的编程是否欠妥)。通过对这些行为的分离,我们希望可以将它们独立地配置到商业方法,而要改变这些行为也不需要影响到商业方法代码。   假设系统由一系列的BusinessObject所完成业务逻辑功能,系统要求在每一次业务逻辑处理时要做日志记录。这里我们略去具体的业务逻辑代码。 Java代码 public interface BusinessInterface {  public void processBusiness(); } public class BusinessObject implements BusinessInterface {  private Logger logger = Logger.getLogger(this.getClass().getName());  public void processBusiness(){   try {    logger.info("start to processing...");    //business logic here.    System.out.println(“here is business logic”);    logger.info("end processing...");   } catch (Exception e){    logger.info("exception happends...");    //exception handling   }  } } public interface BusinessInterface {  public void processBusiness(); } public class BusinessObject implements BusinessInterface {  private Logger logger = Logger.getLogger(this.getClass().getName());  public void processBusiness(){   try {    logger.info("start to processing...");    //business logic here.    System.out.println(“here is business logic”);    logger.info("end processing...");   } catch (Exception e){    logger.info("exception happends...");    //exception handling   }  } }   这里处理商业逻辑的代码和日志记录代码混合在一起,这给日后的维护带来一定的困难,并且也会造成大量的代码重复。完全相同的log代码将出现在系统的每一个BusinessObject。   按照AOP的思想,我们应该把日志记录代码分离出来。要将这些代码分离就涉及到一个问题,我们必须知道商业逻辑代码何时被调用,这样我们好插入日志记录代码。一般来说要截获一个方法,我们可以采用回调方法或者动态代理。动态代理一般要更加灵活一些,目前多数的AOP Framework也大都采用了动态代理来实现。这里我们也采用动态代理作为例子。   JDK1.2以后提供了动态代理的支持,程序员通过实现java.lang.reflect.InvocationHandler接口提供一个执行处理器,然后通过java.lang.reflect.Proxy得到一个代理对象,通过这个代理对象来执行商业方法,在商业方法被调用的同时,执行处理器会被自动调用。   有了JDK的这种支持,我们所要做的仅仅是提供一个日志处理器。 Java代码 public class LogHandler implements InvocationHandler {  private Logger logger = Logger.getLogger(this.getClass().getName());   private Object delegate;   public LogHandler(Object delegate){    this.delegate = delegate;   }  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {   Object o = null;   try {    logger.info("method stats..." + method);    o = method.invoke(delegate,args);    logger.info("method ends..." + method);   } catch (Exception e){    logger.info("Exception happends...");    //excetpion handling.   }   return o;  } }   现在我们可以把BusinessObject里面的所有日志处理代码全部去掉了。 public class BusinessObject implements BusinessInterface {  private Logger logger = Logger.getLogger(this.getClass().getName());  public void processBusiness(){   //business processing   System.out.println(“here is business logic”);  } } public class LogHandler implements InvocationHandler {  private Logger logger = Logger.getLogger(this.getClass().getName());   private Object delegate;   public LogHandler(Object delegate){    this.delegate = delegate;   }  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {   Object o = null;   try {    logger.info("method stats..." + method);    o = method.invoke(delegate,args);    logger.info("method ends..." + method);   } catch (Exception e){    logger.info("Exception happends...");    //excetpion handling.   }   return o;  } }   现在我们可以把BusinessObject里面的所有日志处理代码全部去掉了。 public class BusinessObject implements BusinessInterface {  private Logger logger = Logger.getLogger(this.getClass().getName());  public void processBusiness(){   //business processing   System.out.println(“here is business logic”);  } }   客户端调用商业方法的代码如下: Java代码 BusinessInterface businessImp = new BusinessObject(); InvocationHandler handler = new LogHandler(businessImp); BusinessInterface proxy = (BusinessInterface) Proxy.newProxyInstance(  businessImp.getClass().getClassLoader(),  businessImp.getClass().getInterfaces(),  handler); proxy.processBusiness(); BusinessInterface businessImp = new BusinessObject(); InvocationHandler handler = new LogHandler(businessImp); BusinessInterface proxy = (BusinessInterface) Proxy.newProxyInstance(  businessImp.getClass().getClassLoader(),  businessImp.getClass().getInterfaces(),  handler); proxy.processBusiness();   程序输出如下: INFO: method stats... here is business logic INFO: method ends...   至此我们的第一次小尝试算是完成了。可以看到,采用AOP之后,日志记录和业务逻辑代码完全分开了,以后要改变日志记录的话只需要修改日志记录处理器就行了,而业务对象本身(BusinessObject)无需做任何修改。并且这个日志记录不会造成重复代码了,所有的商业处理对象都可以重用这个日志处理器。   当然在实际应用,这个例子就显得太粗糙了。由于JDK的动态代理并没有直接支持一次注册多个InvocationHandler,那么我们对业务处理方法既要日志记录又要性能统计时,就需要自己做一些变通了。一般我们可以自己定义一个Handler接口,然后维护一个队列存放所有Handler, 当InvocationHandler被触发的时候我们依次调用自己的Handler。所幸的是目前几乎所有的AOP Framework都对这方面提供了很好的支持.这里推荐大家使用Spring。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流光CN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值