文章目录
- 基础环境
- AOP介绍
- AOP的七大术语
- 切点表达式
- Spring的AOP的使用
- 环境准备
- 基于AspectJ的AOP注解式开发
- 通知类型
- 前置通知@Before
- 后置通知@AfterReturning
- 环绕通知@Around
- 异常通知@AfterThrowing
- 最终通知@After
- 关于JoinPoint
- 切面的先后顺序
- 通用切点表达式
- 全注解式开发AOP
- 基于XML配置方式的AOP(了解)
上一篇:(十四)Spring之回顾代理模式
IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)
AOP是对OOP的补充延伸。
AOP底层使用的就是动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。
基础环境
spring6里程碑版本的仓库
依赖:spring context依赖、junit依赖、log4j2依赖
log4j2.xml文件放到类路径下。
AOP介绍
一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
- 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
使用AOP可以很轻松的解决以上问题。
总结AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
- 第一:代码复用性增强。
- 第二:代码易维护。
- 第三:使开发者更关注业务逻辑。
AOP的七大术语
-
1.连接点 Joinpoint:描述的是位置
在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。 -
2.切点 Pointcut:本质上就是方法
在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点) -
3.通知 Advice:本质上就是增强代码
通知又叫增强,就是具体你要织入的代码。
通知包括:- 前置通知:切点(方法)之前
- 后置通知:切点(方法)之后
- 环绕通知:前置后置都有通知
- 异常通知:catch里面
- 最终通知:finally里面
-
4.切面 Aspect
切点 + 通知就是切面。 -
5.织入 Weaving
把通知应用到目标对象上的过程。 -
6.代理对象 Proxy
一个目标对象被织入通知后产生的新对象。 -
7.目标对象 Target
被织入通知的对象。
代码体现:
public class UserService{
public void do1(){
System.out.println("do 1");
}
public void do2(){
System.out.println("do 2");
}
public void do3(){
System.out.println("do 3");
}
public void do4(){
System.out.println("do 4");
}
public void do5(){
System.out.println("do 5");
}
// 核心业务方法
public void service(){
try {
//连接点 Joinpoint
// 对于do1()来说 前置通知
do1();//切点 Pointcut (1)前置通知和后置通知都有叫环绕通知 (2)这一整个叫做切面(切点+通知)
// 对于do1()来说 后置通知
//连接点 Joinpoint
do2();//切点 Pointcut
//连接点 Joinpoint
do3();//切点 Pointcut
//连接点 Joinpoint
do5();//切点 Pointcut
//连接点 Joinpoint
}catch (Exception e){
//连接点 Joinpoint 异常通知
}finally {
//连接点 Joinpoint 最终通知
}
}
}
切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
- 可选项。
- 没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
返回值类型:
- 必填项。
- *表示返回值类型任意。
全限定类名:
- 可选项。
- 两个点“…”代表当前包以及子包下的所有类。
- 省略时表示所有的类。
方法名:
- 必填项。
- *表示所有方法。
- set*表示所有的set方法。
形式参数列表:
- 必填项
- () 表示没有参数的方法
- (…) 参数类型和个数随意的方法
- (*) 只有一个参数的方法
- (*, String) 第一个参数类型随意,第二个参数是String的。
异常:
- 可选项。
- 省略时表示任意异常类型。
例如:
表示service包下所有的类中以delete开始的所有方法
execution(public * com.mall.service.*.delete*(..))
所有类的所有方法
execution(* *(..))
mall包下所有的类的所有的方法
execution(* com.mall..*(..))
Spring的AOP的使用
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。(常用)
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。(不常用)
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。而实际开发基本使用注解方式,最重点的是注解方式的学习。
什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
环境准备
使用Spring+AspectJ的AOP除了还需要引入aop依赖和aspects依赖:
而如果使用Maven则只需引入context依赖就会自动关联spring-aop依赖,没有使用Maven则需要把aop的jar包引入项目。
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
Spring配置文件中添加context命名空间和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 http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
基于AspectJ的AOP注解式开发
首先需要创建spring.xml文件:
- 1.引入context命名空间和aop命名空间
- 2.添加组件扫描
- 3.开启自动代理
开启之后,Spring容器在扫描类的时候,会查看类上是否有@Aspect注解,如果有,则会自动给这个类生成代理对象。
使用aop:aspectj-autoproxy开启自动代理- proxy-target-class属性:默认为false
-
true:表示强制使用CGLIB动态代理
-
false:表示,接口使用JDK动态代理,类使用CGLIB动态代理
-
- proxy-target-class属性:默认为false
<?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.aop.annotation.service"/>
<!--
开启aspectj自动代理
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
创建目标类和目标方法,纳入spring管理:
@Service
public class UserService {//目标类
public void login(){//目标方法
System.out.println("正在登录。。。");
}
public void logout(){
System.out.println("正在退出。。。");
}
}
创建切面类:纳入spring管理,除此之外,需要加一个@Aspect注解,通知Spring框架这个类是一个切面类,到时候这个切面类会编写通知,添加切点表达式
@Component
@Aspect
public class LogAspect {//切面=切点+通知
}
通知类型
通知类型包括:
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
前置通知@Before
@Before注解代表该通知为前置通知,注解里面需要写切点表达式
在LogAspect 切面类添加前置通知:
//前置通知
@Before("execution(* com.aop.annotation.service..*(..))")
public void beforeAdvice(){
System.out.println("前置通知。。。增强代码。。。");
}
测试程序:
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
System.out.println("---------------------------------");
userService.logout();
}
后置通知@AfterReturning
@AfterReturning代表该通知为后置通知,注解里面需要写切点表达式
在LogAspect 切面类添加后置通知:
//后置通知
@AfterReturning("execution(* com.aop.annotation.service..*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知。。。增强代码。。。");
}
再次运行测试程序:
环绕通知@Around
@Around代表该通知为环绕通知,注解里面需要写切点表达式
环绕通知会收到一个ProceedingJoinPoint参数,用来执行目标方法。
在LogAspect 切面类添加环绕通知:
@Around("execution(* com.aop.annotation.service..*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前面代码
System.out.println("前环绕。。。");
//执行目标
joinPoint.proceed();
//后面代码
System.out.println("后环绕。。。");
}
再次运行测试程序:
可见环绕是最大的通知,在前置之前,在后置之后。
异常通知@AfterThrowing
@AfterThrowing代表该通知为异常通知,注解里面需要写切点表达式
为了更好演示,重新创建一个目标类和目标方法:该类抛一个异常
@Service
public class OrderService {
public void generate(){
System.out.println("正在生成订单。。。");
throw new RuntimeException("异常");
}
}
在LogAspect 切面类添加异常通知:
@AfterThrowing("execution(* com.aop.annotation.service..*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知。。。");
}
修改测试程序:
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
System.out.println("---------------------------------");
userService.logout();
System.out.println("---------------------------------");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
运行程序:发现发生异常后,后环绕和后置通知已经没了,这是因为执行方法的时候抛异常之后不会再往下执行通知
最终通知@After
@After代表该通知为最终通知,注解里面需要写切点表达式
在LogAspect 切面类添加最终通知:
@After("execution(* com.aop.annotation.service..*(..))")
public void afterAdvice(){
System.out.println("最终通知。。。");
}
再次运行测试程序:
可见最终通知一定会执行,不管有没有异常,最终通知,在后置之后,后环绕之前,由此可见环绕通知确实是最大的通知
关于JoinPoint
JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.。
JoinPoint是在Spring调用的切面方法时候自动传过来的,我们可以直接使用
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中, 添加了两个方法.
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
切面的先后顺序
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
在LogAspect切面类添加注解:
@Order(2)
把OrderService目标类的异常注释掉:
@Service
public class OrderService {
public void generate(){
System.out.println("正在生成订单。。。");
//throw new RuntimeException("异常");
}
}
再定义一个切面类:优先级设置为1,使用一下JoinPoint
/**
* 安全切面
*/
@Component
@Aspect
@Order(1)//数字越小优先级越高
public class SecurityAspect {
@Before("execution(* com.aop.annotation.service..*(..))")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("安全的前置通知。。。");
Signature signature = joinPoint.getSignature();//获取目标方法的签名
System.out.println(signature.getName());
}
}
测试程序:
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
如果把优先级换一下,再次运行:
通用切点表达式
回顾之前的切点表达式,发现切点表达式是重复的
这样的缺点是:
- 第一:切点表达式重复写了多次,没有得到复用。
- 第二:如果要修改切点表达式,需要修改多处,难维护。
怎么解决,可以使用通用切点表达式:@Pointcut注解将切点表达式单独的定义出来,在需要的位置引入即可。
在LogAspect 切面类添加一个方法使用@Pointcut注解:
方法只是个标记,方法名随意,方法体不需要写任何代码,只需要使用一个@Pointcut注解标注
//通用切点表达式
@Pointcut(value = "execution(* com.aop.annotation.service..*(..))")
public void execution_use(){
}
怎么引用,只需要在通知注解中写该方法即可,例如:
//@After("execution(* com.aop.annotation.service..*(..))")
@After("execution_use()")
这个通用表达式配置,只要一配置,任何切面类都可以使用,例如刚才的安全切面类,可以这样写。
/**
* 安全切面
*/
@Component
@Aspect
@Order(3)//数字越小优先级越高
public class SecurityAspect {
//@Before("execution(* com.aop.annotation.service..*(..))")
@Before("com.aop.annotation.service.LogAspect.execution_use()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("安全的前置通知。。。");
/*
这个JoinPoint是在Spring调用的这个方法时候自动传过来的
我们可以直接使用
*/
Signature signature = joinPoint.getSignature();//获取目标方法的签名
System.out.println(signature.getName());
}
}
再次运行test的测试程序:
全注解式开发AOP
就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:
@Configuration //代替Spring配置文件
@ComponentScan("com.aop.annotation.service")//代替注解扫描
@EnableAspectJAutoProxy(proxyTargetClass = true)//代替开启aspectj自动代理,
public class SpringConfig {
}
测试程序:
@Test
public void testNoXML(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
基于XML配置方式的AOP(了解)
第一步:编写目标类
/**
* 目标类
*/
public class UserService {
public void login(){
System.out.println("正在登录。。。");
}
}
第二步:编写切面类,并且编写通知
/**
* 切面
*/
public class TimerAspect {
//通知
public void arountAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
//前环绕
long begin = System.currentTimeMillis();
joinPoint.proceed();//执行目标
//后环绕
long end = System.currentTimeMillis();
System.out.println("耗时:"+ (end-begin));
}
}
第三步:编写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">
<!--配置bean-->
<bean id="userService" class="com.spring.aspectj.service.UserService"></bean>
<bean id="timerAspect" class="com.spring.aspectj.service.TimerAspect"></bean>
<!--aop配置-->
<aop:config>
<!--切点表达式-->
<aop:pointcut id="mypointcut" expression="execution(* com.spring.aspectj.service..*(..))"/>
<!--切面-->
<aop:aspect ref="timerAspect">
<!--哪个通知就配哪个-->
<aop:around method="arountAdvice" pointcut-ref="mypointcut"/>
</aop:aspect>
</aop:config>
</beans>
测试程序:
@Test
public void testXML(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}