代理模式可以说是我们在java学习中非常常见的一个设计模式了,在很多地方我们都可以看到代理模式的影子。
比如:
- Spring 的 Proxy 模式(AOP编程 )AOP的底层机制就是动态代理
- mybatis中执行sql时mybatis会为mapper接口通过jdk动态代理的方法生成接口的实现类
- Feign对于加了@FeignClient 注解的类会在Feign启动时,为其创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器
可以看出,代理模式就是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。为什么需要代理模式呢,两个方面:
- 客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用。代理类和委托类实现相同的接口。类似于我们使用mybatis,只需要写xml文件,mybaties会在解析xml的时候已经对每个xxMapper接口都包装了一个MapperProxyFactory,在获取对应mapper的时候,进行代理实例化。用动态代理的方式执行sql。
- 代理类除了是客户类和委托类的中介之外,我们还可以通过给代理类增加额外的功能来扩展委托类的功能。类似于AOP中我们使用切面对切点进行加工,比如加入一些前置拦截器,环绕通知之类的扩展功能。
代理模式一般分为三类:
1. 静态代理
2. 动态代理
3. CGLIB代理
=========================================================================
静态代理:
静态代理的被代理类和代理类同时实现相应的一套接口,通过代理类调用重写接口的方法,实际上调用的是原始对象的同样的方法。
我们模拟一个买显卡的场景。我们要买一个RTX3090,显卡在显卡商店出售,我们有两个选择,一个是去显卡商店买,一个是去显卡代理商那里买。
=========================================================================
我们首先定义一个显卡类:
package ProxyPattern.StaticProxy.Items;
import java.math.BigDecimal;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName GraphicCard.java
* @Description 显卡类
* @createTime 2022年03月02日 15:49:00
*/
public class GraphicCard {
public String name;
public BigDecimal price;
public GraphicCard(){
}
public GraphicCard(String name,BigDecimal price){
this.name= name;
this.price = price;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
定义一个商店接口,不论是厂家直售还是代购,都要实现卖显卡的这个功能:
package ProxyPattern.StaticProxy;
import ProxyPattern.StaticProxy.Items.GraphicCard;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName GraphicCardShop.java
* @Description 显卡商店接口
* @createTime 2022年03月02日 15:59:00
*/
public interface GraphicCardShop {
GraphicCard saleCards();
}
定义一个商店的实现类,实现商店接口,就是厂家直销:
package ProxyPattern.StaticProxy;
import ProxyPattern.StaticProxy.Items.GraphicCard;
import java.math.BigDecimal;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName GraphicCardsShopImpl.java
* @Description 显卡商店
* @createTime 2022年03月02日 16:01:00
*/
public class GraphicCardsShopImpl implements GraphicCardShop{
@Override
public GraphicCard saleCards() {
System.out.println("从商店购买");
GraphicCard RTX3090 = new GraphicCard("RTX3090",new BigDecimal(12999));
System.out.println("买到显卡了");
return RTX3090;
}
}
=========================================================================
然后我们开始考虑静态代理:
定义一个显卡代理商,代理商从厂家拿货,然后加价销售:
package ProxyPattern.StaticProxy;
import ProxyPattern.StaticProxy.Items.GraphicCard;
import java.math.BigDecimal;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName GraphicCardProxy.java
* @Description 显卡代理商
* @createTime 2022年03月02日 16:13:00
*/
public class GraphicCardProxy implements GraphicCardShop{
GraphicCardShop graphicCardShop = new GraphicCardsShopImpl();
@Override
public GraphicCard saleCards() {
System.out.println("从代理商购买");
//代理商去商店买卡
GraphicCard graphicCard = graphicCardShop.saleCards();
System.out.println("代理商进货价: "+graphicCard.getPrice());
//代理商加价
graphicCard.setPrice(graphicCard.getPrice().add(graphicCard.getPrice()));
System.out.println("代理商供货");
return graphicCard;
}
}
测试一下:
package ProxyPattern.StaticProxy;
import ProxyPattern.StaticProxy.Items.GraphicCard;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName StaticProxyTest.java
* @Description 代理商测试类
* @createTime 2022年03月02日 16:11:00
*/
public class StaticProxyTest {
public static void main(String[] args) {
GraphicCardShop shop = new GraphicCardsShopImpl();
GraphicCard graphicCard = shop.saleCards();
System.out.println("显卡类型"+graphicCard.getName());
System.out.println("显卡价格"+graphicCard.getPrice());
System.out.println("\n ==============================================\n");
GraphicCardProxy proxyShop = new GraphicCardProxy();
GraphicCard graphicCard1 = proxyShop.saleCards();
System.out.println("显卡类型: "+graphicCard1.getName());
System.out.println("显卡价格: "+graphicCard1.getPrice());
}
}
可以看到,我们使用代理类,对被代理的类进行了增强(加钱),被代理类和代理类同时实现相应的一套接口,所以是静态代理。
但是我们想一想,显卡需要显卡代理商,主板又需要主板代理商,也就是我每新增一个非顶层对象,都要为其增加一个代理对象,非常不方便,所以静态代理的优缺点就显而易见了:
优点: 可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点: 代理对象与目标对象要实现相同的接口,我们得为每一个服务都得创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
=========================================================================
动态代理:
- 代理对象,不需要实现接口
- 代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
动态代理的代理类不用再实现接口了。但是,要求被代理对象必须有接口。
=========================================================================
我们先来看看JDK动态代理:
JDK 动态代理是实现了被代理对象的接口,通过反射机制调用代理方法读取接口信息。
也就是使用Java.lang.reflect.Proxy类可以直接生成一个代理对象。
=========================================================================
依旧是代理显卡的实现:
我们首先编写动态处理器实现InvocationHandler:
package ProxyPattern.DynamicProxy.JDKProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName JDKProxyHandler.java
* @Description JDK代理动态处理器
* @createTime 2022年03月03日 16:07:00
*/
public class JDKProxyHandler implements InvocationHandler {
private Object object;
public JDKProxyHandler(final Object object){
this.object=object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(object,args);
return result;
}
}
我们可以看到,这个处理器使用了一个Object的大类,而不是归于固定的单类,增加了扩展性
每一个代理的实例都会有一个关联的调用处理程序(InvocationHandler)。程序对代理实例进行调用时,将对方法的调用进行编码并指派到它的调用处理器的invoke方法。所以对代理对象实例方法的调用都是通过InvocationHandler中的invoke方法来完成的,而invoke方法会根据传入的代理对象、方法名称以及参数决定调用代理的哪个方法。
然后我们定义一个代理商店从显卡商店买卡和加价:
package ProxyPattern.DynamicProxy.JDKProxy;
import ProxyPattern.Items.GraphicCardShop;
import ProxyPattern.Items.GraphicCardsShopImpl;
import ProxyPattern.Items.GraphicCard;
import java.lang.reflect.Proxy;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName JDKGraphicCardProxy.java
* @Description 代理商店
* @createTime 2022年03月03日 16:29:00
*/
public class JDKGraphicCardProxy {
GraphicCardShop graphicCardShop = new GraphicCardsShopImpl();
GraphicCardShop proxyShop = (GraphicCardShop) Proxy.newProxyInstance(
GraphicCardShop.class.getClassLoader(), new Class[]{GraphicCardShop.class}, new JDKProxyHandler(graphicCardShop)
);
public GraphicCard getCard(){
System.out.println("从代理商购买");
//代理商买卡
GraphicCard graphicCard = proxyShop.saleCards();
System.out.println("代理商进货价: "+graphicCard.getPrice());
//加价
graphicCard.setPrice(graphicCard.getPrice().add(graphicCard.getPrice()));
System.out.println("代理商供货");
return graphicCard;
}
}
我们使用Proxy.newProxyInstance方法,通过反射获取一个代理类:
newProxyInstance需要三个参数:
第一个是需要被代理的类的类加载器classloader,我们通过反射拿到。
第二个是代理对象的要实现的接口 ,我们通过反射直接拿到代理对象的接口。
第三个是(接口)执行处理类,也就是我们之前定义好的动态处理器,传入需要代理的对象。
之后我们定义方法实现买卖加价操作。
测试一下:
package ProxyPattern.DynamicProxy.JDKProxy;
import ProxyPattern.StaticProxy.GraphicCardProxy;
import ProxyPattern.Items.GraphicCardShop;
import ProxyPattern.Items.GraphicCardsShopImpl;
import ProxyPattern.Items.GraphicCard;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName JDKProxyTest.java
* @Description JDK动态代理测试类
* @createTime 2022年03月02日 16:11:00
*/
public class JDKProxyTest {
public static void main(String[] args) {
GraphicCardShop shop = new GraphicCardsShopImpl();
GraphicCard graphicCard1 = shop.saleCards();
System.out.println("显卡类型"+graphicCard1.getName());
System.out.println("显卡价格"+graphicCard1.getPrice());
System.out.println("\n ==============================================\n");
JDKGraphicCardProxy proxy = new JDKGraphicCardProxy();
GraphicCard graphicCard2 = proxy.getCard();
System.out.println("显卡类型: "+graphicCard2.getName());
System.out.println("显卡价格: "+graphicCard2.getPrice());
}
}
=========================================================================
CGLAB动态代理:
CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
CGLIB缺点:对于final方法,无法进行代理。
=========================================================================
我们使用CGLib进行动态代理,首先需要导入第三方的包,有两种方式:
1.在Spring中集成了CGLib的包,所以在我们可以在maven中直接导入Spring的包调用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>2.6.3</version>
</dependency>
2.直接导入CGLib的包:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
然后我们需要设置一个动态代理类,也就是拦截器:
package ProxyPattern.DynamicProxy.CGLIBProxy;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName CglibProxy.java
* @Description 生成动态代理类
* @createTime 2022年03月04日 09:45:00
*/
public class CglibProxy implements MethodInterceptor {
private Object target;
/**
* Enhancer类是CGLib中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展
*/
public Object getInstance(final Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 复写了一个intercept方法,调用invoke在应用程序的主线程上执行指定的委托
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(this.target,objects);
}
}
动态代理类继承了CGLib中的 MethodInterceptor 拦截器,通过复写intercept方法实现拦截和增强的方法。我们甚至可以直接在intercept中实现环绕通知,log输出。
然后实现一个代理商店类,模仿代理商店进货和加价的操作:
package ProxyPattern.DynamicProxy.CGLIBProxy;
import ProxyPattern.Items.GraphicCard;
import ProxyPattern.Items.GraphicCardShop;
import ProxyPattern.Items.GraphicCardsShopImpl;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName CglibGraphicCardProcy.java
* @Description 代理商店
* @createTime 2022年03月04日 10:15:00
*/
public class CglibGraphicCardProcy {
GraphicCardShop shop = new GraphicCardsShopImpl();
CglibProxy cglibProxy = new CglibProxy();
GraphicCardsShopImpl graphicCardsCglibProxy = (GraphicCardsShopImpl) cglibProxy.getInstance(shop);
public GraphicCard getCard(){
System.out.println("从代理商购买");
//代理商买卡
GraphicCard graphicCard = graphicCardsCglibProxy.saleCards();
System.out.println("代理商进货价: "+graphicCard.getPrice());
//加价
graphicCard.setPrice(graphicCard.getPrice().add(graphicCard.getPrice()));
System.out.println("代理商供货");
return graphicCard;
}
}
测试一下:
package ProxyPattern.DynamicProxy.CGLIBProxy;
import ProxyPattern.Items.GraphicCard;
import ProxyPattern.Items.GraphicCardShop;
import ProxyPattern.Items.GraphicCardsShopImpl;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName CglibProxyTest.java
* @Description CGlib动态代理测试
* @createTime 2022年03月04日 09:59:00
*/
public class CglibProxyTest {
public static void main(String[] args) {
GraphicCardShop shop = new GraphicCardsShopImpl();
GraphicCard graphicCard1 = shop.saleCards();
System.out.println("显卡类型"+graphicCard1.getName());
System.out.println("显卡价格"+graphicCard1.getPrice());
System.out.println("\n ==============================================\n");
//创建代理商店
CglibGraphicCardProcy cglibGraphicCardProcy = new CglibGraphicCardProcy();
//代理商拿卡
GraphicCard graphicCard2 = cglibGraphicCardProcy.getCard();
System.out.println("显卡类型: "+graphicCard2.getName());
System.out.println("显卡价格: "+graphicCard2.getPrice());
}
}
CGLIB创建的动态代理对象比JDK创建的动态代理对象的性能更高,但是CGLIB创建代理对象时所花费的时间却比JDK多得多。所以对于单例的对象,因为无需频繁创建对象,用CGLIB合适,反之使用JDK方式要更为合适一些。同时由于CGLib由于是采用动态创建子类的方法,对于final修饰的方法无法进行代理。
=========================================================================
一点小坑:
在练习CGLib的时候,根据网上的写法出现了两个问题:
1.CGLib代理方法不属于JDK中,需要自己导包引入,否则找不到 MethodInterceptor。
2.在动态代理类中出现了死循环:
原来的代码是这样的:
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//可以加入前置通知
return methodProxy.invoke(o,objects);
//可以加入后置通知
}
我们可以定位到是invoke的时候出现了问题。原因是当我们调用 反射 来实例化动态代理子类的时候,直接调用了动态代理类的加强版本买显卡
方法而不是被代理的买显卡的方法导致了死循环。
所以解决方法就是我们再调用incoke的时候,传的object不是这个object,而是实例化以后的this.target,也就是代理类实例避免死循环,问题解决。