java中的3种代理模式:
前言: 代理模式,是属于23种设计模式中的一种,即,代理模式是一种设计模式。
一、代理模式 (Proxy Pattern) 是什么?
代理模式是一种设计模式,简单说就是在不改变源码的情况下,实现对目标对象的功能扩展。
比如有个歌手对象叫Singer,这个对象有一个唱歌方法叫sing()
1 public class Singer{
2 public void sing(){
3 System.out.println("唱一首歌");
4 }
5 }
假如你希望,通过你的某种方式生产出来的歌手对象,在唱歌前后还要想观众问好和答谢,也即对目标对象Singer的sing方法进行功能扩展。
1 public void sing(){
2 System.out.println("向观众问好"); //不修改源代码,实现新增这个
3 System.out.println("唱一首歌");
4 System.out.println("谢谢大家"); //不修改源代码,实现新增这个
5 }
但是往往你又不能直接对源代码进行修改,可能是你希望原来的对象还保持原来的样子,又或许你提供的只是一个可插拔的插件,甚至你有可能都不知道你要对哪个目标对象进行扩展。这时就需要用到java的代理模式了。网上好多用生活中的经理人的例子来解释“代理”,看似通俗易懂,但我觉得不适合程序员去理解。程序员应该从代码的本质入手。
二、Java的三种代理模式
Java的三种代理模式 :
静态代理、 JDK动态代理、 Cglib动态代理
想要实现以上例子中歌手的需求有三种方式,这一部分我们只看三种模式的代码怎么写,先不涉及实现原理的部分。
- 静态代理
/**
* 目标类要实现的接口
*/
public interface ISinger {
void sing();
}
/**
* 目标类
*/
public class Singer implements ISinger{
public void sing(){
System.out.println("唱一首歌");
}
}
/**
* 【静态代理模式】 代理类和目标类实现相同的接口
*/
public class SingerProxy implements ISinger{
private ISinger target; //注入目标对象,以便调用sing方法
public SingerProxy(ISinger target){ //代理类的构造方法,传入目标对象
this.target=target;
}
// 对目标对象的sing方法进行功能扩展
public void sing() {
System.out.println("向观众问好"); //静态代理前置内容
target.sing();
System.out.println("谢谢大家"); //静态代理后置内容
}
}
/**
* 【静态代理模式】 测试类
*/
public class Test {
public static void main(String[] args) {
//目标对象
ISinger target = new Singer();
//代理对象
ISinger proxy = new SingerProxy(target); //传入目标对象
//执行的是代理的方法
proxy.sing();
}
}
运行测试类的结果:
向观众问好
唱一首歌
谢谢大家
总结:
静态代理缺点:代理对象必须提前写出,如果接口层发生了变化,代理对象的代码也要进行维护。
如果能在运行时动态地写出代理对象,不但减少了一大批代理类的代码,也少了不断维护的烦恼,
不过运行时的效率必定受到影响。这种方式就是接下来的动态代理。
- JDK动态代理 【方法一】
/**
* 目标类要实现的接口
*/
public interface ISinger {
void sing();
}
/**
* 目标类
*/
public class Singer implements ISinger{
public void sing(){
System.out.println("唱一首歌");
}
}
/**
* 【动态代理模式】 代理类
* */
public class SingerDynamicProxy implements InvocationHandler {
private Ising target; //注入目标对象
public SingerDynamicProxy(Ising target) {//代理类的构造方法,传入目标对象
this.target = target;
}
// 每一个动态代理类都必须要实现InvocationHandler这个接口,并且每个代理类的实例都关联到了一个handler,
// 当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。
// InvocationHandler这个接口的唯一一个方法 invoke 方法:
// 该方法接收3个参数:
// proxy: 指代我们所代理的那个真实对象
// method: 指代的是我们所要调用真实对象的某个方法的Method对象
// args: 指代的是调用真实对象某个方法时接受的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("向观众问好 ...静态代理前置内容 ");// 在代理真实对象前我们可以添加一些自己的操作
System.out.println("Method:" + method);
Object obj = method.invoke(target, args); // 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
System.out.println("方法参数: "+ Arrays.toString(args));
System.out.println("谢谢大家 ...静态代理后置内容 ");// 在代理真实对象后我们也可以添加一些自己的操作
return obj;
}
}
public class Test2{
public static void main(String[] args) {
//目标对象
Ising target=new Singer();
//代理对象
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler proxy = new SingerDynamicProxy(target);
/*
* 通过Proxy的newProxyInstance方法来创建我们的代理对象,三个参数:
* 第一个参数 :加载器用于定义代理类的类加载器 ,handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
* 第二个参数: 被代理的对象的类的接口列表实现 ,realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
* 第三个参数: 设置调用处理程序,将方法调用分派给的代理对象, handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
*/
Ising ising = (Ising) Proxy.newProxyInstance(
proxy.getClass().getClassLoader(),
target.getClass().getInterfaces(),
proxy
);
ising.sing();
}
}
运行测试类的结果:
向观众问好 ...静态代理前置内容
Method:public abstract void com.sjq.test.proxy.Ising.sing()
唱一首歌
方法参数: null
谢谢大家 ...静态代理后置内容
总结:
动态代理缺点:可以看出静态代理和JDK代理有一个共同的缺点,
就是目标对象必须实现一个或多个接口,如果没有,则可以使用Cglib代理。
-
JDK动态代理 【方法二】
(后期补充。) -
Cglib动态代理
前提条件:
需要引入cglib的jar文件,由于Spring的核心包中已经包括了Cglib功能,所以也可以直接引入spring-core-3.2.5.jar
目标类不能为final
目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
三、总结:
三种代理模式各有优缺点和相应的适用范围,主要看目标对象是否实现了接口。以Spring框架所选择的代理模式举例。
在Spring的AOP编程中:
如果加入容器的目标对象有实现接口,用JDK动态代理 (不建议用静态代理,几乎被淘汰,缺点多);
如果目标对象没有实现接口,用Cglib动态代理;
四、备注:
学习链接1:https://www.cnblogs.com/boboxing/p/8126046.html
学习链接2:https://www.cnblogs.com/maohuidong/p/7992894.html
学习链接3:https://zhuanlan.zhihu.com/p/50777496