1. 代理模式
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。
代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务
使用代理最主要的原因就是,在不改变目标对象方法的情况下对方法进行增强,比如,我们希望对方法的调用增加日志记录,或者对方法的调用进行拦截,等等
A. 抽象主题角色
声明了真实主题和代理主题的共同接口,这样一来在任何可以使用真实主题的地方都可以是使用代理主题
B. 代理主题(Proxy
)角色:
代理主题角色内部含有对真实主题的引用,从而可以在任何时候操作真实主题对象;代理主题角色提供一个与真实主题角色相同的接口,以便可以在任何时候都可以替代真实主题控制对真实主题的引用,负责在需要的时候创建真实主题对象(和删除真实主题对象);代理角色通常在将客户端调用传递给真实的主题之前或之后,都要执行某个操作,而不是单纯地将调用传递给真实主题对象。
C. 真实主题角色
定义了代理角色所代表地真实对象
为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java 动态代理机制以巧妙的方式近乎完美地实践了代理模式的设计理念。
代理,简单来说就是,代理类和委托类都实现相同的接口,代理类代替委托类去执行从相同接口中实现的委托类中的方法,而代理类在代替委托类去执行方法之前和之后可以进行其他相应的处理
2. 动态代理的特点
特点:
字节码随用随创建,随用随加载
作用:
在不修改源码的基础上对方法增强
分类:
① 基于接口的动态代理:
涉及到的类:`Proxy`
提供者:`jdk`
如何创建:使用`Proxy`类中的`newProxyInstance`方法
使用要求:被代理的类必须实现一个接口
② 基于子类的动态代理
4. 静态代理
静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用,通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能
假设现在我们有一个类Calculator
,代表一个计算器,它可以进行加减乘除操作
public class Calculator {
//加
public int add(int a, int b) {
int result = a + b;
return result;
}
//减
public int subtract(int a, int b) {
int result = a - b;
return result;
}
//乘法、除法...
}
现有一个需求:在每个方法执行前后打印日志。你有什么好的方案?
4.1 直接修改
很多人最直观的想法是直接修改Calculator
类
public class Calculator {
//加
public int add(int a, int b) {
System.out.println("add方法开始...");
int result = a + b;
System.out.println("add方法结束...");
return result;
}
//减
public int subtract(int a, int b) {
System.out.println("subtract方法开始...");
int result = a - b;
System.out.println("subtract方法结束...");
return result;
}
//乘法、除法...
}
上面的方案是有问题的:
-
直接修改源程序,不符合开闭原则。应该对扩展开放,对修改关闭
-
如果
Calculator
有几十个、上百个方法,修改量太大 -
存在重复代码(都是在核心代码前后打印日志)
-
日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开
Calculator
花十分钟删除日志打印的代码!
4.2 静态代理实现日志打印
静态代理的实现比较简单:编写一个代理类,实现与目标对象相同的接口,并在内部维护一个目标对象的引用。通过构造器塞入目标对象,在代理对象中调用目标对象的同名方法,并添加前拦截,后拦截等所需的业务功能
按上面的描述,代理类和目标类需要实现同一个接口,所以我打算这样做:
将Calculator
抽取为接口
- 创建目标类
CalculatorImpl
实现Calculator
- 创建代理类
CalculatorProxy
实现Calculator
接口
/**
* Calculator接口
*/
public interface Calculator {
int add(int a, int b);
int subtract(int a, int b);
}
目标对象实现类
/**
* 目标对象实现类,实现Calculator接口
*/
public class CalculatorImpl implements Calculator {
//加
public int add(int a, int b) {
int result = a + b;
return result;
}
//减
public int subtract(int a, int b) {
int result = a - b;
return result;
}
//乘法、除法...
}
代理对象实现类
/**
* 代理对象实现类,实现Calculator接口
*/
public class CalculatorProxy implements Calculator {
//代理对象内部维护一个目标对象引用
private Calculator target;
//构造方法,传入目标对象
public CalculatorProxy(Calculator target) {
this.target = target;
}
//调用目标对象的add,并在前后打印日志
@Override
public int add(int a, int b) {
System.out.println("add方法开始...");
int result = target.add(a, b);
System.out.println("add方法结束...");
return result;
}
//调用目标对象的subtract,并在前后打印日志
@Override
public int subtract(int a, int b) {
System.out.println("subtract方法开始...");
int result = target.subtract(a, b);
System.out.println("subtract方法结束...");
return result;
}
//乘法、除法...
}
使用代理对象完成加减乘除,并且打印日志
public class Test {
public static void main(String[] args) {
//把目标对象通过构造器塞入代理对象
Calculator calculator = new CalculatorProxy(new CalculatorImpl());
//代理对象调用目标对象方法完成计算,并在前后打印日志
calculator.add(1, 2);
calculator.subtract(2, 1);
}
}
4.3 静态代理的弊端
静态代理虽然解决了直接修改源程序,不符合开闭原则直接修改源程序,不符合开闭原则的问题,但是还存在其他问题
-
如果
Calculator
有几十个、上百个方法,修改量太大 -
存在重复代码(都是在核心代码前后打印日志)
-
日志打印硬编码在代理类中,不利于后期维护:比如你花了一上午终于写完了,组长告诉你这个功能取消,于是你又要打开
Calculator
花十分钟删除全部新增代码!
上面案例中,代理类是我们事先编写的,要和目标对象类实现相同接口。由于CalculatorImpl
(目标对象)需要日志功能,我们即编写了CalculatorProxy
(代理对象),并通过构造器传入CalculatorImpl
(目标对象),调用目标对象同名方法的同时添加增强代码
代理对象构造器的参数类型是Calculator
,这意味着它只能接受Calculator
的实现类对象,亦即我们写的代理类CalculatorProxy
只能给Calculator
做代理,它们绑定死了!
如果现在我们系统需要全面改造,给其他类也添加日志打印功能,就得为其他几百个接口都各自写一份代理类…
也就是说静态代理的弊端在于
① 难以扩展新方法
② 每个代理只服务一种对象
自己手动写一个类并实现接口实在太麻烦了。仔细一想,我们其实想要的并不是代理类,而是代理对象!那么,能否让JVM
根据接口自动生成代理对象呢?
比如,有没有一个方法,我传入接口,它就给我自动返回代理对象呢?
5. 动态代理
代理类在程序运行时创建的代理方式被成为动态代理。 我们上面静态代理的例子中,代理类是自己定义好的,在程序运行之前就已经编译完成。然而动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改代理类中每个的方法
5.1 动态代理实现原理
对象创建的过程
先把字节码文件加载到方法区生成的是Class
对象,Class
对象,是Class
类的实例,之后再由Class
对象在堆中创建实例对象
要创建一个实例,最关键的就是得到对应的Class
对象
这也就对应我们上面所说,能不能不写代理类,直接得到代理Class
对象,然后根据它创建代理实例
Class
对象包含了一个类的所有信息,比如构造器、方法、字段等。如果我们不写代理类,这些信息从哪获取呢?
代理类和目标类理应实现同一组接口。
之所以实现相同接口,是为了尽可能保证代理对象的内部结构和目标对象一致,这样我们对代理对象的操作最终都可以转移到目标对象身上,代理对象只需专注于增强代码的编写。
所以,可以这样说:接口拥有代理对象和目标对象共同的类信息。所以,我们可以从接口那得到理应由代理类提供的信息。但是别忘了,接口是无法创建对象的,怎么办?
5.2 jdk
动态代理
(jdk
动态代理要使用接口)
在java
的java.lang.reflect
包下提供了一个Proxy
类和一个InvocationHandler
接口,通过这个类和这个接口可以生成JDK
动态代理类和动态代理对象
① 如何根据接口,动态创建出一个代理类类对象
** getProxyClass(ClassLoader, interfaces)
**
getProxyClass(ClassLoader, interfaces)
这个方法,会从你传入的接口Class
中,“拷贝”类结构信息到一个新的Class
对象中,但新的Class
对象带有构造器,是可以创建对象的。
所以,一旦我们明确接口,完全可以通过接口的Class
对象,创建一个代理Class
,通过代理Class
即可创建代理对象。
只要创建出了代理Class
对象,问题就迎刃而解了
根据代理Class
的构造器创建对象时,需要传入InvocationHandler
。
代理对象的内部确实有个成员变量invocationHandler
用来接收传入的InvocationHandler
,
每次调用代理对象的方法,JVM
都会将方法指向invoke
,也就是最终都是在调用InvocationHandler
的invoke()
方法:
既然这样,我们就应该在invoke
方法中去调用委托类原来的方法,并加上自己的增强代码
为了做到这样,我们需要在invoke
方法中手动new
出委托
但这种写法不够优雅,属于硬编码。我这次代理A对象,下次想代理B对象还要进来改invoke()
方法,太差劲了。改进一下,让调用者把目标对象作为参数传进来:
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
//传入目标对象
//目的:1.根据它实现的接口生成代理对象 2.代理对象调用目标对象方法
Calculator calculatorProxy = (Calculator) getProxy(target);
calculatorProxy.add(1, 2);
calculatorProxy.subtract(2, 1);
}
private static Object getProxy(final Object target) throws Exception {
//参数1:随便找个类加载器给它, 参数2:目标对象实现的接口,让代理对象实现相同接口
Class proxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(), target.getClass().getInterfaces());
Constructor constructor = proxyClazz.getConstructor(InvocationHandler.class);
Object proxy = constructor.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
});
return proxy;
}
}
Proxy.newProxyInstance()
不过实际编程中,一般不用getProxyClass()
,而是使用Proxy
类的另一个静态方法:Proxy.newProxyInstance()
,直接返回代理实例,连中间得到代理Class
对象的过程都帮你隐藏:
public class ProxyTest {
public static void main(String[] args) throws Throwable {
CalculatorImpl target = new CalculatorImpl();
Calculator calculatorProxy = (Calculator) getProxy(target);
calculatorProxy.add(1, 2);
calculatorProxy.subtract(2, 1);
}
private static Object getProxy(final Object target) throws Exception {
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),/*类加载器*/
target.getClass().getInterfaces(),/*让代理对象和目标对象实现相同接口*/
new InvocationHandler(){/*代理对象的方法最终都会被JVM导向它的invoke方法*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
Object result = method.invoke(target, args);
System.out.println(result);
System.out.println(method.getName() + "方法执行结束...");
return result;
}
});
return proxy;
}
}
5.3 我们需要注意的
动态代理生成的代理对象,最终都可以用接口接收,和目标对象一起形成了多态,可以随意切换展示不同的功能。但是切换的同时,只能使用该接口定义的方法
6. 我们在spring
中用到的动态代理
在UserController
中注入Service
层对象一定是我们写的UserServiceImpl
的实例吗?
@Autowired
private UserService userService;
实际上,Spring依赖注入的对象并不一定是我们自己写的类的实例,也可能是userServiceImpl的代理对象。
比如当我给UserServiceImpl
加了@Transactional
注解时,Spring读取到这个注解,便知道我们要使用事务。而我们编写的UserService
类中并没有包含任何事务相关的代码。如果给你,你会怎么做?动态代理嘛!
同样地,Spring
为了实现事务,也编写了一个通知类,TransactionManager
。利用动态代理创建代理对象时,Spring
会把transactionManager
织入代理对象,然后将代理对象注入到UserController
所以我们在UserController
中使用的userService
其实是代理对象,而代理对象才支持事务
7. 使用动态代理实现事务控制
public class BeanFactory {
/**
* 创建账户业务层实现类的代理对象
* @return
*/
public static IAccountService getAccountService() {
//1.定义被代理对象
final IAccountService accountService = new AccountServiceImpl(); 、
//2.创建代理对象
IAccountService proxyAccountService = (IAccountService)
Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),new InvocationHandler(){
//执行被代理对象的任何方法,都会经过该方法。此处添加事务控制
@Override
public Object invoke(Object proxy, Method method,Object[] args) throws Throwable {
Object rtValue = null;
try {
//开启事务
TransactionManager.beginTransaction();
//执行业务层方法
rtValue = method.invoke(accountService, args);
//提交事务
TransactionManager.commit();
}catch(Exception e) {
//回滚事务
TransactionManager.rollback();
e.printStackTrace();
}finally {
//释放资源
TransactionManager.release();
}
return rtValue;
}
});
return proxyAccountService;
}
}
这样,业务层用于控制事务的重复代码就都可以删掉了
8. 代理模式的应用场景
① 远程代理
这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
② 虚拟代理
这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
③ 安全代理
这种方式通常用于控制不同种类客户对真实对象的访问权限。
④ 智能指引
主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
⑤ 延迟加载
指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载
9. 代理模式和装饰者模式
两者都是对类的方法进行增强,但装饰器模式强调的是增强自身,在被装饰之后你能够够在被增强的类上使用增强后的方法。增强过后还是你,只不过能力变强了。
而代理模式则强调要别人帮你去做一些本身与你业务没有太多关系的职责。代理模式是为了实现对象的控制,因为被代理的对象往往难以直接获得或者是其内部不想暴露出来