AOP: 面向切面编程。

springboot中使用AOP

第一步:

首先导入两个依赖

<!--springboot自带的aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第二步:

定义切面类。

为什么使用AOP编程范式?
(1)分离功能需求和非功能需求;
(2)集中处理某一关注点;
(3)侵入性少,增强代码可读性及可维护性。

下边记录AOP切面在springboot中的使用。

AOP的应用场景
权限控制、缓存控制、事务控制、分布式追踪、异常处理等

举个例子

如果在service层的某些方法上需要加上权限验证,使用传统的OOP思想只能在方法内部添加验证,如:

public void insert() {
  checkUserAdmin.check();	//加入权限验证方法
  repository.insert();		//调用dao(mapper)层插入数据库一条记录
}

这样看起来很完美,如果有100个方法都需要验证呢,这样在代码内部添加验证的方式不易我们统一管理,且修改了源代码,具有入侵性。
而使用AOP之后,你可以建一个切面类,对要进行权限验证的方法进行切入即可!

在程序运行时,动态的将代码切入到类的指定方法或者位置上的思想,就是面向切面编程。

AOP的常用的几个术语
(1)Target:目标类,要被代理的类,例如,UserService;

(2)JoinPoint(连接点):所谓的连接点,是指那些被拦截到的方法;

(3)PointCut(切入点):被增强的连接点(所谓的增强其实就是添加的新功能);

(4)Advice(通知、增强),增强代码;

(5)Weaving(织入):是指把增强的advice应用到目标对象target来创建新的代理对象proxy的 过程。

(6)proxy:代理类;

(7)Aspect(切面):是切入点pointcut和通知advice的结合。

常见的五种通知
(1)前置通知(@Before):在我们执行目标方法之前运行
(2)后置通知(@After):在我们执行目标方法结束之后,不管有没有异常

@After(value="execution(* com.example.aspectJ.demo1.ProductDao.findAll(..))")
public void after(){
   System.out.println("最终通知==================");
}

(3)返回通知(@AfterReturning):在我们的目标方法正常返回值后运行
(4)异常通知(AfterThrowing):在我们的目标方法出现异常后运行
(5)环绕通知(@Around):动态代理,需要手动执行jionPoint.process(),其实就是执行我们的目标方法执行之前,相当于前置通知,执行之后就相当于我们的后置通知

关于AOP PointCut() 切入点表达式参数的详解
(1)excution表达式
这个定义的是切入点的位置,称为:execution切点函数。

分为五个部分:
execution(): 表达式主体;
修饰符: 匹配所有目标类以什么修饰的方法(如:public 方法)
返回值:返回值,通常以* 代替,返回什么类型都可以!
描述包名:表示拦截哪个包(需要写包的全路径),通常会写 … 表示当前包和当前包的子 包;
类名:表示要拦截的类, * 表示所有类都拦截,或者拦截指定一个类
方法名():表示要拦截的方法, * 表示所有方法都拦截,或者拦截指定一个方法
():方法后边会有括号,里边可以指定拦截的参数, … 两个点表示拦截任何参数。
execution(
· 修饰符 —— 可以省略,如果省略,就是所有的修饰符都会被拦截!
· 返回值 —— 必填,方法的返回值
· 描述包名 —— 可省略
· 类名 —— 可省略,如果省略,就是所有的类都会被拦截!
· 方法名(参数)—— 必填
· 方法抛出异常 —— 必填
)

代码示例:
匹配所有目标类的public方法;
要拦截的包名为:com.lxc.springboot.service包及子孙包下的所有类方法,… 两个点表示当前包及以下的子孙包,一个点表示当前的包;
两个点后边的 * 星号表示类名,这里要拦截所有的类;
后边的 .*(…) 表示方法,*星号表示所有的方法,(…) 括号中表示方法参数,两个点表示任何参数。

// 定义一个切入点,关于切入点如何定义?
@Pointcut("execution(public * com.lxc.springboot.service..*.*(..))")
public void pointFn(){}
 
// 定义一个通知,在执行pointFn这个方法之前(切入入进去之前),我们需要执行check方法
@Before("pointFn()")
public void check() {
    System.out.println("check"); // 前置通知
}

注意:

切入点表达式可以和操作符(&& || !)结合使用

// 定义一个切入点,关于切入点如何定义?
@Pointcut("execution(public * com.lxc.springboot.service..*.add(..)) || execution(public * com.lxc.springboot.service..*.query(..))")
public void pointFn(){}
 
// 定义一个通知,在执行pointFn这个方法之前(切入入进去之前),我们需要执行check方法
@Before("pointFn()")
public void check() {
    System.out.println("check"); // 前置通知
}

(2)within表达式

//匹配StudentService类里所有方法
@Pointcut("within(com.example.service.StudentService)")
public void matchType(){}

//匹配com.example包及子包下所有类方法
@Pointcut("within(com.example..*)")
public void matchPackage(){}

(3)注解匹配

​ 1> 方法上的注解

只要任何方法上边带有 @MyAnnotation注解,都会打印一段话:
在这里插入图片描述

package com.lxc.springboot.aspact;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**

 * 创建一个切面
   */
   @Aspect // 标注是一个切面
   @Component
   public class Aspact {

   // 定义一个切入点,参数是定义在哪个包。哪个类、哪个方法切入,关于切入点如何定义?
   @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
   public void pointFn(){}

   // 定义一个通知,在执行pointFn这个方法之前(切入进去之前),我们需要执行check方法
   @Before("pointFn()")
   public void check() {
       System.out.println("对带有了@MyAnnotation注解的方法,做check检查");
   }

}

对应的自定义注解为:

  • package com.lxc.springboot.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
    
     * @自定义注解
       */
       @Retention(RetentionPolicy.RUNTIME) // 注解运行在哪一个时期的
       @Target(ElementType.METHOD) // 注解用在哪上边?
       public @interface MyAnnotation {}
    

    测试:

@Service // 让spring扫描到这个包
public class UserService {
    @Resource
    public UserMapper userMapper;

    // 根据id查询单个用户
    @MyAnnotation
    public List<User> getUserById(int id) {
        return userMapper.getUserById(id);
    }

}

带有 @MyAnnotation 注解的方法,左侧有一个小图标标志

在这里插入图片描述

输出结果:

在这里插入图片描述

JoinPoint
上边例子中, 使用的前置通知 @Before() ,除了环绕通知 其他四种通知都会拿到 JoinPoint joinPoint 这么一个参数,下边记录下该参数种的常用方法:

(1)Signature joinPoint.getignature() 获取封装课署名信息的对象,在该对象种,可以获取到 目标方法名、参数、所属class类等信息。
joinPoint.getignature().getDeclaringType().getSimpleName() 获取切入点方法对应的类名
joinPoint.getSignature().getName() 获取切入点方法名

(2)Object[] getArgs()
获取传入目标方法的参数。

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

(4)Object getThis()
获取代理的对象。

package com.lxc.springboot.aspact;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
 
/**
 * 创建一个切面
 */
@Aspect // 标注是一个切面,共容器读取,作用于类
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    // 定义一个通知,在执行pointFn这个方法之前(切入入进去之前),我们需要执行check方法
    @Before("pointFn()")
    public void check(JoinPoint joinPoint) {
        System.out.println("************** 获取切入点的相关信息 **************");
        // 获取切入点的方法
        System.out.println("【切入点方法为】"+joinPoint); // execution(List com.lxc.springboot.service.UserService.getUserById(int))
        // 获取切入点方法名对应的类名
        System.out.println("【切入点方法名的简单类名为】"+joinPoint.getSignature().getDeclaringType().getSimpleName());
        // 获取切入点方法名
        System.out.println("【切入点方法名为】"+joinPoint.getSignature().getName()); // getUserId
        // 获取切入点方法参数列表
        Object[] args = joinPoint.getArgs();
        System.out.println("【切入点方法参数列表】"+Arrays.toString(args)); // 一个集合 [Ljava.lang.Object;@6baa953e
        // 获取被代理的对象
        System.out.println("【被代理的对象】"+joinPoint.getTarget()); // com.lxc.springboot.service.UserService@7ee8e0a8
        // 获取代理的对象
        System.out.println("【代理的对象】"+joinPoint.getThis()); // com.lxc.springboot.service.UserService@7ee8e0a8
 
        System.out.println("----------------------------");
        System.out.println("对带有了@MyAnnotation注解的方法,做check检查");
    }
 
}

在这里插入图片描述

@After() 后置方法 参数 JoinPoint

与前置通知的方法一样,这里不记录了,注意:后置通知获取不到目标方法返回的结果,在后置结果通知中才能获取到。

注意:

无论结果正常返回还是抛出异常,后置通知都会执行!!!

// ... 这里省略包引用
 
@Aspect // 标注是一个切面
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    // 定义一个通知,在执行pointFn这个方法之前(切入入进去之前),我们需要执行check方法
    @Before("pointFn()")
    public void check(JoinPoint joinPoint) {
        System.out.println("前置通知执行了");
    }
    // 在目标方法执行后,无论发生什么异常都会执行通知
    // 【注意】,在后置通知中还不能访问目标方法执行返回的结果,执行结果需要到
    //        返回通知里访问!!!
    @After("pointFn()")
    public void afterCheck(JoinPoint joinPoint) {
        System.out.println("后置通知执行了");
    }
 
}

@AfterReturning 返回通知(后置结果通知)

@AfterReturning (value=“”, returning=“” ) 结果通知参数会有两个,
参数一:value 值就是切点方法;
参数二:returning 值的是结果参数,该值必须要跟下边方法中参数二 中的参数 一致才行。

// ··· 这里省略包的引入
 
@Aspect // 标注是一个切面
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    // 返回通知:在方法正常结束后,时可以拿到方法的返回值的!!!
    // returning 值是result,所以下边方法参数二 的参数也必须为result
    // 通过result我们可以获取到目标方法返回的结果!!!
    @AfterReturning(value = "pointFn()", returning = "result")
    public void afterRe(JoinPoint joinPoint, Object result) {
        System.out.println("==========结果通知执行了==========");
        // joinPoint参数与前置通知、后置通知一样,不记录了
        System.out.println("返回的参数为:"+result);
        System.out.println("返回的JSON格式的参数为:"+JSON.toJSONString(result));
    }
}

在这里插入图片描述

@AfterThrowing 异常通知(后置结果异常通知)

@AfterThrowing (value = “pointFn()”, throwing = “e”) 异常通知参数会有两个,
参数一:value 值就是切点方法;
参数二:throwing 值的是异常参数,该值必须要跟下边方法中参数二 中的参数 一致才行。

// ··· 省略包的引入
@Aspect // 标注是一个切面
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    // 在目标方法出现异常时会执行的代码,可以获取到异常对象、信息等
    @AfterThrowing(value = "pointFn()", throwing = "e")
    public void afterTh(JoinPoint joinPoint, Exception e) {
        // joinPoint参数与前置通知、后置通知一样,不记录了
        // 获取异常信息(这个异常信息是我们自定义的!)
        System.out.println("==========结果通知执行了==========");
        System.out.println(e.getMessage());
    }
 
}
// 插入数据
@MyAnnotation
public void insertUser(Map<String, Object> user) throws Exception {
    throw new Exception("故意抛出一个错误,让AfterThrowing通知");
}

在这里插入图片描述

@Around 环绕通知(等于前置通知 + 返回通知 + 异常通知 + 后置通知)

环绕通知是通知类行中功能最强大的,它是JoinPoint的子接口,环绕通知需要携带 ProceedingJoinPoint 类型的参数。

环绕通知的几个重点:

(1)环绕通知类似于动态代理的全过程,ProceedingJoinPoint pjp 类型的参数可以决定是否执行目标方法(也就是说必须要手动调用 pip.proceed() 方法,目标方法才能执行), 如果忘记这样做就会导致通知被执行了 , 但目标方法没有被执行 ,且让环绕通知必须要有返回值,返回值即目标方法的返回值。

(2)如果环绕通知没有返回值,会出现空指针异常的情况。

// ···· 此处省略引入包
@Aspect // 标注是一个切面
@Component
public class Aspact {
 
    // 定义一个切入点,关于切入点如何定义?
    @Pointcut("@annotation(com.lxc.springboot.annotation.MyAnnotation)")
    public void pointFn(){}
 
    @Around("pointFn()")
    public void aroundFn(ProceedingJoinPoint pjp) {
        String methodName = pjp.getSignature().getName();
        System.out.println("==========环绕通知执行了==========");
        Object result = null;
        try{
            // == 前置通知
            System.out.println("【目标方法】"+methodName);
            // 执行目标方法
            result = pjp.proceed();
            // == 结果通知
            System.out.println("目标方法返回结果为:"+result);
        }catch (Throwable e) {
            // == 异常通知
            System.out.println(e.getMessage());
        }
        // == 后置通知
        System.out.println("后置通知执行");
    }
}

在这里插入图片描述

除了环绕通知和异常通知,来看下 前置通知、结果通知和后置通知的执行顺序
在这里插入图片描述

指定切面优先级问题
• 在同一个目标方法上应用多个切面时,除非明确指定,否则他们的的优先级时不确定的。
• 切面的优先级可以通过实现Ordered接口或利用Order注解指定。
• 实现 Ordered 接口 , getOrder () 方法的返回值越小 , 优先级越高,切面出的越在前面。
• 若使用 @Order 注解 , 序号出现在注解中

// 优先级最高
@Order(1)
@Aspect
@Component
public class ValidationAspect {
    @Before("execution(public int Spring4_AOP.aopAnnotation.*.*(int ,int))")
    public void validateArgs(JoinPoint joinPoint){
        System.out.println("-->validate:" + Arrays.asList(joinPoint.getArgs()));
    }
}
// 优先级最低
@Order(2)
@Aspect
@Component
public class LoggerAscept{
    @Before("execution(public int Spring4_AOP.aopAnnotation.*.*(int ,int))")
    public void loggerAscept(JoinPoint joinPoint){
        // ··· 
    }
}

最后,来看下同时使用拦截器和aop时,执行顺序

很明显拦截器在最外层执行,而aop切面在里层执行
在这里插入图片描述

小例子

例一:
使用AOP,打印输出接口耗时、请求参数和返回参数

package com.lxc.springboot.acept;
 
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import com.lxc.springboot.domain.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
 
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
 
@Aspect
@Component // springboot最基本的注解,表示把这个类交给spring来管理
public class AspectLog {
    Logger LOG = LoggerFactory.getLogger(AspectLog.class);
    @Pointcut("execution(* com.lxc.springboot.service.*.*(..))")
    public void cutPoint(){};
    @Before("cutPoint()")
    public void doBefore(JoinPoint joinPoint) {
        long startTime = System.currentTimeMillis();
        // 开始打印日志
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attr.getRequest(); // 获取请求上下文
        Signature signature = joinPoint.getSignature(); // 目标方法名
        // 打印日志信息
        LOG.info("目标方法:{}, 对应的类名:{}", signature.getName(), signature.getDeclaringType().getSimpleName());
        LOG.info("请求方法:{}", request.getMethod());
        LOG.info("请求地址:{}", request.getRequestURL());
        LOG.info("远程地址:{}", request.getRemoteAddr());
        LOG.info("远程域名:{}", request.getRemoteHost());
        LOG.info("端口号:{}", request.getRemotePort());
        // 打印传递的参数
        Object[] args = joinPoint.getArgs();
        Object[] filterArgs = new Object[args.length]; // 创建一个新的集合,初始化长度
        // 只要是ServletRequest、ServletResponse、MultipartFile都不会添加到filterArgs中
        for(int i = 0; i < args.length; i++) {
            if(args[i] instanceof ServletRequest
                    || args[i] instanceof ServletResponse
                    || args[i] instanceof MultipartFile) {
                continue;
            }
            filterArgs[i] = args[i];
        }
        // 排除敏感字段/太长的字段都不会显示
        String[] excludeProperties = {"password", "file"};
        PropertyPreFilters filters = new PropertyPreFilters();
        PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
        excludeFilter.addExcludes(excludeProperties);
        LOG.info("请求的参数为:{}", JSONObject.toJSONString(filterArgs, excludeFilter));
    }
    @Around("cutPoint()")
    public Object doAround(ProceedingJoinPoint pjp) {
        Object result = null;
        try {
            // 前置通知
            long StartTime = System.currentTimeMillis();
            result = pjp.proceed();
            // 排除敏感字段/太长的字段都不会显示
            String[] excludeProperties = {"password", "file"};
            PropertyPreFilters filters = new PropertyPreFilters();
            PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter();
            excludeFilter.addExcludes(excludeProperties);
            LOG.info("返回的结果为:{}", JSONObject.toJSONString(result, excludeFilter));
            LOG.info("====================结果耗时:{} ms =====================", System.currentTimeMillis() - StartTime);
        }catch (Throwable e) {
            System.out.println("aop异常通知");
        }
        System.out.println("aop后置通知");
        return result;
    }
}

前端每次请求都会获取到详细的信息:有远程电脑的信息、请求路径、方法、传递的参数,后台返回的参数及接口的响应耗时!!!

img

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐