4.1 代理模式
代理模式的定义:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
为什么要用代理模式?
- 中介隔离作用:在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
- 开闭原则:回顾上一章说的设计原则,代理模式遵循的是开闭原则的设计思想。代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能,这样做我们只需要修改代理类而不需要再修改委托类,符合代码设计的开闭原则。代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。代理类本身并不真正实现服务,而是同过调用委托类的相关方法,来提供特定的服务。真正的业务功能还是由委托类来实现,但是可以在业务功能执行的前后加入一些公共的服务。例如我们想给项目加入缓存、日志这些功能,我们就可以使用代理类来完成,而没必要打开已经封装好的委托类。
有哪几种代理模式?
我们有多种不同的方式来实现代理。如果按照代理创建的时期来进行分类的话, 可以分为两种:静态代理、动态代理。静态代理是由程序员创建或特定工具自动生成源代码,在对其编译。在程序员运行之前,代理类.class文件就已经被创建了。动态代理是在程序运行时通过反射机制动态创建的。
4.2 动态代理
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。
4.2.1 JDK动态代理
Java动态代理类位于java.lang.reflect包下,一般主要涉及到以下两个类:
Interface InvocationHandler
:该接口中仅定义了一个方法
public object invoke(Object obj,Method method, Object[] args)
|
在实际使用时,第一个参数obj一般是指代理类,method是被代理的方法,如上例中的request(),args为该方法的参数数组。这个抽象方法在代理类中动态实现。
- Proxy:该类即为动态代理类,其中主要包含以下内容:
protected Proxy(InvocationHandler h)
:构造函数,用于给内部的h赋值。
static Class getProxyClass (ClassLoaderloader, Class[] interfaces)
:获得一个代理类,其中loader是类装载器,interfaces是真实类所拥有的全部接口的数组。
static Object newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)
:返回代理类的一个实例,返回后的代理类可以当作被代理类使用(可使用被代理类的在Subject接口中声明过的方法)
所谓DynamicProxy是这样一种class:它是在运行时生成的class,在生成它时你必须提供一组interface给它,然后该class就宣称它实现了这些 interface。你当然可以把该class的实例当作这些interface中的任何一个来用。当然,这个DynamicProxy其实就是一个Proxy,它不会替你作实质性的工作,在生成它的实例时你必须提供一个handler,由它接管实际的工作。
在使用动态代理类时,我们必须实现InvocationHandler
接口
通过这种方式,被代理的对象(RealSubject)可以在运行时动态改变,需要控制的接口(Subject接口)可以在运行时改变,控制的方式(DynamicSubject类)也可以动态改变,从而实现了非常灵活的动态代理关系。
必须有接口才能代理。
public interface HelloWord { String sayHello(String toWhom);
void sayBye(); }
123456
|
实现接口的方法,被代理的类:
public class HelloWordImpl implements HelloWord { @Override public String sayHello(String toWhom) { System.out.println("进入了真实逻辑"); return "hello," + toWhom; }
@Override public void sayBye() { System.out.println("bye"); } } 123456789101112
|
代理类实现InvocationHandler接口,重写invoke方法,接入代理的逻辑:
public class JdkProxy implements InvocationHandler {
private Object target = null;
public Object bind(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理逻辑开始"); Object object = method.invoke(target, args); System.out.println("代理逻辑结束"); return object; } }
|
测试类:
public class JdkProxyTest { public static void main(String[] args) { JdkProxy jdkProxy = new JdkProxy(); HelloWord helloWord = (HelloWord) jdkProxy.bind(new HelloWordImpl()); System.out.println(helloWord.sayHello("Tom")); helloWord.sayBye(); } }
|
JDK动态代理步骤:
创建一个实现接口InvocationHandler
的类,它必须实现invoke方法
- 创建被代理的类以及接口
- 通过Proxy的静态方法
newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)
创建一个代理
通过代理调用方法
4.2.2 CGLIB动态代理
cglib是一个java字节码的生成工具,它动态生成一个被代理类的子类,子类重写被代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。
被代理类:
public class HelloServiceImpl { public void sayHello(){ System.out.println("Hello Zhanghao"); }
public void sayBey(){ System.out.println("Bye Zhanghao"); } }
|
实现MethodInterceptor接口生成方法拦截器:
public class HelloMethodInterceptor implements MethodInterceptor{ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("Before: " + method.getName()); Object object = methodProxy.invokeSuper(o, objects); System.out.println("After: " + method.getName()); return object; } }
|
生成代理类对象并打印在代理类对象调用方法之后的执行结果:
public class Client { public static void main(String[] args) { System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/zhanghao/Documents/toy/spring-framework-source-study/"); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(HelloServiceImpl.class); enhancer.setCallback(new HelloMethodInterceptor()); HelloServiceImpl helloService = (HelloServiceImpl) enhancer.create(); helloService.sayBey(); } } result: Before: sayBey Bye Zhanghao After: sayBey
|
构建代理类过程
我们可以从上面的代码示例中看到,代理类是由enhancer.create()创建的。Enhancer是CGLIB的字节码增强器,可以很方便的对类进行拓展。
创建代理类的过程:
- 生成代理类的二进制字节码文件;
- 加载二进制字节码,生成Class对象;
- 通过反射机制获得实例构造,并创建代理类对象。
enhancer.create()实现:
public Object create() { classOnly = false; argumentTypes = null; return createHelper(); } private Object createHelper() { validate(); if (superclass != null) { setNamePrefix(superclass.getName()); } else if (interfaces != null) { setNamePrefix(interfaces[ReflectUtils.findPackageProtected(interfaces)].getName()); } return super.create(KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null, ReflectUtils.getNames(interfaces), filter, callbackTypes, useFactory, interceptDuringConstruction, serialVersionUID)); }
|
cglic一共会自动生成三个字节码文件。其中一个类HelloServiceImpld855d4dc 继承了被代理类 HelloServiceImpl。这个类就是加强的代理类,其中会生成两个方法CGLIB1()和sayHello()。 其中sayHello():
public final void sayHello() { MethodInterceptor var10000 = this.CGLIB$CALLBACK_0; if(this.CGLIB$CALLBACK_0 == null) { CGLIB$BIND_CALLBACKS(this); var10000 = this.CGLIB$CALLBACK_0; }
if(var10000 != null) { var10000.intercept(this, CGLIB$sayHello$1$Method, CGLIB$emptyArgs, CGLIB$sayHello$1$Proxy); } else { super.sayHello(); } }
|
当代理对象的执行sayHello方法时,会首先判断一下是否存在实现了MethodInterceptor接口的CGLIB$CALLBACK_0;,如果存在,则将调用MethodInterceptor中的intercept方法。
与JDK代理对比
JDK代理要求被代理的类必须实现接口,有很强的局限性。而CGLIB动态代理则没有此类强制性要求。简单的说,CGLIB会让生成的代理类继承被代理类,并在代理类中对代理方法进行强化处理(前置处理、后置处理等)。但是如果被代理类被final修饰,那么它不可被继承,即不可被代理;同样,如果被代理类中存在final修饰的方法,那么该方法也不可被代理。
4.3 AOP
Aspect Oriented Programming:面向切面编程
什么时候会出现面向切面编程的需求?按照软件重构的思想,如果多个类中出现重复的代码,就应该考虑定义一个共同的抽象类,将这些共同的代码提取到抽象类中,比如Teacher,Student都有username,那么就可以把username及相关的get、set方法抽取到SysUser中,这种情况,我们称为纵向抽取。
但是如果,我们的情况是以下情况,又该怎么办? 给所有的类方法添加性能检测,事务控制,该怎么抽取? AOP就是希望将这些分散在各个业务逻辑代码中的相同代码,通过横向切割的方式抽取到一个独立的模块中,让业务逻辑类依然保存最初的单纯。
AOP术语
- 连接点(Joinpoint) 程序执行的某个特定位置,如某个方法调用前,调用后,方法抛出异常后,这些代码中的特定点称为连接点。简单来说,就是在哪加入你的逻辑增强 连接点表示具体要拦截的方法,上面切点是定义一个范围,而连接点是具体到某个方法
- 切点(PointCut) 每个程序的连接点有多个,如何定位到某个感兴趣的连接点,就需要通过切点来定位。比如,连接点--数据库的记录,切点--查询条件 切点用于来限定Spring-AOP启动的范围,通常我们采用表达式的方式来设置,所以关键词是范围
- 增强(Advice) 增强是织入到目标类连接点上的一段程序代码。在Spring中,像BeforeAdvice等还带有方位信息 通知是直译过来的结果,我个人感觉叫做“业务增强”更合适 对照代码就是拦截器定义的相关方法,通知分为如下几种: 前置通知(before):在执行业务代码前做些操作,比如获取连接对象 后置通知(after):在执行业务代码后做些操作,无论是否发生异常,它都会执行,比如关闭连接对象 异常通知(afterThrowing):在执行业务代码后出现异常,需要做的操作,比如回滚事务 返回通知(afterReturning),在执行业务代码后无异常,会执行的操作 环绕通知(around),这个目前跟我们谈论的事务没有对应的操作,所以暂时不谈
- 目标对象(Target) 需要被加强的业务对象
- 织入(Weaving) 织入就是将增强添加到对目标类具体连接点上的过程。 织入是一个形象的说法,具体来说,就是生成代理对象并将切面内容融入到业务流程的过程。
- 代理类(Proxy) 一个类被AOP织入增强后,就产生了一个代理类。
- 切面(Aspect) 切面由切点和增强组成,它既包括了横切逻辑的定义,也包括了连接点的定义,SpringAOP就是将切面所定义的横切逻辑织入到切面所制定的连接点中。
比如上文讨论的数据库事务,这个数据库事务代码贯穿了我们的整个代码,我们就可以这个叫做切面。 SpringAOP将切面定义的内容织入到我们的代码中,从而实现前后的控制逻辑。 比如我们常写的拦截器Interceptor,这就是一个切面类。
4.4 使用Spring AOP开发步骤
如果用maven的同学,引入pom依赖就好了
1) 先引入aop相关jar文件 (aspectj aop优秀组件)
- spring-aop-3.2.5.RELEASE.jar 【spring3.2源码】
- aopalliance.jar 【spring2.5源码/lib/aopalliance】
- aspectjweaver.jar 【spring2.5源码/lib/aspectj】或【aspectj-1.8.2】
- aspectjrt.jar 【spring2.5源码/lib/aspectj】或【aspectj-1.8.2】
注意: 用到spring2.5版本的jar文件,如果用jdk1.7可能会有问题。
- 需要升级aspectj组件,即使用aspectj-1.8.2版本中提供jar文件提供。
2) bean.xml中引入aop名称空间
xmlns:context="http://www.springframework.org/schema/context"
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
4.4.1 引入名称空间
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> </beans>
|
4.4.2 注解方式实现AOP编程
我们之前手动的实现AOP编程是需要自己来编写代理工厂的,现在有了Spring,就不需要我们自己写代理工厂了。Spring内部会帮我们创建代理工厂。也就是说,不用我们自己写代理对象了。
因此,我们只要关心切面类、切入点、编写切入表达式指定拦截什么方法就可以了!
还是以上一个例子为案例,使用Spring的注解方式来实现AOP编程
4.4.2.1 在配置文件中开启AOP注解方式
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="aa"/>
<!-- 开启aop注解方式 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
|
4.4.2.2 代码
切面类
@Component @Aspect public class AOP {
@Before("execution(* aa.*.*(..))") public void begin() { System.out.println("开始事务"); }
@After("execution(* aa.*.*(..))") public void close() { System.out.println("关闭事务"); } }
|
UserDao实现了IUser接口
@Component public class UserDao implements IUser {
@Override public void save() { System.out.println("DB:保存用户"); }
}
|
IUser接口
public interface IUser { void save(); }
|
测试代码:
public class App {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("aa/applicationContext.xml");
IUser iUser = (IUser) ac.getBean("userDao");
System.out.println(iUser.getClass());
iUser.save(); } }
|
4.4.3目标对象没有接口
上面我们测试的是UserDao有IUser接口,内部使用的是动态代理...那么我们这次测试的是目标对象没有接口
OrderDao没有实现接口
@Component public class OrderDao {
public void save() {
System.out.println("我已经进货了!!!"); } }
|
测试代码:
public class App {
public static void main(String[] args) {
ApplicationContext ac = new ClassPathXmlApplicationContext("aa/applicationContext.xml");
OrderDao orderDao = (OrderDao) ac.getBean("orderDao");
System.out.println(orderDao.getClass());
orderDao.save();
} }
|
4.4.4 AOP注解API
api:
- @Aspect 指定一个类为切面类
- @Pointcut("execution(* cn.itcast.e_aop_anno.*.*(..))") 指定切入点表达式
- @Before("pointCut_()") 前置通知: 目标方法之前执行
- @After("pointCut_()") 后置通知:目标方法之后执行(始终执行)
- @AfterReturning("pointCut_()") 返回后通知: 执行方法结束前执行(异常不执行)
- @AfterThrowing("pointCut_()") 异常通知: 出现异常时候执行
- @Around("pointCut_()") 环绕通知: 环绕目标方法执行
@Before("pointCut_()") public void begin(){ System.out.println("开始事务/异常"); }
@After("pointCut_()") public void after(){ System.out.println("提交事务/关闭"); }
@AfterReturning("pointCut_()") public void afterReturning() { System.out.println("afterReturning()"); }
@AfterThrowing("pointCut_()") public void afterThrowing(){ System.out.println("afterThrowing()"); }
@Around("pointCut_()") public void around(ProceedingJoinPoint pjp) throws Throwable{ System.out.println("环绕前...."); pjp.proceed(); System.out.println("环绕后...."); }
|
4.4.5 表达式优化
我们的代码是这样的:每次写Before、After等,都要重写一次切入点表达式,这样就不优雅了。
@Before("execution(* aa.*.*(..))") public void begin() { System.out.println("开始事务"); }
@After("execution(* aa.*.*(..))") public void close() { System.out.println("关闭事务"); }
|
于是乎,我们要使用@Pointcut这个注解,来指定切入点表达式,在用到的地方中,直接引用就行了!
那么我们的代码就可以改造成这样了:
@Component @Aspect public class AOP {
@Pointcut("execution(* aa.*.*(..))") public void pt() {
}
@Before("pt()") public void begin() { System.out.println("开始事务"); }
@After("pt()") public void close() { System.out.println("关闭事务"); } }
|
4.4.6 XML方式实现AOP编程
XML文件配置
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userDao" class="aa.UserDao"/> <bean id="orderDao" class="aa.OrderDao"/>
<bean id="aop" class="aa.AOP"/>
<aop:config >
<aop:pointcut id="pointCut" expression="execution(* aa.*.*(..))"/>
<aop:aspect ref="aop">
<aop:before method="begin" pointcut-ref="pointCut"/> <aop:after method="close" pointcut-ref="pointCut"/>
</aop:aspect> </aop:config>
</beans>
|
测试:
public class App {
@Test public void test1() {
ApplicationContext ac = new ClassPathXmlApplicationContext("aa/applicationContext.xml");
OrderDao orderDao = (OrderDao) ac.getBean("orderDao");
System.out.println(orderDao.getClass());
orderDao.save();
}
@Test public void test2() {
ApplicationContext ac = new ClassPathXmlApplicationContext("aa/applicationContext.xml");
IUser userDao = (IUser) ac.getBean("userDao");
System.out.println(userDao.getClass());
userDao.save();
} }
|
4.5 切入点表达式
切入点表达式主要就是来配置拦截哪些类的哪些方法
4.5.1 语法解析
那么它的语法是这样子的:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
|
符号讲解:
- ?号代表0或1,可以不写
- “*”号代表任意类型,0或多
- 方法参数为..表示为可变参数
参数讲解:
- modifiers-pattern?【修饰的类型,可以不写】
- ret-type-pattern【方法返回值类型,必写】
- declaring-type-pattern?【方法声明的类型,可以不写】
- name-pattern(param-pattern)【要匹配的名称,括号里面是方法的参数】
- throws-pattern?【方法抛出的异常类型,可以不写】
官方也有给出一些例子给我们理解:
4.5.2 测试代码
<!-- 【拦截所有public方法】 --> <!--<aop:pointcut expression="execution(public * *(..))" id="pt"/>-->
<!-- 【拦截所有save开头的方法 】 --> <!--<aop:pointcut expression="execution(* save*(..))" id="pt"/>-->
<!-- 【拦截指定类的指定方法, 拦截时候一定要定位到方法】 --> <!--<aop:pointcut expression="execution(public * cn.itcast.g_pointcut.OrderDao.save(..))" id="pt"/>-->
<!-- 【拦截指定类的所有方法】 --> <!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.*(..))" id="pt"/>-->
<!-- 【拦截指定包,以及其自包下所有类的所有方法】 --> <!--<aop:pointcut expression="execution(* cn..*.*(..))" id="pt"/>-->
<!-- 【多个表达式】 --> <!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) || execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>--> <!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) or execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>--> <!-- 下面2个且关系的,没有意义 --> <!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) && execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>--> <!--<aop:pointcut expression="execution(* cn.itcast.g_pointcut.UserDao.save()) and execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
<!-- 【取非值】 --> <!--<aop:pointcut expression="!execution(* cn.itcast.g_pointcut.OrderDao.save())" id="pt"/>-->
|