Spring框架(中) AOP
Spring(中) AOP
(一)代理模式
1.静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
举例:
package com.deserts.proxy.staticproxy;
/**
* @ClassName ProxyTest
* @Description TODO
* @Author deserts
* @Date 2020/10/16 12:12
*/
interface IShoeFactory{
void produce();
}
class ShoeFactory implements IShoeFactory{
@Override
public void produce() {
System.out.println("生产鞋子");
}
}
class ShoeProxy implements IShoeFactory{
private IShoeFactory shoeFactory;
public ShoeProxy (IShoeFactory shoeFactory){
this.shoeFactory = shoeFactory;
}
@Override
public void produce() {
shoeFactory.produce();
}
}
public class ProxyTest {
public static void main(String[] args) {
ShoeProxy proxy = new ShoeProxy(new ShoeFactory());
proxy.produce();
}
}
优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类;或者需要实现很多接口,代码混乱。
一旦接口增加方法,目标对象与代理对象都要维护。
2.JDK动态代理
基本介绍:
-
代理对象,不需要实现接口,但是目标对象要实现接口,否则不能用动态代理
-
代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象
-
动态代理也叫做:JDK代理、接口代理
举例:
package com.deserts.proxy.jdkproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @ClassName JDKProxyTest
* @Description TODO
* @Author deserts
* @Date 2020/10/16 12:23
*/
interface IShoeFactory{
void produce();
}
class ShoeFactory implements IShoeFactory{
@Override
public void produce() {
System.out.println("生产鞋子中");
}
}
class ShoeProxyFactory{
private Object target;
public ShoeProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnResult = method.invoke(target, args);
return returnResult;
}
});
}
}
public class JDKProxyTest {
public static void main(String[] args) {
IShoeFactory proxy = (IShoeFactory)new ShoeProxyFactory(new ShoeFactory()).getProxyInstance();
proxy.produce();
}
}
(二)AOP概述
1.一个动态代理的例子
1.1 要求
①执行加减乘除运算
②日志:在程序执行期间追踪正在发生的活动
1.2 实现
创建计算器接口,让计算器类去实现;创建动态代理的类,利用构造器或者方法传入计算器对象,创建获取被代理类的方法。
1.ICalculator接口
public interface ICalculator {
int add(int i, int j);
int sub(int i, int j);
int mul(int i, int j);
int div(int i, int j);
}
2.Calculator类
package com.deserts.aop.proxy;
/**
* @ClassName Calculator
* @Description TODO
* @Author deserts
* @Date 2020/10/15 19:23
*/
public class Calculator implements ICalculator{
@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;
}
}
3.代理类
package com.deserts.aop.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.function.Predicate;
/**
* @ClassName ProxyFactory
* @Description TODO
* @Author deserts
* @Date 2020/10/15 19:26
*/
public class ProxyFactory {
Object target;
public ProxyFactory(Object target){
this.target = target;
}
public Object getProxy(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnResult = null;
try {
Log.before(method.getName(), args);
returnResult = method.invoke(target, args);
Log.after(method.getName(), returnResult);
} catch (Exception e) {
Log.exceptionInfo(method.getName(), e);
} finally {
Log.end();
}
return returnResult;
}
});
}
}
4.封装日志功能,错误信息的类
package com.deserts.aop.proxy;
import java.util.Arrays;
/**
* @ClassName Log
* @Description TODO
* @Author deserts
* @Date 2020/10/15 19:54
*/
public class Log {
public static void before(String methodName, Object... args){
System.out.println(methodName + "即将执行,参数为:" + Arrays.asList(args));
}
public static void after(String methodName, Object result){
System.out.println(methodName + "执行结束,结果为:" + result);
}
public static void exceptionInfo(String methodName, Exception e){
System.out.println(methodName + "执行过程出现了异常,异常信息为:" + e.getCause());
}
public static void end(){
System.out.println("程序执行结束");
}
}
5.测试类
package com.deserts.aop.proxy;
/**
* @ClassName Client
* @Description TODO
* @Author deserts
* @Date 2020/10/15 19:34
*/
public class Client {
public static void main(String[] args) {
ICalculator proxy = (ICalculator)new ProxyFactory(new Calculator()).getProxy();
proxy.add(1,2);
proxy.div(2, 0);
}
}
2.AOP概述
-
AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。
- 面向对象 纵向继承机制
- 面向切面 横向抽取机制
-
AOP编程操作的主要对象是切面(aspect),而切面用于模块化横切关注点(公共功能)。
-
在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”
-
AOP的好处:
① 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
② 业务模块更简洁,只包含核心业务代码
③ AOP图解
3.AOP术语
3.1 横切关注点
从每个方法中抽取出来的同一类非核心业务。
3.2 切面(Aspect)
封装横切关注点信息的类,每个关注点体现为一个通知方法。
3.3 通知(Advice)
切面必须要完成的各个具体工作
3.4 目标(Target)
被通知的对象
3.5 代理(Proxy)
向目标对象应用通知之后创建的代理对象
3.6 连接点(Joinpoint)
横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
3.7 切入点(pointcut)
定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3.8 图解
4.AspectJ
4.1 简介
AspectJ:Java社区里最完整最流行的AOP框架。
在Spring2.0以上版本中,可以使用基于AspectJ注解或基于XML配置的AOP。
4.2 在Spring中启用AspectJ注解支持
1.导入jar包
- com.springsource.net.sf.cglib-2.2.0.jar
- com.springsource.org.aopalliance-1.0.0.jar
- com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
- spring-aop-4.0.0.RELEASE.jar
- spring-aspects-4.0.0.RELEASE.jar
2.引入AOP命名空间
3.配置扫描包,配置被代理的类和日志类
日志类需要@Aspect注解,@Before注解将方法配置为前置通知,需要指定value值,值为切入点表达式。同时需要@Component注解让Spring组件扫描去管理。
被代理类也需要@Component注解,让Spring去管理:
测试类使用时需要创建Spring容器去管理被配置好的类的对象:
运行结果:
4.3 用AspectJ注解声明切面
① @Before:前置通知,在方法执行之前执行
② @After:后置通知,在方法执行之后执行
③ @AfterRunning:返回通知,在方法返回结果之后执行
④ @AfterThrowing:异常通知,在方法抛出异常之后执行
⑥ @Around:环绕通知,围绕着方法执行
(三) AOP细节
1.细节一:容器创建的对象
IOC容器中保存的是组件的代理对象,可以理解成这个代理对象也去实现了目标对象所实现的接口
如:
运行结果:
可以看到,该对象是class com.sun.proxy.$Proxy7代理对象,底层原理是jdk动态代理。
除了实现接口的方式去代理,spring还提供了cglib代理,为没有接口的类进行代理。
如:
测试类:
运行结果:
可以看到cglib为我们创建了代理对象
2.细节二:切入点表达式
2.1 *:
1)匹配一个或多个字符,如:execution(public int com.deserts.aop.proxy.Calculator.*(int,int))
-
匹配任意类型的参数,如:execution(public int com.deserts.aop.proxy.Calculator.add(*,*))
-
只能匹配一层路径,如:execution(public int com.deserts.aop.*.Calculator.add(int,int))
4)权限类型前不能写,可以不写权限类型。public(可选类型)。
2.2 … :
- 匹配任意多个参数,如:execution(public int com.deserts.aop.proxy.Calculator.add(…))
2)匹配多层路径,如:execution(public int com.deserts…Calculator.*(int,int))
2.3 两种类型
最精确的:execution(public int com.deserts.aop.proxy.Calculator.add(int,int))
最模糊的:execution(*.*(…)) 不建议写,见方法就切
2.4 使用&&、||、!
在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
表达式 | 含义 |
---|---|
execution (* *.add(int,…)) || excution( *.sub(int, …)) | |
!execution (* *.add(int,…)) | 匹配不是任意类中第一个参数为int类型的add方法 |
3.细节三:通知方法的执行顺序
正常执行:@Before(前置通知) ==== @After(后置通知) ==== @AfterReturning(正常返回)
异常执行: @Before(前置通知) ==== @After(后置通知) ==== @AfterThrowing(方法异常)
4.细节四:JoinPoint获取获取目标方法的信息
主要通过传入通知方法的JoinPoint joinPoint参数,在方法内调用参数相关方法进行获取,如:
5.细节五:使用returning、throwing来接收返回值、异常
接收返回值:
接收异常信息:
6.细节六:Spring对通知方法的约束
1)spring对通知方法的没有太大约束,唯一的约束是参数列表的参数不能乱写,如果有未知的参数,需要告诉spring这个参数是用来做什么的,因为这些通知方法是spring所管理的,主要是通过反射来调用的。
2)如returning、throwing指定接收返回值、异常的参数,参数类型要尽量的大,如Object、Exception
7.细节七:抽取可重用的切入点表达式
-
抽取可重用的切入点表达式:
- 1.创建一个返回值为void,参数为空的空方法
- 2.给方法加上注解@PointCut,并在参数里写入要抽取的切入点表达式
- 3.将方法名放入要使用该表达式的值里面
-
示例:
8.细节八:环绕通知
- 环绕通知是通知里面最强大的,可以当作其它四个通知的整合。
- 对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。
- 在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行
- 注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。
示例:
9.细节九:环绕通知的执行顺序&抛出异常让其它通知感受到
测试:将四个普通通知和环绕通知都打开,运行查看结果:
通知顺序:
新的顺序:
让普通通知感受到异常:将该异常抛出去
10.细节十:多切面运行顺序
都是普通通知时,会根据切面类的名称的字母顺序,小的在外层,如LogUtil类在VaAspect类的外层,先进去后出来
若想改变切面运行顺序,可以加上@Order注解,如:
value值越小,优先级越高,也就是在最外层。
当有环绕通知时,环绕通知会先于该切面类的普通通知去执行,因为该环绕通知是加在这个切面类的。
如环绕通知加到LogUtils上:
AOP使用场景
1.日志记录
2.权限验证
3.安全检查
4.事务控制
(四)以xml方式配置切面
1.基于注解的AOP
2.基于配置的AOP
步骤
-
在IOC容器中创建管理目标类的bean和切面类的bean
-
配置切面类中的通知方法,配置切面时需要引用上面配置好的切面类
-
测试
3.注解和配置的选择
注解:快速方便
配置:功能完善
选择:重要的用配置,不重要的用注解
下一篇: Spring学习第二天