AOP(Aspect Oriented Programming)
称为面向切面编程,它是一种编程思想,是对OOP(Object Oriented Programming)
的补充,可以进一步提高编程效率,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等,Struts2
的拦截器设计就是基于AOP
的思想,是个比较经典的例子
要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web
层级设计中,web
层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面
举例说明,一般在编写web服务接口时,进入某个接口后会检验当前是否需要登录,有部分接口需要登录才能访问,而有部分接口不需要登录也可以访问
按照正常的逻辑而言,我们会这样做:
但是这种方法存在一个问题,即代码的重复度高,每个接口都要编写相当的检测代码,不利于维护和扩展。因此,为了提高代码的复用性,考虑将所有公共代码抽取成一个公共方法,每个接口调用该方法进行检测,这里有点切面的味道了
第二版的方法相比第一版的重复代码减少了,系统的扩展性和代码的复用性也提高了,但是还是存在问题,即每个接口都要调用该方法,也会存在相同代码。因此,便提出了“切面”的概念,将该方法注入到接口调用的某个地方(切点),因此无需编写调用方法的代码,每次在方法运行前会自动在切点处调用该方法,进一步提升了系统的扩展性,并降低了代码之间的耦合度
这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。红框处,就是面向切面编程
切面(Aspect)
切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP
就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中
@Component
@Aspect //可以简单地认为, 使用 @Aspect 注解的类就是切面
public class LogAspect {
}
目标对象(Target)
**目标对象指将要被增强的对象,即包含主业务逻辑的类对象。**或者说是被一个或者多个切面所通知的对象,即就是被通知的对象。这些对象中已经只剩下干干净净的核心业务逻辑代码,所有的共有功能等待AOP容器的切入
连接点(JoinPoint)
程序执行过程中明确的点,如方法的调用或特定的异常被抛出。连接点由两个信息确定:
(1)方法(表示程序执行点,即在哪个目标方法)
(2)相对点(表示方位,即目标方法的什么位置,比如调用前,后等)
简单来说,连接点就是被拦截到的程序执行点,因为Spring只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法
@Before("pointcut()")
public void log(JoinPoint joinPoint) { //这个JoinPoint参数就是连接点
}
切入点(PointCut)
切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配连接点,给满足规则的连接点添加通知
@Pointcut(value = "@annotation(authorityVerify)")
public void pointcut(AuthorityVerify authorityVerify) { //该切入点的匹配规则是所有加了@AuthorityVerify注解的函数,并且通过authorityVerify还可以获得注解中的相关信息
}
@Pointcut("execution(* com.remcarpediem.test.aop.service..*(..))")
public void pointcut() {//该切入点的匹配规则是com.remcarpediem.test.aop.service包下的所有类的所有函数
}
通知(Advice)
通知是AOP在特定切入点上执行的增强处理,是拦截到连接点之后要执行的代码,通知可以分为前置通知Before
、后置通知AfterReturning
、异常通知AfterThrowing
、最终通知After
、环绕通知Around
五类
//@Around说明这是一个环绕通知,环绕通知是指在方法执行前后进行拦截
@Around("pointcut()")
public void log(JoinPoint joinPoint) {
try {
return joinPoint.proceed();//执行方法,环绕通知中必须包含该行代码
} catch (BaseException e) {//即使方法执行报错,也会被环绕通知拦截
throw e;
} catch (Throwable throwable) {
throwable.printStackTrace();
throw new RuntimeException(throwable);
}
}
织入(Weaving)
**织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。**织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理
增强器(Adviser)
Advisor是切面的另外一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。Advisor由切入点和Advice组成。 Advisor这个概念来自于Spring对AOP的支撑,在AspectJ中是没有等价的概念的。Advisor就像是一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个Bean表示,并且必须实现一个默认接口
// AbstractPointcutAdvisor是默认接口
public class LogAdvisor extends AbstractPointcutAdvisor {
private Advice advice; // Advice
private Pointcut pointcut; // 切入点
@PostConstruct
public void init() {
// AnnotationMatchingPointcut是依据修饰类和方法的注解进行拦截的切入点。
this.pointcut = new AnnotationMatchingPointcut((Class) null, Log.class);
// 通知
this.advice = new LogMethodInterceptor();
}
}
注:以上代码示例都是基于SpringAop完成
AOP前面说过是一种编程思想,因此需要具体的实现方式来实现
AOP的实现主要分为静态代理和动态代理,静态代理的实现方式是AspectJ,而动态代理则以Spring AOP为代表
AspectJ
是语言级的实现,它扩展了Java语言,定义了AOP语法AspectJ
在编译期织入代码,它有一个专门的编译器,用来生成遵守Java
字节码规范的class
文件,需要在工程外处理,不易于移植AspectJ
肯定是强于AOP
的AspectJ
是在jdk
基础上实现了,不用额外添加jdk
外的执行文件Spring AOP
使用纯Java
实现,它不需要专门的编译过程,也不需要特殊的类装载器Spring AO
P在运行时通过代理的方式织入代码,只支持方法类型的连接点Spring
支持对AspectJ
的集成Spring AOP
实现主要是通过代理类的方式实现,有java
动态代理和CGLIB
代理两种方式
Java
动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy
,Java
类继承机制不允许多重继承)。而CGLIB
能够代理普通类Java
动态代理使用Java
原生的反射API
进行操作,在生成类上比较高效;CGLIB
使用ASM
框架直接对字节码进行操作,在类的执行过程中比较高效CGLIB
代理Java
动态代理@Documented
@Target(ElementType.METHOD) //表明该注解只能放在方法上
@Retention(RetentionPolicy.RUNTIME) //定义注解保留时间
public @interface AvoidRepeatCommit {
/**
* 避免重复提交的时间间隔,默认为1000ms
* */
int repeatTime() default 1000;
}
@Aspect
@Component
@Slf4j
public class AvoidRepeatCommitAspect {
@Autowired
private RedisCache redisCache;
@Pointcut(value = "@annotation(avoidRepeatCommit)")
public void pointcut(AvoidRepeatCommit avoidRepeatCommit){
}
@Around(value = "pointcut(avoidRepeatCommit)")
public Object doAround(ProceedingJoinPoint joinPoint, AvoidRepeatCommit avoidRepeatCommit) throws Throwable {
//将请求ip、方法名、方法参数封装成hash值,存到redis中,利用redis进行拦截
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String ipAddr = IpUtils.getIpAddr(request);
//获取方法签名
Object[] args = joinPoint.getArgs();
StringBuilder argsSb = new StringBuilder();
for(Object arg : args){
argsSb.append(arg.getClass().getName());
}
Signature signature = joinPoint.getSignature();
String className = signature.getClass().getName();
String methodName = signature.getName();
// 得到类名和方法
String ipKey = String.format("%s#%s#%s", className, methodName, argsSb.toString());
// 转换成HashCode
int ipKeyHashCode = Math.abs(ipKey.hashCode());
String redisIpKey = RedisConstant.AVOID_REPEAT_COMMIT + RedisConstant.REDIS_SEGMENTATION + ipAddr + RedisConstant.REDIS_SEGMENTATION + ipKeyHashCode;
log.info("ipKey={},hashCode={},key={}", ipKey, ipKeyHashCode, redisIpKey);
String repeatVal = redisCache.getCacheObject(redisIpKey);
int repeatTime = avoidRepeatCommit.repeatTime();
if(!StringUtils.isEmpty(repeatVal)){
log.info("请勿重复提交请求");
return R.error("请勿重复提交请求");
}
redisCache.setCacheObject(redisIpKey, StringUtils.getUUID(), repeatTime, TimeUnit.MILLISECONDS);
return joinPoint.proceed();
}
}
在想要使用的方法上加上@AvoidRepeatCommit
注解即可,然后Spring AOP
会自动实现避免重复sql
提交