springboot AOP的使用
AOP: 面向切面编程。springboot中使用AOP第一步:首先导入两个依赖<!--springboot自带的aop--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId&g
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)