一、SpringAOP的初识与原理
1、概述
- AOP:面向切面编程
- OOP:面向对象编程
- 面相切面编程:是基于OOP基础之上的新编程思想,OOP面向的主要是对象是类,而AOP面向的主要对象是切面,它在处理日志、安全管理、事务管理、权限控制等方面具有非常重要的作用。是一种
非侵入式
(不改变原来的代码)的代码增强
的开发工具,可以减
少重复代码的编写,降低模块间的耦合度,并有利于提高程序的可拓展性
和可维护性
。 - AOP是Spring中的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC进行补充。
- Spring AOP 是基于动态代理实现的,如果被代理的对象已经实现了某个接口,则可以使用JDK动态代理(核心是invocationHandler接口和Proxy类)的方式来创建代理对象;如果被代理对象没有实现某个接口,可以使用Cglib(核心是MethodInterceptor接口和Enhancer类),基于继承的方式,生成一个被代理对象的子类作为代理;
- 简而言之,在程序运行期间,在不修改原有代码的情况下,增强跟主业务没有关系的公共功能代码到之前写好的方法中的指定位置, 这种编程的方式叫AOP;
2、代理模式
- AOP的底层是通过代理模式来实现。
- 代理模式是一种设计模式,其主要目的是为其他对象提供一种代理,以控制对该对象的访问。这意味着用户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
- 代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。
- 总的来说,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能。
- 代理模型分为静态代理和动态代理(
JDK动态代理
、CGLB动态代理
)两种。
2.1 静态代理
- 静态代理就是需要手动去为每一个被代理对象去创建一个代理类。
- 例如,有一个游戏代练的示例:
/** * 游戏接口(公共接口) */ public interface IGamePlayer { //登录游戏 void start(); //玩游戏 void play(); }
/** * 菜鸟游戏玩家(目标对象-被代理对象) */ public class GamePlayer implements IGamePlayer { //玩家名称 private String name; //构造赋值 public GamePlayer(String name){ this.name = name; } @Override public void start() { System.out.println("正在登录游戏........"); System.out.println(name + ":开始了游戏"); } @Override public void play() { System.out.println(name + "技术太low,游戏失败!"); } }
@Test public void testProxy(){ GamePlayer player = new GamePlayer("菜鸟"); player.start(); player.play(); }
- 现在,因为菜鸟玩家的技术能力太低,玩游戏把把都输,他需要一个代练帮他玩游戏提升等级,我们可以帮他创建一个代理对象帮他代玩。
/** * 代练游戏玩家(静态代理类) */ public class ProxyGamePlayer implements IGamePlayer{ //代练 private String proxyName; //菜鸟玩家 private GamePlayer gamePlayer; public ProxyGamePlayer(String name){ this.proxyName = name; this.gamePlayer = new GamePlayer(name); } @Override public void start() { //代练以菜鸟的身份去玩游戏 System.out.println("拿到"+proxyName+"的游戏账号密码"); gamePlayer.start(); } @Override public void play() { //代练帮菜鸟玩游戏 System.out.println("代练技术太强,赢得了游戏!"); } }
@Test public void testProxy(){ IGamePlayer gamePlayer = new ProxyGamePlayer("菜鸟"); gamePlayer.start(); gamePlayer.play(); }
- 静态代理的
弊端
:- 需要为每一个被代理的类创建一个代理类,虽然这种方式可以实现,但是成本太高。
2.2 动态代理
2.2.1 JDK动态代理
-
核心关键
- 一个类:Proxy
- 概述:Proxy是所有代理类的基类;
- 作用:创建代理对象,newProxyInstance();
- 一个接口:InvocationHandler
- 概述:实现动态织入效果的关键接口;
- 作用:通过反射机制,执行invoke()方法来实现动态织入的效果;
- 一个类:Proxy
-
还是以游戏代练为例,实现Jdk动态代理:
- 创建一个为
被代理对象(目标对象)
创建代理类的工具类
public class GamePlayerProxy { /** * 被代理对象(目标对象) */ private Object targetObj; /** * 有参构造,避免目标对象为null * @param targetObj */ public GamePlayerProxy(Object targetObj){ this.targetObj = targetObj; } /** * 获取代理类对象 */ public Object getProxyObject(){ Object proxyObject = null; /* * ClassLoader loader 类加载器,通常指被代理类的接口的类加载器 * Class<?> [] interfaces 类型,通常指被代理类的接口的类型 * InvacationHandler handler 委托执行的代理类,具体功能增强的逻辑在这里实现 */ ClassLoader loader = targetObj.getClass().getClassLoader(); Class<?>[] interfaces = targetObj.getClass().getInterfaces(); proxyObject = Proxy.newProxyInstance(loader, interfaces, new InvocationHandler() { //重写invoke()实现动态织入效果 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object invoke = ""; if (method.getName().equals("play")){ //逻辑替换 System.out.println("代练技术太强,已成功帮菜鸟赢得了游戏!"); }else { //逻辑增强 //执行被代理的方法 /* * Object targetObj 被代理的对象 * Object... args 被代理的方法的参数 */ invoke = method.invoke(targetObj, args); } return invoke; } }); //返回创建好的代理类对象 return proxyObject; }
- 使用代理类对象来代玩游戏:
@Test public void testJdkProxy(){ //被代理(目标)对象 IGamePlayer gamePlayer = new GamePlayer("菜鸟"); //获取代理对象(代理对象不能转换为目标对象) GamePlayerProxy gamePlayerProxy = new GamePlayerProxy(gamePlayer); IGamePlayer player = (IGamePlayer) gamePlayerProxy.getProxyObject(); //代理玩游戏 player.start(); player.play(); }
- 创建一个为
2.2.2 CGLB动态代理
- 后期补充…
二、SpringAOP的应用
- 通过上述的例子,已经实现了代打游戏的目的,但是这种动态代理的实现方式调用的是JDK的基本实现,如果需要被代理的目标对象没有实现任何接口,那么是无法为它创建代理对象的,这也是致命的缺陷。而在Spring中我们可以不编写上述如此复杂的代码,只需要利用AOP,就能够轻轻松松实现上述功能,当然,Spring AOP的底层实现也依赖的是动态代理。
1、AOP的核心概念及术语
切面
(Aspect): 指关注点模块化,这个关注点可能会横切多个对象。连接点
(Join point): 在程序执行过程中某个特定的点。在Spring AOP中,一个连接点总
是代表一个方法的执行。通知
(Advice): 在切面的某个特定的连接点上执行的动作。切点
(Pointcut): 匹配连接点的断言。引入
(Introduction): 声明额外的方法或者某个类型的字段。目标对象
(Target object): 被一个或者多个切面所通知的对象。织入
(Weaving): 把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象的过程。这个过程可以在编译时、类加载时、运行时完成。
2、AOP的通知类型
前置通知
- 语法:@Before(value=“execution()”)
- 执行时机:在切入点表达式中指定的方法执行之前执行(无论是否发生异常,都会执行);
后置通知
- 语法:@After(value=“execution()”)
- 执行时机:在切入点表达式中指定的方法执行之后执行(无论是否发生异常,都会执行);
返回通知
- 语法:@AfterReturning(pointcut = “execution()”,returning = “result”)
- 执行时机:在切入点表达式中指定的方法返回结果时执行(如果发生异常,则不执行);
异常通知
- 语法:@AfterThrowing(pointcut = “execution()”,throwing = “e”)
- 执行时机:在切入点表达式中指定的方法发生异常时执行;
环绕通知
- 语法:@Around(value = “pointCut()”)
- 作用:环绕通知可以对前置通知、后置通知、返回通知、异常通知进行整合使用;
3、AOP的应用场景
- 日志管理
- 权限认证
- 安全检查
- 事务控制
4、AOP的相关配置
4.1 添加pom依赖
<!--Aop的AspectJ框架的jar包 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.28</version>
</dependency>
4.2 将目标类加入IOC容器
/**
* 计算器接口(公共接口)
*/
public interface CalculatorService {
int add(int a,int b);
int sub(int a,int b);
int mul(int a,int b);
int div(int a,int b);
}
/**
*计算器实现类对象(目标对象)
*/
@Service
public class CalculatorServiceImpl implements CalculatorService {
@Override
public int add(int a, int b) {
System.out.println("正在执行加法......");
return a+b;
}
@Override
public int sub(int a, int b) {
System.out.println("正在执行减法......");
return a-b;
}
@Override
public int mul(int a, int b) {
System.out.println("正在执行乘法......");
return a*b;
}
@Override
public int div(int a, int b) {
System.out.println("正在执行除法......");
return a/b;
}
}
4.3 声明切面类并加入IOC容器
@Component //标识该类是一个组件类(保证这个切面在IOC容器中)
@Aspect //标识该类是一个切面
public class LogUtil {
//可以采用引用切点的方式,让其他通知共同使用
@Pointcut(value = "execution(* org.example.mvc.impl.*.*(..))")
public void pointCut(){}
//前置通知
@Before(value = "execution(public int org.example.mvc..CalculatorServiceImpl.*(int, int))")
public void beforeMethod(JoinPoint joinPoint){
//获取方法名称
String methodName = joinPoint.getSignature().getName();
//获取参数
Object[] args = joinPoint.getArgs();
System.out.println("【前置通知】计算器 "+methodName+" 方法,执行参数为:"+ Arrays.toString(args));
}
//后置通知
// @After("pointCut() && @annotation(logger)")
@After(value = "pointCut()")
public void afterMethod(JoinPoint joinPoint){
//获取方法名称
String methodName = joinPoint.getSignature().getName();
System.out.println("【后置通知】计算器 "+methodName+" 方法,执行完毕");
}
//后置返回通知
@AfterReturning(pointcut = "pointCut()",returning = "result")
//注意:returning属性中的返回结果名称要与入参中的参数名一致
public void afterReturn(JoinPoint joinPoint,Object result){
//获取方法名称
String methodName = joinPoint.getSignature().getName();
System.out.println("【后置返回通知】计算器 "+methodName+" 方法,执行结果为:"+ result);
}
//后置异常通知
@AfterThrowing(pointcut = "pointCut()",throwing = "ex")
//注意:throwing属性中的异常名称要与入参中的异常参数名一致
public void afterThrowing(JoinPoint joinPoint,Exception ex){
//获取方法名称
String methodName = joinPoint.getSignature().getName();
StringWriter stringWriter = new StringWriter();
ex.printStackTrace(new PrintWriter(stringWriter,true));
System.out.println("【后置异常通知】计算器 "+methodName+" 方法,执行时出现异常:" + stringWriter.getBuffer().toString());
}
//环绕通知
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Object result = null;
try {
//前置通知
System.out.println("【环绕-前置通知】计算器 "+methodName+" 方法,执行参数为:"+ Arrays.toString(args));
//触发目标对象中的目标方法
result = joinPoint.proceed();
//返回通知
System.out.println("【环绕-返回通知】计算器 "+methodName+" 方法,执行结果为:"+ result);
} catch (Throwable throwable) {
throwable.printStackTrace();
//异常通知
System.out.println("【环绕-异常通知】计算器 "+methodName+" 方法,执行时出现异常:" + throwable.getMessage());
} finally {
//后置通知
System.out.println("【环绕-后置通知】计算器 "+methodName+" 方法,执行完毕");
}
return result;
}
}
-
切点表达式语法:
execution(访问修饰符 方法返回值类型 包名.类名.方法名(参数...))
- (1)访问修饰符:可写可不写;
- (2)方法返回值类型:如果是JDK自带的类型,则直接可以用类型名,例如(Int)。如果不是,则需要写上自定义类型的全局限定名,如果返回值的类型不同,则可以用通用符
*
代表任何类型; - (3)包名:可以写具体的包名,也可以用通用符
*
占位代表任何包名,..
代表子孙包。例如:cn.、cn.trs.、cn.trs.service…* - (4)类的全局限定名:可以写具体的类名,也可以使用通配符
*
代表任何类的名字; - (5)方法名:可以写具体的方法名,也可以使用通配符
*
代表任何方法的名字,也可以模糊匹配*Add
=> userAdd()、roleAdd() - (6)参数:如果是JDK自带类型,可以不用写类型的全局限定名,否则,需要写上自定义类型的全局限定名。如果需要匹配任意参数可以写:
..
代替
-
合并切点表达式:
- 可以使用
&&
、||
、!
等符号进行合并操作,也可以通过名字来指向切点表达式。
//&&:两个表达式同时 execution( public int cn.trs.inter.MyCalculator.*(..)) && execution(* *.*(int,int) ) //||:任意满足一个表达式即可 execution( public int cn.trs.inter.MyCalculator.*(..)) && execution(* *.*(int,int) ) //!:只要不是这个位置都可以进行切入 //&&:两个表达式同时 execution( public int cn.trs.inter.MyCalculator.*(..))
- 可以使用
4.4 开启组件扫描和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: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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="org.example"></context:component-scan>
<!--开启AspectJ的注解对AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 在Spring容器中,如果有接口,那么会使用jdk自带的动态代理,如果没有接口,那么会使用cglib的动态代理。