代理
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
先来看一个需求:
设计一个计算器程序,要求在计算的前后输出日志
先定义一个接口
package com.atguigu.spring6.aop.annoaop;
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
创建接口的实现类
package com.atguigu.spring6.aop.annoaop;
import org.springframework.stereotype.Component;
//基本实现类
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
在程序里面设计一个在计算前后输出日志的功能
package com.atguigu.spring6.aop.example;
//带日志
public class CalculatorLogImpl implements Calculator{
@Override
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
①现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
在没有使用代理之前,我们调用的逻辑是这样的:
下面我们要使用代理,代理的调用逻辑是这样的:
使用代理以后得代码
package com.atguigu.spring6.aop.example;
public class CalculatorStaticProxy implements Calculator{
//被代理目标对象传递过来
private Calculator calculator;
public CalculatorStaticProxy(Calculator calculator) {
this.calculator = calculator;
}
@Override
public int add(int i, int j) {
//输出日志
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
//调用目标对象的方法实现核心业务
int addResult = calculator.add(i, j);
System.out.println("[日志] add 方法结束了,结果是:" + addResult);
return addResult;
}
@Override
public int sub(int i, int j) {
return 0;
}
@Override
public int mul(int i, int j) {
return 0;
}
@Override
public int div(int i, int j) {
return 0;
}
}
上面就是使用了代理技术,不过使用的是静态代理。
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
动态代理的过程:
实现代码
package com.atguigu.spring6.aop.example;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
public class ProxyFactory {
//目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//返回代理对象
public Object getProxy() {
/**
* Proxy.newProxyInstance()方法
* 有三个参数
* 第一个参数:ClassLoader: 加载动态生成代理类的来加载器
* 第二个参数: Class[] interfaces:目录对象实现的所有接口的class类型数组
* 第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程
*/
//第一个参数:ClassLoader: 加载动态生成代理类的来加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//第二个参数: Class[] interfaces:目录对象实现的所有接口的class类型数组
Class<?>[] interfaces = target.getClass().getInterfaces();
//第三个参数:InvocationHandler:设置代理对象实现目标对象方法的过程
InvocationHandler invocationHandler =new InvocationHandler() {
//第一个参数:代理对象
//第二个参数:需要重写目标对象的方法
//第三个参数:method方法里面参数
@Override
public Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable {
//方法调用之前输出
System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
//调用目标的方法
Object result = method.invoke(target, args);
//方法调用之后输出
System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
return result;
}
};
return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);
}
}
package com.atguigu.spring6.aop.example;
public class TestCal {
public static void main(String[] args) {
//创建代理对象(动态)
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator)proxyFactory.getProxy();
//proxy.add(1,2);
proxy.mul(2,4);
}
}
AOP
AOP(Aspect Oriented Programming)是一种面向切面编程的思想,切面指的是系统的某一个方面。AOP是针对程序中的某一类(某个方面)的功能做统一处理,比如用户登录权限的效验。Spring AOP是一个框架,提供了AOP思想的实现,AOP和Spring AOP的关系就像IoC和DI。
AOP可以实现一下功能:
统一日志记录、统一方法执行时间统计、统一格式返回格式设置、统一的异常处理、事务的开启和提交等,AOP编程是对OOP编程的补充和完善。
在登录授权中AOP的作用:
AOP的组成
AOP主要由四点组成,分别是:切面、切点、连接点、通知
切面
切面由切点和通知组成,它是针对某一个功能的具体定义,这个功能可能是登录验证功能,也有可能是日志记录功能,这里可以将AOP形象的比喻为一个数据库,一个AOP就是一个数据库。
切点
切点是一个保存了众多连接点的一个集合,切点是切面中的某个方法,如果将切面比作数据库,那么切点就是数据库里面的表。
通知
我们将切面必须完成工作称为通知,切面的工作就是通知。通知有五种:前置通知(使用@Before)、后置通知(使用@After)、返回之后通知(使用@AfterReturning)、抛出异常通知(使用AfterThrowing)、环绕通知(使用@Around)
连接点
所有可能触发AOP(拦截方法的点)就称之为连接点
上面AOP的组成可以用一张图来表示:
Spring AOP的实现
基于注解的方式实现
Spring AOP的实现步骤如下:
1.添加Spring AOP框架支持;
2.定义切面和切点;
3.定义通知;
添加Spring AOP框架支持
在pom.xml文件中插入以下框架
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面和切点
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
}
对上面代码的解释:
@Aspect这个注解表示这个类是一个AOP类,也就是说这个类是一个切面;
@Component这个注解表示组件,当我们不知道具体该给某个类使用那个注解来注入到Spring框架时,我们就可以使用@Component这个注解;
@Pointcut这个注解表示该方法是一个切点,它后面跟的是AspectJ表达式(切点表达式)。该表达式的语法:execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
上面的包和类一般情况下是要有的,但可以省略;异常可省略一般不写。
AspectJ支持三种通配符:
*:匹配任意字符,只匹配一个元素(这个元素可以是包、类、或者方法、
方法参数)。
..:匹配任意字符,可以匹配多个元素,在表示类时,必须和*联合使用。
+:表示按照类型匹配指定类的所有类,必须跟在类名后面,如com.cad.Car+,表示继承该类的所有子类包括本身。
表达式示例:
execution(* addUser(String,int)):匹配addUser方法,且第一个参数类型是String,第二个参数是int。
excution(* com.cad.demo.User.*(..)):匹配User类里面的所有方法。
excution(* com.cad.demo.User+.*(..)):匹配该类的子类包括该类的所有方法。
excution(* com.cad.*.*(..):匹配com.cad包下面的所有类的所有方法。
excution(* com.cad..*.*(..)):匹配com.cad包下、子孙包下所有类的所有方法。
定义通知(在什么时机执行什么方法)
通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证就是具体要执行的业务。Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知使用@Before:通知方法会在目标方法执行调用之前执行。
后置通知使用@After:通知方法会在目标方法返回或者抛出异常后调用。
返回之后通知使用@AfterRuturning:通知方法会在目标方法返回后调用。
抛出异常后通知使用@AfterThrowing:通知方法会在目标方法抛出异常后调用。
环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
执行前置通知:
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void doBefore(){
//业务代码...
System.out.println();
System.out.println("执行了前置通知");
System.out.println();
}
}
演示结果:
执行后置通知:
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
//后置通知
@After("pointcut()")
public void doAfter(){
//业务代码...
System.out.println();
System.out.println("执行了后置通知");
System.out.println();
}
}
执行返回之后通知:
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void doBefore(){
//业务代码...
System.out.println();
System.out.println("执行了前置通知");
System.out.println();
}
//后置通知
@After("pointcut()")
public void doAfter(){
//业务代码...
System.out.println();
System.out.println("执行了后置通知");
System.out.println();
}
//return之前通知
@AfterReturning("pointcut()")
public void doAfterReturning(){
//业务代码...
System.out.println();
System.out.println("执行了doAfterReturning通知");
System.out.println();
}
}
可以看到执行返回之后通知它是在返回的时候之后调用的,在返回通知之前调用。
执行抛出异常后通知:
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void doBefore(){
//业务代码...
System.out.println();
System.out.println("执行了前置通知");
System.out.println();
}
//后置通知
@After("pointcut()")
public void doAfter(){
//业务代码...
System.out.println();
System.out.println("执行了后置通知");
System.out.println();
}
//return之前通知
@AfterReturning("pointcut()")
public void doAfterReturning(){
//业务代码...
System.out.println();
System.out.println("执行了doAfterReturning通知");
System.out.println();
}
//抛出异常之前通知
@AfterThrowing("pointcut()")
public void doAfterThrowing(){
//业务代码...
System.out.println();
System.out.println("执行了doAfterThrowing通知");
System.out.println();
}
}
执行环绕通知:
//添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object o = null;
System.out.println("Around 方法开始执行");
try {
o = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Around方法结束执行");
return o;
}
可以看到环绕通知是在所有通知方法之前和之后调用,因此可以用来统计方法的执行时间。
@Aspect //定义切面
@Component
public class UserAspect {
//切点(配置拦截规则)
@Pointcut("execution(* com.example.myblog.controller.UserController.*(..))")
public void pointcut(){
}
//添加环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object o = null;
StopWatch stopWatch = new StopWatch();
try {
stopWatch.start();
o = joinPoint.proceed();
stopWatch.stop();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println(joinPoint.getSignature().getDeclaringTypeName()+"."+
joinPoint.getSignature().getName()+" 方法执行花费的时间: "+
stopWatch.getTotalTimeMillis()+"ms");
return o;
}
}
基于XML的方式实现
package com.atguigu.spring6.aop.xmlaop;
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
package com.atguigu.spring6.aop.xmlaop;
import org.springframework.stereotype.Component;
//基本实现类
@Component
public class CalculatorImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部 result = " + result);
//为了测试,模拟异常出现
// int a = 1/0;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
package com.atguigu.spring6.aop.xmlaop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
//切面类
@Component //ioc容器
public class LogAspect {
//前置通知
public void beforeMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("Logger-->前置通知,方法名称:"+methodName+",参数:"+Arrays.toString(args));
}
// 后置通知
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->后置通知,方法名称:"+methodName);
}
// 返回通知,获取目标方法的返回值
public void afterReturningMethod(JoinPoint joinPoint,Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->返回通知,方法名称:"+methodName+",返回结果:"+result);
}
// 异常通知 获取到目标方法异常信息
//目标方法出现异常,这个通知执行
public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("Logger-->异常通知,方法名称:"+methodName+",异常信息:"+ex);
}
// 环绕通知
public Object aroundMethod(ProceedingJoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
String argString = Arrays.toString(args);
Object result = null;
try {
System.out.println("环绕通知==目标方法之前执行");
//调用目标方法
result = joinPoint.proceed();
System.out.println("环绕通知==目标方法返回值之后");
}catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("环绕通知==目标方法出现异常执行");
} finally {
System.out.println("环绕通知==目标方法执行完毕执行");
}
return result;
}
//重用切入点表达式
@Pointcut(value = "execution(* com.atguigu.spring6.aop.xmlaop.CalculatorImpl.*(..))")
public void pointCut() {}
}
<?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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.atguigu.spring6.aop.xmlaop"></context:component-scan>
<!--配置aop五种通知类型-->
<aop:config>
<!--配置切面类-->
<aop:aspect ref="logAspect">
<!--配置切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.atguigu.spring6.aop.xmlaop.CalculatorImpl.*(..))"/>
<!--配置五种通知类型-->
<!--前置通知-->
<aop:before method="beforeMethod" pointcut-ref="pointcut"></aop:before>
<!--后置通知-->
<aop:after method="afterMethod" pointcut-ref="pointcut"></aop:after>
<!--返回通知-->
<aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointcut"></aop:after-returning>
<!--异常通知-->
<aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointcut"></aop:after-throwing>
<!--环绕通知-->
<aop:around method="aroundMethod" pointcut-ref="pointcut"></aop:around>
</aop:aspect>
</aop:config>
</beans>
package com.atguigu.spring6.aop.xmlaop;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAop {
@Test
public void testAdd() {
ApplicationContext context =
new ClassPathXmlApplicationContext("beanaop.xml");
Calculator calculator = context.getBean(Calculator.class);
calculator.add(4,3);
}
}
切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
场景:日志记录
记录用户的登录和退出:
//定义切面
@Aspect
@Component
public class UserAspect {
/**
* 定义切点,切点是一个包含了众多拦截点的一个集合
* @param joinPoint 所有需要aop处理的方法都称为连接点
*/
//配置代理规则
@Before("execution(* com.example.demo.controller.UserController.*(..))")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
System.out.println("Executing " + className + "." + methodName + "()");
// 记录日志到日志文件或数据库
}
}
用户的登录和注销操作在UserController类里面,当用户请求登录和注销操作时:
Spring AOP实现原理
Spring AOP是构建在动态代理基础上的,因此Spring对AOP的支持局限于方法级别的拦截。Spring AOP支持JDK Proxy和CGLIB方式实现动态代理,而这两类方式底层都是通过反射来实现的。
动态代理分两种,基于接口的动态代理和基于类的动态代理。
基于接口的动态代理
基于接口的动态代理使用Java的反射机制,在运行时动态地创建代理对象。代理对象实现了与原始对象相同的接口,并将方法调用转发给原始对象,同时还可以在方法调用前后执行其他逻辑。使用`java.lang.reflect.Proxy`类和`java.lang.reflect.InvocationHandler`接口可以实现基于接口的动态代理。
基于类的动态代理
基于类的动态代理是通过继承或扩展来实现的。在运行时,它创建一个类来继承原始类,并覆盖其中的方法以添加额外的业务逻辑。使用第三方库,如CGLIB(Code Generation Library)可以实现基于类的动态代理。
AspectJ
AspectJ是AOP的框架, Spring依赖AspectJ的注解实现AOP的功能,AspectJ本质是一个静态代理。SpringAOP属于运行时增强,而AspectJ是编译时增强,SpringAOP基于代理,而AspectJ基于字节码操作。