aop,又叫面向切面编程,通俗理解就是,将那些与业务无关,却为业务模块所共同调用的逻辑代码封装起来,形成一个切面,减少重复代码,降低模块间的耦合度,方便后期操作和维护。
Spring AOP属于运行时的增强,而Aspect AOP属于编译时的增强。SpringAOP是基于代理(Proxying),而AspectAOP是基于字节码操作。如果切面比较少,两者差不多,如果切面太多,最好使用AspectAOP,它比SpringAOP快很多。
使用spring-boot-starter-aop启动器默认帮我们引入了对Aspectj的实现
@Aspect -- 作用是把当前类标识为一个切面供容器读取
@Pointcut -- (切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
@Before -- 标识一个前置增强方法,相当于BeforeAdvice的功能
@AfterReturning -- 后置增强,相当于AfterReturningAdvice,方法退出时执行
@AfterThrowing -- 异常抛出增强,相当于ThrowsAdvice
@After -- final增强,不管是抛出异常或者正常退出都会执行
@Around -- 环绕增强,相当于MethodInterceptor
@Aspect
– 作用是把当前类标识为一个切面供容器读取
@Aspect
@Component
@Slf4j
public class LogAspect {
}
@Pointcut -- (切入点)
:就是带有通知的连接点,在程序中主要体现为书写切入点表达式或者基于注解的切入点
基于注解的切入点
@Pointcut("@annotation(cn.zysheep.annotation.Log)")
//@annotation(cn.zysheep.annotation.Log),为自定义注解
public void Pointcut() {}
基于切入点表达式
execution()
是最常用的切点函数
@Pointcut("execution(* cn.zysheep.springaop.service.impl..*.*(..))")
public void Pointcut() {}
整个表达式可以分为四个部分:
1. 第一个*号:表示返回类型, *号表示所有的类型
2. 包名: 表示需要拦截的包名,后面的..表示当前包和当前包的所有子包,cn.zysheep.springaop.service.impl包、子孙包下所有类的方法。
3. 第二个*号: 表示类名,*号表示所有的类。
4. *(..):最后这个星号表示方法名, *号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
// cn.zysheep.controller包中所有的类的所有方法切面
// @Pointcut("execution(public * cn.zysheep.controller.*.*(..))")
// 只针对 UserController 类切面
// @Pointcut("execution(public * cn.zysheep.controller.UserController.*(..))")
// 统一切点,对cn.zysheep及其子包中所有的类的所有方法切面
// @Pointcut("execution(* cn.zysheep.controller.*.*(..))")
个人认为基于注解的方式可以实现更加细粒度的操作,比如日志管理,只要自己声明一个自定义的注解@Log,可以在任意三层中定义到不同的方法中
标识一个前置增强方法,相当于BeforeAdvice的功能
//@Before: 前置通知 ,Pointcut()定义的切入点函数
@Before("Pointcut()")
public void beforeMethod() {
log.info("调用了前置通知");
}
后置增强,相当于AfterReturningAdvice
,方法退出时执行
//@AfterRunning: 返回通知 result为切入点返回内容
@AfterReturning(value="Pointcut()",returning="result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){
log.info("调用了返回通知,result :{}",result);
}
异常抛出增强,相当于ThrowsAdvice
//@AfterThrowing: 异常通知 e为切入点执行异常的信息
@AfterThrowing(value="Pointcut()",throwing="e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e){
log.info("调用了异常通知 joinPoint:{},e :{}",joinPoint, e.getStackTrace());
}
final增强,不管是抛出异常或者正常退出都会执行
//@After: 后置通知
@After("Pointcut()")
public void afterMethod(JoinPoint joinPoint){
//joinPoint: 当前的连接点,即执行的切入点
log.info("调用了后置通知,joinPoint:{}",joinPoint);
}
@Around功能非常强大,作用如下:
//@Around: 环绕通知
@Around("Pointcut()")
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around执行方法之前");
// 执行方法
Object object = proceedingJoinPoint.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
log.info("around执行方法之后--返回值: " +object);
return object;
}
1、通过RequestContextHolder获取url中的参数,RequestContextHolder就是一个典型的ThreadLocal应用,用于在当前线程中获取当前请求及其属性,如果要在service层中使用request,或者其他任何地方,都可以直接调用RequestContextHolder来获取request对象和response对象。
2、 通过 joinPoint.getArgs()
获取请求参数数组
@Before("BrokerAspect()")
public void doBefore(JoinPoint joinPoint){
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
logger.info("URL参数={}",requestAttributes.getRequest().getQueryString());
}
业务场景: 生产环境用户的隐私数据,比如手机号、身份证或者一些账号配置等信息,入库时都要进行不落地脱敏,也就是在进入我们系统时就要实时的脱敏处理。
用户数据进入系统,脱敏处理后持久化到数据库,用户查询数据时还要进行反向解密。这种场景一般需要全局处理,那么用AOP切面来实现在适合不过了。
使用ProceedingJoinPoint时如果要改变参数,必须调用
proceed(Object[] var1)
方法,传入新的参数数组,数组元素类型必须和目标方法相互对应,否则会报ClassCastException异常。
@Slf4j
@Aspect
@Component
public class EncryptHandler {
@Autowired
private StringEncryptor stringEncryptor;
@Pointcut("@annotation(cn.zysheep.annotation.EncryptMethod)")
public void pointCut() {
}
/**
* 1、获取参数进行加密
* 2、执行目标方法,业务操作加密数据
* 3、返回前端为解密数据
* @param joinPoint
* @return
*/
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
/**
* 1、获取参数进行加密
*/
Object[] encrypt = encrypt(joinPoint);
/**
* 2、修改请求参数,执行目标方法,业务操作加密数据
*/
Object result = joinPoint.proceed(encrypt); // 目标方法返回值
/**
* 3、修改返回值,返回前端为解密数据
*/
Object decrypt = decrypt(result);
return decrypt;
}
public Object[] encrypt(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args.length != 0) {
for (int i = 0; i < args.length; i++) {
Object o = args[i];
if (o instanceof String) {
args[i] = stringEncryptor.encrypt(String.valueOf(o));
}
}
}
return args;
}
public Object decrypt(Object result) {
String decrypt = null;
if (result instanceof String) {
decrypt = stringEncryptor.decrypt(String.valueOf(result));
}
return decrypt;
}
}
@EncryptMethod
@GetMapping("/getParam")
public String getParam( @EncryptField String username) {
System.out.println("保存数据库业务操作===>username: "+username);
return username;
}
proceed方法的返回值就是目标方法的返回值,我们可以拿到他做一些修改,或者返回新的数据。
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.0version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
//模块名
String value() default "";
}
@within和@annotation的区别:
@Around("@within(cn.zysheep.annotation.Log") :这个用于拦截标注在类上面的@Log注解
@Around("@annotation(cn.zysheep.annotation.Log") :这个用于拦截标注在方法上面的@Log注解
@Aspect
@Component
@Slf4j
public class LogAspect {
long beginTime;
@Pointcut("@annotation(cn.zysheep.annotation.Log)")
public void Pointcut() {}
@Before("Pointcut()")
public void beforeMethod() {
log.info("调用了前置通知@Before");
beginTime = System.currentTimeMillis();
}
@AfterReturning(value="Pointcut()",returning="result")
public void afterReturningMethod(JoinPoint joinPoint, Object result){
log.info("调用了返回通知@AfterReturning,result :{}",result);
}
@AfterThrowing(value="Pointcut()",throwing="e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e){
log.info("调用了异常通知@AfterThrowing joinPoint:{},e :{}",joinPoint, e.getMessage());
}
@After("Pointcut()")
public void afterMethod(JoinPoint joinPoint){
//joinPoint: 当前的连接点,即执行的切入点
log.info("调用了后置通知@After,joinPoint:{}",joinPoint);
}
@Around("Pointcut()")
public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("@Around执行方法之前");
// 执行方法
Object object = proceedingJoinPoint.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
log.info("@Around执行方法之后--返回值: " +object);
return object;
}
}
@RestController
@Slf4j
public class TestAopController {
@GetMapping("/save")
@Log("保存数据")
public String save() {
log.info("执行了controller中的save方法");
return "保存数据";
}
}
@SpringBootApplication
public class AOPApplication {
public static void main(String[] args) {
SpringApplication.run(AOPApplication.class,args);
}
}
controller
中的save方法Object targetObj =joinPoint.getTarget();
// 可以发挥反射的功能获取关于类的任何信息,例如获取类名如下
String className = joinPoint.getTarget().getClass().getName();
getSignature():是获取到这样的信息 :修饰符+ 包名+组件名(类名) +方法名
String methodName = joinPoint.getSignature().getName()
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
// 获取方法上的注解
Log annoObj= method.getAnnotation(Log.class);
Object[] args = joinPoint.getArgs();