动态代理
动态代理的实现方式常用的有两种:使用JDK的Proxy与通过CGLIB生成代理。
动态代理的作用:
- 在目标类源代码不改变的情况下,增强功能
- 减少代码的重复
- 专注业务逻辑代码
- 解耦合,让你的业务功能和日志,事务非业务功能分离。
JDK动态代理
jdk动态代理要求目标对象必须实现接口,这是java设计上的要求。
从jdk1.3以来,java语言通过java.lang.reflect包提供三个类支持代理模式Proxy,Method和 InovcationHandler。
CGLIB动态代理
CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code 生成类库,它可以在运行期扩展Java类与实现Java接口。它广泛的被许多AOP的框架使用,例如Spring AOP。
cglib是第三方的工具库,创建代理对象。
cglib的原理是继承,cglib通过继承目标类,创建它的子类,在子类中重写父类中同名的方法,实现功能的修改。
CGLIB代理的生成原理是生成目标类的子类,而子类是增强过的,这个子类对象就是代理对象。所以,使用CGLIB生成动态代理要求自标类必须能够被继承,即不能是 final的类。
对比:
- 使用JDK的 Proxy实现代理,要求目标类与代理类实现相同的接口。若目标类不存在接口,则无法使用该方式实现
- 对于无接口的类,要为其创建动态代理,就要使用CGLIB来实现。
- cglib经常被应用在框架中,例如Spring ,Hibernate,mybatis等。
- cglib 的代理效率高于jdk。对于cglib一般的开发中并不使用。
AOP概述
AOP简介
AOP(Aspect Orient Programming),面向切面编程。面向切面编程是从动态角度考虑程序运行过程。
AOP 底层,就是采用动态代理模式实现的。采用了两种代理:JDK 的动态代理,与 CGLIB的动态代理。
AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程,可通过运行期动态代理实现程序功能的统一维护的一种技术。
AOP 是 Spring 框架中的一个重要内容。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、事务、日志、缓存等。
若不使用 AOP,则会出现代码纠缠,即交叉业务逻辑与主业务逻辑混合在一起。这样,会使主业务逻辑变的混杂不清。例如,转账,在真正转账业务逻辑前后,需要权限控制、日志记录、加载事务、结束事务等交叉业务逻辑,而这些业务逻辑与主业务逻辑间并无直接关系。但,它们的代码量所占比重能达到总代码量的一半甚至还多。它们的存在,不仅产生了大量的“冗余”代码,还大大干扰了主业务逻辑——转账。
面向切面编程的好处
- 减少重复;
- 专注业务;
- 注意:面向切面编程只是面向对象编程的一种补充。
AOP编程术语
-
切面(Aspect)
切面泛指交叉业务逻辑。事务处理、日志处理就可以理解为切面。
常用的切面是通知(Advice),实际就是对主业务逻辑的一种增强。
-
连接点(JoinPoint)
连接点指可以被切面织入的具体方法。
通常业务接口中的方法均为连接点。
-
切入点(Pointcut)
切入点指声明的一个或多个连接点的集合。通过切入点指定一组方法。
被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
-
目标对象(Target)
目标对象指将要被增强的对象。即包含主业务逻辑的类的对象。
-
通知(Advice)
通知表示切面的执行时间,Advice 也叫增强。
换个角度来说,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。
通知类型不同,切入时间不同。切入点定义切入的位置,通知定义切入的时间。
AspectJ对AOP的实现
对于 AOP 这种编程思想,很多框架都进行了实现。Spring 就是其中之一,可以完成面向切面编程。然而,AspectJ 也实现了 AOP 的功能,且其实现方式更为简捷,使用更为方便,而且还支持注解式开发。所以,Spring 又将 AspectJ 的对于 AOP 的实现也引入到了自己的框架中。
在 Spring 中使用 AOP 开发时,一般使用 AspectJ 的实现方式。
AspectJ 简介
AspectJ 是一个优秀面向切面的框架,它扩展了 Java 语言,提供了强大的切面实现。
AspectJ全称是Eclipse AspectJ
官网地址:http://www.eclipse.org/aspectj/
a seamless aspect-oriented extension to the Javatm programming language(一种基于 Java 平台的面向切面编程的语言)
Java platform compatible(兼容 Java 平台,可以无缝扩展)
easy to learn and use(易学易用)
AspectJ框架实现AOP有两种方式:
- 使用xml配置文件:配置全局事务
- 使用注解:在项目中要用AOP功能,一般都使用注解,AspectJ常用5个注解。
AspectJ框架的使用
AspectJ的通知类型
切面的执行时间,这个执行时间在规范中叫做 Advice(通知,增强)。
AspectJ中常用的通知有5种类型:(常使用注解,也可以使用xml)
- 前置通知:
@Before
- 后置通知:
@AfterReturning
- 环绕通知:
@Around
- 异常通知:
@AfterThrowing
- 最终通知:
@After
用法见实现步骤
AspectJ 的切入点表达式
AspectJ 定义了专门的表达式用于指定切入点。
表达式的原型是:
execution(modifiers-pattern?
ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
解释:
modifiers-pattern] 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
?表示可选的部分
以上表达式共 4 个部分。
语法: execution(访问权限? 方法返回值 包名类名?.方法声明(参数) 异常类型?)
切入点表达式要匹配的对象就是目标方法的方法名。execution 表达式中明显就是方法的签名。
在其中可以使用以下符号:
举例:
execution(public * *(..))
指定切入点为:任意公共方法。
execution(* set*(..))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service..*.*(..))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“..”出现在类名中时,后面必须跟“*”,表示包、子包下的所有类。
execution(* *..service.*.*(..))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
AspectJ的aop实现步骤
使用aop的目的是在不改变原来的类和代码前提下,给已经是存在的一些类和方法,增加额外的功能。
基本步骤:
-
新建maven项目
-
加入依赖:spring依赖、aspectj依赖、Junit单元测试依赖
-
创建目标类:接口和实现类(将给类的方法增加功能)
-
创建切面类:普通类
- 在类的上面加入@Aspect
- 在类中定义方法,方法就是切面要执行的功能代码
- 在方法上面加入aspect中的通知(Advice)注解,例如:
@Before
等,还有需要指定切入点表达是execution()
-
创建spring配置文件:声明对象、把对象交给容器管理。
(声明对象可以用注解或者xml文件的
<bean>
标签)- 声明目标对象
- 声明切面类对象
- 声明aspect框架中的自动代理生成器标签。(自动代理生成器:用来完成代理对象的自动创建功能)
补充:
- 在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类+ 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。
<aop:aspectj-autoproxy/>
的底层是由AnnotationAwareAspectJAutoProxyCreator
实现的。从其类名就可看出,是基于 AspectJ 的注解适配自动代理生成器。- 其工作原理是,
<aop:aspectj-autoproxy/>
通过扫描找到@Aspect 定义的切面类,再由切
面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点。
- 其工作原理是,
- 创建测试类,从spring容器中获取目标对象(实际就代理对象),通过代理执行方法,实现aop的功能增强。
示例:
导入依赖:
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- spring框架依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<!-- aspect依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
创建一个目标类接口和目标实现类:
package com.maj.ba01;
// 目标类接口
public interface SomeService {
void doSome(String name, Integer age);
}
package com.maj.ba01;
// 目标类
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
System.out.println("-------目标方法doSome()-------");
}
}
创建一个切面类:
package com.maj.ba01;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/*
* 定义方法:用来实现切面功能的
* 定以方法的要求:
* 1.公共方法 public
* 2.方法没有返回值 void
* 3.方法名自定义
* 4.方法可以无参数,可以有参数
* 若有参数,参数不是自定义的,有几个参数可以使用
*
* */
/*
* @Before: 前置通知注解
* 属性:value="execution(....)" 切入点表达式
* */
@Before(value = "execution(public void com.maj.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(){
// 切面要执行的功能代码
System.out.println("前置通知,在目标方法之前输出执行时间:"+ new Date());
}
}
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
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 声明目标类对象 -->
<bean id="someService" class="com.maj.ba01.SomeServiceImpl" />
<!-- 声明切面类对象 -->
<bean id="myAspect" class="com.maj.ba01.MyAspect" />
<!--
声明自动代理生产器:
使用aspect框架内部的功能,创建目标对象的代理对象。
创建代理对象是在内存中实现的,修改目标对象在内存中的结构。
所以,目标对象就是被修改后的代理对象。
aspectj-autoproxy: 会把spring容器中的所有的目标对象一次性都生成代理对象
-->
<aop:aspectj-autoproxy />
<!-- <aop:aspectj-autoproxy proxy-target-class="true" /> -->
<!--
proxy-target-class属性:
默认不写时,则是有接口的实现类用jdk动态代理,无接口就用cglib动态代理
proxy-target-class属性值为true,则是强制使用cglib动态代理
-->
</beans>
测试类:
package com.maj;
import com.maj.ba01.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyText01 {
@Test
public void text01(){
String config = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(config);
// 从容器中获取目标对象(这里虽然看着是拿到了目标对象,实际是代理对象)
SomeService proxy = (SomeService) ac.getBean("someService");
// 通过代理对象执行方法,实现目标方法执行时就增强了功能
proxy.doSome("何鼎东",3);
}
}
运行结果:
前置通知,在目标方法之前输出执行时间:Wed Jul 14 22:23:18 CST 2021
-------目标方法doSome()-------
前置通知
注解:@Before
- 在目标方法执行之前执行。
- 被注解为前置通知的方法,可以包含一个 JoinPoint 类型参数。
- 用来获取要加入切面功能的目标方法(例如,doSome(String name, Integer age)方法,可以获取其中的所有相关信息)
- 该类型的对象本身就是切入点表达式。
- 通过该参数,可获取切入点表达式、方法签名(定义)、 目标对象等。
- JoinPoint参数的值是由框架赋予,且必须放在参数中第一个位置。
- 不光前置通知的方法,可以包含一个 JoinPoint 类型参数,所有的通知方法均可包含该参数。
package com.maj.ba01;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
@Aspect
public class MyAspect {
@Before(value = "execution(public void com.maj.ba01.SomeServiceImpl.doSome(String, Integer))")
public void myBefore(JoinPoint jp){
// 获取方法的完整定义
System.out.println("方法的签名(定义):" + jp.getSignature());
System.out.println("方法的名称:" + jp.getSignature().getName());
// 获取方法的实参
Object[] args = jp.getArgs();
for (Object arg:args){
System.out.println("参数值:" + arg);
}
// 切面要执行的功能代码
System.out.println("前置通知,在目标方法之前输出执行时间:"+ new Date());
}
}
后置通知
注解:@afterReturning
-
在目标方法执行之后执行。
-
由于是目标方法之后执行,所以可以获取到目标方法的返回值。
-
该注解的 returning 属性就是用于指定接收方法返回值的变量名的。
-
被注解为后置通知的方法,除了可以包含 JoinPoint 参数外,还可以包含用于接收返回值的变量。该变
量最好为 Object 类型,因为目标方法的返回值可能是任何类型。
示例:
package com.maj.ba02;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/*
* 后置通知定义方法:
* 方法定义的要求:
* 1. 公共方法public
* 2. 方法没有返回值
* 3. 方法名称自定义
* 4. 方法有参数的,推荐是Object, 参数名自定义
*/
/**
* @AfterReturning: 后置通知
* 属性:1. value 切入点表达式
* 2. returning 定义的变量
* 位置: 在方法定义上面
* 特点: 1. 在目标方法之后执行的。
* 2. 能够获取到目标方法的返回值,可以根据这个返回值做不同的处理
* 3. 可以修改这个返回值
*/
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))", returning = "res")
public void myAfterReturning(JoinPoint joinPoint, Object res){
/*
Object res: 是目标方法执行后的返回值,根据返回值做你的切面的功能处理
这里的参数名必须与returning的值相同。
*/
System.out.println("后置通知:在目标方法之后执行, 获取的目标方法的返回值是:" + res);
}
}
环绕通知
注解:@Around
- 环绕通知就等同于jdk动态代理,(InvocationHandler接口)
- 在目标方法执行之前之后执行。
- 被注解为环绕增强的方法要有返回值,Object 类型。并且方法可以包含一个 ProceedingJoinPoint 类型的参数
- 接口 ProceedingJoinPoint 其有一个
proceed()
方法,用于执行目标方法。若目标方法有返回值,则该方法的返回值就是目标方法的返回值。- ProceedingJoinPoint 是继承的JoinPoint ,所以也可以用后者的方法
- 环绕增强方法将其返回值返回。
- 该增强方法实际是拦截了目标方法的执行。
- 环绕通知经常是用来做事务的
示例:
package com.maj.ba03;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import java.util.Date;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/*
* 环绕通知:用来实现切面功能的
* 定以方法的要求:
* 1.公共方法 public
* 2.方法必须有一个返回值, 推荐使用Object
* 3.方法名自定义
* 4.方法有参数,固定的参数:ProceedingJoinPoint
*
* */
/**
* @Around: 环绕通知
* 属性:value="切入点表达式"
* 特点:
* 1.功能最强的通知
* 2. 在目标方法的前后都可以增强功能
* 3.控制目标方法是否被调用执行
* 4.可以修改原来的目标方法的执行结果,影响最后调用的结果
*
* 参数ProceedingJoinPoint的作用:执行目标方法
* 返回值: 就是目标方法的执行结果,可以被修改。
*/
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
Object result = null;
System.out.println("环绕通知:在目标方法之前做事");
// 执行目标方法
result = pjp.proceed();
System.out.println("环绕通知:在目标方法之后做事");
// 返回目标方法的返回值(这里可以是修改后的值)
return result;
}
}
异常通知
注解:@myAfterTrowing
在目标方法抛出异常后执行。
该注解的 throwing 属性用于指定所发生的异常类对象。
被注解为异常通知的方法可以包含一个参数 Throwable,参数名称为 throwing 指定的名称,表示发生的异常对象。
package com.maj.ba04;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/*
* 异常通知:用来实现切面功能的
* 定以方法的要求:
* 1.公共方法 public
* 2.方法没有返回值 void
* 3.方法名自定义
* 4.方法有参数Exception,和JoinPoint
*
* */
/**
* @myAfterTrowing:异常通知
* 属性:1.value="切入点表达式"
* 2.throwing 自定义的变量,表示目标方法抛出的异常对象
* 变量名必须与参数名一样
* 特点:
* 1.在目标方法抛出异常时执行的
* 2.可以做异常的监控程序,监控目标方法执行时是否有异常
* @param ex
*/
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))", throwing = "ex")
public void myAfterTrowing(Exception ex){
System.out.println("异常通知:方法发生异常时执行");
System.out.println("发送信息或者邮件给开发人员,程序有异常了,异常信息:"+ex.getMessage());
}
}
最终通知
注解:@After
- 无论目标方法是否抛出异常,该增强均会被执行。
package com.maj.ba05;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/*
* 最终 通知:用来实现切面功能的
* 定以方法的要求:
* 1.公共方法 public
* 2.方法没有返回值 void
* 3.方法名自定义
* 4.方法没有自己特有的参数,只有JoinPoint参数
*
* */
/**
* doThird:最终通知
* 属性:value="切入点表达式"
* 位置:在方法的上面
* 特点:1. 总是会执行
* 2. 在目标方法之后执行
* 3.一般多是做资源清除工作的
*/
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("执行最终通知:无论是否有异常,总是会被执行");
}
}
@Pointcut 定义切入点
类似于给切入点表达起别名
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。
AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
用法:
- 将@Pointcut 注解在一个方法之上
- 以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。
- 代表的就是@Pointcut 定义的切入点。
这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。
示例:
package com.maj.ba06;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect // 这是aspect框架中的注解,用来表示当前类是切面类,增强功能的类
public class MyAspect {
/**
* doThird:最终通知
* 属性:value="切入点表达式"
* 位置:在方法的上面
* 特点:1. 总是会执行
* 2. 在目标方法之后执行
* 3.一般多是做资源清除工作的
*/
@After(value = "mypt()")
public void myAfter(){
System.out.println("执行最终通知:无论是否有异常,总是会被执行");
}
@Before(value = "mypt()")
public void myBefore(){
System.out.println("前置通知:在目标方法之前执行");
}
/**
*@Pointcut: 定义和管理切入点
* 属性:1.value="切入点表达式"
* 位置:在自定义的方法上
* 特点:1.给切入点表达取别名
* 2.在其他的通知中,如果想要用这个切入点表达式,直接在value属性的值上调用次方法
*/
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
private void mypt(){
// 无需代码
}
}