代理模式(Proxy Pattern)

前言

关于代理,举个常见的例子——租房子!租房子对于每位漂泊在外的猿友来说都是一件深恶痛绝的事,需要考虑和衡量:上班便利与否,房租是否合适,房子周围设施是否健全,同时还要和各种中介打交道,这其中还有些黑中介从中作梗,一句话"太累心"。。。说道中介,其实就是房东的代理,他们帮房东出租房子,利用他们的平台发放广告,有些上档次的中介公司,还会帮忙装修,购置家具等,简直是一条龙服务,让房东只需提供房子,其他的什么也不用管,坐等收钱。对于租客来说,租客也不需要和房东直接打交道(其实很多人都想直接找房东租房,避免那笔中介费!),租客看房子、对房子家具装修的要求只需提给中介就行,对于租客和房东来说都比较省心。

虽说代理商的存在,会给租客和出租者增加一笔支出,但是代理商存在具有重要意义:首先,对于房东来说,其目的就是出租自己闲置房屋收取租费,将房子交给代理商打理,一方面可以通过代理商的广告渠道将房子尽快出租出去,另一方面,代理商可以对房屋进行装修置办等,更有利于设施参差不齐的房屋出租出去,作为出租者只需提供房源,无需操心这些事情,其实是一件省心省力的事。再是,代理商能够整合房源,具有一定规模的装修能力,可以处理各种房源,帮助房东把房子出租出去。最后,对于租客来说,能从中介那里获得大量的房源信息,也便于筛选和决策。(注:作者所指的代理商,是真正良心的代理商,并不是黑中介)

简介

说了这么多,作者的目的是让大家理解代理的意义,以及使用代理带来的便利。本着"万物皆对象"的原则,我们先用java代码来描述下"租房"这件事:

/// 出租者,具有闲置房屋资源
public class Lessor {
    // 每月租金
    private double rent;
    // 代表房屋资源
    private int houses;

    public Lessor(int houses,int rent){
        this.houses = houses;
        this.rent = rent;
    }
	// 出租者向往出租,获取房租,monthNum:出租月数
    public double rentOut(int monthNum) {
        if (this.houses > 0) {
            houses--;
            return this.rent * monthNum;
        }
        return 0;
    }
}

/// 代理商
public class HousingProxy {
    // 服务费用费率
    private static double SERVICE_FEE_RATE = 0.1;
    // 这个代理商能力有限,一次只能代理一个房东
    private Lessor lessor;
    public HousingProxy(Lessor lessor) {
        this.lessor = lessor;
    }
	// 代理商出租房屋,收取租费
    public double rentOut(int monthNum) {
        // 代理商装修置办房子,并发各种广告招租
        System.out.println("中介简单装修");
        System.out.println("中介购置家具");
        System.out.println("中介发放招租广告");
        // 代理商并没有房子,最终还是房东出房子
        double lessorMoney = lessor.rentOut(monthNum);
        System.out.println("房子出租出去,房东获得租金:" + new DecimalFormat("#.00").format(lessorMoney));
        return lessorMoney * (1 + SERVICE_FEE_RATE);
    }
}
/// 出租
public class App {
    public static void main(String[] args) {
        HousingProxy housingProxy = new HousingProxy(new Lessor(1,1200));
        double money = housingProxy.rentOut(3);
        System.out.println("租户租房成功,租户付费:" + new DecimalFormat("#.00").format(money));
    }
}
/* 
console输出结果:>>
    中介简单装修
    中介购置家具
    中介发放招租广告
    房子出租出去,房东获得租金:3600.00
    租户租房成功,租户付费:3960.00
*/

既然出租者和中介都具有出租能力,我们就用一个接口来约束一下,规范下"出租",让租客体验"无差别租房":

public interface Rent {
    double rentOut(int monthNum);
}
// 新的出租者
public class Lessor implements Rent {
    //...省略
    @Override
    public double rentOut(int monthNum) {
		//...省略
    }
     //...省略
}
// 新的代理商
public class HousingProxy implements Rent {
     //...省略
     @Override
    public double rentOut(int monthNum) {
		//...省略
    } 
    //...省略
}

至此我们通过一个"租房"的例子,让大家了解了代理,当然这里使用的代理模式是静态代理,关于动态代理下文会讲解。

定义

先看下代理模式的定义:

为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。(摘自百度百科)

组成

代理模式(Proxy Pattern)_第1张图片

实际应用

关于代理模式的定义、适用场景、类图、优缺点等等概念和知识点不是这篇文章的核心(网上随便一搜讲解这些的博文很多,可自行搜索),作者要通过实际开发中的列子来讲解代理模式该怎么使用:

案例

描述:某电信行业项目,代码比较老旧,使用的ssh框架基于mvc方式开发,现有一个数据上传接口,由于有时数据量比较大,上传会比较耗时,所以局方提出需求,需要记录这个时长。下面给出部分代码

/// service接口
public interface BureauService{
    // 省略其他方法
    // 数据上传方法
	int dataInput(List<Bureau> bureauList);
}
/// service实现类
@Service("BureauService")
public class BureauServiceImpl implements BureauService {
	@Autowired
	private BureauDao bureauDao;
    
	// 省略其他方的实现法
    @Override
    public int dataInput(List<Bureau> bureauList) {
        // 调用dao的批量数据插入方法
        return bureauDao.saveAll(bureauList);
    }
}

这个接口比较简单,添加记录时间的功能也不繁琐,完全可以修改已有代码:

@Service("BureauService")
public class BureauServiceImpl implements BureauService {
	@Autowired
	private BureauDao bureauDao;
	// 省略其他方的实现法
    @Override
    public int dataInput(List<Bureau> bureauList) {
        long t1 = System.currentTimeMillis();
        int rows =  bureauDao.saveAll(bureauList);
        long t2 = System.currentTimeMillis();
        LogUtils.log("数据上传耗时:" + (t2 - t1));
        return rows;
    }
}

但是本着"开闭原则",这样的修改是不合理的。经验丰富的老司机肯定会想到用aop去实现,但是很多时候,做决策以及技术选型要考虑实际情况的,此时完全可以用代理模式去解决这个问题,做到投入少回报高:

/// 构建代理类
public class BureauServiceProxy implements BureauService {
    private BureauService bureauService;
    
    public BureauServiceProxy(BureauService bureauService){
        this.bureauService = bureauService;
    }	
	// 省略其他方法的实现
    @Override
    public int dataInput(List<Bureau> bureauList) {
        long t1 = System.currentTimeMillis();
        int rows =  BureauService.dataInput(bureauList);
        long t2 = System.currentTimeMillis();
        LogUtils.log("数据上传耗时:" + (t2 - t1));
        return  rows;
    }
 /// 使用
public class BureauController {
	@Resource
    private BureauService bureauService
    // 省略其他方法
    public JsonResult dataUpload(InputStream is){
        // 省略
        BureauServiceProxy proxy = new BureauServiceProxy(bureauService);
        proxy.dataInput(bureauList);
    }
} 

至此我们在没有修改业务代码的情况下,完成了耗时时间的记录。当然,作者还是比较支持使用aop去实现的,毕竟下一次用户提出其他接口也要记录时间,或者需要记录其他什么数据时,如果涉及的类太多,这么写就麻烦了,我们需要写许多这样的代理类。这也说明使用静态代理是有一定弊端的!

动态代理

上节结尾,我们提到使用静态代理在某些情况下,会增加类的数量,增加一定工作量。作为程序员,我们怎么能忍受大量重复功能的代码呢,必须重新架构设计。

我们可以分析下代理模式:

  • 首先,代理类是程序运行所需类之外的新增类,需要用户额外去编写
  • 其次,代理类执行的方法是需要开发者去编写
  • 再是,代理类需要持有一个真正实现类的实例

可见代理模式的工作方式单一可控,可以抽象成同样的动作:根据接口生成代理类,该代理类持有真正实现类的对象,支持用户自行编辑代理类中实现接口的方法;试想一下,我们可以写一个用于根据接口生成类的"工具",用它可以产生代理类,同时我们可以拦截该代理类中的方法,对其进行修改,用这个工具我们就可以批量自动生成代理类了!这就是动态代理

动态代理:我们不需要手动的为每个类去创建代理类了,让程序在运行期动态生成代理类,我们只提供要代理的类(或接口),以及要实现的功能即可。

注意:动态代理,只是我们在生成代理类上做了优化,代理模式的原理并没有改变!

我们常用的动态代理,一个是jdk自带的,另一个是cglib动态代理。jdk动态代理有一个强制性要求,就是被代理的类必须实现了某一个接口,或者本身就是接口。而cglib则不需要这样的要求,为JDK的动态代理提供了很好的补充。

JDK动态代理

讲到JDK动态代理,有两个类必须要了解:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler

Proxy是生成代理类的工具,可以获得代理类以及代理类的对象(注:此类暂时不做源码分析,只让读者了解怎么使用)。

  • 静态方法:newProxyInstance用于获得代理对象

    /**
     *	参数说明
     *	loader:定义代理类的类加载器,需要和被代理的类使用同一个类加载器
     *	interfaces:接口代理类要实现的接口列表
     *	h:调用处理的类,该类实现InvocationHandler接口
     *
     *	return:返回代理类对象
     */
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException{...}
    
  • 静态方法:getProxyClass用于获得代理类

    /**
     *	参数说明
     *	loader:定义代理类的类加载器,需要和被代理的类使用同一个类加载器
     *	interfaces:接口代理类要实现的接口列表
     *
     *	return:返回代理类
     */
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException{...}
    

InvocationHandler是调用处理,可以理解为方法拦截器,是一个接口类

  • 用途:处理代理实例上的方法调用并返回结果。 在与其关联的代理实例上调用方法时,将在调用处理程序上调用该方法。

  • InvocationHandler源码:

    public interface InvocationHandler {
        /**
         *  说明:通过Proxy#newProxyInstance获得的代理实例,在调用方法时,会调用InvocationHandler#invoke方法,
         *       在invoke方法中开发者可以调用被代理对象的方法,同时可以自定义一些操作,
         *       甚至可以不调用被代理对象的方法,完全自定义此方法。
         *
         *	参数说明
         *	proxy:调用方法的代理实例
         *	method:Method的实例,与在代理实例上调用的接口方法相对应
         *	args:方法参数,此参数是使用代理方法时传入的参数
         *
         *	return:Method的执行结果或用户自定义结果
         */
    	public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable;
    }
    

先看一个具体的例子,了解下如何使用:

/// 代理模式,需要定义一个接口
public interface ISubject(){
    void doSomething();
}
/// 真实实现类
public class RealSubject implements ISubject{
    @Override
    public void doSomething() {
        System.out.println("doSomething");
    }
}
/// 用于获得代理类
public class SubjectProxy implements InvocationHandler {
    // 被代理实例
    private Object target;

    public SubjectProxy(Object target){
        this.target = target;
    }
	// 获取被代理接口实例对象,所需参数上文已经解释过
    public <T> T getProxy(){
        return  (T) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this
        );
    }
	// 类似静态代理中对方法的处理,此处只是使用反射的方式实现
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在被代理实例的方法前增加操作
        System.out.println("---before---");
        // 因为target的类和代理类都实现了同一个接口,因此该method可以在target上使用
        Object result = method.invoke(target,args);
        // 在被代理实例的方法后增加操作
        System.out.println("---after---");
        // 返回被代理实例的调用结果
        return result;
    }
}
/// 测试类
public class App {
    public static void main(String[] args) {
        RealSubject subject = new RealSubject();
        ISubject proxy = new SubjectProxy(subject).getProxy();
        proxy.doSomething();
    }
}
/// 控制台输出
/***
 ---before---
 doSomething
 ---after---
 ---- 结果与预期相同,说明代理成功
***/

梳理一下JDK动态代理:

Proxy用于生成代理实例,需要三项:被代理类的类加载器、被代理类的接口数组、实现了InvocationHandler接口的实例;该代理类是由Proxy通过拼装字节码动态生成的类,同时代理类也实现了和被代理类同样的接口。代理类访问控制被代理类的方法是在InvocationHandler#invoke中实现的,所以代理类要实现的操作是在InvocationHandler#invoke方法中进行的。

使用案例

案例:使用过mybatis的猿友们一定知道,在mybatis中我们定义一个mapper接口和一个对应的mapper.xml文件(或者在接口方法上使用注解)就可以获取mapper实例执行相应方法,不知道猿友们有没有过这样的疑问:我们并没有像service层那样,每个service接口都有一个实现类,开发中一般不会给mapper接口写实现类的,那mapper接口怎么产生的实例,并能执行方法的?答案就是,mybatis使用了JDK动态代理来创建实现了mapper接口的动态类,并获得了相应的实例。

在讲解InvocationHandler源码时,大家有没有发现,invoke()方法中可以不调用被代理实例的方法,只要符合返回结果的形式,完全可以自行编写方法来替换掉被代理类的方法,也就是说JDK动态代理可以脱离被代理实例工作。先看一个例子:

/// 定义一个接口
public interface UserMapper {
    void update();
}
/// main方法
public class App {
    public static void main(String[] args) {
        /* 获取代理实例!三个参数:
           1、接口的类加载器
           2、创建接口集合
           3、匿名内部类实现InvocationHandler
        */
        UserMapper userMapper = (UserMapper)Proxy.newProxyInstance(
                UserMapper.class.getClassLoader(),
                new Class[]{UserMapper.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 在这里我们输出一句话,注意这里并没有操作method
                        System.out.println("调用UserMapper#update方法");
                        return null;
                    }
                });
        userMapper.update();
    }
}
/***控制台输出
	调用UserMapper#update方法
***/

结论:JDK动态代理,不仅能用来做代理模式,还可以动态生成实现指定接口的类

知道这个原理,mybatis的mapper接口产生实例的方式就清楚了。我们来看mybatis中的两个类

  • MapperProxy

    /// 类,该类继承了InvocationHandler接口
    public class MapperProxy<T> implements InvocationHandler, Serializable {
    
        private static final long serialVersionUID = -6424540398559729838L;
        private final SqlSession sqlSession;
        private final Class<T> mapperInterface;
        private final Map<Method, MapperMethod> methodCache;
    
        // 构造方法
        public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
            this.sqlSession = sqlSession;
            this.mapperInterface = mapperInterface;
            this.methodCache = methodCache;
        }
    	// 这是重点,mybatis如何处理接口中方法的
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                // 继承自Object的方法,不做任何处理,直接调用
                if (Object.class.equals(method.getDeclaringClass())) {
                    return method.invoke(this, args);
                // 接口中default修饰的默认方法的处理
                } else if (isDefaultMethod(method)) {
                    return invokeDefaultMethod(proxy, method, args);
                }
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
            // 这里才是接口中方法的处理
            final MapperMethod mapperMethod = cachedMapperMethod(method);
            // 最终调用交由mapperMethod的实现
            return mapperMethod.execute(sqlSession, args);
        }
    
        private MapperMethod cachedMapperMethod(Method method) {
            MapperMethod mapperMethod = methodCache.get(method);
            if (mapperMethod == null) {
                mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
                methodCache.put(method, mapperMethod);
            }
            return mapperMethod;
        }
    
        @UsesJava7
        private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
            throws Throwable {
            final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
                .getDeclaredConstructor(Class.class, int.class);
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }
            final Class<?> declaringClass = method.getDeclaringClass();
            return constructor
                .newInstance(declaringClass,
                             MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
                             | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
                .unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
        }
    
        /**
       * Backport of java.lang.reflect.Method#isDefault()
       */
        private boolean isDefaultMethod(Method method) {
            return (method.getModifiers()
                    & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC
                && method.getDeclaringClass().isInterface();
        }
    }
    
  • MapperProxyFactory

    /// 类,mapper代理工厂类,用于生成mapper接口的代理实例
    public class MapperProxyFactory<T> {
    
        // mapper接口
        private final Class<T> mapperInterface;
        // 缓存,使用ConcurrentHashMap,保证线程安全
        private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();
    
        public MapperProxyFactory(Class<T> mapperInterface) {
            this.mapperInterface = mapperInterface;
        }
    
        public Class<T> getMapperInterface() {
            return mapperInterface;
        }
    
        public Map<Method, MapperMethod> getMethodCache() {
            return methodCache;
        }
    
        @SuppressWarnings("unchecked")
        protected T newInstance(MapperProxy<T> mapperProxy) {
            // JDK动态代理,使用Proxy#newProxyInstance获去mapper接口的实例
            return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
        }
    
        // 获得实现了InvocationHandler接口的MapperProxy实例
        public T newInstance(SqlSession sqlSession) {
            final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
            return newInstance(mapperProxy);
        }
    }
    

关于MapperMethod是如何工作的,作者会在讲解mybatis时详细说明,此处不展开讨论,我们只需要知道mybatis中mapper接口的工作原理即可。

总结

代理模式属于结构型模式,虽然在我们平时开发中使用很多,但大都是spring的AOP封装好的框架,使用细节几乎对我们是透明的。作者在这篇文章中通过几个简单的例子,让大家理解代理模式,同时也希望大家能够深入框架底层,了解框架原理,验证自己的知识,汲取框架优秀的编程思想,做到知其然知其所以然,才能更好的使用框架解决开发中的问题。

你可能感兴趣的:(设计模式)