AOP
概念
-
什么是AOP?
它是“面向切面编程”的缩写,或者说“面向方面编程”
软件里有各个功能(业务逻辑),把它们分离开来,可以降低耦合度,提高程序的可重用性。
举个例子:
我们想做一个最基本的登录功能,做好后我想在登录功能的基础上添加(完善)功能:权限判断(管理员用户、普通用户…)
-
原视方式:修改源代码实现
-
现在:不通过修改源代码方式,在原有基础之上添加新的功能。(美国宪法)
可以把独立出一个权限判断新模块,给它配置到主干里区。
这样大大降低了耦合度,某天不用到这个判断,只需要去掉这个模块。
底层原理
AOP是怎么实现的呢?
-
AOP底层使用动态代理的方式实现
有两种情况的动态代理:
-
有接口情况
使用JDK动态代理
- 🔺创建接口实现类代理对象,增强类的方法。
-
没有接口情况
使用CGLIB动态代理
- 创建当前类子类代理对象,同样不是new出来的。
-
通过代理对象,把要增加的功能做到。
动态代理:
有接口情况
有接口就有实现类,现在想要在登录基础之上加个新功能(判断),这就叫增强一个类中的方法
如何用AOP方式实现呢?这就需要JDK动态代理实现。
需要再创建一个接口实现类的代理对象,但不是new出来的。而是代理对象。
有它之前的功能,但与new出来对象是不一样的,他是代理对象。
没有接口情况
比如我想增强User类中的方法,以前我们是创建User类的子类,继承父类。(原视方式)但现在我们要通过动态代理做到:CGLIB动态代理。
要创建当前类子类的代理对象,同样不是new出来的。
通过代码,写一下JDK动态代理代码编写,加深印象。Spring5已经帮我们封装好了。
使用JDK动态代理
使用Proxy类里面的方法,来创建出代理对象。
java.lang(工具类).reflect包下有类叫Proxy类,其提供的方法能帮我们创建出代理对象。
返回指定接口的代理类的实例(对象)。
newProxyInstance
方法,里面有三个参数:
-
ClassLoader
类加载器 -
我们针对的是有接口情况,所以我们写接口的class。
即增强方法所在的类的这个类实现的接口;支持多个接口,所以返回Class类型的数组形式。
-
最重要
InvocationHandler
写增强的部分:它单独是一个接口!实现这个接口,创建代理对象,写增强的方法
但这里返回的是InvocationHandler h
实例对象(即我们创建的代理对象),返回指定接口的代理类的实例。(两种方法:1.直接创建匿名内部类,生成对象;2.创建一个InvocationHandler
接口的实现类,new它的实例对象。
现在我们来写一下吧~
基本背景:
-
创建接口,定义方法。
别忘了接口里的方法都是抽象方法,没有方法体的。
-
创建接口的实现类,实现方法
下面重点:增强功能
- 使用Proxy类创建接口代理对象
增强接口有多种写法:
- 写个匿名内部类,直接new
public class JDKProxy {
public static void main(String[] args) {
//用newProxyInstance方法创建接口实现类代理对象
//1.类加载器;2.要实现的接口(需要数组类型)
//3.增强接口
Class[] interfaces = {UserDao.class};
Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
});
}
}
- 单独写个类来实现接口
public class JDKProxy {
public static void main(String[] args) {
//用newProxyInstance方法创建接口实现类代理对象
//1.类加载器;2.要实现的接口(需要数组类型)
//3.增强接口
Class[] interfaces = {UserDao.class};
Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),interfaces,new UserDaoProxy());
}
}
//创建代理对象代码
//实现类实现接口
class UserDaoProxy implements InvocationHandler{
//实现接口里的方法
//增强功能的逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
现在我们想要增强的是UserDaoImpl类的方法,所以要创建它的代理对象,因此要把对象传到UserDaoProxy里来。
相当于副本。
class UserDaoProxy implements InvocationHandler{
//1 把创建的是谁的代理对象,把谁传递过来
//有参数构造传递
private Object obj;
public UserDaoProxy(Object obj){
this.obj = obj;
}
这里为了通用用了Object类型,当然这个实例我们可以直接用UserDaoImpl类
//创建代理对象代码
//实现类实现接口
class UserDaoProxy implements InvocationHandler{
//1 把创建的是谁的代理对象,把谁传递过来
//有参数构造传递
private Object obj;
public UserDaoProxy(Object obj){
this.obj = obj;
}
//实现接口里的方法
//增强功能的逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法前执行
System.out.println("方法之前执行..."+method.getName()+":传递的参数"+ Arrays.toString(args));
//被增强的方法执行(对象,参数)
//对象是obj,因为我们已经将它传过来了
Object res = method.invoke(obj,args);
//方法之后
System.out.println("方法之后执行.."+obj);
return res;
}
}
invoke有“提及,唤起”之意。
此时之前new的UserDaoProxy类对象报红,因为我们上面为它增加了有参构造方法,传入了需要增强功能类的对象。
这样的话,我们修改一下:
public class JDKProxy {
public static void main(String[] args) {
//用newProxyInstance方法创建接口实现类代理对象
//1.类加载器;2.要实现的接口(需要数组类型)
//3.增强接口
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
Proxy.newProxyInstance(JDKProxy.class.getClassLoader(),interfaces,new UserDaoProxy(userDao));
}
}
上面Proxy.newProxyInstance
方法会返回我们的代理对象。这里有一个快捷键小技巧:后面加.var
帮助生成属性(成员对象)。
这里做一个强转UserDao
,因为UserDaoImpl
实现类实现接口是它。强转体现多态。
UserDao dao =(UserDao)Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));.
输出一下:
public class JDKProxy {
public static void main(String[] args) {
//用newProxyInstance方法创建接口实现类代理对象
//1.类加载器;2.要实现的接口(需要数组类型)
//3.增强接口
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao =(UserDao)Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
int result = dao.add(1, 2);
System.out.println("result" + result);
}
得到结果:
方法之前执行...add:传递的参数[1, 2]
add方法执行了
方法之后执行..com.atguigu.spring5.UserDaoImpl@2b193f2d
result3
总结一下JDK手动底层实现动态代理:
总结
-
写一个UserDao接口及它的实现类UserDaoImpl
-
实现类UserDaoImpl中实现两个基础方法(add增加,update更新)
-
使用JDK动态代理方式
-
创建一个JDKProxy类模拟JDK动态代理,里面包含main方法
-
使用newProxyInstance创建接口实现类的代理对象
-
newProxyInstance里传入参数有三个:
-
类加载器:
JDKProxy.class.getClassLoader()
-
要实现的接口(可能有很多,所以定义为数组形式,返回类型Class)
本案例中只有一个,就是UserDao接口
-
InvocationHandler
接口返回实现类的实例对象
进一步:
创建其实现类,该实现类通过有参构造函数,传递需要增强方法的类的实例对象。
在这个里面再写增强方法(即创建副本,在这基础上修改副本),我理解为两层嵌套,复制包裹最初的方法。
-
-
写好后,调用newProxyInstance创建的所谓的”代理对象“,这一步就是说这个代理对象可以用啦!
-
相关术语
1.连接点
举例:创建一个User类,里面有四个方法,实现增删改查功能。
类里面的哪些方法可以被增强,那这些方法就被成为连接点。
2.切入点
实际真正被增强的方法。四个方法理论上都可以被增强(连接点),但实际上我只增强”增方法“,那么只有”增方法“被成为切入点。
3.通知(增强)
-
实际增强的逻辑的部分被成为通知(增强)
即实际新增的功能。
-
通知有很多种类型:
-
前置@Before通知
add方法(示例中的切入点)之前执行
-
后置(返回)@AfterReturning通知
add方法之后执行
返回结果之后执行。
-
环绕@Around通知
方法前后都执行
里面有一个参数:
ProceedingJoinPoint proceedingJoinPoint
调里面的方法:(执行被增强方法)
proceedingJoinPoint.proceed()
-
异常@AfterThrowing通知
add方法出现异常之后才会执行,未发生不执行。
但异常未处理,所以若有@AfterReturning也不会执行了。
-
最终@After通知
类似finally,不管怎样都会执行。有异常也会执行。
After…
AfterThrowing…
-
4.切面
是一个动作。
-
把通知应用到切入点的过程
把权限判断加入到登录方法的过程
准备工作
Spring中有多种方式可以做到AOP实现。
Spring框架中一般基于AspectJ实现针对AOP相关的操作
-
什么是AspectJ
它并不是Spring组成部分,而是独立的AOP框架。
即不需要Spring也能单独使用。
一般把AspectJ和Spring框架一起使用,进行AOP操作。
这样在开发中更方便。
-
基于它主要是有两种方式实现AOP操作:
-
基于xml配置文件实现(少用)
-
基于注解方式实现(一般经常使用,更方便)
-
-
在项目的工程里面引入AOP相关依赖。
之前已经引入了:
现在我们还要在之前下好的Spring框架:
D:\Program Files\spring-5.2.6.RELEASE-dist\
中引入:
spring-aspects-5.2.6.RELEASE.jar
我们之前提到AspectJ本身并不是Spring中一部分,所以还需另外引入它的依赖。
接下来配置中需要用到一个东西:切入点的表达式
切入点表达式
-
切入点表达式的作用:
知道我们要对哪个类里面的哪个方法进行增强
-
语法结构:
execution([访问权限修饰符][返回类型][全类名][方法名称]([参数列表])
”执行,实施“
举例一:对com.atguigu.dao.BookDao类里面的add方法进行增强
execution(public com.atguigu.dao.BookDao.add(..))
修饰符可以省略不写,返回类型可以用*
举例二:相对类里所有方法进行增强
execution(public com.atguigu.dao.BookDao.*(..)
举例三:对com.atguigu.dao包里所有类,类里面所有方法进行增强
execution(public com.atguigu.dao.*.*(..)
这上面三个示例是实际中比较常用的例子。
AspectJ注解方式
-
创建被增强类,在类里面创建方法
-
创建增强类(编写增强的逻辑)
创建方法,让不同方法代表不同的通知类型(执行前呀,执行后呀…)
-
进行通知的配置
-
在spring中的配置文件中,开启注解的扫描
可以写配置类(完全注解开发),也可以写配置文件
我们这次用后者
-
使用注解创建User类和UserProxy类的实例对象
-
在增强的类上面添加注解
@Aspect
它就表示让这个类生成代理对象
-
在spring配置文件中写一段配置,开启生成代理对象
-
我们需要用到相关的名称空间:context。添加一下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ksDRxDrO-1666512986487)(https://raw.githubusercontent.com/Fanyup/cloudimg/master/img/ApplicationFrameHost_kPWL25TiNR.png)]
除此之外,为了我们能开启生成代理对象,我们还要加个名称空间:叫aop
component"组成部分"
我们想做个前置通知的效果:所以第四步
-
配置不同类型对象通知
-
在增强类的里面,在作为通知的方法上面
添加想要添加想要通知类型注解
并且使用切入点表达式来配置
-
前置通知用到是
@Before(切入点表达式)
注解value可以省略
-
增强类:
//增强的类
@Component
//生成代理对象
@Aspect
public class UserProxy {
@Before(value="execution(* com.atguigu.spring5.aopanno.User.add())")
public void before(){
System.out.println("之前执行");
}
}
被增强类:
//被增强类
@Component
public class User {
public void add(){
System.out.println("add..");
}
}
配置文件:
-
开启注解扫描
-
生成代理对象
(主要代码,不包含名称空间的引入)
<!--开启注解方式的【组件扫描】-->
<context:component-scan base-package="com.atguigu.spring5.aopanno"></context:component-scan>
<!--开启Aspect生成代理对象-->
<!--找有没有@Aspect,找到,就生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
测试一下效果:
@Test
public void testAopAnno(){
//加载配置文件,获取对象
ApplicationContext context = new ClassPathXmlApplicationContext("bean1.xml");
//这里获取User类实例对象
User user = context.getBean("user", User.class);
user.add();
}
成功返回结果:
之前执行
add..
注意:我们这里明明调用的是User类实例对象而不是代理对象,怎么会调用了它更改后的”副本“呢?
返回的还是User类,但这个类中间被代理过了!
下面说一些细节问题:
提取相同的切入点
把相同的切入点进行抽取。
写一个方法pointdemo()
在方法上用@Pointcut(把相同切入点放进来)注解
以后就可以在不同注解类型里写一样的东西(即封装工具)
示例:@Before(value=“porintdemo()”)
pointdemo()是我们定义的方法名,该方法使用@Pointcut提取切入点注解。
增强类设置优先级
对一个方法有多个增强类。
即多个增强类对同一个方法进行增强,
我们可以设置它们的优先级。谁先执行,谁后执行。
-
在增强类上添加注解@Order(数字类型)
数字越小,优先级越高
AspectJ:xml配置文件方式
一般很少用到,这部分仅作了解。
-
创建两个类,增强类和被增强类,创建方法。
-
在spring配置文件中创建两个类对象
-
在spring配置文件中配置切入点。
即对内个类做增强(Aop增强)
标签
<aop:config>
里:-
切入点
<aop:pointcut id="p" expression"切入点表达式"/>
-
配置切面:把before方法配置到类之前执行:
<aop:aspect ref="bookProxy">
-
增强作用在具体的方法上
<aop:before method="现在增强的方法" point-ref="p"/>
把新增增强方法作用到这个p需被增强方法(切入点,即实际被增强的方法)通知(实际被增强功能)(名词)。
-
-
完全注解方式
不需要写配置文件,完全使用注解开发
创建配置类,不需要创建xml文件
@Configuration
//开启组件扫描
@ComponentScan(basePackages = {"com.atguigu"})
//👇生成代理对象,不写默认是false
//等同于在xml中配置aspectj-autoproxy,表示开启对注解AOP的支持
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {
}
注:CGLIB网课漏讲了
里:
-
切入点
<aop:pointcut id="p" expression"切入点表达式"/>
-
配置切面:把before方法配置到类之前执行:
<aop:aspect ref="bookProxy">
-
增强作用在具体的方法上
<aop:before method="现在增强的方法" point-ref="p"/>
把新增增强方法作用到这个p需被增强方法(切入点,即实际被增强的方法)通知(实际被增强功能)(名词)。
-
完全注解方式
不需要写配置文件,完全使用注解开发
创建配置类,不需要创建xml文件
@Configuration
//开启组件扫描
@ComponentScan(basePackages = {"com.atguigu"})
//👇生成代理对象,不写默认是false
//等同于在xml中配置aspectj-autoproxy,表示开启对注解AOP的支持
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class ConfigAop {
}