前言
为了理解Spring AOP,我们先来了解一下Java的代理模式
什么是代理?
举个例子来说明代理的作用:
假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的.明星就是一个目标对象,他只要负责活动中的节目,而其他琐碎的事情就交给他的代理人(经纪人)来解决.这就是代理思想在现实中的一个例子
用图表示如下:
代理模式的关键点是:代理对象与目标对象.代理对象是对目标对象的扩展,并会调用目标对象
代理的类型
- 静态代理
- 动态代理
动态代理又包括
- 基于接口的动态代理
- 基于子类的动态代理
静态代理
一个简单的例子:
Service里的业务逻辑我们称之为需要执行的目标方法;
开启事务,提交事务这些我们就可以称之为对目标方法的增强。
“需要执行的目标方法”单独写一个类(目标类),“对目标方法的增强”单独写一个类(增强类),最后再写一个类(代理类),把它两者结合到一起。
public interface PersonService {
public void savePerson();
public void updatePerson();
public void deletePerson();
}
public class PersonServiceImpl implements PersonService{
@Override
public void savePerson() {
System.out.println("添加");
}
@Override
public void updatePerson() {
System.out.println("修改");
}
@Override
public void deletePerson() {
System.out.println("删除");
}
}
增强类:
public class Transaction {
public void beginTransaction(){
System.out.println("开启事务 ");
}
public void commit(){
System.out.println("提交事务");
}
}
代理类:
public class PersonServiceProxy implements PersonService{
//目标类
private PersonService personService;
//增强类
private Transaction transaction;
//利用构造函数将目标类和增强类注入
public PersonServiceProxy(PersonService personService,Transaction transaction){
this.personService = personService;
this.transaction = transaction;
}
@Override
public void savePerson() {
transaction.beginTransaction();
personService.savePerson();
transaction.commit();
}
@Override
public void updatePerson() {
transaction.beginTransaction();
personService.updatePerson();
transaction.commit();
}
@Override
public void deletePerson() {
transaction.beginTransaction();
personService.deletePerson();
transaction.commit();
}
}
测试:
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ProxyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("com/cj/study/proxy/applicationContext-proxy.xml");
PersonService personService = (PersonService)context.getBean("personServiceProxy");
personService.savePerson();
}
}
bean.xml
<?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-2.5.xsd">
<bean id="personServie" class="com.cj.study.proxy.PersonServiceImpl"></bean>
<bean id="transaction" class="com.cj.study.proxy.Transaction"></bean>
<bean id="personServiceProxy" class="com.cj.study.proxy.PersonServiceProxy">
<constructor-arg index="0" ref="personServie"></constructor-arg>
<constructor-arg index="1" ref="transaction"></constructor-arg>
</bean>
</beans>
执行结果:
开启事务
添加
提交事务
静态代理是在程序运行前,代理类的.class文件就已经存在了
静态代理的缺点
- 假设一个系统中有100个Service,则需要创建100个代理对象
- 如果一个Service中有很多方法需要事务(增强动作),发现代理对象的方法中还是有很多重复的代码
- 由第一点和第二点可以得出:静态代理的重用性不强
那么该如何解决?这就轮到了我们动态代理的登场
动态代理
动态代理实现的目的和静态代理一样,都是对目标方法进行增强,而且让增强的动作和目标动作分开,达到解耦的目的
动态代理分为基于接口的动态代理和基于子类的动态代理
基于接口的动态代理
基于接口的动态代理:
涉及的类:Proxy
提供者:JDK官方
如何创建代理对象:
使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
例子:
package com.fym.cglib;
import com.fym.proxy.IProducer;
/**
* @Author fym
* @Date 2020/9/5 21:35
*
* 一个生产者
*/
public class Producer{
//销售
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱"+money);
}
//售后
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱"+money);
}
}
package com.fym.proxy;
/**
* @Author fym
* @Date 2020/9/5 21:37
*
* 对生产厂家要求的接口
*/
public interface IProducer {
//销售
public void saleProduct(float money);
//售后
public void afterService(float money);
}
如何创建代理对象:
使用Proxy类中的newProxyInstance方法
创建代理对象的要求:
被代理类最少实现一个接口,如果没有则不能使用
newProxyInstance方法的参数
ClassLoader:类加载器
它是用于加载代理对象字节码的,和被代理对象使用相同的类加载器
写法是固定写法,代理的是谁就写Proxy.newProxyInstance(xxx.getClass().getClassLoader())
Class[]:字节码数组
它是用于让代理对象和被代理对象有相同方法
代理的是谁就写(xxx.getClass().getInterfaces());
InvocationHandler:用于提供增强的代码
它是让我们写如何代理 我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的
此接口的实现类都是谁用谁写,就是谁要对方法增强就谁写
package com.fym.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @Author fym
* @Date 2020/9/5 21:38
*
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer=new Producer();
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 执行被代理对象的任何接口方法都会经过该方法
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue=null;
//1.获取方法执行的参数
Float money=(Float)args[0];
//2.判断当前方法是不是销售
if ("saleProduct".equals(method.getName())){
returnValue= method.invoke(producer,money*0.8f);
}
return returnValue;
}
}
);
proxyProducer.saleProduct(1000f);
}
}
基于子类的动态代理
基于子类的动态代理:
涉及的类:Enhancer
提供者:第三方cglib库
如何创建代理对象:
使用Enhancer类中的create方法
创建代理对象的要求:
被代理类不能是最终类
package com.fym.cglib;
import com.fym.proxy.IProducer;
/**
* @Author fym
* @Date 2020/9/5 21:35
*
* 一个生产者
*/
public class Producer{
//销售
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱"+money);
}
//售后
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱"+money);
}
}
package com.fym.cglib;
import com.fym.proxy.IProducer;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @Author fym
* @Date 2020/9/5 21:38
*
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer=new Producer();
/**
create方法的参数
Class:字节码
它是用于指定被代理对象的字节码
写法是固定写法,代理的是谁就写Proxy.newProxyInstance(xxx.getClass().getClassLoader());
Callback:用于提供增强的代码
他是让我们写如何代理,我们一般都是写一个接口的实现类,通常情况下都是匿名内部类,但不是必须的。
此接口的实现类都是谁用谁写
我们一般写的都是该接口的子接口的实现类:MethodIntercepter
*/
Producer producer1= (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* methodProxy是当前执行方法的代理对象
* */
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue=null;
//1.获取方法执行的参数
Float money=(Float)args[0];
//2.判断当前方法是不是销售
if ("saleProduct".equals(method.getName())){
returnValue= method.invoke(producer,money*0.8f);
}
return returnValue;
}
});
producer1.saleProduct(10000f);
}
}
执行结果与基于接口的动态代理一致。
两个类型代理的小总结
- JDK动态代理,要求目标类实现接口,但是有时候目标类直接一个单独的对象,并没有实现任何的接口,这时就得使用CGLib动态代理
- JDK动态代理是JDK里自带的,CGLib动态代理需要引入第三方的jar包
- CGLib动态代理,它是在内存中构建一个子类对象,从而实现对目标对象功能的扩展
- CGLib动态代理,是基于继承来实现代理,所以无法对final类、private方法和static方法进行代理
我们用动态代理的做法去实现目标方法的增强,实现代码的解耦,是没有问题的,但是还是需要自己去生成代理对象,自己手写拦截器,在拦截器里自己手动的去把要增强的内容和目标方法结合起来,这用起来还是有点繁琐,那么解决方法应该是什么呢?
我们最终的主角Sring AOP登场!
Spring AOP
什么是Spring AOP
AOP(Aspect-OrientedProgramming,面向切面(方面)编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。我们知道OOP的特点就是封装、继承、多态。我们很容易就能建立对象的层次结构,也就是说OOP允许你定义从上到下的关系,但不适合定义从左到右的关系,什么是从左到右的关系?例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
而AOP技术则恰恰相反,它利用一种称为**“横切”**的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面/切面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。
比如:我们写一段业务代码,例如进行一些简单的增删查改,需要日志功能,那么我们的日志类就是一个“切面”,那我们这个日志类根本就没有影响到我们自己的业务逻辑,也不会让我们整个操作有大量重复代码。
为什么使用Spring AOP
有了Spring的AOP后,我们就不用自己去写方法的增强了,例如上面的动态代理什么的,只需要在配置文件里进行配置,告诉Spring你的哪些类需要生成代理类、你的哪个类是增强类、是在目标方法执行之前增强还是目标方法执行后增强。配置好这些后Spring按照你的配置去帮你生成代理对象,按照你的配置把增强的内容和目标方法结合起来。就相当于自己写代码也能实现和AOP类似的功能,但是有了Spring AOP以后有些事情Spring帮你做了,而且人家Spring做成了可配置化,用起来非常简单而且很灵活
上边用的JDK动态代理和cglib动态代理,这两种在Spring的AOP里都有用到,Spring是根据不同的情况去决定是使用JDK的动态代理生成代理对象,还是使用cglib去生成代理对象,是不是很智能呢~
Spring AOP里的关键名词
**切面类:**就是要执行的增强的方法所在的类
**通知:**切面类里的增强方法
**目标方法:**要执行的目标方法,其实也可以说是切入点方法
**织入:**把通知和目标方法进行结合,形成代理对象的过程就叫织入
代理对象的方法=目标方法+通知
说这些概念,我们也不知道具体啥意思啊?下面以一个简单的例子来看看Spring AOP的简单应用
下面就是简单的业务类,保存、更新、删除操作,这些都是我们的目标方法
package com.fym.service;
/**
* @Author fym
* @Date 2020/9/6 14:21
*/
public interface IAccountService {
//模拟保存账户
void saveAccount();
//模拟更新账户
void updateAccount(int i);
//删除账户
int deleteAccount();
}
package com.fym.service.impl;
import com.fym.service.IAccountService;
/**
* @Author fym
* @Date 2020/9/6 14:23
*/
public class AccountServiceImpl implements IAccountService {
@Override
public void saveAccount() {
System.out.println("执行了保存");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新");
}
@Override
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
这是日志类,也就是我们前面说的切面类。因为一个操作要记录一下日志嘛,我们不可能在业务类那里每一个操作方法里面都打印一句“开始记录日志了。。。”,而且这只是一个小例子随便打印一句话,真正的切面类可不一定像我们的例子那么简单,可想而知如果我们不设置切面类代码会有多冗杂。
那这个切面类里面的printLog就是通知咯,因为它是切面类里面的增强方法
package com.fym.utils;
/**
* @Author fym
* @Date 2020/9/6 14:37
*
* 用于记录日志的工具类。它里面提供了公共的代码
*/
public class Logger {
//用于打印日志,计划让其在切入点方法执行前执行(切入点方法就是业务层方法)
public void printLog(){
System.out.println("Logger类的printLog方法开始记录日志了。。。");
}
}
bean.xml
spring中基于xml的aop配置步骤
- 把通知Bean也交给spring来管理
- 使用aop:config标签表明开始aop配置
- 使用aop:aspect标签表明开始配置切面
id属性;是给切面提供一个唯一标识
ref属性:是指定通知类bean的id - 在aop:aspect标签的内部使用对应的标签来配置通知的类型
我们现在的示例是让pringLog方法在切入点方法执行之前执行
aop:before :表示配置前置通知
method属性:用于指定Logger类中哪个方法是前置通知
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置spring的ioc把service对象配置进来-->
<bean id="accountService" class="com.fym.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="logger" class="com.fym.utils.Logger"></bean>
<!--配置aop-->
<aop:config>
<!--配置切面-->
<aop:aspect id="logAdvice" ref="logger">
<!--配置通知的类型并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* *..*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
我们可能会疑惑这个execution是个啥啊里面那么多星号还有点点、括号什么的,一脸懵逼
这里解释一下,这是切入点表达式的写法
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名…类名.方法名(参数列表)
标准的表达式写法:
public void com.fym.service.impl.AccountServiceImpl.saveAccount()
这么说是不是还是有点儿不懂,我们一步步来看
参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用…表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式通常写法:
切到业务层实现类下的所有方法
针对这个例子就是:
* com.fym.service.impl.*.*(..)
好啦,那都配置好了之后,我们写一个测试类来看看效果吧
package com.fym.test;
import com.fym.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @Author fym
* @Date 2020/9/6 15:03
* 测试aop的配置
*/
public class AOPTest {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac=new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as=(IAccountService)ac.getBean("accountService");
//3.执行方法
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
运行结果如下
Spring AOP的具体加载步骤
1、当spring容器启动的时候,加载了spring的配置文件
2、为配置文件中所有的bean创建对象
3、spring容器在创建对象的时候它会解析aop:config的配置
解析切入点表达式,用切入点表达式和纳入spring容器中的bean做匹配
如果匹配成功,则会为该bean创建代理对象,代理对象的方法=目标方法+通知
如果匹配不成功,则为该bean创建正常的对象
其实就是你通过表达式告诉Spring哪些bean需要它帮你生成代理对象而不是生成原有的正常对象
理解这一点相当重要!
4、在客户端利用context.getBean获取对象时,如果该对象有代理对象则返回代理对象,如果没有代理对象,则返回目标对象
说明:如果目标类实现了接口,spring容器会采用jdk的动态代理产生代理对象,产生的代理类和目标类实现了相同的接口;
如果目标类没实现接口,spring容器会采用cglib的方式产生代理对象,产生的代理类是目标类的子类
对于Spring AOP的原理我们都知道是动态代理,但是具体的不太清楚,通过本文从代理–>静态代理–>动态代理–>AOP,大概基本清晰了一点吧~
参考文章
静态代理:
https://caoju.blog.csdn.net/article/details/90713500
AOP部分参考:
https://caoju.blog.csdn.net/article/details/90719595
https://blog.csdn.net/moreevan/article/details/11977115/