如果说 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|分布式|高并发|架构师

Spring AOP全面详解(超级详细)的更多相关文章

  1. Spring系列25:Spring AOP 切点详解

    本文内容 Spring 10种切点表达式详解 切点的组合使用 公共切点的定义 声明切点@Poincut @Poincut 的使用格式如下: @Poincut("PCD") // 切 ...

  2. Java注解最全详解(超级详细)

    Java注解是一个很重要的知识点,掌握好Java注解有利于学习Java开发框架底层实现.@mikechen Java注解定义 Java注解又称Java标注,是在 JDK5 时引入的新特性,注解(也被称 ...

  3. spring框架 AOP核心详解

    AOP称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子. 一 AOP的基本概念 (1)Asp ...

  4. spring原理案例-基本项目搭建 02 spring jar包详解 spring jar包的用途

    Spring4 Jar包详解 SpringJava Spring AOP: Spring的面向切面编程,提供AOP(面向切面编程)的实现 Spring Aspects: Spring提供的对Aspec ...

  5. spring事务管理(详解和实例)

    原文地址: 参考地址:https://blog.csdn.net/yuanlaishini2010/article/details/45792069 写这篇博客之前我首先读了<Spring in ...

  6. Spring jar包详解

    Spring jar包详解 org.springframework.aop ——Spring的面向切面编程,提供AOP(面向切面编程)的实现 org.springframework.asm——spri ...

  7. Spring——jar包详解(转)

    Spring——jar包详解 org.springframework.aop ——Spring的面向切面编程,提供AOP(面向切面编程)的实现 org.springframework.asm——spr ...

  8. spring事务配置详解

    一.前言 好几天没有在对spring进行学习了,由于这几天在赶项目,没有什么时间闲下来继续学习,导致spring核心架构详解没有继续下去,在接下来的时间里面,会继续对spring的核心架构在继续进行学 ...

  9. (转)Spring JdbcTemplate 方法详解

    Spring JdbcTemplate方法详解 文章来源:http://blog.csdn.net/dyllove98/article/details/7772463 JdbcTemplate主要提供 ...

随机推荐

  1. B - A Simple Task

    https://vjudge.net/contest/446582#problem/B 这道题是一道不错的线段树练代码能力的题. #include<bits/stdc++.h> using ...

  2. Docker运行资源控制

    概述 ​ 一个 docker host 上会运行若干容器,每个容器都需要 CPU.内存和 IO 资源.对于 KVM,VMware 等虚拟化技术,用户可以控制分配多少 CPU.内存资源给每个虚拟机.对于 ...

  3. 【RocketMQ】NameServer的启动

    NameServer是一个注册中心,Broker在启动时向所有的NameServer注册,生产者Producer和消费者Consumer可以从NameServer中获取所有注册的Broker列表,并从 ...

  4. CSCMS代码审计

    很久之前审的了. 文章首发于奇安信攻防社区 https://forum.butian.net/share/1626 0x00 前言 CSCMS是一款强大的多功能内容管理系统,采用php5+mysql进 ...

  5. 7.脚本三剑客之awk

    脚本三剑客之awk 目录 脚本三剑客之awk awk介绍 awk工作原理 awk命令格式 awk基础用法 awk命令高级用法 date命令使用 awk介绍 AWK 是一种处理文本文件的语言,是一个强大 ...

  6. rosbag遍历数据出错:(unicode error) 'utf-8' codec can't decode byte 0xcd in position 31: invalid continuation byte

    主题: 前言 针对ros系统记录的bag文件,可以使用python的rosbag包,按照不同起止时间和topic进行提取. 然而,有的topic可以使用rosbag读取,但是不能遍历,存在解码错误.原 ...

  7. Linux文件的特殊属性

    文件的特殊属性 作用:文件的权限不能显示root用户,为了防止root用户的误操作,所以需要特殊属性来防止root用户的误操作. chattr工具: 可以给文件添加特殊的属性 +i:对这个文件不能修改 ...

  8. JQuery实现图片轮播无缝滚动

    图片轮播无缝滚动实例 实现效果展示预览: 思路: 1.设置当前索引curIndex,和前一张索引prevIndex.(curIndex为下一次要显示的图片索引,prevIndex为现在看见的图片) 2 ...

  9. halcon数组的一些使用

    没啥好讲的,这里对于不是数组部分的东西就不进行讲解了. area_center(RegionOpening,Area, Row, Column).使用area_center来求区域的中心和面积时,返回 ...

  10. mysql拆分字符串做条件查询

    mysql拆分字符串作为查询条件 有个群友问一个问题 这表的ancestors列存放的是所有的祖先节点,以,分隔 例如我查询dept_id为103的所有祖先节点,现在我只有一个dept_id该怎么查 ...