关于代理,举个常见的例子——租房子!租房子对于每位漂泊在外的猿友来说都是一件深恶痛绝的事,需要考虑和衡量:上班便利与否,房租是否合适,房子周围设施是否健全,同时还要和各种中介打交道,这其中还有些黑中介从中作梗,一句话"太累心"。。。说道中介,其实就是房东的代理,他们帮房东出租房子,利用他们的平台发放广告,有些上档次的中介公司,还会帮忙装修,购置家具等,简直是一条龙服务,让房东只需提供房子,其他的什么也不用管,坐等收钱。对于租客来说,租客也不需要和房东直接打交道(其实很多人都想直接找房东租房,避免那笔中介费!),租客看房子、对房子家具装修的要求只需提给中介就行,对于租客和房东来说都比较省心。
虽说代理商的存在,会给租客和出租者增加一笔支出,但是代理商存在具有重要意义:首先,对于房东来说,其目的就是出租自己闲置房屋收取租费,将房子交给代理商打理,一方面可以通过代理商的广告渠道将房子尽快出租出去,另一方面,代理商可以对房屋进行装修置办等,更有利于设施参差不齐的房屋出租出去,作为出租者只需提供房源,无需操心这些事情,其实是一件省心省力的事。再是,代理商能够整合房源,具有一定规模的装修能力,可以处理各种房源,帮助房东把房子出租出去。最后,对于租客来说,能从中介那里获得大量的房源信息,也便于筛选和决策。(注:作者所指的代理商,是真正良心的代理商,并不是黑中介)
说了这么多,作者的目的是让大家理解代理的意义,以及使用代理带来的便利。本着"万物皆对象"的原则,我们先用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) {
//...省略
}
//...省略
}
至此我们通过一个"租房"的例子,让大家了解了代理,当然这里使用的代理模式是静态代理,关于动态代理下文会讲解。
先看下代理模式的定义:
为其他对象提供一种
代理
以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。(摘自百度百科)
关于代理模式的定义、适用场景、类图、优缺点等等概念和知识点不是这篇文章的核心(网上随便一搜讲解这些的博文很多,可自行搜索),作者要通过实际开发中的列子来讲解代理模式该怎么使用:
描述:某电信行业项目,代码比较老旧,使用的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动态代理,有两个类必须要了解: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封装好的框架,使用细节几乎对我们是透明的。作者在这篇文章中通过几个简单的例子,让大家理解代理模式,同时也希望大家能够深入框架底层,了解框架原理,验证自己的知识,汲取框架优秀的编程思想,做到知其然知其所以然,才能更好的使用框架解决开发中的问题。