意图
为其他对象提供一种代理以控制对这个对象的访问
代理模式的诞生
【产品】:Kerwin,我记得你是在通州租房住吧?
【开发】:是啊,怎么了?
【产品】:你是房东直租还是中介啊?我最近真是特别烦中介,收费都好黑!
【开发】:我啊,我租的房子名义上倒是房东直租,但估计还是中介,你知道吗,中介的扩张是一个必然。
【产品】:扩张?你指的是全北京的房子都是中介的意思吗?
【开发】:现在肯定不至于全部都是,但也是大部分了,为什么会这样呢,因为中介需要控制租户,控制租金市场,如果租户直租房东,房东钱多人好,就很有可能很便宜,这就会打乱市场价格,所以拿下所有房老板,不仅为了赚钱,也是为了控制这种市场关系。
【产品】:我看你们程序员平常“傻傻的”,怎么对这个这么了解?莫非有计算机相关的故事?
【开发】:被你说中了,这个就是代理模式!它的诞生就是为了控制对象的访问,不过我们一般是用来增强其功能,不像XXX?
HeadFirst 核心代码
「定义正常业务类接口」
代码语言:javascript复制public interface PhoneInterface {
/***
* 更新电话号码
* @param phoneNum 电话号码
* @throws Exception 可能抛出Exception 异常
*/
void updatePhone(Long phoneNum);
}
「实现正常业务类」
代码语言:javascript复制public class PhoneServiceImpl implements PhoneInterface{
@Override
public void updatePhone(Long phoneNum) {
System.out.println("update phoneNum is: -> " phoneNum);
}
}
「静态代理业务类」
代码语言:javascript复制public class PhoneServiceProxy implements PhoneInterface {
/** 代理模式一般自行New对象, 反观装饰器模式则是传入对象 **/
private PhoneInterface phoneInterface;
public PhoneServiceProxy() {
this.phoneInterface = new PhoneServiceImpl();
}
@Override
public void updatePhone(Long phoneNum) {
before(phoneNum);
phoneInterface.updatePhone(phoneNum);
after();
}
private void before(Long phoneNum) {
System.out.println(MessageFormat.format("log start time:{0} , phoneNum is: {1}", new Date(), phoneNum));
if (null == phoneNum || String.valueOf(phoneNum).length() != 11) {
throw new RuntimeException("Update phoneNum fail, phoneNum is wrong.");
}
}
private void after() {
System.out.println(MessageFormat.format("log end time:{0}", new Date()));
}
}
静态代理模式的设计思路:
- Proxy 代理类
- RealSubject 定义被代理的实体
- Subject 定义RealSubject和Proxy共用接口
简单来说,
- 需要一个普通接口及其普通实现类
- 代理类同时实现该接口,自行new出对应实现类对象,对接口方法的前后增加额外操作
❝如果看着有点模棱两可,建议看完本文后,访问专题设计模式开源项目,里面有具体的代码示例,链接在最下面 ❞
装饰器模式和代理模式的区别
- 持有对象方式:代理模式一般是New,装饰器模式则是传入同一接口对象
- 意图:装饰器模式意在增强方法功能,代理模式意在控制对象的访问(例如代码中增加校验)
动态代理
刚才 HeadFirst核心代码 章节展示的是其静态代码的书写方式,如果所有的类都基于这样实现,那势必发生类膨胀的无解问题,因此真正常用的还是动态代理,分为两种 CGLIB | JDK动态代理
JDK 动态代理之MyBatis
「注意事项:」
- JDK动态代理的本质是创造一个实现了同一个接口的Proxy代理类,去进行真正的调用
- JDK动态代理在实现中的本质是反射技术
- 由于所有的代理类都实现了Proxy.class -> 包括帮我们创造的代理类也是,因此由于JAVA单继承的特点,只能想要实现代理必须实现某一个接口
「JDK 动态代理必不可少的三要素:InvocationHandler,newProxyInstance,invoke」
代码语言:javascript复制public class MybatisInvocation implements InvocationHandler {
/**
* 代理指定的接口
* @param tClass 接口class
* @param <T> 接口类型
*/
@SuppressWarnings("unchecked")
public static <T> T newProxyInstance(Class<T> tClass) {
return (T) Proxy.newProxyInstance(tClass.getClassLoader(), new Class[]{tClass}, new MybatisInvocation());
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.isAnnotationPresent(Select.class)) {
Select select = method.getAnnotation(Select.class);
System.out.println(MessageFormat.format("Method Name: {0} , Annotation Value is: {1}", method.getName(), select.value()));
}
// 获取到SQL及参数, 即可通过JDBC进行数据库操作查询数据, MyBatis不再神秘
return Arrays.asList("I", " am", " Kerwin~");
}
}
「被代理的接口」
代码语言:javascript复制public interface MyBatis {
@Select("select * from demo")
List<String> select();
}
「测试调用」
代码语言:javascript复制public class App {
public static void main(String[] args) {
// JDK动态代理:模拟 MyBatis 核心代理阶段
MyBatis batis = MybatisInvocation.newProxyInstance(MyBatis.class);
System.out.println("Result:" batis.select());
}
}
「输出结果」
代码语言:javascript复制Method Name: select , Annotation Value is: select * from demo
Result:[I, am, Kerwin~]
MyBatis中的JDK 动态代理:我们在使用MyaBtis的时候,肯定想过,它凭什么一个接口就可以输出结果,利用JDK 动态代理,可以非常方便的构建接口的代理,我们便可以在 Invoke
方法中大做文章,解析方法注解的值,解析其方法返回值,然后利用JDBC即可实现数据库查询实现一个简单ORM框架,推荐大家自行尝试一下
CGLIB 动态代理之AOP
「基础使用」
代码语言:javascript复制public class PhoneCglibProxy implements MethodInterceptor {
Object target;
public PhoneCglibProxy(Object o) {
this.target = o;
}
public Object newProxyInstance(){
Enhancer en = new Enhancer();
// 设置要代理的目标类
en.setSuperclass(target.getClass());
// 设置要代理的拦截器
en.setCallback(this);
// 生成代理类的实例
return en.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println(MessageFormat.format("Method Name is: {0} Params is: {1}", method.getName(), Arrays.toString(objects)));
return methodProxy.invokeSuper(o, objects);
}
}
「测试调用」
代码语言:javascript复制public class App {
/***
* CGLIB动态代理
* 如果类是final的,则无法生成代理对象,报错
* 如果方法是final的,代理无效
*
* 关键代码:
* 1.PhoneCglibProxy 实现 MethodInterceptor 方法拦截器接口 同时实现其 newProxyInstance方法 -> 该方法内容比较固定
* 2.通过代理工厂构建, 创建对象, 使用即可
*
* Spring 3.2之后默认包含了cglib依赖
* 普通项目 CGLIB依赖如下:
*
* <dependency>
* <groupId>cglib</groupId>
* <artifactId>cglib-nodep</artifactId>
* <version>2.2.2</version>
* </dependency>
*
* 推荐代码阅读顺序:
*
* @see PhoneServiceImpl
* @see PhoneCglibProxy
*/
public static void main(String[] args){
PhoneServiceImpl phone = new PhoneServiceImpl();
PhoneCglibProxy proxyFactory = new PhoneCglibProxy(phone);
PhoneServiceImpl service = (PhoneServiceImpl) proxyFactory.newProxyInstance();
service.updatePhone(15186564812L);
}
}
CGLIB 动态代理:Spring 3.2之后默认包含了cglib依赖,在使用中也要注意 final 关键字会使CGLIB代理失效,另外Spring AOP 默认采用JDK 动态代理,同时配合CGLIB代理一起实现的。
两种动态代理总结
- JDK 动态代理只能针对实现了接口的类的接口方法进行代理
- CgLib 动态代理基于继承来实现代理,所以无法对final类、private方法和static方法实现代理
「Spring AOP」:
- 如果目标对象实现了接口,则默认采用JDK 动态代理
- 如果目标对象没有实现接口,则采用CgLib 动态代理
- 如果目标对象实现了接口,且强制CgLib 代理,则采用CgLib进行动态代理
❝关于两种动态原理的实现原理可以查查其他的文章~ ❞
遵循的设计原则
- 封装变化:在父级接口中提供 default 方法,子类实现其对应的状态方法即可
- 多用组合,少用继承:代理模式经常和策略模式做对比,它们都是利用组合而非继承增强其变化和能力
什么场景适合使用代理模式
当我们需要为额外控制对象方法的执行时,比如历史项目的接口都没有记录日志,在Spring环境下,我们可以对所有的Bean方法增加日志功能,又或是多数据源时,通过注解标明对应的数据源,解耦代码等等
最后
「附上GOF一书中对于代理模式的UML图:」
相关代码链接
GitHub地址
- 兼顾了《HeadFirst》以及《GOF》两本经典书籍中的案例
- 提供了友好的阅读指导