文章目录
代理模式应用场景
代理模式就是一层代理,日常生活中的中介、经纪人,代码中的事务代理、非侵入式日志等都是代理。
代理模式就是代理对象持有被代理对象的引用,在代理类中访问代理对象的时候,在方法前后做一些处理逻辑。
比如经纪人持有明星的访问权,记者要采访明星,经纪人会对记者的访问做一些筛选、档期判断等操作,之后符合条件的话才会进入到访问明星的阶段。
代理模式属于结构型模式。
代理模式与之后的装饰模式结构有相似的地方,处理逻辑的重心不同,不必太拘泥于两者的区别
代理模式的分类
代理模式总体上分为两种,静态代理和动态代理,可以理解为静态代理是一对一的代理,动态代理是利用反射等技术实现的一对多的通用代理,一个是手写的代理类,一个是动态生成的代理类
静态代理
静态代理理解起来比较简单,用两个例子来说明下
明星-经纪人之静态代理
这是个比较通俗的例子
package com.luhui.pattern.proxy;
/**
* 通用接口,以备后患
*/
public interface Person {
}
package com.luhui.pattern.proxy.staticproxy;
import com.luhui.pattern.proxy.Person;
/**
* 明星1号
*/
public class Star1 implements Person{
public void interviewed(){
System.out.println("明星1号接受记者采访");
}
}
package com.luhui.pattern.proxy.staticproxy;
import com.luhui.pattern.proxy.Person;
/**
* 经纪人类
*/
public class Agent implements Person {
private Star1 star1;
public Agent(Star1 star1){
this.star1 = star1;
}
public void interviewed(){
before();
this.star1.interviewed();
after();
}
private void before(){
System.out.println("查看明星档期");
}
private void after(){
System.out.println("与记者沟通采访结果");
}
}
package com.luhui.pattern.proxy.staticproxy;
/**
* 测试类
*/
public class AgentProxyTest {
public static void main(String[] args) {
Agent agent = new Agent(new Star1());
agent.interviewed();
}
这里的经纪人就是明星1号的一个代理类,因为这个代理类是我们手写的,所以是静态代理。
分库分表中的业务问题
这个比较接近生产中的场景了,比如我们面对与日俱增的订单量,需要对产生的数据根据年份进行分库分表,不同年份的订单放到不同的数据库中。这个时候就需要有个代理类来对订单进行判断,然后再路由到对应年份的数据据库中进行操作(与此类似的还有日常微服务开发中的开发者模式等)。
package com.luhui.pattern.proxy.dbroute;
import lombok.Getter;
import lombok.Setter;
/**
* 订单类
*/
@Getter
@Setter
public class Order {
/**
* 订单ID
*/
private String orderId;
/**
* 订单信息
*/
private Object orderInfo;
/**
* 创建时间(一般而言都是必备字段)
*/
private Long createTime;
}
package com.luhui.pattern.proxy.dbroute;
/**
* 模拟订单操作的持久层
*/
public class OrderDao {
public int insert(Order order){
System.out.println("OrderDao创建Order成功!");
return 1;
}
}
package com.luhui.pattern.proxy.dbroute;
public interface IOrderService {
int createOrder(Order order);
}
package com.luhui.pattern.proxy.dbroute;
public class OrderService implements IOrderService {
private OrderDao orderDao;
public OrderService(){
orderDao = new OrderDao();
}
public int createOrder(Order order) {
System.out.println("OrderService调用orderDao创建订单");
return orderDao.insert(order);
}
}
如上所述,一个实体类-持久层-业务接口-业务层的简单架子,这个时候再加上分库分表的代理逻辑
package com.luhui.pattern.proxy.dbroute.proxy;
import com.luhui.pattern.proxy.dbroute.IOrderService;
import com.luhui.pattern.proxy.dbroute.Order;
import com.luhui.pattern.proxy.dbroute.db.DynamicDataSourceEntity;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 分库分表的静态代理
*/
public class OrderServiceStaticProxy implements IOrderService {
private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
/**
* 代理类持有被代理类引用
*/
private IOrderService orderService;
public OrderServiceStaticProxy(IOrderService orderService) {
this.orderService = orderService;
}
/**
* 代理前置工作
*/
private void before(Order order){
Long time = order.getCreateTime();
Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据" );
// 模拟设置数据源
DynamicDataSourceEntity.set(dbRouter);
}
/**
* 代理后置工作
*/
private void after(){
// 模拟重置数据源
DynamicDataSourceEntity.restore();
}
public int createOrder(Order order) {
before(order);
this.orderService.createOrder(order);
after();
return 0;
}
}
以下是个利用ThreadLocal模拟数据源的实体类,如果只是单纯的了解代理模式,不想涉及的太多,可以忽略这个类。如果想再多了解的,可以看我之前的另一个博客
链接: 关于ThreadLocal的使用心得.
package com.luhui.pattern.proxy.dbroute.db;
/**
* 这是一个模拟数据源设置的实体类
*/
public class DynamicDataSourceEntity {
public final static String DEFAULE_SOURCE = null;
private final static ThreadLocal<String> local = new ThreadLocal<String>();
private DynamicDataSourceEntity(){}
public static String get(){return local.get();}
public static void restore(){
local.set(DEFAULE_SOURCE);
}
//DB_2019
//DB_2020
public static void set(String source){local.set(source);}
public static void set(int year){local.set("DB_" + year);}
}
如上所述就是一个贴近日常业务场景的一个代理模式的例子。
综合两个例子来看,静态代理就是我们手写一个类作为代理类,代理类持有被代理类的引用,在我们使用被代理类的时候,先访问代理类(访问的时候初始化一个被代理类实例),通过代理类的访问控制逻辑后(比如经纪人的档期判断,订单创建时间的判断),再来访问代理类的逻辑(访问明星,创建订单),之后可以根据实际情况选择是否增加对应的后置逻辑(比如流的关闭,ThreadLocal的remove等一些资源的释放)
动态代理
我们已经知道了两者的区别,那么话题紧接上面的静态代理例子。
明星-经纪人之静态代理缺点
众所周知一名强大的经纪人往往是服务于多个明星的,如果是面对这样的场景,我们要按照上面静态代理的思路来实现的话,就需要写很多个经纪人代理类了。
/**
* 经纪人类-服务于明星1号
*/
public class Agent implements Person {
private Star1 star1;
public Agent(Star1 star1){
this.star1 = star1;
}
public void interviewed(){
before();
this.star1.interviewed();
after();
}
private void before(){
System.out.println("查看明星1档期");
}
private void after(){
System.out.println("与记者沟通采访结果");
}
}
/**
* 经纪人类-服务于明星2号
*/
public class Agent implements Person {
private Star2 star2;
public Agent(Star2 star2){
this.star2 = star2;
}
public void interviewed(){
before();
this.star2.interviewed();
after();
}
private void before(){
System.out.println("查看明星1档期");
}
private void after(){
System.out.println("与记者沟通采访结果");
}
}
/**
* 经纪人类-服务于明星3号
*/
public class Agent implements Person {
private Star2 star2;
......
诸如此类,如果数量繁多的话,我们要写很多代理类,写很多重复代码,显然不符合我们使用设计模式的初衷。这时候就需要动态代理
明星-经纪人之JDK动态代理
package com.luhui.pattern.proxy.dynamicproxy.jdkproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 使用JDK动态代理实现的经纪人类(注意,这个不是真正的代理类)
*/
public class JDKAgent implements InvocationHandler {
private Object target;
/**
* 获取一个代理类
*
* @param target 为了实现动态,这里肯定不能传具体的代理类,所以是Object
* @author ngls.luhui 2020-12-22 15:03
* @return java.lang.Object
*/
public Object getInstance(Object target) throws Exception{
// 1.0 获得被代理类的实例对象并存储
this.target = target;
// 2.0 获得被代理类的字节码对象
Class<?> clazz = target.getClass();
// 3.0 根据被代理类的字节码对象生成并返回代理类
return Proxy.newProxyInstance(clazz.getClassLoader(),clazz.getInterfaces(),this);
}
/**
* 实现InvocationHandler接口所必须实现的方法,用来写代理的逻辑
*
* @author ngls.luhui 2020-12-22 15:06
* @return java.lang.Object
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object obj = method.invoke(this.target,args);
after();
return obj;
}
private void before(){
System.out.println("查看明星档期");
}
private void after(){
System.out.println("与记者沟通采访结果");
}
}
package com.luhui.pattern.proxy.dynamicproxy.jdkproxy;
import com.luhui.pattern.proxy.Person;
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.lang.reflect.Method;
public class JDKProxyTest {
public static void main(String[] args) {
try {
// 访问代理类被代理的方法
Object obj = new JDKAgent().getInstance(new Star1());
Method method = obj.getClass().getMethod("interviewed",null);
method.invoke(obj);
}catch (Exception e){
e.printStackTrace();
}
}
}
这个时候会发现getMethod这一步报错了,不急。
我们先debug看一下
可以发现,我们获得的代理类对象是一个很奇怪的$Proxy0类型,这个其实是一个动态生成的代理类。
我们可以把这个class从内存中输出到磁盘里看一看,加上如下代码
public class JDKProxyTest {
public static void main(String[] args) {
try {
Person obj = (Person)new JDKAgent().getInstance(new Star1());
// Method method = obj.getClass().getMethod("interviewed",null);
// method.invoke(obj);
//用于打印出 $Proxy0的class文件
byte [] bytes = ProxyGenerator.generateProxyClass("$Proxy0",new Class[]{Person.class});
FileOutputStream os = new FileOutputStream("E://$Proxy0.class");
os.write(bytes);
os.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
执行以后找到如下文件,进行反编译(反编译的手段多样,可使用jad,也可使用idea自带的反编译插件)
反编译打开后,发现生成的代理类中并没有interviewed方法,所以执行以下这行代码必然会出错
Method method = obj.getClass().getMethod("interviewed",null);
原因其实是,jdk动态代理是根据代理类和被代理类的统一接口而实现的,只有接口中有的方法,才会生成在代理类中。所以在Person接口中增加interviewed()方法后,执行成功。
在编写JDKAgent的时候其实也有体现
再次反编译动态生成的代理类,interviewed方法已经有了
所以通过这个bug,也能体会到jdk动态代理的最大特点了。
明星-经纪人之CGLib动态代理
上述例子中,为了解决jdk代理带来的bug,我们给People接口增加了interviewed方法。
但是,从逻辑上讲,是有点牵强的,这样的话所有继承People接口的类都要实现interviewed方法,但不是每个人都要有被采访的方法的。
所以,我们来看另一种代理方法-cglib的动态代理
package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGlibAgent implements MethodInterceptor {
public Object getInstance(Class<?> clazz) throws Exception{
//相当于Proxy,代理的工具类
Enhancer enhancer = new Enhancer();
//要把哪个设置为即将生成的新类父类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return enhancer.create();
}
/**
* 我们调用的所有的原始方法都会被替代为调用该方法
* 我们可以在该方法中使用反射的形式来调用原始方法(比较费时)
* 或者使用MethodProxy,使用MethodProxy性能更好
* @param o: 代理类对象
* @param method : 原始方法对象
* @param objects : 方法参数
* @param proxy : MethodProxy,该参数涉及到Cglib的FastClass机制
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object obj = methodProxy.invokeSuper(o,objects);
after();
return obj;
}
private void before(){
System.out.println("查看明星档期");
}
private void after(){
System.out.println("与记者沟通采访结果");
}
}
package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;
/**
* 明星1号,不需要继承接口了
*/
public class Star1{
public void interviewed(){
System.out.println("明星1号接受记者采访");
}
}
package com.luhui.pattern.proxy.dynamicproxy.cglibproxy;
import net.sf.cglib.core.DebuggingClassWriter;
public class CglibTest {
public static void main(String[] args) {
try {
// 用来打印生成的代理类信息
//System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"E://cglib_proxy_classes");
Star1 star1 = (Star1) new CGlibAgent().getInstance(Star1.class);
star1.interviewed();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下
可以看到,这次的Star1并没有实现任何接口,所以可以看出cglib的代理是不依赖接口的。而且从下面的setSuperclass可以猜测一下,cglib的实现原理是生成一个被代理类的子类来进行代理的。
接下来反编译一下生成的代理类,通过调试或者打印类信息我们可以得到,这个就是生成的代理类信息
首先第一个关键点,确实是通过继承来实现的,而且生成的代理类,继承了被代理类Star1的全部方法。
我们看下interviewed方法
可以看下,其实其他的equals和toString等方法也是一样的,所以我们在CGlibAgent中重写的intercept方法相当于给所有方法加了个截面
如果要仅对interviewed方法生效,可以通过method参数对before和after的调用进行控制。
关于cglib的原理,其实使用了fastClass的机制,上面三个class文件中
这里的FastCalss简单来说就是:为代理类和被代理类各生成一个 Class,这个 Class 会为代 理类或被代理类的方法分配一个 index(int 类型)。这个 index 当做一个入参,FastClass 就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比 JDK 动态代理通过反射调用高。
CGLib 和 JDK 动态代理对比
- JDK 动态代理是实现了被代理对象的接口,CGLib 是继承了被代理对象。
- JDK 和 CGLib 都是在运行期生成字节码,JDK 是直接写 Class 字节码,CGLib 使用 ASM 框架写 Class 字节码
- JDK Proxy生成代理的逻辑简单,执行效率相对要低,每次都要反射动态调用,CGLib 生成代理逻辑更复杂,调用效率更高,生成一个包含了所有的逻辑的FastClass,不再需要反射调用。
代理模式与Spring
- 当 Bean 有实现接口时,Spring 就会用 JDK 的动态代理
- 当 Bean 没有实现接口时,Spring 选择 CGLib
- Spring 可以通过配置强制使用 CGLib,只需在 Spring 的配置文件中加入如下代码
<aop:aspectj-autoproxy proxy-target-class="true"/>
静态代理和动态的本质区别
- 静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。
- 动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。
- 若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成, 无需修改代理类的代码。
代理模式的优缺点
优点
- 代理模式能将代理对象与真实被调用的目标对象分离。
- 一定程度上降低了系统的耦合度,扩展性好。
- 可以起到保护目标对象的作用。
- 可以对目标对象的功能增强。
缺点
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度。