1. AOP基本概念
- 面向切面编程
利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。AOP主要意图为将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
- 举例说明:
有三个功能模块,现在要对三个功能模块都加入日志记录的功能,原有方法是在每一个模块的代码中加入相应的日志记录功能代码
通过AOP,则可以在不修改原有功能代码的基础上,加入日志记录的功能
2. AOP底层原理
AOP底层使用动态代理
-
有两种情况动态代理
(1)有接口情况,使用JDK动态代理
创建接口实现类代理对象,增强类的方法
(2)无接口情况,使用CGLIB动态代理
创建子类代理对象,增强类的方法
3. JDK动态代理实现
使用JDK动态代理,需要使用Proxy类里面的方法创建代理对象
-
调用Proxy类中的newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 方法
ClassLoader loader:类加载器
Class<?>[] interface:增强方法所在的类,这个类实现的接口,支持多个接口,只要是实现了指定接口的类的方法,都可以被增强
InvocationHandler h:实现InvocationHandler接口,创建代理对象,编写增强的方法
-
JDK动态代理代码
(1)创建接口,定义方法
public interface UserDao { int add(int a, int b); String update(String id); }
(2)创建接口实现类,实现方法
public class UserDaoImpl implements UserDao { @Override public int add(int a, int b) { System.out.println("执行了add方法"); return a + b; } @Override public String update(String id) { System.out.println("执行了update方法"); return id; } }
(3)编写JDKProxy类,实现相应的增强逻辑
public class JDKProxy implements InvocationHandler { // 1. 传递要被代理的对象 通过有参构造传递 private Object object; public JDKProxy(Object object) { this.object = object; } // 增强的逻辑 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Exception { // 被增强的方法执行之前 System.out.println(method.getName() + "方法执行前"); System.out.println("方法执行前传递的参数" + Arrays.toString(args)); // 执行原本对象的方法 Object res = method.invoke(object, args); // 被增强的方法执行之后 System.out.println(method.getName() + "方法执行后"); return res; } }
(4)使用Proxy类创建接口的代理对象
public class Main { public static void main(String[] args) { // 创建要被代理的对象 UserDao userDao = new UserDaoImpl(); // 定义要被增强方法所属类实现的接口 Class[] interfaces = { UserDao.class}; // 创建代理对象 JDKProxy jdkProxy = new JDKProxy(userDao); // 获取接口实现类的代理对象 UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, jdkProxy); dao.add(1, 2); System.out.println("###############"); dao.update("aaa"); } }
4. CGLib动态代理实现
利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理
-
需要引入cglib的依赖
需要引入asm依赖,否则会抛出
Exception in thread "main" java.lang.NoClassDefFoundError: org/objectweb/asm/Type
异常还需要注意版本问题,否则会抛出
class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class
异常,经过测试,cglib 3.2.5和asm-6.0不会出现问题 -
CGLib动态代理代码
(1)创建一个类,不实现任何接口,类中定义一个普通方法,一个最终方法
public class UserDaoImpl { public void update(String name) { System.out.println("调用了update方法" + name); } final public void check() { System.out.println("调用了check方法"); } }
(2)编写代理类,实现相应增强方法
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; import java.util.Arrays; public class CGLibProxy implements MethodInterceptor { // 1. 传递要被代理的对象 通过有参构造传递 private Object object; public Object createCGLibProxyObject(Object object) { this.object = object; // 通过CGLIB动态代理获取代理对象的过程 Enhancer enhancer = new Enhancer(); // 设置enhancer对象的父类 enhancer.setSuperclass(object.getClass()); // 设置enhancer的回调对象 enhancer.setCallback(this); // 返回创建的代理对象 return enhancer.create(); } @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { // 被增强的方法执行之前 System.out.println(method.getName() + "方法执行前"); System.out.println("方法执行前传递的参数" + Arrays.toString(objects)); // 执行原本对象的方法 Object res = methodProxy.invoke(object, objects); System.out.println("方法执行后"); return res; } }
(3)通过代理对象执行相应方法
public class Main { public static void main(String[] args) { // 创建要被代理的对象 UserDaoImpl userDaoImpl = new UserDaoImpl(); // 创建代理对象 CGLibProxy proxy = new CGLibProxy(); // 获取类的代理对象 UserDaoImpl cgLibProxyObject = (UserDaoImpl) proxy.createCGLibProxyObject(userDaoImpl); // 通过代理对象调用目标方法 cgLibProxyObject.update("张三"); System.out.println("#############"); // 由于check方法被final修饰,因此无法被cglib代理 cgLibProxyObject.check(); } }
5. AOP相关术语
-
连接点
类里面哪些方法可以被增强,这些方法称为连接点
-
切入点
实际被真正增强的方法,称为切入点
-
通知(增强)
- 实际增强的逻辑部分称为通知(增强)
- 通知有多种类型
- 环绕通知
- 前置通知
- 方法返回通知
- 异常通知
- 后置通知(最终通知)
-
切面
把通知应用到切入点的过程称为切面
6. AOP操作简单说明
Spring框架一般基于AspectJ实现AOP操作
-
什么是AspectJ
AspectJ不是Spring组成部分,是一个独立的AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作
-
基于AspectJ实现AOP操作
(1)基于xml配置文件进行实现
(2)基于注解方式进行实现
-
在项目中引入AOP相关依赖
在已有依赖基础上引入以下相关依赖包
-
切入点表达式
(1)切入点表达式的作用:知道对哪个类里面的哪个方法进行增强
(2)语法结构:
execution( [权限修饰符] [返回值类型] [类的全路径] [方法名称] ([参数列表]) )
举例1:对com.study.spring5.demo15.dao.UserDao类的add方法进行增强
execution(* com.study.spring5.demo15.dao.UserDao.add(…))
* 表示所有权限
… 表示参数
举例2:对com.study.spring5.demo15.dao.UserDao类的所有方法进行增强
execution(* com.study.spring5.demo15.dao.UserDao.*(…))
举例3:对com.study.spring5.demo15.dao包中的所有类,类的所有方法进行增强
execution(* com.study.spring5.demo15.dao.*.*(…))
7. 基于xml配置文件方式的AspectJ操作
日常使用注解方式,xml配置文件了解即可
-
创建两个类,增强类与被增强类,编写相应方法
public class Car { public void buy() { System.out.println("buy()"); } }
public class CarProxy { public void before() { System.out.println("before()"); } }
-
在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" 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"> <!-- 创建car对象 --> <bean id="car" class="com.study.spring5.demo17.Car"></bean> <!-- 创建carProxy对象 --> <bean id="carProxy" class="com.study.spring5.demo17.CarProxy"></bean> </beans>
-
在spring配置文件中配置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/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"> <!-- 创建car对象 --> <bean id="car" class="com.study.spring5.demo17.Car"></bean> <!-- 创建carProxy对象 --> <bean id="carProxy" class="com.study.spring5.demo17.CarProxy"></bean> <!-- 配置AOP增强 --> <aop:config> <!-- 切入点 --> <aop:pointcut id="p" expression="execution(* com.study.spring5.demo17.Car.buy(..))"/> <!-- 配置切面 --> <aop:aspect ref="carProxy"> <!-- 配置增强作用的具体方法 把before作用到bug方法上 --> <!-- 即把通知应用到切入点的过程称为切面 --> <aop:before method="before" pointcut-ref="p"/> </aop:aspect> </aop:config> </beans>
-
编写主方法
public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean17.xml"); Car car = applicationContext.getBean("car", Car.class); car.buy(); } }
8. 基于注解方式的AspectJ操作
-
各类通知实现过程
(1)在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: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.study.spring5.demo16"/> <!-- 开启Aspect生成代理对象 --> <aop:aspectj-autoproxy/> </beans>
(2)使用注解创建User对象,在类中定义方法
import org.springframework.stereotype.Component; @Component public class User { public void add() { System.out.println("调用了User::add方法"); // System.out.println(1 / 0); } }
(3)使用注解创建增强类UserProxy,在作为通知方法上面添加通知类型的注解,并使用切入点表达式进行配置
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Component // 生成代理对象 @Aspect public class UserProxy { // @Before注解表示该方法作为前置通知 @Before(value = "execution(* com.study.spring5.demo16.User.add(..))") public void before() { System.out.println("before"); } // @After注解表示该方法作为后置通知(最终通知) // 在方法之后执行 @After(value = "execution(* com.study.spring5.demo16.User.add(..))") public void after() { System.out.println("after"); } // @AfterReturning注解表示该方法返回通知 // 在方法返回值之后执行 @AfterReturning(value = "execution(* com.study.spring5.demo16.User.add(..))") public void afterReturning() { System.out.println("after returning "); } // @Around注解表示该方法作为环绕通知 @Around(value = "execution(* com.study.spring5.demo16.User.add(..))") public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("around之前"); // 执行被增强的方法 proceedingJoinPoint.proceed(); System.out.println("around之后"); } // @AfterReturning注解表示该方法作为异常通知 // 在方法抛出异常之后执行 @AfterThrowing(value = "execution(* com.study.spring5.demo16.User.add(..))") public void afterThrowing() { System.out.println("after throwing "); } }
(4)主方法
public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean16.xml"); User user = applicationContext.getBean("user", User.class); user.add(); } }
不触发异常时候的通知结果
触发异常时候的通知结果
由此可以总结,当出现异常时,方法返回通知和环绕结束通知都不会执行,后置(最终)通知任何情况下都会被执行
try { // 执行前置通知; // 执行目标方法; // 执行返回通知; } catch (Exception e) { // 执行异常通知; } finally { // 执行后置通知; }
-
对相同切入点进行抽取(重用切入点)
可以使用@Pointcut注解对切入点进行抽取
修改UserProxy类中代码,加入
// 相同切入点抽取 // addPoint()方法等价于execution(* com.study.spring5.demo16.User.add(..)) @Pointcut(value = "execution(* com.study.spring5.demo16.User.add(..))") public void addPoint() { }
而后修改原本before方法的注解
// @Before注解表示该方法作为前置通知 // @Before(value = "execution(* com.study.spring5.demo16.User.add(..))") @Before(value = "addPoint()") public void before() { System.out.println("before"); }
由此就可以实现对相同切入点的抽取工作,方便后续维护工作
-
有多个增强类对同一个方法进行增强,可以设置增强类优先级
创建另一个代理对象类
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect public class UserLogProxy { @Before(value = "execution(* com.study.spring5.demo16.User.add(..))") public void before() { System.out.println("UserLogProxy::before"); } }
如果想要调整增强类的顺序,可以在使用@AspectJ注解的类上使用@Order(数字类型)注解,数字越小优先级越高
修改UserLogProxy类代码,在类上添加@Order()注解
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Aspect @Order(value = 2) public class UserLogProxy { ... }
修改UserProxy的代码,在类上添加@Order()注解
@Component // 生成代理对象 @Aspect @Order(1) public class UserProxy { ... }
通过修改@Order()的值,自行测试结果即可
9. 基于完全注解方式的AspectJ操作
-
User类
import org.springframework.stereotype.Component; @Component public class User { public void add() { System.out.println("调用了User::add方法"); } }
-
UserProxy类
import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Component @Aspect public class UserProxy { // @Before注解表示该方法作为前置通知 @Before(value = "addPoint()") public void before() { System.out.println("before"); } // 相同切入点抽取 @Pointcut(value = "execution(* com.study.spring5.demo18.User.add(..))") public void addPoint() { } }
-
配置类(替换掉第8章节中的xml配置文件)
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackages = { "com.study.spring5.demo18"}) // 相当于在xml配置文件中使用 <aop:aspectj-autoproxy/> 开启Aspect生成代理对象 @EnableAspectJAutoProxy public class ConfigAop { }
-
主方法
public class Main { public static void main(String[] args) { ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigAop.class); User user = applicationContext.getBean("user", User.class); user.add(); } }
-
其他说明
在声明@EnableAspectJAutoProxy注解时,可以指定参数proxyTargetClass = true,
如 @EnableAspectJAutoProxy(proxyTargetClass = true)
相当于xml配置文件的
<aop:aspectj-autoproxy proxy-target-class=“true”/>
这个值默认为false,proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。如果proxy-target-class 属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用
需要注意的是,即使你未声明 proxy-target-class=“true” ,但运行类没有继承接口,spring也会自动使用CGLIB代理。高版本spring自动根据运行类选择 JDK 或 CGLIB 代理