一、定义
代理模式:为另一个对象提供替身或占位符以控制对这个对象的访问。我们也将代理描述成另一个对象的“代表”。
实例代理模式创建“代表对象”,让“代表对象”控制某对象的访问,被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。
代理模式的变体有很多,下面会提到主要的三种变体:远程代理、虚拟代理、动态代理。
- 远程代理控制访问远程对象。
- 虚拟代理控制访问创建开销大的资源。
- 动态代理基于权限控制对资源的访问。
下面我们来看看类图
- ISubject:它为RealSubject和Proxy提供了接口,通过实现同一接口,Proxy在RealSubject出现的地方取代它。
- RealSubject是真正做事的对象,它是被Proxy代理和控制访问的对象。
- Proxy持有RealSubject的引用。在某些例子中,Proxy还会负责RealSubject对象的创建和销毁。客户和RealSubject的交互都必须通过Proxy。因为Proxy和RealSubject实现了相同的接口(ISubject),所以任何用到RealSubject的地方,都可以用Proxy取代。Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的控制。
1、远程代理
1.1 定义
远程代理可以作为远程对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。
何为远程对象?这是一种对象,活在不同的JVM堆中(更一般的说法为:在不同的地址空间运行的远程对象)。
何为本地代表?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
但是要如何创建一个代理,知道如何调用在另一个JVM中的对象的方法?Java RMI(RemoteMethodInvocation)远程方法调用可以做到。
1.2 下面来看看RMI的工作过程要点
- 客户对象Client调用客户辅助对象ClientHelper的doBigThing()方法。
- 客户辅助对象打包调用信息(变量、方法名称等),然后通过网络将它运给服务辅助对象ServiceHelper。
- 服务辅助对象把来自客户辅助对象的信息解包,找出被调用的方法(以及在哪个对象内),然后调用真正的服务对象RealSubject上的真正方法。
- 服务对象上的方法被调用,将结果返回给服务辅助对象。
- 服务辅助对象把调用的返回信息打包,然后通过网络运回给客户辅助对象。
- 客户辅助对象把返回值解包,返回给客户对象。对于客户来说,这是完全透明的。
随着 RMI 技术的演进和现代分布式计算框架的普及,Java 平台对 RMI 的支持进行了精简。Java 21 正式移除了
rmic工具,这意味着开发者不能再使用它来生成 RMI 所需的客户端存根代码。
这一变化是 Java 21 中一系列 RMI 相关移除和弃用操作的一部分,旨在减少对过时、使用率低且存在潜在安全风险的组件的依赖。官方建议开发者迁移到更现代、更安全的分布式通信框架,如 gRPC 或 RESTful API。
2、虚拟代理
2.1 定义
虚拟代理作为创建开销大的对象的代表。虚拟代理经常知道我们真正需要一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
下面是一个例子:显示CD封面
从在线服务中获取封面的图片,但是限于连接带宽和网络负载,下载可能需要一些时间,所以在等待图像加载的时候,应该显示一些东西。我们也不希望在等待图像时整个应用程序被挂起。一旦图像加载完成,刚才显示的东西应该消失,图像显示出来。
想做到这样,简单的方式就是虚拟代理。虚拟代理可以代理Icon,管理背景的加载,并在加载未完成时显示“CD封面加载中,请稍候。。。”,一旦加载完成,代理就把显示的职责委托给Icon。
下面让我们看看代码
import javax.swing.*;
import java.awt.*;
import java.net.URL;
/**
* ImageProxy同ImageIcon一样,都实现了Icon接口
* ImageProxy就是一个虚拟代理
*/
public class ImageProxy implements Icon {
// 此ImageIcon是我们希望在加载后显示出来的真正图像
private volatile ImageIcon imageIcon;
// 图像Url
final URL imageUrl;
Thread retrievalThread;
boolean retrieving = false;
// 将图像的URL传入构造器中。这是我们希望显示的图像所在的位置
public ImageProxy(URL url) {
this.imageUrl = url;
}
// ImageIcon被两个不同的线程使用,因此,除了使变量volatile(保护读取)外,我们还使用synchronized(保护写入)。
synchronized void setImageIcon(ImageIcon imageIcon) {
this.imageIcon = imageIcon;
}
/**
* 在屏幕上画出一个Icon图像(通过委托给ImageIcon)
* 如果ImageIcon没有被完整创建出来,那就由ImageProxy来创建一个
* @param c a {@code Component} to get properties useful for painting
* @param g the graphics context
* @param x the X coordinate of the icon's top-left corner
* @param y the Y coordinate of the icon's top-left corner
*/
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
if (imageIcon != null) {
// 如果已经有ImageIcon,就告诉它画出自己
imageIcon.paintIcon(c, g, x, y);
} else {
// 没有ImageIcon,显示“加载中...”的消息
g.drawString("Loading CD Cover, please wait...", x + 300, y + 190);
// 如果还没有试着取出图像
if (!retrieving) {
// 那么就开始取出图像
retrieving = true;
/*
在这里加载真正的icon图像。
请注意,使用ImageIcon加载图像是同步的。也就是说,只有在图像加载完之后,ImageIcon构造器才会返回。
这样,我们的程序会耗在这里,动弹不得,也没办法显示消息。所以我们要使用异步的方式操作:使用线程。
*/
// 我们不希望挂起整个用户界面,所以用另一个线程取出图像
retrievalThread = new Thread(new Runnable() {
public void run() {
try {
// 在线程中,实例化此ImageIcon对象,其构造器会在图像加载完成后在返回。
setImageIcon(new ImageIcon(imageUrl, "CD Cover"));
// 当图像准备好时,我们告诉Swing,需要重绘。
c.repaint();
} catch (Exception e) {
e.printStackTrace();
}
}
});
retrievalThread.start();
}
}
}
/**
* 在图像加载完毕前,返回默认的宽。
* 图像加载完毕后,交给ImageIcon处理
* @return
*/
@Override
public int getIconWidth() {
if (imageIcon != null) {
return imageIcon.getIconWidth();
}
return 800;
}
/**
* 在图像加载完毕前,返回默认的高。
* 图像加载完毕后,交给ImageIcon处理
* @return
*/
@Override
public int getIconHeight() {
if (imageIcon != null) {
return imageIcon.getIconHeight();
}
return 800;
}
}
/**
* 这是测试类
*/
public class ImageProxyTest {
ImageComponent imageComponent;
JFrame jFrame = new JFrame("CD Cover viewer");
JMenuBar jMenuBar;
JMenu jMenu;
Hashtable<String, String> albums = new Hashtable<>();
public static void main(String[] args) throws Exception {
ImageProxyTest imageProxyTest = new ImageProxyTest();
}
public ImageProxyTest() throws Exception {
albums.put("Buddha","https://cdn.stocksnap.io/img-thumbs/960w/brick-building_CNR0WYMVJX.jpg");
albums.put("Ama","https://cdn.stocksnap.io/img-thumbs/960w/holding-glass_V0ZNGACCAS.jpg");
albums.put("Alex","https://cdn.stocksnap.io/img-thumbs/960w/building-architecture_FFCNJ3BDFC.jpg");
URL initialURL = new URL(albums.get("Buddha"));
jMenuBar = new JMenuBar();
jMenu = new JMenu("Favorite Albums");
jMenuBar.add(jMenu);
jFrame.setJMenuBar(jMenuBar);
for (Enumeration e = albums.keys(); e.hasMoreElements();) {
String name = (String) e.nextElement();
JMenuItem jMenuItem = new JMenuItem(name);
jMenu.add(jMenuItem);
jMenuItem.addActionListener(event -> {
imageComponent.setIcon(new ImageProxy(getAlbumUrl(event.getActionCommand())));
jFrame.repaint();
});
}
// set up frame and menus
Icon icon = new ImageProxy(initialURL);
imageComponent = new ImageComponent(icon);
jFrame.getContentPane().add(imageComponent);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setSize(800, 600);
jFrame.setVisible(true);
}
URL getAlbumUrl(String name) {
try {
return new URL(albums.get(name));
} catch (Exception e) {
return null;
}
}
}
import javax.swing.*;
import java.awt.*;
/**
* 图片组件
*/
public class ImageComponent extends JComponent {
private Icon icon;
public ImageComponent(Icon icon) {
this.icon = icon;
}
public void setIcon(Icon icon) {
this.icon = icon;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
int h = icon.getIconHeight();
int w = icon.getIconWidth();
int x = (800 - w) / 2;
int y = (600 - h) / 2;
icon.paintIcon(this, g, x, y);
}
}
- ImageProxy类代理ImageIcon类,所以它同ImageIcon一样,也实现了Icon接口。
- ImageProxy类有一个ImageIcon类的引用。
- 如果ImageIcon没有被完整创建出来,那就由ImageProxy来创建一个,屏幕上显示“请稍候”。如果ImageIcon创建出来了,就将绘制任务委托给ImageIcon的实例。
- 因为不希望挂起整个用户界面,所以使用异步(新开一个线程)的方式加载图片并实例化ImageIcon类。
3、动态代理
3.1 定义
Java在java.lang.reflect包中有自己的代理支持,利用这个包你可以在运行时动态的创建代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个Java技术为:动态代理。
下面我们来看看动态代理的类图
名词解释
InvocationHandler:调用处理器
因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。我们要把代码放在InvocationHandler中。InvocationHandler的工作是响应代理的任何调用。你可以把InvocationHandler想象成代理收到方法调用后,请求做实际工作的对象。
3.2 创建动态代理
1. 创建InvocationHandler
当代理的方法被调用时,不管是哪个方法,代理都会把这个调用转发给InvocationHandler,调用它的invoke()方法。
让我们看看这是如何工作的?
2. 创建动态代理
3. 利用适当的代理包装真实对象
下面来看看代码
public interface PersonBean {
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setHotOrNotRating(int hotOrNotRating);
}
public class PersonBeanImpl implements PersonBean {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
// 其他的getters、 setters
@Override
public int getHotOrNotRating() {
if (rating == 0) return 0;
return (rating / ratingCount);
}
@Override
public void setHotOrNotRating(int hotOrNotRating) {
this.rating += hotOrNotRating;
this.ratingCount++;
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* PersonBean的动态代理
* 允许拥有者查看自己的信息、设置自己的信息、但是不能给自己评分
*
* 所有调用处理器都实现了InvocationHandler接口
*/
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean personBean;
// 将personBean传入构造器,并保持它的引用
public OwnerInvocationHandler(PersonBean personBean) {
this.personBean = personBean;
}
/**
* 每次proxy的方法被调用,就会导致proxy调用此方法
* @param proxy 方法被调用的代理实例
*
* @param method 真正调用的方法
*
* @param args 方法参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (method.getName().startsWith("get")) {
return method.invoke(personBean, args);
} else if (method.getName().equals("setHotOrNotRating")) {
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(personBean, args);
}
} catch (InvocationTargetException e) {
// 如果真正主题抛出异常的话,就会执行这里
e.printStackTrace();
}
// 如果调用其他方法,一律不处理,返回null
return null;
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
/**
* 这是一个测试类
*/
public class MatchMarkingTest {
public static void main(String[] args) {
MatchMarkingTest matchMarkingTest = new MatchMarkingTest();
matchMarkingTest.drive();
}
private void drive() {
PersonBean joe = new PersonBeanImpl();
joe.setName("Joe JavaBean");
joe.setGender("man");
joe.setInterests("bowling, Go");
joe.setHotOrNotRating(20);
PersonBean ownerProxy = getOwnerProxy(joe);
// Proxy.isProxyClass 判断是不是一个代理类
System.out.println("is proxy?" + Proxy.isProxyClass(ownerProxy.getClass()));
System.out.println("is proxy?" + Proxy.isProxyClass(joe.getClass()));
System.out.println("name is " + ownerProxy.getName());
System.out.println("interests is " + ownerProxy.getInterests());
ownerProxy.setInterests("balls");
System.out.println("interests is " + ownerProxy.getInterests());
try {
ownerProxy.setHotOrNotRating(30);
} catch (Exception e) {
System.out.println("cannot set rating from owner proxy");
}
System.out.println("hotOrNotRating is " + ownerProxy.getHotOrNotRating());
}
/**
* 此方法需要一个Person对象作为参数,然后返回它的代理。
* 因为代理和主题有相同的接口,所以我们返回一个PersonBean。
* @param person
* @return
*/
PersonBean getOwnerProxy(PersonBean person) {
// 将person传入调用处理器的构造器中。这正是处理器能够访问RealSubject的原因
return getProxy(person, new OwnerInvocationHandler(person));
}
/**
* 此方法创建了代理
* @param person
* @param handler
* @return
*/
PersonBean getProxy(PersonBean person, InvocationHandler handler) {
// 利用proxy类的静态newProxyInstance方法创建代理
// 将PersonBean的类载入器当做参数
// person.getClass().getInterfaces() 是代理需要实现的接口
return (PersonBean) Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), handler);
}
}
4、其他的代理模式变体
- 防火墙代理:控制网络资源的访问,保护主题免于“坏客户”的侵害。
- 智能引用代理:当主题被引用时,进行额外的动作,例如计算一个对象被引用的次数。
- 缓存代理:为开销大的运算结果提供暂时存储:它也允许多个客户共享结果,以减少计算或网络延迟。
- 同步代理:在多线程的情况下为主题提供安全的访问。
- 复杂隐藏代理:用来隐藏一个类的复杂集合的复杂度,并进行访问控制。有时候也称为“外观代理”。
- 写入时复制代理:用来控制对象的复制,方法是延迟对象的复制,直到客户真的需要为止。这是虚拟代理的变体。
二、总结要点
- 代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
- 远程代理管理客户和远程对象之间的交互。
- 虚拟代理控制访问实例化开销大的对象。
- 保护代理基于调用者控制对对象方法的访问。
- 代理模式有许多变体,例如:缓存代理,同步代理、防火墙代理、写入时复制代理。
- 代理在结构上类似装饰者,但是目的不同。
- 装饰者模式为对象加上行为,而代理则是控制访问。
- Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。
- 就和其他包装者一样,代理会造成你的设计中类的数目增加。
三、应用场景
代理模式在SpringBoot中的应用非常广泛。是实现 面向切面编程(AOP) 的核心机制。它允许开发者在不修改业务原有代码的前提下,动态的为对象添加额外的功能,从而实现关注点的分离,提升代码的可维护性和复用性。
核心应用场景
代理模式在Spring Boot中主要用于处理那些与核心业务逻辑无关,但又横跨多个模块的“横切关注点”,主要包括:
- 事务管理:通过@Transactional注解,Spring会自动为方法创建代理,在方法执行前后自动管理数据库事务的开启、提交或回滚。
- 日志记录:可以在方法调用前后自动记录执行时间、参数、返回值等信息,便于系统监控和问题排查。
- 安全控制:在方法执行前检查用户权限,实现方法级别的访问控制。
- 性能监控:统计特定方法的执行耗时,用于性能分析。
- 缓存管理:通过@Cacheable等注解,代理层可以判断结果是否已缓存,避免重复计算。