AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理

AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理

1 AOP介绍


1.1 基本定义

AOP(Aspect Oriented Programming)称为面向切面编程,它是一种编程思想,是对OOP(Object Oriented Programming)的补充,可以进一步提高编程效率,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子

1.2 解释

要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面

举例说明,一般在编写web服务接口时,进入某个接口后会检验当前是否需要登录,有部分接口需要登录才能访问,而有部分接口不需要登录也可以访问

按照正常的逻辑而言,我们会这样做:

AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理_第1张图片

但是这种方法存在一个问题,即代码的重复度高,每个接口都要编写相当的检测代码,不利于维护和扩展。因此,为了提高代码的复用性,考虑将所有公共代码抽取成一个公共方法,每个接口调用该方法进行检测,这里有点切面的味道了

AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理_第2张图片

第二版的方法相比第一版的重复代码减少了,系统的扩展性和代码的复用性也提高了,但是还是存在问题,即每个接口都要调用该方法,也会存在相同代码。因此,便提出了“切面”的概念,将该方法注入到接口调用的某个地方(切点),因此无需编写调用方法的代码,每次在方法运行前会自动在切点处调用该方法,进一步提升了系统的扩展性,并降低了代码之间的耦合度

AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理_第3张图片

这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。红框处,就是面向切面编程

1.3 AOP中的相关概念

  • 切面(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、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理_第4张图片

2 AOP实现


AOP前面说过是一种编程思想,因此需要具体的实现方式来实现

AOP的实现主要分为静态代理和动态代理,静态代理的实现方式是AspectJ,而动态代理则以Spring AOP为代表

  • AspectJ(静态)
    • AspectJ是语言级的实现,它扩展了Java语言,定义了AOP语法
    • AspectJ在编译期织入代码,它有一个专门的编译器,用来生成遵守Java字节码规范的class文件,需要在工程外处理,不易于移植
    • 因为是编译器期执行,在运行时没有开销,所以性能上AspectJ肯定是强于AOP
    • AspectJ是在jdk基础上实现了,不用额外添加jdk外的执行文件
  • Spring AOP(动态)
    • Spring AOP使用纯Java实现,它不需要专门的编译过程,也不需要特殊的类装载器
    • Spring AOP在运行时通过代理的方式织入代码,只支持方法类型的连接点
    • Spring支持对AspectJ的集成

AOP、ASPECT、Spring AOP、JDK动态代理、CGLib动态代理_第5张图片

3 Spring AOP实现


Spring AOP实现主要是通过代理类的方式实现,有java动态代理和CGLIB代理两种方式

3.1 CGLIB和Java动态代理的区别

  • Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为ProxyJava类继承机制不允许多重继承)。而CGLIB能够代理普通类
  • Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效

3.2 Spring AOP默认的使用方式

  • 如果目标对象没有实现接口,则默认会采用CGLIB代理
  • 如果目标对象实现了接口,默认会采用Java动态代理

3.3 SpringAOP的具体案例–利用注解避免sql语句重复提交

3.3.1 自定义注解类

@Documented 
@Target(ElementType.METHOD) //表明该注解只能放在方法上
@Retention(RetentionPolicy.RUNTIME) //定义注解保留时间  
public @interface AvoidRepeatCommit {

    /**
     * 避免重复提交的时间间隔,默认为1000ms
     * */
    int repeatTime() default 1000;
}

3.3.2 切面类

@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();
    }

}

3.3.3 使用

在想要使用的方法上加上@AvoidRepeatCommit注解即可,然后Spring AOP会自动实现避免重复sql提交

你可能感兴趣的:(Java学习,java,jvm,后端)