开始时间:2022-01-09
课程链接:动力节点Spring框架教程
AOP 面向切面编程
AOP(Aspect Orient Programming),面向切面编程。 面向切面编程是从动态角度考虑程序运行过程。
AOP 底层,就是采用动态代理模式实现的。采用了两种代理: JDK 的动态代理,与 CGLIB的动态代理。
面向切面编程,就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到主业务逻辑中。所谓交叉业务逻辑是指,通用的、==与主业务逻辑无关的代码,==如安全检查、事务、日志、缓存等。(做机械的厂、做互联网的厂、做房地产的厂,都会有财务部门,后勤部门,这些和主业务逻辑无关)
面向切面编程的好处
1.减少重复;
2.专注业务;
注意:面向切面编程只是面向对象编程的一种补充。
术语
-
Aspect:切面,表示增强的功能, 就是一堆代码,完成某个一个功能。非业务功能,
常见的切面功能有日志, 事务, 统计信息, 参数检查, 权限验证。 -
JoinPoint:连接点 ,连接业务方法和切面的位置。 就某类中的业务方法(例如我们判断是否是我们需要执行的方法的if语句,就是一个连接点)
-
Pointcut : 切入点 ,指多个连接点方法的集合。多个方法
-
目标对象: 给哪个类的方法增加功能, 这个类就是目标对象
-
Advice:通知,通知表示切面功能执行的时间。
切面三要素
1)切面的功能代码,切面干什么 (增加什么功能)
2)切面的执行位置,使用Pointcut表示切面执行的位置(在哪些业务中增加功能)
3)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后。
AOP的实现
aspectJ:
一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能。
aspectJ框架实现aop有两种方式:
1.使用xml的配置文件 : 配置全局事务
2.使用注解,我们在项目中要做aop功能,一般都使用注解, aspectj有5个注解。
AspectJ 中常用的通知有五种类型:
(1)前置通知
(2)后置通知
(3)环绕通知
(4)异常通知
(5)最终通知
- 切面的执行时间, 这个执行时间在规范中叫做Advice(通知,增强):五种
1)@Before
2)@AfterReturning
3)@Around
4)@AfterThrowing
5)@After
在aspectj框架中使用注解表示的。也可以使用xml配置文件中的标签 - 切面的执行位置,
AspectJ 定义了专门的表达式用于指定切入点。表达式的原型是:
execution(modifiers-pattern? ret-type-pattern
declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
modifiers-pattern 访问权限类型
ret-type-pattern 返回值类型
declaring-type-pattern 包名类名
name-pattern(param-pattern) 方法名(参数类型和参数个数)
throws-pattern 抛出异常类型
带?表示可选的部分
符号 | 意义 | 备注 |
---|---|---|
* | 0至多个任意字符 | 通配符,和正则表达式差不多 |
·· | 用在方法参数中,表示任意多个参数,用在包名后,表示当前包及其子路径 | 像是文件夹的表示方法 |
+ | 用在类名后,表示当前类及其子类;用在接口后,表示当前接口及其实现类 |
看几个切入点表达式的例子
举例:
execution(public * *(..))
两个空格,说明有三个参数,对应位置找一下
指定切入点为:任意公共方法。
execution(* set*(..))
一个空格,两个参数
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service.*.*(..))
一个空格,两个参数
指定切入点为:定义在 service 包里的任意类的任意方法。
在Maven中使用AspectJ
ch06-aop-aspectj: 使用aspectj框架实现aop。
使用aop:目的是给已经存在的一些类和方法,增加额外的功能。 前提是不改变原来的类的代码。
使用aspectj实现aop的基本步骤:
1.新建maven项目
2.加入依赖
1)spring依赖
2)aspectj依赖
3)junit单元测试
3.创建目标类:接口和他的实现类。
要做的是给类中的方法增加功能
4.创建切面类:普通类
1)在类的上面加入 @Aspect
2)在类中定义方法, 方法就是切面要执行的功能代码
在方法的上面加入aspectj中的通知注解,例如@Before
有需要指定切入点表达式execution()
5.创建spring的配置文件:声明对象,把对象交给容器统一管理
声明对象你可以使用注解或者xml配置文件<bean>
1)声明目标对象
2)声明切面类对象
3)声明aspectj框架中的自动代理生成器标签。
自动代理生成器:用来完成代理对象的自动创建功能的。
6.创建测试类,从spring容器中获取目标对象(实际就是代理对象)。
通过代理执行方法,实现aop的功能增强。
2.添加依赖
<!--spring和aspectJ的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.5.RELEASE</version>
</dependency>
3.创建目标类和接口类
创建接口 (在idea中,在接口后面的接口名上 按 enter+alt 可以直接选 自动创建该接口的实现类)
package BUPT.ba01;
public interface SomeService {
void doSome(String name,Integer age);
void doOther();
}
实现类
package BUPT.ba01;
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name,Integer age) {
System.out.println("目标方法doSome的name:" + name + "以及age:" + age);
}
@Override
public void doOther() {
}
}
4.创建切面类
package BUPT.ba01;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import java.util.Date;
/*
表示当前类是切面类
*/
@Aspect
public class MyAspect {
/**
* 定义方法,方法是实现切面功能的。
* 方法的定义要求:
* 1.公共方法 public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法可以有参数,也可以没有参数。
*/
//给切面表达式
@Before(value = "execution(public void BUPT.ba01.SomeServiceImpl.doSome(String,Integer))")
public void myBefore() {
//就是你切面要执行的功能代码
System.out.println("前置通知, 切面功能:在目标方法之前输出执行时间:" + new Date());
}
}
5.创建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: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/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--把对象交给spring容器,由spring容器统一创建,管理对象-->
<!--声明目标对象-->
<bean id="someService" class="BUPT.ba01.SomeServiceImpl" />
<!--声明切面类对象-->
<bean id="myAspect" class="BUPT.ba01.MyAspect" />
<!--声明自动代理生成器:使用aspectj框架内部的功能,创建目标对象的代理对象。
创建代理对象是在内存中实现的, 修改目标对象的内存中的结构。 创建为代理对象
所以目标对象就是被修改后的代理对象.
aspectj-autoproxy:会把spring容器中的所有的目标对象,一次性都生成代理对象。
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
== 6 测试类==
public class AppTest {
@Test
public void test01() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeService proxy = (SomeService) AC.getBean("someService");
//通过代理的对象执行方法
proxy.doSome("JDH", 23);
}
}
输出就可以了
前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 11:29:07 CST 2022
目标方法doSome的name:JDH以及age:23
注解+切入表达式的结合才能实现相互的耦合和精准定位
我们再来看看JoinPoint的使用
在MyAspect里面添加
这里的切入表达式把两个方法都弄进来了,doSome和doOther
@Before(value = "execution(void *..SomeServiceImpl.do*(..))")
public void myBefore(JoinPoint jp){
//获取方法的完整定义
System.out.println("方法的签名(定义)="+jp.getSignature());
System.out.println("方法的名称="+jp.getSignature().getName());
//获取方法的实参
Object args [] = jp.getArgs();
for (Object arg:args){
System.out.println("参数="+arg);
}
//就是你切面要执行的功能代码
System.out.println("2=====前置通知, 切面功能:在目标方法之前输出执行时间:"+ new Date());
}
此时的测试类输出
public void test01() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeService proxy = (SomeService) AC.getBean("someService");
//通过代理的对象执行方法
System.out.println("======执行doSome之前的输出======");
proxy.doSome("JDH", 23);
System.out.println();
System.out.println("==============doSome和doOther的分割线===============");
System.out.println();
proxy.doOther();
}
方法的签名(定义)=void BUPT.ba01.SomeServiceImpl.doSome(String,Integer)
方法的名称=doSome
参数=JDH
参数=23
2=====前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 16:18:27 CST 2022
目标方法SomeServiceImpl的doSome的name:JDH以及age:23
==============doSome和doOther的分割线===============
方法的签名(定义)=void BUPT.ba01.SomeServiceImpl.doOther()
方法的名称=doOther
2=====前置通知, 切面功能:在目标方法之前输出执行时间:Sun Jan 09 16:18:27 CST 2022
目标方法是SomeServiceImpl的doOther
也就是说,通过JoinPoint我们能够拿到所执行方法的相关参数,这类似于反射的功能
后置通知@AfterReturning
看一个例子
* 定义方法,方法是实现切面功能的。
* 方法的定义要求:
* 1.公共方法 public
* 2.方法没有返回值
* 3.方法名称自定义
* 4.方法必须有参数
*/
//给切面表达式
//returning用来表示目标方法的返回值,返回值和形参的返回值要一致
定义切面类
package BUPT.ba02;
import ...;
@Aspect
public class MyAspect {
//给切面表达式
//returning用来表示目标方法的返回值,返回值和形参的返回值要一致
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturing(JoinPoint jp, Object res) {
// Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理
System.out.println("后置通知:方法的定义" + jp.getSignature());
System.out.println("后置通知:在目标方法之后执行的,获取的返回值是:" + res);
if (res.equals("BUPT")) {
//做一些功能
System.out.println("确认输出了BUPT,这里的位置可以用来实现增强功能");
} else {
System.out.println("没有输出BUPT,这里的位置可以用来实现增强其他功能");
}
}
}
我们可以通过在MyAspect类里面 修改res,然后返回,这样就能改变输出了,但是这里改了也不会变
因为这里返回的是String类,String类的传递方式比较特殊,有一个字符池的因素,具体可以再百度。
下面的环绕通知能够修改,是因为有一个return的过程,这里是没有的。
如果是其他引用类型,进行引用传递,地址不变,但值被修改了,那么返回值也会随之变化的。
定义接口实现类
package BUPT.ba02;
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
System.out.println("目标方法SomeServiceImpl的doSome的name:" + name + "以及age:" + age);
}
@Override
public String doOther(String name, Integer age) {
System.out.println("目标方法是SomeServiceImpl的doOther");
return name;
}
}
定义测试类
package BUPT;
import ...;
public class MyTest02 {
@Test
public void MyTest() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeService proxy = (SomeService) AC.getBean("someService");
//通过代理的对象执行方法
String str = proxy.doOther("bupt", 25);
System.out.println("在Test里面获取了返回值str:"+str);
}
}
目标方法是SomeServiceImpl的doOther
后置通知:方法的定义String BUPT.ba02.SomeServiceImpl.doOther(String,Integer)
后置通知:在目标方法之后执行的,获取的返回值是:bupt
没有输出BUPT,这里的位置可以用来实现增强其他功能
在Test里面获取了返回值str:bupt
环绕通知@Around
MyAspect类
定义方法,方法是实现切面功能的。
方法的定义要求:
1.公共方法 public
2.方法必须有返回值,推荐Object
3.方法名称自定义
4.方法必须有参数 固定参数
@Around: 环绕通知 属性:value 切入点表达式 位置:在方法的定义什么 特点:
1.它是功能最强的通知
2.在目标方法的前和后都能增强功能。
3.控制目标方法是否被调用执行
4.修改原来的目标方法的执行结果。 影响最后的调用结果
package BUPT.ba03;
import ...;
@Aspect
public class MyAspect {
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
String name = "";
//获取第一个参数值
Object args[] = pjp.getArgs();
if (args != null && args.length > 1) {
Object arg = args[0];
name = (String) arg;
}
//实现环绕通知
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间:" + new Date());
//1.目标方法调用
if ("BUPT".equals(name)) {
//符合条件,调用目标方法
result = pjp.proceed(); //method.invoke(); Object result = doFirst();
}
System.out.println("环绕通知:在目标方法之后,提交事务");
//2.在目标方法后加入功能
//修改目标方法的执行结果, 影响方法最后的调用结果
if (result != null) {
result = "Hello AspectJ AOP";
}
//返回目标方法的执行结果
return result;
}
}
实现类添加一个接口的实现方式
@Override
public String doFirst(String name, Integer age) {
System.out.println("====业务方法doFirst()====");
return "doFirst";
}
测试类
package BUPT;
import ...;
public class MyTest03 {
@Test
public void myTest() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeService proxy = (SomeService) AC.getBean("someService");
//通过代理的对象执行方法
String str = proxy.doFirst("BUPT", 25);
System.out.println("在Test里面获取了返回值str:"+str);
}
}
输出结果
环绕通知:在目标方法之前,输出时间:Sun Jan 09 17:16:34 CST 2022
====业务方法doFirst()====
环绕通知:在目标方法之后,提交事务
在Test里面获取了返回值str:Hello AspectJ AOP
这里能修改String,是因为多了一个return的过程
PointCut注解
/**
* @Pointcut: 定义和管理切入点, 如果你的项目中有多个切入点表达式是重复的,可以复用的。
* 可以使用@Pointcut
* 属性:value 切入点表达式
* 位置:在自定义的方法上面
* 特点:
* 当使用@Pointcut定义在一个方法的上面 ,此时这个方法的名称就是切入点表达式的别名。
* 其它的通知中,value属性就可以使用这个方法名称,代替切入点表达式了
*/
MyAspect类
package BUPT.ba04;
import ..
/**
* @Aspect : 是aspectj框架中的注解。
* 作用:表示当前类是切面类。
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 位置:在类定义的上面
*/
@Aspect
public class MyAspect {
@After(value = "mypt()")
public void myAfter(){
System.out.println("执行最终通知,总是会被执行的代码");
//一般做资源清除工作的。
}
@Before(value = "mypt()")
public void myBefore(){
System.out.println("前置通知,在目标方法之前先执行的");
}
@Pointcut(value = "execution(* *..SomeServiceImpl.doSome(..))" )
private void mypt(){
//无需代码,
}
}
测试方法
package BUPT;
import BUPT.ba04.SomeService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest03 {
@Test
public void myTest() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeService proxy = (SomeService) AC.getBean("someService");
//通过代理的对象执行方法
proxy.doSome("BUPT", 25);
}
}
输出
前置通知,在目标方法之前先执行的
目标方法SomeServiceImpl的doSome的name:BUPT以及age:25
执行最终通知,总是会被执行的代码
cglib动态代理
jdk动态代理,必须有接口,目标类必须实现接口, 没有接口时,需要使用cglib动态代理
把接口类去掉
重写Test方法
注意proxy对应的是实现类而不是接口了
package BUPT;
import ...;
public class MyTest03 {
@Test
public void myTest() {
String config = "applicationContext.xml";
ApplicationContext AC = new ClassPathXmlApplicationContext(config);
//从目标容器中获取目标对象
SomeServiceImpl proxy = (SomeServiceImpl) AC.getBean("someService");
//目标类没有接口,使用cglib动态代理,
//通过代理的对象执行方法
//得到BUPT.ba05.SomeServiceImpl$$EnhancerBySpringCGLIB$$fccf21be
System.out.println(proxy.getClass().getName());
//如果是proxy代理,则是com.sun.proxy.$Proxy14
proxy.doSome("BUPT", 25);
}
}
这样就是cglib方式的动态代理了
但有接口也可以用cglib
在xml中配置
<!--
如果你期望目标类有接口,使用cglib代理
proxy-target-class="true":告诉框架,要使用cglib动态代理
-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
这样还是用的cglib方式
结束时间:2022-01-10