乐趣区

关于java:Spring-AOP全面详解超级详细

如果说 IOC 是 Spring 的外围,那么面向切面编程 AOP 就是 Spring 另外一个最为重要的外围 @mikechen

AOP 的定义

AOP(Aspect Orient Programming), 直译过去就是 面向切面编程,AOP 是一种编程思维,是面向对象编程(OOP)的一种补充。

面向切面编程,实现在不批改源代码的状况下给程序动静对立增加额定性能的一种技术,如下图所示:\
\
AOP 能够拦挡指定的办法并且对办法加强,而且无需侵入到业务代码中,使业务与非业务解决逻辑拆散,比方 Spring 的事务,通过事务的注解配置,Spring 会主动在业务办法中开启、提交业务,并且在业务解决失败时,执行相应的回滚策略。

 

AOP 的作用

AOP 采取横向抽取机制(动静代理),取代了传统纵向继承机制的重复性代码,其利用次要体现在事务处理、日志治理、权限管制、异样解决等方面。

次要作用是拆散功能性需要和非功能性需要,使开发人员能够集中处理某一个关注点或者横切逻辑,缩小对业务代码的侵入,加强代码的可读性和可维护性。

简略的说,AOP 的作用就是保障开发者在不批改源代码的前提下,为零碎中的业务组件增加某种通用性能。

 

AOP 的利用场景

比方典型的 AOP 的利用场景:\

  • 日志记录
  • 事务管理
  • 权限验证
  • 性能监测

AOP 能够拦挡指定的办法,并且对办法加强,比方:事务、日志、权限、性能监测等加强,而且无需侵入到业务代码中,使业务与非业务解决逻辑拆散。

 

Spring AOP 的术语

在深刻学习 SpringAOP 之前,让咱们先对 AOP 的几个根本术语有个大抵的概念。

AOP 外围概念

Spring AOP 告诉分类

Spring AOP 织入期间

 

Spring AOP 三种应用形式

AOP 编程其实是很简略的事件,纵观 AOP 编程,程序员只须要参加三个局部:

1、定义一般业务组件

2、定义切入点,一个切入点可能横切多个业务组件

3、定义加强解决,加强解决就是在 AOP 框架为一般业务组件织入的解决动作

所以进行 AOP 编程的要害就是定义切入点和定义加强解决,一旦定义了适合的切入点和加强解决,AOP 框架将主动生成 AOP 代理,即:代理对象的办法 = 加强解决 + 被代理对象的办法。

形式 1:应用 Spring 自带的 AOP

public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice,MethodInterceptor {
    @Override
    public void before(Method method, Object[] objects, Object target) throws Throwable {// 前置告诉}

    @Override
    public void afterReturning(Object result, Method method, Object[] objects, Object target) throws Throwable {// 后置告诉}
     @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        // 盘绕告诉
        // 指标办法之前执行
        methodInvocation.proceed();   // 指标办法

        // 指标办法之后执行
        return resultVal;
    }
}

配置告诉时需实现 org.springframework.aop 包下的一些接口

  • 前置告诉:MethodBeforeAdvice
  • 后置告诉:AfterReturningAdvice
  • 盘绕告诉:MethodInterceptor
  • 异样告诉:ThrowsAdvice

创立被代理对象

<bean id="orderServiceBean" class="com.apesource.service.impl.OrderServiceImpl"/>
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

告诉(Advice)

<bean id="logAdviceBean" class="com.apesource.log.LogAdvice"/>
<bean id="performanceAdviceBean" class="com.apesource.log.PerformanceAdvice"/>

切入点(Pointcut):通过正则表达式形容指定切入点(某些 指定办法)

<bean id="createMethodPointcutBean"         class="org.springframework.aop.support.JdkRegexpMethodPointcut">
    <!-- 注入正则表达式:形容那些办法为切入点 -->
    <property name="pattern" value=".*creat.*"/>
</bean>

Advisor(高级告诉) = Advice(告诉) + Pointcut(切入点)

<bean id="performanceAdvisorBean" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <!-- 注入切入点 -->
        <property name="pointcut" ref="createMethodPointcutBean"/>

        <!-- 注入告诉 -->
        <property name="advice" ref="performanceAdviceBean"/>
    </bean>

创立主动代理

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!--Bean 名称规定(数组):指定那些 bean 创立主动代理 -->
        <property name="beanNames">
            <list>
                <value>*ServiceBean</value>
                <value>*TaskBean</value>
            </list>
        </property>

        <!-- 告诉列表:须要执行那些告诉 -->
        <property name="interceptorNames">
            <list>
                <value>logAdviceBean</value>
                <value>performanceAdvisorBean</value>
            </list>
        </property>
</bean>

形式 2:应用 Aspectj 实现切面(一般 POJO 的实现形式)

导入 Aspectj 相干依赖

<!--aop 依赖 1:aspectjrt -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
</dependency>

<!--aop 依赖 2:aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

告诉办法名轻易起,没有限度

public class LogAspectj {
    // 前置告诉
    public void beforeAdvice(JoinPoint joinPoint){System.out.println("==========【Aspectj 前置告诉】==========");
    }

    // 后置告诉:办法失常执行后,有返回值,执行该后置告诉:如果该办法执行出现异常,则不执行该后置告诉 
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){System.out.println("==========【Aspectj 后置告诉】==========");
    }
    public void afterAdvice(JoinPoint joinPoint){System.out.println("==========【Aspectj 后置告诉】==========");
    }

    // 盘绕告诉
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("##########【盘绕告诉中的前置告诉】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【盘绕告诉中的后置告诉】##########");
        return returnVale;
    }

    /**
     * 异样告诉:办法出现异常时,执行该告诉
     */
    public void throwAdvice(JoinPoint joinPoint, Exception ex){System.out.println("出现异常:" + ex.getMessage());
    }

}

应用 Aspectj 实现切面,应用 Spring AOP 进行配置

<!-- 业务组件 bean-->
<bean id="userServiceBean" class="com.apesource.service.impl.UserServiceImpl"/>

<!-- 日志 Aspect 切面 -->
<bean id="logAspectjBean" class="com.apesource.log.LogAspectj"/>

<!-- 应用 Aspectj 实现切面,应用 Spring AOP 进行配置 -->
<aop:config>
    <!-- 配置切面 -->
    <!-- 注入切面 bean-->
    <aop:aspect ref="logAspectjBean">
        <!-- 定义 Pointcut:通过 expression 表达式,来查找 特定的办法(pointcut)-->
        <aop:pointcut id="pointcut"
                     expression="execution(* com.apesource.service.impl.*.create*(..))"/>

        <!-- 配置 "前置告诉"-->
        <!-- 在 pointcut 切入点 (serviceMethodPointcut) 查找到 的办法执行 "前",来执行以后 logAspectBean 的 doBefore-->
        <aop:before method="beforeAdvice" pointcut-ref="pointcut"/>

        <!-- 配置“后置告诉”-->
        <!--returning 属性:配置以后办法中用来接管返回值的参数名 -->
        <aop:after-returning returning="returnVal" 
                             method="afterReturningAdvice" pointcut-ref="pointcut"/> 
        <aop:after method="afterAdvice" pointcut-ref="pointcut"/>
        
        <!-- 配置 "盘绕告诉"-->
        <aop:around method="aroundAdvice" pointcut-ref="pointcut"/>
        
        <!-- 配置“异样告诉”-->
        <!--throwing 属性:配置以后办法中用来接管以后异样的参数名 -->
        <aop:after-throwing throwing="ex" method="throwAdvice" pointcut-ref="pointcut"/>
    </aop:aspect>

</aop:config>

形式 3:应用 Aspectj 实现切面(基于注解的实现形式)

// 申明以后类为 Aspect 切面,并交给 Spring 容器治理
@Component
@Aspect
public class LogAnnotationAspectj {
    private final static String EXPRESSION = 
                            "execution(* com.apesource.service.impl.*.create*(..))";

    // 前置告诉   
    @Before(EXPRESSION)
    public void beforeAdvice(JoinPoint joinPoint){System.out.println("==========【Aspectj 前置告诉】==========");
    }


    // 后置告诉:办法失常执行后,有返回值,执行该后置告诉:如果该办法执行出现异常,则不执行该后置告诉
    @AfterReturning(value = EXPRESSION,returning = "returnVal")
    public void afterReturningAdvice(JoinPoint joinPoint,Object returnVal){System.out.println("==========【Aspectj 后置告诉】==========");
    }

    // 后置告诉
    @After(EXPRESSION)
    public void afterAdvice(JoinPoint joinPoint){System.out.println("==========【Aspectj 后置告诉】==========");
    }

    // 盘绕告诉
    @Around(EXPRESSION)
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("##########【盘绕告诉中的前置告诉】##########");
        Object returnVale = joinPoint.proceed();
        System.out.println("##########【盘绕告诉中的后置告诉】##########");
        return returnVale;
    }

    // 异样告诉:办法出现异常时,执行该告诉
    @AfterThrowing(value = EXPRESSION,throwing = "ex")
    public void throwAdvice(JoinPoint joinPoint, Exception ex){System.out.println("**********【Aspectj 异样告诉】执行开始 **********");
        System.out.println("出现异常:" + ex.getMessage());
        System.out.println("**********【Aspectj 异样告诉】执行完结 **********");
    }

}
<!-- 主动扫描器 -->
<context:component-scan base-package="com.apesource"/>

<!-- 配置 Aspectj 的主动代理 -->
<aop:aspectj-autoproxy/>

 

Spring AOP 的实现原理

Spring 的 AOP 实现原理其实很简略,就是通过动静代理实现的。

Spring AOP 采纳了两种混合的实现形式:JDK 动静代理和 CGLib 动静代理。

  • JDK 动静代理:Spring AOP 的首选办法。每当指标对象实现一个接口时,就会应用 JDK 动静代理。指标对象必须实现接口
  • CGLIB 代理:如果指标对象没有实现接口,则能够应用 CGLIB 代理。

JDK 动静代理

Spring 默认应用 JDK 的动静代理实现 AOP,类如果实现了接口,Spring 就会应用这种形式实现动静代理。

JDK 实现动静代理须要两个组件,首先第一个就是 InvocationHandler 接口。

咱们在应用 JDK 的动静代理时,须要编写一个类,去实现这个接口,而后重写 invoke 办法,这个办法其实就是咱们提供的代理办法。

如下源码所示:

/**
 * 动静代理
 *
 * @author mikechen
 */
public class JdkProxySubject implements InvocationHandler {

    private Subject subject;

    public JdkProxySubject(Subject subject) {this.subject = subject;}

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("before 前置告诉");
        Object result = null;

        try {result = method.invoke(subject, args);
        }catch (Exception ex) {System.out.println("ex:" + ex.getMessage());
            throw ex;
        }finally {System.out.println("after 后置告诉");
        }
        return result;
    }
}

而后 JDK 动静代理须要应用的第二个组件就是 Proxy 这个类,咱们能够通过这个类的 newProxyInstance 办法,返回一个代理对象。

生成的代理类实现了原来那个类的所有接口,并对接口的办法进行了代理,咱们通过代理对象调用这些办法时,底层将通过反射,调用咱们实现的 invoke 办法。

public class Main {public static void main(String[] args) { 
   // 获取 InvocationHandler 对象 在构造方法中注入指标对象 
   InvocationHandler handler = new JdkProxySubject(new RealSubject()); 

   // 获取代理类对象 
  Subject proxySubject = (Subject)Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Subject.class}, handler); 

   // 调用指标办法
   proxySubject.request(); proxySubject.response();

}

运行后果:

before 前置告诉
执行指标对象的 request 办法......
after 后置告诉
before 前置告诉
执行指标对象的 response 办法......
after 后置告诉

 

JDK 动静代理优缺

长处

JDK 动静代理是 JDK 原生的,不须要任何依赖即可应用;

通过反射机制生成代理类的速度要比 CGLib 操作字节码生成代理类的速度更快;

毛病

如果要应用 JDK 动静代理,被代理的类必须实现了接口,否则无奈代理;

JDK 动静代理无奈为没有在接口中定义的办法实现代理,假如咱们有一个实现了接口的类,咱们为它的一个不属于接口中的办法配置了切面,Spring 依然会应用 JDK 的动静代理,然而因为配置了切面的办法不属于接口,为这个办法配置的切面将不会被织入。

JDK 动静代理执行代理办法时,须要通过反射机制进行回调,此时办法执行的效率比拟低;

 

CGLib 代理

CGLIB 组成构造

Cglib 是一个弱小的、高性能的代码生成包,它宽泛被许多 AOP 框架应用,为他们提供办法的拦挡,如下图所示 Cglib 与 Spring 等利用的关系:

  • 最底层的是字节码Bytecode,字节码是 Java 为了保障“一次编译、到处运行”而产生的一种虚构指令格局,例如 iload_0、iconst_1、if_icmpne、dup 等
  • 位于字节码之上的是ASM,这是一种间接操作字节码的框架,利用 ASM 须要对 Java 字节码、Class 构造比拟相熟
  • 位于 ASM 之上的是 CGLIBGroovyBeanShell,后两种并不是 Java 体系中的内容而是脚本语言,它们通过 ASM 框架生成字节码变相执行 Java 代码,这阐明 在 JVM 中执行程序并不一定非要写 Java 代码 —- 只有你能生成 Java 字节码,JVM 并不关怀字节码的起源,当然通过 Java 代码生成的 JVM 字节码是通过编译器间接生成的,算是最“正统”的 JVM 字节码
  • 位于 CGLIBGroovyBeanShell 之上的就是 HibernateSpring AOP 这些框架了,这一层大家都比拟相熟
  • 最上层的是 Applications,即具体利用,个别都是一个 Web 我的项目或者本地跑一个程序

所以,Cglib 的实现是在字节码的根底上的,并且应用了开源的 ASM 读取字节码,对类实现加强性能的。

 

以上

作者简介

陈睿 |mikechen,10 年 + 大厂架构教训,《BAT 架构技术 500 期》系列文章作者,分享十余年 BAT 架构教训以及面试心得!

浏览 mikechen 的互联网架构更多技术文章合集

Java 并发 |JVM|MySQL|Spring|Redis| 分布式 | 高并发 | 架构师

关注「mikechen 的互联网架构」公众号,回复 【架构】 支付我原创的《300 期 + BAT 架构技术系列与 1000 + 大厂面试题答案》

退出移动版