文章目录
一、AOP 概念
1.1 aop 思想
APO (面向切面编程) 是一种编程思想,它通过将通用的横向关注点(日志、事务、权限控制等)与业务逻辑分离,实现解耦,使得代码更易于维护。核心就是将非核心代码抽取出来,在实际运行时将代码切回业务代码。如何切回?使用cglib动态代理和jdk动态代理实现。
1.2 aop 应用场景
- 日志记录:在系统中记录日志是非常重要的,可以使用aop来实现记录日志的功能,可以在业务方法执行前、执行后或者抛出异常时记录日志
- 事务处理:在数据库操作中,使用事务可以保证数据的一致性,可以使用aop 来实现处理事务的功能,可以在方法开始前开启事务,在方法执行完毕后提交事务,或者在抛出异常时回滚事务
- 安全控制:在系统中包含某些需要安全控制的操作,如登录、授权等,可以使用aop 实现安全控制的功能 ,在方法执行前进行权限判断,如果用户没有权限,则抛出异常或者转到错误页面,防止未经授权的访问
- 异常处理: 系统中可能会出现各种异常,如空指针异常、数据库链接异常等,可以使用aop 来实现处理异常的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)
- 性能监控: 在系统中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈进行优化,可以使用aop 来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算执行时间并输出到日志中
- 缓存控制:在系统中有些数据需要缓存起来以提高访问速度,可以使用aop 来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有直接返回,否则执行方法并将返回值存入缓存中
二、aop 如何使用
2.1 八个核心名词
-
横切关注点
- 从每个方法中抽取出来的同一类非核心业务(如日志是一个业务,事务是一个业务)。在同一个项目,可以使用多个横向关注点,对同一个方法进行不同方面(日志、事务等)的的增强
- AOP 把软件系统分为两个部分,核心关注点和横切关注点,核心业务是核心关注点,与之关系不大的部分是横切关注点。
- 横切关注点的特点是,它们经常发生在核心关注点的多处,而且各处基本相似,比如权限认证、日志、事务、异常等
-
通知(也叫增强)
- 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法叫做通知方法 或 增强方法
- 前置通知:在被代理的目标的方法前执行
- 返回通知: 在被代理的目标方法成功后执行,可以修改返回值
- 异常通知: 在被代理的目标方法异常结束后执行
- 后置通知:在被代理的目标方法最终结束后执行,不可以修改返回值
- 环绕通知: 使用 try…catch…finally 结构围绕整个被代理的目标方法,包括上面四种通知的所有位置
-
连接点 (joinpoint)
这是一个纯逻辑概念,指哪些可能被拦截到的点,也就被 通知增强 的各个点 -
切入点 (pointcut)
定位连接点的方式,可以理解为被选中的连接点。是一个表达式,比如execution(* com.spring.service.impl…(…)), 指定好了切入点,框架才知道在哪里进行增强 -
切面(aspect)
切入点和增强的结合,是一个类。在各个切入点进行增强,好比切西瓜,形成一个切面 -
目标 target
被代理的目标对象 -
代理 proxy
为了对目标对象应用通知而创建的对象,好比中介,目标是房东 -
织入
是指把通知应用到目标上,生成代理对象的过程,可以在编译时织入,也可以在运行时织入,spring 采用后者,也就是动态代理
2.2 代码实现
- 加入依赖
spring-context 已经集成了aop ,aop 只为 ioc 容器中的对象创建代理对象
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.6</version>
</dependency>
-
步骤
- 编写业务类
- 定义增强类,在类中编写增强方法,获取核心业务方法信息(方法名,参数,访问修饰符,所属的类信息等),编写非核心业务逻辑,如日志输出等
- 在增强方法加注解,指定切入点(通过切点表达式指明)以及增强的时机(前置,后置等),这样就知道在哪个类的哪个方法上进行怎么样的增强了
- 在增强类上加**@Component注解**,注入ioc容器,加**@Aspect注解**,指定为切面
- 编写配置类,加@Configuration注解,指定为配置类,指定扫描包(加@ComponentScan注解),增强类也必须注入ioc 容器,开启aspectj aop支持(加@EnableAspectJAutoProxy注解)
- 在junit 测试类上加上@SpringJUnitConfig(value = JavaConfig.class),指定配置类,那么spring 将自动为我们创建ioc 容器,而不用我们手动 new ClassPathXmlApplication
-
补充
- 配置类相当于一个.xml文件,含有扫描包的路径。
- @SpringJUnitConfig(value = JavaConfig.class) 相当于 new ClassPathXmlApplication(JavaConfig.class)
- @SpringJUnitConfig(locations = {“xxx.xml”}) 相当于 new ClassPathXmlApplication(“xxx.xml”)
- @EnableAspectJAutoProxy 相当于在xml 中配置aop 标签,@ComponentScan注解 相当于 component-sacn标签
<?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"
xmlns:context="http://www.springframework.org/schema/context"
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">
<context:component-scan base-package="com.binbin.service"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 编写业务类
接口
public interface Calculator {
public double div(int i, int j);
}
实现类
@Component
public class CalculatorImpl implements Calculator {
@Override
public Double div(int i, int j){
try {
System.out.println("业务方法--正在计算除法");
double r = i / j;
return r;
}catch (Exception e){
System.out.println("业务方法--出现异常");
throw e; // 抛异常
}finally {
// 回收资源
System.out.println("业务方法----finally!");
}
}
}
- 编写增强类,也就是切面
@Component
@Aspect
public class LogAdvice {
@Before(value = "execution(* com.binbin.service.impl.*.*(..))")
public void atBefore(JoinPoint joinPoint){
// 获取方法名
String methodName = joinPoint.getSignature().getName();
System.out.println("Method Name: " + methodName);
System.out.println("Before-前置增强!");
}
@After(value = "execution(* com.binbin.service.impl.*.*(..))")
public void atAfter(JoinPoint joinPoint){
System.out.println("After-后置增强!");
}
@AfterReturning(value = "execution(* com.binbin.service.impl.*.*(..))",returning="res")
public void atAfterReturning(JoinPoint joinPoint,Object res){
System.out.println("AfterReturning -后置返回增强! 返回结果 res = " + res.toString());
}
@AfterThrowing(value = "execution(* com.binbin.service.impl.*.*(..))",throwing = "e")
public void error(Exception e){
System.out.println("AfterThrowing-异常处理增强!" + e);
}
}
- 编写配置类
@Configuration
@ComponentScan(basePackages = {"com.binbin.service","com.binbin.advice"})
@EnableAspectJAutoProxy // 开启aop 功能
public class JavaConfig {
}
- 编写测试类
@SpringJUnitConfig(value = JavaConfig.class) // 由spring 创建ioc 容器,相当于 new ClassPathXmlApplication("xxx.xml") 或者 new ClassPathXmlApplication(xxx.class)
public class SpringTest {
@Autowired
Calculator calculator;
@Test
public void test() {
try {
Double r = calculator.div(1, 1);
}catch (Exception e){
System.out.println();
}
}
}
调用calculator.div(1, 1):
依次触发 Before、AfterReturning、After,无异常
Method Name: div
Before-前置增强!
业务方法--正在计算除法
业务方法----finally!
AfterReturning -后置返回增强! 返回结果 res = 1.0
After-后置增强
调用calculator.div(1, 0):
依次触发 Before、AfterThrowing、After有异常
Method Name: div
Before-前置增强!
业务方法--正在计算除法
业务方法--出现异常
业务方法----finally!
AfterThrowing-异常处理增强!java.lang.ArithmeticException: / by zero
After-后置增强!
- 小结
执行顺序:前置增强、业务核心、返回值后置或异常增强、后置增强
异常还是会传播到方法调用处