问题
- 面向对象思想OOP是的核心在于它是纵向继承的
- 若想封装某一个功能,那这个功能在代码中必须连续的,它不能说是我这个功能的代码必须在另一个功能开始的前面有一段代码,结束后面也有一段代码,这样的代码是无法抽取和封装的
代理模式
- 假如我有一个具体的业务实现方法,它是在一个接口的实现类中,现在有一个代理对象,它能够访问这个实现类,而我们不能直接访问这个实现类,我们需要通过这个代理对象来访问这个实现类中的方法,这,就是代理模式
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类,对象,方法
- 目标:被代理套用了非核心逻辑代码的类,对象,方法
- 一般目标对象实现了核心业务逻辑,而代理类中的代理方法实现一些附加功能
静态代理实现
- 首先要求代理类和目标类实现同一个接口
- 因为代理类和目标类能完成的功能一定是一样的,但是代理类能扩展
- 在代理类中声明一个目标类的成员变量
- 在代理类中的方法中使用对应的目标类方法,可以根据具体要求,在代理方法中对目标类方法实现扩展
- 目标对象不能被直接访问,只能通过代理对象访问
- 举例
- 我现在有一个Cal接口,这个接口中有add,substract方法
- 我让我的目标类和代理类CalImpl和Calproxy都实现了这个接口方法
- 我在目标类实现方法中写我这个代码的核心逻辑和功能
- 我在代理类相同的方法中通过目标成员变量调用目标类的方法,而且可以在方法里写点扩展,比如打印结果这种功能
动态代理
-
虽然静态代理确实在一定程度上实现了解耦,我们可以将一些非核心功能从目标类中移除,但是静态代理不灵活,代码写死了,就比如我有一个日志功能扩展,但是由于目标类只有一个,假如我有其他目标类也需要,那么就需要再弄几个代理类,代码太冗余了
-
动态代理,动态的生成代理类
- 由于我们的目标类不确定,它可能是多个类
- 我们这里用一个代理工厂类来生产代理类
- 我创建了一个ProxyFactory,它就是代理工厂类
- 在这个类里面我需要一个成员变量
private Object target;
,它用来表示我们的目标类,因为我们是动态代理,目标类不确定 - 接着写一个构造器和一个返回生产的代理类的方法
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
return Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)
}
- 我们使用Proxy类的创建代理实例的方法返回代理类
- 这里有三个参数
- ClassLoader loader:指定加载动态生成的代理的类加载器
- 根类加载器 bootstrapClassLoader 用来加载JVM所需要的类
- 扩展类加载器 extensionsClassLoader 用来加载扩展类库
- 应用类加载器 AppClassLoader 用来加载我们自己写的类和第三方jar包里的类
- 所以这里我们要指定的是应用类加载器,它比较容易获取,通过任何一个应用类来获取,直接用this.getClass().getClassLoader()
- Class[ ] interfaces:这里用来指定目标类实现接口的接口数组,在静态代理中我们的目标类和代理类都会实现同样的接口来方便我们重写目标类的方法,这里也一样,我们要知道目标类实现了哪些接口
- 使用target.getClass().getInterfaces可以获得目标类实现的所有接口
- InvocationHander h:因为我们的代理类中始终是要重写目标类的方法的,在静态代理中,我们在重写目标类的方法中就是在合适的位置调用目标类的方法,与我们自己编写的代码结合形成扩展
- 这个参数是接口,而这个接口又只有一个抽象方法,就是invoke
- `InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
要扩展的功能
Object result = method.invoke(target, args);
要扩展的功能
return result;
}
};
- 我解释一下它是如何在不知道我们目标类的情况下,实现对目标类的方法的重写
- 我们传入的三个参数,分别是代理对象,目标类方法,参数数组
- 首先我们重写的这个invoke就是一个反射方法,大家还记得原本的invoke方法把,xxx.invoke(对象) 表示用反射调用对象的方法
- `Object result = method.invoke(target, args);
- 这一步表示反射调用target的method方法,也就是目标方法,并传入参数数组
- 在这个重写方法的最后返回result,这个result应该是给Proxy.newProxyinstance用的吧
- `Object result = method.invoke(target, args);
- 到此为止,三个参数都搞定了,就可以返回一个代理类实例了
- ClassLoader loader:指定加载动态生成的代理的类加载器
- 现在我们来使用一下我们设计的动态代理
ProxyFactory proxyFactory = new ProxyFactory(new CalImpl());
Cal proxy = (Cal)proxyFactory.getProxy();
proxy.add(1, 1);
- 这里注意一下获取到代理类后的强转,由于我们是不知道目标类,这里我直接new这个目标类了,但是一般我们是不知到目标类的,所以在强转的时候,转为目标类实现接口的类型,这样来调用代理方法
-
两种动态代理
- jdk动态代理,要去必须有接口,最终生成的代理类和目标类实现相同的接口,在com.sun.proxy包下,类名为$proxy123… 就后面接数字表次序
- cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下
AOP
AOP概述
- 面向切面编程
- 抽取和套用
- 抽取:抽取非核心业务代码,将它交给切面管理
- 套用:把抽取出来的代码套用到需要的地方
- 术语
-
横切关注点
- 从每个核心方法中抽取出来的同一类非核心业务,比如我的加减乘除方法的开头总有有一个打印语句。
- 横切关注点往往在目标类的核心方法中不止一个,可能多个
- 这个横切关注点的个数是跟据你在核心方法中添加的附加功能的个数·而决定的
-
通知
- 每个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法
- 意思就是,在目标类的核心方法中,我们写这一段非核心代码叫横切关注点,在切面中叫通知
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功后执行
- 异常通知:在被代理的目标方法异常结束后执行
- 后置通知:在被代理的目标方法最终结束后执行
- 环绕通知:使用try…catch…finally结构围绕整个被代理目标方法,包括上面四种通知对应的所有位置
-
切面
- 封住通知方法的类叫做切面
-
目标
- 被代理的目标对象
-
代理
- 向目标对象应用通知之后创建的代理对象(AOP中的代理对象不需要我们自己创建了,它被封装了)
-
连接点
- 抽取横切关注点的位置
- 我们必须要知道连接点的位置,因为我们套用要知道连接点的位置
-
切入点
- 定位连接点的方式
- 每个类都包含多个连接点,所以连接点事类中客观存在的事务
- 如果把连接点看做数据库中的记录,那么切入点就是查询记录的sql语句
-
织入
- 将切面切入到目标方法之中,使目标方法得到增强的过程被称之为织入。
-
- 作用
- 简化代码:把方法中固定位置的重复的代码抽取出来
- 代码增强:把特定的功能封装到切面类中,哪里需要,就往哪套,被套用了切面逻辑的方法就被切面给增强了
基于注解的AOP
准备工作
- 在IOC所需要的依赖基础上导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
- 创建切面类和目标类,并将切面类和目标类交给IOC容器管理,用扫描和注解
- 告诉IOC我的切面类是一个切面,例子
@Component
@Aspect
- `public class LogAspect {}
基于注解的AOP功能
- 在spring配置文件中开启基于注解的AOP功能
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 前置通知
- 设置
@Before("execution(public int zt.CalImpl.add(int ,int))")
public void beforeAdviceMethod(){
System.out.println("这是前置通知");
}
- 要在方法名打上@Before注解表示这是前置通知方法,before里面的值为切入点表达式,具体到哪个方法
- 这样前置通知方法就会在add方法之前执行
- 测试
- 首先获得ioc容器
- 由于在ioc容器中我们无法获取目标对象bean,我们只能通过目标对象实现的接口类型来获得代理对象bean
- 然后通过代理对象调用目标方法,完成测试
- 设置
- 切入点表达式
- 设置在标识通知的注解的value属性中
@Before("execution(public int zt.CalImpl.add(int ,int))")
- execution中的值
- public int 表示访问范围和返回类型
- zt.CalImpl表示具体的类
- add(int,int)表示具体的方法
@Before("execution(* zt.*.*(..))")
- 使用通配符,第一个* 表示任意的访问修饰符
- 第二个* 表示zt包下的所有类
- 第三个*表示类下的所有方法
- … 表示任意的参数列表
- 这样这个包下面的任意一个类的任意一个方法都会有这个前置通知
- 获取连接点的信息
- 在通知方法的参数位置,设置joinPoint类型的参数,就可以获取连接点所对应方法的信息
public void beforeAdviceMethod(JoinPoint joinPoint)
Signature signature = joinPoint.getSignature();
可以获得目标方法的签名信息Object[] args = joinPoint.getArgs();
可以获得目标方法的参数信息- 签名信息就是 方法返回 方法的名称 参数类型
- 切入点表达式的复用
- 假如我好几个通知都是用同一个切入点,我希望它们能重用,而不是再写一遍
- 怎么重用
- 用
@Pointcut
标识一个空方法 @Pointcut("execution(* zt.*.*(..))")
public void poincut(){}
- 这个就是声明一个公用的切入点的表示式
- 在需要使用这个切入点表达式的地方的注解的值中写空方法的方法名
@Before("poincut()")
- 这样就实现了复用
- 用
- 如果你想在其它类中使用这个切入点表达式,也是可以的。
- 其他通知的使用
- 后置通知
@After
- 在目标对象方法的finally字句中执行,无论报不报异常,也会执行
- 一般用于资源的关闭之类的
- 返回通知
AfterReturning
- 在目标对象调用返回之后才执行,如果方法中出现异常,没有返回值,则不会执行
- 能够获取目标对象的方法的返回值
@AfterReturning(value = "poincut()", returning = "result")
public void AfterAdviceMethod(JoinPoint joinPoint, Object result)
- 通过在注解中的属性returning指定方法中哪个参数来接收目标对象方法的返回值
- 异常通知
@AfterThrowing
- 在目标对象方法的catch语句中执行,只有出现异常才会执行
- 它能够获取目标对象方法中抛出的异常信息
@AfterThrowing(value = "poincut()",throwing = "exception")
public void AfterAdviceMethod(JoinPoint joinPoint,Exception exception)
- 后置通知
- 各种通知的顺序
- spring版本5.3以前:
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- spring版本5.3以后
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
- spring版本5.3以前:
- 环绕通知
- 环绕通知和其他通知有些区别,它的话相当于我们之前些的动态代理,包含了其他通知,而且我们一般不会一起设置环绕通知和其他四种通知
- 在动态代理中我们需要有一条表示目标方法执行的语句,环绕通知也需要,不然不知道目标方法在什么时候执行
@Around("poincut()")
public Integer AroundService(ProceedingJoinPoint joinPoint){
Object result = null;
try {
System.out.println("前置通知");
Integer[] integers = {1,1};
result = joinPoint.proceed(integers);
} catch (Throwable e) {
System.out.println("异常通知");
}finally {
System.out.println("后置通知");
}
System.out.println("返回通知");
return (Integer) result;
}
1. ProceddingJoinPoint可以来表示我们的目标方法执行,joinPoint.proceed(参数),参数为Object数组
2. proceed方法的异常只能用try-catch,因为我要有异常通知
3. 方法的返回值类型要和目标方法的返回值一致,如果目标方法返回的是基本数据类型,就返回对应包装类
- 切面的优先级
- 每一个切面都有一个默认优先级
- 调整切面的优先级,我们可以通过
@Order(value)
中的value来改变,value属性是数字数字越小,优先级越高,这个注解是作用在切面类上的 - 默认值是Integer的最大值
基于xml的AOP实现(了解即可)
- 首先删除AOP的注解
- 创建spring配置文件
- 在配置文件中配置好ioc容器的扫描,并在类中写好注解,毕竟aop是基于ioc的
- 接下来的配置
<aop:config>
<!-- 设置一个切入点表达式-->
<aop:pointcut id="pointcut" expression="execution(* zt.CalImpl.add(int,int))"/>
<!-- 设置一个切面类,这个切面类只从ioc容器中的bean选择-->
<aop:aspect ref="logAspect" order="1">
<!-- 设置通知方法-->
<!-- 环绕通知,有属性切入点pointcut和切入方法指定method,还有参数列表 arg-names-->
<aop:around method="AroundService" pointcut-ref="pointcut"></aop:around>
<!-- 环绕通知,有属性切入点pointcut和切入方法指定method,还有参数列表 arg-names-->
<aop:after-throwing method="AfterAdviceMethod" pointcut-ref="pointcut" throwing="exception"></aop:after-throwing>
<aop:before method="beforeAdviceMethod" pointcut-ref="pointcut"></aop:before>
</aop:aspect>
</aop:config>
- 基于xml的aop学习过注解后就比较好理解了