代理(Proxy)

Intent

控制对其它对象的访问。

一个类代表另一个类的功能

我们创建具有现有对象的对象,以便向外界提供功能接口。

使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。

注意事项:

1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

2、和装饰器模式的区别:装饰器模式为了增强功能(他这里的增强功能更多的是拓展功能),而代理模式是为了加以控制,也包含增强功能,就像我们平时用的java的代理和spring的代理,实际是增强原有方法的功能。

Class Diagram

代理有以下四类:

  • 远程代理(Remote Proxy):控制对远程对象(不同地址空间)的访问,它负责将请求及其参数进行编码,并向不同地址空间中的对象发送已经编码的请求。

  • 虚拟代理(Virtual Proxy):根据需要创建开销很大的对象,它可以缓存实体的附加信息,以便延迟对它的访问,例如在网站加载一个很大图片时,不能马上完成,可以用虚拟代理缓存图片的大小信息,然后生成一张临时图片代替原始图片。

  • 保护代理(Protection Proxy):按权限控制对象的访问,它负责检查调用者是否具有实现一个请求所必须的访问权限。

  • 智能代理(Smart Reference):取代了简单的指针,它在访问对象时执行一些附加操作:记录对象的引用次数;当第一次引用一个对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。

Implementation

以下是一个虚拟代理的实现,模拟了图片延迟加载的情况下使用与图片大小相等的临时内容去替换原始图片,直到图片加载完成才将图片显示出来。

public interface Image {
    void showImage();
}
public class HighResolutionImage implements Image {
​
    private URL imageURL;
    private long startTime;
    private int height;
    private int width;
​
    public int getHeight() {
        return height;
    }
​
    public int getWidth() {
        return width;
    }
​
    public HighResolutionImage(URL imageURL) {
        this.imageURL = imageURL;
        this.startTime = System.currentTimeMillis();
        this.width = 600;
        this.height = 600;
    }
​
    public boolean isLoad() {
        // 模拟图片加载,延迟 3s 加载完成
        long endTime = System.currentTimeMillis();
        return endTime - startTime > 3000;
    }
​
    @Override
    public void showImage() {
        System.out.println("Real Image: " + imageURL);
    }
}
public class ImageProxy implements Image {
​
    private HighResolutionImage highResolutionImage;
​
    public ImageProxy(HighResolutionImage highResolutionImage) {
        this.highResolutionImage = highResolutionImage;
    }
​
    @Override
    public void showImage() {
        while (!highResolutionImage.isLoad()) {
            try {
                System.out.println("Temp Image: " + highResolutionImage.getWidth() + " " + highResolutionImage.getHeight());
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        highResolutionImage.showImage();
    }
}
public class ImageViewer {
​
    public static void main(String[] args) throws Exception {
        String image = "http://image.jpg";
        URL url = new URL(image);
        HighResolutionImage highResolutionImage = new HighResolutionImage(url);
        ImageProxy imageProxy = new ImageProxy(highResolutionImage);
        imageProxy.showImage();
    }
}

Business Scenario

模拟mybatis的dao实现

相信很多人在使用mybatis都会有疑问,mybatis使用是如何通过注解或xml文件实现的数据库查询呢,答案就是通过代理模式,这里我们模仿者写一下。

代理模式实现

代理模式结构

自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Select {
​
    String value() default "";
​
}

dao

public interface IUserDao {
​
    @Select("select userName from user where id = #{uId}")
    String queryUserInfo(String uId);
​
}

代理类的定义

public class MapperFactoryBean<T> implements FactoryBean<T> {
​
    private Logger logger = LoggerFactory.getLogger(MapperFactoryBean.class);
​
    private Class<T> mapperInterface;
​
    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }
​
    @Override
    public T getObject() throws Exception {
        InvocationHandler handler = (proxy, method, args) -> {
            Select select = method.getAnnotation(Select.class);
            logger.info("SQL:{}", select.value().replace("#{uId}", args[0].toString()));
            return args[0] + ",我是模拟的查询结果!";
        };
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{mapperInterface}, handler);
    }
​
    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }
​
    @Override
    public boolean isSingleton() {
        return true;
    }
​
}
  • 通过继承 FactoryBean,提供bean对象,也就是方法; T getObject()。

  • 在方法 getObject()中提供类的代理以及模拟对sql语句的处理,这里包含了用户调用dao层方法时候的处理逻辑。

  • MapperFactoryBean提供构造函数来透传需要被代理类,class<T> mapperinterface。

  • getObjectType()提供对象类型反馈,以及 isSingleton()返回类是单例的。

代理类注册

public class RegisterBeanFactory implements BeanDefinitionRegistryPostProcessor {
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();// 用于定义一个spring的bean
        beanDefinition.setBeanClass(MapperFactoryBean.class); // 指定bean的class信息
        beanDefinition.setScope("singleton");
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(IUserDao.class);// 透传构造
​
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(beanDefinition, "userDao");//定义holder
        BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);// 注册
    }
​
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        // left intentionally blank
    }
​
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
       default-autowire="byName">
​
    <bean id="userDao" class="org.itstack.demo.design.agent.RegisterBeanFactory"/>
​
</beans>

测试

public class ApiTest {
​
    private Logger logger = LoggerFactory.getLogger(ApiTest.class);
​
    @Test
    public void test_IUserDao() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring-config.xml");
        IUserDao userDao = beanFactory.getBean("userDao", IUserDao.class);
        String res = userDao.queryUserInfo("100001");
        logger.info("测试结果:{}", res);
    }
​
}

Summary

代理模式在我们开发中应用广泛,像spring的aop就是一个典型的例子。这种设计方式可以让代码更加整洁、干净易于维护,虽然在这部分开发中额外增加了很多类也包括了自己处理bean的注册等,但是这样的中间件复用性极高也更加智能,可以非常方便的扩展到各个服务应用中。