一、AOP是什么?AOP与拦截器的区别?
太抽象的不说,如果你知道Struts2的拦截器,拦截器就是应用的AOP的思想,它用于拦截Action以进行一些预处理或结果处理。而Spring的AOP是一种更通用的模式,可以拦截Spring管理的Bean,功能更强大,适用范围也更广,它是通过动态代理与反射机制实现的。
二、AOP一些概念
1.通知(Advice)
通知定义了在切入点代码执行时间点附近需要做的工作。
Spring支持五种类型的通知:
Before(前) org.apringframework.aop.MethodBeforeAdvice
After-returning(返回后) org.springframework.aop.AfterReturningAdvice
After-throwing(抛出后) org.springframework.aop.ThrowsAdvice
Arround(周围) org.aopaliance.intercept.MethodInterceptor
Introduction(引入) org.springframework.aop.IntroductionInterceptor
2.连接点(Joinpoint)
程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法调用时、异常抛出时、方法返回后等等。
3.切入点(Pointcut)
通知定义了切面要发生的“故事”,连接点定义了“故事”发生的时机,那么切入点就定义了“故事”发生的地点,例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定。
4.切面(Aspect)
通知、连接点、切入点共同组成了切面:时间、地点和要发生的“故事”。
5.引入(Introduction)
引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。
6.目标(Target)
即被通知的对象,如果没有AOP,那么通知的逻辑就要写在目标对象中,有了AOP之后它可以只关注自己要做的事,解耦合!
7.代理(proxy)
应用通知的对象,详细内容参见设计模式里面的动态代理模式。
8.织入(Weaving)
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器;
(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;
(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术。
三、使用AOP的方式
1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面
3.纯POJO切面
4.注入式AspectJ切面
四、如何实现AOP
1.代理方式实现AOP
(1)可睡觉的接口,任何可以睡觉的人或机器都可以实现它。
public interface Sleepable {
public void sleep();
}
(2)接口实现类,“Me”可以睡觉,“Me”就实现可以睡觉的接口。
public class Me implements Sleepable{
public void sleep() {
System.out.println("\n睡觉!不休息哪里有力气学习!\n");
}
}
(3)Me关注于睡觉的逻辑,但是睡觉需要其他功能辅助,比如睡前脱衣服,起床脱衣服,这里开始就需要AOP替“Me”完成!解耦!首先需要一个SleepHelper类。因为一个是切入点前执行、一个是切入点之后执行,所以实现对应接口。
public class SleepHelper implements MethodBeforeAdvice, AfterReturningAdvice {
public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable {
System.out.println("睡觉前要脱衣服!");
}
public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable {
System.out.println("起床后要穿衣服!");
}
}
(4)最关键的来了,Spring核心配置文件application.xml配置AOP。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
<span style="white-space:pre"> </span>xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<span style="white-space:pre"> </span>xmlns:aop="http://www.springframework.org/schema/aop"
<span style="white-space:pre"> </span>xsi:schemaLocation="http://www.springframework.org/schema/beans
<span style="white-space:pre"> </span>http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
<span style="white-space:pre"> </span>http://www.springframework.org/schema/aop
<span style="white-space:pre"> </span>http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
<!-- 定义被代理者 -->
<bean id="me" class="com.springAOP.bean.Me"></bean>
<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
<bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>
<!-- 定义切入点位置 -->
<bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="pattern" value=".*sleep"></property>
</bean>
<!-- 使切入点与通知相关联,完成切面配置 -->
<bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="sleepHelper"></property>
<property name="pointcut" ref="sleepPointcut"></property>
</bean>
<!-- 设置代理 -->
<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- 代理的对象,有睡觉能力 -->
<property name="target" ref="me"></property>
<!-- 使用切面 -->
<property name="interceptorNames" value="sleepHelperAdvisor"></property>
<!-- 代理接口,睡觉接口 -->
<property name="proxyInterfaces" value="com.springAOP.bean.Sleepable"></property>
</bean>
</beans>
其中:
<beans>是Spring的配置标签,beans里面几个重要的属性:
xmlns:
是默认的xml文档解析格式,即spring的beans。地址是http://www.springframework.org/schema/beans;通过设置这个属性,所有在beans里面声明的属性,可以直接通过<>来使用,比如<bean>等等。一个XML文件,只能声明一个默认的语义解析的规范。例如上面的xml中就只有beans一个是默认的,其他的都需要通过特定的标签来使用,比如aop,它自己有很多的属性,如果要使用,前面就必须加上aop:xxx才可以。类似的,如果默认的xmlns配置的是aop相关的语义解析规范,那么在xml中就可以直接写config这种标签了。
xmlns:xsi:
是xml需要遵守的规范,通过URL可以看到,是w3的统一规范,后面通过xsi:schemaLocation来定位所有的解析文件。
xmlns:aop:
这个是重点,是我们这里需要使用到的一些语义规范,与面向切面AOP相关。
xmlns:tx:
Spring中与事务相关的配置内容。
(5)测试类,Test,其中,通过AOP代理的方式执行Me的sleep()方法,会把执行前、执行后的操作执行,实现了AOP的效果!
public class Test {
public static void main(String[] args){
@SuppressWarnings("resource")
//如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式
//ApplicationContext appCtx = new ClassPathXmlApplicationContext("application.xml");
ApplicationContext appCtx = new FileSystemXmlApplicationContext("application.xml");
Sleepable me = (Sleepable)appCtx.getBean("proxy");
me.sleep();
}
}
执行结果:
(6)通过org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator简化配置。
将配置文件中设置代理的代码去掉,加上:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
然后,在Test中,直接获取me对象,执行sleep方法,就可以实现同样的功能!
通过自动匹配,切面会自动匹配符合切入点的bean,会被自动代理,实现功能!
————————————————
2.更简单的方式,通过AspectJ提供的注解实现AOP
1)同样的例子,修改后的SleepHelper:
@Aspect
public class SleepHelper{
public SleepHelper(){
}
@Pointcut("execution(* *.sleep())")
public void sleeppoint(){}
@Before("sleeppoint()")
public void beforeSleep(){
System.out.println("睡觉前要脱衣服!");
}
@AfterReturning("sleeppoint()")
public void afterSleep(){
System.out.println("睡醒了要穿衣服!");
}
}
(2)在方法中,可以加上JoinPoint参数以进行相关操作,如:
//当抛出异常时被调用
public void doThrowing(JoinPoint point, Throwable ex)
{
System.out.println("doThrowing::method "
+ point.getTarget().getClass().getName() + "."
+ point.getSignature().getName() + " throw exception");
System.out.println(ex.getMessage());
}
(3)然后修改配置为:
<aop:aspectj-autoproxy />
<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
<bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>
<!-- 定义被代理者 -->
<bean id="me" class="com.springAOP.bean.Me"></bean>
(4)最后测试,一样的结果!
public class Test {
public static void main(String[] args){
@SuppressWarnings("resource")
//如果是web项目,则使用注释的代码加载配置文件,这里是一般的Java项目,所以使用下面的方式
//ApplicationContext appCtx = new ClassPathXmlApplicationContext("application.xml");
ApplicationContext appCtx = new FileSystemXmlApplicationContext("application.xml");
Sleepable me = (Sleepable)appCtx.getBean("me");
me.sleep();
}
}
3.使用Spring来定义纯粹的POJO切面(名字很绕口,其实就是纯粹通过<aop:fonfig>标签配置,也是一种比较简单的方式)
1)修改后的SleepHelper类,很正常的类,所以这种方式的优点就是在代码中不体现任何AOP相关配置,纯粹使用xml配置。
public class SleepHelper{
public void beforeSleep(){
System.out.println("睡觉前要脱衣服!");
}
public void afterSleep(){
System.out.println("睡醒了要穿衣服!");
}
}
(2)配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd ">
<!-- 定义通知内容,也就是切入点执行前后需要做的事情 -->
<bean id="sleepHelper" class="com.springAOP.bean.SleepHelper"></bean>
<!-- 定义被代理者 -->
<bean id="me" class="com.springAOP.bean.Me"></bean>
<aop:config>
<aop:aspect ref="sleepHelper">
<aop:before method="beforeSleep" pointcut="execution(* *.sleep(..))" />
<aop:after method="afterSleep" pointcut="execution(* *.sleep(..))" />
</aop:aspect>
</aop:config>
</beans>
(3)配置的另一种写法
<aop:config>
<aop:aspect ref="sleepHelper">
<aop:pointcut id="sleepHelpers" expression="execution(* *.sleep(..))" />
<aop:before pointcut-ref="sleepHelpers" method="beforeSleep" />
<aop:after pointcut-ref="sleepHelpers" method="afterSleep" />
</aop:aspect>
</aop:config>
五、AspectJ与AOP与CGLIB
AspectJ与AOP
1.spectJ是一套独立的面向切面编程的解决方案。
1.1AspectJ 安装
AspectJ 下载地址(http://www.eclipse.org/aspectj/downloads.php)。
1.2下载AspectJ jar包,然后双击安装。安装好的目录结构为:
bin:存放了 aj、aj5、ajc、ajdoc、ajbrowser 等命令,其中 ajc 命令最常用,它的作用类似于 javac
doc:存放了 AspectJ 的使用说明、参考手册、API 文档等文档
lib:该路径下的 4 个 JAR 文件是 AspectJ 的核心类库
1.3AspectJ HelloWorld 实现
业务组件SayHelloServicepackagecom.ywsc.fenfenzhong.aspectj.learn;publicclassSayHelloService{publicvoidsay(){System.out.print("Hello AspectJ");}}需要来了,在需要在调用say()方法之后,需要记录日志。那就是通过AspectJ的后置增强吧。
LogAspect日志记录组件,实现对com.ywsc.fenfenzhong.aspectj.learn.SayHelloService后置增强packagecom.ywsc.fenfenzhong.aspectj.learn;publicaspectLogAspect{pointcutlogPointcut():execution(voidSayHelloService.say());after():logPointcut(){System.out.println("记录日志 ...");}}
编译SayHelloService
执行命令 ajc-d.SayHelloService.javaLogAspect.java生成SayHelloService.class执行命令 javaSayHelloService输出HelloAspectJ记录日志
ajc.exe 可以理解为 javac.exe 命令,都用于编译 Java 程序,区别是 ajc.exe 命令可识别 AspectJ 的语法;我们可以将 ajc.exe 当成一个增强版的 javac.exe 命令.执行ajc命令后的 SayHelloService.class 文件不是由原来的 SayHelloService.java 文件编译得到的,该 SayHelloService.class 里新增了打印日志的内容——这表明 AspectJ 在编译时“自动”编译得到了一个新类,这个新类增强了原有的 SayHelloService.java 类的功能,因此 AspectJ 通常被称为编译时增强的 AOP 框架。
与 AspectJ 相对的还有另外一种 AOP 框架,它不需要在编译时对目标类进行增强,而是运行时生成目标类的代理类,该代理类要么与目标类实现相同的接口,要么是目标类的子类——总之,代理类的实例可作为目标类的实例来使用。一般来说,编译时增强的 AOP 框架在性能上更有优势——因为运行时动态增强的 AOP 框架需要每次运行时都进行动态增强。
2.Spring AOP也是对目标类增强,生成代理类。但是与AspectJ的最大区别在于---Spring AOP的运行时增强,而AspectJ是编译时增强。
曾经以为AspectJ是Spring AOP一部分,是因为Spring AOP使用了AspectJ的Annotation。使用了Aspect来定义切面,使用Pointcut来定义切入点,使用Advice来定义增强处理。虽然使用了Aspect的Annotation,但是并没有使用它的编译器和织入器。其实现原理是JDK 动态代理,在运行时生成代理类。
CGLIB与JDK动态代理
1、JDK动态代理
利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,
在调用具体方法前调用InvokeHandler来处理。
2、CGLIB动态代理
利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
3、何时使用JDK还是CGLIB?
1)如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。
2)如果目标对象实现了接口,可以强制使用CGLIB实现AOP。
3)如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理和CGLIB之间转换。
4、如何强制使用CGLIB实现AOP?
1)添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)
2)在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
4.4具体实现
接口:
package com.jpeony.spring.proxy.compare;
/**
* 用户管理接口(真实主题和代理主题的共同接口,这样在任何可以使用真实主题的地方都可以使用代理主题代理。)
* --被代理接口定义
*/
public interface IUserManager {
void addUser(String id, String password);
}
实现类:
package com.jpeony.spring.proxy.compare;
/**
* 用户管理接口实现(被代理的实现类)
*/
public class UserManagerImpl implements IUserManager {
@Override
public void addUser(String id, String password) {
System.out.println("======调用了UserManagerImpl.addUser()方法======");
}
}
JDK代理实现:
package com.jpeony.spring.proxy.compare;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* JDK动态代理类
*/
public class JDKProxy implements InvocationHandler {
/** 需要代理的目标对象 */
private Object targetObject;
/**
* 将目标对象传入进行代理
*/
public Object newProxy(Object targetObject) {
this.targetObject = targetObject;
//返回代理对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(), this);
}
/**
* invoke方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 一般我们进行逻辑处理的函数比如这个地方是模拟检查权限
checkPopedom();
// 设置方法的返回值
Object ret = null;
// 调用invoke方法,ret存储该方法的返回值
ret = method.invoke(targetObject, args);
return ret;
}
/**
* 模拟检查权限的例子
*/
private void checkPopedom() {
System.out.println("======检查权限checkPopedom()======");
}
}
CGLIB代理实现:
package com.jpeony.spring.proxy.compare;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* CGLibProxy动态代理类
*/
public class CGLibProxy implements MethodInterceptor {
/** CGLib需要代理的目标对象 */
private Object targetObject;
public Object createProxyObject(Object obj) {
this.targetObject = obj;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass());
enhancer.setCallback(this);
Object proxyObj = enhancer.create();
// 返回代理对象
return proxyObj;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
Object obj = null;
// 过滤方法
if ("addUser".equals(method.getName())) {
// 检查权限
checkPopedom();
}
obj = method.invoke(targetObject, args);
return obj;
}
private void checkPopedom() {
System.out.println("======检查权限checkPopedom()======");
}
}
客户端测试类:
package com.jpeony.spring.proxy.compare;
/**
* 代理模式[[ 客户端--》代理对象--》目标对象 ]]
*/
public class Client {
public static void main(String[] args) {
System.out.println("**********************CGLibProxy**********************");
CGLibProxy cgLibProxy = new CGLibProxy();
IUserManager userManager = (IUserManager) cgLibProxy.createProxyObject(new UserManagerImpl());
userManager.addUser("jpeony", "123456");
System.out.println("**********************JDKProxy**********************");
JDKProxy jdkPrpxy = new JDKProxy();
IUserManager userManagerJDK = (IUserManager) jdkPrpxy.newProxy(new UserManagerImpl());
userManagerJDK.addUser("jpeony", "123456");
}
}
程序运行结果:
5、JDK动态代理和CGLIB字节码生成的区别?
1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,
并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,
对于final类或方法,是无法继承的。
6、CGlib比JDK快?
1)使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,
在jdk6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的方法进行代理,
因为CGLib原理是动态生成被代理类的子类。
2)在jdk6、jdk7、jdk8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,
只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理,
总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。
7、Spring如何选择用JDK还是CGLIB?
1)当Bean实现接口时,Spring就会用JDK的动态代理。
2)当Bean没有实现接口时,Spring使用CGlib是实现。
3)可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
8.总结JDK动态代理与CGLIB
JDK代理是不需要第三方库支持,只需要JDK环境就可以进行代理,使用条件:
1)实现InvocationHandler
2)使用Proxy.newProxyInstance产生代理对象
3)被代理的对象必须要实现接口
CGLib必须依赖于CGLib的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,
覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用JDK的代理