springboot AOP的使用

原文地址:https://blog.csdn.net/qq_45905045/article/details/124886104

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;

}

}

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

文章知识点与官方知识档案匹配,可进一步学习相关知识

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值