动态代理
面向切面编程。
在项目运行的时候,在不改变已有代码的情况下,自动的向方法中添加新的功能。
AOP 的本质实际上就是动态代理。
Java 代理:
- 静态代理
- 动态代理:
- JDK
- CGLIB
JDK 动态代理:
/**
* 1. JDK 动态代理
* - 代理的工具,都是 JDK 自己提供的,不需要额外的 jar
* - JDK 只能代理有接口的类,没有接口的类,是代理不了的
* 2. CGLIB 动态代理
*/
public class MainDemo01 {
public static void main(String[] args) {
//先创建一个计算器对象
CalculatorImpl calculatorImpl = new CalculatorImpl();
//创建一个代理对象
//类加载器
//这个方法返回的是一个代理对象,第二参数是指这个返回的代理对象实现了哪个接口
//代理对象的处理器
Calculator calculator = (Calculator) Proxy.newProxyInstance(MainDemo01.class.getClassLoader(), new Class[]{
Calculator.class}, new InvocationHandler() {
/**
* 具体的代理逻辑
* @param o 这个参事实际上就是自动生成的代理对象本身
* @param method 这个就是生成的代理对象中的方法
* @param objects 生成的代理对象的方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
String name = method.getName();
Object invoke;
if ("add".equals(name)) {
//如果是加法在执行
long startTime = System.currentTimeMillis();
invoke = method.invoke(calculatorImpl, objects);
long endTime = System.currentTimeMillis();
System.out.println(name + " 方法执行耗时 " + (endTime - startTime) + " 毫秒");
} else {
invoke = method.invoke(calculatorImpl, objects);
}
return invoke;
}
});
calculator.add(3, 4);
calculator.min(3, 4);
}
}
- Proxy.newProxyInstance 方法的返回值,必须用接口来接收,不能用 CalculatorImpl 来接收。本质上,Proxy.newProxyInstance 的作用,相当于自动帮你生成了一个类,自动生成的类,实现了 Calculator 接口,所以这个方法的返回值是 Calculator 接口的实例,但不是 CalculatorImpl 的实例。
动态代理生成的类,大概是这个样子:
public class com.sun.proxy.$Proxy0 implements Calculator{
public int add(int a,int b){
long startTime = System.currentTimeMills();
//利用反射执行 CalculatorImpl 对象的 add 方法
long endTime = System.currentTimeMills();
//打印执行时间
//返回第二步的执行结果
}
public void min(int a,int b){
}
}
AOP
概念
- 切点(pointcut):要增加代码的地方,一般是在某个方法执行前后加入目标代码,这个方法的位置,就是切点。
- 通知/增强(advice):要添加的代码,称之为 advice。
- 切面:(aspect):切点+通知。
AOP底层就是动态代理,而动态代理有两种实现方式,在 Spring 中,默认情况下,如果被代理的对象有接口,则动态代理使用 JDK 动态代理,如果被代理的对象没有接口,则被代理的对象使用 CGLIB 动态代理。
XML 配置 AOP
CGLIB
首先定义一个计算器类,这个类没有接口:
public class CalculatorImpl{
public int add(int a, int b) {
return a + b;
}
public void min(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
然后定义通知/增强:
/**
* 这是通知/增强
* <p>
* AOP 中存在五种通知:
* 1. 前置通知:在目标方法执行之前主执行
* 2. 后置通知:在目标方法执行之后执行
* 3. 异常通知:当目标方法抛出异常的时候执行
* 4. 返回通知:当目标方法返回值的时候执行
* 5. 环绕通知:集大成者,上面四种都包含在这个里边
*/
public class LogAdvice {
/**
* 前置通知
*/
public void before(JoinPoint jp) {
//获取目标方法名称
String name = jp.getSignature().getName();
System.out.println(name + "开始执行了...");
}
/**
* 后置通知
*
* @param jp
*/
public void after(JoinPoint jp) {
System.out.println(jp.getSignature().getName() + " 方法执行结束了...");
}
/**
* 异常通知
* <p>
* 当目标方法抛出异常的时候,这个方法会执行
* <p>
* 注意异常的参数,只有目标方法抛出的异常,是这个异常参数或者它的子类的时候,才会进入到这个方法中
* <p>
* <p>
* <p>
* 这个异常通知的原理,相当于目标方法用 try-catch 裹起来
* <p>
* try{
* <p>
* method.invoke(xxxx)
* }catch(NullPointerException e){
* <p>
* }
*/
public void throwing(JoinPoint jp, Exception e) {
System.out.println(jp.getSignature().getName() + " 方法抛出 " + e.getMessage() + " 异常");
}
/**
* 返回通知,目标方法返回值和这里参数类型匹配的时候,这个方法会被触发
* <p>
* 注意,返回值为 void,对应的类型为 Void,而 Void 是 Object 的子类,所以,这里如果用 Object 去接收返回类型,那么返回值为 void 的方法也会进入到返回通知中
*/
public void returning(JoinPoint jp, Object result) {
System.out.println(jp.getSignature().getName() + " 方法返回了 " + result);
}
/**
* 环绕通知
*/
public Object around(ProceedingJoinPoint pjp) {
try {
//这行代码就类似于 method.invoke 方法
//当执行这行代码的时候,目标方法才会被真正的执行
Object proceed = pjp.proceed();
//注意这个地方有返回值
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
XML 中配置 AOP:
<?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 class="com.qfedu.demo.p1.service.CalculatorImpl" id="calculator"/>
<bean class="com.qfedu.demo.p1.LogAdvice" id="logAdvice"/>
<aop:config>
<!--
id 表示切点的名称
expression:表示切点的定义,第一个 * 表示方法返回值任意(这个位置也可以给定一个具体的返回类型)
第二个 * 表示 service 下的所有类
第三个 * 表示 任意方法
.. 表示参数任意(参数可有可无,如果有,参数类型也是任意的)
-->
<aop:pointcut id="pc1" expression="execution(* com.qfedu.demo.p1.service.*.*(..))"/>
<!--ref 表示通知的 Bean-->
<aop:aspect ref="logAdvice">
<!--定义前置通知-->
<aop:before method="before" pointcut-ref="pc1"/>
<aop:after method="after" pointcut-ref="pc1"/>
<aop:after-throwing method="throwing" pointcut-ref="pc1" throwing="e"/>
<aop:after-returning method="returning" pointcut-ref="pc1" returning="result"/>
<aop:around method="around" pointcut-ref="pc1"/>
</aop:aspect>
</aop:config>
</beans>
最后加载 Spring 容器:
public class Demo01 {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//注意这里返回的对象,是 Spring 容器根据 Calculator 接口自动生成的一个实现类的对象
//是一个代理的对象
CalculatorImpl calculator = (CalculatorImpl) ctx.getBean("calculator");
System.out.println("calculator.getClass() = " + calculator.getClass());
// calculator.add(3, 4);
calculator.min(3,4);
}
}
注意,这个地方虽然我们用 CalculatorImpl 类型接收的 Spring 容器中的 Bean,但实际上返回的对象并不是 CalculatorImpl 本身,而是它的子类的实例,此时使用的动态代理是 CGLIB动态代理。
calculator.getClass() = class com.qfedu.demo.p1.service.CalculatorImpl$$EnhancerBySpringCGLIB$$9e71c934
JDK
该两个地方,就可以变为 JDK 动态代理:
首先给 CalculatorImpl 添加一个接口:
public class CalculatorImpl implements Calculator { @Override public int add(int a, int b) { // int i = 1 / 0; return a + b; } @Override public void min(int a, int b) { System.out.println(a + "-" + b + "=" + (a - b)); } } public interface Calculator { int add(int a, int b); void min(int a, int b); }
加载容器的时候,使用 Calculator 去接收:
public class Demo01 { public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); //注意这里返回的对象,是 Spring 容器根据 Calculator 接口自动生成的一个实现类的对象 //是一个代理的对象 Calculator calculator = (Calculator) ctx.getBean("calculator"); System.out.println("calculator.getClass() = " + calculator.getClass()); // calculator.add(3, 4); calculator.min(3,4); } }
此时,由于被代理的对象有接口,所以就会使用 JDK 动态代理。
打印的日志如下:
calculator.getClass() = class com.sun.proxy.$Proxy5
当被代理的对象有接口的时候,通过修改 XML 中的配置,也可以实现使用 CGLIB 动态代理:
<aop:config proxy-target-class="true">
<!--
id 表示切点的名称
expression:表示切点的定义,第一个 * 表示方法返回值任意(这个位置也可以给定一个具体的返回类型)
第二个 * 表示 service 下的所有类
第三个 * 表示 任意方法
.. 表示参数任意(参数可有可无,如果有,参数类型也是任意的)
-->
<aop:pointcut id="pc1" expression="execution(* com.qfedu.demo.p1.service.*.*(..))"/>
<!--ref 表示通知的 Bean-->
<aop:aspect ref="logAdvice">
<!--定义前置通知-->
<aop:before method="before" pointcut-ref="pc1"/>
<aop:after method="after" pointcut-ref="pc1"/>
<aop:after-throwing method="throwing" pointcut-ref="pc1" throwing="e"/>
<aop:after-returning method="returning" pointcut-ref="pc1" returning="result"/>
<aop:around method="around" pointcut-ref="pc1"/>
</aop:aspect>
</aop:config>
proxy-target-class="true"
通过修改该属性,可以在有接口的情况下,也是用 CGLIB 动态代理。
Java 代码配置 AOP
直接定义一个切面即可,切面中包含了切点和通知:
/**
* LogAspect 这是一个切面,切面包含两部分:切点和通知
* <p>
* AOP 中存在五种通知:
* 1. 前置通知:在目标方法执行之前主执行
* 2. 后置通知:在目标方法执行之后执行
* 3. 异常通知:当目标方法抛出异常的时候执行
* 4. 返回通知:当目标方法返回值的时候执行
* 5. 环绕通知:集大成者,上面四种都包含在这个里边
*
* @Aspect 就表示当前类是一个切面
* @EnableAspectJAutoProxy 表示开启自动代理
*/
@Component
@Aspect
@EnableAspectJAutoProxy
public class LogAspect {
/**
* 统一定义切点
*/
@Pointcut("execution(* com.qfedu.demo.p2.service.*.*(..))")
public void pc() {
}
/**
* 前置通知
* @Before 方法表示这是一个前置通知
*/
@Before("pc()")
public void before(JoinPoint jp) {
//获取目标方法名称
String name = jp.getSignature().getName();
System.out.println(name + "开始执行了...");
}
/**
* 后置通知
*
* @param jp
*/
@After("pc()")
public void after(JoinPoint jp) {
System.out.println(jp.getSignature().getName() + " 方法执行结束了...");
}
/**
* 异常通知
* <p>
* 当目标方法抛出异常的时候,这个方法会执行
* <p>
* 注意异常的参数,只有目标方法抛出的异常,是这个异常参数或者它的子类的时候,才会进入到这个方法中
* <p>
* <p>
* <p>
* 这个异常通知的原理,相当于目标方法用 try-catch 裹起来
* <p>
* try{
* <p>
* method.invoke(xxxx)
* }catch(NullPointerException e){
* <p>
* }
*/
@AfterThrowing(value = "pc()",throwing = "e")
public void throwing(JoinPoint jp, Exception e) {
System.out.println(jp.getSignature().getName() + " 方法抛出 " + e.getMessage() + " 异常");
}
/**
* 返回通知,目标方法返回值和这里参数类型匹配的时候,这个方法会被触发
* <p>
* 注意,返回值为 void,对应的类型为 Void,而 Void 是 Object 的子类,所以,这里如果用 Object 去接收返回类型,那么返回值为 void 的方法也会进入到返回通知中
*/
@AfterReturning(value = "pc()",returning = "result")
public void returning(JoinPoint jp, Object result) {
System.out.println(jp.getSignature().getName() + " 方法返回了 " + result);
}
/**
* 环绕通知
*/
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) {
try {
//这行代码就类似于 method.invoke 方法
//当执行这行代码的时候,目标方法才会被真正的执行
Object proceed = pjp.proceed();
//注意这个地方有返回值
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
另外需要注意,将被代理 Bean 要注册到 Spring 容器中:
public interface Calculator {
int add(int a, int b);
void min(int a, int b);
}
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public void min(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
最后在配置类中,统一扫描到被代理的对象以及切面:
@Configuration
@ComponentScan(basePackages = "com.qfedu.demo.p2")
public class JavaConfig {
}