22.AOP概念、应用、原理

190 阅读10分钟

AOP概念、应用、原理

Aop是什么

与OOP对比,面向切面,传统的OOP开发中的代码逻辑是自上而下的,而这些过程会产生一些横切性问题,这些横切性的问题和我们的主业务逻辑关系不大,这些横切性问题不会影响到主逻辑实现的,但是会散落到代码的各个部分,难以维护。即侵入性太强,耦合度太高!

AOP是处理一些横切性问题,AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。

Aop的应用场景

主要应用于:身份&权限验证、效率检查、事务管理、exception拦截处理

Aop的实现

织入是将增强添加对目标类具体连接点上的过程。AOP像一台织布机,将目标类、增强或引介通过AOP这台织布机天衣无缝地编织到一起。根据不同的实现技术,AOP有三种织入的方式:

a、编译期织入,这要求使用特殊的Java编译器。

b、类装载期织入,这要求使用特殊的类装载器。

c、动态代理织入,在运行期为目标类添加增强生成子类的方式。

Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

springAop和AspectJ的关系

Aop是一种概念,springAop、AspectJ都是Aop的实现,SpringAop有自己的语法,但是语法复杂,所以SpringAop借助了AspectJ的注解,但是底层实现还是自己的。spring底层使用的是JDK或者CGLIB来完成的代理。

AOP实现的关键就在于AOP框架自动创建的AOP代理。AOP代理则可分为静态代理(例如:原生AspectJ)和动态代理(例如:spring aop)两大类。

其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强。

而动态代理则在运行时借助于JDK动态代理、CGLIB代理等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强。

原文链接:blog.csdn.net/liuxiao7238…

spring Aop的概念

切入点(pointcut):在哪些类、哪些方法上切入,通常是一个正则表达式

执行点(JoinPoint):通过pointcut选取出来的集合中的具体的一个执行点,我们就叫JoinPoint

通知(advice):在方法前、方法后、方法前后、异常等做什么。通知:位置 + 业务逻辑

切面(aspect):切面 = pointcut + advice。即在什么时机、什么地方、做什么。

织入(weaving):把切面加入对象,并创建出代理对象的过程。把代理逻辑加入到目标对象上的过程叫做织入。

advice通知类型

Before 连接点(目标对象中的方法)执行之前,但是无法阻止连接点的正常执行,除非该段执行抛出异常。

After (目标对象中的方法)正常执行之后,执行过程中正常执行返回退出,非异常退出。

After throwing 执行抛出异常的时候。

AfterReturning 无论(目标对象中的方法)是正常退出还是异常退出,都会执行。

Around advice:围绕(目标对象中的方法)执行,例如方法调用。这是最有用的切面方式。around通知可以在方法调用之前和之后执行自定义行为。

各种连接点joinPoint

execution的最小粒度是方法,用于匹配方法执行。在aop中主要使用。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

这里问号表示当前项可以有也可以没有,其中各项的语义如下
modifiers-pattern:方法的可见性,如public,protected;
ret-type-pattern:方法的返回值类型,如int,void等;
declaring-type-pattern:方法所在类的全路径名,包名 如com.spring.Aspect;
name-pattern:方法名类型,如buisinessService();
param-pattern:方法的参数类型,如java.lang.String;
throws-pattern:方法抛出的异常类型,如java.lang.Exception;

example:
//匹配com.chenss.dao包下的任意接口和类的任意方法
@Pointcut("execution(* com.chenss.dao.*.*(..))")

//匹配com.chenss.dao包下的任意接口和类的public方法
@Pointcut("execution(public * com.chenss.dao.*.*(..))")

//匹配com.chenss.dao包下的任意接口和类的public 无方法参数的方法
@Pointcut("execution(public * com.chenss.dao.*.*())")

//匹配com.chenss.dao包下的任意接口和类的第一个参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String, ..))")

//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")

//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")

//匹配任意的public方法
@Pointcut("execution(public * *(..))")

//匹配任意的以te开头的方法
@Pointcut("execution(* te*(..))")

//匹配com.chenss.dao.IndexDao接口中任意的方法
@Pointcut("execution(* com.chenss.dao.IndexDao.*(..))")

//匹配com.chenss.dao包及其子包中任意的方法
@Pointcut("execution(* com.chenss.dao..*.*(..))")

关于这个表达式的详细写法,可以脑补也可以参考官网很容易的,可以作为一个看spring官网文档的入门,打破你害怕看官方文档的心理,其实你会发觉官方文档也是很容易的.

docs.spring.io/spring-fram…

由于Spring切面粒度最小是达到方法级别,而execution表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的信息,并且在Spring中,大部分需要使用AOP的业务场景也只需要达到方法级别即可,因而execution表达式的使用是最为广泛的。

within

within表达式的最小粒度为类,within与execution相比,粒度更大,仅能实现到包和接口、类级别。

而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等

@Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法

args

args表达式的作用是匹配指定参数类型和指定参数数量的方法,与包名和类名无关

args同execution不同的地方在于:

gs匹配的是运行时传递给方法的参数类型

execution(* *(java.io.Serializable))匹配的是方法在声明时指定的方法参数类型。

//匹配运行时传递的参数类型为指定类型的、且参数个数和顺序匹配
@Pointcut("args(java.io.Serializable)")
//接受一个参数,并且传递的参数的运行时类型具有@Classified
@Pointcut("@args(com.chenss.anno.Chenss)")

this

JDK代理时,指向接口和代理类proxy,cglib代理时 指向接口和子类(不使用proxy)

target

指向接口和子类

/**
 * 此处需要注意:
 * 如果配置设置proxyTargetClass=false,或默认为false,则是用JDK代理,否则使用的是CGLIB代理
 * JDK代理的实现方式是基于接口实现,代理类继承Proxy,实现接口。
 * 而CGLIB继承被代理的类来实现。
 * 所以使用target会保证目标不变,关联对象不会受到这个设置的影响。
 * 但是使用this对象时,会根据该选项的设置,判断是否能找到对象。
 */
//目标对象,也就是被代理的对象。限制目标对象为com.chenss.dao.IndexDaoImpl类
@Pointcut("target(com.chenss.dao.IndexDaoImpl)")
//当前对象,也就是代理对象,代理对象时通过代理目标对象的方式获取新的对象,与原值并非一个
@Pointcut("this(com.chenss.dao.IndexDaoImpl)")
//具有@Chenss的目标对象中的任意方法
@Pointcut("@target(com.chenss.anno.Chenss)")
//等同于@target
@Pointcut("@within(com.chenss.anno.Chenss)")

@annotation

这个很简单,用于方法级别。 //匹配带有com.chenss.anno.Chenss注解的方法bean @Pointcut("@annotation(com.chenss.anno.Chenss)")

//自动检测名称为dao1的bean上的任意方法 @Pointcut("bean(dao1)") @Pointcut("bean(dao*)")

注意:上述所有的表达式可以混合使用,|| && !

切面执行顺序

可以实现Orderd接口或者PriorityOrdered接口 重写getOrder。返回值小的先执行。

@Override
public int getOrder() {
    return 1;
}

通知执行顺序

当方法符合切点规则不符合环绕通知的规则时候,执行的顺序如下

如果没异常:@Before→@After→@AfterRunning

如果有异常:@Before→@After→@AfterThrowing

当方法符合切点规则并且符合环绕通知的规则时候,执行的顺序如下

如果没异常:@AroundBefore→@Before→@AroundAfter→@After执行 ProceedingJoinPoint.proceed() 之后的操作→@AfterReturning

如果有异常:@AroundBefore→@Before→@AroundAfter→@After执行 ProceedingJoinPoint.proceed() 之后的操作→@AfterThrowing

springAop支持AspectJ

启用@EnableAspectJAutoProxy

使用注解启用@AspectJ支持 要使用注解启用@AspectJ支持,请添加@EnableAspectJAutoProxy注释

@EnableAspectJAutoProxy
@Configuration
@Import(InterService.class)
public class BeanConfig extends Config {}

使用XML配置启用@AspectJ支持 要使用基于xml的配置启用@AspectJ支持,可以使用aop:aspectj-autoproxy元素

<aop:aspectj-autoproxy/>

声明Aspect&pointCut&&Advice通知

申明一个@Aspect注释类,并且定义成一个bean交给Spring管理。

切入点表达式由@Pointcut注释表示。切入点声明由两部分组成:一个签名包含名称和任何参数,以及一个切入点表达式,该表达式确定我们对哪个方法执行感兴趣。

切入点确定感兴趣的 join points(连接点),从而使我们能够控制何时执行通知。Spring AOP只支持Spring bean的方法执行 join points(连接点),所以您可以将切入点看作是匹配Spring bean上方法的执行。

advice通知与pointcut切入点表达式相关联,并在切入点匹配的方法执行@Before之前、@After之后或前后运行。

声明切面的第一种方式

package tyrant;
​
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Aop implements PriorityOrdered {

    //指定切入点表达式,拦截那些方法,即为那些类生成代理对象
    //@Pointcut("execution(* com.bie.aop.UserDao.save(..))")  ..代表所有参数
    //@Pointcut("execution(* com.bie.aop.UserDao.*())")  指定所有的方法
    //@Pointcut("execution(* com.bie.aop.UserDao.save())") 指定save方法
    @Pointcut("execution(* tyrant.*.log(..))")
    public void pointCut(){

    }

    @Before("pointCut()")
    public void Before(){
        System.out.println("Before");
    }

    @After("pointCut()")
    public void After(){
        System.out.println("After");
    }

    @AfterThrowing("pointCut()")
    public void AfterThrowing(){
        System.out.println("AfterThrowing");
    }

    @AfterReturning("pointCut()")
    public void AfterReturning(){
        System.out.println("AfterReturning");
    }

    @Around("execution(* tyrant.*.log(..))")
    public void Around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Around Before");
        joinPoint.proceed();
        System.out.println("Around After");
    }

    @Override
    public int getOrder() {
        return 1;
    }
}

另外一种方式是实现Advisor接口

package allProcess;

import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.AfterAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.util.SocketUtils;

import java.lang.reflect.Method;
@Component
public class MyAdvisor implements Advisor {

    public MyAdvisor() {
        System.out.println("MyAdvisor");
    }

    @Override
    public Advice getAdvice() {
        Advice advice = new MethodBeforeAdvice() {
            @Override
            public void before(Method method, Object[] args, Object target) throws Throwable {
                System.out.println("好爽啊!");
            }
        };
        return advice;
    }

    @Override
    public boolean isPerInstance() {
        return false;
    }
}

package tyrant;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;


@Aspect
@Component
public class Aop2 implements PriorityOrdered {

    //指定切入点表达式,拦截那些方法,即为那些类生成代理对象
    //@Pointcut("execution(* com.bie.aop.UserDao.save(..))")  ..代表所有参数
    //@Pointcut("execution(* com.bie.aop.UserDao.*())")  指定所有的方法
    //@Pointcut("execution(* com.bie.aop.UserDao.save())") 指定save方法

    @Pointcut("execution(* tyrant.*.log(..))")
    public void pointCut(){

    }

    @Before("pointCut()")
    public void Before(){
        System.out.println("Before2");
    }

    @After("pointCut()")
    public void After(){
        System.out.println("After2");
    }

    @AfterThrowing("pointCut()")
    public void AfterThrowing(){
        System.out.println("AfterThrowing2");
    }

    @AfterReturning("pointCut()")
    public void AfterReturning(){
        System.out.println("AfterReturning2");
    }

    @Around("execution(* tyrant.*.log(..))")
    public void Around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("Around Before2");
        joinPoint.proceed();
        System.out.println("Around After2");
    }

    @Override
    public int getOrder() {
        return 2;
    }
}

SpringAOP详解-如何指定Cglib代理

xml方式

<aop:aspectj-autoproxy proxy-target-class="true"/>

注解方式

@EnableAspectJAutoProxy(proxyTargetClass=true)

具体实现,在BeanFactoryPostProcessor对BeanDefinition进行设置,之后在Bean实例化的时候就会根据这个属性选择Cglib代理方式

if (proxyTargetClass) {
    targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
else {
    proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE);
}

jdk和cglib效率比较

总结:cglib是通过继承来操作子类的字节码生成代理类,JDK是通过接口,然后利用java反射完成对类的动态创建,严格意义上来说cglib的效率高于JDK的反射,但是这种效率取决于代码功力,其实可以忽略不计,毕竟JDK是JVM的亲儿子。

为什么jdk代理需要基于接口

JDK动态代理原理: 利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。是在程序运行的过程中,根据被代理的接口来动态生成代理类的class文件,并加载运行的过程。

之所以只支持实现了接口的类的代理。从原理上讲是因为JVM动态生成的代理类有如下特性:

继承了Proxy类,实现了代理的接口,最终形式如下(HelloInterface为被代理类实现的接口):

public final class $Proxy0 extends Proxy implements HelloInterface{  
 //
}

从使用上讲,创建代理类时必须传入被代理类实现的接口。 因为java不能多继承,这里已经继承了Proxy类了,不能再继承其他的类,所以JDK的动态代理不支持对实现类的代理,只支持接口的代理。