结构型模式包含七种:代理模式、桥接模式、装饰者模式、适配器模式、外观模式、组合模式、享元模式
1.代理模式定义
让你能够提供对象的代替品或占位符,代理控制着对于原对象的访问,并允许将请求提交给对象前后进行一些处理;
1.1 代理模式优缺点
优点
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点
- 增加了系统的复杂度;
1.2 代理模式的使用场景
- 当需要对一个对象的访问提供一些额外操作时,可以使用代理模式;
- 远程代理——RPC框架
- 防火墙代理,防火墙将你的请求转到互联网,互联网的相应通过防火墙转到你;
- 保护一个对象,控制对一个对象的访问;
2.代理模式原理
- 抽象主题类(Subject):声明了真实主题和代理主题的共同接口,保证任何使用真实主题的地方都可以使用代理主题,客户端一般针对抽象主题类进行编程;
- 代理类(Proxy):提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以在任何时候访问、控制或扩展真实主题的功能;
- 真实主题类(Real Subject):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象;
3.静态代理模式的实现
需要代理对象与目标对象实现一样的接口
- 优点:可以在不修改目标对象的前提下扩展目标对象的功能
- 缺点:冗余,由于代理对象要实现与目标对象一致的接口,会产生过多的代理类;不宜维护,一旦接口增加方法,目标对象与代理对象都要修改;
【代码】
接口类
public interface IUserDao {
void save();
}
目标对象
public class UserDaoImpl implements IUserDao {
@Override
public void save() {
System.out.println("保存数据");
}
}
静态代理对象
public class UserDaoProxy implements IUserDao {
private IUserDao target;
public UserDaoProxy(IUserDao target) {
this.target = target;
}
@Override
public void save() {
System.out.println("开启事务"); //扩展额外功能
target.save();
System.out.println("提交事务");
}
}
客户端
//目标对象
UserDaoImpl userDao = new UserDaoImpl();
//代理对象
UserDaoProxy proxy = new UserDaoProxy(userDao);
proxy.save();
4.动态代理的实现
动态代理有两种:JDK动态代理与cglib动态代理
4.1 JDK动态代理
JDK动态代理是指利用JDK API动态的在内存中构建代理对象,从而实现对目标对象的代理功能;(核心就是反射)
public class ProxyFactory {
private Object target; //维护一个目标对象
public ProxyFactory(Object target) {
this.target = target;
}
//为目标对象生成代理对象
public Object getProxyInstance(){
//使用Proxy获取代理对象
return Proxy.newProxyInstance(
target.getClass().getClassLoader(), //目标类使用的类加载器
target.getClass().getInterfaces(), //目标对象实现的接口类型
new InvocationHandler(){ //事件处理器
/**
* invoke方法参数说明
* @param proxy 代理对象
* @param method 对应于在代理对象上调用的接口方法Method实例
* @param args 代理对象调用接口方法时传递的实际参数
* @return: java.lang.Object 返回目标对象方法的返回值,没有返回值就返回null
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开启事务");
//执行目标对象方法
method.invoke(target, args);
System.out.println("提交事务");
return null;
}
}
);
}
}
//测试
public static void main(String[] args) {
IUserDao target = new UserDaoImpl();
System.out.println(target.getClass());//目标对象信息
IUserDao proxy = (IUserDao) new ProxyFactory(target).getProxyInstance();
System.out.println(proxy.getClass()); //输出代理对象信息
proxy.save(); //执行代理方法
}
4.1.1 类的动态形成
Java虚拟机中类的加载过程分为五个阶段:加载、验证、准备、解析、初始化;
其中加载阶段需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流;(包含三种:从本地获取、从网络中获取、运行时计算生成;其中计算生成用的最多的就是动态代理)
- 将这个字节流所代表的静态结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的对象,作为方法区这个类的各种数据访问入口;
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用
4.1.2 代理类的调用过程
public final class $Proxy0
extends Proxy
implements IUserDao {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
try {
m3 = Class.forName("com.mashibing.proxy.example01.IUserDao").getMethod("save", new Class[0]);
return;
}
}
public final void save() {
try {
this.h.invoke(this, m3, null);
return;
}
}
}
- 动态代理类对象继承了 Proxy 类,并且实现了被代理的所有接口,以及 equals、hashCode、toString 等方法;
- 代理类的构造函数,参数是 InvocationHandler 实例,Proxy.newInstance 方法就是通过这个构造函数来创建代理实例的;
- 类和所有方法都被 public final 修饰,所以代理类只可被使用,不可再被继承;
- 每个方法都有一个 Method 对象来描述,Method 对象在静态代码块中创建;
- 调用方法的时候通过 this.h.invoke(this, m3, null),实际上这个就是调用我们重写的 invoke 方法;
4.2 cglib动态代理
cglib是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现目标对象功能的扩展,cglib为没有实现接口的类提供代理,为JDK动态代理提供了补充;(Spring AOP基于cglib进行封装,实现cglib方式的动态代理,也就是使用spring的话不需要引入cglib)
5.动态代理与静态代理的对比
- 静态代理在编译时就已经实现了,编译完成后代理类是一个实际的class文件;
- 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中;
-
jdk代理和CGLIB代理:
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
-
动态代理和静态代理:
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题