1.AOP编程
1.1. AOP概念
1.3个概念:
1.AOP (Aspect Oriented Programing):面向切面编程 = Spring动态代理开发。
以切面为基本单位的程序开发,通过切面间的彼此协同,相互调用,完成程序的构建。
切面 = 切入点 + 额外功能。
2.OOP (Object Oritened Programing):面向对象编程 Java。
以对象为基本单位的程序开发,通过对象间的彼此协同,相互调用,完成程序的构建。
2.POP (Producer Oriented Programing):面向过程(方法、函数)编程 C 。
以过程为基本单位的程序开发,通过过程间的彼此协同,相互调用,完成程序的构建。
2.AOP的概念
本质就是Spring动态代理开发,通过代理类为原始类增加额外功能。
好处:利于原始类呃维护。
注意:AOP编程不可能取代OOP,是OOP编程的有益补充。
1.2.AOP编程的开发步骤
1.原始对象
2.额外功能:实现MethodInterceptor接口
3.定义切入点:要添加额外功能之处。
4.组装切面:额外功能+切入点。
1.3.切面的名词解释
1.为什么要将切入点
+额外功能
的组合成为切面呢?
几何学:
- 面是由线构成,线是由点构成。所以也可以说面是由点构成的,而且这些点必须是性质相同的点。
切面也是由相同的点构成的:
- 即切入点,这些切入点具有相同性质,这些相同的性质就是额外功能。所以切入点+额外功能=切面。
比如有3个Service中,都有一个方法需要加上事务的功能,那么每个Servcie中的这个方法加上事务功能,就构成了一个点。三个Service中的点,就构成了一个切面,切在了整个Service层中。
2.AOP底层实现
即动态代理的底层实现原理。
2.1.核心问题
1.AOP如何创建动态代理类(动态字节码技术)
2.Spring工厂如何加工创建代理对象:通过原始对象的id值,获得的是代理对象。
2.2.JDK动态代理
Spring在创建动态代理对象的过程中,有两种创建方式:
JDK的动态代理
第三方Cglib框架创建动态代理。
1.类加载器的作用:在对象的创建过程中尤为重要
- 1.通过类加载器将对象的字节码文件加载到JVM中。
- 2.通过类加载器创建类的Class对象,进而创建这个类的对象:
User.class --> User类的Class对象 —> new User() —> user - 加载字节码,创建Class字节码对象。
2.如何获得类加载器。
虚拟机会为每一个类的.class文件,自动分配与之对应的ClassLoader。
3.类加载器在类加载器中的作用。
-
在动态代理中要先获得动态代理类,再创建代理对象。但是我们根本没有写
.java
源文件,所以也就没有编译后的.class
文件。那么动态代理是怎么获得这个动态代理类对应的.class
文件,进而创建对象的呢? -
动态代理是通过动态字节码技术来创建字节码的,没有
.java
源文件和.class
文件,是动态字节码技术创建字节码的,直接将字节码写在了虚拟机中。所以不是类加载器加载.class
文件到虚拟机中,没有加载.class
字节码这一过程。 -
那动态字节码技术又是怎么实现的呢?对应的API就是(Proxy类的API):生成了动态代理的字节码直接写到了虚拟机中,进而获得动态代理类的字节码(Class)对象,进而再调用构造。
//1.得到代理类Proxy的Class类对象:有InvocationHandle属性。Proxy类中有这个属性 Class<?> proxyClass = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces()); //2.得到参数是InvocationHandle的构造器 Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class); //3. 根据构造器得到代理对象:有参构造 Object proxy = constructor.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //4.增强该类的所有方法 System.out.println("打印日志..."); Object result = method.invoke(target, args); System.out.println("结束打印..."); return result; } }); return proxy;
//或者直接:返回指定接口的代理类的实例 newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
-
注意Proxy类的API:生成了动态代理的字节码直接写到了虚拟机中;之后要获得动态代理类的字节码(Class)对象时就要类加载器的帮助了。但是这里就出现了麻烦的地方,因为以前创建对象时,加载
.class
文件到JVM中,是JVM自动为每个.class
文件分配一个类加载器,可是次数没有加载.class
字节码这一步,也就JVM没有为我们自动分配类加载器,所以类加载就要我们指定了。 -
我们需要类加载器帮我们创建动态代理类的Class对象,可是动态代理类没有对应的
.class
文件,虚拟机也就不会为其分配类加载器。但是我又需要,那么借用一个ClassLoader。所以第一个参数,就是我们要借用一个ClassLoader。这个ClassLoader的目的就是完成代理类的Class对象创建。//或者直接:返回指定接口的代理类的实例 newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h)
-
所以ClassLoader参数的作用:就是帮我们创建动态字节码技术生成的字节码(动态代理类)的Class对象,进而创建代理对象。
-
借谁的,都可以,只要这个类有
.class
文件。一般用的都是目标类的类加载器。 -
JDK8之前,内部类访问外部变量,外部变量要定义为final。
2.3.CGlib动态代理
也是Spring底层默认支持的一种动态代理方式,也是动态代理。
1.JDK的动态代理中,要求原对象和代理对象必须实现相同的接口。
2.原因:
- 这样目标对象和代理对象都有接口中的方法,而且可以合接口形成多态,向调用者屏蔽具体的实现细节,迷惑调用者(在调用者看来还是调用目标类中的原方法)。
- 而且我可以在代理类中提供对应方法新的实现:加入新的功能,同时调用对应原始方法。
3.如果有一个原始类,没有实现任何接口,我想为它创建代理对象该怎么做呢?
- CGlib创建的代理类,继承原始类。那么代理类就继承了所有原始类的方法,就可以对这些方法进行代理扩展了,并且用过
super.method
调用原始方法。
4.CGlib创建动态代理的原理:父子继承关系创建代理对象,原始类作为父类,代理类作为子类,这样即可以保证2者方法一致,同时在代理类中提供新的实现。(额外功能+原始方法)
5.代码:
- 通过CGlib创建动态代理:Spring默认引入了CGlib的jar包
- 核心类:Enhancer.create() —> 创建代理对象
- 核心三要素:父类,类加载,增强方法
/* 2 通过cglib方式创建动态代理对象 Proxy.newProxyInstance(classloader,interface,invocationhandler) Enhancer.setClassLoader() Enhancer.setSuperClass() Enhancer.setCallback(); ---> MethodInterceptor(cglib包的) Enhancer.create() ---> 代理 */ //目标类没有实现接口 public class UserService { public void login(){ System.out.println("login...."); } public void register(){ System.out.println("register..."); } } public class TestCGlib { public static void main(String[] args) { UserService userService = new UserService(); /** * 通过CGlib创建动态代理:Spring默认引入了CGlib的jar包 * 核心类:Enhancer.create() ---> 创建代理对象 */ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setClassLoader(UserService.class.getClassLoader()); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("前-----CGlib增强...."); Object result = method.invoke(userService, args); System.out.println("后-----CGlib增强...."); return result; } }); UserService userServiceProxy = (UserService)enhancer.create(); userServiceProxy.login(); userServiceProxy.register(); } }
2.4.小结
1. JDK动态代理 Proxy.newProxyInstance() 通过接口创建代理的实现类
2. Cglib动态代理 Enhancer 通过继承父类创建的代理类
3.再看BeanPostProcessor
Spring创建的每一个bean对象,都会调用BeanPostProcessor
实现类中重写的方法:postProcessAfterInitialization
,postProcessBeforeInitialization
这两个方法
3.1回顾BeanPostProcessor
注意1:无论我们有没有实现BeanPostProcessor接口,重写postProcessAfterInitialization
,postProcessBeforeInitialization
这两个方法。Spring工厂再创建完一个bean对象后都会调用这两个方法。如果我们没有重写这个两个方法,也不用担心会调用到空方法。因为这两个方法在接口中是default
修饰,有默认的实现:即不作任何加工,返回原对象。
注意2:BeanPostProcessor的两个方法:postProcessAfterInitialization
,postProcessBeforeInitialization
的含义:
- 1.
postProcessBeforeInitialization
:初始化操作之前的后置处理 - 2.
postProcessAfterInitialization
:初始化操作之后的后置处理 - 3.参数含义:
参数1Object bean
是Spring工厂创建对象;
参数2String beanName
是bean的id值。
注意3:实际开发中初始化操作很少做,一般都是靠后置Bean来完成对象的再加工。既然初始化很少做,所以postProcessBeforeInitialization
一般也不做加工,直接返回源对象即可。具体的加工逻辑在postProcessAfterInitialization
中实现。
注意4:无论做不做加工postProcessAfterInitialization
,postProcessBeforeInitialization
这两个方法都要返回原对象。那么有非常多的Bean会调用这两个方法,很可能会报强制类型转换的错误。所以每个类如果要进行后置处理的话,可以其单独建一个bean后置处理类,或者进行 bean instanceof Object
判断。
3.2Spring动态代理是结合BeanPostProcessor完成动态代理创建
1.为什么通过原始对象的id值,直接获得代理对象?
就是因为BeanPostProcessor的加工,得到代理对象返回给调用者。
2.具体步骤图示:
3.3模拟Spring动态代理的编码演示
1.得到原始对象
public interface UserService {
void login(User user);
void register(String user, String password);
}
public class UserServiceImpl implements UserService {
@Override
public void login(User user) {
System.out.println("UserServiceImpl----login");
}
@Override
public void register(String user, String password) {
System.out.println("UserServiceImpl----register");
}
}
<bean id="userService" class="com.txl.factory.UserServiceImpl"/>
2.实现BeanPostProcessor进行加工
public class ProxyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Object beanProxy = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理功能---log---Before----");
Object result = method.invoke(bean, args);
System.out.println("代理功能---log---After----");
return result;
}
});
return beanProxy;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
<bean id="proxyBeanPostProcessor" class="com.txl.factory.ProxyBeanPostProcessor"/>
3.配置文件中对BeanPostProcessor进行配置
因为pring工厂在创建完一个bean对象后都会调用这两个方法,而且接口也有默认的实现,所以不用整合,直接配一下实现类的Bean即可。
而如果是通过MethodInterceptor来实现额外功能的话,要为这个其指定切入点。即要整合。
<bean id="proxyBeanPostProcessor" class="com.txl.factory.ProxyBeanPostProcessor"/>
原始对象创建好后,就会调用postProcessAfterInitialization
,postProcessBeforeInitialization
这两个方法,将加工结果(代理对象)返回给我们。所以我们根据原始对象的id获取原始对象时,得到的是代理对象。
4.测试代码:通过原始对象id获得代理对象。
public class Test {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/applicationContext2.xml");
UserService userService = (UserService)applicationContext.getBean("userService");
userService.login(new User());
userService.register("txl", "123456");
}
}
//结果:
代理功能---log---Before----
UserServiceImpl----login
代理功能---log---After----
代理功能---log---Before----
UserServiceImpl----register
代理功能---log---After----
4.基于注解的AOP编程
无论是不是基于注解:核心还是AOP编程或者说Spring的动态代理开发。
1.原始对象
2.额外功能:一般实现MethodInterceptor接口
3.切入点
4.组装切面
4.1.回顾一下之前的非注解的开发步骤
-
1.原始对象
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register:--->业务运算+DAO调用"); } @Override public boolean login(String name, String password) { System.out.println("UserServiceImpl.login:--->业务运算+DAO调用"); return true; } }
-
2.注册原始对象bean
<!--目标类--> <bean id="userService" class="com.txl.UserServiceImpl"></bean>
-
3.继承MethodInterceptor接口方法拦截器,实现额外功能的类
其中methodInvocation作为原始方法,切入点。public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { if(methodInvocation.getMethod().getName() == "login"){ System.out.println("before-login-proxy-log-------"); boolean flag = (boolean)methodInvocation.proceed(); System.out.println("after-----------"); return flag; } Object obj = methodInvocation.proceed(); return obj;//注意返回值的匹配:如果返回null只能匹配void类型的原始方法 } }
-
4.注册提供额外功能的类:MethodInterceptor
<!--提供额外功能的类:MethodInterceptor--> <bean id="around" class="com.txl.dynamticproxy.Around" />
-
5.整合提供额外功能类(方法拦截器)和切面(原始方法)
<aop:config> <!--配置切入点--> <aop:pointcut id="pc" expression="execution(* *(..))"/> <!--将切入点和额外功能进行整合--> <aop:advisor advice-ref="around" pointcut-ref="pc"/> </aop:config>
-
6.测试代码
@org.junit.Test public void test2(){ ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml"); //Spring规定,Spring工厂通过原始对象id值获得的是代理对象。 UserService userService = (UserService)context.getBean("userService"); userService.register(new User()); userService.login("txl", "123456"); }
4.2.基于注解的AOP编程的开发步骤
1.原始对象
2.额外功能:一般实现MethodInterceptor接口
3.切入点
4.组装切面
-
1.原始对象
public class UserServiceImpl implements UserService { @Override public void register(User user) { System.out.println("UserServiceImpl.register:--->业务运算+DAO调用"); } @Override public boolean login(String name, String password) { System.out.println("UserServiceImpl.login:--->业务运算+DAO调用"); return true; } }
-
2.注册原始对象bean
<!--目标类--> <bean id="userService" class="com.txl.UserServiceImpl"></bean>
-
3.定义切面类:额外功能+切入点
加上@Aspect注解,表明这个类是切面类。@Aspect public class MyAspect { /** * 1.自己定义一个方法:作为额外功能,但是这个方法要加上@Around()注解; * 2.ProceedingJoinPoint joinPoint:代表原始方法; * 3.@Around() 注解中定义切入点: * @param joinPoint * @return */ @Around("execution(* login(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("额外功能--------"); Object result = joinPoint.proceed(); return result; } }
-
4.配置文件中配置切面类:
<!--2.配置切面类--> <bean id="around" class="com.txl.aspect.MyAspect"/>
-
5.告诉Spring开启基于注解的AOP编程
<!--3.告诉Spring开启基于注解的AOP编程--> <aop:aspectj-autoproxy/>
-
6.核心:切面类
1.作为切面类:@Aspect
2.定义了额外功能:@Around
3.定义了切入点:@Around(“execution(* login(…))”)@Aspect public class MyAspect { /** * 自己定义一个方法:作为额外功能,但是这个方法要加上@Around()注解; * ProceedingJoinPoint joinPoint:代表原始方法; * @Around() 注解中定义切入点: * @param joinPoint * @return */ @Around("execution(* login(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("额外功能--------"); Object result = joinPoint.proceed(); return result; } }
-
7.结合配置
<!--1.原始对象--> <bean id="userService" class="com.txl.aspect.UserServiceImpl"/> <!--2.配置切面类--> <bean id="around" class="com.txl.aspect.MyAspect"/> <!--3.告诉Spring开启基于注解的AOP编程--> <aop:aspectj-autoproxy/>
4.3.切入点复用
将切入点的配置@Around("execution(* login(..))")
,抽取到一个独立的函数上。
1.如果要为login方法加两个额外的功能:日志和事务。那么要分别写两个额外功能函数,而且这两个额外功能都要配置同一个切点login:@Around("execution(* login(..))")
@Aspect
public class MyAspect {
@Around("execution(* login(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:日志--------");
Object result = joinPoint.proceed();
return result;
}
@Around("execution(* login(..))")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:事务--------");
Object result = joinPoint.proceed();
return result;
}
}
2.发现切入点表达式冗余:进行抽取复用
抽取出来一个独立的方法,配合@Around(value = "myPointcut()")
进行复用。
@Aspect
public class MyAspect {
@Pointcut("execution(* login(..))")
public void myPointcut(){
}
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:日志--------");
Object result = joinPoint.proceed();
return result;
}
@Around(value = "myPointcut()")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:事务--------");
Object result = joinPoint.proceed();
return result;
}
}
4.4.基于注解的动态代理的实现切换
1.默认是JDK的动态代理。
<aop:aspectj-autoproxy proxy-target-class="false"/>
2.切换成CGlib的动态代理实现。
<aop:aspectj-autoproxy proxy-target-class="true"/>
4.5.传统的AOP编程的动态代理的实现切换
1.默认也是JDK的动态代理实现
<aop:config proxy-target-class="false">
<!--配置切入点-->
<aop:pointcut id="pc" expression="execution(* com.txl.UserServiceImpl.login(String,String))"/>
<!--将切入点和额外功能进行整合-->
<aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>
2.切换成CGlib实现
<aop:config proxy-target-class="true">
<!--配置切入点-->
<aop:pointcut id="pc" expression="execution(* com.txl.UserServiceImpl.login(String,String))"/>
<!--将切入点和额外功能进行整合-->
<aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>
5.AOP开发中的一个坑
5.1复现bug
1.可能存在这样的情况:同一个类中的方法存在相互调用,比如UserServiceImpl中的login方法调用register方法(没有意义,不推荐,只是举例):
2.为UserServiceImpl的所有方法都加上日志和事务的额外功能。
execution(* *..UserServiceImpl.*(..)))
@Aspect
public class MyAspect {
@Pointcut("execution(* *..UserServiceImpl.*(..)))")
public void myPointcut(){
}
@Around(value = "myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:日志--------");
Object result = joinPoint.proceed();
return result;
}
@Around(value = "myPointcut()")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("额外功能:事务--------");
Object result = joinPoint.proceed();
return result;
}
}
3.测试:
预期:得到代理对象,调用login方法时,会加上日志和事务的功能;同时login方法调用register方法时,也会为register方法加上日志和事务功能。
结果:login加上了额外功能,register没有加上额外功能。
4.坑:在同一个业务类中,进行业务方法间的相互调用,只有最外层的方法,才是加入了额外功能(内部的方法,通过普通的方式调用,都调用的是原始方法)。如果想让内层的方法也调用代理对象的方法,就要AppicationContextAware获得工厂,进而获得代理对象。
5.原因:额外功能都是在原方法的基础上加的,无论怎么样添加额外功能,都会调用原方法。那么是谁调用原方法?原对象,不是代理对象!
所以在原对象调用login方法的前后
,加上了额外功能。但是原对象调用login方法时,login方法内部又调用了register方法时,register方法被包含在了login方法中,额外功能保住了login,所以register就没有额外功能。
5.2.解决
1.怎么达到预期:
- 最简单的想法:创建工厂获得代理对象,代替原对象this调用register。
- 但是这样显然是不合理的:Spring的工厂是一个重量资源,尽量只创建一次。此时测试类中和Service都创建了工厂。
2.拿到测试类的工厂
:实现ApplicationContextAware
接口为当前类添加一个工厂属性。
-
注意:工厂被创建出来后,在项目运行期间不会销毁。而
ApplicationContextAware
接口中的setApplicationContext
方法,会将项目运行的工厂对象赋值给当前的类中的工厂属性。 -
源码注释:Set the ApplicationContext that this object runs in.
public class UserServiceImpl implements UserService, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void login(User user) {
System.out.println("UserServiceImpl----login");
UserService userService = (UserService)applicationContext.getBean("userService");
userService.register("ttxxll", "123456");
//this.register("ttxxll", "123456");
}
@Override
public void register(String user, String password) {
System.out.println("UserServiceImpl----register");
}
}
3.测试结果