代理模式

代理模式

案例

张三在北京上班,最近有事要回老家上海。于是他想着去火车站买票,接下来就简单的模拟这一过程。

1.创建火车站的类:

/**
 * 模拟火车站,提供购票的方法
 */
public class TrainStation {
    public String buy(String start, String end) {
        return "车票:[起点:" + start + "\t终点:" + end + "]";
    }
}

2.买票的过程:

public class Main {
    public static void main(String[] args) {
        TrainStation trainStation = new TrainStation();
        String ticket = trainStation.buy("北京", "上海");
        System.out.println("张三的家离火车站很远,于是坐了两个小时的车来到火车站,买了:" + ticket);
    }
}

3.结果:

张三的家离火车站很远,于是坐了两个小时的车来到火车站,买了:车票:[起点:北京	终点:上海]

张三回到家后,对朋友李四抱怨说:”买个车票一来一回都花掉了半天的时间,太累了“。李四笑了笑说道:”楼下就有一个火车票代售处,你为什么不去那买呢?“。张三听了后,忙问道:”代售处可以买票吗?“。李四说:“当然可以啦,实际上代售处卖的也是火车站的票,但是这样你就不用走这么远了”。张三听到后,感觉很有道理,他还想到了代理模式与这一过程很是相似。

模式介绍

代理模式是一种结构型模式,它为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

角色构成

  • Subject(抽象主题角色):它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。
  • Proxy(代理主题角色):它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中,客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。
  • RealSubject(真实主题角色):它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

UML类图

proxy

代理模式的结构图比较简单,其实质是通过引入一个代理主题角色类,其内部引用了真实主题角色类的对象。

代码改造

1.抽象主题角色类:

/**
 * 抽象主题角色类:抽象售票类
 */
public interface TicketOffice {
    String buy(String start, String end);
}

2.真实主题角色类:

/**
 * 真实主题角色类:火车站
 */
public class TrainStation implements TicketOffice {
    public String buy(String start, String end) {
        return "车票:[起点:" + start + "\t终点:" + end + "]";
    }
}

3.代理主题角色类:

/**
 * 代理主题角色类:代售处
 */
public class SalesAgency implements TicketOffice {
    private TrainStation trainStation;

    public SalesAgency() {
        this.trainStation = new TrainStation();
    }

    public String buy(String start, String end) {
        return trainStation.buy(start, end);
    }
}

4.测试代码:

public class Main {
    public static void main(String[] args) {
        TicketOffice salesAgency = new SalesAgency();
        String ticket = salesAgency.buy("北京", "上海");
        System.out.println("张三花了5分钟来到家楼下的火车票代售点,买了:" + ticket);
    }
}

5.测试结果:

张三花了5分钟来到家楼下的火车票代售点,买了:车票:[起点:北京	终点:上海]

可以看到张三经过这次的买票经历后,以后回家就可以在楼下代售处买火车票了。这里我们实际上用到的是静态代理的模式,而在 JDK 中给我们提供了动态代理的使用方式。它需要实现InvocationHandler接口:

/**
 * 通过实现 InvocationHandler 接口运用动态代理模式
 */
public class DynamicProxy implements InvocationHandler {
    // 被代理的对象
    private Object object;

    // 注入真实主题角色
    public DynamicProxy(Object object) {
        this.object = object;
    }

    // 重写 invoke() 方法,内部调用真实主题角色的方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(object, args);
    }
}

测试代码:

public class Main {
    public static void main(String[] args) {
        System.out.println("-----------动态代理-----------");
        DynamicProxy dynamicProxy = new DynamicProxy(new TrainStation());
        TicketOffice ticketOffice = (TicketOffice) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{TicketOffice.class}, dynamicProxy);
        System.out.println("张三花了5分钟来到家楼下的火车票代售点,买了:" + ticketOffice.buy("北京", "上海"));
    }
}

测试结果:

-----------动态代理-----------
张三花了5分钟来到家楼下的火车票代售点,买了:车票:[起点:北京	终点:上海]

模式应用

代理模式的应用还是很广泛的,比如 MyBatis 框架中就用到了 JDK 动态代理。MyBatis 是一种应用非常广泛的持久层框架,它为我们与数据库的交互提供了极大的便利,下面通过一个简单例子来深刻理解动态代理模式。

1.首先引入依赖:

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.4.6</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
</dependencies>

2.实体类:

public class User {
    private String id;
    private String username;

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", username='" + username + '\'' +
                '}';
    }

}

3.mapper 接口:

public interface UserMapper {
    User getUserById(Integer id);
}

4.mapper.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.phoegel.proxy.analysis.UserMapper">
    <select id="getUserById" resultType="com.phoegel.proxy.analysis.User">
        select * from user where id = #{id}
    </select>
</mapper>

5.MyBatis 配置文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/phoegel/proxy/analysis/UserMapper.xml"/>
    </mappers>
</configuration>

6.测试类:

public class Main {
    public static void main(String[] args) {
        // mybatis 配置文件位置:类路径下的 mybatis-config.xml 文件
        String resource = "mybatis-config.xml";
        try {
            // 读取 mybatis 配置文件
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 创建 sqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 创建 session
            SqlSession session = sqlSessionFactory.openSession();
            // 获取 mapper 映射
            UserMapper userMapper = session.getMapper(UserMapper.class);
            // 这里返回的实际上是 MapperProxy 类实例,即代理类实例
            System.out.println("userMapper:" + userMapper);
            // 调用查询方法
            User user = userMapper.getUserById(1);
            // 输出查询结果
            System.out.println(user);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

7.测试结果:

userMapper:org.apache.ibatis.binding.MapperProxy@3d24753a
User{id='1', username='张三'}

从输出中可以看到,这里实际调用的类是MapperProxy类的实例中的方法,而这个类看名字就知道它与代理模式密切相关,下面是它的源码:

public class MapperProxy<T> implements InvocationHandler, Serializable {
  ...
  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
  }
  ...
}

可以看到MapperProxy就是实现了InvocationHandler接口的类,它最主要的作用就是重写了invoke()方法。而MapperProxy类实例是在MapperProxyFactory工厂类中实例化的,具体代码如下:

1.首先是客户端调用:传入UserMapper.class参数

UserMapper userMapper = session.getMapper(UserMapper.class);

2.DefaultSqlSession类中调用getMapper(Class<T> type):传入UserMapper.class参数

@Override
public <T> T getMapper(Class<T> type) {
  return configuration.<T>getMapper(type, this);
}

3.Configuration类中调用getMapper(Class<T> type, SqlSession sqlSession):传入UserMapper.classDefaultSqlSession类实例

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

4.MapperRegistry类中调用getMapper(Class<T> type, SqlSession sqlSession):传入UserMapper.classDefaultSqlSession类实例

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    // 调用 mapperProxyFactory 工厂类中的方法返回 MapperProxy 类实例
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

5.MapperProxyFactory类中调用newInstance(SqlSession sqlSession):传入DefaultSqlSession类实例

public T newInstance(SqlSession sqlSession) {
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

6.调用本类MapperProxyFactorynewInstance(MapperProxy<T> mapperProxy)方法:传入MapperProxy类实例

protected T newInstance(MapperProxy<T> mapperProxy) {
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

整个流程最终就是调用了 JDK 中动态代理方法返回的MapperProxy类实例,所以最终客户端打印了userMapper:org.apache.ibatis.binding.MapperProxy@3d24753a,后面就是调用userMapper.getUserById(1)去执行 sql 语句返回结果了。可以看到这里的MapperProxy与案例中的DynamicProxy类的作用是一样的,同时也是 JDK 动态代理的核心。

总结

1.主要优点

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。
  • 此外,不同类型的代理模式也具有独特的优点,例如:
    • 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率。
    • 虚拟代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销。
    • 缓冲代理为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间。
    • 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限。

2.主要缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢,例如保护代理。
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理。

3.适用场景

  • 当客户端对象需要访问远程主机中的对象时可以使用远程代理。
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理,例如一个对象需要很长时间才能完成加载时。
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理。通过使用缓冲代理,系统无须在客户端每一次访问时都重新执行操作,只需直接从临时缓冲区获取操作结果即可。
  • 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理。
  • 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/proxy
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/14099308.html

posted @ 2020-12-07 20:03  Phoegel  阅读(78)  评论(0编辑  收藏  举报