spring-aop笔记
面向对象的缺陷及静态代理
面向对象
1. 代码缺陷
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能代码重复,分散在各个业务功能方法中!冗余,且不方便统一维护!
2. 解决思路
核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
将重复的代码统一提取,并且[[动态插入]]到每个业务方法!
3. 技术困难
解决问题的困难:提取重复附加功能代码到一个类中,可以实现
但是如何将代码插入到各个方法中,我们不会,我们需要引用新技术-------------------代理模式
代理模式:
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)
- 动词:指做代理这个动作,或这项工作
- 名词:扮演代理这个角色的类、对象、方法
- 目标:被代理“套用”了核心逻辑代码的类、对象、方法。(房东)
代理在开发中实现的方式具体有两种:静态代理,[动态代理技术]
静态代理:
(缺陷:每个添加此功能的核心类都要添加一个静态代理 )
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
代码举例:
目标接口:
public interface Calculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
目标(实现类):
/**
* 实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能!
* 目标类
*/
public class CalculatorPureImpl implements Calculator {
@Override
public int add(int i, int j) {
int result = i + j;
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}
静态代理类:(继承目标接口,实现接口方法,使用构造函数传入目标,这样就可以通过目标类的对象调用核心代码;在方法中写非核心代码,核心代码又目标对象调用;当我们测试时我们调用代理的方法,而代理又调用了目标的方法)
public class StaticProxyCalculator implements Calculator {
//使用构造函数传入目标
private Calculator calculator;
public StaticProxyCalculator(Calculator target){
this.calculator=target;
}
@Override
public int add(int i, int j) {
//非核心代码
System.out.println("i = " + i + ", j = " + j);
//调用目标
int result = calculator.add(i, j);
System.out.println("result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
return 0;
}
@Override
public int mul(int i, int j) {
return 0;
}
@Override
public int div(int i, int j) {
return 0;
}
}
动态代理
JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须**实现接口**!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)
- cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹)
JDK代理:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy(){
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
*/
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* invoke:调用代理的方法都会执行此方法!及其中需要重写的方法
* proxy:代理对象
* method:代理对象需要实现的方法,即其中需要重写的方法
* args:method所对应方法的参数
*/
Object result = null;
try {
System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
//通过反射调用目标方法进行核心业务0
result = method.invoke(target, args);
System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
} finally {
System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
/**
* jdk生成代理对象
* 参数1:类加载器
* 参数2:目标类的接口
* 参数3:具体要进行的代理动作(非核心动作-调用目标方法[执行核心动作])
*/
}
}
测试代码:
public class UseAop {
public static void main(String[] args) {
//目标
Calculator target=new CalculatorPureImpl();
/**
* 静态代理:
* 继承目标接口,实现接口方法,使用构造函数传入目标,这样就可以通过目标类的对象调用核心代码;
* 在方法中写非核心代码,核心代码又目标对象调用;当我们测试时我们调用代理的方法,而代理又调用了目标的方法
*/
//代理
Calculator proxy=new StaticProxyCalculator(target);
//调用(我们调用代理的方法,而代理又调用了目标的方法)
int add = proxy.add(1, 1);
System.out.println("add = " + add);
System.out.println("**************************************************");
/**
* jdk代理
* 创建jdk代理工厂的对象(factory),在传入目标类
* 通过工厂类对象(factory)调用getProxy()方法,得到代理对象proxy1
* 注意:这里的代理对象(proxy1)要强转为目标接口类型的,(因为该代理对象于目标是兄弟关系,所以不能强转为目标类型的)
*/
ProxyFactory factory=new ProxyFactory(target);
Calculator proxy1 = (Calculator) factory.getProxy();
proxy1.add(2,2);
}
}
执行结果:
1. 代理总结
代理方式可以解决附加功能代码干扰核心代码和不方便统一维护的问题!
他主要是将附加功能代码提取到代理中执行,不干扰目标核心代码!
但是我们也发现,无论使用静态代理和动态代理(jdk,cglib),程序员的工作都比较繁琐!
需要自己编写代理工厂等!
但是,提前剧透,我们在实际开发中,不需要编写代理代码,我们可以使用[Spring AOP]框架,
他会简化动态代理的实现!!!
动态代理执行过程
动态代理执行过程
在动态代理中目标方法的是由目标类的对象执行的
代理对象:代理对象是由动态代理机制生成的,它实现了与目标类相同的接口或继承了目标类。代理对象的作用是拦截目标方法的调用,并在调用前后执行额外的逻辑
目标对象:目标对象是实际执行行业务逻辑的对象,也就是被代理的原始对象。当代理对象拦截到方法调用后,最终会通过反射或其他方式将调用转发给目标对象,由目标对象执行具体的方法逻辑。
执行流程
1 客户端调用代理对象的方法
2 代理对象拦截方法调用,执行额外的逻辑(如前置处理)
3 代理对象将方法调用转发给目标对象。
4 目标对象执行实际的方法逻辑
5 代理对象执行后置处理(如后置处理)
6 返回结果给客户
总结:
有接口+aop(JDk)-----------------------------放入容器的是接口的一个代理类对象,而不是本身实现类,不可以根据实现类获取代理组件
没有接口+aop( cglib)-------------------------根据目标类生成了一个子类(代理类)放入到容器,可以使用实现类获取代理组件
如果使用aop技术,目标类有接口,必须使用接口类型接收IoC容器中的代理组件
代理组件:是指在Spring框架中,通过动态生成一个代理对象来包装目标对象。增强目标对象的功能
调用过程:当调用目标对象的方法时,实际上调用的是代理对象的方法。
解释:当调用目标方法时,实际执行目标方法的对象时目标对象,但调用过程时通过代理对象来控制的。代理对象会将方法调用委托给目标对象,由目标对象执行实际的方法逻辑。
代理对象的类型:代理对象不是目标对象的实例,而是目标对象的包装。
面向切面编程思想
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系。
散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
oop不能实现方法的局部修改
AOP利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。便于减少系统的重复代码,降低模块之间的耦合度。
aop将代码中重复的非核心业务提取到一个公共模块,最终在利用动态技术横向插入到各个方法中,-------------解决非核心代码冗余的问题
aop8个核心名词理解
1-横切关注点(散落在核心关注方法中的一些重复代码)
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
2(重点)-通知(增强)---------------------提取的那些横切关注点代码
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(**寿终正寝**)-------------提交事务
- 异常通知:在被代理的目标方法异常结束后执行(**死于非命**)-------------事务回滚
- 后置通知:在被代理的目标方法最终结束后执行(**盖棺定论**)-------------一定会执行
- 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
3-连接点 joinpoint(目标方法[能被横向切入的方法])
这也是一个纯逻辑概念,不是语法定义的。
指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法
4-切入点 pointcut
定位连接点的方式,或者可以理解成被选中的连接点!-----------------切点一定是连接点
是一个表达式,比如execution(* com.spring.service.impl.*.*(..))。符合条件的每个方法都是一个具体的连接点。
5-切面 aspect
切入点和通知的结合。是一个类。
6-目标 target
被代理的目标对象。
7-代理 proxy
向目标对象应用通知之后创建的代理对象。
8-织入 weave
指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
annotation快速实现
* 实现步骤:
* 1 导入依赖
* ioc/di---------spring-context
* aop-----------------spring-aop(依赖传递,不需要导了) ;aspectj(依赖传递,不需要导了) ;spring-aspectj(需要导入)
* 2 正常编写核心业务即可,类对象加入ioc容器(aop也是基于是ioc容器中的对象才能代理)
* 3 编写ioc的配置环境类和文件即可
* 4 测试环境
* 5 增强类,定义三个增强方法(存储横切关注点的代码)
* 6 增强类的配置(插入切点的位置,切点的指定,切面配置等等)
* 7 开启aop的配置
* 增强类的内部要存储增强代码
* (前面类的创建没有变化,只不过我们件一些方法中重复的与业务无关的代码抽离出来写在一个增强类中变成一个个方法,
* 并且指定这些方法要作用在那个类的那个方法上----切点指定)
* 1. 定义方法存储增强代码
* 具体定义几个方法,根据插入的位置决定
* 2. 使用注解配置 指定插入目标方法的位置
* 前置 @Before
* 后置 @AfterReturning
* 异常 @AfterThrowing
* 最后 @After
* 环绕 @Around
*
* try{
* 前置
* 目标方法执行
* 后置
* }catch(){
* 异常
* }finally{
* 最后
* }
* 3. 配置切点表达式[选中插入的方法 切点]------------写在注解的括号里
* 4. 补全注解
* 加入ioc容器 @component
* 配置切面 @Aspect = 切点 + 增强(最终增强类就会形成一个切面)
* spring aop 重点是配置-》jdk|cglib
* 5。 开启aspect注解的支持
* 在配置类中使用 @EnableAspectJAutoProxy//表示开启aspectj注解
* 注意:该类也需要被扫描到
增强类代码举例:
@Component
@Aspect
public class LogAdvice {
@Before("execution(* com.atguigu.service.impl.*.*(..))")
public void start(){
System.out.println("方法开始了");
}
@After("execution(* com.atguigu.service.impl.*.*(..))")
public void after(){
System.out.println("方法结束了");
}
@AfterThrowing("execution(* com.atguigu.service.impl.*.*(..))")
public void error(){
System.out.println("方法报错了");
}
}
测试类
@SpringJUnitConfig(value = JavaConfig.class)
public class SpringAopTest {
@Autowired
private Calculator calculator;
/**
* 为什么该处执行目标方法的对象不能是目标类,而是其接口
* aop的底层会使用代理技术,因为该类有接口所以会选择jdk动态代理,所以生成的代理类是实现目标类的接口的,
* 代理类与目标类的关系是兄弟关系,所以此处必须将对象的类型声明为接口的类型而非实现类,否则是取不到代理对象的
*/
@Test
public void test1(){
int add = calculator.add(1, 1);
System.out.println("add = " + add);
}
}
获取切点细节信息
1 JointPoint接口
需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。
- 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
- 要点2:通过目标方法签名对象获取方法名
- 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
2 获取方法返回值
在返回通知中,通过**@AfterReturning**注解的returning属性获取目标方法的返回值!
3 获取异常对象捕捉
在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象
* 定义四个增强方法,获取目标方法的信息 返回值 异常对象
* 1 定义方法--增强代码
* 2 使用注解指定对应的位置
* 3 配置切点表达式选中方法
* 4 切面和ioc的配置
* 5 开启aspectj注解的支持
* 扩展:增强方法中获取目标方法的信息
* 1 全部增强方法中,获取目标方法的信息(方法名,参数,访问修饰符,所属的类的信息....)
* 在方法的形参中添加一个JoinPoint joinPoint
* joinPoint包含目标方法的信息!
* 2 返回的结果- @AfterReturning
* 在方法形参中声明Object result来接收结果
* 在注解中 @AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "形参名")
* 3 异常的信息- @AfterThrowing
* 在方法形参中声明Throwable throwable来接收结果
* 在注解中 @AfterThrowing(value = "execution(* com..impl.*.*(..))",throwing = "形参名")
代码举例:
增强类
@Component
@Aspect
public class MyAdvice {
@Before("execution(* com..impl.*.*(..))")
public void before(JoinPoint joinPoint){
//1 获取方法所属的类的信息
String simpleName = joinPoint.getTarget().getClass().getSimpleName();
//2 获取方法名称
int modifiers = joinPoint.getSignature().getModifiers();
String s = Modifier.toString(modifiers);//方法的访问修饰符
//3 获取参数列表
Object[] args = joinPoint.getArgs();//获取目标方法参数
}
@AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "result")
public void afterReturning(Object result){
}
@After("execution(* com..impl.*.*(..))")
public void after(){
}
@AfterThrowing(value = "execution(* com..impl.*.*(..))",throwing = "throwable")
public void afterThrowing(Throwable throwable){
}
}
切点表达式语法
* 切点表达式:
* 固定语法 execution(1 2 3 4 5(6))
* 1 是访问修饰符
* public /private
* 2 方法的返回值类型
* String/int/void
* 如果不考虑访问修饰符和返回值!这两位整合在一起写 *
* 必须是一起不考虑,不能出现 *String 的情况
* 3 包的位置
* 具体包:com.atguigu.service.impl
* 单层模糊:com.atguigu.service.* *单层模糊 (service包下任意包都可以)
* 多层模糊:com..impl ..是任意层的模糊 (找所有的impl下的类)
* 注意:..不能用在开头 不能写成..impl 可以写成 *..impl
* 4 类的名称
* 具体:CalculatorPureImpl
* 模糊:* (指包下任意类)
* 部分模糊:*Impl (指包下以Impl结尾的类)
* 5 方法名 语法和类名一致
* 6 (6)形参数列表
* 没有参数:()
* 有具体参数:(String) (String,int)----------------注意:有顺序要求
* 模糊参数:(..) 有没有参数都可以
* 部分模糊:(String..) 第一个是String后面有没有无所谓
* (..int) 最后一个是int前面有没有无所谓
* (String..int)String开头int结尾
* 1. **切点表达式案例**
* 1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
* execution(public int xx.xx.jj.*(..))
* 2.查询某包下类中第一个参数是String的方法
* execution(* xx.xx.jj.*(String..))
* 3.查询全部包下,无参数的方法!
* execution(* *..*.*()) //------------注意:任意类下任意方法 *.*
* 4.查询com包下,以int参数类型结尾的方法
* execution(* com..*.*(..int))
* 5.查询指定包下,Service开头类的私有返回值int的无参数方法
* execution(private int xx.xx.Service*.*())
切点表达式的提取和复用
* 1 当前类中提取
* 定义一个空方法(作用就是承载@Pointcut(切点表达式))
* 在增强注解中引用切点表达式的方法即可 例如:@注解("函数()")
* @Pointcut("execution(* com.atguigu.service.impl.*.*(..))")
* public void pc(){
* }
* @Before("pc()")
* public void start(){
* System.out.println("方法开始了");
* }
* 2 创建一个存储切点的类
* 单独维护切点表达式 类的全限定符。方法名()
*例如:
* 切点类(专门用于存储切点)
*public class MyPointCut {
* @Pointcut("execution(* com.atguigu.service.impl.*.*(..))")
* public void pc(){
* }
*
* @Pointcut("execution(* com..impl.*.*(..))")
* public void myPc(){
* }
* }
* @Before("com.atguigu.pointcut.MyPointCut.myPc()")
* public void before(JoinPoint joinPoint){}
环绕通知
* 环绕通知(可以定义在任意位置)
* 环绕通知的方法返回值类型必须是Object,(为什么:环绕通知需要在通知(方法)中定义目标方法的执行,所以有放返回值)
* 环绕通知的方法必须有一个ProceedingJoinPoint类型的形参(joinPoint)
* 参数joinPoint就是目标方法,(ProceedingJoinPoint joinPoint不仅可以获取目标方法的信息,还可以执行方法)
* JoinPoint与ProceedingJoinPoint的区别
* JoinPoint:仅能获取目标类的信息,ProceedingJoinPoint不仅能获取目标类的通知还能执行目标类的方法
代码举例
@Component
@Aspect
public class TxAroundAdvice {
@Around("com.atguigu.advice.LogAdvice.pc()")
public Object transaction(ProceedingJoinPoint joinPoint){
//保证目标方法被执行
/*
通过参数joinPoint调用proceed()方法;注意需要参入参数
传入的参数就是目标方法需要的参数
如何获取目标方法的参数:
通过 参数joinPoint调用getArgs()方法即可获得目标方法的参数
注意:proceed()方法执行以后会有异常
1 直接抛出--------------------不能加入异常通知
2 使用try catch抛出---------------可以加入异常通知
*/
Object[] args = joinPoint.getArgs();
Object result = null;
try {
//增强代码-》before
System.out.println("开启事务");
result = joinPoint.proceed(args);
System.out.println("结束事务");
} catch (Throwable e) {
System.out.println("事务回滚");
throw new RuntimeException(e);
} finally {
}
return result;
}
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.kler.cn/a/586801.html 如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!