AOP:面向切面编程
-
概述:
- 什么是AOP:
- 概念:在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。(百度百科)
- 简单的说:就是将程序中重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
- AOP的作用和优势:
- 作用:从定义中来看,就是为了在程序运行期间,不修改源码对已有方法进行增强。
- 优势:减少重复代码 提交了开发效率 维护方便
- 实现方式: 就是动态代理的技术
- 具体的作用:实现事务的控制 日志 和 安全模块
- 什么是AOP:
-
动态代理:
- 实现动态代理的两种常用的方式:
- 基于接口的动态代理:
- jdk 官方的Proxy类
- 要求:被代理的类至少实现一个接口
- 实现: 基于接口的动态代理
接口名 新对象名 = (接口名)Proxy.newProxyInstance( //表示的是被代理对象使用相同的类加载器 被代理的对象.getClass().getClassLoader(), // 被代理对象的类加载器,固定写法 //和被代理对象具有相同的行为。实现相同的接口 被代理的对象.getClass().getInterfaces(), // 被代理对象实现的所有接口,固定写法 new InvocationHandler() { // 匿名内部类,通过拦截被代理对象的方法来增强被代理对象 /* 被代理对象的任何方法执行时,都会被此方法拦截到 其参数如下: proxy: 代理对象的引用,不一定每次都用得到 method: 被拦截到的方法对象 args: 被拦截到的方法对象的参数 返回值: 被增强后的返回值 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if("方法名".equals(method.getName())) { // 增强方法的操作 rtValue = method.invoke(被代理的对象, args); // 增强方法的操作 return rtValue; } } });
- 基于子类的动态代理:
- 第三方的CGlib (如果想要使用的话 需要导入依赖 asm.jar)
- 要求:被代理类不能用 final 修饰的类(最终类)
- 实现:
Actor cglibActor = (Actor) Enhancer.create(actor.getClass(), new MethodInterceptor() { /** * 执行被代理对象的任何方法,都会经过该方法。在此方法内部就可以对被代理对象的任何 方法进行增强。 * * 参数: * 前三个和基于接口的动态代理是一样的。 * MethodProxy:当前执行方法的代理对象。 * 返回值: * 当前执行方法的返回值 */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {// 注意的是这里不能使用Exception // 进行方法的增强的代码 Object invoke = null; System.out.println("teacher ma qi fei le "); if("teach".equals(method.getName())){ invoke = method.invoke(teacherDao, objects); } System.out.println("wuhu wuhu wuhu wuhu wuhu wuhu"); System.out.println("teacher ma shang dang le "); return invoke; }
- JDK和CGLIB动态代理原理
- JDK动态代理: 利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
- CGLIB动态代理:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理(所以 滥用CGLIB动态代理会导致方法区内存泄漏)
- 何时使用JDK还是CGLIB?
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP(默认使用JDK代理)
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换
- 如何强制使用CGLIB实现AOP?
- 添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)
- 在Spring配置文件中加入
<aop:aspectj-autoproxy proxy-target-class="true"/>
- JDK动态代理和CGLIB字节码生成的区别?
- JDK动态代理只能对实现了接口的类生成代理,而不能针对类。在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐
- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,对于final类或方法,是无法继承的 同样的 static 方法也是不能实现代理的 使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在jdk6之前比使Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,因为CGLib原理是动态生成被代理类的子类
- 基于接口的动态代理:
- 实现动态代理的两种常用的方式:
-
Spring中的AOP
- AOP相关术语(进行了解)
- Joinpoint(连接点): 被拦截到的点,在Spring中指的是方法,且Spring中只支持方法类型的连接点.
- Pointcut(切入点): 我们对其进行增强的方法.
- Advice(通知/增强): 对切入点进行的增强操作包括
前置通知,后置通知,异常通知,最终通知,环绕通知
五种 - Introduction(引介):引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 属性
- Target(目标对象):代理的目标对象
- Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程
- 目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译 器。AspectJ的织入编译器就是以这种方式织入切面的
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特 殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织 入切面时,AOP容器会为目标对象动态地创建一个代理对象。 Spring AOP就是以这种方式织入切面的
- 目标对象的生命周期里有多个点可以进行织入:
- Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
- Aspect(切面): 是切入点和通知的结合
- Spring 中代理的选择:Spring中会通过是否是实现了接口来进行那种动态代理方式的选择
- Spring提供了4种类型的AOP支持: 基于代理的经典Spring AOP(这种方式现在比较的笨重不再详解)纯POJO切面; @AspectJ注解驱动的切面; 注入式AspectJ切面(适用于Spring各版本)前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础 之上,因此,Spring对AOP的支持局限于方法拦截
- 基于xml的AOP的配置:
- 配置的步骤:
- 将通知的bean也交给spring来管理:
在bean.xml
文件中引入约束并将通知类注入Spring容器中<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--使用的xml方式来配置logger类 使用注解的方式就是使用component注解--> <bean id="logger" class="com.itheima.utils.Logger"></bean> </beans>
- 使用
aop:config
标签来表明开始aop的位置:
所有关于AOP配置的代码都写在<aop:config>
标签内<aop:config> <!-- AOP配置的代码都写在此处 --> </aop:config>
- 使用
aop:aspect
标签来表明配置切面- 属性:
- id属性:给切面提供一个唯一标识
- ref属性:是指定通知类bean的id
<aop:aspect id="logAdvice" ref="logger"> <!-- 配置通知的类型 且配置通知方法 和 切入点方法的关联--> </aop:aspect>
- 属性:
- 在
aop:aspect
标签内部使用对应的标签来配置通知的类型- 示例: Logger类中的printLog方法在切入点方法之前执行, 所以是前置通知使用的标签是
aop:before
表示配置前置通知。- 属性:
- method属性:用于指定Logger类中那个方法是前置通知。
- pointcut属性:用于指定切入点的表达式,该表达式的含义是对业务层中那些方法进行增强。
- ponitcut-ref: 指定切入点的表达式的id 只能和pointcut属性两者取其一
- 切入点表达式写法: execution(表达式)
- 表达式的写法:[访问修饰符] 返回值 包名.类名.方法名(…参数列表) [ ] 表示的是能不写
excution(public void com.ithieima.service.impl.AccountServiceImpl.saveAccount())
- 表达式的写法:[访问修饰符] 返回值 包名.类名.方法名(…参数列表) [ ] 表示的是能不写
- 属性:
<aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
- 示例: Logger类中的printLog方法在切入点方法之前执行, 所以是前置通知使用的标签是
- 完整的配置:
bean.xml
文件<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean> <bean id="logger" class="com.itheima.utils.logger"></bean> <!--配置aop--> <aop:config> <!--配置切面 aspect--> <aop:aspect id="logAdvice" ref="logger"> <!--配置通知的类型 且配置通知方法 和 切入点方法的关联--> <aop:before method="printLog" pointcut="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())"></aop:before> </aop:aspect> </aop:config> </beans>
- 将通知的bean也交给spring来管理:
- 具体的通知类型:
<aop:before>
: 配置前置通知,指定的增强方法在切入点方法之前执行.<aop:after-returning>
: 配置后置通知,指定的增强方法在切入点方法正常执行之后执行.<aop:after-throwing>
: 配置异常通知,指定的增强方法在切入点方法产生异常后执行.<aop:after>
: 配置最终通知,无论切入点方法执行时是否发生异常,指定的增强方法都会最后执行.<aop:around>
: 配置环绕通知,可以在代码中手动控制增强代码的执行时机.- 属性:
- method属性:用于指定定通知类中的增强方法名
- pointcut属性:用于指定切入点的表达式,该表达式的含义是对业务层中那些方法进行增强。
- ponitcut-ref: 指定切入点的表达式的id 只能和pointcut属性两者取其一
- 切入点表达式的写法: execution([修饰符] 返回值类型 包路径.类名.方法名(参数))【只需要记住最后一种写法】
- 全匹配方式:
<aop:pointcut expression="execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
- 其中访问修饰符可以省略:
<aop:pointcut expression="execution(void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
*
能代替的五种类型:- 返回值可使用
*
,表示任意返回值 - 包路径可以使用
*
,表示任意包. 但是***.的个数要和包的层级数相匹配** - 类名可以使用
*
,表示任意类 - 方法名可以使用
*
,表示任意方法 - 参数列表可以使用
*
,表示参数可以是任意数据类型,但是必须存在参数
<aop:pointcut expression="execution(* com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut> <aop:pointcut expression="execution(* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut> <aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut> <aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut> <aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut>
- 返回值可使用
- 包路径可以使用
*..
,表示当前包,及其子包(因为本例子中将bean.xml
放在根路径下,因此..
可以匹配项目内所有包路径)<aop:pointcut expression="execution(* *..AccountServiceImpl.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
- 参数列表可以使用
..
表示有无参数均可,有参数可以是任意类型<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
- 全通配方式,可以匹配匹配任意方法
<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
- 切入点表达式的一般写法: 一般我们都是对业务层所有实现类的所有方法进行增强,因此切入点表达式写法通常为【重点】
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"></aop:pointcut>
- 全匹配方式:
- 属性:
- 配置的步骤:
- 基于注解的AOP配置:(半注解的形式)
- 配置的步骤:
- 在
bean.xml
文件中引入约束并配置- 步骤:
- 在配置文件中导入
context
的名称空间 和 所需要的jar
包<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> </beans>
- 配置spring框架开启注解aop的支持
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 配置spring创建 容器时扫描的包
<context:component-scan base-package="com.itheima"></context:component-scan>
- 完整的XML文件:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置spring创建 容器时扫描的包--> <context:component-scan base-package="com.itheima"></context:component-scan> <!--配置spring框架开启注解aop的支持--> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
- 在配置文件中导入
- 步骤:
- 进行通知类的注解配置:
- 步骤:
-
将切面类存入Spring容器中 使用的注解是@Component 表明当前类是一个切面类 使用的注解是@Aspect 相当于xml配置中的
<aop:aspect>
标签@Component("logger") @Aspect // 表示的是当前类是一个切面类 public class Logger { //将提供日志的公共代码进行抽取 }
-
进行注解表达式的配置 使用的注解是@Pointcut 相当于
<aop: xxx>
通知中的pointcut
属性 或者是<aop:pointcut>
标签// 配置的是注解表达式 @Pointcut("execution(public void com.itheima.service.impl.AccountServiceImpl.saveAccount())") private void pt1(){}
-
在增强方法上使用注解配置通知 这里例子中是前置通知相当于
<aop:before>
标签// 进行引用 注意的是一定到加上括号 @Before("pt1()") public void beforePrintLog(){ System.out.println("Logger类中的printLog方法执行了,开始记录日志了"); }
- 用于声明通知的五种注解
- @Before: 声明该方法为前置通知.相当于xml配置中的
<aop:before>
标签 - @AfterReturning: 声明该方法为后置通知.相当于xml配置中的<
aop:after-returning>
标签 - @AfterThrowing: 声明该方法为异常通知.相当于xml配置中的
<aop:after-throwing>
标签 - @After: 声明该方法为最终通知.相当于xml配置中的
<aop:after>
标签 - @Around: 声明该方法为环绕通知.相当于xml配置中的
<aop:around>
标签
- @Before: 声明该方法为前置通知.相当于xml配置中的
- 属性
- String value():用于指定切入点表达式或切入点表达式的引用
- String argNames() default “”; 用来接收AspectJ表达式中的参数
- 用于声明通知的五种注解
-
环绕通知的配置:
// 配置环绕通知 @Around("pt1()") public Object aroundPringLog(ProceedingJoinPoint pjp){ Object rtValue = null; try{ Object[] args = pjp.getArgs(); printLogBefore(); // 执行前置通知 常常用来开启事务 rtValue = pjp.proceed(args); // 执行切入点方法 printLogAfterReturning(); // 执行后置通知 常常用来事务的提交 return rtValue; }catch (Throwable t){ printLogAfterThrowing(); // 执行异常通知 常常用来进行事务的回滚 throw new RuntimeException(t); }finally { printLogAfter(); // 执行最终通知 常常用来进行资源的释放 } } }
-
- 步骤:
- 在
- 配置的步骤:
- 纯注解的AOP配置:
- 在Spring配置类前添加
@EnableAspectJAutoProxy
注解,可以使用纯注解方式配置AOP@Configuration @ComponentScan(basePackages="cn.maoritian") @EnableAspectJAutoProxy // 允许AOP public class SpringConfiguration { // 进行具体配置 }
- 使用的注解:
- @ComponentScan:属性
Class<?>[] basePackageClasses() default {};
这样的属性能指定 字节码文件 不是 String类型的类名
- @ComponentScan:属性
- 在Spring配置类前添加
- Spring-AOP通过注解@DeclareParents引入新的方法:
-
Spring中使用Before、After、AfterRunning、AfterThrowing以及Around 共5中通知方式为目标方法增加切面功能 常常是用在 安全 日志等模块 但是想要在原有的类上增加一个新的方法 最简单的方式就是再累中直接增加目标方法,但是 原有的目标类可能非常的复杂这样的方式也许非常的复杂 再这样的场景中使用代理就是非常的实用
-
简单的说就是将所需要添加的方法 建立一个类 将原有的目标类和新建的有需要添加方法的类使用一个代理类进行代理:这样的方式Spring已经想到了 并且实现了 即通过@DeclareParents注解就可以实现该功能
-
@DeclareParents注解由三部分组成:
-
value属性指定了哪种类型的bean要引入该接口(标记符后面的+ 表示接口的所有子类型,而不是接口本身)
-
defaultImpl属性指定了为引入功能提供实现的类
-
@DeclareParents注解所标注的静态属性指明了要引入了接口
// 目标接口 public interface Person { void sleep(); } // 目标接口的实现类 @Component("women") public class Women implements Person { @Override public void sleep() { System.out.println("睡觉了!!!!"); } }
// 添加功能需要的接口 public interface Animal { void eat(); } // 添加方法实现类 @Component public class FemaleAnimal implements Animal { @Override public void eat() { System.out.println("开饭了 !!!"); } }
@Aspect @Component public class AspectConfig { //"+"表示person的所有子类;defaultImpl 表示默认需要添加的新的类 @DeclareParents(value = "com.xxx.annotation.Person+", defaultImpl = FemaleAnimal.class) public static Animal animal; }
使用这样的方式就能在不改变原有的类上进行新方法的添加 在使用的时候直接使用原有的bean类型(不是使用代理类型)就行 但是使用Spring aop命名空间中的<aop:declare-parents>标签同样可以实现相同的功能
<aop:config> <aop:aspect> <aop:declare-parents types-matching="哪种类型的bean要引入该接口" implement-interface="为引入功能提供实现的类" delegate-ref="标注的属性" </aop:aspect> </aop:config>
-
-
- AOP相关术语(进行了解)