一 AOP的定义
OOP面向对象,允许开发者定义纵向的关系,但并适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
AOP(Aspect OrientedProgramming, 面向切面/方面编程) 旨在从业务逻辑中分离出来横切逻辑【eg:权限认证、日志、事务处理等】,提高模块化,即通过AOP解决代码耦合问题,让职责更加单一。
它是面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。
二 基本概念
Aspect(切面): 通常是一个类,里面可以定义切入点和增强
Joint point(连接点) :程序执行过程中明确的点,一般是方法的调用
Pointcut(切点) :表示一组 joint point,它定义了相应的 Advice 将要发生的地方。
Advice(增强) :AOP在特定的切入点上执行的增强处理,有before,after,afterReturning,afterThrowing,around
Target(目标对象) :织入 Advice 的目标对象.。
Weaving(织入) :将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
@Aspect ---------------------------这一个整体是切面
@Component
public class OperationAspect {
/**
* 后置返回通知
* 这里需要注意的是:
* 如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息
* 如果参数中的第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
* returning 限定了只有目标方法返回值与通知方法相应参数类型时才能执行后置返回通知,否则不执行,对于returning对应的通知方法参数为Object类型将匹配任何目标返回值
*/
@AfterReturning(pointcut = "within(com.example..*) && @annotation(operationLog)",//注意联调时修改包名!
returning = "result")
public void getOperationLog(JoinPoint jp, OperationLog operationLog, Object result) throws Exception {
Object getTargetResult = jp.getTarget();
Object getThisResult = jp.getThis();
System.out.println("目标对象:" + getTargetResult);
System.out.println("代理对象:" + getThisResult);
System.out.println("目标对象的类名:" + getTargetResult.getClass().getName());
System.out.println("代理对象的类名:" + getThisResult.getClass().getName());
}
}
用 AspectJ注解声明切面
• 要在 Spring 中声明 AspectJ 切面 , 只需要在 IOC 容器中将切面声明为 Bean 实例 (上例子中使用了@Component). 当在 SpringIOC 容器中初始化 AspectJ 切面之后 ,Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理 .
• 在 AspectJ 注解中 , 切面只是一个带有 @Aspect 注解的 Java 类 .
• 通知是标注有某种注解的简单的 Java 方法 .
• AspectJ 支持 5 种类型的通知注解 :
– @Before: 前置通知 , 在方法执行之前执行
– @After: 后置通知 , 在方法执行之后执行
– @ AfterRunning : 返回通知 , 在方法返回结果之后 执行
– @ AfterThrowing : 异常通知 , 在方法抛出异常之后
– @Around: 环绕通知 , 围绕着方法执行(可以在方法前,也可以在方法后)
三 AOP流行的框架
AOP现有两个主要的流行框架,即Spring AOP和Spring+AspectJ
详见链接
四 AOP的代理
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
名称 | 代理类型 | 基本原理 | 特性 |
---|---|---|---|
AspectJ | 静态代理------------ | 代理类在编译期间生成,但是在生成时将相应的切面织入到代理类中 | |
Cglib | 动态代理 | 代理类在运行时动态生成,底层使用字节码处理框架ASM,来转换字节码并生成新的类 | 弥补JDK动态代理只能代理接口的不足,cglib可以动态代理类 ,该类不能被final修饰 |
JDK动态代理 | 动态代理 | 代理类在运行时动态生成,源码级别会调用一个Native方法 | 只能代理接口 |
(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。(即:编译出来的class文件,字节码就已经被织入了)
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
① JDK动态代理只提供接口的代理,不支持类的代理。 核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
②如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
静态代理与动态代理区别
在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
五 静态代理vs动态代理
代理模式作为23种经典设计模式之一,其比较官方的定义为“为其他对象提供一种代理以控制对这个对象的访问”
,简单点说就是,之前A类自己做一件事,在使用代理之后,A类不直接去做,而是由A类的代理类B来去做。代理类其实是在之前类的基础上做了一层封装。java中有静态代理、JDK动态代理、CGLib动态代理的方式。静态代理指的是代理类是在编译期就存在的,相反动态代理则是在程序运行期动态生成的
首先需要定义一个接口(UserInterface ),被代理类(User)需要实现这个接口。
一、接口
public interface UserInterface {
void sayHello(String str);
void sayBye();
String test(String str);
}
二、被代理类
public class User implements UserInterface{
public void sayHello(String str){
System.out.println("sayHello方法:"+str);
}
public void sayBye(){
System.out.println("sayBye方法");
}
public String test(String str){
System.out.println("test方法");
return str;
}
}
5.1 静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(目标对象)与代理对象(Proxy)一起实现相同的接口或者是继承相同父类。
public class UserStaticProxy implements UserInterface {
private User user = new User();
public void sayHello(String str){
System.out.println("---------静态代理--------");
user.sayHello(str);
}
public void sayBye(){
System.out.println("---------静态代理--------");
user.sayBye();
}
public String test(String str){
System.out.println("---------静态代理--------");
return user.test(str);
}
}
/******************************controller使用**********************************/
@Test
public void staticProxyTest() {
UserStaticProxy userStaticProxy = new UserStaticProxy();
userStaticProxy.sayHello("hofdah");
userStaticProxy.sayBye();
userStaticProxy.test("test");
}
/******************************调用结果**********************************/
---------静态代理--------
sayHello方法:hofdah
---------静态代理--------
sayBye方法
---------静态代理--------
test方法
缺点:
- 代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多。
- 一旦接口增加方法,目标对象与代理对象都要维护。
5.2 jdk动态代理
在使用jdk动态代理类时,代理类必须实现InvocationHandler接口
5.2.1 步骤
1、创建被代理的类以及接口
2、创建代理类:实现接口InvocationHandler,同时必须重写invoke方法(在此实现增强功能,并利用反射调用被代理类的方法)
3、通过Proxy的静态方法newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)创建一个代理
4.、通过代理调用方法
5.2.2 demo
public class UserJdkProxy implements InvocationHandler{
private User user;
public UserJdkProxy(User user) {
this.user = user;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable{
System.out.println("-------jdk动态代理begin ");
Object result = method.invoke(user,args);
System.out.println(" jdk动态代理end-------");
return result;
}
}
/******************************controller使用**********************************/
@Test
public void jdkProxyTest() {
User user = new User();
InvocationHandler userJdkProxy = new UserJdkProxy(user);
UserInterface proxy = (UserInterface) Proxy.newProxyInstance(user.getClass().getClassLoader(),
user.getClass().getInterfaces(),
userJdkProxy);
proxy.sayHello("hofdah");
proxy.sayBye();
proxy.test("test");
System.out.println("代理对象类名:"+proxy.getClass().getName());
}
/******************************调用结果**********************************/
-------jdk动态代理begin
sayHello方法:hofdah
jdk动态代理end-------
-------jdk动态代理begin
sayBye方法
jdk动态代理end-------
-------jdk动态代理begin
test方法
jdk动态代理end-------
代理对象类名:com.sun.proxy.$Proxy10
关于代理对象类名:详见5.2.3
5.2.3 主要方法
Java动态代理类位于java.lang.reflect包下,一般主要涉及到以下两个类:
(1) Interface——InvocationHandler
InvocationHandler 是一个接口,官方文档解释说,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。
该接口中仅定义了一个方法:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
obj:把代理对象自己传递进来
method:把代理对象当前调用的方法传递进来
args:把方法参数传递进来
InvocationHandler 内部只有一个 invoke() 方法,正是这个方法决定了怎么样处理代理传递过来的方法调用。
原因:Proxy 动态产生的代理会调用 InvocationHandler 实现类,所以 InvocationHandler 是实际执行者。
(2) Class——Proxy:该类即为动态代理类,其中主要包含以下内容:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
loader:代表与目标对象相同的类加载器-------目标对象.getClass().getClassLoader()
interfaces:代表与目标对象实现的所有的接口字节码对象数组----数组因为目标类可以有多个接口
h:具体的代理的操作,InvocationHandler接口
返回值:Object,即代理对象
关于前文的代理对象类名:com.sun.proxy.$Proxy10
它是 System.out.println("代理对象类名:"+proxy.getClass().getName());
这条语句打印出来的,那么为什么我们返回的这个代理对象的类名是这样的呢?
UserInterface proxy = (UserInterface) Proxy.newProxyInstance(user.getClass().getClassLoader(),
user.getClass().getInterfaces(),
userJdkProxy);
首先我们解释一下为什么我们这里可以将其转化为UserInterface 类型的对象?原因就是在newProxyInstance这个方法的第二个参数上,我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是UserInterface 类型,所以就可以将其转化为UserInterface 类型了。
同时我们一定要记住,通过 Proxy.newProxyInstance 创建的代理对象是在jvm运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行时动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy为中,最后一个数字表示对象的标号。
5.3 cglib动态代理
对于没有实现接口的目标类,就需要CGLIB。
原理:为目标类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。采用继承,不能对final修饰的类进行代理
使用CGLIB需要实现MethodInterceptor接口,并重写intercept方法,在该方法中对原始要执行的方法前后做增强处理。该类的代理对象可以使用代码中的字节码增强器来获取。
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK要合适一些。
CGLIB采用动态创建子类的方法,对于final修饰的方法无法进行代理。
六 aop的一些坑
6.1 定义切点
@AfterReturning(pointcut = "within(com.example..*) && @annotation(operationLog)&& @annotation(getMapping)",//注意联调时修改包名!
returning = "result")
public void getOperationLog(JoinPoint jp, OperationLog operationLog, GetMapping getMapping,Object result) throws Exception {
其中,每次aop只能对第一个被@annotation修饰的注解切入。
如上例中,永远院无法拦截到getMapping
如果要拦截多个注解下的方法,则需要分别定义切点!
6.2 反射使用 method.invoke(Object obj, Object… args)
code逻辑:下面代码对返回结果进行判断。如果返回失败的结果,则会再一次进行方法的调用。
//判断是否操作成功
if (!baseResult.getCode().equals("0")) {
//获取目标方法的参数信息
Object[] args = jp.getArgs();
Method method1 = ((MethodSignature) jp.getSignature()).getMethod();
method1.invoke(args);
}
会报错! java.lang.IllegalArgumentException: object is not an instance of declaring class
翻译过来就是对象不是声明类的一个实例
java.lang.IllegalArgumentException: object is not an instance of declaring class
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_151]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_151]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
at com.example.worktest.aop.common.OperationAspect.getOperationLog(OperationAspect.java:82) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_151]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_151]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:626) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.aspectj.AspectJAfterReturningAdvice.afterReturning(AspectJAfterReturningAdvice.java:66) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:56) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) [spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
这是java反射的时候报错。
method1.invoke(args);
这一行报错。
原因:没有实例化一个类。
解决方法:通过反射获取有参方法时,getMethod的参数不仅要有方法名,还要有方法参数类型的class对象,因为java函数有重载,不能通过只一个方法名来确定一个方法。
应该传入目标类的对象
method1.invoke(jp.getTarget(), args);
method.invoke(Object obj, Object… args)的源码见下:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
入参:
obj - 从中调用底层方法的对象(简单的说就是调用谁的方法用谁的对象)
args - 用于方法调用的参数
如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null。
如果底层方法所需的形参数为 0,则所提供的 args 数组长度可以为 0 或 null。
如果底层方法是静态的,并且尚未初始化声明此方法的类,则会将其初始化。
如果方法正常完成,则将该方法返回的值返回给调用者;如果该值为基本类型,则首先适当地将其包装在对象中。但是,如果该值的类型为一组基本类型,则数组元素不 被包装在对象中;换句话说,将返回基本类型的数组。如果底层方法返回类型为 void,则该调用返回 null。
因此上述异常出现的解决方法如下:
- 将获取的方法改为静态的。(aop切面无法对static方法进行切面)
- 传入类对象obj(推荐)。即在执行方法前先实例化类。method.invoke(args)改为method.invoke(c.newInstance(),args)或者method.invoke(jp.getTarget().getClass().newInstance(),args)
Java中newInstance()和new()
在Java开发特别是数据库开发中,经常会用到Class.forName( )这个方法。通过查询Java Documentation我们会发现使用Class.forName( )静态方法的目的是为了动态加载类。**在加载完成后,一般还要调用Class下的newInstance( )静态方法来实例化对象以便操作。**因此,单单使用Class.forName( )是动态加载类是没有用的,其最终目的是为了实例化对象。
这里有必要提一下就是Class下的newInstance()和new有什么区别?,首先,newInstance( )是一个方法,而new是一个关键字,其次,Class下的newInstance()的使用有局限,因为它生成对象只能调用无参的构造函数,而使用new关键字生成对象没有这个限制。
好,到此为止,我们总结如下:
Class.forName("")返回的是类
Class.forName("").newInstance()返回的是object
最后用最简单的描述来区分new关键字和newInstance()方法的区别:
newInstance()和new()区别:
1、两者创建对象的方式不同,前者是实用类的加载机制,后者则是直接创建一个类:
2、newInstance创建类是这个类必须已经加载过且已经连接,new创建类是则不需要这个类加载过
3、newInstance: 弱类型(GC是回收对象的限制条件很低,容易被回收)、低效率、只能调用无参构造,new 强类型(GC不会自动回收,只有所有的指向对象的引用被移除是才会被回收,若对象生命周期已经结束,但引用没有被移除,经常会出现内存溢出)