Spring面向切面编程-AOP详解

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring面向切面编程-AOP详解

介绍AOP


前言:在Spring中AOP至关重要,通过AOP能为程序方法添加统一功能,集中的解决一些公共问题。


应用场景包含:日志记录,Deubgging调试,tracing,分析与解控记录进行跟踪优化,Authentication 权限、Caching 缓存、Context passing 内容传递、Error handling 错误处理、Lazy loading 懒加载。


P2021.4.23 目前仅接触到使用AOP进行日志记录,鉴权。


一、实现AOP


1.1、全注解形式实现AOP


前提准备(引入jar包)

引入坐标:


<!--   spring、aspect     -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.2</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.2</version>
</dependency>



context包:实现类如ApplicationContext,AnnotationConfigApplicationContext,注解如@ComponentScan,@Configuration,@EnableAspectJAutoProxy。

aop包:当前暂时没有使用到。

core包:如注解@Component

ascetj包:如实现类(或接口)JoinPoint,ProceedingJoinPoint,注解不用多说如:@Aspect、@Pointcut、@Before、@Around等多个通知。


实现AOP(五种通知)



TargetClass(无接口实现类):


import org.springframework.stereotype.Component;
@Component  //注入到bean中(方便AOP能够进行切面)
public class TargetClass {
    public String executeMethod(String arg1,String arg2){
        System.out.println("TargetClass类的executeMethod方法执行了!"+"arg1="+arg1+",arg2="+arg2);
        return "changluyaya"; //后来补充
    }
}


AOPConfig:AOP配置类用于开启切面以及扫描包(扫描如@Component这类注解,来进行之后的切面操作)


import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration  //提供生成bean定义以及服务请求
@ComponentScan("com.aopexer")  //对于指定程序包进行扫描
@EnableAspectJAutoProxy  //开启切面自动代理类
public class AOPConfig {
}


MyAspect:自定义切面,即AOP代理类,包含五个通知方法


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
 * @ClassName LogAscept
 * @Author ChangLu
 * @Date 2021/4/23 19:45
 * @Description 自定义AOP切面
 */
@Component //表明该类是组件
@Aspect //当前类标识为一个切面供容器读取
public class MyAspect {
    @Pointcut("execution(* com.aopexer.TargetClass.*(..))")//设置切入点,方便下面直接调用
    public void point(){}
    //前置通知
    @Before("point()")
    public void before(JoinPoint joinPoint){
        System.out.println(2+"==>@Before");
    }
    //环绕通知
    @Around("point()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println(1+"==>@Around  方法执行前");
        proceedingJoinPoint.proceed();//执行方法
        System.out.println(5+"==>@Around  方法执行后");
    }
    //后置通知:若是想要接收返回值来对返回值进行一些额外操作则需要配置returning属性来获取
    @AfterReturning(value = "point()",returning = "obj")  //这里obj对应属性参数obj
    public void afterReturning(Object obj){
        System.out.println(3+"==>@AfterReturning");
        System.out.println("3后置通知中拿到返回值:"+obj);
    }
    //最终通知
    @After("point()")
    public void after(){
        System.out.println(4+"==>@After");
    }
    //异常通知
    @AfterThrowing("point()")
    public void afterThrowing(){
        System.out.println("抛出异常后"+"==>@AfterThrowing");
    }
}



测试类Test:用于测试AOP的几个通知


public class Test {
    public static void main(String[] args) {
        //获取一个中央接口:从给定的组件类派生bean定义并自动刷新上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
        TargetClass bean = context.getBean("targetClass", TargetClass.class);//或者context.getBean(TargetClass.class);
        bean.executeMethod("参数1","参数2");
    }
}


无异常测试


被代理类中的方法无异常时测试程序:



结论:在无异常情况下,通知顺序为:@Around(方法前)->@Before->目标方法->@AfterReturing->@After->@Around(方法后)。



有异常测试


在执行方法中添加一个异常程序段:




说明:程序中出现异常时,@AfterReturning与@Around(方法后)没有执行。对于@After最终通知依旧执行,通常可以使用该注解来执行一些资源关闭操作,类似于finally()。



二、认识JoinPont与ProceedingJoinPoint


2.1、初识两个接口


JoinPoint:封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象。


ProceedingJoinPoint:与JoinPoint相同就是新增两个方法,一般用于在环绕通知方法中。


JoinPooint与ProceedingJoinPoint都是接口,并且后者继承前者:



JoinPont接口提供的方法如下:



Signature getSignature():获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息。

Object[] getArgs();:获取被代理方法的参数对象。

Object getTarget();:获取被代理的对象。

Object getThis();:获取代理对象。

ProceedingJoinPoint接口:在JoinPont接口基础上多增加了两个方法



Object proceed() throws Throwable:执行目标方法。

Object proceed(Object[] var1) throws Throwable:传入的新的参数去执行目标方法


2.2、JoinPoint使用


其常用方法如下:



下面切面都是基于该类的方法进行测试:


//指定被代理类方法
package com.aopexer;
@Component  //用于注入到bean中
public class TargetClass {
    public void executeMethod(String arg1,String arg2){
        System.out.println("TargetClass类的executeMethod方法执行了!"+"arg1="+arg1+",arg2="+arg2);
    }
}



常见方法测试


//前置通知
@Before("point()")
public void before(JoinPoint joinPoint){
    System.out.println(joinPoint.getKind());//连接点类型
    System.out.println(joinPoint.getSourceLocation());//①无源位置返回null;②返回默认构造函数的定义类的SourceLocation
    System.out.println(joinPoint.getStaticPart());//封装此连接点的静态部分的对象
    //获取传入目标的方法参数(重点)
    Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
        System.out.println(arg);
    }
    System.out.println(joinPoint.getTarget());//获取目标对象(被代理对象)(重点)
    System.out.println(joinPoint.getThis());//获取代理对象(代理对象自己)(重点)
    System.out.println(joinPoint.toString());//execution表达式(描述信息中等)
    System.out.println(joinPoint.toShortString());//execution表达式(描述信息简短)
    System.out.println(joinPoint.toLongString());//execution表达式(描述信息详细)
}



其中参数值即为传入方法的值!

其中的getStaticPart()对象方法:



说明:比较重要的就是执行方法参数值,execution表达式的详细信息以及被代理对象和代理对象。



通过getSignature()获取对象Signature测试


Signature:连接点标签名,一般用于跟踪或记录应用程序相关连接点反射信息。简而言之就是获取目标类名、方法名、参数类型。



//切面代理类
//前置通知
@Before("point()")
public void before(JoinPoint joinPoint){
    joinPoint.
    System.out.println("-----------------");
    Signature signature = joinPoint.getSignature();//连接点签名
    System.out.println(signature.getDeclaringTypeName());//全限定定类名(重点)
    System.out.println(signature.getDeclaringType());//class+全限定类名
    System.out.println(signature.getName());//方法名(重点)
    System.out.println(signature.getModifiers());//方法权限修饰对应值(1=>PUBLIC)
    System.out.println(signature.toString());//返回值+方法全限定类名+参数类型
    System.out.println(signature.toLongString());//权限类型+返回值+方法全限定类名+全限定参数类型
    System.out.println(signature.toShortString());//execution表达式中的一部分=>类名.方法(..)
    System.out.println("-----------------");
}



2.3、ProceedingJoinPoint使用(配合@Around)


介绍:通常ProceedingJoinPoint与@Around环绕通知进行使用。


在2.1中介绍了ProceedingJoinPoint除了包含有JoinPont的方法还包含两个执行方法,其执行的即为被代理类方法,并且其中的一个执行方法能够重新传参再次调用方法。


实例演示


被代理方法:


@Component  //用于注入到bean中
public class TargetClass {
    public int executeMethod(String arg1,String arg2){
        System.out.println("TargetClass类的executeMethod方法执行了!"+"arg1="+arg1+",arg2="+arg2);
        return 1;
    }
}


环绕通知:作用于上面被代理方法


仅提供环绕通知代码,其他通知与1.1章节中一致。
在该环绕通知中,为了演示效果,再次传值并调用了被代理类方法
//环绕通知
@Around("point()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {//该环绕方法应当有返回值
    System.out.println(1+"==>@Around  方法执行前");
    //执行被代理类方法(根据原有参数)
    Object proceedReturn = proceedingJoinPoint.proceed();
    //通过自己传参再次执行被代理类方法
    proceedingJoinPoint.proceed(new Object[]{"hello","world"});
    System.out.println(5+"==>@Around  方法执行后");
    return proceedReturn;
}


注意:该环绕方法应当有返回值,并且应该将执行方法得到的返回值进行返回。


测试程序:


public class Test {
    public static void main(String[] args) {
        //获取一个中央接口:从给定的组件类派生bean定义并自动刷新上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
        TargetClass bean = context.getBean("targetClass", TargetClass.class);//或者context.getBean(TargetClass.class);
        int i = bean.executeMethod("参数1", "参数2");
        System.out.println(i);
    }
}



说明:若是在环绕通知中多次调用被代理类方法,那么就会出现上面程序的现象,2、3、4通知再次执行!环绕通知中的方法是否有返回值应该决定于被代理类方法,仅举一个极端例子:被代理类方法有返回值,环绕方法无返回值,若是在程序中显示获取被代理类方法的返回值就会报出异常,其他情况均无异常。



三、对注解进行切面进行权限校验


参考文章:AOP中获取自定义注解的参数值


开发环境:spring-aspects 5.3.2、spring-boot-starter-web 2.5.2


ValidateAnnotation.java:自定义注解


/**
 * @author changlu
 * @date 2021/07/30 13:36
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateAnnotation {
    String value();
}


UserController.java


@RestController
@RequestMapping("/api/v1/test")
@Slf4j  //在类上添加该接口
public class UserController {
    //添加自定义注解
    @ValidateAnnotation(value = "user")
    @GetMapping("/{id}")
    public ResultBody test(@PathVariable(value = "id")Integer myid){
        users.add(new User((long)111, "changlu", 11, null,null));
        log.info("msg");
        return ResultBody.success(users);
    }
}


AOPConfig.java


/**
 * @author changlu
 * @date 2021/07/30 13:32
 **/
@Component
@Aspect
public class AOPConfig {
    @Pointcut("@annotation(com.changlu.Annotations.ValidateAnnotation)")
    public void test() {}
    //对于@Before需要通过该种形式来对注解进行切面控制 @annotation(annoation)中annoation与方法参数应该一致!
    @Before("test() && @annotation(annoation)")
    @ResponseBody
    public void before(ValidateAnnotation annoation){
        System.out.println(annoation);
        //一旦注解中的value与校验的相符,就抛出异常向前端响应未通过校验
        if("user".equals(annoation.value())){
            throw new MsgException(CommonEnum.PERMISSION_ERROR);
        }
    }
}



上面实现的案例仅仅只是简单表达通过APO来进行拦截校验的功能实现!



实际业务


1、对一条请求的请求接收到响应做日志处理


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
/**
 * @ClassName WebLogAspect
 * @Author ChangLu
 * @Date 2021/8/16 0:04
 * @Description TODO
 */
@Aspect
@Component
@Slf4j
public class WebLogAspect {
    @Pointcut("execution(public * com.changlu.springbootdemo.controller.*.*(..)))")
    public void webLog() {
    }
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        //收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        log.info("URL : " + request.getRequestURL().toString());
        log.info("HTTP_METHOD :" + request.getMethod());
        log.info("IP : " + request.getRemoteAddr());
        log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "."
                + joinPoint.getSignature().getName());
        log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }
    // 通过设置returning = "res"来得到指定返回的响应结果
    @AfterReturning(returning = "res", pointcut = "webLog()")
    public void doAfterReturning(Object res) throws JsonProcessingException {
        //处理完请求,返回内容(借助jackson工具类来对对象进行序列化操作)
        log.info("RESPONSE : " + new ObjectMapper().writeValueAsString(res));
    }
}


执行一条请求后的日志记录:




总结


1、JoinPont与ProceedingJoinPoint都是接口,后者继承了前者并且多了两个执行方法(用于执行被代理类方法)。


2、JoinPoint常用方法有:获取方法名(joinPoint.getSignature().getName())、获取简单类名(joinPoint.getSignature().getDeclaringTypeName())、获取被代理类方法参数值(joinPoint.getArgs())、获取被代理对象(joinPoint.getTarget())、获取代理对象(joinPoint.getThis())。


3、ProceedingJoinPoint使用于@Around环绕通知方法中,提供了执行被代理方法,该调用执行方法前后可进行业务操作,如获取方法执行时间等等。


4、@Around环绕通知方法对于是否设置返回参数应该取决于被代理类方法,建议是带有返回值,因为AOP往往是针对于多个方法执行的,所有一般都是设置返回值为Object。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
Micronaut AOP与代理机制:实现应用功能增强,无需侵入式编程的秘诀
AOP(面向切面编程)能够帮助我们在不修改现有代码的前提下,为应用程序添加新的功能或行为。Micronaut框架中的AOP模块通过动态代理机制实现了这一目标。AOP将横切关注点(如日志记录、事务管理等)从业务逻辑中分离出来,提高模块化程度。在Micronaut中,带有特定注解的类会在启动时生成代理对象,在运行时拦截方法调用并执行额外逻辑。例如,可以通过创建切面类并在目标类上添加注解来记录方法调用信息,从而在不侵入原有代码的情况下增强应用功能,提高代码的可维护性和可扩展性。
60 1
|
6天前
|
安全 Java 编译器
什么是AOP面向切面编程?怎么简单理解?
本文介绍了面向切面编程(AOP)的基本概念和原理,解释了如何通过分离横切关注点(如日志、事务管理等)来增强代码的模块化和可维护性。AOP的核心概念包括切面、连接点、切入点、通知和织入。文章还提供了一个使用Spring AOP的简单示例,展示了如何定义和应用切面。
35 1
什么是AOP面向切面编程?怎么简单理解?
|
19天前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
28 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
4天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
13 1
|
10天前
|
XML Java 开发者
论面向方面的编程技术及其应用(AOP)
【11月更文挑战第2天】随着软件系统的规模和复杂度不断增加,传统的面向过程编程和面向对象编程(OOP)在应对横切关注点(如日志记录、事务管理、安全性检查等)时显得力不从心。面向方面的编程(Aspect-Oriented Programming,简称AOP)作为一种新的编程范式,通过将横切关注点与业务逻辑分离,提高了代码的可维护性、可重用性和可读性。本文首先概述了AOP的基本概念和技术原理,然后结合一个实际项目,详细阐述了在项目实践中使用AOP技术开发的具体步骤,最后分析了使用AOP的原因、开发过程中存在的问题及所使用的技术带来的实际应用效果。
33 5
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
29天前
|
Java 编译器 Spring
Spring AOP 和 AspectJ 的区别
Spring AOP和AspectJ AOP都是面向切面编程(AOP)的实现,但它们在实现方式、灵活性、依赖性、性能和使用场景等方面存在显著区别。‌
48 2
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
117 9
|
1月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
25 1
|
28天前
|
Java 容器
AOP面向切面编程
AOP面向切面编程
38 0