您现在的位置是:首页 >技术杂谈 >设计模式之【代理模式】,有事找我“经纪人”网站首页技术杂谈
设计模式之【代理模式】,有事找我“经纪人”
文章目录
一、什么是代理模式
代理模式(Proxy Pattern)是指为其他对象提供一种代理,以控制对这个对象的访问,属于结构型模式。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
1、代理模式三大角色
代理模式一般包含三种角色:
- 抽象主题角色(Subject):抽象主题类的主要职责是声明真实主题与代理的共同接口方法,该类可以是接口也可以是抽象类;
- 真实主题角色(RealSubject):该类也被称为被代理类,该类定义了代理所表示的真实对象,是负责执行系统真正的逻辑业务对象;
- 代理主题角色(Proxy):也被称为代理类,其内部持有RealSubject的引用,因此具备完全的对RealSubject的代理权。客户端调用代理对象的方法,同时也调用被代理对象的方法,但是会在代理对象前后增加一些处理代码。
在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。代理模式属于结构型模式。
2、代理、桥接、装饰器、适配器 4 种设计模式的区别
代理、桥接、装饰器、适配器,这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似。笼统来说,它们都可以称为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
尽管代码结构相似,但这 4 种设计模式的用意完全不同,也就是说要解决的问题、应用场景不同,这也是它们的主要区别。这里我就简单说一下它们之间的区别。
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
3、代理模式使用场景
(1)远程(Remote)代理
本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
(2)防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
(3)保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
(4)缓存代理
当请求图片文件等资源时,先从缓存代理取,如果取不到资源再到公网或者数据库取。
(5)同步代理
主要使用在多线程编程中,完成多线程间同步工作。
4、代理模式优缺点
优点:
- 代理模式能将代理对象与真实被调用目标对象分离。
- 在一定程度上降低了系统的耦合性,扩展性好。
- 可以起到保护目标对象的作用。
- 可以增强目标对象的功能。
缺点:
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象中增加一个代理对象,会导致请求处理速度变慢。
- 增加了系统的复杂度。
二、静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
1、静态代理的一般写法
// 抽象接口
public interface ISubject {
void request();
}
// 真实对象
public class RealSubject implements ISubject {
public void request() {
System.out.println("real service is called.");
}
}
// 代理类
public class Proxy implements ISubject {
private ISubject subject;
public Proxy(ISubject subject){
this.subject = subject;
}
public void request() {
before();
subject.request();
after();
}
public void before(){
System.out.println("called before request().");
}
public void after(){
System.out.println("called after request().");
}
}
public static void main(String[] args) {
Proxy proxy = new Proxy(new RealSubject());
proxy.request();
}
2、火车站售票案例
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。
//卖票接口
public interface SellTickets {
void sell();
}
//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}
//代售点
public class ProxyPoint implements SellTickets {
private TrainStation station;
public ProxyPoint(TrainStation station) {
this.station = station;
}
public void sell() {
System.out.println("代理点收取一些服务费用");
station.sell();
}
}
//测试类
public class Client {
public static void main(String[] args) {
ProxyPoint pp = new ProxyPoint(new TrainStation());
pp.sell();
}
}
3、静态代理优缺点
优点:在不修改目标对象的功能前提下,能通过代理对象对目标功能扩展。
缺点:代理对象需要与目标对象实现相同的接口,所以会有很多代理类,一个目标对象需要对应一个代理类。一旦接口增加方法,目标对象与被代理对象都需要维护。
三、动态代理
1、静态代理和动态代理的本质区别
(1)静态代理只能通过手动完成代理操作,如果被代理类增加了新的方法,代理类需要同步增加,违背开闭原则。
(2)动态代理采用在运行时动态生成代码的方法,取消了对被代理类的扩展机制,遵循开闭原则。
(3)若动态代理要对目标类的增强逻辑进行扩展,结合策略模式,只要新增策略类便可完成,无需修改代理类的代码。
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。
2、JDK动态代理
(1)火车票售票案例
//卖票接口
public interface SellTickets {
void sell();
}
//火车站 火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}
//代理工厂,用来创建代理对象
public class ProxyFactory {
private TrainStation station;
public ProxyPoint(TrainStation station) {
this.station = station;
}
public SellTickets getProxyObject() {
//使用Proxy获取代理对象
/*
newProxyInstance()方法参数说明:
ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
InvocationHandler h : 代理对象的调用处理程序
*/
SellTickets sellTickets = (SellTickets)
Proxy.newProxyInstance(station.getClass().getClassLoader(),
station.getClass().getInterfaces(),
new InvocationHandler() {
/*
InvocationHandler中invoke方法参数说明:
proxy : 代理对象
method : 对应于在代理对象上调用的接口方法的 Method 实
例
args : 代理对象调用接口方法时传递的实际参数
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
//执行真实对象
Object result = method.invoke(station, args);
return result;
}
});
return sellTickets;
}
}
//测试类
public class Client {
public static void main(String[] args) {
//获取代理对象
ProxyFactory factory = new ProxyFactory(new TrainStation());
SellTickets proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
(2)使用JDK动态代理手写简易MyBatis
import org.apache.ibatis.annotations.Select;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
/**
* 手写mybatis简单示例
*/
public class MybatisTest1 {
public static void main(String[] args) {
// JDK动态代理
UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(MybatisTest1.class.getClassLoader(), new Class[]{UserMapper.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 获取接口方法上的@Select注解
Select annotation = method.getAnnotation(Select.class);
// 构造方法的参数名称与值的map
Map<String, Object> methodArgsNameMap = buildMethodArgsNameMap(method, args);
if(annotation != null){
// 获取接口上的sql,我们这只处理一个
String sql = parseSQL(annotation.value()[0], methodArgsNameMap);
// TODO - 执行sql、获取返回值等等
System.out.println(sql);
System.out.println(method.getReturnType()); // 获取返回值
System.out.println(method.getGenericReturnType()); // 获取泛型
}
return null;
}
});
userMapper.selectUser(1, "zhangsan");
}
/**
* 构造方法的参数名称与值的map
*/
public static Map<String, Object> buildMethodArgsNameMap(Method method, Object[] args){
Map<String, Object> nameArgMap = new HashMap<>();
Parameter[] parameters = method.getParameters();
int index[] = {0}; // 取巧
Stream.of(parameters).forEach(parameter -> {
String name = parameter.getName();
nameArgMap.put(name, args[index[0]]);
index[0] ++;
});
return nameArgMap;
}
/**
* 解析sql
*/
public static String parseSQL(String sql, Map<String, Object> nameArgsMap) {
StringBuilder parseSQL = new StringBuilder();
for (int i = 0; i < sql.length(); i++) {
char c = sql.charAt(i);
if(c == '#'){
int nextIndex = i + 1;
char nextChar = sql.charAt(nextIndex);
if(nextChar != '{'){
throw new RuntimeException(String.format("sql异常,缺少左括号
sql:%s", sql));
}
// 获取参数
StringBuilder argSb = new StringBuilder();
i = parseSqlArg(argSb, sql, nextIndex);
String argName = argSb.toString();
Object argValue = nameArgsMap.get(argName);
if(argValue == null){
throw new RuntimeException(String.format("参数异常
sql:%s", sql));
}
parseSQL.append(argValue);
continue;
}
parseSQL.append(c);
}
return parseSQL.toString();
}
private static int parseSqlArg(StringBuilder argSb, String sql, int nextChar) {
nextChar ++; // nextChar指向 {
for (; nextChar < sql.length(); nextChar++) {
char c = sql.charAt(nextChar);
if(c != '}'){
argSb.append(c);
continue;
}
if(c == '}'){
return nextChar;
}
}
throw new RuntimeException(String.format("sql异常,缺少右括号
sql:%s", sql));
}
}
interface UserMapper {
@Select("select * from user where id = #{id} and name = #{name}")
List<User> selectUser(Integer id, String name);
}
(3)深入理解
深入理解JDK动态代理原理,使用javassist动手写一个动态代理框架
3、CGLIB动态代理
(1)火车票售票案例
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
//火车站
public class TrainStation {
public void sell() {
System.out.println("火车站卖票");
}
}
//代理工厂
public class ProxyFactory implements MethodInterceptor {
private TrainStation station;
public ProxyPoint(TrainStation station) {
this.station = station;
}
public TrainStation getProxyObject() {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer =new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(target.getClass());
//设置回调函数
enhancer.setCallback(this);
//创建代理对象
TrainStation obj = (TrainStation) enhancer.create();
return obj;
}
/*
intercept方法参数说明:
o : 代理对象
method : 真实对象中的方法的Method实例
args : 实际参数
methodProxy :代理对象中的方法的method实例
*/
public TrainStation intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("代理点收取一些服务费用(CGLIB动态代理方式)");
TrainStation result = (TrainStation) methodProxy.invokeSuper(o, args);
return result;
}
}
//测试类
public class Client {
public static void main(String[] args) {
//创建代理工厂对象
ProxyFactory factory = new ProxyFactory(new TrainStation());
//获取代理对象
TrainStation proxyObject = factory.getProxyObject();
proxyObject.sell();
}
}
(2)深入理解
CGLIB代理到底是个什么东西?这是一篇最全的CGLIB大全
CGLIB 动态代理实现(Spring中)
4、CGLIB和JDK动态代理对比
(1)JDK动态代理实现了被代理对象的接口,CGLIB代理继承了被代理对象。
(2)JDK动态代理和CGLIB代理都在运行期生成字节码,JDK动态代理直接写Class字节码,CGLIB代理使用ASM框架写Class字节码,CGLIB代理实现更复杂,生成代理类的速度比JDK动态代理效率低。
(3)JDK动态代理调用代理方法是通过反射机制调用的,CGLIB代理是通过FastClass机制直接调用方法的,CGLIB代理的执行效率更高。
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理
。
四、源码中的代理
1、SpringAOP
SpringAOP从入门到源码分析大全,学好AOP这一篇就够了(一)